feat: etapa 5 do assistente — revisão de ênfases com timeline

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>
This commit is contained in:
João Henrique
2026-08-19 21:29:27 -04:00
co-authored by Claude Opus 5
parent e7748c2c58
commit 1bebee4359
31 changed files with 4622 additions and 83 deletions
+47
View File
@@ -1183,6 +1183,51 @@ o outro; percentil entrega um punhado útil nos dois casos.
---
## 21 — 2026-08-19 — Teste travado no default antigo de `zoom scale`
- **Sintoma:** `tests/test_voice_actions.py::test_default_scale_when_absent`
quebrando com `KeyError: 'scale'`, sem relação com a alteração em curso.
- **Causa raiz:** `parse_actions` deixou de carimbar `scale=1.3` quando o
parâmetro vem ausente, justamente para que
`server_tools/_shared.py` use o `zoom_scale` configurado pelo usuário. O
teste continuou afirmando o default antigo, então passou a acusar como erro
exatamente o comportamento desejado.
- **Solução adotada:** teste reescrito para o contrato novo — um `scale`
omitido tem que chegar ausente ao aplicador (`test_absent_scale_is_left_absent`).
- **Aprendizado:** quando um default sai do parser e vira configuração, o teste
que afirmava o valor antigo passa a defender o bug. Ao remover um default,
procure o teste que o fixava no mesmo commit — senão ele fica dizendo o
contrário do código, e a próxima pessoa perde tempo achando que quebrou algo.
- **Estado:** `resolvido`
---
## 22 — 2026-08-19 — `VideoPlayer` (AVKit) derruba o app compilado por `swiftc`
- **Sintoma:** "G-ART encerrou inesperadamente" (SIGABRT) toda vez que o
assistente entrava na etapa 5. Nada aparecia na tela antes do crash.
- **Causa raiz:** o app é montado invocando `swiftc` direto
(`MacApp/build_app.sh`), não pelo Xcode. Nesse modo o runtime não consegue
resolver a superclasse Objective-C de `VideoPlayer`:
`failed to demangle superclass of VideoPlayerView from mangled name
'So12AVPlayerViewC'` → `getSuperclassMetadata` chama `fatalError`. É erro de
runtime, então a compilação passa limpa e o problema só aparece ao abrir a
view.
- **Solução adotada:** trocar `VideoPlayer` por um `AVPlayerLayer` dentro de um
`NSViewRepresentable` (`PlayerSurface`/`PlayerLayerView` em
`PhraseReviewView.swift`). Só depende de AVFoundation, que linka normalmente.
Os controles de transporte já viviam na barra da timeline, então não se perde
nada com a chrome do AVKit.
- **Aprendizado:** compilar limpo não prova que um componente de framework
existe em runtime neste build. Ao usar uma view SwiftUI que embrulha uma
classe AppKit/ObjC (AVKit, WebKit, MapKit), abra a tela de fato antes de
concluir. Um harness pequeno (`swiftc` com os mesmos fontes + um `@main` que
monta só aquela view e sai) reproduz o crash em segundos, sem precisar
navegar o app inteiro até lá.
- **Estado:** `resolvido`
---
## Resumo rápido (índice)
| # | Data | Problema | Estado |
@@ -1205,5 +1250,7 @@ o outro; percentil entrega um punhado útil nos dois casos.
| 18 | 2026-08-19 | Legendas dinâmicas geradas com `bold="0" fontFace="Bold"` não renderizam no FCP — negrito deve ser `bold="1"` (atributo) e itálico `fontFace`+`italic="1"` | `resolvido` |
| 19 | 2026-08-19 | `output_dir` usado só como cerca de validação e nunca como destino — toda chamada entre pastas falhava acusando o caminho que ela mesma gerou | `resolvido` |
| 20 | 2026-08-19 | `apply_voice_actions` ausente da ponte e do encadeamento do app — dava para analisar e legendar, não para cortar | `resolvido` |
| 21 | 2026-08-19 | Teste ainda afirmava o default `zoom scale=1.3` removido do parser (agora vem do `zoom_scale` do usuário) | `resolvido` |
| 22 | 2026-08-19 | `VideoPlayer` (AVKit) aborta em runtime no app compilado por `swiftc` — etapa 5 fechava o app; trocado por `AVPlayerLayer` | `resolvido` |
> Mantenha o índice acima sempre sincronizado com as entradas mais recentes.