0,05s existia como margem de segurança contra cortar a palavra em cima,
mas um silêncio que essa ferramenta encontra costuma ser o respiro
natural antes de uma frase nova, não sujeira de edição — e 0,05s raspava
esse respiro quase todo.
Caso real (projeto Mastopexia): a pausa antes de "Com" tinha 0,567s no
áudio original; com padding 0,05 sobrou só ~0,1s no total (0,05 de cada
lado), colando o clipe seguinte a 5ms da palavra em vez de deixar uma
pausa perceptível. 0,2s alinha com a convenção já documentada para folga
em corte de fronteira de frase (editar-por-voz/06-texto-corte-marcador.md).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
generate_dynamic_subtitles e a metade dinâmica de generate_subtitles_by_emphasis
passam a empacotar cada sub-frase da legenda dinâmica num compound clip por
padrão (compound_subphrases=True), completando o wrap_titles_in_compound
e split_into_subphrases do commit anterior — que ainda não tinham chamador
em produção.
Também torna validate_subtitle_layout ciente de compound clips: media cada
grupo (spine principal + cada <media> de compound) no seu próprio espaço de
tempo, em vez de uma varredura .//title global — sem isso, âncoras de
compounds diferentes liam offset "0s" e acusavam colisão espacial entre
frases que nunca dividem a tela, só porque compartilham o mesmo zero de
tempo local.
Testado ponta a ponta na gravação real (Mastopexia): 12 compounds, 41
títulos todos empacotados, zero soltos, zero IDs duplicados, DTD válida.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
split_into_subphrases divide a frase na vírgula — onde a fala respira —
mas funde de volta o pedaço curto ("né?", "Então..."), que lê como parte
da frase anterior e não como bloco próprio.
wrap_titles_in_compound empacota os títulos de uma sub-frase num compound
clip, replicando a estrutura que o próprio Final Cut produz: o primeiro
título vira âncora do spine em offset 0, os demais penduram nele por lane,
e um ref-clip toma o lugar deles na lane original. Os offsets dos filhos
são rebaseados para o espaço de tempo da âncora, senão cada palavra
escorregaria pela diferença entre os dois start.
Junto: _filter_children_for_segment passa a filtrar também o <video> do
Clipe de Ajuste. Sem isso, cada corte subsequente duplicava o zoom em
todos os pedaços resultantes com o offset original intacto, e as cópias
desenhavam empilhadas na mesma posição da timeline.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Object-tracker/tracking-shape (dado de rastreamento de objeto preservado
do asset original) mantinha o mesmo id em cada deepcopy feito por
split_clip/cut_clip_ranges, e o FCP acabava rejeitando o arquivo com "ID
tr1 already defined" depois de vários cortes. Mesmo mecanismo do bug já
corrigido para text-style-def, agora coberto também para tracking-shape.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Dois problemas reais vistos no projeto Mastopexia:
1. cut_clip_ranges só absorvia um keep-segment curto no INÍCIO/FIM do
clipe (a lógica já existente do #6). Um keep curto no MEIO (entre dois
cuts, sem nenhum vizinho mantido pra herdar) nunca era absorvido —
sobrava como clipe de vídeo de 0,07-0,23s na timeline. Generalizado
pra qualquer posição, com limiar maior (6 frames / 0,3s, medido no
material real) — no meio, o pedacinho é descartado (vira parte do
corte ao redor), nas bordas continua sendo herdado pelo vizinho.
2. generate_subtitles_by_emphasis gerava a legenda comum inteira e
desativava (enabled="0") onde a dinâmica cobre. Título desativado
continua aparecendo como clipe riscado na timeline do Final Cut mesmo
sem renderizar — um corte com bastante ênfase virava dezenas de clipes
mortos poluindo a trilha (visto ao vivo pelo usuário: "ficou uma
bosta"). Trocado por não gerar o bloco comum ali, em vez de gerar e
desativar. Custo: reativar ênfase manualmente depois exige regenerar a
legenda comum daquele trecho, não só reabilitar.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
phrase_review_to_actions() cortava cada frase desativada isoladamente
(start..end da própria frase) — quando várias seguidas estavam desativadas,
a pausa ENTRE elas não pertencia a nenhuma frase e sobrevivia como um
clipe minúsculo (0,1-0,5s) na timeline final. Confirmado no projeto
Mastopexia real: 29 cuts individuais geravam mais de uma dezena de fatias
sub-segundo; agrupar frases desativadas consecutivas num único cut (do
início da primeira ao fim da última) reduziu para 3 cuts e 4 fatias
residuais (menores, provavelmente do padding do remove_media_silence —
registrado como dívida separada em 09_MANUTENCAO.md §2.5).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Cortes escritos rente ao timestamp da palavra soavam secos (relatado no
projeto Mastopexia) — o critério e o prompt do modelo local mandavam cobrir
a frase inteira sem orientar a borda que toca fala mantida. Adiciona a regra
de recuar ~0,15-0,25s nas duas pontas quando o corte encosta em conteúdo
que fica, tanto no skill (06-texto-corte-marcador.md) quanto no prompt
embutido do Ollama (llm_local.py) — pra não precisar ajustar na mão de novo.
Também registra em 05_EXPERIENCIAS.md/09_MANUTENCAO.md a dívida de
resolve_actions não tolerar margem quando zoom/marker encosta na borda
de um corte (contornado manualmente, não corrigido em código ainda), e
atualiza a lista de dívidas abertas (etapa 6/offset de whisper já resolvidos
nesta sessão).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Trabalho da branch feat/revisao-enfases: pipeline de edição por voz ganha
alinhamento forçado (whisperx), roteirização por LLM local (Ollama), e a
etapa 5 (revisão de frases) passa a refletir de verdade o que é aplicado.
- generate_subtitles_by_emphasis: legenda comum cobre o clipe inteiro,
legenda dinâmica só nas frases de ênfase, e a comum é desativada
(enabled="0") onde a dinâmica cobre, em vez de nunca ser gerada ali.
- validate_subtitle_layout ignora títulos com enabled="0" — corrige falso
positivo de colisão contra o que está desativado no lugar dele.
- Corrige zoom/marcador sendo descartado quando a borda encosta exatamente
no início de um corte.
- Etapa 5 do Assistente: recarrega quando as decisões da IA mudam (com
fresh=true, ignorando a revisão salva antiga) — resolve a dessincronia
entre "ativa" na tela e o que já foi cortado no FCPXML.
- Etapa "Processar" reaplica as decisões da revisão (_phrase_actions.json)
antes da cadeia de remoção de silêncio/legendas — antes, desativar uma
frase na etapa 5 não tinha efeito nenhum no vídeo final.
- Etapa "Concluído" fundida em "Processar" — abrir no Final Cut/Finder
aparece assim que termina, sem slide extra.
- Palavra clicável na etapa 5 agora funciona como toggle (clique de novo
desfaz) e mostra a própria ênfase (sublinhado colorido + peso da fonte).
- fcpxml/forced_align.py, fcpxml/llm_local.py, ai_edit.py: alinhamento
fonético via whisperx e roteirização local via Ollama/Gemma.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Eram 1.091 linhas com seis famílias de modelo sem relação entre si —
enumerações, tempo racional, timeline, geração, QC e legendas.
timing 304 TimeValue e Timecode
timeline 217 clipes, marcadores, lanes, projeto
enums 183 tipos/cores de marcador, transições, ritmo
subtitles 157 paleta e look das legendas dinâmicas
qc 121 achados de QC e resultado de validação
planning 93 rough cut, ritmo, montagem
O __init__ reexporta os 43 nomes, incluindo os com underscore que o writer
e a suíte já importavam, então nenhum ponto de uso mudou.
Lint zerado, 1454 testes passando.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
O writer tinha 4.199 linhas, das quais 3.300 numa única classe com dezoito
assuntos dentro. Achar o trecho de zoom exigia rolar por marcadores,
velocidade e legendas.
Agora é o pacote fcpxml/writer/, com um arquivo por assunto e o
FCPXMLModifier montado por composição de mixins. Mixins, e não objetos
separados, porque todas essas operações mexem no mesmo documento e nos
mesmos índices — separá-las em objetos independentes transformaria toda
chamada interna em travessia de fronteira sem nada em troca. A divisão que
importa aqui é de leitura, não de estado.
Nenhuma mudança de comportamento e nenhuma alteração nos ~50 pontos que
importam do writer: o __init__ re-exporta tudo, inclusive os nomes com
underscore que a suíte já usava.
core 723 carga, índices, navegação na spine, save
titles 600 títulos e legendas dinâmicas
cut 333 dividir, cortar faixas, apagar
speed 297 velocidade e zoom
(+ 20 módulos menores)
Único ajuste de chamada: quatro testes faziam patch em
fcpxml.writer.subprocess, que agora mora em writer.document (ver
Engine/docs/05_EXPERIENCIAS.md #23).
Lint zerado, 1441 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>