diff --git a/bridge/server.py b/bridge/server.py index 50e24f6..4a54172 100644 --- a/bridge/server.py +++ b/bridge/server.py @@ -75,6 +75,11 @@ VAD_ENABLED = os.environ.get("VAD_ENABLED", "1") not in ("0", "false", "False") VAD_THRESHOLD = float(os.environ.get("VAD_THRESHOLD", "0.4")) VAD_MIN_SPEECH_MS = int(os.environ.get("VAD_MIN_SPEECH_MS", "200")) +# Lock STT to a single language (this deployment is Korean-only). Skipping +# Whisper's language auto-detect both fixes occasional mis-detection (e.g. a +# Korean phrase decoded as Chinese) and shaves a little latency. Empty = auto. +STT_LANGUAGE = os.environ.get("STT_LANGUAGE", "ko").strip() or None + # TTS engine: "melo" (MeloTTS Korean speaker, the warm worker) is the primary # voice; Piper is kept as a fallback if the worker is unreachable. Set # TTS_ENGINE=piper to disable MeloTTS entirely. @@ -208,7 +213,7 @@ def transcribe(wav_bytes: bytes) -> dict: print("[bridge] no speech detected (VAD) — skipping STT", flush=True) return {"text": "", "language": None} - segments, info = _whisper.transcribe(audio, beam_size=1) + segments, info = _whisper.transcribe(audio, beam_size=1, language=STT_LANGUAGE) # Second line of defence: drop non-speech / hallucinated segments by # Whisper's own no_speech_prob. The no_speech_prob hard cutoff (plus the VAD # pre-gate above) is what rejects noise/hallucinations. The avg_logprob @@ -446,7 +451,10 @@ def http_converse_stream(): broadcasting = _coerce_bool(request.args.get("broadcasting")) def gen(): + import time + t0 = time.monotonic() stt = transcribe(raw) + t_stt = time.monotonic() transcript = stt.get("text", "") if not transcript: yield json.dumps({"type": "meta", "transcript": "", "language": stt.get("language"), @@ -454,6 +462,7 @@ def http_converse_stream(): yield json.dumps({"type": "end"}) + "\n" return result = think(transcript, stt.get("language"), broadcasting) + t_think = time.monotonic() reply = result.get("reply", "") yield json.dumps({ "type": "meta", @@ -463,8 +472,11 @@ def http_converse_stream(): "error": result.get("error"), "broadcast_action": result.get("broadcast_action"), }) + "\n" + tts_total = 0.0 for seq, sentence in enumerate(split_sentences(reply)): + ts = time.monotonic() audio = synthesize(sentence) + tts_total += time.monotonic() - ts if audio: yield json.dumps({ "type": "audio", @@ -472,6 +484,12 @@ def http_converse_stream(): "audio_b64": base64.b64encode(audio).decode("ascii"), }) + "\n" yield json.dumps({"type": "end"}) + "\n" + print( + f"[bridge] ⏱️ turn stt={t_stt - t0:.1f}s think(LLM)={t_think - t_stt:.1f}s " + f"tts={tts_total:.1f}s total={time.monotonic() - t0:.1f}s replylen={len(reply)} " + f"transcript={transcript[:40]!r}", + flush=True, + ) return Response(stream_with_context(gen()), mimetype="application/x-ndjson")