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>
Transforma a etapa "colar decisões" numa tela de lapidação: a sugestão da
IA chega carregada e o editor afina frase a frase o que é ênfase e o que
fica fora. Essa marcação é o norte da etapa 6 — só as frases com ênfase
recebem zoom e legenda dinâmica; as demais ficam com legenda comum.
O campo de colar o JSON sobe para a etapa 4, então a numeração das etapas
não muda e a etapa 6 segue intacta.
Backend (fcpxml/phrase_review.py):
- build_phrase_review funde o _voice_timeline.json com as actions da IA
- trim por frase que anda em fronteira de palavra; corte parcial da IA
chega como trim em vez de ser arredondado fora
- phrase_review_to_actions volta a cuts/zooms + emphasis_spans
- merge_saved_decisions reaplica só as decisões salvas sobre uma revisão
remontada da análise atual, para reprocessar a voz não ficar mascarado
- resolve_source acha a mídia: o voice timeline guarda só o nome do arquivo
App (SwiftUI):
- layout de sala de edição: preview em cima, inspector à direita, timeline
atravessando embaixo com seis trilhas rotuladas
- preview enquadra no formato de entrega lido do .fcpxml (fonte horizontal,
projeto vertical), com alternância para a mídia original
- reprodução pula os trechos removidos e para no fim do trecho
- zoom manual por trecho marcado, sem guardar escala: a forma vem das
configurações de Análise de Voz no render
- emoção da fala exposta por frase
Correções encontradas no caminho:
- VideoPlayer (AVKit) aborta em runtime no app compilado por swiftc;
trocado por AVPlayerLayer (ver Engine/docs/05_EXPERIENCIAS.md #22)
- teste que ainda afirmava o default zoom scale=1.3 removido do parser (#21)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>