Files
gart/code/Engine/docs/08_APP_MACOS.md
T
João HenriqueandClaude Opus 5 711c397dfe fix: admin/api apontava para admin/code (inexistente) — crash no app
Ao dividir _shared.py em admin/api/*.py ontem, o cálculo
`Path(__file__).resolve().parent.parent / "code"` foi copiado sem ajustar
para o nível de diretório novo. No arquivo original (admin/models_api.py,
direto em admin/) dois `.parent` chegavam na raiz do repo. Em
admin/api/shared.py, um nível mais fundo, dois `.parent` param em admin/ —
e admin/code nunca existiu. sys.path nunca recebia code/, então toda ação
que passa por `server` (analisar voz, aplicar decisões) crashava o app com
ModuleNotFoundError: server_tools.

O bug sobreviveu a duas rodadas de validação da sessão anterior — lint
zero, 1454 testes verdes, comando testado manualmente pela ponte — porque
todos rodam num venv com install editável (__editable__.fcp_mcp_server.pth)
que já deixa fcpxml/server_tools importáveis por conta própria, mascarando
qualquer erro no cálculo manual de sys.path. Só o app real, no fallback sem
uv, expõe o bug.

Correção: o cálculo de sys.path sai de cada módulo de comando (estava
duplicado em nove arquivos) e passa a existir uma única vez em
admin/api/__init__.py, que roda antes de qualquer submódulo — nenhum
precisa mais da própria cópia.

O teste de regressão precisou de duas tentativas pelo mesmo motivo do bug:
a primeira versão também passava com o bug presente, por rodar no mesmo
venv "de sorte". Só ficou confiável isolando um subprocess que remove
site-packages do sys.path antes de importar — confirmado nos dois sentidos,
falha com o bug reintroduzido e passa com a correção
(TestCodeDirResolution).

Detalhe completo, incluindo por que o comando manual não pegou:
Engine/docs/05_EXPERIENCIAS.md #25.

Lint zerado, 1457 testes passando (3 novos), app compilado.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 09:50:04 -04:00

204 lines
9.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 08 — O app macOS (`MacApp/`) e o Assistente
> **Escopo:** O app SwiftUI e o Assistente: build, telas, ponte e a etapa 5.
> **Não cobre:** Engine Python (→ 02) · ferramentas MCP (→ 03)
O app SwiftUI é como o usuário opera o sistema sem abrir terminal nem conversar
com uma IA. São ~5.500 linhas em `MacApp/Sources/`, e ele **não tem lógica de
edição**: tudo que ele faz é montar argumentos, chamar a ponte Python e mostrar
o resultado.
Última varredura: 2026-08-19
---
## 1. Como o app é construído — leia antes de mexer
**Não existe `.xcodeproj` nem `Package.swift`.** O app é compilado invocando o
`swiftc` direto sobre `MacApp/Sources/*.swift`:
```bash
cd code && ./MacApp/build_app.sh # compila e monta o .app
cd code && ./MacApp/build_app.sh --run # compila e abre
```
Consequências práticas, todas já sentidas:
- **Arquivo novo em `Sources/` entra sozinho** no build. Não há lista de alvos.
- **Não dá para adicionar dependência SPM** sem antes migrar o build inteiro.
- **Compilar não prova que roda.** Componentes SwiftUI que embrulham classes
Objective-C podem falhar só em tempo de execução, ao abrir a tela. Foi o que
aconteceu com `VideoPlayer` (AVKit): compilava limpo e abortava ao abrir a
etapa 5 (`05_EXPERIENCIAS.md` #22). Por isso a regra: **alterou a interface,
abra a tela de fato.**
### Testando uma tela sem navegar o app inteiro
Um harness de vinte linhas compila os mesmos fontes com um `@main` próprio que
monta só a tela em questão. Reproduz crash de runtime em segundos:
```bash
swiftc -parse-as-library -sdk "$(xcrun --sdk macosx --show-sdk-path)" \
-target arm64-apple-macosx26.0 \
MacApp/Sources/PhraseReviewView.swift MacApp/Sources/PhraseReviewModel.swift \
MacApp/Sources/TimelineTracksView.swift MacApp/Sources/Models.swift \
MacApp/Sources/PythonBridge.swift /tmp/HarnessMain.swift -o /tmp/harness
```
O `@main` do harness carrega a tela, imprime o que interessa e chama
`NSApplication.shared.terminate` — dá para afirmar "abriu e funcionou" sem
depender de screenshot.
---
## 2. Estrutura das telas
| Arquivo | Linhas | Papel |
|---------|-------:|-------|
| `WizardView.swift` | 808 | **O Assistente** — fluxo guiado de 7 etapas |
| `TranscriptionView.swift` | 843 | Transcrição avulsa e processamento em lote |
| `ModelDownloadView.swift` | 545 | Catálogo e download de modelos Whisper |
| `CaptionsView.swift` | 545 | Legendas dinâmicas: estilo + preview ao vivo |
| `TimelineTracksView.swift` | 506 | Timeline com trilhas, zoom e playhead |
| `PhraseReviewModel.swift` | 429 | Estado da etapa 5: frases, player, zooms |
| `PhraseReviewView.swift` | 413 | Etapa 5: preview + inspector de frases |
| `VoiceAnalysisView.swift` | 322 | Parâmetros do motor de ênfase |
| `ProjectView.swift` | 293 | Inspeção do `.fcpxml` |
| `Models.swift` | 274 | Espelhos Swift do JSON da ponte |
| `PythonBridge.swift` | 230 | **A ponte** — ver seção 3 |
| `SubtitlePreviewView.swift` | 218 | Preview 9:16 das legendas |
| `App.swift` | 69 | `NavigationSplitView` e as abas |
Abas (`ActiveTab` em `App.swift`): Assistente · Projeto · Legendas · Análise de
Voz · Modelos · Sobre. As cinco últimas são "Avançado" — atalhos para operações
soltas. O Assistente é o caminho principal.
---
## 3. `PythonBridge.swift` — como o app fala com o Python
O app lança `admin/models_api.py` como **subprocesso**, passando o comando e um
JSON como `argv`, e lê **JSON-lines** no stdout.
```swift
PythonBridge.call(command: "build_phrase_review",
arguments: ["voice_timeline": path]) { result, error in … }
```
Dois pontos que já causaram problema e estão resolvidos no código — não os
desfaça sem entender:
- **`uv run` precisa rodar com cwd em `code/`.** O `uv` escolhe o ambiente pelo
diretório do processo, não pelo caminho do script. Rodar da raiz fazia o `uv`
criar um segundo `.venv` vazio e ignorar tudo que estava instalado em
`code/.venv` — librosa e pyannote instalavam com sucesso e o app insistia que
faltavam.
- **`scriptURL` procura `admin/models_api.py`** subindo diretórios a partir do
cwd, do bundle e do home. É o que faz o app funcionar tanto rodando do Xcode
quanto do `.app` montado.
- **O `sys.path` que torna `fcpxml`/`server_tools` importáveis dentro de
`admin/api/` mora só em `admin/api/__init__.py`.** Não copie esse cálculo
para um módulo de comando individual — foi exatamente essa cópia,
desatualizada em um nível de diretório, que quebrou toda ação que passa por
`server` (`05_EXPERIENCIAS.md` #25). E não confie em "testei com `uv run` e
funcionou": esse comando roda no mesmo venv com install editável que
mascara esse tipo de erro. O teste que pega de verdade é
`tests/test_models_api.py::TestCodeDirResolution`.
Para adicionar um comando: função em `admin/api/<assunto>.py`, registro na
tabela de `admin/models_api.py`, e `PythonBridge.call` do lado Swift. Os 37
comandos e seus formatos estão documentados no docstring de `models_api.py`.
---
## 4. O Assistente — as 7 etapas
`WizardStep` (`WizardView.swift`) é um enum sequencial; `canAdvance` decide
quando o botão "Continuar" libera.
| # | Etapa | O que acontece | Comando da ponte |
|---|-------|----------------|------------------|
| 1 | Projeto | Escolhe a pasta de saída e o `.fcpxml` | `project_config` |
| 2 | Transcrever | Transcreve toda a mídia do projeto | `transcribe` |
| 3 | Analisar voz | Mede ênfase, locutores, emoção | `analyze_voice` |
| 4 | Decisões da IA | Copia para o chat, cola o JSON de volta, aplica | `apply_voice_actions` |
| 5 | **Revisar ênfases** | Lapida frase a frase — ver seção 5 | `build_phrase_review` / `save_phrase_review` |
| 6 | Processar | Silêncios, preenchimento, legendas | vários, em cadeia |
| 7 | Concluído | Abre no FCP ou mostra no Finder | — |
**A etapa 4 é a única manual do fluxo**, e de propósito: o julgamento de qual
tomada usar e onde dar zoom é conversa com uma IA (skill `editar-por-voz`), não
um botão. O app monta o pedido pronto no clipboard e recebe o JSON de volta.
**Etapa 1 — armadilha registrada:** não escolha como "o projeto" um arquivo já
gerado pelo fluxo (`_voice_edit`, `_silence_removed`, …). Os cortes de voz
assumem timestamps da mídia **original**; reaplicá-los sobre um arquivo já
cortado desloca tudo em silêncio. O wizard avisa (`looksLikeGeneratedFile`).
---
## 5. Etapa 5 — a sala de edição
Única tela que ocupa a janela toda: o corpo do wizard é uma coluna de 640pt, e
essa etapa escapa dela porque precisa da largura (`step == .revisar` em
`WizardView.body`).
```
┌────────────────────────────┬──────────────┐
│ Preview (AVPlayerLayer) │ Inspector │
│ enquadrado no formato │ de frases │
│ de entrega do projeto │ │
├────────────────────────────┴──────────────┤
│ Timeline: 6 trilhas, zoom, playhead │
└───────────────────────────────────────────┘
```
**Trilhas:** zooms · frases · energia por palavra · emoção · locutor ·
roteiro/bastidor. Todas desenhadas sobre o mesmo eixo de tempo, com uma coluna
fixa à esquerda nomeando cada uma.
**O que o usuário decide por frase:** nível de ênfase (0–3), ativo/inativo,
texto, roteiro/bastidor e o trim das pontas. O trim anda em **fronteira de
palavra** — cortar é apontar para uma palavra, arrastando a borda do bloco ou
clicando na palavra no inspector.
**Zoom manual:** arrastar na timeline marca um trecho; botão direito cria um
zoom nele. O zoom guarda **só o quando** — escala e ramp vêm das configurações
de Análise de Voz no momento do render, então mudar lá restiliza todos.
**Decisões de implementação que parecem detalhe e não são:**
- **O preview não renderiza nada.** Ele toca a mídia original e *pula* os
trechos removidos. Renderizar para conferir um toggle poria minutos entre a
decisão e o resultado. O observador roda a 60 Hz porque o período dele é
exatamente quanto de material cortado dá para ouvir antes do pulo.
- **O enquadramento é o do projeto, não o da mídia.** As gravações são
horizontais e a entrega é vertical; o app lê o formato do `.fcpxml`
(`inspect`) e mostra o corte central aproximado, com um selo para alternar
para a mídia original. O enquadramento real de cada clipe vem do FCP — o
preview é aproximação, e o selo diz isso.
- **Nada é processado aqui.** "Continuar" grava o `_phrase_review.json` e o
`_phrase_actions.json` derivado dele. A geração é da etapa 6.
- **A revisão é sempre remontada da análise atual**, com as decisões salvas
reaplicadas por cima (`merge_saved_decisions`). Assim refazer a análise de voz
melhora a tela em vez de ficar mascarado por uma cópia velha; uma decisão cuja
frase se moveu mais de 0,25 s é descartada em vez de colar na frase errada.
---
## 6. Estado atual e o que falta
**Funciona e foi verificado:** carga das frases com decisões da IA, as 6
trilhas, seleção sincronizada nos três painéis, trim por palavra, zoom manual,
reprodução parando no ponto exato (erro de 0 ms medido), pulo dos trechos
removidos, enquadramento vertical, gravação ao avançar.
**Ainda em aberto:**
- **A etapa 6 não consome o `_phrase_review.json`.** A ligação — zoom e legenda
dinâmica só nas frases de ênfase, legenda comum no resto — é a próxima tarefa.
- **`MacApp/` não tem teste automatizado.** A rede é o harness da seção 1 e o
olho do usuário. Toda mudança de interface precisa ser aberta de fato.
- **O preview aproxima o reenquadramento vertical** pelo corte central; se os
clipes forem reposicionados no FCP, diverge.