// Transcription of a take's mic audio with Deepgram's pre-recorded API. import fs from 'node:fs/promises'; import path from 'node:path'; import type { Transcript, Word } from '../shared/types.ts'; import { extractAudio } from './media.ts'; import { HttpError, getConfig, getVideo, takeDir, updateVideo, writeJsonAtomic } from './store.ts'; const inFlight = new Set(); export async function transcribeTake(videoId: string, takeId: string) { const key = `${videoId}/${takeId}`; if (inFlight.has(key)) return; const cfg = await getConfig(); if (!cfg.deepgramApiKey) throw new HttpError(400, 'No Deepgram API key configured (see Settings)'); const v = await getVideo(videoId); const take = v.takes.find((t) => t.id === takeId); if (!take) throw new HttpError(404, 'Take not found'); if (take.status !== 'ready') throw new HttpError(409, 'Take is not ready'); if (!take.audio) throw new HttpError(400, 'Take has no audio'); inFlight.add(key); const setStatus = (status: 'pending' | 'done' | 'failed', error?: string) => updateVideo(videoId, (v) => { const t = v.takes.find((t) => t.id === takeId); if (t) t.transcript = { status, updatedAt: new Date().toISOString(), ...(error ? { error } : {}) }; }); try { await setStatus('pending'); } catch (e) { inFlight.delete(key); throw e; } // Run in the background; callers poll the take's transcript status. (async () => { const dir = takeDir(videoId, takeId); const audio = path.join(dir, 'transcribe-audio.ogg'); try { await extractAudio(path.join(dir, `${take.audio}.webm`), audio); const body = await fs.readFile(audio); const url = new URL('https://api.deepgram.com/v1/listen'); url.searchParams.set('model', cfg.deepgramModel || 'nova-3'); url.searchParams.set('smart_format', 'true'); url.searchParams.set('punctuate', 'true'); const res = await fetch(url, { method: 'POST', headers: { Authorization: `Token ${cfg.deepgramApiKey}`, 'Content-Type': 'audio/ogg' }, body, }); if (!res.ok) throw new Error(`Deepgram ${res.status}: ${(await res.text()).slice(0, 300)}`); const json = (await res.json()) as any; const alt = json?.results?.channels?.[0]?.alternatives?.[0] ?? {}; const words: Word[] = (alt.words ?? []).map((w: { word: string; punctuated_word?: string; start: number; end: number }) => ({ word: w.word, text: w.punctuated_word ?? w.word, start: w.start, end: w.end, })); const transcript: Transcript = { takeId, model: cfg.deepgramModel, createdAt: new Date().toISOString(), text: alt.transcript ?? '', words, }; await writeJsonAtomic(path.join(dir, 'transcript.json'), transcript); await setStatus('done'); } catch (e) { console.warn('transcription failed:', (e as Error).message); await setStatus('failed', (e as Error).message).catch(() => {}); } finally { await fs.rm(audio, { force: true }); inFlight.delete(key); } })(); }