Skill de Notas Ilustradas para Cursos em Vídeo Longo
Quando Usar
- O usuário forneceu uma URL de vídeo longo (>30 minutos) e deseja um resumo ilustrado, em vez de apenas assistir.
- O vídeo é uma transmissão ao vivo / curso online / palestra com conteúdo baseado em slides + explicação do instrutor (não vídeos curtos de entretenimento).
- O destino da saída é Documento Feishu (padrão) ou Markdown local.
- A plataforma de vídeo suporta o elemento HTML5
<video>(XiaoeTech, ShengeYouShu, Bilibili, NetEase Cloud Classroom, Douyin Education, etc.).
Quando Não Usar
- O vídeo usa Flash / Silverlight / player proprietário (sem elemento video HTML5).
- O vídeo tem menos de 15 minutos (assistir diretamente é mais rápido).
- O conteúdo do vídeo é puro entretenimento (música, Vlog, animação) – a captura de frames não tem valor.
- O usuário deseja preservar a transcrição literal da fala – este skill não faz ASR; é necessário usar Whisper ou Feishu Memo.
Princípio Central (Por que esta abordagem funciona)
Os fluxos de vídeo geralmente são HLS / MPEG-DASH, com autenticação por token – o download direto com ffmpeg resulta em erro 403. No entanto, o elemento video dentro do navegador já decodificou o fluxo em um buffer de quadros, e usar canvas.drawImage(video, ...) permite obter PNGs de qualquer ponto no tempo, contornando toda a cadeia de direitos autorais.
elemento video (após decodificação sem DRM) → canvas.drawImage → toDataURL → jpg local
Visão Geral do Processo
Passo 1: Avaliar acessibilidade do vídeo (login / se é HTML5 video)
Passo 2: Sondagem – obter duração / resolução / src
Passo 3: Amostragem grossa (um frame a cada 8-10 minutos)
Passo 4: IA analisa frames (PPT vs cabeça falante vs editor vs Q&A)
Passo 5: Amostragem complementar em áreas densas de PPT
Passo 6: Escrever artigo em Markdown
Passo 7: Enviar para o Feishu (criar pasta + criar documento + inserir imagens uma a uma)
Passo 1: Login / Verificação de Acessibilidade
Use o navegador Playwright MCP para abrir a URL do vídeo:
mcp__playwright__browser_navigate(url=URL_do_Video)
Verificar:
- Se redirecionar para página de login → veja "Estratégia de Login" abaixo.
- Se a página do curso aparecer diretamente → vá para o Passo 2.
Estratégia de Login (por ordem de prioridade)
A. Plataforma já logada (mais comum, sem ação necessária) — O navegador Playwright MCP mantém cookies dentro da mesma sessão; se o usuário acessou recentemente o mesmo domínio com Playwright e fez login, os cookies ainda estão lá. Tente navegar diretamente.
B. Usuário logado no próprio Chrome → importar cookies — Use cookie-import-browser do diretório ~/.claude/skills/gstack/browse:
cd ~/.claude/skills/gstack/browse
bun run src/cli.ts goto "https://target.com/" # Deve acessar o domínio alvo primeiro
bun run src/cli.ts cookie-import-browser chrome --domain target.com
⚠️ Causa comum de falha: os cookies no Chrome do usuário expiraram (verifique o campo expires_utc).
C. Login autônomo não é viável → Peça ao usuário — CAPTCHA, slider, QR code WeChat nunca podem ser automatizados. Peça educadamente ao usuário para fazer login:
- Dê uma instrução clara em 1 frase: "Abra X no Chrome, faça login e me avise".
- Não escreva mais de 2 parágrafos, nem dê 4 opções.
Nunca caia no loop: tentativa de login → cookie expirado → trocar ferramenta → ferramenta falha novamente → trocar novamente. Após no máximo 2 tentativas, rebaixe diretamente para o Plano B (o usuário envia printscreens manualmente).
Passo 2: Sondagem do Elemento de Vídeo
Após abrir a página do vídeo, execute:
() => {
const v = document.querySelector('video');
if (!v) return {error: 'no video element'};
return {
src: v.src || v.currentSrc,
duration: v.duration,
width: v.videoWidth,
height: v.videoHeight,
paused: v.paused,
readyState: v.readyState,
title: document.title,
url: location.href
};
}
Verificar:
duration > 0ereadyState >= 3→ podemos capturar frames.srcé uma URLblob:...→ está usando MSE, exatamente o que queremos.videoWidth >= 720→ qualidade de frame suficiente.readyState < 3→ vídeo não carregou; pode ser necessário clicar no botão de play primeiro para acionar o carregamento (pular o autoplay com muted é mais estável).
Passo 3: Pontos de Amostragem Grossa
Para um vídeo de N minutos, amostre ceil(N/8) frames (um frame a cada 8 minutos), cobrindo o ritmo geral:
async () => {
const v = document.querySelector('video');
v.pause(); v.muted = true;
const duration = v.duration;
const interval = 480; // 8 minutos
const targets = [];
for (let t = 30; t < duration - 30; t += interval) targets.push(Math.floor(t));
const seekTo = (t) => new Promise((resolve) => {
const onSeeked = () => { v.removeEventListener('seeked', onSeeked); resolve(); };
v.addEventListener('seeked', onSeeked);
v.currentTime = t;
setTimeout(() => { v.removeEventListener('seeked', onSeeked); resolve(); }, 8000);
});
const canvas = document.createElement('canvas');
canvas.width = v.videoWidth;
canvas.height = v.videoHeight;
const ctx = canvas.getContext('2d');
window.__frames = window.__frames || {};
const out = [];
for (const t of targets) {
await seekTo(t);
v.pause();
await new Promise(r => setTimeout(r, 800)); // Aguarda buffer de quadro
ctx.drawImage(v, 0, 0, canvas.width, canvas.height);
window.__frames[t] = canvas.toDataURL('image/jpeg', 0.8);
out.push({t: v.currentTime, target: t, kb: (window.__frames[t].length / 1024) | 0});
}
return out;
}
Detalhes importantes:
v.muted = true: evita a detecção de bloqueio de autoplay.v.pause()após cada seek (alguns players retomam automaticamente após o seek).- Espera de 800ms: o evento
seekednão garante que o quadro foi renderizado; é necessário dar um tempo para o buffer da GPU, caso contrário, o quadro do ponto de tempo anterior pode ser capturado. - Armazene tudo em
window.__framespara evitar retornar 30MB de base64 de uma vez.
Passo 4: Extrair Base64 e Gravar no Disco
Cada chamada evaluate retorna no máximo ~5MB de JSON. Processe em lotes:
() => {
const keys = Object.keys(window.__frames).sort((a,b) => +a - +b);
const out = {};
for (const k of keys.slice(START, START+4)) {
out[k] = window.__frames[k].split(',')[1]; // Remove data:image/jpeg;base64,
}
return out;
}
O parâmetro filename do mcp__playwright__browser_evaluate grava o JSON em ~/.playwright-mcp/ ou ~/. Não escreva diretamente em /tmp (restrições de sandbox).
Script de salvamento:
import json, base64, os
os.makedirs(os.path.expanduser("~/course-frames"), exist_ok=True)
for batch in glob("~/course-frames/batch*.json"):
d = json.load(open(batch))
for t, b64 in d.items():
secs = int(t)
fn = f"frame-{secs:04d}s-{secs//60:03d}m{secs%60:02d}s.jpg"
open(f"~/course-frames/{fn}", "wb").write(base64.b64decode(b64))
Passo 5: IA Classifica + Amostragem Complementar
Use a ferramenta Read para visualizar cada frame (as imagens são exibidas automaticamente). Classifique cada frame:
slide— Contém texto de PPT legível → Obrigatório capturar.editor— Cursor / VS Code / Terminal → Capturar (útil para demonstrações).talking_head— Apenas o rosto do instrutor → Pular.qa/chat— Interação de chat ao vivo → Selecionar 1-2 frames como representação de Q&A.
Amostragem complementar densa: Se entre 2 frames consecutivos (separados por 8 minutos) o conteúdo for completamente diferente (por exemplo, saltar de Princípio 01 para Princípio 05), significa que houve uma transição densa de PPTs; volte e amostre novamente:
// Amostragem complementar no intervalo [inicio, fim] a cada 1-2 minutos
const targets = [];
for (let t = START; t <= END; t += 90) targets.push(t);
Passo 6: Escrever Artigo em Markdown
Estrutura do artigo (ajuste conforme o tipo de curso):
# Título do Curso · Nota de Revisão Completa
> Informações do curso: Instrutor / Data / Duração / Plataforma / Link do vídeo original
## Para quem é este artigo
(1 parágrafo, descrevendo o perfil do leitor e o que será aprendido)
## Estrutura Geral do Curso
00:00 - 08:00 Introdução 08:00 - 25:00 Parte 1 ... ...
## Parte 1 · ...
> ⏱ Código de tempo: **16:00 – 25:00**
### Subseção 1
> ⏱ **16:00** · Slide 9/38

(Ponto chave + Frase de destaque + Lista de verificação)
...
## Próximos Passos Executáveis para Iniciantes
(Ações divididas por semana)
## Apêndice · Links de Recursos Chave
Regras de escrita:
- Cada placeholder de imagem deve usar um nome de arquivo único:
placeholder-frame-{segundos}s.jpgpara facilitar a localização posterior comselection-with-ellipsis. - Cada slide DEVE incluir o código de tempo exato (hh:mm ou mm:ss) + número do slide.
- O conteúdo do slide PPT deve ser copiado diretamente para a tabela, não resumido (o usuário quer ver o texto original).
- Conteúdos estruturados como exemplos negativos/positivos / listas de verificação devem usar tabelas ou listas Markdown.
- No final, DEVE haver uma seção "Próximos Passos Executáveis para Iniciantes", dividida por semanas; caso contrário, o artigo é apenas de leitura.
Passo 7: Enviar para o Feishu
7.1 Verificar Status de Autorização do lark-cli
lark-cli auth status
Na primeira execução deste skill, é necessário autorizar estes escopos (adicione um por vez, conforme necessário):
space:folder:create— Criar pastadocx:document:create— Criar documentodocx:document:write_only+docx:document:readonly— Modificar documento (necessário para inserir mídia)docs:document.media:upload— Fazer upload de imagens para o documento
Comando de autorização:
nohup lark-cli auth login --scope "<escopo>" > /tmp/lark-auth.log 2>&1 &
sleep 5
cat /tmp/lark-auth.log # Obter URL de device-flow
open "<URL>" # Peça ao usuário para autorizar no navegador
# Aguarde o usuário completar
until grep -q "授权成功" /tmp/lark-auth.log; do sleep 3; done
⚠️ Cada escopo deve ser adicionado individualmente – solicitar vários escopos de uma vez resulta em erro "escopo inválido".
7.2 Criar Pasta (padrão na raiz do espaço pessoal do usuário)
lark-cli drive +create-folder --name "<Nome da Pasta>"
# Retorna o folder_token; anote-o
7.3 Preparar Markdown Limpo (sem placeholders de imagens)
sed '/!\[.*\](placeholder-frame/d' article.md > article-clean.md
7.4 Criar Documento
cd ~/course-frames/article # O caminho do Markdown deve estar no diretório atual (limitação da CLI)
lark-cli docs +create \
--markdown @article-clean.md \
--folder-token "<folder_token>" \
--title "<Título do Documento>" \
> /tmp/doc-create.json 2>&1
# Extraia o doc_id
7.5 Inserir Imagens uma a uma
Cada imagem usa o título da seção ou texto único como âncora:
lark-cli docs +media-insert \
--doc "<doc_id>" \
--file "frame-XXXXs-MMMmSSs.jpg" \
--type image \
--selection-with-ellipsis "<texto único do título da seção>" \
--align center \
--caption "<Código de tempo · Descrição>"
Regras de âncora:
- Use os primeiros 15 caracteres do título da seção (suficiente para ser único).
- Várias imagens na mesma seção → use frases únicas de parágrafos diferentes.
- Se falhar, use o formato
start...end:--selection-with-ellipsis "Primeiro parágrafo...Último parágrafo"
7.6 Tratamento de Falhas
- Tempo limite do servidor → Tentar novamente 1 vez. A criação de documentos com mais de 16k caracteres pode exceder o tempo limite; divida o artigo em duas partes.
- Erro "Deve ser criado na pasta raiz" → O folder_token está incorreto ou não foi enviado.
- Placeholder de imagem ainda existente → O arquivo Markdown contém
placeholder-frame-XXXresidual. Primeiro, reescreva o documento com a versão limpa usando--mode overwritee depois insira as imagens.
Antipadrões (Evite estas Armadilhas)
❌ Antipadrão 1: Tentar baixar o vídeo diretamente com ffmpeg
HLS + token quase sempre resultam em erro 403. Mesmo se conseguir baixar, levaria 20 minutos para baixar o vídeo completo e mais 5 minutos para extrair frames com ffmpeg. Com video → canvas, capturamos 16 frames em 5 segundos.
❌ Antipadrão 2: Pedir ao Claude para assistir ao vídeo completo
Você não tem essa capacidade. Mesmo que tivesse (via API de vídeo multimodal), o custo seria 50 vezes maior que a captura de frames. Capturar frames + ler imagens + localizar por timestamp é a alavanca certa.
❌ Antipadrão 3: Chamar drawImage imediatamente após seek sem esperar o suficiente
O evento seeked não garante que o quadro foi renderizado. O buffer da GPU tem um atraso de 200-500ms. Aguarde pelo menos 800ms, 1 segundo é mais seguro. O sintoma é: "os dois primeiros frames estão OK, a partir do terceiro, todos são a mesma imagem."
❌ Antipadrão 4: Solicitar todos os escopos de uma vez
lark-cli auth login --scope "scope1 scope2 scope3" resulta em erro de escopo inválido. Um de cada vez.
❌ Antipadrão 5: Tentar usar o gstack browse para executar operações de vídeo e Feishu simultaneamente
O daemon do gstack browse é instável ao alternar sessões bash. Playwright MCP para captura de vídeo + lark-cli para operações no Feishu é a abordagem mais limpa.
❌ Antipadrão 6: Continuar tentando mesmo com cookie expirado
Verifique se expires_utc < hora atual → avise o usuário diretamente "Faça login novamente". Não tente 5 soluções diferentes.
Respostas para Perguntas Comuns de Usuários
"Por que você não me dá as legendas do vídeo diretamente?" Resposta: É possível, mas requer executar o Whisper separadamente (5-10 minutos adicionais). Se o usuário pedir explicitamente pela transcrição, execute; caso contrário, as notas ilustradas são o padrão.
"Por que não capturar mais frames?" Resposta: Cada frame tem ~200KB. A inserção em documentos Feishu tem limite de tamanho + causa fadiga visual. 12-18 frames é o ponto ideal para cobrir uma aula de 2 horas.
"Posso não usar o Feishu?"
Resposta: A saída padrão é ~/course-frames/article/article.md + arquivos *.jpg no mesmo diretório. O usuário pode enviar manualmente para qualquer lugar.
Especificações de Saída
Após cada execução bem-sucedida, a resposta final DEVE incluir:
- URL do Documento Feishu (clicável diretamente).
- Número total de frames capturados + duração total do vídeo.
- Mini sumário das seções (para o usuário visualizar).
- Uma frase de "Próximo passo sugerido" (ex.: "Comente aqui qual seção deve ser expandida").
Scripts Complementares
scripts/capture-frames.js — Função de sondagem + captura de frames para Playwright evaluate (cole dentro de mcp__playwright__browser_evaluate).
scripts/decode-batches.py — Script para converter base64 em lote para arquivos jpg.
scripts/insert-images.sh — Modelo para inserção em lote de imagens no Feishu via lark-cli.
Consulte um caso de execução completa de referência: examples/deepsea-2026-04-23.md
