# 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 admin/run_app.command # compila, fecha a instância antiga e abre (padrão de revisão) ``` 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/.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 **ou** gera por IA local (Ollama/Gemma 3), aplica | `apply_voice_actions` / `generate_voice_script` | | 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 tem duas saídas:** - **Manual (chat):** o app monta o pedido pronto no clipboard (skill `editar-por-voz`) e recebe o JSON de volta — o julgamento de qual tomada usar e onde dar zoom fica com a IA numa conversa. - **Automática (IA local):** botão "Gerar roteiro por IA local (Ollama/Gemma 3)". Ele manda a *voice timeline inteira* (o arquivo) junto com o brief para um modelo local (Ollama), que decide cortes/zooms/textos de uma vez, devolve o roteiro legível + o JSON de ações e já aplica no FCPXML (non-destructive). Não precisa sair do app nem colar nada. O modelo é escolhido num **picker que lista os modelos instalados no Ollama** (populado via `list_ollama_models` quando a etapa abre); se o Ollama estiver fora do ar, cai para um campo de texto livre. Troque para `llama3` etc. se tiver outro modelo. Requer o Ollama rodando em `localhost:11434`. **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.