68 KiB
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.
### [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/descentda 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_sentenceempilha 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_sentenceempacota palavra a palavra e centra cada linha; orhythmvariava 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 emfont_metrics), apoio em Helvetica Neue Bold, tudo branco, linhas escalonadas porREFERENCE_STAGGER_RATIO. O modo antigo continua disponível emgranularity="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_clipmontava o id comof"{name}_ts0", enamevem do texto da legenda ("3 coisas que você precisa saber - Text"). No DTD,idé do tipoIDerefdo tipoIDREF: 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 comts_(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/IDREFno 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):
- 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). - No caminho
animated=False, o offset era gravado como relativo (0s) em vez de coordenadas de mídia-fonte (startdo clipe-pai + relativo). O FCP lê0scomo "0s da mídia", antes do in-point do clipe (start="220062843/24000s"), então o título nunca cai sobre o vídeo.
- Os dois templates que usávamos — "Essencial - Título"
(
- 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.fcpxmldeposiçã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"),startfixo86486400/24000s, e um bloco de<param>com margens/alinhamento/Custom Speed(com<keyframeAnimation>de tempos nominais constantes). A posição é o paramPosition(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_subtitlesagora usa sempre o "Text" e grava offset em coordenadas de mídia-fonte (startdo pai + relativo) para todos os títulos; posição via paramPosition, nãoadjust-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 usastartdo clipe-pai como origem do offset, nunca0s. - Estado:
resolvidono 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 umasset-clipnão é cortado pelo fim do clipe-pai; o FCP segue desenhando sobre o que vier depois. O último bloco de cada clipe terminava noendda última palavra do Whisper — que frequentemente ultrapassa o corte — e palavras cujostartjá 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 emtests/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:
- 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. - Técnica, e real:
measure_textestimava 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.
- Processo: o arquivo que o usuário importou era a versão anterior, gerada antes do
posicionamento existir (todas as palavras em
- 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
fontToolse embutir como tabela emfcpxml/font_metrics.py(9 variantes × 143 glifos). OfontToolsfoi usado só na geração, viauv 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
stylede 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çãodo template Essential Title, mas não havia como saber a unidade nem a escala. O único valor existente no código era0 -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 pegamkey/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:- 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. - 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.
- 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/358permanece0 69em todos os títulos do arquivo.
- Posição usa a mesma unidade que
- 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:
_TEXTO_TITLE_UIDapontava para.../Subtitles.localized/Subtitle.localized/Subtitle.moti— o template de legenda/caption do FCP, não um template de título.- Cada
<title>recebiarole="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 usamEssential Title.moti/Essential Fade.moti/Text.motie **não têm atributorolenenhum**; os gerados usavamSubtitle.moti+role="subtitles.". Prova adicional: no re-export, o FCP devolveu oscaption_` 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.pyafirmava queWHISPERX/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 orolede caption). A entrada anterior desta lista herdou o mesmo erro. - Solução adotada:
_TEXTO_TITLE_UID→.../Titles.localized/Essential Titles.localized/Essential Title.localized/Essential Title.motie nome do efeito →"Essencial - Título"._TEXTO_TITLE_ROLEremovido eelem.set('role', ...)eliminado de_make_texto_title_clip. Nenhum título gerado carregarole._TEXTO_TITLE_START→86486400/24000s(valor que o FCP escreve para o Essential Title)._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.- Granularidade:
max_words_per_line4 → 1 elane_count3 → 9 (defaults alinhados emmodels.py,server.pye no MacApp), gerando um<title>por palavra. - Comentários falsos reescritos apontando para os exports reais do usuário.
- 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 osname=seguem o padrão que o código gera (caption_<hex>), ele é. - Estado:
resolvidono 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 deoffsetem coordenada de mídia continua correta (reconfirmada contraexemplo de arquivos.fcpxmld), mas orole="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 comstart="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 semanimated, então caía no default e produzia um<title>comref="r_title_basic"(Título Básico) mas comrole="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):
generate_dynamic_subtitles(fcpxml/writer.py) escolhia o efeito certinho porconfig.animated(linhas 2893-2896), mas SEMPRE construía o clip com_make_texto_title_clip(linha 2944) — ignorando oanimated._make_basic_title_clip(que monta o Título Básico sem role/start e só os 2 paramsCompactar/Alinhamento) existia mas nunca era chamado (código morto). Resultado default: ref do básico + corpo do subtitle → o FCP descarta silenciosamente.- Mesmo no caminho animado, o
offsetera 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= start240817577/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" (casoLegendas fora.fcpxmld).
- Onde:
fcpxml/writer.py(generate_dynamic_subtitles, linhas ~2893-2956),fcpxml/models.py(DynamicSubtitleConfig.animated, default erradoFalse),tests/test_dynamic_subtitles.py. - Solução adotada:
generate_dynamic_subtitlesagora despacha peloconfig.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.- No caminho animado,
offset = media_origin + snap(relativo)commedia_origin = _parse_time(parent.get('start'))(coordenada da mídia, igual ao export real); no estático, offset permanece relativo (como r3). DynamicSubtitleConfig.animatedagora éTruepor padrão (decisão do usuário: a feature é a legenda animada/ediável;Falsesó para texto queimado no frame).- 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 branchelsede umif config.Xque 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_silencesnum projeto real (Depoimento da Erika_silence_removed.fcpxmld) e reportou dois clipinhos minúsculos: o primeiro e o último clipe da spine gerada tinhamduration="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.fcpxmlbruto gerado pelo programa, confirmado lendo o XML direto, sem passar pelo FCP.handle_remove_media_silence/cut_clip_ranges(fcpxml/writer.py) aplica umpadding(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 listakeeps(complemento doscut_rangesmesclados). - 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
uide osoffset/durationcorrigidos (entradas abaixo), o usuário mandou o.fcpxmldreal 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_subtitlesitera cada clipe da spine (for el in spine_clips), calcula a janela de palavras correta relativa àquele clipe (el), mas chamavamodifier.generate_dynamic_subtitles(name, ...)passandoname = el.get("name", "")— uma STRING — em vez do elemento. Depois de qualquerremove_silences/corte com ripple, TODOS os fragmentos resultantes de um clipe original mantêm o mesmonameherdado (aqui, ~482 clipes, todosname="0E6A8829", o nome do asset de origem)._require_clip()resolve porself.clips[key], um dict indexado poridou, na falta dele, porname(fcpxml/writer.py:_index_elements) — com nomes duplicados, cada novo clipe indexado SOBRESCREVE o anterior, entãoself.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 amodifier.generate_dynamic_subtitles),fcpxml/writer.py(generate_dynamic_subtitles,_require_clip,_index_elements). - Solução adotada:
generate_dynamic_subtitlesagora aceitaparent_clipcomostr | 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.pyfoi atualizado para passarel(o elemento já em mãos no loop) em vez dename. Adicionado teste de regressão (test_element_param_bypasses_ambiguous_duplicate_name_lookup) que simula dois clipes com o mesmonamee confirma que passar o elemento anexa cada legenda ao clipe certo. - Aprendizado: nunca identificar um clipe específico por
namenum handler que itera múltiplos clipes — qualquer operação de corte/ripple/remoção de silêncio no FCPXML preserva onameoriginal em todos os fragmentos resultantes, entãonamedeixa de ser único assim que o timeline é editado. Sempre que o chamador já tem oET.Elementem 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:
- O
uidfixado 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 ouidde 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 umuidfabricado 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). - Um dos
<param>copiados junto (Opacidade="0") fixava a opacidade do texto em zero — mesmo se ouidestivesse certo, o texto ficaria invisível.
- O
- 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— efeitor4nomeado "Legenda",uid=".../Titles.localized/Subtitles.localized/Subtitle.localized/Subtitle.moti", usado em cinco<title>conectados por palavra/linha comrole="subtitles.subtitles-1". Copiado ouid, oname("Legenda"), ostartfixo (86400314/24000s, diferente do valor anterior), o atributorole, 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 paramsAnimar/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/adivinharuidde efeito nativo do FCP, mesmo que o formato pareça consistente com outros exports reais — sempre copiar de um.fcpxmld/Info.fcpxmlque 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, incluindoAnimar = "4 (Tudo)"mais dezenas de params de animação por caractere (X/Y/Z deslocamento,Objeto Original,Deslocamento Inicial/Final,Direção,Velocidade Personalizadacom keyframes). Nós só escrevíamos 5 params de layout e omitíamos oAnimar. 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 nossoPosiçã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.047em "Tod" vs-243em "a") — são dados 3D por caractere que o FCP calcula e que não dá para reproduzir. Já ostimedos 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/203deAnimar),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 default0 -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 oPosiçãogravado; a revelação palavra-por-palavra continua vindo dooffset/durationde cada palavra (não da animação). Teste atualizado: 5 params de layout + 10Animar = "0 (Nenhum)", semkeyframeAnimation. - 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 paramPosiçã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:
resolvidono 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/durationde<title>com denominador/23s(ex.:offset="2/23s",duration="67/23s"). - Causa raiz:
generate_dynamic_subtitles(fcpxml/writer.py) construía cada offset/duration comTimeValue.from_seconds(seconds, self.fps). Esse classmethod (fcpxml/models.py) fazint(fps)— para um projeto NTSC a 23.976fps (frameDuration="1001/24000s",self.fps ≈ 23.976),int(fps)trunca para23, 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 exata1001/24000s. - Onde:
fcpxml/writer.py:2841-2850(chamada) efcpxml/models.py:314-318(TimeValue.from_seconds, bug latente — outros call-sites como marcadores emfcpxml/writer.py:1442,1511usam 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)porself.snap_seconds_to_frame(...)— helper já existente (fcpxml/writer.py:746) que usa a fração exata deframeDuration(viaframe_duration_fraction(),fcpxml/writer.py:727) em vez do float truncado, já usado em outro lugar do writer para snap da spine. Também trocadomin_dur_seconds = 1.0 / self.fpsporfloat(self.frame_duration_fraction()). - Aprendizado: qualquer conversão de segundos-float para
TimeValuenum projeto FCPXML deve usar a fração exata doframeDurationdo<format>da sequência (viaframe_duration_fraction()/snap_seconds_to_frame()), nuncaint(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_subtitlesnã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 porconfig.lane_countlanes (round-robin). A duração de cada linha se estende até sua própria lane ser reaproveitadalane_countlinhas 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 atributostartfixo (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 (
wordscomstart/endreais), usando a mesma lógica deTimeValue/mapeamento fonte→timeline já estabelecida no projeto. Só a ESTRUTURA XML (uid, params,startfixo) 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_countsubstituindolane),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.fcpxmlcom 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 respectivaapontando 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:
- O
uiddo efeito "Basic Title" foi inventado (nunca verificado contra um export real) — o caminho correto temBumper:Opener.localized, nãoBasic Text.localized. - Os
<title>por palavra estavam sendo anexados como connected clip (vialane) 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. - Palavras com
start/endmuito próximos (ou vindas de um transcript com timestamps imprecisos) geravam durações que arredondavam para 0 frames no fps da sequência.
- O
- Onde:
fcpxml/writer.py,FCPXMLModifier.generate_dynamic_subtitles/_make_title_clip/_ensure_basic_title_effect. - Tentativas que falharam: validar apenas com
fcpxml/dtd.pye 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:
- Encontrado um
uidreal e correto dentro do próprio repositório, emWHISPERX/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 (CompactareAlinhamento, comkeyfixo) também foram copiados de lá; o<param name="Position">fabricado foi removido. - 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. - 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 de0.01sque arredondava para 0 em fps altos.
- Encontrado um
- Aprendizado: nunca fabricar
uid/keyde 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 de1/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 comF821 Undefined name 'clip_id'eF841 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 suportaroutput_dirconfigurável), o corpo inteiro decmd_add_zoom(checagem declip_id+ chamada ahandle_add_zoom) foi colado dentro decmd_remove_silences, antes do bloco correto que já chamavahandle_remove_media_silence— deixandocmd_remove_silencescom código morto/quebrado (chamava o handler errado e checava uma variável inexistente) ecmd_add_zoomtruncado (só validavapath/clip_ide não fazia mais nada). - Onde:
admin/models_api.py, funçõescmd_remove_silencesecmd_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 ahandle_add_zoom) de volta para dentro decmd_add_zoom, removendo-o decmd_remove_silences, que voltou a conter só a chamada correta ahandle_remove_media_silence. - Aprendizado: depois de qualquer edição manual em
admin/models_api.py(ou qualquer arquivo com várias funçõescmd_*de shape parecido), rodar o lint imediatamente pega colagens cruzadas de função —ruffacusa 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 comocaption_L0W0_ts0 already defined. - Causa raiz: os IDs de
<title>/<text-style-def>eram montados comocaption_L{line_idx}W{word_idx}_ts{i}, comline_idx/word_idxreiniciando em 0 a cada chamada degenerate_dynamic_subtitles. Como o handler (handle_generate_dynamic_subtitlesemserver.py) chama esse método uma vez por clipe da spine na mesma instância deFCPXMLModifier, 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étodoFCPXMLModifier.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 viaET.SubElementdireto 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 comoref-clipdevem 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
SubElementdireto (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_pathao lado do projeto e a UI oferecia ações independentes, sem uma pasta de trabalho comum. - Onde:
MacApp/Sources/TranscriptionView.swift,admin/models_api.pyeserver.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_direxplí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:
exportSubtitlespassa a usarprocessedPath(a cópia_silence_removed) quando existe, caindo para oprojectPathcaso 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 clampartl_start/tl_endde cada cue a esse teto; usarfloor(em vez deround) nosrt_stamppara 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(novoFCPXMLModifier.remove_trailing_gaps) eserver.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
startmonotônico, sem micro-clips). O arquivo exportado pelo Final Cut tinha 162 clipes, 80 regressões destarte 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 dohandle_remove_media_silence) eLegendas 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_srtgerava 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 dotranscript_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çavaValueError: 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/), calculavaframes = round(seconds * fps)com ofpsreal (float), mas construía oTimeValuecomoTimeValue(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_spansemserver.py, já corrigidos na entrada anterior) —from_timecodeé usado por_parse_time(), chamado por quase todo owriter.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)(recupera24000/1001a partir do float com precisão total) e usarframes * fps_frac.denominator/fps_frac.numeratorcomo numerador/denominador — mantém os dois em unidades consistentes. Teste de regressão emtests/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: floate construa umTimeValuediretamente (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 deint(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 desegments_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_srtque gera um.srtpor 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 todoasset-clipda spine (projeto real de 82 clipes,Depimento Erika). - Causa raiz:
handle_remove_media_silencee_cut_transcript_spans(server.py) calculavam os limites de corte comTimeValue(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 como1/24sexato em vez do1001/24000sreal — uma correção deEngine/docs/05_EXPERIENCIAS.md(entrada anterior) existia comoFCPXMLModifier.snap_spine_times_to_frames()mas só era chamada porhandle_add_marker; nenhum handler de corte/ripple a usava. - Onde:
server.py(handle_remove_media_silence,_cut_transcript_spans) efcpxml/writer.py(FCPXMLModifier.save()). - Tentativas que falharam: nenhuma — a correção certa (
Fractionexato) já existia no código, só não estava conectada aos caminhos que realmente cortam a spine. - Solução adotada: (1)
save()agora chamasnap_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 novoFCPXMLModifier.snap_seconds_to_frame(), que arredonda para o frame mais próximo usando a fração exata deframeDurationem vez da base fixa de 2400. Teste de regressão emtests/test_media_intel.py::test_ntsc_rate_output_stays_frame_alignedreproduzduration="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
Fractiona partir doframeDurationreal da sequência, nuncafpsarredondado. 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.swifteMacApp/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.fcpxmlcom avisos de queoffsetedurationnão estavam em limites de quadro. - Causa raiz: a edição ripple produzia frações de tempo válidas
matematicamente, mas desalinhadas do
frameDurationexato da sequência. - Onde:
fcpxml/writer.pyeserver.pyno 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
offsetedurationda spine usando a fração exata deframeDurationantes 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
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.