// Speech-to-Text - Unified interface for multiple STT models // Endpoint: /stt/create, /stt/{generation_id} import type { Uploadable } from 'openai/uploads.js'; import { type AIMLAPIClient } from '../client'; import { buildPath } from './_paths'; // ===== VERSION ===== const VERSION = 'v1' as const; /** * Audio file input for speech-to-text */ export interface AudioInput { /** * The audio file to transcribe (for file upload) */ file?: Uploadable; /** * The URL of the audio file to transcribe (for URL-based transcription) */ url?: string; } /** * Base speech-to-text parameters */ export interface SpeechToTextParams { /** * The model to use for transcription */ model: 'aai/slam-1' | 'aai/universal' | '#g1_whisper-tiny' | 'openai/gpt-4o-mini-transcribe'; /** * The audio file or URL to transcribe */ audio: AudioInput; } /** * Extended speech-to-text parameters that support all model-specific options */ export interface SpeechToTextCreateParams extends SpeechToTextParams { // SLAM-1 specific parameters audio_start_from?: number; audio_end_at?: number; language_code?: string; language_confidence_threshold?: number; language_detection?: boolean; punctuate?: boolean; format_text?: boolean; disfluencies?: boolean; multichannel?: boolean; speaker_labels?: boolean; speakers_expected?: number; content_safety?: boolean; iab_categories?: boolean; custom_spelling?: Array<{ from: string; to: string }>; auto_highlights?: boolean; word_boost?: string[]; boost_param?: 'low' | 'default' | 'high'; filter_profanity?: boolean; redact_pii?: boolean; redact_pii_audio?: boolean; redact_pii_audio_quality?: 'mp3' | 'wav'; redact_pii_policies?: string[]; redact_pii_sub?: 'entity_name' | 'hash'; sentiment_analysis?: boolean; entity_detection?: boolean; summarization?: boolean; summary_model?: 'informative' | 'conversational' | 'catchy'; summary_type?: 'bullets' | 'bullets_verbose' | 'gist' | 'headline' | 'paragraph'; auto_chapters?: boolean; speech_threshold?: number; // Whisper Tiny specific parameters custom_intent?: string[]; custom_topic?: string[]; custom_intent_mode?: string; custom_topic_mode?: string; detect_language?: boolean; detect_entities?: boolean; detect_topics?: boolean; diarize?: boolean; dictation?: boolean; filler_words?: boolean; intents?: string[]; keywords?: string[]; language?: string; measurements?: boolean; multi_channel?: boolean; numerals?: boolean; paragraphs?: boolean; profanity_filter?: boolean; search?: boolean; sentiment?: boolean; smart_format?: boolean; summarize?: boolean; topics?: string[]; utterances?: boolean; utt_split?: boolean; // GPT-4o Mini Transcribe specific parameters prompt?: string; temperature?: number; } /** * Speech-to-text creation response */ export interface SpeechToTextCreateResponse { generation_id: string; } /** * Response from getting speech-to-text status */ export interface SpeechToTextStatusResponse { id: string; status: 'queued' | 'generating' | 'completed' | 'error'; result?: TranscriptionResult; error?: string; } /** * Transcription word details */ export interface TranscriptionWord { word: string; start: number; end: number; confidence: number; speaker?: string | null; } /** * Transcription sentence */ export interface Sentence { text: string; start: number; end: number; } /** * Transcription paragraph */ export interface Paragraph { sentences: Sentence[]; num_words: number; start: number; end: number; } /** * Transcription alternative */ export interface TranscriptionAlternative { transcript: string; confidence: number; words: TranscriptionWord[]; paragraphs?: Paragraph[]; } /** * Channel results */ export interface ChannelResults { alternatives: TranscriptionAlternative[]; } /** * Transcription metadata * Transcription results metadata */ export interface TranscriptionMetadata { /** * A unique transaction key; currently always "deprecated". */ transaction_key: string; /** * A UUID identifying this specific transcription request. */ request_id: string; /** * The SHA-256 hash of the submitted audio file (for pre-recorded requests). */ sha256: string; /** * ISO-8601 timestamp. */ created: string; /** * Length of the audio in seconds. */ duration: number; /** * The top-level results object containing per-channel transcription alternatives. */ channels: number; /** * List of model UUIDs used for this transcription */ models: string[]; /** * Mapping from each model UUID (in 'models') to detailed info: its name, version, and architecture. */ model_info: Record< string, { /** * The human-readable name of the model — identifies which model was used. */ name: string; /** * The specific version of the model. */ version: string; /** * The architecture of the model — describes the model family / generation. */ arch: string; } >; } /** * Full transcription result (detailed format) */ export interface DetailedTranscriptionResult { /** * Metadata about the transcription response, including timing, models, and IDs. */ metadata: TranscriptionMetadata; /** * The top-level results object containing per-channel transcription alternatives. */ results: { /** * Per-channel transcription results */ channels: Record; }; } /** * Simple transcription result (for OpenAI models) */ export interface SimpleTranscriptionResult { /** * The transcribed text */ text: string; /** * Usage information */ usage?: { /** * Type of usage (tokens) */ type: 'tokens'; /** * Number of input tokens */ input_tokens: number; /** * Details about input tokens */ input_token_details: { /** * Number of text tokens */ text_tokens: number; /** * Number of audio tokens */ audio_tokens: number; }; /** * Number of output tokens */ output_tokens: number; /** * Total number of tokens */ total_tokens: number; }; } /** * Union type for different transcription result formats */ export type TranscriptionResult = DetailedTranscriptionResult | SimpleTranscriptionResult; /** * Response from getting speech-to-text status */ export interface SpeechToTextStatusResponse { /** * The generation ID */ generation_id: string; /** * Status of the transcription task */ status: 'queued' | 'generating' | 'completed' | 'error'; /** * The transcription result if completed */ result?: TranscriptionResult; /** * Error message if failed */ error?: string; } /** * Speech-to-Text resource class that handles parameter transformation */ export class SpeechToText { constructor(private readonly _client: AIMLAPIClient) {} /** * Create a speech-to-text transcription task * Transforms our extended interface to the API format before making the request * @see https://docs.aimlapi.com/api-references/speech-models/speech-to-text/stt-create * * @example * ```typescript * const result = await client.speechToText.create({ * model: 'aai/slam-1', * audio: { url: 'https://example.com/audio.mp3' } * }); * console.log("Generation ID:", result.generation_id); * ``` */ async create(params: SpeechToTextCreateParams): Promise { // Handle file uploads with FormData if (params.audio.file) { const formData = new FormData(); formData.append('model', params.model); formData.append('audio', this._toBlob(params.audio.file), 'audio.wav'); // Add model-specific parameters this._addModelSpecificParams(formData, params); return this._client.post(buildPath('/stt/create', VERSION), { body: formData, }); } // Handle URL-based requests with JSON const apiParams = this._transformToApiParams(params); return this._client.post(buildPath('/stt/create', VERSION), { body: apiParams, }); } /** * Get the status and result of a speech-to-text task * @see https://docs.aimlapi.com/api-references/speech-models/speech-to-text/stt-get * * @example * ```typescript * const status = await client.speechToText.getStatus("generation-id"); * if (status.status === "completed") { * console.log("Transcript:", status.result); * } * ``` */ async getStatus(generationId: string): Promise { return this._client.get(buildPath(`/stt/${generationId}`, VERSION)); } /** * Create a transcription with automatic polling for completion * Useful for long audio files that take time to process. * * @param params - The transcription parameters * @param pollInterval - How often to poll for status (in seconds) * @param pollTimeout - Maximum time to wait (in seconds) */ async createWithPolling( params: SpeechToTextCreateParams, pollInterval: number = 10, pollTimeout: number = 600 ): Promise { // Start the transcription const result = await this.create(params); // Poll for completion const startTime = Date.now(); while (Date.now() - startTime < pollTimeout * 1000) { const status = await this.getStatus(result.generation_id); if (status.status === 'completed') { if (!status.result) { throw new Error('Transcription completed but no result available'); } return status.result; } if (status.status === 'error') { throw new Error(`Transcription failed: ${status.error || 'Unknown error'}`); } // Wait before next poll await new Promise((resolve) => setTimeout(resolve, pollInterval * 1000)); } throw new Error(`Transcription timed out after ${pollTimeout} seconds`); } /** * Transform our extended SpeechToTextCreateParams to the API format */ private _transformToApiParams(params: SpeechToTextCreateParams): any { const transformed: any = { model: params.model, }; // Add audio URL if (params.audio.url) { transformed.url = params.audio.url; } // Filter and add all defined parameters const allParams = [ // SLAM-1 parameters 'audio_start_from', 'audio_end_at', 'language_code', 'language_confidence_threshold', 'language_detection', 'punctuate', 'format_text', 'disfluencies', 'multichannel', 'speaker_labels', 'speakers_expected', 'content_safety', 'iab_categories', 'custom_spelling', 'auto_highlights', 'word_boost', 'boost_param', 'filter_profanity', 'redact_pii', 'redact_pii_audio', 'redact_pii_audio_quality', 'redact_pii_policies', 'redact_pii_sub', 'sentiment_analysis', 'entity_detection', 'summarization', 'summary_model', 'summary_type', 'auto_chapters', 'speech_threshold', // Whisper Tiny parameters 'custom_intent', 'custom_topic', 'custom_intent_mode', 'custom_topic_mode', 'detect_language', 'detect_entities', 'detect_topics', 'diarize', 'dictation', 'filler_words', 'intents', 'keywords', 'language', 'measurements', 'multi_channel', 'numerals', 'paragraphs', 'profanity_filter', 'punctuate', 'search', 'sentiment', 'smart_format', 'summarize', 'topics', 'utterances', 'utt_split', // GPT-4o Mini Transcribe parameters 'language', 'prompt', 'temperature', ]; // Add all defined parameters allParams.forEach((param) => { if (params[param as keyof SpeechToTextCreateParams] !== undefined) { transformed[param] = params[param as keyof SpeechToTextCreateParams]; } }); return transformed; } /** * Add model-specific parameters to FormData (for file uploads) */ private _addModelSpecificParams(formData: FormData, params: SpeechToTextCreateParams): void { // Filter and add all defined parameters const allParams = [ // SLAM-1 parameters 'audio_start_from', 'audio_end_at', 'language_code', 'language_confidence_threshold', 'language_detection', 'punctuate', 'format_text', 'disfluencies', 'multichannel', 'speaker_labels', 'speakers_expected', 'content_safety', 'iab_categories', 'auto_highlights', 'boost_param', 'filter_profanity', 'redact_pii', 'redact_pii_audio', 'redact_pii_audio_quality', 'redact_pii_sub', 'sentiment_analysis', 'entity_detection', 'summarization', 'summary_model', 'summary_type', 'auto_chapters', 'speech_threshold', // Whisper Tiny parameters 'custom_intent', 'custom_topic', 'custom_intent_mode', 'custom_topic_mode', 'detect_language', 'detect_entities', 'detect_topics', 'diarize', 'dictation', 'filler_words', 'intents', 'keywords', 'language', 'measurements', 'multi_channel', 'numerals', 'paragraphs', 'profanity_filter', 'punctuate', 'search', 'sentiment', 'smart_format', 'summarize', 'topics', 'utterances', 'utt_split', // GPT-4o Mini Transcribe parameters 'language', 'prompt', 'temperature', ]; // Add all defined parameters to FormData allParams.forEach((param) => { const value = params[param as keyof SpeechToTextCreateParams]; if (value !== undefined) { if (param === 'custom_spelling' && Array.isArray(value)) { (value as Array<{ from: string; to: string }>).forEach((item, index) => { formData.append(`custom_spelling[${index}][from]`, item.from); formData.append(`custom_spelling[${index}][to]`, item.to); }); } else if (param === 'word_boost' && Array.isArray(value)) { value.forEach((word, index) => { formData.append(`word_boost[${index}]`, word); }); } else if (param === 'redact_pii_policies' && Array.isArray(value)) { value.forEach((policy, index) => { formData.append(`redact_pii_policies[${index}]`, policy); }); } else if (Array.isArray(value)) { value.forEach((item, index) => { formData.append(`${param}[${index}]`, String(item)); }); } else { formData.append(param, String(value)); } } }); } /** * Convert Uploadable to Blob */ private _toBlob(uploadable: Uploadable): Blob { // If it's already a Blob, return as-is if (uploadable instanceof Blob) { return uploadable; } // If it's a Buffer, convert to Blob if (Buffer.isBuffer(uploadable)) { return new Blob([uploadable as any]); } // If it's a string (file path or URL), throw an error if (typeof uploadable === 'string') { throw new Error( 'String paths are not supported directly. Use fs.createReadStream() or pass a Buffer/Blob.' ); } // For ReadStream or other types, we can't easily convert without async // Return the uploadable as-is and let the fetch handler deal with it return uploadable as unknown as Blob; } }