Same CJS-compatible pattern as batch 1 (import express = require;
const {...} = require for internal utils; export = router). No
behavior change — TS only strips annotations during compile.
AI route touchpoints:
/api/generate-hpi-encounter, /generate-hpi-dictation → hpi.ts
/api/generate-soap → soap.ts
/api/refine, /shorten, /clarify → refine.ts
/api/text-to-speech → tts.ts
/api/transcribe, /transcribe/status → transcribe.ts
All five handlers retain identical request/response shapes; the
shared/types.ts contract was defined on day 2 from these very files.
One fetch() typing fix: the transcribe route's LiteLLM branch uses
DOM fetch, but the file imports Express's Response type, so the
`.then((r: Response) => ...)` annotation was shadowing the global
Response. Removed the explicit annotation — tsc infers correctly.
Progress: 9 of 54 files migrated. tsc --noEmit green.
101 lines
5.2 KiB
TypeScript
101 lines
5.2 KiB
TypeScript
import express = require('express');
|
|
import type { Request, Response } from 'express';
|
|
const axios = require('axios');
|
|
const { synthesizeWithGoogleTTS, isGoogleTTSConfigured } = require('../utils/ttsGoogle');
|
|
const { authMiddleware } = require('../middleware/auth');
|
|
const logger = require('../utils/logger');
|
|
const { gatewayUrl } = require('../utils/errors');
|
|
|
|
const router = express.Router();
|
|
|
|
// Provider priority (auto-detect):
|
|
// TTS_PROVIDER=google → Google Cloud TTS direct (HIPAA eligible)
|
|
// TTS_PROVIDER=litellm → LiteLLM proxy
|
|
// TTS_PROVIDER=elevenlabs → ElevenLabs (not HIPAA)
|
|
// Auto: litellm > google > elevenlabs
|
|
function getTTSProvider(): string {
|
|
const env = process.env.TTS_PROVIDER;
|
|
if (env === 'google') return 'google';
|
|
if (env === 'litellm') return 'litellm';
|
|
if (env === 'elevenlabs') return 'elevenlabs';
|
|
if (process.env.LITELLM_API_BASE) return 'litellm';
|
|
if (isGoogleTTSConfigured()) return 'google';
|
|
if (process.env.ELEVENLABS_API_KEY) return 'elevenlabs';
|
|
return 'none';
|
|
}
|
|
|
|
const ttsProvider = getTTSProvider();
|
|
console.log('🔊 TTS provider:', ttsProvider +
|
|
(ttsProvider === 'google' ? ' (voice: ' + (process.env.GOOGLE_TTS_VOICE || 'en-US-Journey-F') + ')' : ''));
|
|
|
|
router.post('/text-to-speech', authMiddleware, async (req: Request, res: Response) => {
|
|
try {
|
|
const text = (req.body.text || '').substring(0, 5000);
|
|
if (!text) return res.status(400).json({ error: 'No text provided' });
|
|
|
|
const db = require('../db/database');
|
|
const userPrefs = await db.get('SELECT tts_voice FROM users WHERE id = ?', [req.user!.id]);
|
|
const userVoice = userPrefs?.tts_voice;
|
|
let adminVoice: string = (await db.getSetting('tts.voice')) || '';
|
|
let adminModel: string = (await db.getSetting('tts.model')) || '';
|
|
if (!adminModel && adminVoice && (adminVoice.indexOf('/') !== -1 || /^(openai-tts|tts-1|elevenlabs|vertex-gemini.*tts|openai-gpt.*tts)/i.test(adminVoice))) { adminModel = adminVoice; adminVoice = ''; }
|
|
|
|
const VERTEX_VOICES = ['Puck', 'Charon', 'Kore', 'Fenrir', 'Aoede', 'Leda', 'Orus', 'Zephyr'];
|
|
const resolvedVoice: string = userVoice || adminVoice || '';
|
|
if (resolvedVoice) {
|
|
if (VERTEX_VOICES.indexOf(resolvedVoice) !== -1) adminModel = 'vertex-gemini-2.5-flash-tts';
|
|
else if (resolvedVoice.length > 15 && /^[a-zA-Z0-9]+$/.test(resolvedVoice)) adminModel = 'elevenlabs-v3';
|
|
}
|
|
|
|
if (ttsProvider === 'google') {
|
|
const voice = resolvedVoice || process.env.GOOGLE_TTS_VOICE || 'en-US-Journey-F';
|
|
const buffer = await synthesizeWithGoogleTTS(text, voice);
|
|
res.set('Content-Type', 'audio/mpeg');
|
|
res.set('X-TTS-Provider', 'google-tts/' + voice);
|
|
logger.audit(req.user!.id, 'text_to_speech', 'TTS generated', req, { category: 'clinical' });
|
|
return res.send(buffer);
|
|
}
|
|
|
|
if (ttsProvider === 'litellm') {
|
|
if (!process.env.LITELLM_API_BASE) return res.status(400).json({ error: 'LITELLM_API_BASE not set.' });
|
|
const rawModel = adminModel || process.env.LITELLM_TTS_MODEL || 'tts-1';
|
|
const ttsModel = rawModel.indexOf('/') !== -1 || rawModel.indexOf('-tts-') !== -1 || rawModel.startsWith('openai-') || rawModel.startsWith('elevenlabs-') || rawModel.startsWith('gemini-') || rawModel.startsWith('vertex-') ? rawModel : 'openai/' + rawModel;
|
|
const ttsVoice = resolvedVoice || process.env.LITELLM_TTS_VOICE || 'alloy';
|
|
const ttsHeaders: Record<string, string> = { 'Content-Type': 'application/json' };
|
|
if (process.env.LITELLM_API_KEY) ttsHeaders['Authorization'] = 'Bearer ' + process.env.LITELLM_API_KEY;
|
|
const ttsResp = await axios.post(gatewayUrl('/audio/speech'),
|
|
{ model: ttsModel, voice: ttsVoice, input: text },
|
|
{ headers: ttsHeaders, responseType: 'arraybuffer', timeout: 60000 }
|
|
);
|
|
res.set('Content-Type', 'audio/mpeg');
|
|
res.set('X-TTS-Provider', 'litellm/' + ttsModel);
|
|
logger.audit(req.user!.id, 'text_to_speech', 'TTS generated', req, { category: 'clinical' });
|
|
return res.send(Buffer.from(ttsResp.data));
|
|
}
|
|
|
|
if (ttsProvider === 'elevenlabs') {
|
|
if (!process.env.ELEVENLABS_API_KEY) return res.status(400).json({ error: 'ElevenLabs not configured' });
|
|
const resp = await axios({
|
|
method: 'POST',
|
|
url: 'https://api.elevenlabs.io/v1/text-to-speech/pNInz6obpgDQGcFmaJgB',
|
|
headers: { 'xi-api-key': process.env.ELEVENLABS_API_KEY, 'Content-Type': 'application/json' },
|
|
data: { text, model_id: 'eleven_turbo_v2_5', voice_settings: { stability: 0.5, similarity_boost: 0.75 } },
|
|
responseType: 'arraybuffer',
|
|
});
|
|
res.set('Content-Type', 'audio/mpeg');
|
|
res.set('X-TTS-Provider', 'elevenlabs');
|
|
logger.audit(req.user!.id, 'text_to_speech', 'TTS generated', req, { category: 'clinical' });
|
|
return res.send(Buffer.from(resp.data));
|
|
}
|
|
|
|
res.status(400).json({ error: 'TTS not configured. Set GOOGLE_VERTEX_PROJECT, LITELLM_API_BASE, or ELEVENLABS_API_KEY.' });
|
|
} catch (err: any) {
|
|
const detail = err.response && err.response.data
|
|
? JSON.stringify(err.response.data).substring(0, 500)
|
|
: err.message;
|
|
console.error('[TTS] Error (' + ttsProvider + '):', detail);
|
|
res.status(500).json({ error: 'TTS failed: ' + detail });
|
|
}
|
|
});
|
|
|
|
export = router;
|