Files
gart/code/Engine/docs/08_APP_MACOS.md
T
João HenriqueandClaude Sonnet 5 7b5aed79ee feat(voz): legenda por ênfase, forced align, IA local e correções de zoom/revisão
Trabalho da branch feat/revisao-enfases: pipeline de edição por voz ganha
alinhamento forçado (whisperx), roteirização por LLM local (Ollama), e a
etapa 5 (revisão de frases) passa a refletir de verdade o que é aplicado.

- generate_subtitles_by_emphasis: legenda comum cobre o clipe inteiro,
  legenda dinâmica só nas frases de ênfase, e a comum é desativada
  (enabled="0") onde a dinâmica cobre, em vez de nunca ser gerada ali.
- validate_subtitle_layout ignora títulos com enabled="0" — corrige falso
  positivo de colisão contra o que está desativado no lugar dele.
- Corrige zoom/marcador sendo descartado quando a borda encosta exatamente
  no início de um corte.
- Etapa 5 do Assistente: recarrega quando as decisões da IA mudam (com
  fresh=true, ignorando a revisão salva antiga) — resolve a dessincronia
  entre "ativa" na tela e o que já foi cortado no FCPXML.
- Etapa "Processar" reaplica as decisões da revisão (_phrase_actions.json)
  antes da cadeia de remoção de silêncio/legendas — antes, desativar uma
  frase na etapa 5 não tinha efeito nenhum no vídeo final.
- Etapa "Concluído" fundida em "Processar" — abrir no Final Cut/Finder
  aparece assim que termina, sem slide extra.
- Palavra clicável na etapa 5 agora funciona como toggle (clique de novo
  desfaz) e mostra a própria ênfase (sublinhado colorido + peso da fonte).
- fcpxml/forced_align.py, fcpxml/llm_local.py, ai_edit.py: alinhamento
  fonético via whisperx e roteirização local via Ollama/Gemma.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-21 18:26:04 -04:00

10 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
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:

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 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.