Communitygithub.com

eternova/long-video-course-notes

Skill para Claude Code: converte automaticamente cursos em vídeo longo em notas ilustradas no documento Feishu. Captura de frames via canvas + reconhecimento por IA + envio pelo lark-cli.

O que é long-video-course-notes?

Este skill automatiza a criação de resumos ilustrados a partir de vídeos longos (acima de 30 minutos) de cursos online, palestras e transmissões ao vivo. Diferente de abordagens que tentam baixar o vídeo, ele utiliza o elemento HTML5 <video> do navegador para capturar frames, contornando sistemas de DRM e autenticação. A IA analisa os frames capturados para classificar entre slides, cabeças falantes e outros tipos de conteúdo, e insere amostras adicionais em áreas densas de conteúdo. O resultado final é um documento formatado no Feishu (ou Markdown local) com imagens, carimbos de tempo, e um resumo estruturado. A ferramenta suporta o lark-cli para criação de pastas e documentos, e inserção de imagens diretamente, oferecendo uma solução completa e eficiente para transformar horas de vídeo em notas de estudo acionáveis.

Funciona com✓Claude Code~Codex CLI✓Cursor
npx skills add eternova/long-video-course-notes

Perguntar na sua IA favorita

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

Prévia

Do README da skill

placeholder-frame-XXXXs.jpg

Documentação

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 > 0 e readyState >= 3 → podemos capturar frames.
  • src é uma URL blob:... → 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 seeked nã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.__frames para 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
![placeholder-frame-XXXXs.jpg](placeholder-frame-XXXXs.jpg)
(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.jpg para facilitar a localização posterior com selection-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 pasta
  • docx:document:create — Criar documento
  • docx: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-XXX residual. Primeiro, reescreva o documento com a versão limpa usando --mode overwrite e 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:

  1. URL do Documento Feishu (clicável diretamente).
  2. Número total de frames capturados + duração total do vídeo.
  3. Mini sumário das seções (para o usuário visualizar).
  4. 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

Habilidades Relacionadas