/** * Voice Recordings API routes * Save audio recordings for training custom speech models */ import { db } from '../db' import { RECORDINGS_DIR } from '../config' import { mkdir } from 'node:fs/promises' import { existsSync } from 'node:fs' import { join } from 'node:path' // Ensure recordings directory exists async function ensureRecordingsDir() { if (!existsSync(RECORDINGS_DIR)) { await mkdir(RECORDINGS_DIR, { recursive: true }) } } export async function handleRecordingsRoutes(req: Request): Promise { const url = new URL(req.url) const path = url.pathname // GET /api/recordings - List all recordings if (path === '/api/recordings' && req.method === 'GET') { const recordings = db.query(` SELECT * FROM voice_recordings ORDER BY created_at DESC `).all() return Response.json({ success: true, recordings, count: recordings.length }) } // POST /api/recordings - Save a new recording if (path === '/api/recordings' && req.method === 'POST') { try { await ensureRecordingsDir() const body = await req.json() const { audio, transcription, microphone, duration_ms } = body if (!audio) { return Response.json({ success: false, error: 'No audio data provided' }, { status: 400 }) } // Decode base64 audio const audioBuffer = Buffer.from(audio, 'base64') // Generate filename with timestamp const timestamp = new Date().toISOString().replace(/[:.]/g, '-') const filename = `recording-${timestamp}.webm` const filepath = join(RECORDINGS_DIR, filename) // Save audio file await Bun.write(filepath, audioBuffer) // Save metadata to database const result = db.run(` INSERT INTO voice_recordings (filename, transcription, duration_ms, microphone, file_size) VALUES (?, ?, ?, ?, ?) `, [filename, transcription || '', duration_ms || 0, microphone || 'unknown', audioBuffer.length]) return Response.json({ success: true, id: result.lastInsertRowid, filename, size: audioBuffer.length, message: 'Recording saved' }) } catch (e: any) { console.error('[Recordings] Error saving:', e) return Response.json({ success: false, error: e.message }, { status: 500 }) } } // GET /api/recordings/:id - Get recording metadata const recordingMatch = path.match(/^\/api\/recordings\/(\d+)$/) if (recordingMatch && req.method === 'GET') { const id = recordingMatch[1] const recording = db.query('SELECT * FROM voice_recordings WHERE id = ?').get(id) if (!recording) { return Response.json({ success: false, error: 'Recording not found' }, { status: 404 }) } return Response.json({ success: true, recording }) } // GET /api/recordings/:id/audio - Download audio file const audioMatch = path.match(/^\/api\/recordings\/(\d+)\/audio$/) if (audioMatch && req.method === 'GET') { const id = audioMatch[1] const recording = db.query('SELECT filename FROM voice_recordings WHERE id = ?').get(id) as { filename: string } | null if (!recording) { return Response.json({ success: false, error: 'Recording not found' }, { status: 404 }) } const filepath = join(RECORDINGS_DIR, recording.filename) if (!existsSync(filepath)) { return Response.json({ success: false, error: 'Audio file not found' }, { status: 404 }) } const file = Bun.file(filepath) return new Response(file, { headers: { 'Content-Type': 'audio/webm', 'Content-Disposition': `attachment; filename="${recording.filename}"` } }) } // DELETE /api/recordings/:id - Delete a recording const deleteMatch = path.match(/^\/api\/recordings\/(\d+)$/) if (deleteMatch && req.method === 'DELETE') { const id = deleteMatch[1] const recording = db.query('SELECT filename FROM voice_recordings WHERE id = ?').get(id) as { filename: string } | null if (!recording) { return Response.json({ success: false, error: 'Recording not found' }, { status: 404 }) } // Delete file const filepath = join(RECORDINGS_DIR, recording.filename) if (existsSync(filepath)) { await Bun.write(filepath, '') // Clear file const file = Bun.file(filepath) // Note: Bun doesn't have a direct unlink, using rm const { rm } = await import('node:fs/promises') await rm(filepath) } // Delete from database db.run('DELETE FROM voice_recordings WHERE id = ?', [id]) return Response.json({ success: true, message: 'Recording deleted' }) } // GET /api/recordings/stats - Get recording statistics if (path === '/api/recordings/stats' && req.method === 'GET') { const stats = db.query(` SELECT COUNT(*) as total_recordings, SUM(file_size) as total_size, SUM(duration_ms) as total_duration_ms, AVG(duration_ms) as avg_duration_ms FROM voice_recordings `).get() as any return Response.json({ success: true, stats: { totalRecordings: stats.total_recordings || 0, totalSizeBytes: stats.total_size || 0, totalSizeMB: ((stats.total_size || 0) / 1024 / 1024).toFixed(2), totalDurationMs: stats.total_duration_ms || 0, totalDurationMinutes: ((stats.total_duration_ms || 0) / 1000 / 60).toFixed(2), avgDurationMs: Math.round(stats.avg_duration_ms || 0) } }) } return null }