feat(release): prepare 1.1.0 candidate
This commit is contained in:
parent
5a34f66981
commit
5205dcdfa9
736 changed files with 115667 additions and 12203 deletions
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue