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)