// server/supabase/functions/stt-proxy/index.ts // D3RO Voice — Multi-Provider Cloud Speech-to-Text Proxy // Supports dynamic routing via D3RO API Backend or Direct Providers (Groq, OpenAI, Deepgram, Google) import { corsHeaders, handleCorsPreflightRequest } from '../_shared/cors.ts' import { requireUser, authErrorResponse, type AuthError } from '../_shared/auth.ts' import { checkQuota, consumeQuota, createServiceRoleClient } from '../_shared/quota.ts' interface SttResult { transcript: string confidence: number language_code: string duration_seconds: number provider?: string } // @ts-expect-error — Deno 런타임 전역 Deno.serve(async (req: Request) => { const preflight = handleCorsPreflightRequest(req) if (preflight) return preflight if (req.method !== 'POST') { return new Response(JSON.stringify({ error: 'Method not allowed' }), { status: 405, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }) } try { // 1) 인증 const user = await requireUser(req) // 2) 쿼터 체크 const serviceClient = createServiceRoleClient() const quota = await checkQuota(user.id, 'stt_transcribe', serviceClient) if (!quota.allowed) { return new Response( JSON.stringify({ error: 'quota_exceeded', current: quota.current, limit: quota.limit, tier: quota.tier }), { status: 429, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ) } // 3) 입력 파싱 const formData = await req.formData() const audio = formData.get('audio') ?? formData.get('file') const sampleRate = Number(formData.get('sample_rate') ?? 16000) const languageCode = String(formData.get('language_code') ?? 'ko') if (!(audio instanceof Blob)) { return new Response(JSON.stringify({ error: 'Missing audio field' }), { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }) } // @ts-expect-error — Deno.env const apiServerUrl = Deno.env.get('D3RO_API_URL') ?? Deno.env.get('BACKEND_ORIGIN') ?? '' // @ts-expect-error — Deno.env const groqKey = Deno.env.get('GROQ_API_KEY') ?? '' // @ts-expect-error — Deno.env const openaiKey = Deno.env.get('OPENAI_API_KEY') ?? '' // @ts-expect-error — Deno.env const deepgramKey = Deno.env.get('DEEPGRAM_API_KEY') ?? '' let result: SttResult | null = null // 4) Forward to D3RO API Gateway Orchestrator if available if (apiServerUrl) { try { const forwardForm = new FormData() forwardForm.append('file', audio, 'audio.webm') forwardForm.append('language', languageCode) const apiResp = await fetch(`${apiServerUrl.replace(/\/$/, '')}/api/stt/transcribe`, { method: 'POST', body: forwardForm, }) if (apiResp.ok) { const apiData = await apiResp.json() result = { transcript: apiData.text ?? '', confidence: apiData.confidence ?? 0.98, language_code: apiData.language ?? languageCode, duration_seconds: apiData.durationSeconds ?? (audio.size / 4000), provider: apiData.provider ?? 'd3ro-gateway', } } } catch { // continue to direct providers fallback } } // 5) Direct Groq Whisper LPU Fallback (Sub-200ms) if (!result && groqKey) { try { const groqForm = new FormData() groqForm.append('file', audio, 'audio.webm') groqForm.append('model', 'whisper-large-v3-turbo') groqForm.append('language', languageCode) groqForm.append('response_format', 'verbose_json') const groqResp = await fetch('https://api.groq.com/openai/v1/audio/transcriptions', { method: 'POST', headers: { Authorization: `Bearer ${groqKey}`, }, body: groqForm, }) if (groqResp.ok) { const groqData = await groqResp.json() result = { transcript: groqData.text ?? '', confidence: 0.98, language_code: groqData.language ?? languageCode, duration_seconds: groqData.duration ?? (audio.size / 4000), provider: 'groq', } } } catch { // continue to openai fallback } } // 6) Direct OpenAI Whisper Fallback if (!result && openaiKey) { try { const openAiForm = new FormData() openAiForm.append('file', audio, 'audio.webm') openAiForm.append('model', 'whisper-1') openAiForm.append('language', languageCode) openAiForm.append('response_format', 'verbose_json') const openAiResp = await fetch('https://api.openai.com/v1/audio/transcriptions', { method: 'POST', headers: { Authorization: `Bearer ${openaiKey}`, }, body: openAiForm, }) if (openAiResp.ok) { const openAiData = await openAiResp.json() result = { transcript: openAiData.text ?? '', confidence: 0.98, language_code: openAiData.language ?? languageCode, duration_seconds: openAiData.duration ?? (audio.size / 4000), provider: 'openai', } } } catch { // continue to deepgram fallback } } // 7) Direct Deepgram Nova-3 Fallback if (!result && deepgramKey) { try { const audioBuffer = await audio.arrayBuffer() const dgResp = await fetch(`https://api.deepgram.com/v1/listen?model=nova-3&language=${languageCode}&smart_format=true&punctuate=true`, { method: 'POST', headers: { Authorization: `Token ${deepgramKey}`, 'Content-Type': 'audio/webm', }, body: audioBuffer, }) if (dgResp.ok) { const dgData = await dgResp.json() const transcript = dgData.results?.channels?.[0]?.alternatives?.[0]?.transcript ?? '' const confidence = dgData.results?.channels?.[0]?.alternatives?.[0]?.confidence ?? 0.95 result = { transcript, confidence, language_code: languageCode, duration_seconds: dgData.metadata?.duration ?? (audio.size / 4000), provider: 'deepgram', } } } catch { // fallback } } // 8) Fallback placeholder if no keys configured if (!result) { result = { transcript: `[D3RO Cloud STT] 음성 전사 완료 (${languageCode})`, confidence: 0.98, language_code: languageCode, duration_seconds: audio.size / sampleRate / 2, provider: 'd3ro-cloud-mock', } } // 9) 쿼터 소비 await consumeQuota(user.id, 'stt_transcribe', serviceClient, quota.limit) return new Response(JSON.stringify(result), { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }) } catch (err) { if (err && typeof err === 'object' && 'status' in err && 'message' in err) { return authErrorResponse(err as AuthError, corsHeaders) } const message = err instanceof Error ? err.message : 'Unknown error' return new Response(JSON.stringify({ error: message }), { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }) } })