// D3RO Cloud STT client. User-facing transcription always passes through the // Supabase Edge gateway that owns authentication, quota and usage accounting. export interface TranscribeAudioParams { audio: Blob | ArrayBuffer | Uint8Array | string language?: string supabaseUrl?: string anonKey?: string token?: string } export interface TranscribeAudioResult { text: string confidence: number language: string durationSeconds: number provider: string latencyMs: number modelId?: string cost?: number } function decodeBase64(value: string): Uint8Array { const normalized = value.replace(/^data:[^,]*,/, '').replace(/\s+/g, '') if (!normalized || normalized.length % 4 !== 0 || !/^[A-Za-z0-9+/]*={0,2}$/.test(normalized)) { throw new Error('Unsupported audio payload format.') } const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' const output: number[] = [] for (let offset = 0; offset < normalized.length; offset += 4) { const chars = normalized.slice(offset, offset + 4) const values = [...chars].map((char) => char === '=' ? 0 : alphabet.indexOf(char)) if (values.some((item) => item < 0)) throw new Error('Unsupported audio payload format.') const combined = (values[0] << 18) | (values[1] << 12) | (values[2] << 6) | values[3] output.push((combined >>> 16) & 0xff) if (chars[2] !== '=') output.push((combined >>> 8) & 0xff) if (chars[3] !== '=') output.push(combined & 0xff) } return Uint8Array.from(output) } function gatewayUrl(value: string): string { try { const url = new URL('/functions/v1/stt-proxy', value) const localHttp = url.protocol === 'http:' && ['localhost', '127.0.0.1'].includes(url.hostname) if ((url.protocol !== 'https:' && !localHttp) || url.username || url.password) throw new Error('invalid') return url.toString() } catch { throw new Error('STT gateway configuration is invalid.') } } function parseResult(value: unknown, latencyMs: number): TranscribeAudioResult { if (!value || typeof value !== 'object' || Array.isArray(value)) { throw new Error('STT gateway returned an invalid response.') } const payload = value as Record if ( typeof payload.transcript !== 'string' || !payload.transcript.trim() || typeof payload.confidence !== 'number' || !Number.isFinite(payload.confidence) || payload.confidence < 0 || payload.confidence > 1 || typeof payload.language_code !== 'string' || !/^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8})?$/.test(payload.language_code) || typeof payload.duration_seconds !== 'number' || !Number.isFinite(payload.duration_seconds) || payload.duration_seconds < 0 || typeof payload.provider !== 'string' || !/^[a-z0-9._-]{1,64}$/.test(payload.provider) ) { throw new Error('STT gateway returned an invalid response.') } return { text: payload.transcript.trim(), confidence: payload.confidence, language: payload.language_code, durationSeconds: payload.duration_seconds, provider: payload.provider, latencyMs, } } export async function transcribeAudio(params: TranscribeAudioParams): Promise { const supabaseUrl = params.supabaseUrl ?? process.env.NEXT_PUBLIC_SUPABASE_URL ?? '' const anonKey = params.anonKey ?? process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY ?? '' const token = params.token ?? '' if (!supabaseUrl || !anonKey || !token) { throw new Error('STT gateway requires an authenticated Supabase session.') } let audio: Blob const makeBlob = Blob as unknown as new (parts: unknown[], options?: { type?: string }) => Blob if (params.audio instanceof Blob) { audio = params.audio } else if (params.audio instanceof ArrayBuffer) { audio = new makeBlob([params.audio], { type: 'audio/wav' }) } else if (params.audio instanceof Uint8Array) { audio = new makeBlob([params.audio.slice().buffer], { type: 'audio/wav' }) } else if (typeof params.audio === 'string') { audio = new makeBlob([decodeBase64(params.audio).buffer], { type: 'audio/wav' }) } else { throw new Error('Unsupported audio payload format.') } const formData = new FormData() formData.append('audio', audio) if (params.language && params.language !== 'auto') formData.append('language_code', params.language) const startedAt = Date.now() const response = await fetch(gatewayUrl(supabaseUrl), { method: 'POST', headers: { Authorization: `Bearer ${token}`, apikey: anonKey, }, body: formData, }) if (!response.ok) throw new Error(`STT gateway request failed (${response.status}).`) return parseResult(await response.json().catch(() => null), Date.now() - startedAt) }