dev-claude-reel

Cria um vídeo vertical 3D (1080x1920) de ~50 segundos onde um desenvolvedor e um personagem 'Claude' têm uma conversa curta e engraçada. Lida com roteiro, vozes ElevenLabs, cena three.js renderizada quadro a quadro no Chrome headless, câmera e cortes, mix de áudio e o MP4 final, além de uma verificação de qualidade que captura movimentos com falhas antes da renderização completa.

O que é dev-claude-reel?

O skill dev-claude-reel automatiza a produção de um videoclipe 3D curto e polido, no formato vertical (1080x1920, ~50 segundos), estrelado por um desenvolvedor e um personagem 'Claude'. Ele orquestra um pipeline completo: desde a definição de um roteiro breve e a geração de vozes realistas via ElevenLabs até a renderização de uma cena three.js quadro a quadro em um navegador Chrome headless, com câmera, cortes e iluminação cinematográficos. O processo inclui mixagem de áudio, legendas embarcadas e uma etapa de verificação de qualidade automatizada ('motion check') que identifica problemas de animação antes da renderização final, economizando tempo e recursos. Projetado para agentes de IA como Claude Code, Cursor ou Codex, ele opera localmente na máquina do usuário, respeitando limites de hardware (uso de GPU, prioridade baixa) e nunca publica o vídeo – a entrega é apenas o arquivo final. O skill depende de Node.js 22+, Chrome/Edge, ffmpeg e uma chave ElevenLabs, e sua execução é dividida em etapas claras e resumíveis, garantindo que cada chamada de renderização dure menos de 8 minutos e que erros não exijam recomeços completos.

Funciona com✓Claude Code~Codex CLI✓Cursor
npx skills add https://github.com/NiklasMVP/dev-claude-reel/tree/HEAD/dev-claude-reel

Installed? Explore more Programação e Desenvolvimento skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

Perguntar na sua IA favorita

Abre um novo chat com esta habilidade de agente já pré-carregada.

Documentação

O que dev-claude-reel faz?

Saída: um mp4, 1080x1920, 30 fps, H.264 + AAC, cerca de 50 segundos. Uma cena 3D low-poly, dois personagens (um desenvolvedor e um personagem Claude), vozes reais, legendas embutidas, uma câmera que corta nos diálogos e uma mixagem limpa.

Como: uma página three.js é controlada por uma única função setT(t). O Chrome headless renderiza um quadro de cada vez, e o ffmpeg junta os quadros e o áudio.

O que este skill não decide: a aparência dos personagens, onde eles estão e sobre o que conversam. Isso vem do usuário, se for fornecido. Caso contrário, escolha algo em algumas linhas e comece.

Pipeline (nesta ordem, sem pular): briefing → roteiro → vozes → timeline → cena → stills → verificação de movimento → mixagem → renderização completa → multiplexação → verificação → entrega


1. Regras básicas

Elas substituem a conveniência. Não substituem o usuário.

  • Não prejudique a máquina. É o computador diário do usuário. Um navegador headless, renderização por GPU, prioridade de processo mais baixa, sem workers paralelos. Se o WebGL reportar um renderizador por software (SwiftShader / "software" / "Basic Render"), pare e avise o usuário. Nunca force software GL.
  • Segredos permanecem em segredo. A chave ElevenLabs fica em ELEVENLABS_API_KEY (env ou .env). Leia-a apenas dentro dos scripts. Nunca imprima, registre ou escreva em qualquer lugar visível.
  • Stills primeiro, filme depois. Sem renderização completa até que uma folha de contato de stills pareça correta. Uma re-renderização completa por um erro de um único take é o erro mais caro neste fluxo de trabalho.
  • Renderize em blocos em primeiro plano. Jobs em segundo plano morrem quando o turno termina. Mantenha cada chamada de renderização abaixo de ~8 minutos e torne-a retomável (seção 11).
  • Um vídeo por chat. O arquivo de cena cresce rápido. Divida-o em partes, não o releia sem motivo, inicie um novo chat para o próximo reel.
  • Entregue um arquivo, nunca o publique. Publicar é decisão do usuário.
  • Seja honesto sobre lacunas. Falta chave, ffmpeg ou Chrome? Diga exatamente o que falta e ofereça a opção viável mais próxima. Nunca finja um resultado.

2. Verificação de configuração (primeiro minuto)

NecessidadeObservações
Node 22+Usa fetch e WebSocket nativos. Única dependência npm: three.js.
Chrome ou EdgeExecute headless.
ffmpeg + ffprobeNo PATH ou em um local conhecido.
three.jsnpm i [email protected], servido do disco por um servidor local estático. Sem CDN, sem rede durante a renderização.
Chave ElevenLabsO nível gratuito funciona (~10k créditos/mês, acesso à API, vozes pré-definidas, sem licença comercial). ~1 crédito por caractere, geração de som custa extra.
GPUTeste uma vez (seção 3).

Orçamento ElevenLabs: termine o roteiro antes de gerar qualquer coisa, conte seus caracteres, faça apenas takes alternativos para falas-chave e armazene em cache cada resultado para que nada seja pago duas vezes.

Modelo: eleven_v3. Se a conta não puder usá-lo, recorra a eleven_multilingual_v2, remova as [tags de áudio] do texto e avise o usuário que a atuação será mais plana.

Sem chave? Avise. Construa roteiro, cena e uma renderização de teste silenciosa até onde for útil, depois peça a chave. Se o usuário insistir em continuar sem: TTS do sistema operacional, tempos de palavra estimados por contagem de caracteres. Avise que a sincronia labial e as legendas serão mais imprecisas.

Estrutura de pastas

Uma pasta por vídeo, arquivos simples, sem etapa de build.

reel/
  package.json      three
  film.html         canvas + sobreposição de legendas + import map, carrega scene.js
  scene.js          Mundo 3D + setT(t) (dividido em partes quando ficar grande)
  timeline.mjs      Temporização + planos, importado pelo Node E pelo navegador
  gen-audio.mjs     takes.json -> vozes ElevenLabs + ambiente (armazenado em cache no disco)
  words.mjs         Tempos das palavras a partir do json de alinhamento
  build-film.mjs    Barramentos de voz por locutor + film.json (envelopes, legendas)
  music.mjs         Faixa de música opcional
  mix.mjs           Mixagem final
  cdp.mjs           Driver Chrome headless
  render.mjs        Stills | completo (em blocos, retomável)
  check.mjs         QA automatizado
  make.mjs          Executa gen-audio -> build-film -> music -> mix
  vo/ sfx/ frames/ stills/

Qualquer alteração em timeline.mjs significa executar make.mjs do início ao fim novamente. Stems de áudio desatualizados são o clássico bug invisível: um som que pertence a uma temporização antiga aparece como um clique aleatório.


3. Conduzindo o Chrome headless (cdp.mjs)

Um pequeno cliente Chrome DevTools Protocol, sem dependências.

Flags de inicialização: --headless=new --remote-debugging-port=<p> --user-data-dir=<tmp> --no-first-run --disable-extensions --disable-background-timer-throttling --disable-renderer-backgrounding --mute-audio --window-size=W,H --use-gl=angle --use-angle=<backend> --ignore-gpu-blocklist --enable-gpu Backend: d3d11 no Windows, metal no macOS, gl no Linux.

Após conectar:

  • Emulation.setDeviceMetricsOverride { width: 1080, height: 1920, deviceScaleFactor: 1, mobile: false }
  • Teste de GPU: leia UNMASKED_RENDERER_WEBGL via WEBGL_debug_renderer_info. Vazio, ou correspondendo a /swiftshader|software|basic render/i → aborta e avisa o usuário.
  • Seja educado: os.setPriority(os.constants.priority.PRIORITY_LOW) no topo de cada script Node. A cada ~5 s, reduza a prioridade do navegador e de todos os processos filho novamente (o Chrome aumenta a prioridade do processo GPU de volta). Obtenha os pids filhos de SystemInfo.getProcessInfo no websocket do navegador (/json/version → webSocketDebuggerUrl). No Windows, também fixe o navegador a um núcleo lógico via PowerShell ((Get-Process -Id <pid>).ProcessorAffinity = <single bit>).

Superfície da API: goto(url), eval(expr) (awaitPromise + returnByValue, lança erro em exceptionDetails), waitFor(expr, timeout), send(method, params), um array logs (chamadas de console, exceções, entradas de log), close() (mata processo, deleta perfil temporário).

Ponto de partida (adapte, não cole cegamente):

import { spawn } from 'node:child_process';
import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path';

const BROWSERS = [
  'C:/Program Files/Google/Chrome/Application/chrome.exe',
  'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe',
  '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
  '/usr/bin/google-chrome',
];
const BIN = BROWSERS.find(p => fs.existsSync(p));
const ANGLE = { win32: 'd3d11', darwin: 'metal' }[process.platform] ?? 'gl';
const sleep = ms => new Promise(r => setTimeout(r, ms));

export async function launch({ width = 1080, height = 1920, port = 9450 } = {}) {
  try { os.setPriority(os.constants.priority.PRIORITY_LOW); } catch {}
  const profile = fs.mkdtempSync(path.join(os.tmpdir(), 'reel-'));
  const proc = spawn(BIN, [
    `--remote-debugging-port=${port}`, `--user-data-dir=${profile}`, '--headless=new',
    '--no-first-run', '--disable-extensions', '--disable-background-timer-throttling',
    '--disable-renderer-backgrounding', '--mute-audio', `--window-size=${width},${height}`,
    '--use-gl=angle', `--use-angle=${ANGLE}`, '--ignore-gpu-blocklist', '--enable-gpu', 'about:blank',
  ], { stdio: 'ignore' });

  let targets;
  for (let i = 0; i < 100 && !targets?.length; i++) {
    try { targets = await (await fetch(`http://127.0.0.1:${port}/json/list`)).json(); }
    catch { await sleep(150); }
  }
  const ws = new WebSocket(targets.find(t => t.type === 'page').webSocketDebuggerUrl);
  await new Promise((ok, fail) => { ws.onopen = ok; ws.onerror = fail; });

  let seq = 0; const waiting = new Map(); const logs = [];
  ws.onmessage = ({ data }) => {
    const msg = JSON.parse(data);
    if (msg.id && waiting.has(msg.id)) {
      const { ok, fail } = waiting.get(msg.id); waiting.delete(msg.id);
      msg.error ? fail(new Error(msg.error.message)) : ok(msg.result);
    } else if (msg.method === 'Runtime.exceptionThrown') {
      const d = msg.params.exceptionDetails;
      logs.push({ type: 'exception', text: d.exception?.description || d.text });
    } else if (msg.method === 'Runtime.consoleAPICalled') {
      logs.push({ type: msg.params.type, text: msg.params.args.map(a => a.value ?? a.description).join(' ') });
    }
  };
  const send = (method, params = {}) => new Promise((ok, fail) => {
    const id = ++seq; waiting.set(id, { ok, fail });
    ws.send(JSON.stringify({ id, method, params }));
  });

  await send('Runtime.enable'); await send('Page.enable'); await send('Log.enable');
  await send('Emulation.setDeviceMetricsOverride', { width, height, deviceScaleFactor: 1, mobile: false });
  // TODO: teste de GPU, timer de redução de prioridade a cada 5 s para proc.pid + filhos, fixação de afinidade no Windows

  const api = {
    send, logs, proc,
    async eval(expression) {
      const r = await send('Runtime.evaluate', { expression, returnByValue: true, awaitPromise: true });
      if (r.exceptionDetails) throw new Error('eval falhou: ' + (r.exceptionDetails.exception?.description || r.exceptionDetails.text));
      return r.result.value;
    },
    async goto(url) { await send('Page.navigate', { url }); await api.waitFor('document.readyState === "complete"', 15000); },
    async waitFor(expr, ms = 10000) {
      for (const t0 = Date.now(); Date.now() - t0 < ms; await sleep(100)) {
        try { if (await api.eval(expr)) return true; } catch {}
      }
      throw new Error('timeout: ' + expr);
    },
    async close() {
      try { ws.close(); } catch {} try { proc.kill(); } catch {}
      setTimeout(() => fs.rmSync(profile, { recursive: true, force: true }), 500);
    },
  };
  return api;
}

4. A página do filme (film.html + scene.js)

A regra de ouro

window.setT(t) é uma função pura do tempo. Dê segundos, ela posiciona cada personagem, objeto, luz e a câmera, renderiza o quadro, pronto. Isso significa:

  • sem performance.now(), sem requestAnimationFrame, sem estado acumulado, sem física
  • sem Math.random(): use um hash seeded de um índice

Por causa disso, os quadros são renderizados em qualquer ordem, os blocos retomam de forma limpa e um único still em qualquer timestamp é barato. Assim que tudo (fontes, texturas) estiver carregado, defina window.TOTAL, window.END_T e window.__ready = true.

film.html

Corpo preto, margem zero, overflow: hidden. Um import map apontando three e three/addons/ para node_modules/three/.... Uma sobreposição de legendas #subwrap / #sub. <script type="module" src="/scene.js">.

Renderizador

const W = 1080, H = 1920;
const renderer = new THREE.WebGLRenderer({ antialias: false, preserveDrawingBuffer: true, powerPreference: 'high-performance' });
renderer.setPixelRatio(1); renderer.setSize(W, H); renderer.toneMapping = THREE.NoToneMapping;
document.body.appendChild(renderer.domElement);

const depthTex = new THREE.DepthTexture(W, H); depthTex.type = THREE.UnsignedIntType;
const linear = { type: THREE.HalfFloatType, colorSpace: THREE.LinearSRGBColorSpace };
const rtScene = new THREE.WebGLRenderTarget(W, H, { ...linear, samples: 4, depthTexture: depthTex });
const rt = (w, h) => new THREE.WebGLRenderTarget(w, h, linear);
const rtDof = rt(W, H);
const rtB1 = rt(W >> 2, H >> 2), rtB1b = rt(W >> 2, H >> 2);   // bloom 1/4
const rtB2 = rt(W >> 3, H >> 3), rtB2b = rt(W >> 3, H >> 3);   // bloom 1/8

const quad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2)); quad.frustumCulled = false;
const qScene = new THREE.Scene(); qScene.add(quad);
const qCam = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
const pass = (mat, target) => { quad.material = mat; renderer.setRenderTarget(target); renderer.render(qScene, qCam); };
const VERT = 'varying vec2 vUv; void main(){ vUv = uv; gl_Position = vec4(position.xy, 0.0, 1.0); }';

camera = new THREE.PerspectiveCamera(30, W / H, 1, 500);   // near/far devem ser iguais a uNear/uFar no shader DOF

A cena é renderizada em um target HDR linear (half-float, 4x MSAA, textura de profundidade), para que telas e luzes possam ultrapassar 1.0. Em seguida, profundidade de campo, depois bloom em HDR e, por fim, uma passagem de composição que faz o tone mapping e a correção gama. Essa cadeia é a diferença entre "cinematográfico" e "demo WebGL".

Ordem de trabalho dentro de setT

  1. posicionar personagens
  2. scene.updateMatrixWorld(true)
  3. aterramento e anexos (objetos às mãos, assentos ao chão)
  4. updateMatrixWorld novamente
  5. atualizar âncoras (posições mundiais das cabeças, mãos, objetos, telas)
  6. posicionar câmera a partir da lista de planos
  7. atualizar legenda
  8. renderizar cena → DOF → poeira → bright-pass → desfoques → composição no canvas

Pós-processamento

Profundidade de campo. Gather com consciência de profundidade usando 64 taps de Vogel-disc. Amostras brilhantes ganham peso extra para que realces se transformem em discos bokeh. Um fundo borrado não pode vazar sobre um primeiro plano nítido.

uniform sampler2D tColor, tDepth;
uniform vec2 uRes;
uniform float uNear, uFar, uFocus, uAperture, uMaxCoc;
varying vec2 vUv;

float linDepth(float d) { float z = d * 2. - 1.; return 2. * uNear * uFar / (uFar + uNear - z * (uFar - uNear)); }
float coc(float z) { return min(uMaxCoc, uAperture * abs(1. / uFocus - 1. / z)); }

void main() {
  float z0 = linDepth(texture2D(tDepth, vUv).x);
  float c0 = coc(z0);
  float wsum = 1.0 / max(c0 * c0, 1.0);
  vec3 acc = texture2D(tColor, vUv).rgb * wsum;
  const int N = 64;
  for (int i = 0; i < N; i++) {
    float fi = float(i) + 0.5;
    float r = sqrt(fi / float(N));
    float a = fi * 2.39996323;
    vec2 off = vec2(cos(a), sin(a)) * r * uMaxCoc;
    float dist = length(off);
    vec2 uv = vUv + off / uRes;
    float zs = linDepth(texture2D(tDepth, uv).x);
    float cs = coc(zs);
    float eff = (zs > z0) ? min(cs, c0) : cs;            // fundo não pode cobrir primeiro plano nítido
    float w = smoothstep(dist - 1.0, dist + 1.0, eff) / max(eff * eff, 1.0);
    vec3 s = texture2D(tColor, uv).rgb;
    w *= 1.0 + 1.6 * clamp(dot(s, vec3(0.3, 0.5, 0.2)) - 0.8, 0.0, 6.0);   // boost bokeh
    acc += s * w; wsum += w;
  }
  gl_FragColor = vec4(acc / wsum, 1.0);
}
  • uFocus: distância da câmera até a âncora de foco, medida ao longo da direção da visualização, recalculada a cada quadro.
  • uAperture: ~1000–2500. Maior = mais raso. Close-ups alto, abertos baixo. Padrão moderado; desfoque pesado parece filtro do Instagram.
  • uMaxCoc: 22 px.

Bloom.

  • Passagem brilhante: `c * clamp((max(r,g,b

Habilidades Relacionadas