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
+8 -5
View File
@@ -7,7 +7,7 @@ Mapa módulo a módulo do núcleo Python: onde cada coisa mora e o que ela faz.
A API pública é reexportada em `fcpxml/__init__.py` — essa é a fonte da verdade
do `__all__`.
Versão: `0.6.35` · Última varredura: 2026-08-19
Versão: `0.13.1` · Última varredura: 2026-09-22
> **Por que existem pacotes aqui.** `writer.py` tinha 4.199 linhas e `models.py`
> 1.091, cada um com muitos assuntos dentro. Viraram pacotes com um módulo por
@@ -21,14 +21,16 @@ Versão: `0.6.35` · Última varredura: 2026-08-19
| Módulo / pacote | Linhas | Papel |
|-----------------|-------:|-------|
| `writer/` | 4.687 | **Edição e escrita de FCPXML** — o coração |
| `writer/` | 5.377 | **Edição e escrita de FCPXML** — o coração |
| `models/` | 1.195 | Data classes e enums |
| `text_layout.py` | 901 | Diagramação das legendas dinâmicas |
| `rough_cut.py` | 798 | Geração de timelines novas |
| `model_manager.py` | 748 | Modelos Whisper: catálogo, download, config |
| `voice_timeline.py` | 600 | O JSON de voz que a IA lê |
| `analise.py` | 218 | `AnalisadorDeArquivo` — orquestra transcrição/diarização/ênfase/emoção e monta o `_voice_timeline.json`; usado por `voice_timeline.py` |
| `phrase_review.py` | 547 | Revisão de frases (etapa 5 do assistente) |
| `speaker_review.py` | 207 | Revisão de falantes (etapa 3 do assistente) |
| `speaker_review.py` | 208 | Revisão de falantes (etapa 3 do assistente) |
| `transcription/` | 325 | Pacote: `engine.py` (adapter faster-whisper), `segments.py`/`text.py`/`timestamps.py` (operações puras sobre transcript); `transcribe.py` é a fachada de compatibilidade |
| `collision.py` | 472 | Colisão entre títulos na tela |
| `font_metrics.py` | 445 | Largura real de glifos por fonte |
| `templates.py` | 387 | Templates de timeline |
@@ -68,8 +70,9 @@ editorial, todos operando sobre o mesmo documento e os mesmos índices.
| `document.py` | 170 | Assets de vídeo, timebases, `write_fcpxml` |
| `markers.py` | 165 | Marcadores: um, por timecode, em lote |
| `audio.py` | 162 | Clipes de áudio e cama musical |
| `generator.py` | 90 | `FCPXMLWriter` — orquestra a criação do zero (estado + delegação) |
| `builders.py` | — | Um builder por tipo de elemento: `FormatBuilder`, `AssetBuilder`, `MarkerBuilder`, `KeywordBuilder`, `ClipBuilder`, `SequenceBuilder`, `LibraryBuilder` |
| `generator.py` | 94 | `FCPXMLWriter` — orquestra a criação do zero (estado + delegação) |
| `builders.py` | 174 | Um builder por tipo de elemento: `FormatBuilder`, `AssetBuilder`, `MarkerBuilder`, `KeywordBuilder`, `ClipBuilder`, `SequenceBuilder`, `LibraryBuilder` |
| `adjustment.py` | 140 | `ClipDeAjuste` — camada de ajuste (filtros `filter-video`/`filter-audio` direto no `<clip>`, sem uso ainda em `server_tools`/`admin/api`) |
| `reorder.py` | 126 | Reordenar e recalcular offsets |
| `trim.py` | 125 | Aparar e propagar o ripple |
| `transitions.py` | 94 | Transições entre vizinhos |
+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`.
+42 -8
View File
@@ -7,7 +7,7 @@ Este é o documento de rota. Os outros descrevem o que **é**; este diz o que
**fazer** e por onde começar quando chega uma implementação, uma melhoria ou
uma correção.
Última varredura: 2026-08-21 · 1.498 testes · lint zerado (fora de server.py/ai_edit.py/llm_local.py, pré-existentes)
Última varredura: 2026-09-22 · 1.543 testes passando (+1 falha pré-existente em `test_forced_align.py` e +1 erro pré-existente em `test_refine_voice_timeline_tool.py`, ver §2.6) · lint zerado em `code/` e em `admin/` (fora de server.py/ai_edit.py/fcpxml/analise.py, pré-existentes — outro trabalho em andamento na branch)
---
@@ -48,10 +48,12 @@ o olho do usuário. Não é para sair criando suíte de UI — mas lógica pura
foi parar na camada de tela (cálculo de trim, mapeamento de tempo) deveria
descer para o Python, onde já existe rede.
### 2.3 `admin/` fica fora do lint
`run_after_fix.sh` roda o ruff de dentro de `code/`, então `admin/` — 1.751
linhas de código que o app depende para funcionar — nunca é verificado.
Incluir mexe no gate, então é decisão consciente, não esquecimento.
### 2.3 ~~`admin/` fica fora do lint~~ — resolvido em 2026-09-22
`run_after_fix.sh` agora roda um segundo passo (`ruff check --config
pyproject.toml ../admin/`) com a mesma config do engine. Precisou de
`# noqa: E402` em 6 imports de `admin/models_api.py`/`admin/models_gui.py`
(padrão `sys.path.insert` antes do import local, convenção já usada no
projeto). Lint de `admin/` está zerado.
### 2.4 Confirmações visuais pendentes no FCP
Várias entradas do `05_EXPERIENCIAS.md` estão marcadas como resolvidas *no XML*
@@ -68,9 +70,39 @@ causa raiz. Não investigado a fundo ainda.
→ `fcpxml/writer/cut.py` (`cut_clip_ranges`, `min_keep_seconds`), padding do
`remove_media_silence`.
### 2.6 Submódulo `WHISPERX` com conteúdo modificado e não commitado
Está fora dos commits de propósito, porque ninguém verificou o que mudou lá
dentro. Precisa ser olhado e resolvido — ou commitado, ou revertido.
### 2.6 ~~`fcpxml/writer/adjustment.py` gerava um wrapper `<adjustment>` inválido~~ — resolvido em 2026-09-22
`ClipDeAjuste` embrulhava filtros num `<clip><adjustment>...</adjustment></clip>`,
que não existe no DTD real da Apple. Corrigido para anexar
`filter-video`/`filter-audio` direto como filhos do `<clip>` (na ordem que o
DTD exige: vídeo antes de áudio). Teste de regressão em
`tests/test_writer_adjustment.py`. Segue sem uso em `server_tools`/`admin/api`
— só deixou de estar pronto pra alguém reusar do jeito errado.
→ `05_EXPERIENCIAS.md` #34.
### 2.7 `test_refine_voice_timeline_tool.py` quebrado: `voice_timeline.extract_pitch` ausente
`TestRefineVoiceTimelineHandler::test_max_zooms_caps_the_list` tenta
`monkeypatch.setattr(vt, "extract_pitch", ...)` mas `fcpxml/voice_timeline.py`
não tem mais (ou nunca teve, nesta branch) essa função. Pertence ao trabalho
de análise de voz já em andamento nesta branch (`voice_timeline.py`
modificado, não commitado) — não investigado a fundo, só registrado aqui
para não se perder.
→ `fcpxml/voice_timeline.py`, `tests/test_refine_voice_timeline_tool.py`.
### 2.8 ~~Submódulo `WHISPERX` com conteúdo modificado e não commitado~~ — resolvido em 2026-09-22
Não era um submódulo git registrado (sem `.gitmodules`) — era uma pasta
`.git` solta de 2,6 GB dentro de `code/`, com cópias/backups congelados do
próprio projeto (`WHISPERX_backup_88476/`, uma cópia inteira e antiga de
`fcp-mcp-server-main`). Só 3 referências no código ativo, todas em
comentários (`fcpxml/diarize.py`, `tests/test_diarize.py`,
`admin/api/shared.py`), nenhum import ou caminho dependia dela. Além do
peso morto, ela também inflava qualquer lint rodado com `--exclude`
explícito (que sobrescreve o `exclude` do `pyproject.toml`) — foi assim que
um `ruff check . --exclude docs/` chegou a acusar 510 erros, quase todos
dentro dela. Movida para `~/Archives/G-ART-WHISPERX-backup` (fora do
workspace git), copiada e verificada (`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 dentro de `code/`.
---
@@ -102,6 +134,8 @@ quanto arquivo gigante.
```bash
cd code && ./Engine/run_after_fix.sh # lint zerado + 1.498 testes
admin/run_app.command # se mexeu no app (padrão de revisão)
admin/run.command # app + atualização incremental da RAG
rag/search_gart.sh "consulta" # busca híbrida no índice RAG (ver rag/README.md)
```
E, além do script:
+269
View File
@@ -0,0 +1,269 @@
# 10 - Mapa de Reestruturacao de Funcionalidades
> Escopo: roteiro pratico para reorganizar o codigo sem quebrar o produto.
> Baseado na varredura de 2026-08-24 sobre engine Python, ponte do app,
> ferramentas MCP e app SwiftUI.
## 1. Diagnostico rapido
O projeto ja tem uma arquitetura-alvo correta: `fcpxml/` como engine puro,
`server.py` + `server_tools/` como camada MCP, `admin/` como ponte JSON-lines
do app e `MacApp/` como interface. A melhoria agora nao e "reinventar" a
arquitetura, e reduzir os pontos onde as responsabilidades ainda se misturam.
### Pontos fortes
- Engine Python bem testado e com regra clara: logica de timeline fica em
`fcpxml/`.
- `writer/` ja foi quebrado em mixins por assunto, preservando API publica.
- `server.py` funciona como composition root e usa dispatch por dicionario.
- Documentacao interna registra decisoes, armadilhas e padroes do projeto.
- Fluxos criticos tem testes extensos em `code/tests/`.
### Dores atuais
- Alguns arquivos voltaram a virar centros de gravidade:
- `server_tools/voice.py` (~999 linhas)
- `server_tools/subtitles.py` (~760 linhas)
- `MacApp/Sources/WizardView.swift` (~979 linhas)
- `MacApp/Sources/TranscriptionView.swift` (~843 linhas)
- `fcpxml/model_manager.py` (~748 linhas)
- `admin/` e `server_tools/` expõem fluxos parecidos por caminhos diferentes,
o que aumenta risco de uma funcionalidade existir no MCP e faltar no app.
- ~~`admin/` ainda fica fora do lint principal~~ — resolvido na Fase 0
(2026-09-22): `admin/` entrou no gate de `run_after_fix.sh`.
- ~~`WHISPERX` e backups aparecem junto da base ativa~~ — resolvido na
Fase 0 (2026-09-22): movido para fora do workspace git.
- O app SwiftUI quase nao tem rede automatizada; compilar nao garante que uma
tela abre.
## 2. Mapa de dominios desejado
```text
Produto
MacApp/ Interface e experiencia do usuario
admin/ Ponte JSON-lines do app
server.py + server_tools/ Entrada MCP
Engine
fcpxml/models/ Dados e contratos
fcpxml/parser.py FCPXML -> objetos
fcpxml/writer/ Escrita e edicao de XML
fcpxml/voice_* Analise e decisoes por voz
fcpxml/text_layout.py Layout de legendas
fcpxml/model_manager.py Catalogo, configs e modelos
Suporte
tests/ Rede automatizada
Engine/docs/ Decisoes e operacao
examples/ Fixtures de uso
Legado / referencia
WHISPERX/ Deve sair do caminho ativo ou virar referencia clara
```
Regra de organizacao: uma funcionalidade nasce no engine, depois ganha duas
portas finas se necessario: uma tool MCP em `server_tools/` e um comando do app
em `admin/api/`.
## 3. Reestruturacao por fases
### Fase 0 - Higiene antes de mexer — `concluída em 2026-09-22`
Objetivo: reduzir ruido e proteger a base antes de mover codigo.
- ~~Decidir o destino de `code/WHISPERX`~~ — não era submodule (sem
`.gitmodules`), era 2,6 GB de backups órfãos do próprio projeto sem
nenhuma referência ativa. Movido para `~/Archives/G-ART-WHISPERX-backup`
(fora do workspace git), copiado com `rsync` e conferido com `diff -rq`
antes de remover o original. Detalhe: essa pasta também inflava qualquer
`ruff check --exclude docs/` manual (a flag sobrescrevia o `exclude` do
`pyproject.toml`, que já ignorava `WHISPERX/`) — ver `05_EXPERIENCIAS.md`
#34.
- ~~Incluir `admin/` em uma checagem de lint separada antes de colocar no
gate obrigatório~~ — checado com a config real do projeto (não o default
do ruff): só 6 erros, todos `E402` por `sys.path.insert` antes de import
local. Resolvido com `# noqa: E402` (convenção já usada no projeto) e
`admin/` entrou direto no gate obrigatório (`run_after_fix.sh`, passo
2/3), sem precisar de etapa intermediária "separada".
- Corrigido de quebra: `fcpxml/writer/adjustment.py` gerava um `<adjustment>`
inválido no DTD — não estava no escopo original da Fase 0, mas surgiu na
investigação e era pequeno o bastante para resolver junto (ver
`05_EXPERIENCIAS.md` #34).
- Atualizados: `02_MODULES.md` (versão, linhas de `writer/`, módulos novos
`builders.py`/`adjustment.py`/`analise.py`/`transcription/`),
`09_MANUTENCAO.md` (contagem de testes/lint, itens §2.3/§2.6/§2.8
resolvidos, novo item §2.7 registrando `test_refine_voice_timeline_tool`).
- **Pendente, não fechado nesta rodada:** "documentar oficialmente quais
pastas são produto ativo, legado e backup" como um documento à parte —
o que existia de fato como "legado" (`WHISPERX`) já foi resolvido, não
sobrou candidato claro para justificar um novo documento agora.
Entrega obtida: lint de `admin/` no gate, `code/writer/adjustment.py`
correto e testado, ~2,6 GB fora do caminho ativo, docs sincronizados com o
código atual.
### Fase 1 - Contratos entre camadas
Objetivo: impedir que MCP, app e engine driftam entre si.
- Criar um registro unico de capacidades, por exemplo:
- nome interno da funcionalidade;
- funcao pura do engine;
- handler MCP, se existir;
- comando `admin`, se existir;
- tela Swift, se existir;
- testes associados.
- Adicionar teste que detecta comandos importantes presentes no MCP mas ausentes
na ponte do app, quando fizer sentido.
- Padronizar o retorno dos comandos `admin/api`: `ok`, `path`, `message`,
`error`, `unchanged`, `artifacts`.
Entrega esperada: mapa vivo de funcionalidades e menos "funciona no Claude,
nao aparece no app".
### Fase 2 - Dividir `server_tools/voice.py`
Objetivo: separar o fluxo de voz por etapas reais do produto.
Divisao sugerida:
```text
server_tools/voice/
__init__.py Reexporta TOOLS e HANDLERS
analysis.py analyze_voice_features, build_voice_timeline
speakers.py diarize_media, remove_speakers
refinement.py refine_voice_timeline, remove_speech_gaps
actions.py apply_voice_actions
local_ai.py generate_voice_script
config.py get/save_voice_analysis_config
```
Cuidados:
- Manter os nomes publicos reexportados para nao quebrar testes/imports.
- Mover em uma etapa por arquivo, rodando testes de voz a cada passo.
- Nao mover regra de negocio para `server_tools/voice/`; se aparecer regra
nova, ela deve descer para `fcpxml/voice_*`.
Testes minimos: `test_voice_actions.py`, `test_voice_actions_tool.py`,
`test_voice_timeline.py`, `test_voice_timeline_tool.py`, `test_diarize.py`,
`test_voice_features.py`.
### Fase 3 - Separar `fcpxml/model_manager.py`
Objetivo: reduzir mistura entre catalogo, download, configuracao e estado.
Divisao sugerida:
```text
fcpxml/model_manager/
__init__.py API publica atual
catalog.py models.json, recomendados, metadata
storage.py diretorios, instalados, migracao
download.py download/cancel/progresso
transcription_config.py modelo selecionado, idioma
voice_config.py analise de voz, silencio, legendas
```
Cuidados:
- Preservar imports atuais via `__init__.py`.
- Separar funcoes puras de funcoes com I/O para facilitar teste.
- Nao acoplar config do app a nomes de tela Swift.
Testes minimos: `test_models.py`, `test_models_api.py` se existir,
`test_voice_analysis_config.py`, `test_project_config.py`.
### Fase 4 - Reorganizar o Assistente SwiftUI
Objetivo: tornar o fluxo de 7 etapas legivel e testavel por partes.
Divisao sugerida:
```text
MacApp/Sources/Wizard/
WizardView.swift Casca, navegacao e estado global
WizardState.swift Estado do fluxo e canAdvance
ProjectStepView.swift
TranscribeStepView.swift
VoiceAnalysisStepView.swift
AIScriptStepView.swift
ReviewStepHost.swift
ProcessStepView.swift
DoneStepView.swift
```
Boas praticas para essa fase:
- Extrair primeiro views pequenas, sem alterar comportamento.
- Depois extrair calculos puros de `canAdvance`, nomes de arquivos e selecao
de artefatos para tipos testaveis.
- Usar harness manual documentado em `08_APP_MACOS.md` para abrir as telas
tocadas.
Entrega esperada: cada etapa do wizard vira um arquivo com responsabilidade
unica.
### Fase 5 - Unificar validacao e saida da ponte `admin/`
Objetivo: deixar os comandos do app tao disciplinados quanto os handlers MCP.
- Criar helpers de path/output equivalentes aos de `server_tools/_shared`,
ou mover helpers comuns para uma camada compartilhada que nao saiba de MCP.
- Trocar chamadas diretas a `server.generate_output_path` por helper de dominio
que nao puxe `server.py` quando a ponte so precisa de path.
- Adicionar lint de `admin/` ao fluxo de manutencao depois de corrigir erros
existentes.
Entrega esperada: ponte mais fina, menos import acidental de transporte MCP.
### Fase 6 - Tests e gates de seguranca
Objetivo: fazer a reorganizacao ser barata de continuar.
- Criar testes de "arquitetura":
- `fcpxml/` nao importa `server`, `server_tools` nem `admin`;
- handlers MCP sempre retornam via `_text_result`;
- comandos `admin` retornam JSON no formato padrao.
- Criar teste de import publico para garantir que reexports antigos continuam.
- Para SwiftUI, manter harnesses por tela critica ate existir um build mais
estruturado.
Entrega esperada: mover arquivos deixa de ser aposta.
## 4. Prioridade recomendada
1. Fase 0: limpar mapa ativo vs legado.
2. Fase 1: criar registro de capacidades.
3. Fase 2: dividir voz em `server_tools`.
4. Fase 5: fortalecer `admin/`.
5. Fase 4: quebrar `WizardView`.
6. Fase 3: dividir `model_manager.py`.
7. Fase 6: ampliar gates conforme as fases estabilizam.
Motivo: primeiro se reduz incerteza, depois se separa o arquivo que mais muda
no fluxo novo de voz, e so entao se mexe nas telas maiores.
## 5. Checklist para cada refatoracao
- Mover sem mudar comportamento na primeira passada.
- Preservar API publica com reexports.
- Rodar testes focados depois de cada movimento.
- Rodar `cd code && ./Engine/run_after_fix.sh` antes de concluir.
- Se mexeu em `MacApp/`, compilar e abrir a tela afetada.
- Atualizar docs no mesmo commit.
- Registrar aprendizado em `05_EXPERIENCIAS.md` quando houver bug real.
## 6. Principios de boas praticas para este projeto
- Engine puro: sem MCP, sem Swift, sem JSON de tela.
- Camadas de entrada finas: validam, chamam engine, formatam resposta.
- Tempo de timeline sempre racional (`TimeValue`), exceto metricas de audio e
UI onde segundos float sao apenas apresentacao/analise.
- Original nunca e sobrescrito.
- XML sempre entra por `safe_xml.py`.
- Dependencias opcionais continuam lazy.
- Arquivo grande so e problema quando contem varios assuntos.
- Toda funcionalidade importante deve ter dono, porta MCP/app documentada e
teste correspondente.