diff --git a/backtalk/config.py b/backtalk/config.py index d59f7f2..6b6571a 100644 --- a/backtalk/config.py +++ b/backtalk/config.py @@ -131,6 +131,8 @@ # language pipeline (a=American, b=British, e/f/h/i/j/p/z = other # languages), so keep voice and accent matched. "voice": "bm_lewis", + # Device for local Kokoro TTS pipeline: "cpu" (default) or "cuda" + "tts_device": "cpu", # Speech recognition (faster-whisper, local, free). # Models: tiny.en / base.en / small.en / medium.en — small.en is the # accuracy/speed sweet spot on a normal machine. diff --git a/backtalk/ears.py b/backtalk/ears.py index ad52343..6cb9b66 100644 --- a/backtalk/ears.py +++ b/backtalk/ears.py @@ -289,12 +289,13 @@ def warm(): verbose=None) _model, _backend = repo, "mlx" else: - from faster_whisper import WhisperModel want = CFG["stt_device"] + compute = CFG["stt_compute"] log(f"[ears] loading {CFG['stt_model']} " - f"({want}/{CFG['stt_compute']})...") + f"({want}/{compute})...") + from faster_whisper import WhisperModel _model = WhisperModel(CFG["stt_model"], device=want, - compute_type=CFG["stt_compute"]) + compute_type=compute) # PROVE the device before the greeting, not at the first # spoken sentence. WhisperModel CONSTRUCTS perfectly well # against a GPU it cannot actually use: "auto" picks CUDA @@ -418,6 +419,12 @@ def record_held(is_held, max_s: float = 60.0, min_s: float = 0.25) -> str | None frames.append(block[:, 0].copy()) if len(frames) * FRAME_MS / 1000 < min_s: return None + try: + from backtalk.signals import set_state + set_state("thinking") + except Exception: + pass + print("[ptt] transcribing...", flush=True) return transcribe(np.concatenate(frames)) diff --git a/backtalk/main.py b/backtalk/main.py index a7b85d1..6403a81 100644 --- a/backtalk/main.py +++ b/backtalk/main.py @@ -679,7 +679,7 @@ async def amain(): loop = asyncio.get_event_loop() # Warm the engines while the greeting plays: the STT model load and # the brain's prompt-cache toll both hide behind the spoken line. - loop.run_in_executor(None, warm_ears) + warm_ears_fut = loop.run_in_executor(None, warm_ears) # THE BRAIN CONNECT, guarded. This is the one startup step that # needs a signed-in Claude Code, internet, and available usage. # When it fails or hangs, the mouth still works, so SAY SO instead @@ -707,6 +707,11 @@ async def _warmup(): mouth.wait_done(timeout=30) raise SystemExit(1) log("[backtalk] brain warm") + try: + await asyncio.wait_for(asyncio.shield(warm_ears_fut), 60) + log("[backtalk] ears warm and ready") + except Exception as e: + log(f"[ears] warmup error: {e!r}") # the hidden warmup ping is plumbing, not conversation brain.session.update(turns=0, out_tokens=0, in_tokens=0, cost=0.0) # a configured effort level applies at launch (saved by the spoken diff --git a/backtalk/mouth.py b/backtalk/mouth.py index c3bc4f5..b7428f7 100644 --- a/backtalk/mouth.py +++ b/backtalk/mouth.py @@ -171,8 +171,9 @@ def warm(): lang = (CFG["voice"] or "bm_lewis")[0] log(f"[mouth] loading kokoro (lang '{lang}', " f"voice {CFG['voice']})...") - _pipe = KPipeline(lang_code=lang) - log("[mouth] voice ready") + device = CFG.get("tts_device", "cpu") + _pipe = KPipeline(lang_code=lang, device=device) + log(f"[mouth] voice ready ({device})") return _pipe