Ao dividir _shared.py em admin/api/*.py ontem, o cálculo `Path(__file__).resolve().parent.parent / "code"` foi copiado sem ajustar para o nível de diretório novo. No arquivo original (admin/models_api.py, direto em admin/) dois `.parent` chegavam na raiz do repo. Em admin/api/shared.py, um nível mais fundo, dois `.parent` param em admin/ — e admin/code nunca existiu. sys.path nunca recebia code/, então toda ação que passa por `server` (analisar voz, aplicar decisões) crashava o app com ModuleNotFoundError: server_tools. O bug sobreviveu a duas rodadas de validação da sessão anterior — lint zero, 1454 testes verdes, comando testado manualmente pela ponte — porque todos rodam num venv com install editável (__editable__.fcp_mcp_server.pth) que já deixa fcpxml/server_tools importáveis por conta própria, mascarando qualquer erro no cálculo manual de sys.path. Só o app real, no fallback sem uv, expõe o bug. Correção: o cálculo de sys.path sai de cada módulo de comando (estava duplicado em nove arquivos) e passa a existir uma única vez em admin/api/__init__.py, que roda antes de qualquer submódulo — nenhum precisa mais da própria cópia. O teste de regressão precisou de duas tentativas pelo mesmo motivo do bug: a primeira versão também passava com o bug presente, por rodar no mesmo venv "de sorte". Só ficou confiável isolando um subprocess que remove site-packages do sys.path antes de importar — confirmado nos dois sentidos, falha com o bug reintroduzido e passa com a correção (TestCodeDirResolution). Detalhe completo, incluindo por que o comando manual não pegou: Engine/docs/05_EXPERIENCIAS.md #25. Lint zerado, 1457 testes passando (3 novos), app compilado. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
138 lines
4.1 KiB
Python
138 lines
4.1 KiB
Python
"""Catálogo de modelos: listar, baixar, escolher, apagar.
|
|
|
|
Extraído de models_api.py — a tabela de comandos segue lá.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import shutil
|
|
import subprocess
|
|
import threading
|
|
|
|
from fcpxml.diarize import (
|
|
diarization_capability,
|
|
)
|
|
from fcpxml.model_manager import (
|
|
download_model,
|
|
get_models_dir,
|
|
is_model_downloaded,
|
|
list_installed_models,
|
|
load_catalog,
|
|
load_hf_token,
|
|
load_num_speakers,
|
|
load_selected_model,
|
|
load_transcript_language,
|
|
model_cache_dir,
|
|
save_models_dir,
|
|
save_selected_model,
|
|
save_transcript_language,
|
|
)
|
|
|
|
from . import shared
|
|
from .shared import (
|
|
RECOMMENDED,
|
|
)
|
|
|
|
# Downloads em andamento, para o comando `cancel` conseguir interrompê-los.
|
|
# Mora aqui, e não no shared, porque só `download` e `cancel` o tocam — e o
|
|
# lock é próprio: ele protege este dicionário, não a saída em stdout.
|
|
_CANCEL: dict[str, threading.Event] = {}
|
|
_CANCEL_LOCK = threading.Lock()
|
|
|
|
|
|
def cmd_catalog() -> None:
|
|
catalog = load_catalog()
|
|
installed = list_installed_models()
|
|
diar_ok, diar_msg = diarization_capability(load_hf_token())
|
|
shared.emit(
|
|
{
|
|
"models": catalog,
|
|
"installed": installed,
|
|
"selected": load_selected_model(),
|
|
"language": load_transcript_language(),
|
|
"models_dir": str(get_models_dir()),
|
|
"installed_count": len(installed),
|
|
"recommended": list(RECOMMENDED),
|
|
"diarization": diar_ok,
|
|
"diarization_message": diar_msg,
|
|
"hf_token_set": bool(load_hf_token()),
|
|
"num_speakers": load_num_speakers(),
|
|
}
|
|
)
|
|
|
|
def cmd_download(args: dict) -> int:
|
|
model = str(args.get("model", ""))
|
|
if model not in _model_names():
|
|
shared.emit({"type": "error", "message": f"Modelo desconhecido: {model}"})
|
|
return 1
|
|
ev = threading.Event()
|
|
with _CANCEL_LOCK:
|
|
_CANCEL[model] = ev
|
|
try:
|
|
download_model(model, progress_cb=lambda f: shared.emit({"type": "progress", "fraction": f}), cancel_event=ev)
|
|
installed = is_model_downloaded(model)
|
|
shared.emit({"type": "done", "installed": installed})
|
|
if installed:
|
|
save_selected_model(model)
|
|
return 0 if installed else 1
|
|
except Exception as exc:
|
|
shared.emit({"type": "error", "message": str(exc)})
|
|
return 1
|
|
finally:
|
|
with _CANCEL_LOCK:
|
|
_CANCEL.pop(model, None)
|
|
|
|
def cmd_cancel(args: dict) -> None:
|
|
model = str(args.get("model", ""))
|
|
ev = _CANCEL.get(model)
|
|
if ev is not None:
|
|
ev.set()
|
|
shared.emit({"ok": True})
|
|
|
|
def cmd_select(args: dict) -> None:
|
|
model = str(args.get("model", ""))
|
|
if not is_model_downloaded(model):
|
|
shared.emit({"ok": False, "error": "Modelo não está instalado."})
|
|
return
|
|
save_selected_model(model)
|
|
shared.emit({"ok": True, "selected": load_selected_model()})
|
|
|
|
def cmd_set_language(args: dict) -> int:
|
|
"""Persist the transcription language (the default for every transcription)."""
|
|
lang = str(args.get("language", "auto"))
|
|
try:
|
|
saved = save_transcript_language(lang)
|
|
except ValueError as exc:
|
|
shared.emit({"ok": False, "error": str(exc)})
|
|
return 1
|
|
shared.emit({"ok": True, "language": saved})
|
|
return 0
|
|
|
|
def cmd_delete(args: dict) -> None:
|
|
model = str(args.get("model", ""))
|
|
try:
|
|
shutil.rmtree(model_cache_dir(model), ignore_errors=True)
|
|
except Exception:
|
|
pass
|
|
shared.emit({"ok": True})
|
|
|
|
def cmd_open_finder(args: dict) -> None:
|
|
target = str(args.get("path") or model_cache_dir(str(args.get("model", ""))))
|
|
try:
|
|
subprocess.Popen(["open", target])
|
|
except OSError:
|
|
pass
|
|
shared.emit({"ok": True})
|
|
|
|
def cmd_set_models_dir(args: dict) -> int:
|
|
try:
|
|
d = save_models_dir(str(args.get("dir", "")))
|
|
shared.emit({"ok": True, "models_dir": d})
|
|
return 0
|
|
except ValueError as exc:
|
|
shared.emit({"ok": False, "error": str(exc)})
|
|
return 1
|
|
|
|
def _model_names() -> list[str]:
|
|
return [m["internal_name"] for m in load_catalog()]
|