feat(release): prepare 1.1.0 candidate

This commit is contained in:
Yun Chan 2026-08-29 18:33:45 +09:00
parent 5a34f66981
commit 5205dcdfa9
736 changed files with 115667 additions and 12203 deletions

View file

@ -1,13 +1,11 @@
// packages/api-client/src/transcribe.ts
// D3RO Cloud STT Transcription Unified Client
// 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 // Blob, Binary, or Base64 string
audio: Blob | ArrayBuffer | Uint8Array | string
language?: string
prompt?: string
model?: string
provider?: string
apiBaseUrl?: string
supabaseUrl?: string
anonKey?: string
token?: string
}
@ -17,75 +15,108 @@ export interface TranscribeAudioResult {
language: string
durationSeconds: number
provider: string
modelId: string
latencyMs: number
cost: number
modelId?: string
cost?: number
}
/**
* 전사(STT) API를 호출합니다.
* 사용자는 클라우드 서비스를 통해 자연스럽게 음성을 전사할 수 있으며,
* 관리자에서 설정된 기본 프로바이더(Groq, OpenAI, Deepgram, Google 등)를 통해 자동으로 처리됩니다.
*/
export async function transcribeAudio(params: TranscribeAudioParams): Promise<TranscribeAudioResult> {
const apiBase = (params.apiBaseUrl || process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5000').replace(/\/$/, '')
const url = `${apiBase}/api/stt/transcribe`
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)
}
const headers: Record<string, string> = {}
if (params.token) {
headers['Authorization'] = `Bearer ${params.token}`
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<string, unknown>
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<TranscribeAudioResult> {
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.')
}
// 1. If audio is Blob or ArrayBuffer, send via FormData
let audio: Blob
const makeBlob = Blob as unknown as new (parts: unknown[], options?: { type?: string }) => Blob
if (params.audio instanceof Blob) {
const formData = new FormData()
formData.append('file', params.audio, 'recording.webm')
if (params.language) formData.append('language', params.language)
if (params.prompt) formData.append('prompt', params.prompt)
if (params.model) formData.append('model', params.model)
if (params.provider) formData.append('provider', params.provider)
const res = await fetch(url, {
method: 'POST',
headers,
body: formData,
})
if (!res.ok) {
const errText = await res.text()
throw new Error(`STT transcription failed (${res.status}): ${errText}`)
}
return (await res.json()) as TranscribeAudioResult
} else if (params.audio instanceof ArrayBuffer || params.audio instanceof Uint8Array) {
const blob = new Blob([params.audio as BlobPart], { type: 'audio/webm' })
return transcribeAudio({ ...params, audio: 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') {
// 2. If audio is Base64 string, send JSON payload
const payload = {
audioBase64: params.audio,
language: params.language || 'ko',
initialPrompt: params.prompt,
modelId: params.model,
provider: params.provider,
}
const res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...headers,
},
body: JSON.stringify(payload),
})
if (!res.ok) {
const errText = await res.text()
throw new Error(`STT transcription failed (${res.status}): ${errText}`)
}
return (await res.json()) as TranscribeAudioResult
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)
}