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 {}
|
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):
|
def cmd_doctor(args):
|
||||||
"""Run diagnostic checks."""
|
"""Run diagnostic checks."""
|
||||||
import re
|
import re
|
||||||
@@ -227,6 +325,9 @@ def cmd_doctor(args):
|
|||||||
else:
|
else:
|
||||||
checks.append(("WhatsApp bridge (optional)", True))
|
checks.append(("WhatsApp bridge (optional)", True))
|
||||||
|
|
||||||
|
# ---- Voice stack checks (Pas 10) ----
|
||||||
|
checks.extend(_voice_doctor_checks())
|
||||||
|
|
||||||
# Print results
|
# Print results
|
||||||
all_pass = True
|
all_pass = True
|
||||||
for label, passed in checks:
|
for label, passed in checks:
|
||||||
|
|||||||
Reference in New Issue
Block a user