From 0cc01c1450e29dbb03062771c56ca504d36aa153 Mon Sep 17 00:00:00 2001 From: Marius Mutu Date: Wed, 27 May 2026 14:44:13 +0000 Subject: [PATCH] =?UTF-8?q?feat(voice):=20Pas=2010=20=E2=80=94=20eco=20doc?= =?UTF-8?q?tor=20voice=20stack=20checks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cli.py: +101 / -0, append 9 checks după existing 15: 1. libopus loaded by discord.py (load_default fallback) 2. ffmpeg in PATH 3. Supertonic TTS reachable :7788 (5s timeout POST) 4. faster-whisper importable (no model load — too slow for doctor) 5. silero-vad importable 6. discord.ext.voice_recv importable (vendor package guard) 7-9. assets/voice/{thinking,beep_200ms,mhm}.wav exist + size thresholds Helper _voice_doctor_checks() returns list[tuple[str, bool]] matching doctor's reporting style. Replicates voice_setup.py logic in doctor format (voice_setup uses ANSI colors directly, doctor uses (label, ok) tuples — separate Option B implementation). Graceful ImportError handling per check — never crashes the rest of eco doctor. Exit code 1 corectly surfaces missing libopus (Discord voice silent without it). Use `sudo apt install -y libopus0` to clear. Co-Authored-By: Claude Opus 4.7 (1M context) --- cli.py | 101 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/cli.py b/cli.py index c5419c2..348e7c3 100755 --- a/cli.py +++ b/cli.py @@ -114,6 +114,104 @@ def _load_sessions_file() -> dict: return {} +def _voice_doctor_checks() -> list[tuple[str, bool]]: + """Voice-stack health checks (Pas 10). + + Mirrors the logic in tools/voice_setup.py but returns (label, ok) tuples + so they integrate with cmd_doctor's PASS/FAIL output. All checks degrade + gracefully — ImportError on optional voice deps is reported as FAIL, never + raised, so the rest of `eco doctor` is unaffected. + """ + import importlib.util + import json as _json + import urllib.error + import urllib.request + + results: list[tuple[str, bool]] = [] + + # 1. libopus0 loaded by discord.py + try: + import discord + if not discord.opus.is_loaded(): + try: + discord.opus._load_default() + except Exception: + pass + results.append(("libopus loaded (discord.py)", discord.opus.is_loaded())) + except ImportError: + results.append(("libopus loaded (discord.py)", False)) + except Exception: + results.append(("libopus loaded (discord.py)", False)) + + # 2. ffmpeg in PATH + results.append(("ffmpeg in PATH", shutil.which("ffmpeg") is not None)) + + # 3. Supertonic TTS reachable at http://127.0.0.1:7788/ + supertonic_url = "http://127.0.0.1:7788/v1/audio/speech" + supertonic_ok = False + try: + payload = _json.dumps({ + "model": "supertonic-3", + "input": "test", + "voice": "M2", + "response_format": "wav", + "lang": "ro", + }).encode("utf-8") + req = urllib.request.Request( + supertonic_url, + data=payload, + headers={"Content-Type": "application/json"}, + method="POST", + ) + with urllib.request.urlopen(req, timeout=5) as resp: + supertonic_ok = resp.status == 200 + except (urllib.error.URLError, ConnectionError, OSError): + supertonic_ok = False + except Exception: + supertonic_ok = False + results.append(("Supertonic TTS reachable at :7788", supertonic_ok)) + + # 4. faster-whisper importable (don't load model — too slow) + results.append(( + "faster-whisper importable", + importlib.util.find_spec("faster_whisper") is not None, + )) + + # 5. silero-vad importable + results.append(( + "silero-vad importable", + importlib.util.find_spec("silero_vad") is not None, + )) + + # 6. discord.ext.voice_recv importable (vendor package) + voice_recv_ok = False + try: + voice_recv_ok = importlib.util.find_spec("discord.ext.voice_recv") is not None + except (ImportError, ValueError, ModuleNotFoundError): + voice_recv_ok = False + except Exception: + voice_recv_ok = False + results.append(("discord.ext.voice_recv importable", voice_recv_ok)) + + # 7-9. Voice assets present and non-trivial size + voice_assets = [ + ("assets/voice/thinking.wav", 1024), + ("assets/voice/beep_200ms.wav", 512), + ("assets/voice/mhm.wav", 512), + ] + for rel_path, min_bytes in voice_assets: + path = PROJECT_ROOT / rel_path + ok = False + try: + ok = path.exists() and path.stat().st_size > min_bytes + except OSError: + ok = False + label = f"{rel_path} (>{min_bytes}B)" + results.append((label, ok)) + + return results + + def cmd_doctor(args): """Run diagnostic checks.""" import re @@ -227,6 +325,9 @@ def cmd_doctor(args): else: checks.append(("WhatsApp bridge (optional)", True)) + # ---- Voice stack checks (Pas 10) ---- + checks.extend(_voice_doctor_checks()) + # Print results all_pass = True for label, passed in checks: