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>
9.8 KiB
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 runprecisa rodar com cwd emcode/. Ouvescolhe o ambiente pelo diretório do processo, não pelo caminho do script. Rodar da raiz fazia ouvcriar um segundo.venvvazio e ignorar tudo que estava instalado emcode/.venv— librosa e pyannote instalavam com sucesso e o app insistia que faltavam.scriptURLprocuraadmin/models_api.pysubindo diretórios a partir do cwd, do bundle e do home. É o que faz o app funcionar tanto rodando do Xcode quanto do.appmontado.- O
sys.pathque tornafcpxml/server_toolsimportáveis dentro deadmin/api/mora só emadmin/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 porserver(05_EXPERIENCIAS.md#25). E não confie em "testei comuv rune 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.jsone o_phrase_actions.jsonderivado 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.