pediatric-ai-scribe-v3/src/routes/transcribe.js
Daniel Onyejesi 4808d08aa7 Fix STT/TTS properly per LiteLLM docs
STT: Vertex AI Chirp not supported via LiteLLM proxy (confirmed by docs).
     Now uses Gemini directly (transcribeGoogle.js) — auto-detected when
     GOOGLE_VERTEX_PROJECT is set, fallback to AWS then OpenAI.

TTS: LiteLLM Vertex TTS DOES work but requires the model_list ALIAS
     (tts-1) not the underlying path (vertex_ai/text-to-speech).
     Also pass voice param — LiteLLM supports Google Cloud voice names.
     Auto-detected when LITELLM_API_BASE is set.
2026-03-30 07:28:47 -04:00

119 lines
5.9 KiB
JavaScript

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;
console.log('[Transcribe] Received ' + (fileSize / 1024).toFixed(0) + 'KB audio (' + (req.file.mimetype || 'unknown') + ') via ' + provider);
if (provider === 'google') {
var text = await transcribeWithGemini(req.file.buffer, req.file.mimetype || 'audio/webm');
console.log('[Transcribe] Google/Gemini done in ' + (Date.now() - startTime) + 'ms');
return res.json({ success: true, text: text, provider: 'google-gemini', 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.' });
var sttModel = process.env.LITELLM_STT_MODEL || 'vertex_ai/chirp';
var FormData = require('form-data');
var axios = require('axios');
var form = new FormData();
form.append('file', req.file.buffer, { filename: 'audio.webm', contentType: req.file.mimetype || 'audio/webm' });
form.append('model', sttModel);
var sttHeaders = Object.assign({}, form.getHeaders());
if (process.env.LITELLM_API_KEY) sttHeaders['Authorization'] = 'Bearer ' + process.env.LITELLM_API_KEY;
var sttResp = await require('axios').post(
process.env.LITELLM_API_BASE.replace(/\/+$/, '') + '/v1/audio/transcriptions',
form, { headers: sttHeaders, timeout: 120000 }
);
var text = (sttResp.data && sttResp.data.text) ? sttResp.data.text : String(sttResp.data || '');
console.log('[Transcribe] LiteLLM (' + sttModel + ') done in ' + (Date.now() - startTime) + 'ms');
return res.json({ success: true, text: text, provider: 'litellm-stt', 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;