refactor: models_api.py vira ponto de entrada sobre admin/api/
A ponte JSON do app tinha 1.395 linhas e 37 comandos de oito assuntos diferentes num arquivo só. Agora models_api.py guarda apenas a referência dos comandos, a tabela de despacho e o main(); cada assunto virou um módulo em admin/api/ (models, project, editing, zoom, subtitles, transcription, voice, review), com a base comum em shared.py. Nada muda para o app: ele continua chamando admin/models_api.py por caminho, e os 37 comandos respondem igual — verificado rodando a ponte de verdade. Duas coisas que a divisão obrigou a arrumar: - A saída passa por `shared.emit` chamada pelo módulo, não pelo nome importado. Isso preserva a propriedade de que trocar `emit` num lugar só captura a saída de todos os comandos — que era acidental quando tudo morava no mesmo arquivo, e vira intencional agora. - `_CANCEL` e o lock eram globais compartilhados. O registro de downloads foi para models.py, junto de quem o usa, com lock próprio: o antigo protegia ao mesmo tempo o dicionário e a escrita em stdout, duas coisas sem relação. Também: admin/test_models_api.py estava fora de `testpaths` e nunca rodava. Movido para code/tests/ e ligado ao gate — 1441 → 1454 testes (ver Engine/docs/05_EXPERIENCIAS.md #24). Lint zerado, 1454 testes passando. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
4f5cf94443
commit
6090e229e9
@@ -0,0 +1,114 @@
|
||||
"""Zoom (punch-in): por janela, por clipe e por trecho da transcrição.
|
||||
|
||||
Extraído de models_api.py — a tabela de comandos segue lá.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# code/ is the package root for fcpxml and server modules.
|
||||
_CODE_DIR = str(Path(__file__).resolve().parent.parent / "code")
|
||||
if _CODE_DIR not in sys.path:
|
||||
sys.path.insert(0, _CODE_DIR)
|
||||
|
||||
|
||||
from . import shared
|
||||
from .shared import (
|
||||
_derived_output,
|
||||
_transcript_json_path,
|
||||
)
|
||||
from .shared import _load_cached_transcript # noqa: E402
|
||||
|
||||
|
||||
def cmd_add_zoom(args: dict) -> int:
|
||||
"""Add an ease-in/ease-out punch-in zoom to one clip."""
|
||||
path = str(args.get("path", ""))
|
||||
clip_id = str(args.get("clip_id", "")).strip()
|
||||
if not path or not Path(path).exists():
|
||||
shared.emit({"ok": False, "error": "Arquivo de projeto não encontrado."})
|
||||
return 1
|
||||
if not clip_id:
|
||||
shared.emit({"ok": False, "error": "Informe o nome do clipe."})
|
||||
return 1
|
||||
try:
|
||||
from server import handle_add_zoom
|
||||
|
||||
output = _derived_output(path, "_zoom", args)
|
||||
contents = asyncio.run(handle_add_zoom({**args, "filepath": path, "output_path": output}))
|
||||
message = "\n".join(getattr(content, "text", str(content)) for content in contents)
|
||||
if not Path(output).exists():
|
||||
shared.emit({"ok": False, "error": message})
|
||||
return 1
|
||||
shared.emit({"ok": True, "path": output, "message": message})
|
||||
return 0
|
||||
except Exception as exc:
|
||||
shared.emit({"ok": False, "error": str(exc)})
|
||||
return 1
|
||||
|
||||
def cmd_zoom_clips(args: dict) -> int:
|
||||
"""Return timeline clips with enough identity for the zoom picker."""
|
||||
path = Path(str(args.get("path", "")))
|
||||
output_dir = str(args.get("output_dir", "")).strip()
|
||||
if not path.exists():
|
||||
shared.emit({"ok": False, "error": "Arquivo de projeto não encontrado."})
|
||||
return 1
|
||||
try:
|
||||
from server import _require_timeline
|
||||
|
||||
_, timeline = _require_timeline(str(path))
|
||||
clips = []
|
||||
for index, clip in enumerate(timeline.clips):
|
||||
media = clip.media_path or ""
|
||||
cached = _load_cached_transcript(_transcript_json_path(media, output_dir)) if media else None
|
||||
clips.append({
|
||||
"id": f"{index}:{clip.start.seconds:.6f}",
|
||||
"index": index,
|
||||
"name": clip.name,
|
||||
"start": clip.start.seconds,
|
||||
"duration": clip.duration_seconds,
|
||||
"media": Path(media).name if media else "",
|
||||
"preview": ((cached or {}).get("text", "") or "")[:180],
|
||||
"has_transcript": cached is not None,
|
||||
})
|
||||
shared.emit({"ok": True, "clips": clips})
|
||||
return 0
|
||||
except Exception as exc:
|
||||
shared.emit({"ok": False, "error": str(exc)})
|
||||
return 1
|
||||
|
||||
def cmd_zoom_segments(args: dict) -> int:
|
||||
"""Return sentence/word ranges for one timeline clip."""
|
||||
path = Path(str(args.get("path", "")))
|
||||
output_dir = str(args.get("output_dir", "")).strip()
|
||||
try:
|
||||
from server import _require_timeline
|
||||
|
||||
_, timeline = _require_timeline(str(path))
|
||||
index = int(args.get("index", -1))
|
||||
if index < 0 or index >= len(timeline.clips):
|
||||
raise ValueError("Clipe selecionado não existe.")
|
||||
clip = timeline.clips[index]
|
||||
if not clip.media_path:
|
||||
raise ValueError("Este clipe não possui mídia associada.")
|
||||
data = _load_cached_transcript(_transcript_json_path(clip.media_path, output_dir))
|
||||
if data is None:
|
||||
shared.emit({"ok": True, "segments": [], "message": "Transcreva este clipe primeiro."})
|
||||
return 0
|
||||
segments = []
|
||||
for number, segment in enumerate(data.get("segments", [])):
|
||||
text = str(segment.get("text", "")).strip()
|
||||
if text:
|
||||
segments.append({
|
||||
"id": number,
|
||||
"start": float(segment.get("start", 0)),
|
||||
"end": float(segment.get("end", 0)),
|
||||
"text": text,
|
||||
})
|
||||
shared.emit({"ok": True, "segments": segments})
|
||||
return 0
|
||||
except Exception as exc:
|
||||
shared.emit({"ok": False, "error": str(exc)})
|
||||
return 1
|
||||
Reference in New Issue
Block a user