// server/supabase/functions/stt-proxy/index.ts // Google Cloud Speech-to-Text 프록시. // 요청: multipart/form-data (audio + sample_rate + language_code) // 응답: { transcript, confidence, language_code, duration_seconds } 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 } // @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') const sampleRate = Number(formData.get('sample_rate') ?? 16000) const languageCode = String(formData.get('language_code') ?? 'ko-KR') if (!(audio instanceof Blob)) { return new Response(JSON.stringify({ error: 'Missing audio field' }), { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }) } // 4) Google Cloud STT 호출 — placeholder // // 실제 구현 시: // const audioBytes = new Uint8Array(await audio.arrayBuffer()) // const base64Audio = btoa(String.fromCharCode(...audioBytes)) // const gcpKey = Deno.env.get('GOOGLE_CLOUD_STT_KEY')! // const response = await fetch( // `https://speech.googleapis.com/v1/speech:recognize?key=${gcpKey}`, // { // method: 'POST', // headers: { 'Content-Type': 'application/json' }, // body: JSON.stringify({ // config: { // encoding: 'LINEAR16', // sampleRateHertz: sampleRate, // languageCode, // enableAutomaticPunctuation: true // }, // audio: { content: base64Audio } // }) // } // ) // const data = await response.json() // const transcript = data.results?.[0]?.alternatives?.[0]?.transcript ?? '' // const confidence = data.results?.[0]?.alternatives?.[0]?.confidence ?? 0 // // 스캐폴딩 단계에서는 placeholder 응답. const placeholder: SttResult = { transcript: '[stt-proxy placeholder — Google Cloud STT not yet wired]', confidence: 0, language_code: languageCode, duration_seconds: (audio.size / sampleRate / 2) // 16-bit mono 가정 } // 5) 쿼터 소비 await consumeQuota(user.id, 'stt_transcribe', serviceClient, 1) return new Response(JSON.stringify(placeholder), { 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' } }) } })