Add CLI subcommands and doctor diagnostics
This commit is contained in:
parent
9c7d7b35b1
commit
ad1af63fac
7 changed files with 385 additions and 13 deletions
159
src/diagnostics.py
Normal file
159
src/diagnostics.py
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from dataclasses import asdict, dataclass
|
||||
from pathlib import Path
|
||||
|
||||
from aiprocess import ensure_model
|
||||
from config import Config, load
|
||||
from desktop import get_desktop_adapter
|
||||
from recorder import resolve_input_device
|
||||
|
||||
|
||||
@dataclass
|
||||
class DiagnosticCheck:
|
||||
id: str
|
||||
ok: bool
|
||||
message: str
|
||||
hint: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class DiagnosticReport:
|
||||
checks: list[DiagnosticCheck]
|
||||
|
||||
@property
|
||||
def ok(self) -> bool:
|
||||
return all(check.ok for check in self.checks)
|
||||
|
||||
def to_json(self) -> str:
|
||||
payload = {"ok": self.ok, "checks": [asdict(check) for check in self.checks]}
|
||||
return json.dumps(payload, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
def run_diagnostics(config_path: str | None) -> DiagnosticReport:
|
||||
checks: list[DiagnosticCheck] = []
|
||||
cfg: Config | None = None
|
||||
|
||||
try:
|
||||
cfg = load(config_path or "")
|
||||
checks.append(
|
||||
DiagnosticCheck(
|
||||
id="config.load",
|
||||
ok=True,
|
||||
message=f"loaded config from {_resolved_config_path(config_path)}",
|
||||
)
|
||||
)
|
||||
except Exception as exc:
|
||||
checks.append(
|
||||
DiagnosticCheck(
|
||||
id="config.load",
|
||||
ok=False,
|
||||
message=f"failed to load config: {exc}",
|
||||
hint="run `aman init --force` to regenerate a default config",
|
||||
)
|
||||
)
|
||||
|
||||
checks.extend(_audio_check(cfg))
|
||||
checks.extend(_hotkey_check(cfg))
|
||||
checks.extend(_injection_backend_check(cfg))
|
||||
checks.extend(_model_check())
|
||||
return DiagnosticReport(checks=checks)
|
||||
|
||||
|
||||
def _audio_check(cfg: Config | None) -> list[DiagnosticCheck]:
|
||||
if cfg is None:
|
||||
return [
|
||||
DiagnosticCheck(
|
||||
id="audio.input",
|
||||
ok=False,
|
||||
message="skipped because config failed to load",
|
||||
hint="fix config.load first",
|
||||
)
|
||||
]
|
||||
input_spec = cfg.recording.input
|
||||
explicit = input_spec is not None and (not isinstance(input_spec, str) or bool(input_spec.strip()))
|
||||
device = resolve_input_device(input_spec)
|
||||
if device is None and explicit:
|
||||
return [
|
||||
DiagnosticCheck(
|
||||
id="audio.input",
|
||||
ok=False,
|
||||
message=f"recording input '{input_spec}' is not resolvable",
|
||||
hint="set recording.input to a valid device index or matching device name",
|
||||
)
|
||||
]
|
||||
if device is None:
|
||||
return [
|
||||
DiagnosticCheck(
|
||||
id="audio.input",
|
||||
ok=True,
|
||||
message="recording input is unset; default system input will be used",
|
||||
)
|
||||
]
|
||||
return [DiagnosticCheck(id="audio.input", ok=True, message=f"resolved recording input to device {device}")]
|
||||
|
||||
|
||||
def _hotkey_check(cfg: Config | None) -> list[DiagnosticCheck]:
|
||||
if cfg is None:
|
||||
return [
|
||||
DiagnosticCheck(
|
||||
id="hotkey.parse",
|
||||
ok=False,
|
||||
message="skipped because config failed to load",
|
||||
hint="fix config.load first",
|
||||
)
|
||||
]
|
||||
try:
|
||||
desktop = get_desktop_adapter()
|
||||
desktop.validate_hotkey(cfg.daemon.hotkey)
|
||||
except Exception as exc:
|
||||
return [
|
||||
DiagnosticCheck(
|
||||
id="hotkey.parse",
|
||||
ok=False,
|
||||
message=f"hotkey '{cfg.daemon.hotkey}' is not available: {exc}",
|
||||
hint="pick another daemon.hotkey such as Super+m",
|
||||
)
|
||||
]
|
||||
return [DiagnosticCheck(id="hotkey.parse", ok=True, message=f"hotkey '{cfg.daemon.hotkey}' is valid")]
|
||||
|
||||
|
||||
def _injection_backend_check(cfg: Config | None) -> list[DiagnosticCheck]:
|
||||
if cfg is None:
|
||||
return [
|
||||
DiagnosticCheck(
|
||||
id="injection.backend",
|
||||
ok=False,
|
||||
message="skipped because config failed to load",
|
||||
hint="fix config.load first",
|
||||
)
|
||||
]
|
||||
return [
|
||||
DiagnosticCheck(
|
||||
id="injection.backend",
|
||||
ok=True,
|
||||
message=f"injection backend '{cfg.injection.backend}' is configured",
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def _model_check() -> list[DiagnosticCheck]:
|
||||
try:
|
||||
model_path = ensure_model()
|
||||
return [DiagnosticCheck(id="model.cache", ok=True, message=f"model is ready at {model_path}")]
|
||||
except Exception as exc:
|
||||
return [
|
||||
DiagnosticCheck(
|
||||
id="model.cache",
|
||||
ok=False,
|
||||
message=f"model is not ready: {exc}",
|
||||
hint="check internet access and writable cache directory",
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def _resolved_config_path(config_path: str | None) -> Path:
|
||||
from constants import DEFAULT_CONFIG_PATH
|
||||
|
||||
return Path(config_path) if config_path else DEFAULT_CONFIG_PATH
|
||||
Loading…
Add table
Add a link
Reference in a new issue