chore(fase0): higiene do repositório + corrige gitignore que escondia fcpxml/models/

Fase 0 do roteiro de reestruturação (Engine/docs/10_MAPA_REESTRUTURACAO.md):
move code/WHISPERX (2,6 GB de backups órfãos, sem uso ativo, sem
.gitmodules) para ~/Archives/G-ART-WHISPERX-backup fora do workspace git;
traz admin/ para o gate de lint de run_after_fix.sh; corrige
fcpxml/writer/adjustment.py, que gerava um wrapper <adjustment> inexistente
no DTD 1.13 (filtros agora vão direto no <clip>, na ordem exigida), com
teste de regressão novo.

Achado à parte: .gitignore tinha uma regra solta "models/" (pensada só
para o cache do Whisper em code/models/) que também escondia do git todo o
pacote fcpxml/models/ — nunca commitado, sem proteção nenhuma. Corrigida
para /code/models/, ancorada na raiz.

Docs atualizados no mesmo commit (02_MODULES, 09_MANUTENCAO,
10_MAPA_REESTRUTURACAO, 05_EXPERIENCIAS #34 e #36), conforme a regra do
CLAUDE.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
João Henrique
2026-09-23 08:28:44 -04:00
co-authored by Claude Sonnet 5
parent 0fdfe33613
commit d13f643ebc
19 changed files with 2013 additions and 29 deletions
+161
View File
@@ -51,6 +51,9 @@ que merece entrada.
| 31 | 2026-08-24 | Revisão de falantes ("Quem fica na edição") salvava certo, mas etapa 4 (Revisão de frases) lia a timeline crua, ignorando falantes mutados/linhas riscadas | `resolvido` |
| 32 | 2026-08-24 | Separar legendas dinâmicas de convencionais por role: `<title>` aceita `role` (CDATA), NÃO `videoRole` — este último é DTD-inválido para títulos e quebra a validação | `resolvido` |
| 33 | 2026-09-22 | Legenda comum sobreposta à composição dinâmica em `generate_subtitles_by_emphasis`; regenerar acumulava títulos em vez de substituir | `resolvido` |
| 34 | 2026-09-22 | `ClipDeAjuste` gerava wrapper `<adjustment>` inválido no DTD; `code/WHISPERX` era 2,6 GB de backup órfão que inflava o lint quando rodado com `--exclude` explícito | `resolvido` |
| 35 | 2026-09-22 | Indexação RAG (`admin/update_rag.py`) abortava a transação inteira ao achar um chunk que estoura o contexto do modelo de embedding | `resolvido` |
| 36 | 2026-09-23 | `.gitignore` com regra `models/` solta escondia do git o pacote inteiro `fcpxml/models/` (dados do engine), não só o cache do Whisper | `resolvido` |
> Mantenha o índice acima sempre sincronizado com as entradas mais recentes.
@@ -1732,3 +1735,161 @@ conjunto de títulos por cima do anterior em vez de substituí-lo.
skipped, 1 falha e 1 erro de ambiente sem relação com a mudança (WhisperX/
`extract_pitch` ausente, torchcodec sem libs de sistema); lint dos arquivos
alterados limpo.
---
### Entrada 34 — 2026-09-22: `<adjustment>` inválido no DTD e `WHISPERX` órfão inflando o lint
**Sintoma 1:** `fcpxml/writer/adjustment.py` (`ClipDeAjuste`, código de uma
sessão anterior não commitado) montava
`<clip><adjustment><filter-video .../></adjustment></clip>` para camadas de
ajuste. Nada usava o módulo ainda (sem chamada em `server_tools`/
`admin/api`), mas ficava pronto para alguém reusar do jeito errado.
**Causa 1:** o DTD real da Apple (`FCPXMLv1_13.dtd`) não define nenhum
elemento `<adjustment>`. A produção real de `<clip>` é
`(note?, %timing-params;, %intrinsic-params;, (spine|(%clip_item;)|caption)*,
(%marker_item;)*, audio-channel-source*, (%video_filter_item;)*,
filter-audio*, metadata?)` — ou seja, `filter-video`/`filter-audio` são
filhos diretos do `<clip>`, sem wrapper, e nessa ordem (vídeo antes de
áudio).
**Solução 1:** `ClipDeAjuste.criar()` agora anexa os filtros direto no
`<clip>`, ordenados com vídeo antes de áudio
(`sorted(filtros, key=lambda f: f.tag != "filter-video")`). Teste de
regressão novo: `tests/test_writer_adjustment.py` (sem wrapper, ordem
correta, um `<effect>` por `uid` em `resources`).
**Sintoma 2 (achado ao investigar o mesmo módulo):** um `ruff check .
--exclude docs/` rodado manualmente no início desta sessão acusou **510
erros** — muito acima do que a suíte normalmente reporta.
**Causa 2:** `code/WHISPERX` era uma pasta `.git` solta de **2,6 GB** dentro
de `code/` (não um submodule registrado — sem `.gitmodules`), contendo
cópias/backups congelados do próprio projeto, incluindo uma cópia inteira e
antiga de `fcp-mcp-server-main` dentro de si mesma. O `pyproject.toml` já
excluía `WHISPERX/` do lint por padrão (`[tool.ruff] exclude = ["docs/",
"WHISPERX/"]`), mas passar `--exclude docs/` na linha de comando
**sobrescreve** esse `exclude` em vez de complementá-lo — foi assim que o
lint passou a varrer os 2,6 GB de código velho lá dentro. Confirmado por
grep que só 3 arquivos no código ativo referenciam "WHISPERX", todos em
comentários explicativos (`fcpxml/diarize.py`, `tests/test_diarize.py`,
`admin/api/shared.py`) — nenhum import ou caminho real dependia da pasta.
**Solução 2:** pasta movida para `~/Archives/G-ART-WHISPERX-backup` (fora do
workspace git), copiada com `rsync -a --no-perms` e conferida com
`diff -rq` antes de remover o original. `WHISPERX/` também saiu do
`exclude` do ruff em `code/pyproject.toml` (não faz mais sentido excluir um
caminho que não existe mais em `code/`).
**Aprendizado:** (1) um wrapper de elemento "que faz sentido conceitualmente"
não substitui checar o DTD real antes de escrever o gerador — o padrão do
projeto (`dtd.py`, DTDs em `bm/*/FCPXMLv1_13.dtd`) existe exatamente para
isso. (2) uma flag de linha de comando como `--exclude` em ferramentas de
lint tipicamente **substitui** a config do projeto, não a estende — rodar
`ruff check .` sem flags (herdando `pyproject.toml`) é o comando correto
para refletir o gate real; qualquer variação manual com `--exclude` pode
mentir sobre o estado do lint. (3) uma pasta de backup improvisada dentro do
diretório ativo do projeto (mesmo que "só para não perder nada") é dívida
que cresce sem ninguém perceber — 2,6 GB não apareceram de uma vez.
**Estado:** `resolvido` — `tests/test_writer_adjustment.py` (3 testes)
passando; `admin/` trazido ao lint gate no mesmo commit (ver
`09_MANUTENCAO.md` §2.3); suíte completa 1543 passando, 8 skipped, 1 falha
+ 1 erro pré-existentes de outro trabalho em andamento (sem relação com
esta correção).
### Entrada 35 — 2026-09-22: chunk grande demais derrubava a indexação RAG inteira
**Sintoma:** `admin/update_rag.command` (primeira indexação completa do
G-ART, banco `rag_gart` recém-provisionado) morria sempre no mesmo ponto com
`requests.exceptions.HTTPError: 500 Server Error` na chamada ao Ollama —
sempre logo após imprimir `code/fcpxml/export.py`, ou seja, no arquivo
seguinte na ordem alfabética.
**Causa raiz:** `code/fcpxml/font_metrics.py` é uma tabela de larguras de
glifo (`METRICS = {...}`), texto extremamente denso em tokens (muitos
números/pontuação curtos) — um chunk de ~4900 caracteres (dentro do limite
`CHUNK_MAX_CHARS = 5000`) virou 2653 tokens no tokenizer do
`nomic-embed-text`, estourando o contexto de 2048 tokens do servidor Ollama
local (`llama.cpp`: "input length exceeds the context length"). Reproduzido
isolando o arquivo e chamando `/api/embeddings` chunk a chunk — 6 dos 9
chunks falhavam. `CHUNK_MAX_CHARS` mede caracteres, não tokens; assume
implicitamente ~1 token por poucos caracteres, o que não vale para conteúdo
não-prosa (tabelas numéricas, JSON denso).
Segundo problema, apontado por que a primeira tentativa não recuperou nada:
`admin/update_rag.py::index()` roda a varredura inteira (centenas de
arquivos) em **uma única transação**, com `commit()` só no fim e
`rollback()` em qualquer exceção — um único chunk problemático em um único
arquivo descartava a indexação inteira, mesmo que os outros 300+ arquivos
já tivessem embedado e inserido com sucesso.
**Solução:** `_embed()` agora detecta essa resposta específica do Ollama
(`ChunkTooLarge`, checado por `500` + `"context length"` no corpo) e o loop
principal captura essa exceção por chunk, pula só aquele chunk (aviso em
stderr) e continua o arquivo — sem abortar a transação. Não trunca nem
reduz `CHUNK_MAX_CHARS` globalmente (afetaria todo o corpus por causa de
poucos arquivos atípicos); a lacuna fica só nos poucos chunks realmente
grandes demais, e o resto do arquivo ainda fica pesquisável.
**Aprendizado:** um limite de chunk em caracteres é uma aproximação, não uma
garantia de contexto — arquivos de dados brutos (tabelas, mapeamentos
numéricos, JSON/CSV embutido em `.py`) tokenizam bem mais denso que prosa ou
código comum e podem violar o limite do modelo mesmo dentro do teto de
caracteres. Uma indexação em lote sobre centenas de arquivos não deve ficar
tudo-ou-nada numa única transação: uma falha isolada e recuperável (chunk
específico, arquivo específico) deve ser contida ali, não descartar o
trabalho inteiro já validado.
**Estado:** `resolvido` — indexação completa rodou até o fim: 304 arquivos,
1702 chunks, 0 removidos. Também nesta sessão: criada a pasta `rag/` na raiz
(schema, busca híbrida `search.py`/`search_gart.sh`, `SETUP.md`) — ver
`rag/README.md` para a divisão de responsabilidades com `admin/update_rag.py`.
---
### Entrada 36 — 2026-09-23: `.gitignore` escondia `fcpxml/models/` inteiro do git
**Sintoma:** ao investigar por que um `git diff` de um arquivo recém-editado
(`fcpxml/models/timeline.py`, durante a correção da Entrada 34) não mostrava
nada, `git status` também não listava o arquivo como modificado nem como
untracked — como se ele simplesmente não existisse para o git.
**Causa:** `.gitignore` tinha a regra solta `models/` (comentada como
"WhisperX models cache", pensada para ignorar o cache de ~11 GB de modelos
Whisper baixados em `code/models/`). Uma regra sem `/` inicial no
`.gitignore` casa com **qualquer diretório com esse nome em qualquer
profundidade** — não só `code/models/`, mas também `code/fcpxml/models/`, o
pacote de data classes (`TimeValue`, `Clip`, `Timeline`, `Marker`, etc.) que
sustenta todo o engine. Confirmado: `git ls-tree -r HEAD` não tem nenhum
`fcpxml/models.py` nem `fcpxml/models/` em nenhum commit do histórico — o
pacote inteiro (1.234 linhas, 7 módulos) só existia em disco, sem nenhuma
proteção de versionamento, desde que a divisão de `models.py` em pacote foi
feita (sessão anterior, nunca commitada).
**Risco:** qualquer operação que limpa arquivos não rastreados
(`git clean -fd`, reinstalar do zero, trocar de máquina via `git clone`)
apagaria essa base sem chance de recuperação — nenhum commit para reverter.
**Solução:** regra trocada para `/code/models/` (ancorada na raiz do repo,
só o cache real), preservando `whisper/` (sem uso hoje, mas inofensiva) e
tudo mais. Confirmado com `git check-ignore -v`: `fcpxml/models/timeline.py`
não é mais ignorado; `code/models/models--Systran--faster-whisper-base`
continua ignorado. `fcpxml/models/` passou a aparecer como `??` no
`git status` — visível, pronto para ser commitado quando o dono do trabalho
revisar.
**Aprendizado:** regra de `.gitignore` sem `/` inicial (ex.: `models/`) casa
em qualquer profundidade da árvore — é fácil escrever pensando só no caso
que motivou a regra (um cache na raiz) e esquecer que o mesmo nome de pasta
pode existir, com sentido completamente diferente, dentro do código-fonte.
Regra de bolso: nomes de pasta genéricos (`models/`, `build/`, `cache/`,
`data/`) no `.gitignore` deveriam quase sempre vir ancorados (`/caminho/
exato/`), a menos que a intenção seja mesmo ignorar toda ocorrência do nome
em qualquer lugar da árvore.
**Estado:** `resolvido` — regra corrigida, `fcpxml/models/` confirmado
visível ao git (não commitado ainda; fica para quem já está com esse
trabalho em andamento decidir quando commitar). Nenhum código alterado,
só o `.gitignore`.