/* * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. */ import { CreateSpeechAcceptEnum, textToSpeechCreateSpeech, } from "../funcs/textToSpeechCreateSpeech.js"; import { textToSpeechPredictDuration } from "../funcs/textToSpeechPredictDuration.js"; import { StreamSpeechAcceptEnum, textToSpeechStreamSpeech, } from "../funcs/textToSpeechStreamSpeech.js"; import { ClientSDK, RequestOptions } from "../lib/sdks.js"; import * as operations from "../models/operations/index.js"; import { unwrapAsync } from "../types/fp.js"; // #region imports import { applyPronunciationDictionary, chunkText, DEFAULT_MAX_TEXT_LENGTH, detectAudioFormat, mergeMp3Binary, mergeWavBinary, type PronunciationDictionaryEntry, removeMp3Header, removeWavHeader, } from "../lib/custom_utils/index.js"; type CreateSpeechOptions = RequestOptions & { acceptHeaderOverride?: CreateSpeechAcceptEnum; maxTextLength?: number; pronunciationDictionary?: PronunciationDictionaryEntry[]; }; type StreamSpeechOptions = RequestOptions & { acceptHeaderOverride?: StreamSpeechAcceptEnum; maxTextLength?: number; pronunciationDictionary?: PronunciationDictionaryEntry[]; }; // #endregion imports export { CreateSpeechAcceptEnum } from "../funcs/textToSpeechCreateSpeech.js"; export { StreamSpeechAcceptEnum } from "../funcs/textToSpeechStreamSpeech.js"; export class TextToSpeech extends ClientSDK { // #region sdk-class-body private _createSpeechOriginal?: typeof TextToSpeech.prototype.createSpeech; private _streamSpeechOriginal?: typeof TextToSpeech.prototype.streamSpeech; constructor(options: any) { super(options); // Store original methods before overriding this._createSpeechOriginal = this.createSpeech.bind(this); this._streamSpeechOriginal = this.streamSpeech.bind(this); // Override with auto-chunking versions this.createSpeech = this.createSpeechWithChunking.bind(this) as any; this.streamSpeech = this.streamSpeechWithChunking.bind(this) as any; } /** * Check if text needs to be chunked */ private shouldChunkText(text: string, maxLength: number): boolean { return text.length > maxLength; } /** * Apply pronunciation dictionary before chunking (opt-in). */ private applyPronunciationDictionary( text: string, pronunciationDictionary?: PronunciationDictionaryEntry[], ): string { if (!pronunciationDictionary) return text; return applyPronunciationDictionary(text, pronunciationDictionary); } /** * Extract audio data from response */ private async extractAudioFromResponse( response: operations.CreateSpeechResponse | operations.StreamSpeechResponse, ): Promise { const result = response.result; if (result instanceof Uint8Array) { return result; } if (result instanceof Blob) { return new Uint8Array(await result.arrayBuffer()); } if (result instanceof ArrayBuffer) { return new Uint8Array(result); } if ( typeof result === "object" && result !== null && "getReader" in result ) { // ReadableStream const reader = (result as ReadableStream).getReader(); const chunks: Uint8Array[] = []; while (true) { const { done, value } = await reader.read(); if (done) break; if (value) chunks.push(value); } // Merge all chunks const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0); const merged = new Uint8Array(totalLength); let offset = 0; for (const chunk of chunks) { merged.set(chunk, offset); offset += chunk.length; } return merged; } // Handle CreateSpeechResponseBody with audioBase64 if (typeof result === "object" && result !== null) { if ("audioBase64" in result) { const audioBase64 = (result as any).audioBase64; if (typeof audioBase64 === "string") { // Decode base64 to binary const binaryString = atob(audioBase64); const bytes = new Uint8Array(binaryString.length); for (let i = 0; i < binaryString.length; i++) { bytes[i] = binaryString.charCodeAt(i); } return bytes; } } // Fallback: try to access other properties if ("content" in result) { const content = (result as any).content; if (content instanceof Uint8Array) return content; if (typeof content === "string") { return new TextEncoder().encode(content); } } if ("_content" in result) { const content = (result as any)._content; if (content instanceof Uint8Array) return content; } } // Enhanced error message with object inspection const resultType = typeof result; const resultConstructor = result?.constructor?.name || "unknown"; const resultKeys = result && typeof result === "object" ? Object.keys(result).join(", ") : "N/A"; throw new Error( `Unsupported result type: ${resultType}, ` + `constructor: ${resultConstructor}, ` + `keys: [${resultKeys}]`, ); } /** * Merge multiple audio responses into one */ private async mergeAudioResponses( responses: operations.CreateSpeechResponse[], ): Promise { if (responses.length === 0) { throw new Error("No responses to merge"); } const firstResponse = responses[0]; if (!firstResponse) { throw new Error("First response is undefined"); } if (responses.length === 1) { return firstResponse; } // Extract audio data from all responses const audioChunks: Uint8Array[] = await Promise.all( responses.map((r) => this.extractAudioFromResponse(r)), ); const firstChunk = audioChunks[0]; if (!firstChunk) { throw new Error("First audio chunk is undefined"); } // Detect format from first chunk const audioFormat = detectAudioFormat(firstChunk); // Merge based on format let mergedAudio: Uint8Array; if (audioFormat === "wav") { mergedAudio = mergeWavBinary(audioChunks); } else { // MP3 or other formats mergedAudio = mergeMp3Binary(audioChunks); } // Convert Uint8Array to ReadableStream for compatibility with CreateSpeechResponse type const stream = new ReadableStream({ start(controller) { controller.enqueue(mergedAudio); controller.close(); }, }); // Return merged response with proper type return { result: stream, headers: firstResponse.headers ?? {}, }; } /** * Create extended streaming response for long text * Streams each text chunk progressively without waiting for full completion */ private createExtendedStreamingResponse( firstResponse: operations.StreamSpeechResponse, remainingChunks: string[], originalRequest: operations.StreamSpeechRequest, options?: RequestOptions & { acceptHeaderOverride?: StreamSpeechAcceptEnum; }, ): operations.StreamSpeechResponse { let audioFormat: "wav" | "mp3" | null = null; let isFirstAudioChunk = true; // Use arrow function to preserve 'this' context const processStream = async ( controller: ReadableStreamDefaultController, ) => { try { // Stream first response (first text chunk) if (firstResponse.result) { const result = firstResponse.result; if (typeof result === "object" && "getReader" in result) { const reader = (result as ReadableStream).getReader(); try { while (true) { const { done, value } = await reader.read(); if (done) break; if (value) { // Detect format from first audio chunk if (isFirstAudioChunk) { const detectedFormat = detectAudioFormat(value); audioFormat = detectedFormat as "wav" | "mp3"; isFirstAudioChunk = false; } controller.enqueue(value); } } } finally { reader.releaseLock(); } } } // Process remaining text chunks sequentially for (const chunk of remainingChunks) { const chunkRequest: operations.StreamSpeechRequest = { ...originalRequest, apiConvertTextToSpeechUsingCharacterRequest: { ...originalRequest.apiConvertTextToSpeechUsingCharacterRequest, text: chunk, }, }; // Call original streaming method if (!this._streamSpeechOriginal) { throw new Error("Original streamSpeech method not found"); } const chunkResponse = await this._streamSpeechOriginal( chunkRequest, options, ); // Stream this text chunk's audio if (chunkResponse.result) { const result = chunkResponse.result; if (typeof result === "object" && "getReader" in result) { const reader = (result as ReadableStream).getReader(); let isFirstChunkOfThisText = true; try { while (true) { const { done, value } = await reader.read(); if (done) break; if (value) { let processedAudio = value; // Remove header from first chunk of subsequent text chunks if (isFirstChunkOfThisText && audioFormat) { if (audioFormat === "wav") { processedAudio = removeWavHeader(value); } else if (audioFormat === "mp3") { processedAudio = removeMp3Header(value); } isFirstChunkOfThisText = false; } controller.enqueue(processedAudio); } } } finally { reader.releaseLock(); } } } } controller.close(); } catch (error) { controller.error(error); } }; const stream = new ReadableStream({ start: (controller) => processStream(controller), }); return { result: stream, headers: firstResponse.headers ?? {}, }; } /** * Create speech with auto-chunking support (internal implementation) */ private async createSpeechWithChunking( request: operations.CreateSpeechRequest, options?: CreateSpeechOptions, ): Promise { const { pronunciationDictionary, ...restOptions } = options ?? {}; const maxLength = options?.maxTextLength ?? DEFAULT_MAX_TEXT_LENGTH; const text = request.apiConvertTextToSpeechUsingCharacterRequest?.text ?? ""; const normalizedText = this.applyPronunciationDictionary( text, pronunciationDictionary, ); const baseRequest: operations.CreateSpeechRequest = { ...request, apiConvertTextToSpeechUsingCharacterRequest: { ...request.apiConvertTextToSpeechUsingCharacterRequest, text: normalizedText, }, }; // Short text: call original method directly if (!this.shouldChunkText(normalizedText, maxLength)) { if (!this._createSpeechOriginal) { throw new Error("Original createSpeech method not found"); } return this._createSpeechOriginal(baseRequest, restOptions); } // Long text: chunk, process sequentially (to avoid schema parsing issues), and merge const textChunks = chunkText(normalizedText, maxLength); // Determine Accept header based on output format const outputFormat = baseRequest.apiConvertTextToSpeechUsingCharacterRequest ?.outputFormat; const acceptHeader: CreateSpeechAcceptEnum = outputFormat === "mp3" ? CreateSpeechAcceptEnum.audioMpeg : CreateSpeechAcceptEnum.audioWav; // Process chunks sequentially to avoid race conditions in schema parsing const responses: operations.CreateSpeechResponse[] = []; for (const chunk of textChunks) { const chunkRequest: operations.CreateSpeechRequest = { ...baseRequest, apiConvertTextToSpeechUsingCharacterRequest: { ...baseRequest.apiConvertTextToSpeechUsingCharacterRequest, text: chunk, }, }; if (!this._createSpeechOriginal) { throw new Error("Original createSpeech method not found"); } const response = await this._createSpeechOriginal(chunkRequest, { ...restOptions, acceptHeaderOverride: acceptHeader, }); responses.push(response); } return this.mergeAudioResponses(responses); } /** * Stream speech with auto-chunking support (internal implementation) */ private async streamSpeechWithChunking( request: operations.StreamSpeechRequest, options?: StreamSpeechOptions, ): Promise { const { pronunciationDictionary, ...restOptions } = options ?? {}; const maxLength = options?.maxTextLength ?? DEFAULT_MAX_TEXT_LENGTH; const text = request.apiConvertTextToSpeechUsingCharacterRequest?.text ?? ""; const normalizedText = this.applyPronunciationDictionary( text, pronunciationDictionary, ); const baseRequest: operations.StreamSpeechRequest = { ...request, apiConvertTextToSpeechUsingCharacterRequest: { ...request.apiConvertTextToSpeechUsingCharacterRequest, text: normalizedText, }, }; // Short text: call original method directly if (!this.shouldChunkText(normalizedText, maxLength)) { if (!this._streamSpeechOriginal) { throw new Error("Original streamSpeech method not found"); } return this._streamSpeechOriginal(baseRequest, restOptions); } // Long text: chunk and stream sequentially const textChunks = chunkText(normalizedText, maxLength); if (textChunks.length === 0) { throw new Error("No text chunks to process"); } const firstChunk = textChunks[0]; if (!firstChunk) { throw new Error("First text chunk is undefined"); } // Get first response to start streaming const firstChunkRequest: operations.StreamSpeechRequest = { ...baseRequest, apiConvertTextToSpeechUsingCharacterRequest: { ...baseRequest.apiConvertTextToSpeechUsingCharacterRequest, text: firstChunk, }, }; if (!this._streamSpeechOriginal) { throw new Error("Original streamSpeech method not found"); } const firstResponse = await this._streamSpeechOriginal( firstChunkRequest, restOptions, ); // Single chunk: return as-is if (textChunks.length === 1) { return firstResponse; } // Multiple chunks: create extended streaming response const remainingChunks = textChunks.slice(1); return this.createExtendedStreamingResponse( firstResponse, remainingChunks, baseRequest, restOptions, ); } // #endregion sdk-class-body /** * Convert text to speech * * @remarks * Convert text to speech using the specified voice */ async createSpeech( request: operations.CreateSpeechRequest, options?: RequestOptions & { acceptHeaderOverride?: CreateSpeechAcceptEnum; }, ): Promise { return unwrapAsync(textToSpeechCreateSpeech( this, request, options, )); } /** * Convert text to speech with streaming response * * @remarks * Convert text to speech using the specified voice with streaming response. Returns binary audio stream. */ async streamSpeech( request: operations.StreamSpeechRequest, options?: RequestOptions & { acceptHeaderOverride?: StreamSpeechAcceptEnum; }, ): Promise { return unwrapAsync(textToSpeechStreamSpeech( this, request, options, )); } /** * Predict text-to-speech duration * * @remarks * Predict the duration of text-to-speech conversion without generating audio */ async predictDuration( request: operations.PredictDurationRequest, options?: RequestOptions, ): Promise { return unwrapAsync(textToSpeechPredictDuration( this, request, options, )); } }