UNPKG

infomaniak-ai-provider

Version:

> the project is under active development, for now only text and embedding models are supported.

386 lines (381 loc) 12.6 kB
// src/infomaniak-provider.ts import { OpenAICompatibleChatLanguageModel, OpenAICompatibleEmbeddingModel, OpenAICompatibleImageModel } from "@ai-sdk/openai-compatible"; import { loadApiKey, loadSetting } from "@ai-sdk/provider-utils"; // src/transcription/infomaniak-transcription-model.ts import { combineHeaders, convertBase64ToUint8Array, createJsonResponseHandler, getFromApi, parseProviderOptions, postFormDataToApi } from "@ai-sdk/provider-utils"; import { z as z3 } from "zod/v4"; // src/infomaniak-error.ts import { createJsonErrorResponseHandler } from "@ai-sdk/provider-utils"; import { z } from "zod/v4"; var infomaniakErrorDataSchema = z.object({ result: z.string(), error: z.object({ code: z.string(), description: z.string(), errors: z.array(z.any()) }).optional() }); var infomaniakFailedResponseHandler = createJsonErrorResponseHandler({ errorSchema: infomaniakErrorDataSchema, errorToMessage: (data) => { var _a; return ((_a = data.error) == null ? void 0 : _a.description) || data.result; } }); // src/transcription/infomaniak-transcription-options.ts import { z as z2 } from "zod/v4"; var infomaniakTranscriptionProviderOptions = z2.object({ /** * Only if timestamp_granularities[]:word is True, merge these punctuation symbols with the previous word */ appendPunctuations: z2.array(z2.string()).optional(), /** * Defines the maximum duration for an active segment in sec. For subtitle tasks, it's recommended to set this to a short duration (5-10 seconds) to avoid long sentences. */ chunkLength: z2.number().min(2).max(30).optional(), /** * Subtitle task. Underline each word as it is spoken in srt and vtt output formats (requires timestamp_granularities[]:word) */ highlightWords: z2.boolean().optional(), /** * The language of the input audio in ISO-639-1 format. */ language: z2.string().optional(), /** * Subtitle task. The maximum number of lines in a segment in srt and vtt output formats (requires timestamp_granularities[]:word) */ maxLineCount: z2.number().min(1).max(1e3).optional(), /** * Subtitle task. The maximum number of characters in a line before breaking the line in srt and vtt output formats (requires timestamp_granularities[]:word) */ maxLineWidth: z2.number().min(1).max(1e3).optional(), /** * Subtitle task. The maximum number of words in a segment (requires timestamp_granularities[]:word) */ maxWordsPerLine: z2.number().min(1).max(1e3).optional(), /** * If the no_speech probability is higher than this value AND the average log probability over sampled tokens is below log_prob_threshold, consider the segment as silent. */ noSpeechThreshold: z2.number().optional(), /** * Only if timestamp_granularities[]:word is True, merge these punctuation symbols with the next word */ prependPunctuations: z2.array(z2.string()).optional(), /** * An optional text to guide the model's style or continue a previous audio segment. The prompt should match the audio language. */ prompt: z2.string().optional(), /** * The format of the transcript output * @default 'json' */ responseFormat: z2.enum(["text", "json", "srt", "verbose_json", "vtt"]).default("json").optional(), /** * The timestamp granularities to populate for this transcription. * @default ['segment'] */ timestampGranularities: z2.array(z2.enum(["word", "segment"])).default(["segment"]).optional() }); // src/transcription/infomaniak-transcription-model.ts var languageMap = { afrikaans: "af", arabic: "ar", armenian: "hy", azerbaijani: "az", belarusian: "be", bosnian: "bs", bulgarian: "bg", catalan: "ca", chinese: "zh", croatian: "hr", czech: "cs", danish: "da", dutch: "nl", english: "en", estonian: "et", finnish: "fi", french: "fr", galician: "gl", german: "de", greek: "el", hebrew: "he", hindi: "hi", hungarian: "hu", icelandic: "is", indonesian: "id", italian: "it", japanese: "ja", kannada: "kn", kazakh: "kk", korean: "ko", latvian: "lv", lithuanian: "lt", macedonian: "mk", malay: "ms", marathi: "mr", maori: "mi", nepali: "ne", norwegian: "no", persian: "fa", polish: "pl", portuguese: "pt", romanian: "ro", russian: "ru", serbian: "sr", slovak: "sk", slovenian: "sl", spanish: "es", swahili: "sw", swedish: "sv", tagalog: "tl", tamil: "ta", thai: "th", turkish: "tr", ukrainian: "uk", urdu: "ur", vietnamese: "vi", welsh: "cy" }; var infomaniakTranscriptionResponseSchema = z3.object({ batch_id: z3.uuid() }); var infomaniakBatchResponseSchema = z3.object({ status: z3.enum(["pending", "success", "error"]), url: z3.url().nullable(), file_name: z3.string().nullable(), file_size: z3.number().nullable(), data: z3.json().nullable() }); var infomaniakTranscriptionDataSchema = z3.object({ duration: z3.number().nullish(), language: z3.string().nullish(), segments: z3.array(z3.object({ avg_logprob: z3.number(), compression_ratio: z3.number(), end: z3.number(), id: z3.number(), no_speech_prob: z3.number().min(0).max(1), seek: z3.number(), start: z3.number(), temperature: z3.number().min(0).max(1), text: z3.string(), tokens: z3.array(z3.number()) })).nullish(), task: z3.string().nullish(), text: z3.string(), words: z3.array(z3.object({ end: z3.number(), start: z3.number(), word: z3.string() })).nullish() }); var InfomaniakTranscriptionModel = class { constructor(modelId, config) { this.modelId = modelId; this.config = config; } specificationVersion = "v2"; get provider() { return this.config.provider; } async getArgs({ audio, mediaType, providerOptions }) { const warnings = []; const infomaniakOptions = await parseProviderOptions({ provider: "infomaniak", providerOptions, schema: infomaniakTranscriptionProviderOptions }); const formData = new FormData(); const blob = audio instanceof Uint8Array ? new Blob([audio]) : new Blob([convertBase64ToUint8Array(audio)]); formData.append("model", this.modelId); formData.append("file", new File([blob], "audio", { type: mediaType })); if (infomaniakOptions) { const transcriptionModelOptions = { append_punctuations: infomaniakOptions.appendPunctuations, chunk_length: infomaniakOptions.chunkLength, highlight_words: infomaniakOptions.highlightWords, language: infomaniakOptions.language, max_line_count: infomaniakOptions.maxLineCount, max_line_width: infomaniakOptions.maxLineWidth, max_words_per_line: infomaniakOptions.maxWordsPerLine, no_speech_threshold: infomaniakOptions.noSpeechThreshold, prepend_punctuations: infomaniakOptions.prependPunctuations, prompt: infomaniakOptions.prompt, response_format: "verbose_json", // always use verbose_json to get correct data shape timestamp_granularities: infomaniakOptions.timestampGranularities || ["segment"] }; for (const [key, value] of Object.entries(transcriptionModelOptions)) { if (value != null) { if (Array.isArray(value)) { value.forEach((item) => formData.append(`${key}[]`, String(item))); } else { formData.append(key, String(value)); } } } } return { formData, warnings }; } async pollForResults(batchId, options, maxRetries = 60, delayMs = 2e3) { for (let attempt = 0; attempt < maxRetries; attempt++) { const url = this.config.url({ path: `/results/${batchId}` }); const { value: response, responseHeaders } = await getFromApi({ url: url.replace("/openai", ""), // remove the /openai part, maybe remove it from the base anyway headers: combineHeaders(this.config.headers(), options.headers), failedResponseHandler: infomaniakFailedResponseHandler, successfulResponseHandler: createJsonResponseHandler( infomaniakBatchResponseSchema ), abortSignal: options.abortSignal, fetch: this.config.fetch }); if (response.status === "success" && response.data) { const parsedData = infomaniakTranscriptionDataSchema.parse( JSON.parse(response.data) ); return { result: parsedData, responseHeaders, rawResponse: JSON.stringify(response) }; } if (attempt < maxRetries - 1) { await new Promise((resolve) => setTimeout(resolve, delayMs)); } } throw new Error(`Transcription timed out after ${maxRetries} attempts`); } async doGenerate(options) { var _a, _b, _c, _d; const currentDate = ((_b = (_a = this.config._internal) == null ? void 0 : _a.currentDate) == null ? void 0 : _b.call(_a)) ?? /* @__PURE__ */ new Date(); const { formData, warnings } = await this.getArgs(options); const { value: batchResponse } = await postFormDataToApi({ url: this.config.url({ path: "/audio/transcriptions" }), headers: combineHeaders(this.config.headers(), options.headers), formData, failedResponseHandler: infomaniakFailedResponseHandler, successfulResponseHandler: createJsonResponseHandler( infomaniakTranscriptionResponseSchema ), abortSignal: options.abortSignal, fetch: this.config.fetch }); const { result, responseHeaders, rawResponse } = await this.pollForResults( batchResponse.batch_id, options ); const language = result.language != null && result.language in languageMap ? languageMap[result.language] : void 0; return { text: result.text, segments: ((_c = result.segments) == null ? void 0 : _c.map((segment) => ({ text: segment.text, startSecond: segment.start, endSecond: segment.end }))) ?? ((_d = result.words) == null ? void 0 : _d.map((word) => ({ text: word.word, startSecond: word.start, endSecond: word.end }))) ?? [], language, durationInSeconds: result.duration ?? void 0, warnings, response: { timestamp: currentDate, modelId: this.modelId, headers: responseHeaders, body: rawResponse }, providerMetadata: { infomaniak: { batchId: batchResponse.batch_id } } }; } }; // src/infomaniak-provider.ts function createInfomaniak(options = {}) { const getProductId = () => loadSetting({ environmentVariableName: "INFOMANIAK_PRODUCT_ID", settingName: "productId", description: "Infomaniak product ID", settingValue: options.productId }); const getApiUrl = () => `https://api.infomaniak.com/1/ai/${getProductId()}/openai`; const getHeaders = () => ({ Authorization: `Bearer ${loadApiKey({ apiKey: options.apiKey, environmentVariableName: "INFOMANIAK_API_KEY", description: "Infomaniak API key" })}`, ...options.headers }); const getCommonModelConfig = (modelType) => ({ provider: `infomaniak.${modelType}`, url: ({ path }) => { const url = new URL(`${getApiUrl()}${path}`); return url.toString(); }, headers: getHeaders, fetch: options.fetch }); const createChatModel = (modelId) => { return new OpenAICompatibleChatLanguageModel( modelId, { ...getCommonModelConfig("chat"), includeUsage: options.includeUsage ?? false } ); }; const createTextEmbeddingModel = (modelId) => { return new OpenAICompatibleEmbeddingModel( modelId, { ...getCommonModelConfig("text_embedding"), url: ({ path }) => { const url = new URL(`${getApiUrl()}/v1${path}`); return url.toString(); } } ); }; const createImageModel = (modelId) => new OpenAICompatibleImageModel(modelId, getCommonModelConfig("image")); const createTranscriptionModel = (modelId) => new InfomaniakTranscriptionModel(modelId, { ...getCommonModelConfig("transcription") }); const provider = (modelId) => createChatModel(modelId); provider.chatModel = createChatModel; provider.languageModel = createChatModel; provider.textEmbeddingModel = createTextEmbeddingModel; provider.imageModel = createImageModel; provider.transcription = createTranscriptionModel; return provider; } var infomaniak = createInfomaniak(); export { createInfomaniak, infomaniak }; //# sourceMappingURL=index.mjs.map