/screen-record — Vídeos roteirizados de demonstração de navegador
Quando usar
O usuário deseja um vídeo polido de um site realizando alguma ação — normalmente para LinkedIn, Twitter, uma apresentação de vendas, uma atualização para investidores ou documentação de onboarding. Eles descrevem o que deve aparecer na tela, o que o cursor faz e, aproximadamente, o que a narração deve dizer. Você cria o roteiro; o mecanismo cuida da animação do cursor, TTS via ElevenLabs, música, efeitos sonoros de clique e legendas embutidas.
Não use esta skill para: gravações de tela reais de uma sessão já em andamento (use Captura de Tela do macOS ou QuickTime), GIFs (use uma ferramenta de GIF) ou vídeos que precisam de uma narração humana real (você ainda pode criar o roteiro e depois eles regravam).
Como funciona
O mecanismo está em engine.mjs. Ele exporta runDemo(config) que:
- Gera um clipe TTS por dica com ElevenLabs.
- Gera uma faixa de música ambiente suave + um efeito sonoro de clique de UI com ffmpeg.
- Inicia o Chromium headless em 1920×1080, grava o vídeo.
- Executa sua função
scenes(helpers)que controla o navegador. - Mixa o áudio (narração alta + música baixa + cliques sutis) e queima legendas no estilo ASS.
- Retorna os caminhos para os arquivos finais
.mp4,.webm,.srt,.ass.
Fluxo de trabalho
Passo 1 — leia a solicitação do usuário
Defina: URL a ser gravada, fluxo (o que o cursor deve fazer), tom (formal/casual), duração alvo. Se algo estiver faltando, faça uma pergunta focada em vez de adivinhar.
Passo 2 — verifique a chave da API
Procure por ELEVENLABS_API_KEY nesta ordem:
$CLAUDE_PLUGIN_ROOT/.env.local(ou a pasta.env.localda própria skill)- O
.env.localdo projeto atual process.env
Se estiver ausente, informe ao usuário:
Preciso de uma chave de API do ElevenLabs. Obtenha uma gratuita em https://elevenlabs.io/app/settings/api-keys (10.000 caracteres/mês, suficiente para ~15 demonstrações). Cole no chat e salvarei no
.env.localda skill, ou adicione você mesmo.
Se a chave estiver presente, mas o mecanismo lançar ApiKeyDeadError (HTTP 401/402/403), informe ao usuário:
Sua chave do ElevenLabs foi rejeitada (revogada, expirada ou sem cota). Rotacione-a em https://elevenlabs.io/app/settings/api-keys e atualize o
.env.local. Não tentarei novamente até que você faça isso.
NÃO tente novamente silenciosamente, NÃO recorra ao comando say do macOS a menos que o usuário peça explicitamente.
Passo 3 — escolha uma voz
O padrão é Liam (TX3LPaxmHKxFdv7VOQHJ) — masculino americano enérgico, moderno. Mais leve que George (britânico) e Sarah (feminina americana). Outras vozes do plano gratuito estão em engine.mjs sob FREE_TIER_VOICES. Se o usuário disser "use a voz X", encontre-a no catálogo. Se disser "grave/profundo/leve/quente/feminino", escolha de acordo.
Vozes da biblioteca (Charlotte, Rachel, Adam, etc.) exigem uma assinatura paga do ElevenLabs. Se o usuário pedir uma e estiver no plano gratuito, você receberá HTTP 402 — recorra à voz gratuita mais próxima e avise o usuário.
Passo 4 — analise a página alvo (apenas se necessário)
Se você não conhece a estrutura da página, use Playwright ou curl para inspecionar o DOM e encontrar seletores. Não adivinhe — seletores importam, correspondências ambíguas farão a gravação expirar.
Para um SPA com conteúdo dinâmico, prefira aria-label, role ou seletores de texto em vez de nomes de classe. Use a semântica page.locator(s).first() (os helpers do mecanismo já fazem isso).
Passo 5 — escreva um arquivo de cena
Crie um arquivo em /tmp/screen-record-<nome>.mjs (não polua o repositório do usuário) que importe o mecanismo e chame runDemo. Modelo:
import { runDemo } from '/Users/<você>/Desktop/Projects/skill/screen-record-skills/engine.mjs';
// ou, se instalado via symlink:
// import { runDemo } from '/Users/<você>/.claude/skills/screen-record/engine.mjs';
await runDemo({
baseUrl: 'http://localhost:3000',
outputDir: './output/onboarding-demo',
outputName: 'onboarding',
splash: {
label: 'Visite-nos em',
url: 'www.yourapp.com',
footer: 'YourApp — fluxo de onboarding',
},
cues: [
{ id: 'intro', text: 'Bem-vindo ao YourApp. Deixe-me mostrar a você.' },
{ id: 'signup', text: 'Toque em Cadastre-se para criar uma conta.' },
{ id: 'form', text: 'Digite seu e-mail e uma senha.' },
{ id: 'done', text: 'E é isso — você está dentro.' },
],
// Opcional: simular backend para que a demonstração seja determinística.
routes: [
{ match: /\/api\/signup$/, fulfill: { status: 200, body: { ok: true } } },
],
// Opcional: pré-carregar um usuário autenticado para pular o login.
// initialAuth: { id: 'u1', email: '[email protected]', name: 'Usuário Demo' },
scenes: async ({ page, baseUrl, sleep, speak, awaitCue,
clickAt, typeInto, moveCursorTo, hideSplash, typeSplashUrl,
configSwitch, showSwitch, hideSwitch, setAuth }) => {
// Cena 0: tela de splash (o mecanismo já navegou para baseUrl fora da câmera).
speak('intro');
await typeSplashUrl('www.yourapp.com');
await awaitCue('intro');
await hideSplash();
await sleep(750); // corresponde ao fade do splash
// Cena 1: clicar em cadastre-se
speak('signup');
await clickAt('button:has-text("Cadastre-se")');
await awaitCue('signup');
// Cena 2: preencher o formulário
speak('form');
await typeInto('input[type="email"]', '[email protected]');
await typeInto('input[type="password"]', 'CorrectHorseBattery');
await awaitCue('form');
// Cena 3: enviar
speak('done');
await clickAt('button[type="submit"]');
await sleep(1000);
await awaitCue('done');
},
});
Passo 6 — execute
cd /Users/<você>/Desktop/Projects/skill/screen-record-skills
# Apenas na primeira vez:
npm install --silent
# Todas as vezes:
node /tmp/screen-record-<nome>.mjs
Cada execução leva aproximadamente o tempo do vídeo final mais ~10s de overhead.
Passo 7 — reporte
Informe ao usuário:
- Caminho para o
.mp4final (pronto para LinkedIn, 1920×1080, H.264 + AAC) - Caminho para o
.srt(o LinkedIn importará automaticamente como legendas ocultas) - Ofereça-se para reproduzir com
afplay <caminho>no macOS
Referência de helpers (o que suas cenas podem chamar)
| Helper | O que faz |
|---|---|
page | Página Playwright bruta, se necessário |
baseUrl | A URL base configurada |
sleep(ms) | await sleep(800) |
moveCursorTo(x, y, durationMs?) | Movimento suave para coordenadas |
moveToSelector(selector, durationMs?, offset?) | Movimento suave para o centro do elemento |
clickAt(selector, durationMs?) | Mover + pulsar + clicar (registra o tempo do clique para SFX) |
typeInto(selector, text, perChar?) | Clicar + limpar + digitar caractere por caractere |
recordClick() | Registrar manualmente um timestamp de clique (para SFX) |
speak(cueId) | Marcar início da dica. O fim é start + audioDuration + 0.35s |
awaitCue(cueId) | Dormir até que a narração da dica termine |
typeSplashUrl(text, perChar?) | Digitação animada na barra de URL do splash |
hideSplash() | Esmaecer o splash (sempre dormir ~750ms depois) |
configSwitch({ fromInitial, fromLabel, toInitial, toLabel, title, sub }) | Definir o conteúdo do cartão de troca de conta |
showSwitch() / hideSwitch() | Alternar a sobreposição de troca de conta |
setAuth(user) | Definir localStorage[authStorageKey] para um novo usuário (usado com showSwitch para demonstrações de troca de conta) |
Regras básicas de ritmo
- Uma frase por dica. Mantenha abaixo de ~12 palavras.
- Para ações de UI,
speak(cue)ANTES da ação para que a narração toque enquanto o cursor se move. - Para cenas com muita digitação, a digitação leva mais tempo que a narração — use
sleepem vez deawaitCuepara controlar o ritmo. - A duração total do vídeo é geralmente
sum(audioDurations) + sum(sleeps). Aponte para 30–75s no LinkedIn.
Seletores comuns que quebram
text=Foosozinho pode corresponder a muitos elementos. Usepage.locator(...).first()que é o que o mecanismo já faz, OU usebutton:has-text("Foo")para limitar a uma tag.- Nomes de classe do Tailwind são frágeis. Prefira
aria-label, role, tag semântica + texto. - Modais/diálogos: aguarde por
[role="dialog"]ou pelo texto do título do modal antes de clicar dentro.
O que NÃO fazer
- Não escreva texto de narração real no arquivo de cena — mantenha a narração em
cues[].textpara que legendas e áudio permaneçam sincronizados. - Não queime o cursor no CSS da página — o mecanismo o injeta.
- Não use
await page.screenshot()no meio da cena — você capturará o cursor em posições estranhas; useextractFramesposteriormente, se necessário. - Não faça commit do
.env.local(ele está em.gitignore).