import type { CopilotKitCoreReact } from "./react-core"; import { TranscriptionErrorCode, type TranscriptionErrorResponse, } from "@copilotkit/shared"; export interface TranscriptionResult { text: string; size: number; type: string; } /** * Error info parsed from transcription endpoint error responses. */ export interface TranscriptionErrorInfo { code: TranscriptionErrorCode; message: string; retryable: boolean; } // Re-export error code enum for convenience export { TranscriptionErrorCode }; /** * Convert a Blob to a base64 string */ async function blobToBase64(blob: Blob): Promise { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onloadend = () => { const result = reader.result as string; // Remove the data URL prefix to get pure base64 const base64 = result.split(",")[1]; resolve(base64 ?? ""); }; reader.onerror = () => reject(new Error("Failed to read audio data")); reader.readAsDataURL(blob); }); } /** * Check if an error response matches our expected format */ function isTranscriptionErrorResponse( data: unknown, ): data is TranscriptionErrorResponse { return ( typeof data === "object" && data !== null && "error" in data && "message" in data && typeof (data as TranscriptionErrorResponse).error === "string" && typeof (data as TranscriptionErrorResponse).message === "string" ); } /** * Parse error info from a transcription error response */ function parseTranscriptionError( response: TranscriptionErrorResponse, ): TranscriptionErrorInfo { return { code: response.error, message: response.message, retryable: response.retryable ?? false, }; } /** * Custom error type for transcription failures. * Extends Error with transcription-specific info for contextual error handling. */ export class TranscriptionError extends Error { public readonly info: TranscriptionErrorInfo; constructor(info: TranscriptionErrorInfo) { super(info.message); this.name = "TranscriptionError"; this.info = info; } } /** * Transcribe an audio blob using the CopilotKit runtime * * Supports both REST mode (multipart/form-data) and single-endpoint mode (base64 JSON) * * @throws {TranscriptionError} When transcription fails with typed error information */ export async function transcribeAudio( core: CopilotKitCoreReact, audioBlob: Blob, filename: string = "recording.webm", ): Promise { const runtimeUrl = core.runtimeUrl; if (!runtimeUrl) { throw new TranscriptionError({ code: TranscriptionErrorCode.INVALID_REQUEST, message: "Runtime URL is not configured", retryable: false, }); } const headers: Record = { ...core.headers }; let response: Response; try { if (core.runtimeTransport === "single") { // Single-endpoint mode: POST JSON with base64 audio const base64Audio = await blobToBase64(audioBlob); headers["Content-Type"] = "application/json"; response = await fetch(runtimeUrl, { method: "POST", headers, body: JSON.stringify({ method: "transcribe", body: { audio: base64Audio, mimeType: audioBlob.type || "audio/webm", filename, }, }), }); } else { // REST mode: POST multipart/form-data to /transcribe // Don't set Content-Type - browser will set it with boundary for FormData delete headers["Content-Type"]; const formData = new FormData(); formData.append("audio", audioBlob, filename); response = await fetch(`${runtimeUrl}/transcribe`, { method: "POST", headers, body: formData, }); } } catch (error) { // Network error - fetch failed throw new TranscriptionError({ code: TranscriptionErrorCode.NETWORK_ERROR, message: error instanceof Error ? error.message : "Network request failed", retryable: true, }); } if (!response.ok) { let errorData: unknown; try { errorData = await response.json(); } catch { // Could not parse error response throw new TranscriptionError({ code: TranscriptionErrorCode.PROVIDER_ERROR, message: `HTTP ${response.status}: ${response.statusText}`, retryable: response.status >= 500, }); } // If we got a typed error response, use it if (isTranscriptionErrorResponse(errorData)) { throw new TranscriptionError(parseTranscriptionError(errorData)); } // Unknown error format throw new TranscriptionError({ code: TranscriptionErrorCode.PROVIDER_ERROR, message: typeof errorData === "object" && errorData !== null && "message" in errorData ? String((errorData as { message: unknown }).message) : "Transcription failed", retryable: response.status >= 500, }); } return (await response.json()) as TranscriptionResult; }