// ============================================================ // Transcribe + audio-backup helpers — port of public/js/app.js // transcribeAudio() / _serverTranscribe() / saveAudioBackup(). // // Server-first via /api/transcribe (cookie auth — same-origin). // On failure, blob is saved to /api/audio-backups so the user can // retry from Settings → Audio Backups (or vanilla Audio Backups page). // IndexedDB fallback preserved from vanilla audioBackup.js. // ============================================================ export interface TranscribeResult { success: boolean; text?: string; provider?: string; duration?: number; noProvider?: boolean; error?: string; } let transcribeAvailable: boolean | null = null; let transcribeProvider = 'none'; export async function checkTranscribeStatus(): Promise { try { const r = await fetch('/api/transcribe/status', { credentials: 'include' }); const data = await r.json(); transcribeAvailable = !!data.available; transcribeProvider = data.provider || 'none'; } catch { transcribeAvailable = false; } } export function isTranscribeAvailable(): boolean | null { return transcribeAvailable; } export function getTranscribeProvider(): string { return transcribeProvider; } export async function transcribeAudio(blob: Blob, module = 'encounter'): Promise { if (transcribeAvailable === null) await checkTranscribeStatus(); if (transcribeAvailable === false) { return { success: false, noProvider: true, error: 'No transcription API configured — using live transcript' }; } const form = new FormData(); form.append('audio', blob, 'audio.webm'); try { const r = await fetch('/api/transcribe', { method: 'POST', credentials: 'include', body: form }); const data: TranscribeResult = await r.json(); if (!data.success && blob.size > 0) { // Best-effort: save the blob so the user can retry later. saveAudioBackup(blob, module + '-failed-transcription').catch(() => { /* ignore */ }); } return data; } catch (e) { saveAudioBackup(blob, module + '-failed-transcription').catch(() => { /* ignore */ }); return { success: false, error: (e as Error).message }; } } // ── Audio backup (server-first, IndexedDB fallback) ── export async function saveAudioBackup(blob: Blob, module: string): Promise { // Server first. try { const form = new FormData(); form.append('audio', blob, 'audio.webm'); form.append('module', module); const r = await fetch('/api/audio-backups', { method: 'POST', credentials: 'include', body: form }); const data = await r.json(); if (data.success && data.id) return data.id; } catch { /* fall through */ } // IndexedDB fallback. return saveToIndexedDB(blob, module); } const DB_NAME = 'PedScribeAudioBackup'; const STORE = 'recordings'; let _db: IDBDatabase | null = null; function openDB(): Promise { if (_db) return Promise.resolve(_db); return new Promise((resolve, reject) => { const req = indexedDB.open(DB_NAME, 1); req.onupgradeneeded = (e) => { const db = (e.target as IDBOpenDBRequest).result; if (!db.objectStoreNames.contains(STORE)) { const store = db.createObjectStore(STORE, { keyPath: 'id', autoIncrement: true }); store.createIndex('timestamp', 'timestamp', { unique: false }); } }; req.onsuccess = () => { _db = req.result; resolve(_db); }; req.onerror = () => reject(new Error('IndexedDB open failed')); }); } function saveToIndexedDB(blob: Blob, module: string): Promise { return openDB().then((db) => new Promise((resolve, reject) => { const tx = db.transaction(STORE, 'readwrite'); const store = tx.objectStore(STORE); const req = store.add({ blob, module, timestamp: Date.now(), size: blob.size, mimeType: blob.type }); req.onsuccess = () => resolve(req.result as number); req.onerror = () => reject(new Error('Failed to save audio backup')); })).catch(() => null); }