The AudioSocket/media_runtime playback pipeline already supports chunked
TTS streaming (voice_v2_streaming_tts), but every provider inherited the
base TTSProvider.synthesize_chunks(), which just called the blocking
synthesize() and yielded the entire finished audio as a single "chunk" -
so the caller waited for full-utterance synthesis before any playback
could start regardless of the flag.
ElevenLabs is the production default (AI_VOICE_TTS_PROVIDER=elevenlabs in
deployment/docker-compose.server.yml), so give it a real implementation
that POSTs to the /stream endpoint and yields audio as network chunks
arrive, instead of waiting for the whole response body. Chunk boundaries
are re-aligned to whole 16-bit PCM samples so a split sample at a network
read boundary can't corrupt playback. The full synthesized audio is still
written to the on-disk cache afterwards so repeat phrases stay fast and
skip the vendor call entirely, matching the existing synthesize() cache
behavior.
Added test_elevenlabs_tts_provider_streams_chunks_and_caches_full_audio to
cover: chunk splitting mid-sample gets re-aligned, all yielded chunks are
sample-aligned, the full audio round-trips through the cache, and a
cached synthesis is replayed without invoking the streaming endpoint
again.
Verified via tests/test_ai_voice_tts_provider.py (9/9 pass) and a wider
voice/tts-filtered run across the suite: the only failures present are
the same pre-existing, already-documented ones (sales_service test
cross-file isolation ordering, one known persona-prompt assertion) -
identical set to before this change, no new failures.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- docs: longread.md — deep architecture/flow review of the whole
platform (services, event bus reality vs docs, AI/ML stack honesty
check, tech debt inventory)
- ai_orchestrator_service/voice.py: drop _voice_decision_legacy
(unreferenced) and the shadowed first _voice_decision definition
(silently overwritten by the real one, dead code)
- ui/analyst/app.js: drop duplicate dead definitions of
loadSavedAnalyticsViews/saveAnalyticsView/deleteAnalyticsView and
the first loadAnalyticsTrend implementation, all shadowed by later
declarations in the same file; kept the intentional AI-mode
drilldown wrapper layer (openAnalyticsDrilldown/exportAnalyticsDrilldownCsv/etc.)
since that duplication is deliberate delegation, not dead code
- ui/operator/vendor/sip-0.21.2.min.js: remove byte-identical orphaned
duplicate of ui/operator/sip-0.21.2.min.js (unreferenced anywhere)
Verified via full pytest run: identical set of 97 pre-existing
failures before and after (sales_* test-isolation ordering issue and
one known persona-prompt test), no new regressions.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>