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>
91 lines
3.1 KiB
Python
91 lines
3.1 KiB
Python
"""Revisão de frases: montar a tela de ênfases e salvar o que foi decidido.
|
|
|
|
Extraído de models_api.py — a tabela de comandos segue lá.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
|
|
from . import shared
|
|
|
|
|
|
def cmd_build_phrase_review(args: dict) -> int:
|
|
"""Build the reviewable script (phrases + the AI's decisions) for the wizard.
|
|
|
|
`voice_timeline` points at the _voice_timeline.json; `actions` carries the
|
|
decision list the model returned (inline, in any of the shapes the skill
|
|
emits). The review is always rebuilt from the current analysis, then the
|
|
decisions saved on a previous visit are laid back over it — reopening the
|
|
step must show the edits the user left there without freezing the acoustics
|
|
as they were when they left.
|
|
"""
|
|
from fcpxml.phrase_review import (
|
|
build_phrase_review,
|
|
load_phrase_review,
|
|
merge_saved_decisions,
|
|
)
|
|
|
|
timeline_path = str(args.get("voice_timeline", ""))
|
|
if not timeline_path or not Path(timeline_path).exists():
|
|
shared.emit({"ok": False, "error": "Análise de voz (voice_timeline.json) não encontrada."})
|
|
return 1
|
|
|
|
try:
|
|
with open(timeline_path, encoding="utf-8") as fh:
|
|
timeline = json.load(fh)
|
|
except (OSError, ValueError) as exc:
|
|
shared.emit({"ok": False, "error": f"Erro ao ler a análise de voz: {exc}"})
|
|
return 1
|
|
|
|
extra = [d for d in (args.get("output_dir"), args.get("media_dir")) if d]
|
|
review = build_phrase_review(
|
|
timeline,
|
|
args.get("actions"),
|
|
voice_timeline_path=timeline_path,
|
|
extra_dirs=extra,
|
|
)
|
|
|
|
saved = None if args.get("fresh") else load_phrase_review(timeline_path)
|
|
review = merge_saved_decisions(review, saved)
|
|
shared.emit({"ok": True, "reused": saved is not None, **review})
|
|
return 0
|
|
|
|
def cmd_save_phrase_review(args: dict) -> int:
|
|
"""Persist the edited review and the actions derived from it."""
|
|
from fcpxml.phrase_review import save_phrase_review
|
|
|
|
timeline_path = str(args.get("voice_timeline", ""))
|
|
if not timeline_path:
|
|
shared.emit({"ok": False, "error": "Caminho da análise de voz não informado."})
|
|
return 1
|
|
|
|
phrases = args.get("phrases")
|
|
if not isinstance(phrases, list):
|
|
shared.emit({"ok": False, "error": "Nenhuma frase para salvar."})
|
|
return 1
|
|
|
|
review = {
|
|
"version": args.get("version", "1.0"),
|
|
"source": args.get("source", ""),
|
|
"duration": args.get("duration", 0.0),
|
|
"speakers": args.get("speakers", []),
|
|
"phrases": phrases,
|
|
"zooms": args.get("zooms", []),
|
|
}
|
|
try:
|
|
review_path, actions_path = save_phrase_review(timeline_path, review)
|
|
except OSError as exc:
|
|
shared.emit({"ok": False, "error": f"Erro ao salvar a revisão: {exc}"})
|
|
return 1
|
|
|
|
shared.emit({
|
|
"ok": True,
|
|
"review_path": str(review_path),
|
|
"actions_path": str(actions_path),
|
|
"emphasis_count": sum(1 for p in phrases if int(p.get("emphasis", 0) or 0) >= 1),
|
|
"removed_count": sum(1 for p in phrases if not p.get("active", True)),
|
|
})
|
|
return 0
|