const express = require('express'); const router = express.Router(); const multer = require('multer'); const { whisperClient, litellmClient } = require('../utils/ai'); const { transcribeWithAWS, isAWSTranscribeConfigured } = require('../utils/transcribeAWS'); const { transcribeWithLocal, isLocalWhisperConfigured } = require('../utils/transcribeLocal'); const { transcribeWithGemini, isGoogleSTTConfigured } = require('../utils/transcribeGoogle'); const { authMiddleware } = require('../middleware/auth'); const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 25 * 1024 * 1024 } }); // Provider priority (auto-detect): // TRANSCRIBE_PROVIDER=google → Vertex AI / Gemini (direct, HIPAA eligible) // TRANSCRIBE_PROVIDER=aws → Amazon Transcribe (HIPAA eligible) // TRANSCRIBE_PROVIDER=local → local whisper.cpp / faster-whisper // TRANSCRIBE_PROVIDER=openai → OpenAI Whisper (direct) // TRANSCRIBE_PROVIDER=litellm→ LiteLLM proxy (explicit only — audio routing bugs) // Auto: google > aws > openai function getTranscribeProvider() { var env = process.env.TRANSCRIBE_PROVIDER; if (env === 'google') return 'google'; if (env === 'aws') return 'aws'; if (env === 'local') return 'local'; if (env === 'openai') return 'openai'; if (env === 'litellm') return 'litellm'; // Auto-detect if (isGoogleSTTConfigured()) return 'google'; if (isAWSTranscribeConfigured()) return 'aws'; return 'openai'; } function isTranscribeAvailable() { if (isGoogleSTTConfigured()) return true; if (isLocalWhisperConfigured()) return true; if (isAWSTranscribeConfigured()) return true; if (litellmClient) return true; if (whisperClient) return true; return false; } var provider = getTranscribeProvider(); var medical = process.env.AWS_TRANSCRIBE_MEDICAL === 'true'; var available = isTranscribeAvailable(); console.log('🎙️ Transcribe provider:', provider + (provider === 'aws' && medical ? ' (Medical)' : '') + (provider === 'google' ? ' (model: ' + (process.env.GOOGLE_STT_MODEL || 'gemini-2.0-flash') + ')' : '') + (available ? '' : ' (NOT CONFIGURED — browser speech only)')); router.get('/transcribe/status', authMiddleware, (req, res) => { res.json({ available: available, provider: available ? provider : 'none' }); }); router.post('/transcribe', authMiddleware, upload.single('audio'), async (req, res) => { try { if (!req.file) return res.status(400).json({ error: 'No audio' }); var startTime = Date.now(); var fileSize = req.file.size; // Get user's preferred STT model (if set) var db = require('../db/database'); var userPrefs = await db.get('SELECT stt_model FROM users WHERE id = ?', [req.user.id]); var userModel = userPrefs?.stt_model; console.log('[Transcribe] Received ' + (fileSize / 1024).toFixed(0) + 'KB audio (' + (req.file.mimetype || 'unknown') + ') via ' + provider + (userModel ? ' (user model: ' + userModel + ')' : '')); if (provider === 'google') { var model = userModel || process.env.GOOGLE_STT_MODEL || 'gemini-2.0-flash'; var text = await transcribeWithGemini(req.file.buffer, req.file.mimetype || 'audio/webm', model); console.log('[Transcribe] Google/' + model + ' done in ' + (Date.now() - startTime) + 'ms'); return res.json({ success: true, text: text, provider: 'google-' + model, duration: Date.now() - startTime }); } if (provider === 'local') { var text = await transcribeWithLocal(req.file.buffer, req.file.mimetype || 'audio/webm'); console.log('[Transcribe] Local done in ' + (Date.now() - startTime) + 'ms'); return res.json({ success: true, text: text, provider: 'local-whisper', duration: Date.now() - startTime }); } if (provider === 'aws') { if (!isAWSTranscribeConfigured()) return res.status(400).json({ error: 'AWS Transcribe not configured.' }); var text = await transcribeWithAWS(req.file.buffer, req.file.mimetype || 'audio/webm'); console.log('[Transcribe] AWS done in ' + (Date.now() - startTime) + 'ms'); return res.json({ success: true, text: text, provider: 'aws-transcribe', duration: Date.now() - startTime }); } if (provider === 'litellm') { if (!process.env.LITELLM_API_BASE) return res.status(400).json({ error: 'LITELLM_API_BASE not set.' }); // LiteLLM /audio/transcriptions does NOT support Vertex AI Chirp (Unmapped provider error). // Instead, use /v1/chat/completions with a Gemini model — Gemini understands audio natively // via base64 inline data. Set LITELLM_STT_MODEL to your Gemini model name in LiteLLM. var sttModel = userModel || process.env.LITELLM_STT_MODEL || 'gemini-2.0-flash'; var axios = require('axios'); var base64audio = req.file.buffer.toString('base64'); var mimeType = req.file.mimetype || 'audio/webm'; var base = process.env.LITELLM_API_BASE.replace(/\/+$/, ''); var headers = { 'Content-Type': 'application/json' }; if (process.env.LITELLM_API_KEY) headers['Authorization'] = 'Bearer ' + process.env.LITELLM_API_KEY; var sttResp = await axios.post(base + '/v1/chat/completions', { model: sttModel, messages: [{ role: 'user', content: [ { type: 'input_audio', input_audio: { data: base64audio, format: mimeType.split('/')[1] || 'webm' } }, { type: 'text', text: 'Transcribe this audio of a medical encounter. Output the spoken words only, exactly as heard. No commentary, no formatting.' } ] }] }, { headers: headers, timeout: 120000 }); var text = ''; if (sttResp.data && sttResp.data.choices && sttResp.data.choices[0]) { var msg = sttResp.data.choices[0].message; text = (msg && msg.content) ? msg.content : ''; } console.log('[Transcribe] LiteLLM/' + sttModel + ' done in ' + (Date.now() - startTime) + 'ms'); return res.json({ success: true, text: text.trim(), provider: 'litellm-gemini', duration: Date.now() - startTime }); } // OpenAI Whisper (direct) if (!whisperClient) return res.status(400).json({ error: 'Whisper not configured. Set OPENAI_API_KEY.' }); var file = new File([req.file.buffer], 'audio.webm', { type: req.file.mimetype || 'audio/webm' }); var result = await whisperClient.audio.transcriptions.create({ file, model: 'whisper-1', language: 'en', response_format: 'text', prompt: 'Medical patient encounter. Pediatric. Clinical terms, diagnoses, medications.' }); var text = typeof result === 'string' ? result : result.text; console.log('[Transcribe] Whisper done in ' + (Date.now() - startTime) + 'ms'); res.json({ success: true, text: text, provider: 'openai-whisper', duration: Date.now() - startTime }); } catch (err) { var detail = err.response && err.response.data ? JSON.stringify(err.response.data).substring(0, 500) : err.message; console.error('[Transcribe] Error (' + provider + '):', detail); res.status(500).json({ error: detail }); } }); module.exports = router;