feat(presenter): replace Web Speech API with Piper TTS for high-quality voice

- New API route /api/presenter/tts proxies to compliance-tts-service
- usePresenterMode now uses Audio element with Piper-generated MP3
- Client-side audio caching (text hash → blob URL) avoids re-synthesis
- Graceful fallback to word-count timer if TTS service unavailable
- Add TTS_SERVICE_URL env var to pitch-deck Docker config

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-03-20 12:23:37 +01:00
parent bcbceba31c
commit ddabda6f05
3 changed files with 150 additions and 79 deletions

View File

@@ -0,0 +1,46 @@
import { NextRequest, NextResponse } from 'next/server'
const TTS_SERVICE_URL = process.env.TTS_SERVICE_URL || 'http://compliance-tts-service:8095'
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { text, language = 'de' } = body
if (!text || typeof text !== 'string') {
return NextResponse.json({ error: 'Text is required' }, { status: 400 })
}
const res = await fetch(`${TTS_SERVICE_URL}/synthesize-direct`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text, language }),
signal: AbortSignal.timeout(30000),
})
if (!res.ok) {
const errorText = await res.text()
console.error('TTS service error:', res.status, errorText)
return NextResponse.json(
{ error: `TTS service error (${res.status})` },
{ status: 502 }
)
}
const audioBuffer = await res.arrayBuffer()
return new NextResponse(audioBuffer, {
headers: {
'Content-Type': 'audio/mpeg',
'Cache-Control': 'public, max-age=86400', // Cache 24h — texts are static
'X-TTS-Cache': res.headers.get('X-TTS-Cache') || 'unknown',
},
})
} catch (error) {
console.error('TTS proxy error:', error)
return NextResponse.json(
{ error: 'TTS service not reachable' },
{ status: 503 }
)
}
}