chore: adiciona .gitignore e commit.command
This commit is contained in:
@@ -0,0 +1,708 @@
|
||||
# 05 — Experiências: Registro de Problemas, Erros e Decisões
|
||||
|
||||
> **Propósito:** registrar, de forma cumulativa, todos os problemas estruturais,
|
||||
> erros que se repetiram em várias tentativas e decisões difíceis enfrentadas
|
||||
> durante o desenvolvimento do G-ART. Serve de memória de trabalho para que
|
||||
> futuras implementações **não repitam os mesmos erros** e para que decisões já
|
||||
> tomadas não sejam redescobertas do zero.
|
||||
|
||||
**Regra:** sempre que um problema for detectado (estrutural ou funcional) e
|
||||
houver uma correção ou trabalho em torno dele, **adicione um registro aqui**
|
||||
antes de prosseguir. Um problema que se repete em várias tentativas é sinal de
|
||||
que merece entrada.
|
||||
|
||||
---
|
||||
|
||||
## Como registrar (template de entrada)
|
||||
|
||||
Use o bloco abaixo como modelo. Uma entrada = um problema resolvido/reconhecido.
|
||||
|
||||
```markdown
|
||||
### [DATA] Título curto do problema
|
||||
|
||||
- **Sintoma:** o que acontecia / o erro observado.
|
||||
- **Causa raiz:** o que realmente causava o problema (após investigação).
|
||||
- **Onde:** arquivo(s) e, se útil, função/linha.
|
||||
- **Tentativas que falharam:** o que já foi tentado e não funcionou.
|
||||
- **Solução adotada:** a correção que resolveu.
|
||||
- **Aprendizado:** regra/comportamento a lembrar nas próximas implementações.
|
||||
- **Estado:** `aberto` | `resolvido` | `mitigado` | `evitado por design`
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Registro de Experiências
|
||||
|
||||
### 2026-08-17 — Garantir que dois blocos nunca se sobreponham: empilhar pela TINTA real, não pela cap-height
|
||||
|
||||
- **Sintoma:** na composição progressiva, a cedilha de "começar" (Playfair
|
||||
Display Medium Italic, 230pt) invadia a linha de apoio logo abaixo. As
|
||||
caixas "lógicas" não se cruzavam — as renderizadas, sim.
|
||||
- **Causa raiz:** o empilhamento usava altura nominal `font_size * 0.75`
|
||||
(cap-height). Numa serifada de display itálica os acentos sobem a 1,007em e
|
||||
os descendentes descem a -0,241em: a tinta real ocupa quase o dobro da
|
||||
cap-height, e a folga nominal some.
|
||||
- **Onde:** `fcpxml/font_metrics.py` (`VERTICAL_METRICS`),
|
||||
`fcpxml/text_layout.py` (`ink_extent`, `compose_sentence`, `PlacedBlock`).
|
||||
- **Tentativas que falharam:** aumentar `line_gap` — afasta as linhas em todos
|
||||
os casos e perde o bloco compacto da referência, sem garantir nada: basta
|
||||
uma fonte com acentos mais altos para colidir de novo.
|
||||
- **Solução adotada:** métricas verticais reais extraídas das fontes
|
||||
(`ascent`/`descent` da caixa de linha que o FCP centra na Position, mais os
|
||||
extremos de tinta por classe de glifo: caixa alta, ascendente, x-height,
|
||||
acento maiúsculo/minúsculo, descendente). `ink_extent()` calcula o topo e a
|
||||
base da tinta DO TEXTO em questão; `compose_sentence` empilha essas caixas
|
||||
borda a borda com folga fixa. Não-sobreposição vira propriedade da
|
||||
aritmética, não de um fator de segurança. Fonte sem métricas medidas usa um
|
||||
fallback com 8% de folga extra.
|
||||
- **Aprendizado:** medir largura resolve colisão lado a lado; colisão entre
|
||||
linhas exige medir altura de tinta — e ela depende dos caracteres da linha,
|
||||
não só do corpo da fonte.
|
||||
- **Estado:** `resolvido`
|
||||
|
||||
---
|
||||
|
||||
### 2026-08-17 — Legendas saíam palavra a palavra centradas, e não como a composição progressiva diagramada da referência
|
||||
|
||||
- **Sintoma:** o usuário mandou o reel de referência (@fernandoluz.d) e disse
|
||||
"elas devem aparecer assim": `[que vão] / [melhorar] / [sua legenda]` — um
|
||||
bloco por trecho, palavra-chave grande em serifada itálica, complementares
|
||||
pequenas em grotesca, linhas escalonadas. O gerador entregava um `<title>`
|
||||
por PALAVRA, todos na mesma família, cada linha centrada.
|
||||
- **Causa raiz:** `layout_sentence` empacota palavra a palavra e centra cada
|
||||
linha; o `rhythm` variava tamanho/cor por índice (indigo/amarelo/cinza), não
|
||||
por papel semântico da palavra. Nenhum dos dois produz a diagramação.
|
||||
- **Onde:** `fcpxml/text_layout.py` (`compose_sentence`, `pick_emphasis_index`,
|
||||
`PlacedBlock`), `fcpxml/models.py` (looks editoriais, `granularity`),
|
||||
`fcpxml/writer.py` (emissão por unidade), `fcpxml/font_metrics.py`,
|
||||
`server.py` (parâmetros da tool).
|
||||
- **Tentativas que falharam:** tentar aproximar o visual só trocando os
|
||||
tamanhos do `rhythm` — sem agrupar as palavras de apoio num único título, o
|
||||
resultado continua sendo legenda corrida.
|
||||
- **Solução adotada:** modo `granularity="phrase"` (padrão): a frase vira
|
||||
linhas — apoio antes, palavra-chave sozinha, apoio depois —, uma linha por
|
||||
`<title>`, entrando no instante da sua primeira palavra e todas limpando
|
||||
juntas. Destaque em Playfair Display Medium Italic (métricas reais extraídas
|
||||
da fonte instalada e embutidas em `font_metrics`), apoio em Helvetica Neue
|
||||
Bold, tudo branco, linhas escalonadas por `REFERENCE_STAGGER_RATIO`. O modo
|
||||
antigo continua disponível em `granularity="word"`.
|
||||
- **Aprendizado:** o destaque é semântico, não posicional — escolher a palavra
|
||||
por índice num ciclo nunca reproduz uma diagramação. E toda fonte nova exige
|
||||
métricas reais antes de entrar no layout: sem elas, a medição de largura
|
||||
erra e duas linhas colidem.
|
||||
- **Estado:** `resolvido`
|
||||
|
||||
---
|
||||
|
||||
### 2026-08-17 — Importação recusada: `id` do `<text-style-def>` derivado do texto da legenda não é um XML Name válido
|
||||
|
||||
- **Sintoma:** ao gerar títulos/legendas, a validação de DTD falhava com
|
||||
`Syntax of value for attribute ref of text-style is not valid` +
|
||||
`Syntax of value for attribute id of text-style-def is not valid`, e o Final
|
||||
Cut recusava o arquivo na importação.
|
||||
- **Causa raiz:** `_make_text_title_clip` montava o id como
|
||||
`f"{name}_ts0"`, e `name` vem do texto da legenda
|
||||
(`"3 coisas que você precisa saber - Text"`). No DTD, `id` é do tipo `ID` e
|
||||
`ref` do tipo `IDREF`: o valor precisa ser um **XML Name** — sem espaços,
|
||||
sem acentos, nunca começando por dígito. Os três casos apareciam de uma vez
|
||||
em texto português.
|
||||
- **Onde:** `fcpxml/writer.py` (`_make_text_title_clip`, novo
|
||||
`_unique_text_style_id`).
|
||||
- **Tentativas que falharam:** confiar em `_sanitize_xml_value`, que protege
|
||||
*conteúdo* de atributo (CDATA) mas não impõe as regras de XML Name.
|
||||
- **Solução adotada:** `_unique_text_style_id()` — dobra o texto para ASCII
|
||||
(NFKD), troca tudo que não seja `[A-Za-z0-9_.-]` por `_`, prefixa com `ts_`
|
||||
(garante início por letra, inclusive quando o texto é só CJK/emoji e o slug
|
||||
fica vazio) e sufixa um contador conferido contra um cache de ids do
|
||||
documento, mantendo unicidade document-wide sem varrer a árvore por título.
|
||||
- **Aprendizado:** todo atributo do tipo `ID`/`IDREF` no FCPXML precisa ser
|
||||
gerado, nunca derivado de texto do usuário. Sanitizar valor de atributo e
|
||||
sanitizar identificador são problemas diferentes.
|
||||
- **Estado:** `resolvido`
|
||||
|
||||
---
|
||||
|
||||
### 2026-08-17 — Títulos ("Essencial - Título"/"Título Básico") nunca apareciam no FCP; o template que renderiza é o "Text" (Basic Text)
|
||||
|
||||
- **Sintoma:** título gerado no início do vídeo ficava invisível ou "sumia da
|
||||
timeline" (mas continuava na lista de clipes), mesmo com posição/offset
|
||||
aparentemente corretos. Com o template animado, o texto não desenhava; com o
|
||||
estático, o título caía antes do in-point do clipe e o FCP o descartava.
|
||||
- **Causa raiz (duas, no mesmo ciclo):**
|
||||
1. Os dois templates que usávamos — "Essencial - Título"
|
||||
(`Essential Title.moti`) e "Título Básico" (`Bumper:Opener/Basic
|
||||
Title.moti`) — **não resolvem para um template desenhável** no FCP. A
|
||||
importação é silenciosa: nada aparece, sem erro. É o MESMO modo de falha
|
||||
silenciosa já registrado duas vezes antes nesta sessão (uid fabricado).
|
||||
2. No caminho `animated=False`, o offset era gravado como **relativo**
|
||||
(`0s`) em vez de coordenadas de mídia-fonte (`start` do clipe-pai +
|
||||
relativo). O FCP lê `0s` como "0s da mídia", antes do in-point do clipe
|
||||
(`start="220062843/24000s"`), então o título nunca cai sobre o vídeo.
|
||||
- **Onde:** `fcpxml/writer.py` — `_TEXTO_TITLE_UID`, `_BASIC_TITLE_UID`,
|
||||
`_TEXTO_TITLE_PARAMS`, `_make_texto_title_clip`, `_make_basic_title_clip`,
|
||||
`generate_dynamic_subtitles`.
|
||||
- **Como o usuário resolveu:** criou dois títulos à mão no FCP e exportou
|
||||
(`teste.fcpxmld` e `posição.fcpxmld`, FCP 1.14 em inglês). Ambos usam o
|
||||
template **"Text"** (`uid=".../Titles.localized/Basic
|
||||
Text.localized/Text.localized/Text.moti"`, `name="Text"`), `start` fixo
|
||||
`86486400/24000s`, e um bloco de `<param>` com margens/alinhamento/`Custom
|
||||
Speed` (com `<keyframeAnimation>` de tempos nominais constantes). A posição
|
||||
é o param `Position` (chave `.../13260/3296672360/1/100/101`) com valor
|
||||
estático `"x y"` — **sem** `<adjust-transform>`.
|
||||
- **Solução adotada:** substituir os dois templates por um único "Text"
|
||||
(Basic Text), copiado verbatim dos exports reais. Novo
|
||||
`_make_text_title_clip` + `_ensure_text_title_effect` + `add_text_title`.
|
||||
`generate_dynamic_subtitles` agora usa sempre o "Text" e grava offset em
|
||||
coordenadas de mídia-fonte (`start` do pai + relativo) para **todos** os
|
||||
títulos; posição via param `Position`, não `adjust-transform`. Removidos os
|
||||
templates/código morto "Essencial - Título"/"Título Básico".
|
||||
- **Aprendizado:** o único teste que vale para template de título é um
|
||||
roundtrip de importação REAL no FCP — e o padrão-ouro é o export que o
|
||||
próprio FCP produz quando o usuário adiciona o título à mão. Quando isso
|
||||
existir, copiar **verbatim** (uid, params, `start`) e não "simplificar"
|
||||
nada. Título conectado SEMPRE usa `start` do clipe-pai como origem do
|
||||
offset, nunca `0s`.
|
||||
- **Estado:** `resolvido` no XML — pendente de confirmação de importação real
|
||||
no FCP pelo usuário.
|
||||
|
||||
---
|
||||
|
||||
### 2026-08-17 — Legendas dinâmicas sobrepondo entre clipes: título conectado NÃO é aparado pelo out-point do clipe-pai
|
||||
|
||||
- **Sintoma:** ao gerar, blocos de legenda de um clipe continuavam na tela por
|
||||
cima das legendas do clipe seguinte — duas frases desenhadas ao mesmo tempo.
|
||||
- **Causa raiz:** um `<title>` conectado a um `asset-clip` **não** é cortado
|
||||
pelo fim do clipe-pai; o FCP segue desenhando sobre o que vier depois. O
|
||||
último bloco de cada clipe terminava no `end` da última palavra do Whisper —
|
||||
que frequentemente ultrapassa o corte — e palavras cujo `start` já caía
|
||||
depois do corte também eram emitidas.
|
||||
- **Onde:** `fcpxml/writer.py`, `generate_dynamic_subtitles()`.
|
||||
- **Tentativas que falharam:** comprimir a palavra tardia para o último frame
|
||||
do clipe (empilhava vários títulos no mesmo frame e na mesma lane).
|
||||
- **Solução adotada:** descartar palavras que começam depois da duração do
|
||||
clipe-pai e limitar (`clamp`) o fim de cada bloco a essa duração. Testes em
|
||||
`tests/test_dynamic_subtitles.py::TestClipBoundaryClamping`.
|
||||
- **Aprendizado:** nada anexado a um clipe pode sobreviver ao próprio clipe;
|
||||
tempos vindos do Whisper precisam sempre ser recortados pela janela do clipe,
|
||||
não só filtrados pelo `start`.
|
||||
- **Estado:** `resolvido`
|
||||
|
||||
---
|
||||
|
||||
### 2026-08-17 — Palavras sobrepostas na tela: largura de texto ESTIMADA subestimava; solução foi embutir as métricas reais das fontes
|
||||
|
||||
- **Sintoma:** o usuário reportou que as palavras apareciam **todas sobrepostas** no Final
|
||||
Cut. A verificação automática do XML dizia "0 sobreposições" — porque conferia contra a
|
||||
minha própria estimativa de largura, não contra o que o FCP realmente desenha. Verificar
|
||||
um cálculo com o mesmo cálculo não verifica nada.
|
||||
- **Duas causas, uma de processo e uma técnica:**
|
||||
1. **Processo:** o arquivo que o usuário importou era a versão anterior, gerada antes do
|
||||
posicionamento existir (todas as palavras em `0 -45.4935`, literalmente no mesmo
|
||||
ponto). Como o caminho de saída é sempre o mesmo (`_dynamic_subtitles.fcpxmld`), é
|
||||
fácil reabrir a versão velha sem perceber.
|
||||
2. **Técnica, e real:** `measure_text` estimava a largura por uma tabela AFM genérica de
|
||||
Helvetica. Comparada com as fontes reais do macOS, o erro ia de **-1,4% a +7,0%** — e
|
||||
o caso negativo é fatal: uma largura menor que a real faz duas palavras encostarem.
|
||||
"dificuldade" a 170pt media 867,8 contra 880,1 reais.
|
||||
- **Investigação:** as variantes reais (`Helvetica.ttc`, `HelveticaNeue.ttc`) diferem entre
|
||||
si em até **21,5% do em** em alguns glifos — Light, Regular, Neue e Light Italic têm
|
||||
avanços distintos. Nenhuma tabela única serve para todas.
|
||||
- **Solução adotada:** extrair os avanços reais das fontes do sistema com `fontTools` e
|
||||
**embutir como tabela** em `fcpxml/font_metrics.py` (9 variantes × 143 glifos). O
|
||||
`fontTools` foi usado só na geração, via `uv run --with` — **não** virou dependência do
|
||||
projeto, e o layout não lê fonte em runtime, então o resultado é idêntico em qualquer
|
||||
máquina. Erro medido depois: **+2,0% constante** (só a margem de segurança), nunca
|
||||
abaixo. Margem de segurança reduzida de 1,03 para 1,02, já que a medida agora é exata.
|
||||
Espaço entre palavras passou de 5 pontos fixos para 14% do corpo da fonte maior.
|
||||
- **Ferramenta que destravou o problema:** gerar um **preview HTML** que desenha as
|
||||
palavras nas posições calculadas, com as fontes e tamanhos reais. Permite ver o layout
|
||||
sem reimportar no FCP a cada tentativa. Nota: o painel de preview bloqueia JavaScript
|
||||
(CSP), então o HTML precisa ser estático, com as posições já escritas no `style` de cada
|
||||
elemento — nada de calcular no navegador.
|
||||
- **Aprendizado:** **nunca validar uma saída com a mesma estimativa que a produziu.** Se o
|
||||
código estima larguras, a verificação tem de medir contra a fonte real, senão ela apenas
|
||||
confirma o próprio erro. E quando existe uma fonte de verdade acessível (o arquivo de
|
||||
fonte no disco), extrair os dados dela e embutir sai mais barato e mais exato do que
|
||||
qualquer aproximação — sem custo de dependência.
|
||||
- **Estado:** `resolvido` (layout aprovado pelo usuário no preview HTML; confirmação de
|
||||
importação no FCP pendente)
|
||||
|
||||
### 2026-08-17 — Calibrar coordenadas de título pedindo um export ao usuário, em vez de adivinhar a escala
|
||||
|
||||
- **Sintoma:** para posicionar cada palavra na tela era preciso escrever o param
|
||||
`Posição` do template Essential Title, mas não havia como saber a unidade nem a escala.
|
||||
O único valor existente no código era `0 -45.4935`, idêntico em todos os títulos —
|
||||
variância zero, portanto nada a inferir.
|
||||
- **Risco reconhecido antes de agir:** este mesmo arquivo já registra (entrada de
|
||||
2026-08-14) que um `<param name="Position">` **fabricado** foi removido justamente por
|
||||
ser inadivinhável, e que nem o DTD nem os testes unitários pegam `key`/valor inválido.
|
||||
Chutar aqui reproduziria a falha silenciosa pela terceira vez.
|
||||
- **Solução adotada:** em vez de estimar, pedir ao usuário um export do Final Cut com
|
||||
palavras posicionadas à mão. Ele enviou `Exemplo Letra.fcpxmld` (projeto 2160x3840) com
|
||||
a frase "Toda a minha vida, assim," — cinco palavras posicionadas no Inspetor, o resto
|
||||
no default. Três fatos saíram dos números:
|
||||
1. **Posição usa a mesma unidade que `fontSize`.** As distâncias centro a centro na
|
||||
linha 1 (254,06 e 265,08) batem com a soma das meias-larguras calculadas pelas
|
||||
métricas Helvetica nos tamanhos 170/128/151 (245,1 e 265,0). Uma unidade diferente
|
||||
apareceria como razão constante; não há nenhuma.
|
||||
2. **O canvas é 1080x1920 pontos** — metade do quadro, porque o FCP posiciona em pontos
|
||||
sobre mídia 2x. A linha 1 vai de -460,3 a +495,7, preenchendo essa largura com
|
||||
margens pequenas, exatamente como o quadro de referência aparenta.
|
||||
3. **y cresce para cima**: "vida," (linha 2) em -233,65 contra a linha 1 em ~-101.
|
||||
Também desambiguou **qual** param responde ao Inspetor: as cinco palavras carregam
|
||||
valores distintos em `9999/10085/10086/1/100/101`, enquanto `.../2/358` permanece
|
||||
`0 69` em todos os títulos do arquivo.
|
||||
- **Validação:** o layout recalculado reproduz o do usuário — espaçamento entre linhas
|
||||
131,8 contra 132,7 (erro de 0,7%) e o y das duas linhas coincidindo na casa decimal.
|
||||
Os valores viraram testes (`tests/test_text_layout.py`,
|
||||
`TestCalibrationAgainstRealExport`), então qualquer regressão de escala falha.
|
||||
- **Descoberta colateral:** o espaçamento entre linhas do usuário (132,65) é menor que o
|
||||
corpo da maior fonte da linha (170), o que só fecha porque o texto ocupa a altura de
|
||||
caixa-alta (~0,75 do corpo), não o em-box inteiro. Usar o em-box afastaria as linhas
|
||||
~40% a mais do que ele fez.
|
||||
- **Aprendizado:** quando um valor não é derivável dos dados em mãos, **pedir um artefato
|
||||
de calibração ao usuário custa minutos e elimina a adivinhação**. Cinco palavras
|
||||
arrastadas à mão renderam escala, unidade, orientação do eixo, espaçamento e a paleta —
|
||||
tudo o que três rodadas anteriores de chute não conseguiram. E vale desconfiar de
|
||||
qualquer constante que apareça idêntica em todas as instâncias de um arquivo: variância
|
||||
zero significa que ela nunca foi exercitada, não que esteja certa.
|
||||
- **Estado:** `resolvido`
|
||||
|
||||
### 2026-08-17 — Legendas dinâmicas invisíveis porque eram geradas como CAPTION, não como TÍTULO — e a "referência verificada" do código nunca tinha funcionado
|
||||
|
||||
- **Sintoma:** legendas dinâmicas **nunca** apareceram no Final Cut. Importava sem
|
||||
nenhum erro, DTD passava, e nada era desenhado sobre o vídeo. Sintoma idêntico ao das
|
||||
entradas anteriores (uid inválido → descarte silencioso), o que levou várias rodadas de
|
||||
correção a atacarem o alvo errado (uid, offset, dispatch de builder).
|
||||
- **Causa raiz:** o programa emitia **caption**, não **título animado**. Duas coisas
|
||||
acopladas, ambas erradas:
|
||||
1. `_TEXTO_TITLE_UID` apontava para `.../Subtitles.localized/Subtitle.localized/Subtitle.moti`
|
||||
— o template de **legenda/caption** do FCP, não um template de título.
|
||||
2. Cada `<title>` recebia `role="subtitles.subtitles-1"`. Esse role faz o Final Cut
|
||||
tratar o elemento como legenda e roteá-lo para a **pista de captions**, que não é
|
||||
desenhada sobre o vídeo a menos que a exibição de legendas esteja ligada.
|
||||
- **Como foi descoberto:** o usuário montou títulos à mão dentro do FCP e exportou
|
||||
(`Teste de Texto.fcpxmld`, `exemplo de arquivos.fcpxmld`). Comparando os títulos dele
|
||||
(que aparecem) com os gerados (que somem): os dele usam `Essential Title.moti` /
|
||||
`Essential Fade.moti` / `Text.moti` e **não têm atributo `role` nenhum`**; os gerados
|
||||
usavam `Subtitle.moti` + `role="subtitles.*"`. Prova adicional: no re-export, o FCP
|
||||
devolveu os `caption_*` com offsets negativos e fora do clipe (−2,13s num clipe de
|
||||
1,835s), porque os realocou como captions noutro sistema de coordenadas, enquanto os
|
||||
títulos manuais voltaram coerentes dentro do clipe (0,58s e 1,38s).
|
||||
- **Onde:** `fcpxml/writer.py` (`_TEXTO_TITLE_UID`, `_TEXTO_TITLE_ROLE`,
|
||||
`_TEXTO_TITLE_PARAMS`, `_TEXTO_TITLE_START`, `_make_texto_title_clip`),
|
||||
`fcpxml/models.py` (`DynamicSubtitleConfig`), `server.py`, `MacApp/Sources/*.swift`.
|
||||
- **Premissa falsa que ancorou os erros anteriores:** um comentário no próprio
|
||||
`fcpxml/writer.py` afirmava que `WHISPERX/code/"Teste do dia.fcpxmld"` era um export
|
||||
real do FCP *"actually imported and played back"*. O usuário confirmou que **nunca
|
||||
funcionou** — aquele arquivo é output do próprio programa. Como o comentário foi tratado
|
||||
como fonte de verdade, cada correção seguinte se apoiava nele e reproduzia a estrutura
|
||||
errada (inclusive o `role` de caption). A entrada anterior desta lista herdou o mesmo erro.
|
||||
- **Solução adotada:**
|
||||
1. `_TEXTO_TITLE_UID` → `.../Titles.localized/Essential Titles.localized/Essential Title.localized/Essential Title.moti`
|
||||
e nome do efeito → `"Essencial - Título"`.
|
||||
2. `_TEXTO_TITLE_ROLE` **removido** e `elem.set('role', ...)` eliminado de
|
||||
`_make_texto_title_clip`. Nenhum título gerado carrega `role`.
|
||||
3. `_TEXTO_TITLE_START` → `86486400/24000s` (valor que o FCP escreve para o Essential Title).
|
||||
4. `_TEXTO_TITLE_PARAMS` → só os 5 params de layout do Essential Title. Os ~75 params de
|
||||
animação foram deixados de fora de propósito: carregam `<keyframeAnimation>` com tempos
|
||||
**absolutos** calibrados à duração de uma instância específica, e replicá-los em títulos
|
||||
de outra duração produz animação truncada/congelada. Sem eles o template Motion anima
|
||||
pelos próprios defaults.
|
||||
5. Granularidade: `max_words_per_line` 4 → **1** e `lane_count` 3 → **9** (defaults
|
||||
alinhados em `models.py`, `server.py` e no MacApp), gerando um `<title>` por palavra.
|
||||
6. Comentários falsos reescritos apontando para os exports reais do usuário.
|
||||
7. Novo teste de regressão `test_titles_carry_no_caption_role`.
|
||||
- **Aprendizado:** **legenda dinâmica = título animado, não caption.** Um
|
||||
`role="subtitles.*"` num `<title>` o esconde atrás do toggle de legendas — importa limpo
|
||||
e nunca aparece, exatamente o mesmo sintoma de um uid inválido, o que torna os dois fáceis
|
||||
de confundir. E, mais importante: **um comentário dizendo "verificado" não é verificação.**
|
||||
Só vale como referência um arquivo que o usuário confirmou ter saído do Final Cut. Antes de
|
||||
tratar qualquer arquivo como ground truth, checar se ele é output do próprio programa —
|
||||
se os `name=` seguem o padrão que o código gera (`caption_<hex>`), ele é.
|
||||
- **Estado:** `resolvido` no XML (verificado na saída: efeito Essential Title, zero roles,
|
||||
1 título por palavra, offsets dentro da janela do clipe) — **pendente de confirmação de
|
||||
importação real no FCP pelo usuário**, que é o único teste que conta neste histórico.
|
||||
|
||||
### 2026-08-17 — Legendas dinâmicas não apareciam no FCP: dispatch misturava Título Básico com bloco/role do Subtitle + `offset` em coordenada errada (timeline em vez de mídia)
|
||||
|
||||
> **Nota (revisão posterior):** esta entrada trata `WHISPERX/code/"Teste do dia.fcpxmld"`
|
||||
> como export real verificado do FCP. **Isso está errado** — aquele arquivo é output do
|
||||
> próprio programa e nunca funcionou. Ver a entrada acima. A parte de `offset` em
|
||||
> coordenada de mídia continua correta (reconfirmada contra `exemplo de arquivos.fcpxmld`),
|
||||
> mas o `role="subtitles.*"` e o template Subtitle.moti descritos aqui eram a causa real
|
||||
> das legendas invisíveis.
|
||||
|
||||
- **Sintoma:** ao gerar legendas dinâmicas num projeto real (`Depoimento da Erika - Original.fcpxmld`, clipe único com `start="226220995/24000s"` ≈ 256.59s na mídia de origem) e abrir o resultado no Final Cut, as legendas simplesmente não apareciam — nem na timeline, nem na lista de roles. O app chamava o handler sem `animated`, então caía no default e produzia um `<title>` com `ref="r_title_basic"` (Título Básico) mas com `role="subtitles.subtitles-1"`, `start="86400314/24000s"` e os 19 params do template "Legenda" — um híbrido impossível de resolver no FCP (descarte silencioso, padrão documentado nas entradas de 2026-08-15/2026-08-17).
|
||||
- **Causa raiz (dois bugs num ciclo):**
|
||||
1. `generate_dynamic_subtitles` (`fcpxml/writer.py`) escolhia o efeito certinho por `config.animated` (linhas 2893-2896), mas SEMPRE construía o clip com `_make_texto_title_clip` (linha 2944) — ignorando o `animated`. `_make_basic_title_clip` (que monta o Título Básico sem role/start e só os 2 params `Compactar`/`Alinhamento`) existia mas **nunca era chamado** (código morto). Resultado default: ref do básico + corpo do subtitle → o FCP descarta silenciosamente.
|
||||
2. Mesmo no caminho animado, o `offset` era escrito como valor **relativo à timeline** (ex.: `1/4800s`). Mas o export real verificado (`WHISPERX/code/"Teste do dia.fcpxmld"`) mostra que o template "Legenda"/Subtitle posiciona os captions em **coordenadas da mídia de origem**: `offset = start_do_clipe + relativo` (ex.: `240822582/24000s` = start `240817577/24000s` + 0.2085s), enquanto o "Título Básico" (r3) usa offset relativo (ex.: `1001/4800s`). Escrever offset relativo pequeno num clip cujo start é ~256s/9000s faz o FCP ler ~0s da mídia — antes do in-point do clipe — e a legenda cai "fora" (caso `Legendas fora.fcpxmld`).
|
||||
- **Onde:** `fcpxml/writer.py` (`generate_dynamic_subtitles`, linhas ~2893-2956), `fcpxml/models.py` (`DynamicSubtitleConfig.animated`, default errado `False`), `tests/test_dynamic_subtitles.py`.
|
||||
- **Solução adotada:**
|
||||
1. `generate_dynamic_subtitles` agora despacha pelo `config.animated`: `True` → `_make_texto_title_clip` (template "Legenda"), `False` → `_make_basic_title_clip` (Título Básico). O híbrido impossível não existe mais.
|
||||
2. No caminho animado, `offset = media_origin + snap(relativo)` com `media_origin = _parse_time(parent.get('start'))` (coordenada da mídia, igual ao export real); no estático, offset permanece relativo (como r3).
|
||||
3. `DynamicSubtitleConfig.animated` agora é `True` por padrão (decisão do usuário: a feature é a legenda animada/ediável; `False` só para texto queimado no frame).
|
||||
4. Adicionados testes de regressão (`test_animated_offset_uses_source_media_coordinates`, `test_animated_effect_is_legenda_subtitle`, `test_static_mode_uses_basic_title_no_role`, `test_animated_and_static_use_separate_effects`).
|
||||
- **Aprendizado:** dois templates diferentes num mesmo método exigem dispatch por builder, e cada um tem seu próprio sistema de coordenadas de `offset` — o template de caption/legenda usa a posição na mídia de origem (nunca um offset relativo pequeno), o título estático usa offset relativo ao clipe. Comparar sempre com o export real (coordenadas + role + params) antes de fechar uma estrutura, e nunca ignorar o branch `else` de um `if config.X` que decide o template — builder único = mistura de corpo de um template com ref de outro = descarte silencioso no FCP.
|
||||
- **Estado:** `resolvido`
|
||||
|
||||
---
|
||||
|
||||
### 2026-08-17 — Clipe-fantasma de 1 frame no início e no fim após remoção de silêncio (raiz real no gerador, diferente da entrada de 2026-08-14)
|
||||
|
||||
- **Sintoma:** usuário testou `remove_silences` num projeto real (`Depoimento da Erika_silence_removed.fcpxmld`) e reportou dois clipinhos minúsculos: o primeiro e o último clipe da spine gerada tinham `duration="1001/24000s"` — exatamente 1 frame a 23.976fps.
|
||||
- **Causa raiz:** diferente da entrada de 2026-08-14 ("Micro-clips... são criados pelo FCP, não pelo corte") — aqui os slivers já vinham no `Info.fcpxml` bruto gerado pelo programa, confirmado lendo o XML direto, sem passar pelo FCP. `handle_remove_media_silence`/`cut_clip_ranges` (`fcpxml/writer.py`) aplica um `padding` (respiro, padrão 0.05s) antes/depois de cada trecho de silêncio cortado. Quando o silêncio detectado toca a própria borda do clipe (começo ou fim), não sobra fala nenhuma daquele lado para o padding "respirar perto de" — o padding vira, sozinho, o segmento "kept" (mantido) daquela ponta, e após o snap para o grid de frames (`snap_seconds_to_frame`) esse segmento de ~0.05s vira exatamente 1 frame, virando clipe próprio em vez de ser absorvido.
|
||||
- **Onde:** `fcpxml/writer.py::FCPXMLModifier.cut_clip_ranges`, construção da lista `keeps` (complemento dos `cut_ranges` mesclados).
|
||||
- **Tentativas que falharam:** n/a — diagnóstico direto lendo o XML bruto e cruzando com a lógica de `cut_clip_ranges`; os números batem exatamente (0.05s de padding ≈ 1.2 frames a 23.976fps → arredonda para 1 frame).
|
||||
- **Solução adotada:** a pedido do usuário ("em vez de criar esse [micro-clipe] novo, ele pode pegar o próprio segundo clipe e aumentar a duração dele para começar antes") — depois de montar `keeps`, se o primeiro segmento tiver menos que ~2 frames de duração, ele é fundido no segmento seguinte (que passa a começar mais cedo); simétrico no fim (o penúltimo segmento passa a terminar mais tarde, absorvendo o último). Isso também restaura o pequeno trecho de silêncio adjacente que teria sido cortado ali — troca aceitável por não deixar clipe-fantasma na timeline.
|
||||
- **Aprendizado:** ao gerar clipes a partir de um algoritmo de corte com padding, sempre checar segmentos residuais nas BORDAS da mídia/clipe (não só entre dois cortes no meio) — o padding não tem "vizinho de fala" do lado de fora do clipe, então o caso de borda precisa de tratamento explícito (fundir em vez de emitir). Existe um padrão irmão já usado alhures no código (`_absorb_into_neighbor`, `fcpxml/writer.py:1112`) para a mesma ideia geral.
|
||||
- **Estado:** `resolvido`
|
||||
|
||||
---
|
||||
|
||||
### 2026-08-17 — As 481 legendas do usuário ficaram todas presas num único clipe errado: `generate_dynamic_subtitles` resolvia o clipe-pai por `name`, ambíguo depois de corte de silêncio
|
||||
|
||||
- **Sintoma:** mesmo com o `uid` e os `offset`/`duration` corrigidos (entradas abaixo), o usuário mandou o `.fcpxmld` real depois de importar no FCP ("como ficou depois de importar") e as 481 legendas apareciam todas grudadas em ~8-17 segundos de um único clipe da timeline, com frases completamente sem relação entre si ("vontade.", "Sou erica Fernanda, tenho", "sou casada.") — claramente vindas de pontos bem distantes de uma entrevista de ~17 minutos, não de um trecho de 8 segundos.
|
||||
- **Causa raiz:** `server.py::handle_generate_dynamic_subtitles` itera cada clipe da spine (`for el in spine_clips`), calcula a janela de palavras correta *relativa àquele clipe* (`el`), mas chamava `modifier.generate_dynamic_subtitles(name, ...)` passando `name = el.get("name", "")` — uma STRING — em vez do elemento. Depois de qualquer `remove_silences`/corte com ripple, TODOS os fragmentos resultantes de um clipe original mantêm o mesmo `name` herdado (aqui, ~482 clipes, todos `name="0E6A8829"`, o nome do asset de origem). `_require_clip()` resolve por `self.clips[key]`, um dict indexado por `id` ou, na falta dele, por `name` (`fcpxml/writer.py:_index_elements`) — com nomes duplicados, cada novo clipe indexado SOBRESCREVE o anterior, então `self.clips["0E6A8829"]` acaba apontando para só UM clipe (o último indexado). Toda chamada do loop, para qualquer um dos 482 clipes reais, resolvia para esse mesmo clipe errado — empilhando ali as legendas de quase o vídeo inteiro.
|
||||
- **Onde:** `server.py` (`handle_generate_dynamic_subtitles`, a chamada a `modifier.generate_dynamic_subtitles`), `fcpxml/writer.py` (`generate_dynamic_subtitles`, `_require_clip`, `_index_elements`).
|
||||
- **Solução adotada:** `generate_dynamic_subtitles` agora aceita `parent_clip` como `str | ET.Element` — se receber o elemento diretamente, usa-o sem passar pelo lookup por nome; só cai em `_require_clip(name)` (mantido para compatibilidade com chamadas antigas/testes) quando recebe uma string. `server.py` foi atualizado para passar `el` (o elemento já em mãos no loop) em vez de `name`. Adicionado teste de regressão (`test_element_param_bypasses_ambiguous_duplicate_name_lookup`) que simula dois clipes com o mesmo `name` e confirma que passar o elemento anexa cada legenda ao clipe certo.
|
||||
- **Aprendizado:** **nunca identificar um clipe específico por `name` num handler que itera múltiplos clipes** — qualquer operação de corte/ripple/remoção de silêncio no FCPXML preserva o `name` original em todos os fragmentos resultantes, então `name` deixa de ser único assim que o timeline é editado. Sempre que o chamador já tem o `ET.Element` em mãos (por ter vindo de uma iteração como `_iter_spine_clips()`), passe o elemento adiante em vez de re-resolvê-lo por um identificador que pode colidir.
|
||||
- **Estado:** `resolvido`
|
||||
|
||||
---
|
||||
|
||||
### 2026-08-17 — Legendas dinâmicas sumiam silenciosamente do Final Cut por `uid` de efeito inválido (mistura de dois templates); dois bugs num só ciclo
|
||||
|
||||
- **Sintoma:** depois de corrigir os erros de frame-boundary do `offset`/`duration` (entrada abaixo, mesma sessão), o Final Cut não reportava mais nenhum erro de importação — mas os 481 `<title>` de legenda simplesmente não apareciam em lugar nenhum: nem na timeline, nem na lista de roles do projeto.
|
||||
- **Causa raiz:**
|
||||
1. O `uid` fixado em `_TEXTO_TITLE_UID` (`.../Titles.localized/Basic Text.localized/Text.localized/Text.moti`) mistura os nomes de dois templates diferentes ("Basic Text" e "Text") e não corresponde a nenhum Motion template real instalado no FCP. Quando o `uid` de um `<effect>` referenciado por um clipe conectado não resolve para um template existente, o Final Cut **descarta silenciosamente** os clipes conectados que dependem dele durante a importação — sem erro, sem aviso na UI. É a segunda vez que um `uid` fabricado aqui é a causa raiz (ver entrada de 2026-08-15 abaixo — da primeira vez foi o "Basic Title", desta vez foi um "Texto" que só passou pelos testes internos porque nunca foi de fato importado de novo no FCP depois de escrito).
|
||||
2. Um dos `<param>` copiados junto (`Opacidade` = `"0"`) fixava a opacidade do texto em zero — mesmo se o `uid` estivesse certo, o texto ficaria invisível.
|
||||
- **Onde:** `fcpxml/writer.py` — `_TEXTO_TITLE_UID`, `_TEXTO_TITLE_PARAMS`, `_TEXTO_TITLE_START`, `_ensure_texto_title_effect`, `_make_texto_title_clip`.
|
||||
- **Solução adotada:** o usuário identificou um export real, já importado e reproduzido com sucesso no FCP, presente no próprio repositório em `WHISPERX/code/"Teste do dia.fcpxmld"/Info.fcpxml` — efeito `r4` nomeado **"Legenda"**, `uid=".../Titles.localized/Subtitles.localized/Subtitle.localized/Subtitle.moti"`, usado em cinco `<title>` conectados por palavra/linha com `role="subtitles.subtitles-1"`. Copiado o `uid`, o `name` ("Legenda"), o `start` fixo (`86400314/24000s`, diferente do valor anterior), o atributo `role`, e o bloco de 19 `<param>` inteiro verbatim — que não inclui nenhum param de opacidade nem `<keyframeAnimation>` manual (a revelação palavra-a-palavra é nativa do template via os params `Animar`/`Intervalo`, não uma curva de velocidade fabricada como no template anterior).
|
||||
- **Aprendizado:** um `uid`/bloco de params "plausível" que passa nos testes internos (`fcpxml/dtd.py`, pytest) **não é prova de que é real** — só um roundtrip de importação de verdade no FCP prova isso, e mesmo assim a falha pode ser silenciosa (sem erro) em vez de uma rejeição explícita. Regra geral reforçada: nunca fabricar/adivinhar `uid` de efeito nativo do FCP, mesmo que o formato pareça consistente com outros exports reais — sempre copiar de um `.fcpxmld`/`Info.fcpxml` que o usuário confirma ter sido importado e reproduzido com sucesso.
|
||||
- **Estado:** `resolvido`
|
||||
|
||||
---
|
||||
|
||||
### 2026-08-17 — Palavras das legendas dinâmicas empilhadas: o template "Essencial - Título" ANIMA por padrão e a animação ignora a posição estática — solução foi desligar `Animar`
|
||||
|
||||
- **Sintoma:** o usuário reportou, repetidamente, **todas as palavras uma em cima da outra** no Final Cut. O XML gerado tinha posições estáticas e distintas (verificado), mas o FCP **ignorava** a posição e empilhava tudo — o sintoma persistiu mesmo com `value="x y"` correto em cada palavra.
|
||||
- **Causa raiz (a definitiva):** o template "Essencial - Título" (Essential Title, Motion) tem um parâmetro **`Animar`** (Animate). No export de calibração (`Exemplo Letra.fcpxmld`, as 5 palavras que o usuário arrastou à mão e funcionaram), cada título carrega **~111 params**, incluindo `Animar = "4 (Tudo)"` mais dezenas de params de animação por caractere (`X/Y/Z deslocamento`, `Objeto Original`, `Deslocamento Inicial/Final`, `Direção`, `Velocidade Personalizada` com keyframes). Nós só escrevíamos 5 params de layout e **omitíamos o `Animar`**. Sem ele, o FCP usa a **animação default** do template — a animação de "Tudo" (fly-in 3D por caractere) — e **é essa animação que posiciona as letras**, não o nosso `Posição`. Resultado: cada caractere cai na posição default e tudo empilha. As palavras da calibração só ficaram no lugar porque o FCP escreveu o bloco de animação completo junto.
|
||||
- **Por que NÃO copiar o bloco de animação:** os params por caractere (`X deslocamento`, `Y deslocamento`, `Z deslocamento`, `Objeto Original`) têm valores **diferentes por palavra** (ex.: `X deslocamento = -960.047` em "Tod" vs `-243` em "a") — são dados 3D por caractere que o FCP calcula e que não dá para reproduzir. Já os `time` dos keyframes são **idênticos em todas as palavras** (`0s`, `1567433324/1000000000s`, `19915648/3840000s`, `6686008967/1000000000s`), confirmando que são a curva default do template, não calibrados por instância.
|
||||
- **Onde:** `fcpxml/writer.py::_make_texto_title_clip`, novo `_TEXTO_ANIMAR_KEYS` (10 chaves `.../201/203` de `Animar`), `tests/test_dynamic_subtitles.py`.
|
||||
- **Tentativas que falharam:** (1) embrulhar a posição em `keyframeAnimation` — piorou, pois o param é estático e o FCP descartou tudo para o default `0 -45.4935`; (2) reverter só para o valor estático — ainda empilhava, porque a animação default continuava ignorando a posição.
|
||||
- **Solução adotada:** escrever `Animar = "0 (Nenhum)"` nas 10 chaves de animação do template, desligando a animação. O título fica **estático** e respeita o `Posição` gravado; a revelação palavra-por-palavra continua vindo do `offset`/`duration` de cada palavra (não da animação). Teste atualizado: 5 params de layout + 10 `Animar = "0 (Nenhum)"`, sem `keyframeAnimation`.
|
||||
- **Aprendizado:** num template Motion de título, a **posição visual pode ser controlada pela animação, não pelo param de layout** — se um título "arrastado à mão" funciona e o gerado empilha, comparar o bloco de params **inteiro** (não só a posição) entre os dois arquivos. O `Animar` é o interruptor-mestre: `4 (Tudo)` anima (e aí só os dados 3D por caractere — irreproduzíveis — colocam as letras no lugar); `0 (Nenhum)` desliga e devolve o controle ao param `Posição`. E: a mesma conclusão errada foi registrada e corrigida duas vezes nesta sessão — a cada iteração, reler o export de calibração **inteiro** antes de decidir o mecanismo.
|
||||
- **Estado:** `resolvido` no XML (posições estáticas distintas + 10× `Animar = "0 (Nenhum)"` verificados no output real; 1124 testes verdes) — **pendente de confirmação de importação real no FCP pelo usuário**, único teste que conta neste histórico.
|
||||
|
||||
---
|
||||
|
||||
### 2026-08-17 — `offset`/`duration` de legendas dinâmicas fora do grid de frames (denominador `/23s` em vez de `/24000s`)
|
||||
|
||||
- **Sintoma:** importação real no Final Cut rejeitada com 457 de 481 erros "O item não está em um limite de quadro de edição", todos apontando para `offset`/`duration` de `<title>` com denominador `/23s` (ex.: `offset="2/23s"`, `duration="67/23s"`).
|
||||
- **Causa raiz:** `generate_dynamic_subtitles` (`fcpxml/writer.py`) construía cada offset/duration com `TimeValue.from_seconds(seconds, self.fps)`. Esse classmethod (`fcpxml/models.py`) faz `int(fps)` — para um projeto NTSC a 23.976fps (`frameDuration="1001/24000s"`, `self.fps ≈ 23.976`), `int(fps)` trunca para `23`, uma base de tempo inválida para o FCPXML. Todo o resto do XML (asset-clips, cortes de silêncio, sequence) já usava a base exata `1001/24000s`.
|
||||
- **Onde:** `fcpxml/writer.py:2841-2850` (chamada) e `fcpxml/models.py:314-318` (`TimeValue.from_seconds`, bug latente — outros call-sites como marcadores em `fcpxml/writer.py:1442,1511` usam o mesmo padrão e podem ter o mesmo problema em taxas NTSC, não corrigido nesta rodada por estar fora do escopo do bug relatado).
|
||||
- **Solução adotada:** trocado `TimeValue.from_seconds(start, self.fps)` / `TimeValue.from_seconds(end - start, self.fps)` por `self.snap_seconds_to_frame(...)` — helper já existente (`fcpxml/writer.py:746`) que usa a fração exata de `frameDuration` (via `frame_duration_fraction()`, `fcpxml/writer.py:727`) em vez do float truncado, já usado em outro lugar do writer para snap da spine. Também trocado `min_dur_seconds = 1.0 / self.fps` por `float(self.frame_duration_fraction())`.
|
||||
- **Aprendizado:** qualquer conversão de segundos-float para `TimeValue` num projeto FCPXML deve usar a fração exata do `frameDuration` do `<format>` da sequência (via `frame_duration_fraction()`/`snap_seconds_to_frame()`), nunca `int(fps)` — taxas NTSC (23.976/29.97/59.94) sempre truncam errado com um fps float.
|
||||
- **Estado:** `resolvido`
|
||||
|
||||
---
|
||||
|
||||
### 2026-08-15 — Abandonado o Compound Clip em `generate_dynamic_subtitles`; voltado a títulos soltos em lanes cicladas, com estrutura copiada de um export real
|
||||
|
||||
- **Sintoma/decisão:** mesmo depois de corrigir os 3 erros de importação do Compound Clip (entrada abaixo), o usuário decidiu recuar da abordagem por completo — "muito problema e muito erro" — e pediu para voltar ao básico: títulos soltos, direto na timeline, em várias lanes, sem Compound Clip.
|
||||
- **O que mudou:** `generate_dynamic_subtitles` não cria mais `<media>`/`<sequence>`/`<gap>`/`<ref-clip>` nenhum. Cada linha (chunk de palavras) vira um `<title>` autônomo, anexado direto no clipe pai via `_dtd_insert`, ciclando por `config.lane_count` lanes (round-robin). A duração de cada linha se estende até sua própria lane ser reaproveitada `lane_count` linhas depois (ou até seu próprio fim, se for uma das últimas) — isso empilha visualmente várias linhas ao mesmo tempo (efeito cascata) sem nunca sobrepor duas linhas na MESMA lane.
|
||||
- **Fonte da estrutura XML:** o usuário mandou um FCPXML real (`com exemplo de título.fcpxmld/Info.fcpxml`) com 3 títulos criados manualmente no FCP usando o template **"Texto"** (`uid=".../Titles.localized/Basic Text.localized/Text.localized/Text.moti"` — diferente do "Basic Title" usado antes). Copiei o bloco de `<param>` inteiro (margens, alinhamento, quebra automática, o par Opacidade/Velocidade Personalizada com `<keyframeAnimation>` que parece ser a animação de revelação nativa do template) e o atributo `start` fixo (`86486400/24000s`, idêntico nos três títulos do export) como constantes fixas em `_TEXTO_TITLE_PARAMS`/`_TEXTO_TITLE_START` — não tentei entender/simplificar esses valores, só copiei verbatim, já que "simplificar" um bloco de params reais foi exatamente o que causou os erros anteriores.
|
||||
- **Importante (o usuário corrigiu isso no meio da conversa):** os offsets/timings do arquivo de exemplo eram só ilustrativos — não estavam sincronizados com nenhuma fala real. O timing de verdade continua vindo 100% da transcrição Whisper (`words` com `start`/`end` reais), usando a mesma lógica de `TimeValue`/mapeamento fonte→timeline já estabelecida no projeto. Só a ESTRUTURA XML (uid, params, `start` fixo) foi copiada do exemplo, nunca os números de tempo.
|
||||
- **Onde:** `fcpxml/writer.py` (`FCPXMLModifier.generate_dynamic_subtitles`, `_make_texto_title_clip`, `_ensure_texto_title_effect`), `fcpxml/models.py` (`DynamicSubtitleConfig.lane_count` substituindo `lane`), `server.py`, `admin/models_api.py`, `MacApp/Sources/CaptionsView.swift`.
|
||||
- **Aprendizado:** quando o usuário oferece um export real do FCP como referência, tratar isso como fonte de verdade para a ESTRUTURA (uid, ordem de elementos, bloco de params), mas nunca para os NÚMEROS de tempo específicos de um exemplo ilustrativo — a menos que ele diga explicitamente que os números também são reais. E: depois de duas rodadas de erro de importação real, a abordagem mais simples e mais próxima de um export real validado sempre vale mais que uma abstração mais "elegante" (Compound Clip) que ninguém verificou contra o importador de verdade do FCP.
|
||||
- **Estado:** `resolvido`
|
||||
|
||||
---
|
||||
|
||||
### 2026-08-15 — Três rejeições de importação real no Final Cut Pro em `generate_dynamic_subtitles` (uid fabricado, `<title>` ancorado em `<gap>`, duração 0)
|
||||
|
||||
- **Sintoma:** ao importar de verdade no Final Cut Pro (não só validar internamente), o app recusou o `Info.fcpxml` com três classes de erro: (1) `uid=".../Titles.localized/Basic Text.localized/..." — O item não pôde ser lido`; (2) `Edição inválida sem nenhuma mídia respectiva` apontando para `.../gap[1]/title[1]`; (3) `Um valor inesperado foi encontrado (duration="0/1s")` em vários `<gap>` dentro dos `<media>` de legenda.
|
||||
- **Causa raiz:**
|
||||
1. O `uid` do efeito "Basic Title" foi **inventado** (nunca verificado contra um export real) — o caminho correto tem `Bumper:Opener.localized`, não `Basic Text.localized`.
|
||||
2. Os `<title>` por palavra estavam sendo anexados como *connected clip* (via `lane`) dentro de um `<gap>` usado só para "segurar" a duração da linha — mas um `<gap>` não é mídia, e FCP rejeita qualquer clipe conectado a um `<gap>` como âncora.
|
||||
3. Palavras com `start`/`end` muito próximos (ou vindas de um transcript com timestamps imprecisos) geravam durações que arredondavam para 0 frames no fps da sequência.
|
||||
- **Onde:** `fcpxml/writer.py`, `FCPXMLModifier.generate_dynamic_subtitles` / `_make_title_clip` / `_ensure_basic_title_effect`.
|
||||
- **Tentativas que falharam:** validar apenas com `fcpxml/dtd.py` e com os testes unitários — nenhum dos dois pega uid/params inválidos (o DTD da Apple não estava disponível neste ambiente) nem a regra "conectado precisa de mídia real por trás", que só o importador real do FCP aplica.
|
||||
- **Solução adotada:**
|
||||
1. Encontrado um `uid` **real e correto** dentro do próprio repositório, em `WHISPERX/code/*.fcpxmld/Info.fcpxml` (um projeto de verdade exportado pelo usuário) — usado esse valor em vez de inventar um novo. Os únicos dois `<param>` que esse template realmente usa (`Compactar` e `Alinhamento`, com `key` fixo) também foram copiados de lá; o `<param name="Position">` fabricado foi removido.
|
||||
2. Reestruturado o compound clip: os `<title>` por palavra agora são conteúdo **primário** da spine interna (como o próprio `<title>` já suporta ser primário), com `<gap>` só preenchendo silêncio real entre eles — nunca mais como pai/âncora de um clipe conectado.
|
||||
3. Adicionado um piso de duração mínima de 1 frame (`1.0 / fps`) tanto por palavra quanto pela linha inteira, em vez do padding fixo de `0.01s` que arredondava para 0 em fps altos.
|
||||
- **Aprendizado:** **nunca fabricar `uid`/`key` de efeitos nativos do FCP** — eles não são adivinháveis e a validação interna (`fcpxml/dtd.py`) só pega isso se o Final Cut Pro estiver instalado localmente; sempre que possível, procurar/pedir um export real como referência antes de inventar. Além disso, "conectado" (`lane`) sempre precisa de um clipe com mídia de verdade por trás — um `<gap>` nunca serve de âncora, mesmo que pareça funcionar nos testes internos (que só checam a árvore XML, não as regras semânticas do importador do FCP). E qualquer duração calculada a partir de subtração de floats de transcrição deve ter um piso de `1/fps`, nunca uma constante fixa pequena.
|
||||
- **Estado:** `resolvido`
|
||||
|
||||
---
|
||||
|
||||
### 2026-08-15 — Corpo de `cmd_add_zoom` colado por engano dentro de `cmd_remove_silences` em `admin/models_api.py`
|
||||
|
||||
- **Sintoma:** lint (`ruff`) falhando com `F821 Undefined name 'clip_id'` e `F841 Local variable 'clip_id' is assigned to but never used`, em duas funções diferentes do bridge Python↔Swift.
|
||||
- **Causa raiz:** durante uma edição manual (introduzindo `_derived_output()` para suportar `output_dir` configurável), o corpo inteiro de `cmd_add_zoom` (checagem de `clip_id` + chamada a `handle_add_zoom`) foi colado dentro de `cmd_remove_silences`, antes do bloco correto que já chamava `handle_remove_media_silence` — deixando `cmd_remove_silences` com código morto/quebrado (chamava o handler errado e checava uma variável inexistente) e `cmd_add_zoom` truncado (só validava `path`/`clip_id` e não fazia mais nada).
|
||||
- **Onde:** `admin/models_api.py`, funções `cmd_remove_silences` e `cmd_add_zoom`.
|
||||
- **Tentativas que falharam:** n/a — identificado direto pelo lint e por leitura do código antes de qualquer tentativa de correção.
|
||||
- **Solução adotada:** movido o fragmento (checagem de `clip_id` + chamada a `handle_add_zoom`) de volta para dentro de `cmd_add_zoom`, removendo-o de `cmd_remove_silences`, que voltou a conter só a chamada correta a `handle_remove_media_silence`.
|
||||
- **Aprendizado:** depois de qualquer edição manual em `admin/models_api.py` (ou qualquer arquivo com várias funções `cmd_*` de shape parecido), rodar o lint imediatamente pega colagens cruzadas de função — `ruff` acusa tanto a variável usada-mas-nunca-definida (função que perdeu o trecho) quanto a definida-mas-nunca-usada (função que ganhou o trecho de outra) no mesmo commit, o que é um sinal forte de bloco trocado de lugar, não dois bugs independentes.
|
||||
- **Estado:** `resolvido`
|
||||
|
||||
---
|
||||
|
||||
### 2026-08-15 — IDs duplicados de `text-style-def` rejeitados pelo Final Cut Pro em legendas dinâmicas
|
||||
|
||||
- **Sintoma:** ao importar o FCPXML gerado por `generate_dynamic_subtitles`, o Final Cut Pro recusava o arquivo com "A validação DTD falhou" e uma lista de IDs como `caption_L0W0_ts0 already defined`.
|
||||
- **Causa raiz:** os IDs de `<title>`/`<text-style-def>` eram montados como `caption_L{line_idx}W{word_idx}_ts{i}`, com `line_idx`/`word_idx` reiniciando em 0 a cada chamada de `generate_dynamic_subtitles`. Como o handler (`handle_generate_dynamic_subtitles` em `server.py`) chama esse método uma vez por clipe da spine na mesma instância de `FCPXMLModifier`, múltiplos clipes geravam exatamente os mesmos IDs — o DTD exige unicidade de ID no documento inteiro, não por clipe.
|
||||
- **Onde:** `fcpxml/writer.py`, método `FCPXMLModifier.generate_dynamic_subtitles`.
|
||||
- **Tentativas que falharam:** nenhuma alternativa testada — o padrão (índices posicionais que resetam por chamada) era o bug desde a primeira implementação; só foi pego ao testar a importação real no Final Cut Pro.
|
||||
- **Solução adotada:** trocar o índice posicional por um prefixo derivado de `uuid.uuid4().hex[:8]` por linha (`caption_{line_uid}_W{word_idx}`), garantindo unicidade mesmo entre chamadas repetidas na mesma instância do modifier. De quebra, corrigido também: o `<ref-clip>` (compound clip) estava sendo anexado via `ET.SubElement` direto no clipe pai, o que o colocava depois de marcadores (`keyword`, `chapter-marker`) já existentes — violando a ordem de filhos exigida pelo DTD (itens-âncora como `ref-clip` devem vir antes de itens de marcador). Trocado para `_dtd_insert(parent, ref_clip)`, que já resolve essa ordenação.
|
||||
- **Aprendizado:** qualquer ID gerado dentro de um método chamado em loop (uma vez por clipe/iteração) sobre a MESMA árvore XML não pode depender de um índice que reinicia a cada chamada — precisa ser único por invocação (UUID, contador persistido na instância, ou verificação contra os IDs já existentes no documento). Além disso, qualquer elemento anexado a um clipe existente via `SubElement` direto (em vez de `_dtd_insert`) só é seguro se o clipe nunca tiver marcadores/filhos de prioridade menor já presentes — na dúvida, sempre usar `_dtd_insert`.
|
||||
- **Estado:** `resolvido`
|
||||
|
||||
---
|
||||
|
||||
### 2026-08-14 — Tolerância e ações consolidadas no processamento em lote
|
||||
|
||||
- **Sintoma:** a tolerância do corte ficava separada dos checkboxes e havia controles individuais repetindo as ações do lote.
|
||||
- **Causa raiz:** o layout foi evoluído incrementalmente, mantendo os fluxos antigos abaixo do novo processamento em lote.
|
||||
- **Solução adotada:** slider dentro do grupo "Remover silêncios", desabilitado quando a opção é desmarcada; campo de frases condicionado ao respectivo checkbox; removidos botões individuais e mantidas apenas as ações finais de abrir pasta e abrir no Final Cut.
|
||||
- **Aprendizado:** quando existe processamento em lote, os parâmetros devem ficar junto da operação e os comandos individuais não devem duplicar o fluxo principal.
|
||||
- **Estado:** `resolvido`
|
||||
|
||||
---
|
||||
|
||||
### 2026-08-14 — Pasta única e processamento em lote no app
|
||||
|
||||
- **Sintoma:** cada operação salvava o resultado em locais diferentes e exigia abrir o Finder ou localizar manualmente cada arquivo.
|
||||
- **Causa raiz:** os comandos do bridge usavam apenas `generate_output_path` ao lado do projeto e a UI oferecia ações independentes, sem uma pasta de trabalho comum.
|
||||
- **Onde:** `MacApp/Sources/TranscriptionView.swift`, `admin/models_api.py` e `server.py`.
|
||||
- **Solução adotada:** nova pasta de saída configurável no topo, cinco checkboxes de processamento e botão único; as etapas são encadeadas e todos os XML/SRT recebem `output_dir` explícito.
|
||||
- **Aprendizado:** operações relacionadas devem compartilhar uma pasta de saída e uma entrada encadeada, evitando artefatos espalhados pelo sistema.
|
||||
- **Estado:** `resolvido`
|
||||
|
||||
---
|
||||
|
||||
### 2026-08-14 — Mapeamento de legenda por intervalo, sem duração inventada
|
||||
|
||||
- **Sintoma:** a tentativa de impor duração mínima gerou legendas deslocadas, duplicadas e piores; havia também cues de duração zero que o FCP rejeitava.
|
||||
- **Causa raiz:** o mapeamento usava apenas o início do segmento e depois estendia artificialmente o fim, ignorando segmentos que atravessavam cortes.
|
||||
- **Onde:** `admin/models_api.py` (`cmd_export_srt`).
|
||||
- **Tentativas que falharam:** descartar todo cue menor que 0.5s e preencher cada cue até 1s.
|
||||
- **Solução adotada:** intersectar o intervalo completo da fala com cada janela de clipe mantida, mapear apenas a interseção, mesclar somente partes contíguas do mesmo segmento e omitir apenas spans que viram zero milissegundos.
|
||||
- **Aprendizado:** sincronização deve transformar intervalos fonte→timeline; nunca inventar duração para corrigir legibilidade.
|
||||
- **Estado:** `resolvido`
|
||||
|
||||
---
|
||||
|
||||
### 2026-08-14 — Exportar legenda a partir da versão cortada, não do projeto original
|
||||
|
||||
- **Sintoma:** a legenda gerada cobria o projeto original (326s) em vez do vídeo cortado (255s), desalinhada com os frames finais.
|
||||
- **Causa raiz:** o botão "Exportar Legendas (SRT)" usava `projectPath` (projeto aberto na tela) em vez do resultado da remoção de silêncio (`processedPath`).
|
||||
- **Onde:** `MacApp/Sources/TranscriptionView.swift` (`exportSubtitles`).
|
||||
- **Tentativas que falharam:** gerar sempre do `projectPath`.
|
||||
- **Solução adotada:** `exportSubtitles` passa a usar `processedPath` (a cópia `_silence_removed`) quando existe, caindo para o `projectPath` caso contrário — a legenda sempre acompanha o corte mais recente.
|
||||
- **Aprendizado:** artefatos derivados do corte (SRT, marcadores) devem ser gerados da mesma versão editada que o usuário está usando, não do arquivo-fonte original.
|
||||
- **Estado:** `resolvido`
|
||||
|
||||
---
|
||||
|
||||
### 2026-08-14 — Legenda SRT ultrapassava a duração do projeto (aviso do FCP)
|
||||
|
||||
- **Sintoma:** ao importar o SRT, o Final Cut avisava "as legendas se estendem além da duração do projeto" e sugeria conectá-las a um clipe vazio no final.
|
||||
- **Causa raiz:** o último bloco de legenda mapeado terminava após o fim da timeline (ex.: SRT até 257.4s num projeto de 255.6s), porque o timestamp final arredondava (`round`) para cima e nenhum teto impedia o overrun.
|
||||
- **Onde:** `admin/models_api.py` (`cmd_export_srt`, `srt_stamp`).
|
||||
- **Tentativas que falharam:** mapear o fim ao fim do clipe apenas; o último cue ainda podia exceder a sequência real.
|
||||
- **Solução adotada:** calcular `timeline_total = _timeline_duration().to_seconds()` e clampar `tl_start`/`tl_end` de cada cue a esse teto; usar `floor` (em vez de `round`) no `srt_stamp` para nunca subir acima de um limite de frame.
|
||||
- **Aprendizado:** SRT que termina após o último frame do projeto é rejeitado pelo FCP; sempre clampar o último cue ao total da timeline e truncar (não arredondar) timestamps.
|
||||
- **Estado:** `resolvido`
|
||||
|
||||
---
|
||||
|
||||
### 2026-08-14 — Remoção de gap vazio final como padrão na remoção de silêncio
|
||||
|
||||
- **Sintoma:** a timeline do resultado de remoção de silêncio podia terminar com um gap "Espaço" vazio após o último clipe (introduzido no round-trip com o Final Cut), deixando um "objeto preto" no final.
|
||||
- **Causa raiz:** nenhum passo garantia a remoção de gaps ao final da spine; o FCP re-adicionava o espaço ao importar.
|
||||
- **Onde:** `fcpxml/writer.py` (novo `FCPXMLModifier.remove_trailing_gaps`) e `server.py` (`handle_remove_media_silence`).
|
||||
- **Tentativas que falharam:** depender do usuário apagar o gap manualmente no FCP.
|
||||
- **Solução adotada:** `remove_trailing_gaps()` remove apenas o `<gap>` final da spine (gaps no meio são preservados) e re-sincroniza a duração da sequência; chamado antes de salvar na remoção de silêncio. `cmd_remove_silences` (app) já herda via delegação ao handler.
|
||||
- **Aprendizado:** operações que encurtam a timeline devem remover gaps finais para o arquivo exportado terminar onde o conteúdo termina.
|
||||
- **Estado:** `resolvido`
|
||||
|
||||
---
|
||||
|
||||
### 2026-08-14 — Micro-clips de 1 frame e gap "Espaço" no final são criados pelo FCP, não pelo corte
|
||||
|
||||
- **Sintoma:** o resultado da remoção de silêncio mostrava, após ~4:11, dezenas de micro-clips de 1 frame (0.042s) e um objeto preto/gap "Espaço" de 755s no final.
|
||||
- **Causa raiz:** o algoritmo gera um arquivo LIMPO (82 clipes, source `start` monotônico, sem micro-clips). O arquivo exportado pelo Final Cut tinha 162 clipes, 80 regressões de `start` e o gap "Espaço" — o FCP re-quebrou os clipes e inseriu o gap ao abrir/salvar/exportar, não o programa.
|
||||
- **Onde:** comparação entre `_out_test.fcpxml` (saída do `handle_remove_media_silence`) e `Legendas fora.fcpxmld` (exportado do FCP).
|
||||
- **Tentativas que falharam:** suspeitar do `cut_clip_ranges`/`_filter_children_for_segment`; o arquivo gerado pelo programa não tem esses micro-clips.
|
||||
- **Solução adotada:** confirmado que o bug não está no código de corte; é artefato da re-exportação pelo FCP. Ajustar a detecção de silêncio (`min_duration`/padding) não resolve porque o arquivo gerado já está correto.
|
||||
- **Aprendizado:** antes de assumir bug no gerador, reproduzir a saída crua e comparar com o artefato final — a re-importação no NLE pode reintroduzir clipes/gaps.
|
||||
- **Estado:** `resolvido` (diagnóstico)
|
||||
|
||||
---
|
||||
|
||||
### 2026-08-14 — Legenda SRT fora de sincronia após corte de silêncio
|
||||
|
||||
- **Sintoma:** ao exportar legenda depois de remover silêncios, o SRT cobria o vídeo inteiro em vez de apenas os trechos que ficaram — legendas apareciam em partes já cortadas.
|
||||
- **Causa raiz:** `cmd_export_srt` gerava o SRT direto do transcript da mídia original (`segments_to_srt`), com timestamps da fonte bruta, ignorando os cortes da timeline editada.
|
||||
- **Onde:** `admin/models_api.py` (`cmd_export_srt`).
|
||||
- **Tentativas que falharam:** exportar os segmentos como vinham do Whisper.
|
||||
- **Solução adotada:** mapear cada segmento da fonte para a posição real na timeline com `clip_offset + (seg_start - clip_source_start)` (mesma lógica do `transcript_markers`), por clipe da spine editada, descartando falas fora da janela usada e ordenando por tempo.
|
||||
- **Aprendizado:** qualquer artefato derivado da transcrição (SRT, cortes) deve ser mapeado fonte→timeline, nunca usar o transcript bruto; reutilizar o mapa já existente em `handle_transcript_markers`.
|
||||
- **Estado:** `resolvido`
|
||||
|
||||
---
|
||||
|
||||
### 2026-08-14 — Terceiro ponto do bug de `int(fps)`: `TimeValue.from_timecode()` corrompia qualquer segundo decimal em taxa NTSC
|
||||
|
||||
- **Sintoma:** ao implementar a nova tool `transcript_markers` (marca no timeline
|
||||
cada frase transcrita), `add_marker_at_timeline("317.9857s", ...)` lançava
|
||||
`ValueError: No spine clip at position 331.478s` — uma posição **fora** da
|
||||
timeline, mesmo com o timestamp de entrada correto e dentro dos limites.
|
||||
- **Causa raiz:** `TimeValue.from_timecode()` (`fcpxml/models.py`), no ramo que
|
||||
parseia segundos decimais simples (`"12.5s"`, sem `/`), calculava
|
||||
`frames = round(seconds * fps)` com o `fps` real (float), mas construía o
|
||||
`TimeValue` como `TimeValue(frames, int(fps))` — numerador calculado com o
|
||||
fps certo, denominador truncado. A 23.976fps isso infla o valor em ~1.04x
|
||||
(317.99s virou 331.48s). Terceiro local com essa mesma classe de bug (os
|
||||
outros dois: `to_frame_timevalue`/`_cut_transcript_spans` em `server.py`,
|
||||
já corrigidos na entrada anterior) — `from_timecode` é usado por
|
||||
`_parse_time()`, chamado por quase todo o `writer.py`, então qualquer
|
||||
handler que passe um timecode decimal (não fração) nessa taxa era afetado.
|
||||
- **Onde:** `fcpxml/models.py` (`TimeValue.from_timecode`).
|
||||
- **Tentativas que falharam:** nenhuma — bug novo, achado testando o handler
|
||||
novo contra a transcrição real em cache antes de expor na UI.
|
||||
- **Solução adotada:** reconstruir a fração exata do fps via
|
||||
`Fraction(fps).limit_denominator(100_000)` (recupera `24000/1001` a partir
|
||||
do float com precisão total) e usar `frames * fps_frac.denominator` /
|
||||
`fps_frac.numerator` como numerador/denominador — mantém os dois em
|
||||
unidades consistentes. Teste de regressão em
|
||||
`tests/test_models.py::test_from_seconds_string_ntsc_rate_exact`.
|
||||
- **Validado:** reproduzido o erro exato reportado, corrigido, e o handler
|
||||
novo (`transcript_markers`) rodou de ponta a ponta contra a transcrição
|
||||
real (54 marcadores, sem erro) depois da correção.
|
||||
- **Aprendizado:** qualquer função que aceite `fps: float` e construa um
|
||||
`TimeValue` diretamente (em vez de delegar pra uma fração exata) é suspeita
|
||||
de ter esse bug em taxas NTSC. Ao corrigir uma instância, procurar outras
|
||||
chamadas de `int(fps)` / `round(2400/fps)` no arquivo inteiro — não parar
|
||||
na primeira encontrada.
|
||||
- **Estado:** `resolvido`
|
||||
|
||||
---
|
||||
|
||||
### 2026-08-14 — Legendas visíveis exigem SRT/título, não marcadores
|
||||
|
||||
- **Sintoma:** pedido de "legenda na timeline que apareça no vídeo" — os marcadores de navegação não mostram texto sobre o vídeo.
|
||||
- **Causa raiz:** marcadores (`transcript_markers`) são apenas navegação; legenda visível no FCP exige SRT importado como idioma de legenda ou um `<title>` conectado (fragilmente dependente da versão do FCP).
|
||||
- **Onde:** `MacApp/Sources/TranscriptionView.swift`, `admin/models_api.py` (`cmd_export_srt`), reuso de `segments_to_srt`.
|
||||
- **Tentativas que falharam:** usar marcadores para legenda; gerar `<title>` de texto no FCPXML é frágil entre versões.
|
||||
- **Solução adotada:** novo comando `export_srt` que gera um `.srt` por mídia transcrita (via transcript cacheado + `segments_to_srt`), exposto como botão "Exportar Legendas (SRT)". O FCP importa o SRT como legenda nativa e desenha sobre o vídeo.
|
||||
- **Aprendizado:** "legenda" no FCP = SRT/caption, não marcador; sempre distinguir navegação (marker) de texto sobreposto (caption).
|
||||
- **Estado:** `resolvido`
|
||||
|
||||
---
|
||||
|
||||
### 2026-08-14 — Remoção de silêncio/transcrição gerava XML fora da grade de frame em projetos NTSC (23.976/29.97fps), confirmado por importação real no FCP
|
||||
|
||||
- **Sintoma:** ao importar no Final Cut Pro o XML gerado por `remove_media_silence`,
|
||||
dezenas de avisos "O item não está em um limite de quadro de edição" em quase
|
||||
todo `asset-clip` da spine (projeto real de 82 clipes, `Depimento Erika`).
|
||||
- **Causa raiz:** `handle_remove_media_silence` e `_cut_transcript_spans`
|
||||
(`server.py`) calculavam os limites de corte com
|
||||
`TimeValue(round(seconds*fps) * round(2400/fps), 2400)` — uma base fixa de
|
||||
2400 ticks/segundo. Para 24/25/30/48/50/60fps isso é exato, mas para
|
||||
23.976fps (`frameDuration="1001/24000s"`) `round(2400/23.976)` arredonda para
|
||||
100, tratando cada frame como `1/24s` exato em vez do `1001/24000s` real —
|
||||
uma correção de `Engine/docs/05_EXPERIENCIAS.md` (entrada anterior) existia
|
||||
como `FCPXMLModifier.snap_spine_times_to_frames()` mas só era chamada por
|
||||
`handle_add_marker`; nenhum handler de corte/ripple a usava.
|
||||
- **Onde:** `server.py` (`handle_remove_media_silence`, `_cut_transcript_spans`)
|
||||
e `fcpxml/writer.py` (`FCPXMLModifier.save()`).
|
||||
- **Tentativas que falharam:** nenhuma — a correção certa (`Fraction` exato)
|
||||
já existia no código, só não estava conectada aos caminhos que realmente
|
||||
cortam a spine.
|
||||
- **Solução adotada:** (1) `save()` agora chama `snap_spine_times_to_frames()`
|
||||
incondicionalmente antes de serializar — todo handler que escreve passa por
|
||||
ali, então a proteção é universal e não depende de cada handler lembrar de
|
||||
chamar. (2) Os dois pontos de corte por segundos (`to_frame_timevalue` /
|
||||
`to_frame`) agora usam o novo `FCPXMLModifier.snap_seconds_to_frame()`, que
|
||||
arredonda para o frame mais próximo usando a fração exata de `frameDuration`
|
||||
em vez da base fixa de 2400. Teste de regressão em
|
||||
`tests/test_media_intel.py::test_ntsc_rate_output_stays_frame_aligned`
|
||||
reproduz `duration="41100/2400s"` com a lógica antiga (não-inteiro em frames)
|
||||
e confirma alinhamento exato com a nova.
|
||||
- **Validado:** reimportação real no Final Cut Pro pelo usuário, sem os avisos.
|
||||
- **Aprendizado:** qualquer cálculo de tempo que assuma uma base fixa de ticks
|
||||
(2400, 600, etc.) quebra silenciosamente em taxas NTSC fracionárias
|
||||
(23.976/29.97/59.94fps) — usar sempre `Fraction` a partir do `frameDuration`
|
||||
real da sequência, nunca `fps` arredondado. E quando existir uma correção
|
||||
"canônica" pronta no código, verificar que TODOS os caminhos relevantes a
|
||||
chamam, não só um.
|
||||
- **Estado:** `resolvido`
|
||||
|
||||
---
|
||||
|
||||
### 2026-08-14 — Fluxo de transcrição dependia de clique redundante e ocultava falhas
|
||||
|
||||
- **Sintoma:** após selecionar um projeto, era necessário clicar novamente para abrir a transcrição; erros do processo Python podiam não aparecer na interface.
|
||||
- **Causa raiz:** a tela filha era condicionada a um botão intermediário, e o bridge não preservava fragmentos incompletos do JSONL nem convertia saída diferente de zero em erro.
|
||||
- **Onde:** `MacApp/Sources/ProjectView.swift` e `MacApp/Sources/PythonBridge.swift`.
|
||||
- **Tentativas que falharam:** depender apenas do callback de linhas completas e deixar a conclusão ignorar o código de saída.
|
||||
- **Solução adotada:** abrir a tela automaticamente após `inspect`, processar a última linha parcial e propagar falhas do subprocesso.
|
||||
- **Aprendizado:** bridges JSONL precisam tratar chunks arbitrários de stdout e sempre validar o status de saída.
|
||||
- **Estado:** `resolvido`
|
||||
|
||||
---
|
||||
|
||||
### 2026-08-14 — Limites de quadro no XML exportado após remoção de silêncio
|
||||
|
||||
- **Sintoma:** o Final Cut Pro rejeitava `Info.fcpxml` com avisos de que
|
||||
`offset` e `duration` não estavam em limites de quadro.
|
||||
- **Causa raiz:** a edição ripple produzia frações de tempo válidas
|
||||
matematicamente, mas desalinhadas do `frameDuration` exato da sequência.
|
||||
- **Onde:** `fcpxml/writer.py` e `server.py` no fluxo de remoção de silêncio.
|
||||
- **Tentativas que falharam:** usar FPS convertido para float e assumir uma
|
||||
base inteira, o que não funciona para 23.976/29.97.
|
||||
- **Solução adotada:** normalizar `offset` e `duration` da spine usando a
|
||||
fração exata de `frameDuration` antes de salvar a cópia modificada.
|
||||
- **Aprendizado:** limites de edição do FCPXML devem ser calculados com
|
||||
`Fraction`, nunca com FPS arredondado ou floats.
|
||||
- **Estado:** `resolvido`
|
||||
|
||||
---
|
||||
|
||||
<!-- NOVAS ENTRADAS DEVEM SER ADICIONADAS ACIMA DESTA LINHA, SEMPRE NO TOPO
|
||||
DA LISTA, PARA QUE A MAIS RECENTE FIQUE EM PRIMEIRO LUGAR. -->
|
||||
|
||||
### 2026-08-14 — Início do registro de experiências
|
||||
|
||||
- **Sintoma:** não havia um local centralizado para registrar erros/estruturas
|
||||
problemáticas; cada correção era tratada isoladamente.
|
||||
- **Causa raiz:** ausência de um artefato de memória de projeto; o contexto de
|
||||
bugs já resolvidos se perdia entre sessões.
|
||||
- **Onde:** `Engine/docs/05_EXPERIENCIAS.md` (este arquivo, recém-criado).
|
||||
- **Tentativas que falharam:** n/a (primeira entrada).
|
||||
- **Solução adotada:** criação deste arquivo com template padronizado, integrado
|
||||
ao fluxo de validação pós-correção (`Engine/run_after_fix.sh`).
|
||||
- **Aprendizado:** registrar problemas continuamente reduz o retrabalho; uma
|
||||
entrada clara evita reabrir bugs já entendidos.
|
||||
- **Estado:** `resolvido`
|
||||
|
||||
---
|
||||
|
||||
## Resumo rápido (índice)
|
||||
|
||||
| # | Data | Problema | Estado |
|
||||
|---|------|----------|--------|
|
||||
| 1 | 2026-08-14 | Início do registro de experiências | `resolvido` |
|
||||
| 4 | 2026-08-14 | XML fora da grade de frame em NTSC (23.976/29.97fps), confirmado no FCP | `resolvido` |
|
||||
| 5 | 2026-08-14 | `TimeValue.from_timecode` corrompia segundos decimais em NTSC (3º ponto do bug) | `resolvido` |
|
||||
| 6 | 2026-08-17 | Clipe-fantasma de 1 frame no início/fim após remoção de silêncio (padding sem vizinho na borda) | `resolvido` |
|
||||
| 7 | 2026-08-17 | Legendas dinâmicas sobrepondo entre clipes (título conectado não é aparado pelo out-point do pai) | `resolvido` |
|
||||
| 8 | 2026-08-17 | Importação recusada: `id` de `<text-style-def>` derivado do texto (acentos/espaços/dígito inicial) não é XML Name válido | `resolvido` |
|
||||
| 9 | 2026-08-17 | Legendas palavra a palavra centradas em vez da composição progressiva diagramada (bloco por trecho, palavra-chave em display italic) | `resolvido` |
|
||||
| 10 | 2026-08-17 | Cedilha/acentos da display italic invadindo a linha vizinha: empilhamento passou a usar a tinta real por classe de glifo | `resolvido` |
|
||||
|
||||
> Mantenha o índice acima sempre sincronizado com as entradas mais recentes.
|
||||
Reference in New Issue
Block a user