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

9.8 KiB
Raw Blame History

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:

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:

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.

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.