feat(voice): Pas 10 — eco doctor voice stack checks
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) <noreply@anthropic.com>
This commit is contained in:
101
cli.py
101
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:
|
||||
|
||||
Reference in New Issue
Block a user