import { logger } from '@services/libs/log'; import { AiAPIConfig } from '@services/agentInstance/promptConcat/promptConcatSchema'; import { AuthenticationError, MissingAPIKeyError, MissingBaseURLError } from './errors'; import type { AIProviderConfig, AITranscriptionResponse } from './interface'; interface TranscriptionOptions { /** Language of the audio (ISO-639-1 format, e.g., 'en', 'zh') */ language?: string; /** Response format (json, text, srt, vtt, verbose_json) */ responseFormat?: string; /** Temperature for sampling (0-1) */ temperature?: number; /** Optional prompt to guide the model */ prompt?: string; } /** * Transcribe audio to text using an AI provider */ export async function generateTranscriptionFromProvider( audioFile: File | Blob, config: AiAPIConfig, signal: AbortSignal, providerConfig?: AIProviderConfig, options: TranscriptionOptions = {}, ): Promise { const provider = config.api.provider; const model = config.api.transcriptionsModel || config.api.model; logger.info(`Using AI transcription provider: ${provider}, model: ${model}`); try { // Check if API key is required const isOllama = providerConfig?.providerClass === 'ollama'; const isLocalOpenAICompatible = providerConfig?.providerClass === 'openAICompatible' && providerConfig?.baseURL && (providerConfig.baseURL.includes('localhost') || providerConfig.baseURL.includes('127.0.0.1')); if (!providerConfig?.apiKey && !isOllama && !isLocalOpenAICompatible) { throw new MissingAPIKeyError(provider); } // Get base URL and prepare headers let baseURL = providerConfig?.baseURL || ''; const headers: Record = {}; // Set up provider-specific configuration switch (providerConfig?.providerClass || provider) { case 'openai': baseURL = 'https://api.openai.com/v1'; headers['Authorization'] = `Bearer ${providerConfig?.apiKey}`; break; case 'openAICompatible': if (!providerConfig?.baseURL) { throw new MissingBaseURLError(provider); } baseURL = providerConfig.baseURL; if (providerConfig.apiKey) { headers['Authorization'] = `Bearer ${providerConfig.apiKey}`; } break; case 'deepseek': throw new Error(`DeepSeek provider does not support transcriptions`); case 'anthropic': throw new Error(`Anthropic provider does not support transcriptions`); case 'ollama': throw new Error(`Ollama provider does not support transcriptions via this API`); default: // For silicon flow and other openai-compatible providers if (!providerConfig?.baseURL) { throw new MissingBaseURLError(provider); } baseURL = providerConfig.baseURL; if (providerConfig.apiKey) { headers['Authorization'] = `Bearer ${providerConfig.apiKey}`; } break; } // Prepare FormData for multipart/form-data request const formData = new FormData(); formData.append('file', audioFile); formData.append('model', model); // Add optional parameters if (options.language) { formData.append('language', options.language); } if (options.responseFormat) { formData.append('response_format', options.responseFormat); } if (options.temperature !== undefined) { formData.append('temperature', options.temperature.toString()); } if (options.prompt) { formData.append('prompt', options.prompt); } // Make the API call const response = await fetch(`${baseURL}/audio/transcriptions`, { method: 'POST', headers, body: formData, signal, }); if (!response.ok) { const errorText = await response.text(); logger.error('Transcription API error', { function: 'generateTranscriptionFromProvider', status: response.status, errorText, }); if (response.status === 401) { throw new AuthenticationError(provider); } else if (response.status === 404) { throw new Error(`${provider} error: Model "${model}" not found`); } else if (response.status === 429) { throw new Error(`${provider} too many requests: Reduce request frequency or check API limits`); } else { throw new Error(`${provider} transcription error: ${errorText}`); } } // Parse response based on format const responseFormat = options.responseFormat || 'json'; let text = ''; let language: string | undefined; let duration: number | undefined; if (responseFormat === 'json' || responseFormat === 'verbose_json') { const data = await response.json() as { text: string; language?: string; duration?: number; }; text = data.text; language = data.language; duration = data.duration; } else { // For text, srt, vtt formats, just get the text text = await response.text(); } return { requestId: crypto.randomUUID(), text, language, duration, model, status: 'done' as const, }; } catch (error) { logger.error(`${provider} transcription error:`, error); if (error instanceof Error && error.name === 'AbortError') { throw error; } // Return error response for consistency return { requestId: crypto.randomUUID(), text: '', model, status: 'error' as const, errorDetail: { name: error instanceof Error ? error.name : 'UnknownError', code: 'TRANSCRIPTION_FAILED', provider, message: error instanceof Error ? error.message : String(error), }, }; } }