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:
João Henrique
2026-08-19 21:46:47 -04:00
co-authored by Claude Opus 5
parent 4f5cf94443
commit 6090e229e9
14 changed files with 1649 additions and 1254 deletions
+128
View File
@@ -0,0 +1,128 @@
"""Edições no projeto: silêncio, corte por texto, preenchimento, marcadores.
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 fcpxml.model_manager import (
load_silence_config,
save_silence_config,
)
from . import shared
from .shared import (
_derived_output,
_emit_no_change_or_error,
)
def cmd_remove_silences(args: dict) -> int:
"""Run the canonical server silence remover into a suffixed copy."""
path = str(args.get("path", ""))
if not path or not Path(path).exists():
shared.emit({"ok": False, "error": "Arquivo de projeto não encontrado."})
return 1
try:
from server import handle_remove_media_silence
output = _derived_output(path, "_silence_removed", args)
contents = asyncio.run(handle_remove_media_silence({**args, "filepath": path, "output_path": output}))
message = "\n".join(getattr(content, "text", str(content)) for content in contents)
if not Path(output).exists():
return _emit_no_change_or_error(path, message)
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_edit_by_transcript(args: dict) -> int:
"""Cut (or keep only) spoken phrases, using each media's cached transcript."""
path = str(args.get("path", ""))
phrases = args.get("phrases") or []
if not path or not Path(path).exists():
shared.emit({"ok": False, "error": "Arquivo de projeto não encontrado."})
return 1
if not isinstance(phrases, list) or not [p for p in phrases if str(p).strip()]:
shared.emit({"ok": False, "error": "Informe ao menos uma frase para cortar."})
return 1
try:
from server import handle_edit_by_transcript
output = _derived_output(path, "_transcript_edit", args)
contents = asyncio.run(handle_edit_by_transcript({**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_remove_filler_words(args: dict) -> int:
"""Cut filler words (um, uh, ...) out, using each media's cached transcript."""
path = str(args.get("path", ""))
if not path or not Path(path).exists():
shared.emit({"ok": False, "error": "Arquivo de projeto não encontrado."})
return 1
try:
from server import handle_remove_filler_words
output = _derived_output(path, "_defillered", args)
contents = asyncio.run(handle_remove_filler_words({**args, "filepath": path, "output_path": output}))
message = "\n".join(getattr(content, "text", str(content)) for content in contents)
if not Path(output).exists():
return _emit_no_change_or_error(path, message)
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_transcript_markers(args: dict) -> int:
"""Add a marker per transcribed segment, using each media's cached transcript."""
path = str(args.get("path", ""))
if not path or not Path(path).exists():
shared.emit({"ok": False, "error": "Arquivo de projeto não encontrado."})
return 1
try:
from server import handle_transcript_markers
output = _derived_output(path, "_transcript_markers", args)
contents = asyncio.run(handle_transcript_markers({**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_silence_config(args: dict) -> int:
"""Read the persisted silence thresholds (noise floor, duration, padding)."""
shared.emit({"ok": True, **load_silence_config()})
return 0
def cmd_set_silence_config(args: dict) -> int:
"""Persist silence thresholds. Only the given fields change."""
config = save_silence_config(
noise_db=args.get("noise_db"),
min_silence=args.get("min_silence"),
padding=args.get("padding"),
)
shared.emit({"ok": True, **config})
return 0