APRENDIZADO & COGNIÇÃO

OpenCut classic tem 63.8k estrelas e quase nenhuma automação. Um patch de uma linha muda isso, e daí saí com quatro armadilhas.

O editor de vídeo open source OpenCut coleciona 63.8k estrelas no GitHub, mas está em reescrita. A versão nova só…

O editor de vídeo open source OpenCut coleciona 63.8k estrelas no GitHub, mas está em reescrita. A versão nova só serve hello world no navegador. O editor de vídeo em si só vive no classic (arquivado). Fiz o fork e coloquei um servidor MCP nele para que o Claude Code consiga operar a timeline sozinho. Publiquei como opencut-mcp v0.1.0.

Nesse caminho apareceram quatro armadilhas que ninguém documentou. Elas moram no código do editor, não no README. Vou detalhar cada uma na ordem em que apareceram, porque a leitura importa: o design que cria as armadilhas também é o que torna a automação viável.

Antes de entrar: os deliverables são kenimo49/opencut-mcp v0.1.0 (MIT) e a página do produto com o screencast está em kenimoto.dev/products/opencut-mcp/.

O EditorCore do classic já era feito para automação

Quando abri o apps/web/src/core/index.ts pela primeira vez, esperava algo espalhado por Zustand ou Redux, com o estado escondido em stores separadas que eu teria que caçar via DOM. Encontrei um singleton limpo com subdivisão por Manager:

export class EditorCore { private static instance: EditorCore | null = null; public readonly timeline: TimelineManager; public readonly command: CommandManager; public readonly playback: PlaybackManager; public readonly scenes: ScenesManager; public readonly project: ProjectManager; public readonly media: MediaManager; public readonly renderer: RendererManager; // ... static getInstance(): EditorCore { ... }}

Cada Manager expõe métodos públicos tipados como addTrack({ type, index }) ou insertElement({ element, placement }). Todas as mutações passam pelo CommandManager, o que dá undo/redo de graça. A intenção do time provavelmente era separação interna, mas de fora parece uma superfície de API pronta para virar ferramentas MCP.

Ou seja: uma única linha window.__editor = EditorCore.getInstance() transforma o page.evaluate do Playwright numa ponte completa. O patch Phase 1 do fork tem exatamente uma linha de código funcional:

if (typeof window !== "undefined" && process.env.NODE_ENV !== "production") { (window as unknown as { __editor: EditorCore }).__editor = EditorCore.getInstance();}

O guard NODE_ENV mantém isso fora dos builds de produção. Nesse ponto os doze Managers e os nove métodos do TimelineManager já estavam acessíveis. O opencut-mcp em si é um processo fino em stdio transport que mantém uma sessão Playwright e traduz cada chamada MCP num page.evaluate.

Armadilha 1: trilhas vazias são removidas a cada comando

Primeiro bug. A sequência addTrack({ type: "audio" }) seguida de insertElement({ ..., placement: { mode: "explicit", trackId } }) falhava na segunda chamada com “Track not found”. O addTrack devolvia um UUID válido, mas scenes.getActiveScene().tracks.audio continuava vazio.

A causa mora no construtor do EditorCore:

this.command.registerReactor(() => { const activeScene = this.scenes.getActiveSceneOrNull(); if (!activeScene) return; const tracks = activeScene.tracks; const prunedTracks = { ...tracks, overlay: tracks.overlay.filter((track) => track.elements.length > 0), audio: tracks.audio.filter((track) => track.elements.length > 0), }; // updateTracks caso o resultado seja diferente});

O CommandManager.execute() roda todos os reactors registrados logo depois de command.execute(). Esse reactor específico remove trilhas de overlay e audio que estão sem elementos. Então:

  1. addTrack acrescenta uma trilha nova (vazia) na cena
  2. O reactor vê a trilha vazia e a remove
  3. Quando o insertElement roda com o trackId devolvido, a trilha já sumiu

A saída é fazer numa chamada só: insertElement com placement: { mode: "auto", trackType: "audio" }. O auto-placement cria a trilha sob demanda enquanto insere o elemento; ela sobrevive ao reactor porque agora tem um elemento dentro.

Nada disso está no README nem em comentários. A leitura vem da definição do reactor cruzada com o sintoma. Se o OpenCut algum dia colocar um botão “New track” na interface, o reactor vai precisar de uma escape hatch para placeholders vazios. Como o classic não tem esse botão, o design é consistente do jeito que está.

Armadilha 2: MediaTime é tick inteiro, não segundo

Depois de resolver o posicionamento de trilha, passei duration: 15.008 (a duração real do asset em segundos) para insertElement e caí no seguinte:

Error: addMediaTime(): expected an integer tick count, got 15.008

O apps/web/src/wasm/media-time.ts explica:

export type MediaTime = number & { readonly __mediaTime: unique symbol };function isMediaTime(value: number): value is MediaTime { return Number.isInteger(value);}function requireMediaTime({ value, context }) { if (!isMediaTime(value)) { throw new Error(`${context}: expected an integer tick count, got ${value}`); } return value;}

MediaTime é um tick inteiro com brand type, refletindo o MediaTime(i64) do Rust core em rust/crates/time/src/media_time.rs. Em runtime, TICKS_PER_SECOND vale 120000, então 15.008 segundos = 1.800.960 ticks.

O que quebra é o descasamento: MediaAsset.duration guarda segundos em float, mas todo TimelineElement espera ticks inteiros. A conversão precisa acontecer na fronteira. O wasm já expõe mediaTimeFromSeconds para isso, e eu adicionei ele no patch Phase 1.1:

w.__opencut = { TICKS_PER_SECOND, mediaTime, mediaTimeFromSeconds, roundMediaTime };

Do lado MCP, todos os argumentos temporais (startTime, duration, trimStart, trimEnd, sourceDuration, splitAt.time, move.newStartTime) passam por window.__opencut.mediaTimeFromSeconds({ seconds }) antes de tocar em insertElement. Concentrar a fronteira num ponto só deixa o chamador MCP trabalhar com segundos crus.

Armadilha 3: AudioElement é união discriminada e rejeita em silêncio

Clipes de vídeo entravam. Clipes de áudio simplesmente não faziam nada. Sem erro, sem exception, tracks.audio continuava vazio.

Levei tempo para achar a razão. O InsertElementCommand.validateElementBasics escreve em console.error e retorna false; o comando sai sem tocar em nada:

private validateElementBasics({ element }) { if (requiresMediaId({ element }) && !("mediaId" in element)) { console.error("Element requires mediaId"); return false; } if ( element.type === "audio" && element.sourceType === "library" && !element.sourceUrl ) { console.error("Library audio element must have sourceUrl"); return false; } // ...}

O AudioElement é união:

export interface UploadAudioElement extends BaseAudioElement { sourceType: "upload"; mediaId: string;}export interface LibraryAudioElement extends BaseAudioElement { sourceType: "library"; sourceUrl: string;}export type AudioElement = UploadAudioElement | LibraryAudioElement;

Eu passava { type: "audio", mediaId, ... }. Sem sourceType, o discriminador não bate em nenhum dos ramos, a validação retorna false, o comando encerra em silêncio. CommandManager.execute não lança exception, então o chamador acha que deu certo.

A correção tem duas partes:

  1. Sempre acrescentar sourceType: "upload" no lado do MCP quando elementType === "audio"
  2. Enganchar console.error em volta da chamada do comando e devolver as mensagens capturadas como validationErrors na resposta da ferramenta

O gancho é obrigatório. Ferramentas que falham em silêncio são piores do que ferramentas que lançam exception: o chamador não consegue distinguir “resultado vazio” de “seu input estava errado”.

Armadilha 4: o file input do Import fica sempre no DOM, só com display:none

O upload de mídia, conceitualmente, passa pelo botão Import do painel de Assets. Se você tentar a rota óbvia (clicar em Import, esperar o file picker do sistema, dirigir o picker pelo Playwright), cai num caminho frágil e dependente de OS.

A leitura de apps/web/src/media/use-file-upload.ts revela o atalho:

fileInputProps: { ref: inputRef, type: "file", style: { display: "none" }, onChange: handleFileChange,},

O hook useFileUpload renderiza um <input type="file"> escondido o tempo todo. display: none não tira o elemento do DOM, então page.locator('input[type="file"]').setInputFiles(path) escreve direto nele. O caminho pelo botão Import some da conta; você aterrissa no mesmo handleFileChange que a UI acabaria acionando.

A ferramenta MCP opencut_add_media tem uns 60 LOC, e a maior parte cuida da detecção assíncrona de conclusão: esperar media.getAssets() receber o asset novo via page.waitForFunction. Sincronizar estado é mais difícil do que a interação em si, o que é um padrão recorrente em trabalho com Playwright.

As doze ferramentas MCP e a demo

Com as quatro armadilhas neutralizadas, o opencut-mcp v0.1.0 expõe doze ferramentas via stdio. Todas passam por Playwright + window.__editor e envolvem TimelineManager / MediaManager / RendererManager um-a-um:

  • opencut_get_state — snapshot de timeline, assets e duração total
  • opencut_add_media — sobe um arquivo local pelo file input escondido
  • opencut_insert_clip — auto-placement cria a trilha e insere o elemento num comando só
  • opencut_split_at — corta num tempo em segundos (com undo)
  • opencut_move / opencut_trim / opencut_delete
  • opencut_undo / opencut_redo
  • opencut_export — aguarda RendererManager.exportProject
  • opencut_screenshot — captura de tela para debug
  • opencut_add_track — adição explícita (quase nunca usado por causa da armadilha 1)

O screencast está embutido na página do produto. Dez segundos de execução ponta a ponta: criar projeto, subir vídeo, subir áudio, colocar cada um numa trilha via auto-placement, cortar o vídeo aos 5 segundos. Tudo via chamadas MCP a partir do Claude Code, zero cliques manuais.

Rodando no seu ambiente

O opencut-mcp aponta para o fork kenimo49/opencut-classic, não para o upstream arquivado. O upstream não tem o patch de exposição.

# 1. Sobe o fork do classic (docker + bun dev:web na porta 3000)git clone https://github.com/kenimo49/opencut-classic.gitcd opencut-classicdocker compose up -d db redis serverless-redis-httpbun install && bun run dev:web# 2. Noutro shell, sobe o opencut-mcpgit clone https://github.com/kenimo49/opencut-mcp.gitcd opencut-mcp && bun installbunx tsx src/index.ts # stdio transport

Aponte o config do seu cliente MCP para o binário do opencut-mcp e o Claude passa a chamar opencut_insert_clip direto. O fork vai se afastar mais do upstream conforme entram novos hooks (data-testid para wait/assert, headless-export toggle), então acompanhe os dois por commit por enquanto. Semver ainda não faz sentido aqui.

Fecho

  • O EditorCore do OpenCut classic é singleton com subdivisão por Manager. A superfície de API para drivers externos já estava pronta.
  • A exposição por window é uma linha (Phase 1) mais os helpers wasm (Phase 1.1). Dois commits no total, ambos limitados a dev mode.
  • As quatro armadilhas são: pruning de trilhas vazias pelo reactor, MediaTime como tick inteiro, união discriminada de AudioElement com silent reject e file input sempre no DOM em display:none. Nenhuma delas está na documentação; a leitura sai do código.
  • opencut-mcp v0.1.0 tem 12 ferramentas, 707 LOC e nota B(87) no code-health (kenimo49/code-health-ops)

Próximo da série: conectar o opencut-mcp a um LLM local (Wan2GP + qwen) para o loop de edição rodar inteiro na RTX 4070, sem chamadas para cloud. As ferramentas que precisam de conteúdo gerado (add_media para imagens, add_text para legendas) plugam direto na saída do Wan2GP.

Quando o reescrito do OpenCut lançar o servidor MCP oficial, essa rota via fork vira obsoleta. Até lá, o opencut-mcp preenche o espaço.

Perguntas frequentes

Respostas institucionais sobre esta cobertura, fontes e limites editoriais.

Sobre o que trata “OpenCut classic tem 63.8k estrelas e quase nenhuma automação. Um…”?
O editor de vídeo open source OpenCut coleciona 63.8k estrelas no GitHub, mas está em reescrita. A versão nova só serve hello world no navegador. O editor de vídeo em si só vive no classic (arquivado).…
O que a matéria explica sobre “O EditorCore do classic já era feito para automação”?
Quando abri o apps/web/src/core/index.ts pela primeira vez, esperava algo espalhado por Zustand ou Redux, com o estado escondido em stores separadas que eu teria que caçar via DOM. Encontrei um singleton limpo com subdivisão por Manager:export class EditorCore { private static instance:…
O que a matéria explica sobre “Armadilha 1: trilhas vazias são removidas a cada comando”?
Primeiro bug. A sequência addTrack({ type: "audio" }) seguida de insertElement({ ..., placement: { mode: "explicit", trackId } }) falhava na segunda chamada com "Track not found". O addTrack devolvia um UUID válido, mas scenes.getActiveScene().tracks.audio continuava vazio.A causa mora no construtor do…
O que a matéria explica sobre “Armadilha 2: MediaTime é tick inteiro, não segundo”?
Depois de resolver o posicionamento de trilha, passei duration: 15.008 (a duração real do asset em segundos) para insertElement e caí no seguinte:Error: addMediaTime(): expected an integer tick count, got 15.008O apps/web/src/wasm/media-time.ts explica:export type MediaTime = number & { readonly __mediaTime: unique…
O que a matéria explica sobre “Armadilha 3: AudioElement é união discriminada e rejeita em silêncio”?
Clipes de vídeo entravam. Clipes de áudio simplesmente não faziam nada. Sem erro, sem exception, tracks.audio continuava vazio.Levei tempo para achar a razão. O InsertElementCommand.validateElementBasics escreve em console.error e retorna false; o comando sai sem tocar em nada:private validateElementBasics({ element }) {…
Quem edita o conteúdo do Estrato Mente?
A cobertura é produzida e revisada pela equipe editorial do Estrato Mente, com editor-chefe responsável pela linha editorial. Conheça a redação em https://mente.estrato.cc/equipe/.
Quais fontes o Estrato prioriza?
Priorizamos fontes primárias (órgãos oficiais, balanços, estudos revisados, documentos públicos) e cruzamos informações antes da publicação, conforme a política editorial.
Como solicitar correção de uma matéria?
Envie o pedido pela página de Correções (https://mente.estrato.cc/correcoes/) ou Contato (https://mente.estrato.cc/contato/). Erros materiais são corrigidos com registro da atualização.
WhatsApp X LinkedIn
Compartilhar: LinkedIn
Nina Barros

Nina Barros

Nina Barros integra a equipe editorial do Estrato Mente, vertical da rede Estrato, na função de Repórter de Finanças comportamentais. O escopo permanente de cobertura inclui vieses e decisões, com atenção ao leitor brasileiro que busca contexto factual, datas oficiais e implicações práticas. Na produção diária, prioriza fontes primárias (órgãos públicos, balanços, papers e documentos oficiais), cruza pelo menos duas referências independentes quando o tema é contestado e evita manchetes que prometam certeza onde há incerteza. Critérios de qualidade: lead com o fato principal, atribuição clara de autoria da equipe Estrato, atualização com selo quando há mudança material e linguagem acessível sem simplificar demais o risco ou o contexto. Trabalha sob revisão da mesa editorial e do editor-chefe do portal. Matérias YMYL recebem segunda leitura antes da publicação e podem ser atualizadas quando surgem novos dados oficiais. Limites: este perfil descreve o papel editorial na marca Estrato. O conteúdo publicado é informativo e não constitui aconselhamento médico, financeiro, jurídico ou profissional personalizado. Transparência ao leitor: metodologia em /metodologia/, ética em /etica-editorial/, correções em /correcoes/, equipe em /equipe/ e canal em /contato/ ([email protected]). Na página-mãe da rede, a bio ampliada e o arquivo de matérias ficam em estrato.cc/blog/, com link para o arquivo do autor em cada portal.

Deixe um comentário