From 2573938237d97d9d18bf73fe243b9c1defc97da2 Mon Sep 17 00:00:00 2001 From: Jacob Lasky Date: Sun, 26 Jul 2026 09:22:35 -0400 Subject: [PATCH] fix(flux): TTS round trip sent MP3, which Flux cannot decode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Caught live-probing the deploy, not by the suite. POST /api/tts-transcribe with mode=streaming and a flux-* model returned: Deepgram closed the stream immediately without transcribing (received 1005 (no status received [internal])) Cause: the round trip generates MP3 and lets Deepgram sniff the container. That is correct on /v1/listen and is why no `encoding` is sent to STT. But Flux's accepted list is linear16/linear32/mulaw/alaw/opus/ogg-opus — MP3 is not on it, and /v2/listen answers an undecodable stream by closing the socket rather than by saying so. Measured, same clip, encoding/sample_rate omitted, paced at 1x: mp3 -> 1005 close, nothing transcribed opus / ogg -> transcribed So the round trip now picks the CONTAINER by which endpoint will read it: Ogg-Opus for Flux, MP3 everywhere else. It still sends no `encoding` param to STT, so the constraint that comment protects is intact — the fix is upstream of it, in what gets generated. Batch is deliberately untouched: a flux model on /v1/listen returns V2_MODEL_ON_V1_LISTEN_ENDPOINT, which already says exactly what is wrong. Why the suite missed it: nothing mocked can discover that MP3 is undecodable by a model whose docs do not list its accepted containers next to the TTS encodings. Only the live round trip shows it, which is the argument for probing the deployment rather than trusting a green pipeline. --- app.py | 33 +++++++++++++++++++++++++++++++-- tests/test_app.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/app.py b/app.py index c24816a..96d08ca 100644 --- a/app.py +++ b/app.py @@ -555,6 +555,29 @@ async def _elevenlabs_tts_generate(text: str, voice_id: str, api_key: str) -> by return resp.content +# TTS container for the round trip, chosen by which STT endpoint will read it. +# +# DO NOT send an `encoding` param to STT to go with this. The round trip +# deliberately lets Deepgram sniff the container, and adding `encoding` is the +# exact trap that makes a round trip return empty transcripts. This picks the +# CONTAINER the STT side can decode, which satisfies the same constraint. +# +# MP3 works on /v1/listen and is what every non-Flux run uses. Flux cannot +# decode it at all: verified live, an MP3 stream to /v2/listen closes with +# `received 1005 (no status received)` and transcribes nothing. Its accepted +# list is linear16/linear32/mulaw/alaw/opus/ogg-opus, and Ogg-Opus is the one +# Deepgram TTS can emit that /v2/listen reads with no encoding hint. +TTS_ROUND_TRIP_ENCODING = "mp3" +TTS_ROUND_TRIP_ENCODING_FLUX = "opus" + + +def _tts_round_trip_encoding(stt_params: dict) -> str: + """The TTS container to generate so the chosen STT model can read it back.""" + if is_flux_model(stt_params.get("model")): + return TTS_ROUND_TRIP_ENCODING_FLUX + return TTS_ROUND_TRIP_ENCODING + + async def _stt_batch(audio_bytes: bytes, stt_params: dict, api_key: str) -> dict: """Transcribe audio bytes via Deepgram pre-recorded (batch) API.""" headers = {"Authorization": f"Token {api_key}"} @@ -613,7 +636,10 @@ async def on_message(msg, **kwargs): "POST", "https://api.deepgram.com/v1/speak", headers={**headers, "Content-Type": "application/json"}, - params={"model": tts_model, "encoding": "mp3"}, + params={ + "model": tts_model, + "encoding": _tts_round_trip_encoding(stt_params), + }, json={"text": text}, ) as tts_resp: tts_resp.raise_for_status() @@ -657,7 +683,10 @@ async def _stt_streaming_raw(text: str, tts_model: str, stt_params: dict, api_ke tts_resp = await client.post( "https://api.deepgram.com/v1/speak", headers={**headers, "Content-Type": "application/json"}, - params={"model": tts_model, "encoding": "mp3"}, + params={ + "model": tts_model, + "encoding": _tts_round_trip_encoding(stt_params), + }, json={"text": text}, ) tts_resp.raise_for_status() diff --git a/tests/test_app.py b/tests/test_app.py index 24d7944..37c0182 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -401,3 +401,34 @@ async def check(): pass asyncio.run(check()) + + +# --- TTS round-trip container --------------------------------------------- + + +def test_round_trip_uses_a_container_flux_can_decode(): + """Flux CANNOT decode MP3. Verified live: an MP3 stream to /v2/listen closes + with `received 1005 (no status received)` and transcribes nothing, which is + how the round trip shipped broken for Flux. Ogg-Opus works.""" + import app as app_mod + + assert app_mod._tts_round_trip_encoding({"model": "flux-general-en"}) == "opus" + assert app_mod._tts_round_trip_encoding({"model": "flux-general-multi"}) == "opus" + + +def test_round_trip_still_uses_mp3_everywhere_else(): + """MP3 is what every v1 run has always used; do not churn it.""" + import app as app_mod + + assert app_mod._tts_round_trip_encoding({"model": "nova-3"}) == "mp3" + assert app_mod._tts_round_trip_encoding({}) == "mp3" + + +def test_round_trip_never_sends_an_encoding_param_to_stt(): + """Load-bearing: the round trip lets Deepgram sniff the container, and adding + an STT `encoding` is the documented trap that returns empty transcripts. The + Flux fix changes the CONTAINER GENERATED, never the STT params.""" + from stt.options import Mode, serialize_params + + for model in ("nova-3", "flux-general-en"): + assert "encoding" not in serialize_params({"model": model}, Mode.STREAMING)