mirror of
https://github.com/Alkatrazz24/Funk-lab.git
synced 2026-07-08 09:24:43 +02:00
feat(stt-client): contrôles du service dans le HUD + commandes stt --start/--restart (#35)
Ajoute la gestion du service STT directement depuis l'interface (Réglages → section « Service ») et deux commandes CLI. - CLI : `stt --start` (démarre le service) et `stt --restart` (recharge la config : modèle ASR, wake word…). Garde-fou si le unit n'est pas installé. - HUD : boutons Redémarrer / Mettre à jour / Arrêter. Le HUD tournant DANS le service, les actions sont lancées en process détaché (start_new_session) → le redémarrage va à son terme même quand le process courant est tué (systemd possède le job). « Mettre à jour » = `stt --update` puis `stt --restart`. - bump 0.8.0 → 0.9.0. Validé : py_compile, --start/--restart dans l'aide, et HUD via Playwright (3 boutons émettent service-restart/-update/-stop, toast, aucune erreur JS, rendu conforme). Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
9c335d169b
commit
3b6099ffa0
5 changed files with 139 additions and 2 deletions
|
|
@ -181,12 +181,19 @@ Lancer le HUD + voix automatiquement à l'ouverture de session (écran « Jarvis
|
|||
```bash
|
||||
stt --install-service # écrit ~/.config/systemd/user/stt.service, enable + start
|
||||
stt --uninstall-service # retire le service
|
||||
stt --start # démarre le service
|
||||
stt --restart # redémarre — recharge la config (modèle ASR, wake word…)
|
||||
stt --stop # éteint l'instance en cours (service systemd OU process lancé à la main)
|
||||
```
|
||||
|
||||
Le unit est lié à `graphical-session.target` (démarre avec la session graphique, s'arrête avec).
|
||||
Suivi : `systemctl --user status|restart stt`, `journalctl --user -u stt -f`.
|
||||
|
||||
Ces actions sont aussi dispo **depuis le HUD** (Réglages → section « Service ») : **Redémarrer**,
|
||||
**Mettre à jour** (`stt --update` puis `--restart`), **Arrêter**. Le HUD tournant *dans* le service,
|
||||
ces actions sont lancées en **process détaché** (`start_new_session`) → le redémarrage va à son terme
|
||||
même quand le process courant est tué (systemd possède le job).
|
||||
|
||||
`stt --stop` arrête proprement : si le service `stt.service` (systemd --user) est actif il fait
|
||||
`systemctl --user stop`, sinon il envoie un `SIGTERM` aux process `stt` vocaux trouvés via `/proc`
|
||||
(en s'excluant lui-même et les sous-commandes utilitaires `--update`/`--version`/…).
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[project]
|
||||
name = "stt"
|
||||
version = "0.8.0"
|
||||
version = "0.9.0"
|
||||
description = "STT — client vocal Jarvis du homelab Funk (voix + HUD, parle au STT-server)"
|
||||
requires-python = ">=3.11"
|
||||
readme = "README.md"
|
||||
|
|
|
|||
|
|
@ -109,6 +109,47 @@ def _cmd_uninstall_service() -> int:
|
|||
return 0
|
||||
|
||||
|
||||
def _service_installed() -> bool:
|
||||
"""Le unit systemd --user stt.service est-il installé ?"""
|
||||
import subprocess
|
||||
|
||||
return subprocess.run(
|
||||
["systemctl", "--user", "cat", SERVICE_NAME],
|
||||
capture_output=True, text=True,
|
||||
).returncode == 0
|
||||
|
||||
|
||||
def _cmd_start() -> int:
|
||||
"""Démarre le service STT (systemd --user)."""
|
||||
import subprocess
|
||||
|
||||
if not _service_installed():
|
||||
print("Service non installé — lance d'abord : stt --install-service")
|
||||
print("(ou simplement `stt` pour un lancement direct, sans service)")
|
||||
return 1
|
||||
subprocess.run(["systemctl", "--user", "start", SERVICE_NAME], check=False)
|
||||
state = subprocess.run(
|
||||
["systemctl", "--user", "is-active", SERVICE_NAME],
|
||||
capture_output=True, text=True,
|
||||
).stdout.strip()
|
||||
print(f"✓ Service STT démarré (état : {state}).")
|
||||
return 0
|
||||
|
||||
|
||||
def _cmd_restart() -> int:
|
||||
"""Redémarre le service STT — recharge la config (modèle ASR, wake word…)."""
|
||||
import subprocess
|
||||
|
||||
if not _service_installed():
|
||||
print("Service non installé — lance d'abord : stt --install-service")
|
||||
return 1
|
||||
# systemd possède le job : même si ce process est tué (cas restart depuis le
|
||||
# HUD, qui tourne DANS le service), le redémarrage va à son terme.
|
||||
subprocess.run(["systemctl", "--user", "restart", SERVICE_NAME], check=False)
|
||||
print("✓ Service STT redémarré.")
|
||||
return 0
|
||||
|
||||
|
||||
def _running_stt_pids() -> list[int]:
|
||||
"""PID des instances `stt` (service voix/HUD) en cours, hors la commande courante.
|
||||
|
||||
|
|
@ -117,7 +158,7 @@ def _running_stt_pids() -> list[int]:
|
|||
"""
|
||||
me = os.getpid()
|
||||
utilitaires = {
|
||||
"--stop", "--update", "--version", "--setup", "--text",
|
||||
"--stop", "--start", "--restart", "--update", "--version", "--setup", "--text",
|
||||
"--install-service", "--uninstall-service",
|
||||
"--install-desktop", "--uninstall-desktop",
|
||||
}
|
||||
|
|
@ -397,6 +438,10 @@ def main(argv: list[str] | None = None) -> int:
|
|||
p.add_argument("--no-tts", action="store_true", help="Réponses texte seulement (voix)")
|
||||
p.add_argument("--window", choices=["app", "kiosk", "none"],
|
||||
help="Ouverture du HUD : app (fenêtre, type Discord) | kiosk (plein écran) | none")
|
||||
p.add_argument("--start", action="store_true",
|
||||
help="Démarre le service STT (systemd --user)")
|
||||
p.add_argument("--restart", action="store_true",
|
||||
help="Redémarre le service STT (recharge la config : ASR, wake word…)")
|
||||
p.add_argument("--stop", action="store_true",
|
||||
help="Éteint le service STT en cours (systemd --user ou process vocal)")
|
||||
p.add_argument("--install-service", action="store_true",
|
||||
|
|
@ -418,6 +463,10 @@ def main(argv: list[str] | None = None) -> int:
|
|||
return _cmd_setup()
|
||||
if args.update:
|
||||
return _cmd_update()
|
||||
if args.start:
|
||||
return _cmd_start()
|
||||
if args.restart:
|
||||
return _cmd_restart()
|
||||
if args.stop:
|
||||
return _cmd_stop()
|
||||
if args.install_service:
|
||||
|
|
|
|||
|
|
@ -683,6 +683,11 @@
|
|||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Actions de service (redémarrer / mettre à jour / arrêter) */
|
||||
.svc-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
||||
.svc-actions .btn { width: auto; font-size: 14px; padding: 11px 10px; }
|
||||
.svc-actions .btn#svcStop { grid-column: 1 / -1; } /* Arrêter sur toute la largeur */
|
||||
|
||||
/* Toast — confirmation éphémère (clic / voix) */
|
||||
.toast {
|
||||
position: fixed;
|
||||
|
|
@ -911,6 +916,17 @@
|
|||
<div class="saved-hint" id="savedHint">Réglages envoyés ✓</div>
|
||||
</div>
|
||||
|
||||
<!-- Service (systemd --user, sur cette machine) -->
|
||||
<div class="field">
|
||||
<span class="flabel">Service</span>
|
||||
<p class="hint">Gérer le service STT sur cette machine (systemd --user).</p>
|
||||
<div class="svc-actions">
|
||||
<button class="btn" id="svcRestart" title="Recharge la config : modèle ASR, wake word…">↻ Redémarrer</button>
|
||||
<button class="btn" id="svcUpdate" title="Met à jour STT puis redémarre">⬆ Mettre à jour</button>
|
||||
<button class="btn danger" id="svcStop" title="Arrête le service">⏻ Arrêter</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
|
|
@ -1363,6 +1379,18 @@ el('resetConv').addEventListener('click', () => {
|
|||
flashSaved();
|
||||
});
|
||||
|
||||
/* ----- Actions de service (backend → `stt --restart|--stop|--update`) -----
|
||||
Après restart/stop le WS tombe (le process meurt) : le HUD passe « déconnecté »
|
||||
puis se reconnecte tout seul quand le service revient. */
|
||||
function serviceAction(action, label) {
|
||||
showToast(label);
|
||||
if (DEMO || !ws || ws.readyState !== WebSocket.OPEN) { console.log('[DÉMO] service →', action); return; }
|
||||
ws.send(JSON.stringify({ type: 'control', action }));
|
||||
}
|
||||
el('svcRestart').addEventListener('click', () => serviceAction('service-restart', '↻ Redémarrage du service…'));
|
||||
el('svcUpdate').addEventListener('click', () => serviceAction('service-update', '⬆ Mise à jour en cours…'));
|
||||
el('svcStop').addEventListener('click', () => serviceAction('service-stop', '⏻ Arrêt du service…'));
|
||||
|
||||
/* ============================================================================
|
||||
MODE DÉMO — boucle d'événements simulée
|
||||
============================================================================ */
|
||||
|
|
|
|||
|
|
@ -102,6 +102,36 @@ async def serve(cfg: dict[str, Any], make_engine, client) -> None:
|
|||
loop.run_in_executor(None, _open_url_external, svc.url)
|
||||
emit({"type": "toast", "text": f"{svc.icon} Ouverture de {svc.name}"})
|
||||
|
||||
# Binaire `stt` résolu une fois (PATH du service --user ≠ shell interactif)
|
||||
import shutil as _shutil
|
||||
import sys as _sys
|
||||
|
||||
stt_bin = _shutil.which("stt") or _sys.argv[0]
|
||||
|
||||
def run_service_action(action: str) -> None:
|
||||
"""Pilote le service depuis le HUD : restart / stop / update.
|
||||
|
||||
Lance `stt --<action>` en processus DÉTACHÉ : pour restart/stop, le process
|
||||
courant (le HUD = le service) meurt, mais systemd possède le job et le mène à
|
||||
terme. Pour update : `stt --update` puis `stt --restart` (la maj précède le
|
||||
redémarrage, sinon on se couperait l'herbe sous le pied).
|
||||
"""
|
||||
labels = {
|
||||
"service-restart": "↻ Redémarrage du service…",
|
||||
"service-stop": "⏻ Arrêt du service…",
|
||||
"service-update": "⬆ Mise à jour en cours (peut prendre un moment)…",
|
||||
}
|
||||
if action not in labels:
|
||||
return
|
||||
emit({"type": "toast", "text": labels[action]})
|
||||
if action == "service-update":
|
||||
argv = ["sh", "-c", f'"{stt_bin}" --update && "{stt_bin}" --restart']
|
||||
elif action == "service-stop":
|
||||
argv = [stt_bin, "--stop"]
|
||||
else:
|
||||
argv = [stt_bin, "--restart"]
|
||||
loop.run_in_executor(None, _spawn_detached, argv)
|
||||
|
||||
async def handler(ws):
|
||||
clients.add(ws)
|
||||
try:
|
||||
|
|
@ -129,6 +159,10 @@ async def serve(cfg: dict[str, Any], make_engine, client) -> None:
|
|||
elif mtype == "control" and msg.get("action") == "open-service":
|
||||
# indépendant du moteur vocal → cliquable même sans micro
|
||||
open_service(str(msg.get("service") or ""))
|
||||
elif mtype == "control" and msg.get("action") in (
|
||||
"service-restart", "service-stop", "service-update"
|
||||
):
|
||||
run_service_action(str(msg.get("action")))
|
||||
elif mtype == "control" and engine is not None:
|
||||
action = msg.get("action")
|
||||
if action == "stop":
|
||||
|
|
@ -229,6 +263,25 @@ def _open_browser(url: str, mode: str) -> None:
|
|||
webbrowser.open(url)
|
||||
|
||||
|
||||
def _spawn_detached(argv: list[str]) -> None:
|
||||
"""Lance une commande détachée (nouvelle session) et muette — best-effort.
|
||||
|
||||
`start_new_session=True` : la commande survit à la mort du HUD (cas restart/stop
|
||||
déclenché depuis l'interface, où le process courant est lui-même arrêté).
|
||||
"""
|
||||
import subprocess
|
||||
|
||||
try:
|
||||
subprocess.Popen(
|
||||
argv,
|
||||
start_new_session=True,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
except Exception: # noqa: BLE001 — action best-effort, jamais fatale
|
||||
pass
|
||||
|
||||
|
||||
def _open_url_external(url: str) -> None:
|
||||
"""Ouvre `url` dans le navigateur HABITUEL de l'utilisateur (onglet normal).
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue