Communitygithub.com

RaganaBloodedge/read-video-skill

Lee el contenido de vídeos remotos sin descargarlos por completo. Soporta más de 1700 sitios de vídeo (Bilibili con adaptador propio, el resto con yt-dlp) y enlaces directos; extrae fotogramas o fragmentos de audio mediante búsqueda precisa con ffmpeg, transcripción offline con SenseVoice y reconocimiento visual con modelos.

¿Qué es read-video-skill?

Esta habilidad permite a un agente de IA (como Claude Code, Cursor o Codex) leer el contenido de vídeos remotos sin descargar el archivo completo. Es útil cuando un usuario proporciona un enlace de vídeo y pide entenderlo, extraer un fotograma específico, transcribir el audio o verificar si una URL soporta acceso por rangos. Soporta más de 1700 sitios (Bilibili con adaptador nativo, el resto con yt-dlp) y enlaces directos. El flujo de trabajo consta de cuatro pasos: 1) Resolver la URL (si es de una página) para obtener enlaces directos con firma y cabeceras, 2) Detectar si el servidor soporta HTTP Range y la posición del índice MP4, 3) Extraer fotogramas o audio usando ffmpeg con `-ss` antes de `-i` para búsqueda eficiente, 4) Transcribir el audio offline con SenseVoice (sin API externa) o pasar los fotogramas a un modelo de visión. La habilidad mide los bytes descargados del servidor para garantizar que no se baja el vídeo completo. Es ideal para preguntas como "¿qué dice este vídeo?", "¿qué hay en el minuto 5?" o para verificar compatibilidad de URLs.

Compatible con~Claude Code~Codex CLI~Cursor
npx skills add RaganaBloodedge/read-video-skill

Preguntar en tu IA favorita

Abre un nuevo chat con esta habilidad de agente ya precargada.

Documentación

read-video —— Lee vídeos "por bytes bajo demanda"

¿Qué problema resuelve esta habilidad?

El usuario envía un enlace de vídeo y quiere entender su contenido. El enfoque ingenuo es descargar el vídeo completo para procesarlo: es lento, ocupa almacenamiento, tiene riesgos de derechos de autor y, a menudo, solo se necesitan unos segundos. Esta habilidad hace lo siguiente: toma solo los bytes cercanos al momento deseado. La pista de audio se transcribe localmente sin conexión (principal para responder "¿de qué trata?"), y la pista de vídeo se decodifica en fotogramas clave que se pasan a un modelo de visión para completar la información visual.

Benchmark real (MP4 de 249 MB / 9 min 56 s, sin faststart):

AcciónBytes del servidor% del total
Tomar fotograma en segundo 302.0 MB0.839%
Tomar fotograma en minuto 81.7 MB0.707%
Tomar 20 segundos de audio desde 480 s6.6 MB2.784%

Cuándo usar / Cuándo no usar

Usar: El usuario proporciona un enlace directo de vídeo o un enlace de página que contiene vídeo (plataformas como Bilibili son resueltas automáticamente por resolve.py, sin necesidad de llamadas API manuales). No usar: Archivos locales (léelos directamente), vídeos largos que requieran comprensión completa segundo a segundo (el coste sigue siendo alto; pide primero al usuario los puntos de tiempo).

Principios estrictos

  1. No descargar el vídeo completo. En ningún paso uses wget/curl -O para el archivo completo.
  2. Primero detectar, luego actuar. No hacer seek sin detectar puede provocar una espera inútil si el servidor no soporta Range.
  3. -ss debe ir antes de -i. Si va después, es seek del lado de salida, equivalente a decodificar secuencialmente hasta el punto de destino, degradándose a traer todo el archivo. Delante es seek del lado de entrada, el demuxer salta directamente.
  4. Medición activa. --measure activo por defecto: usa un proxy local de streaming para contar los bytes del servidor, es la única evidencia sólida de que realmente se tomaron solo los bytes necesarios y prueba para el usuario de que no se descargó el vídeo completo.
  5. Los enlaces de página se pasan a los scripts para resolver. Una URL de página no es un enlace directo, y los enlaces directos tienen firmas que caducan. Usa resolve.py, o pasa directamente el enlace a grab.py/transcribe.py (ellos lo resuelven automáticamente y añaden las cabeceras necesarias). No escribas llamadas API del sitio en la conversación: ese código siempre es diferente y no ha sido revisado.

Flujo de trabajo

Paso 0: Distinguir entrada — enlace de página o enlace directo

  • Enlace de página (Bilibili, YouTube, TikTok, Weibo, Vimeo… más de 1700 sitios) → primero resolver a enlace directo. También puedes pasar el enlace directamente a grab.py / transcribe.py, que lo reconocen y lo resuelven automáticamente añadiendo las cabeceras necesarias.
  • Enlace directo (extensión .mp4 / .m3u8 u otra de medio) → se puede saltar la resolución.

La regla de decisión está en resolvers.is_page_url(): cualquier URL con extensión de medio se trata como enlace directo; el resto se envía a la capa de resolución.

Primer paso: Obtener un flujo accesible

python scripts/resolve.py "<enlace_de_página>"

Devuelve video_url (para fotogramas), audio_url (para transcripción, diferente del anterior), headers (cabeceras requeridas por la CDN), además del título, duración y un conjunto de puntos de tiempo sugeridos para extraer fotogramas.

La línea Plataforma indica el backend utilizado: native (adaptador propio, actualmente Bilibili) o yt-dlp (genérico, más de 1700 sitios). Para forzar, usa --backend native|yt-dlp|auto.

Comando completo en un solo paso (resolución y extracción enlazadas automáticamente, recomendado):

python scripts/transcribe.py --url "<enlace_de_página>" --seconds 90
python scripts/grab.py "<enlace_de_página>" --times 14 29 44 58 73 --prefix shot --outdir ./out

Segundo paso: Detectar (recomendado para enlaces directos, opcional para enlaces de página)

python scripts/probe.py <enlace_directo> --json

Si el enlace directo proviene de una CDN, incluye la fuente: --header 'Referer: https://www.bilibili.com'.

Observa tres campos:

  • seekable — si es false, detente. El servidor ignora Range, y la extracción puntual de fotogramas degenera en una descarga secuencial. Cambia a "descargar y decodificar" (-i <url> sin -ss, toma el fotograma más temprano del flujo) o informa al usuario de que no es posible.
  • container.moov_offset / faststart — si el índice está al inicio (faststart), el coste fijo es de decenas de KB; si está al final, primero hay que tomar un segmento del final del archivo (medido ~441 KB). Es el coste fijo de "tomar un fotograma", inevitable.
  • content_length — denominador para calcular el porcentaje del total.

Tercer paso: Extraer según la intención del usuario

El usuario da un punto de tiempo específico → extracción puntual:

python scripts/grab.py <url> --time 183.5 --out shot.jpg

El usuario no da punto de tiempo → extraer algunos fotogramas para tener una idea general:

python scripts/grab.py <url> --times 5 60 180 420 --prefix shot --outdir /tmp/rv

Consejo para seleccionar puntos: toma de 4 a 6 fotogramas según la densidad de contenido, no los distribuyas uniformemente (eso equivale a decodificar todo el vídeo). Si no estás seguro, toma 3 fotogramas primero, y luego decide si necesitas ver una sección en detalle.

Para entender de qué trata → transcribe primero (recomendado):

python scripts/transcribe.py --url <url> --segments 0:90 320:90 630:97

Un solo comando que "corta audio → reconoce localmente", devuelve texto con marcas de tiempo, sin necesidad de API externa ni inferencia en la nube. La primera ejecución descarga automáticamente el modelo SenseVoice (~240 MB, desde hf-mirror, solo una vez).

Para vídeos largos, toma de 3 a 4 segmentos de 90 segundos cada uno para cubrir la línea principal, no transcribas todo el vídeo — es mucho más rápido que analizar fotograma por fotograma y más adecuado para responder "¿de qué trata?". Primero transcribe, luego usa fotogramas para complementar la información visual — ese es el orden.

Después de transcribir, revisa la tasa de producción. En locución normal, aproximadamente 4-6 caracteres/segundo; la salida del script incluye chars_per_second. Si es inferior a 1 carácter/segundo, el script avisará activamente. En ese caso (música pura, gameplay, demostración silenciosa) no tomes ese texto como contenido, sino cambia a una extracción más densa de fotogramas (8-10) para análisis visual — el audio no tiene información, la respuesta solo está en las imágenes.

Si solo necesitas el archivo de audio (por ejemplo, para enviarlo a otro lugar para transcripción):

python scripts/grab.py <url> --start 0 --seconds 60 --audio clip.wav

Cuando haya pistas de subtítulos incrustadas, prioriza leer los subtítulos (ffmpeg -i <url> para ver si hay flujo subtitle), son más precisos y rápidos que el ASR.

Cuarto paso: Pasar los fotogramas al modelo de visión

grab.py solo obtiene los píxeles localmente, no entiende el contenido. Pasa las rutas de los jpg generados al modelo de visión para hacer preguntas ("¿qué hay en este fotograma?", "¿de qué trata esta sección?"). Si son varios fotogramas, entrégalos en orden temporal para que el modelo los ensarte en una narrativa.

Cómo leer la salida

Salida JSON de grab.py, cada acción con un registro de bytes:

{
  "ok": true,
  "actions": [{
    "kind": "frame", "time": 30,
    "out_bytes": 48211,
    "upstream_bytes": 2097152,
    "requests": 4,
    "wall_seconds": 2.317
  }],
  "total_upstream_bytes": 2097152
}
  • upstream_bytes / content_length es el porcentaje del total. Si supera el 5%, detente y revisa: probablemente la posición de -ss está mal, o el servidor no reconoce Range.
  • requests alto es normal: ffmpeg necesita varias solicitudes (moov, desde el fotograma clave hasta el objetivo, cabecera de audio).
  • ok: false mira err_tail; lo común es timeout y 5XX — el script ya tiene reintentos integrados; si aún falla, prueba con otro punto de tiempo.

Salida de transcribe.py:

{
  "ok": true,
  "text": "Esta es una tierra rodeada de grandes ríos y montañas...",
  "segments": [{"start": 0, "end": 25, "text": "..."}],
  "audio_seconds": 276.7,
  "model": "SenseVoiceSmall-int8"
}

text es la concatenación de todos los segmentos; segments contiene marcas de tiempo, útil cuando se necesita saber "¿qué se dijo en el segundo X?".

Entorno

  • Intérprete: Python del venv alojado localmente. En Windows, la ruta es similar a ~/.workbuddy/binaries/python/envs/default/Scripts/python.exe. Observa que es Scripts/, no el estilo Unix bin/ (escribir bin/python como en la documentación dará error de "archivo no encontrado").
  • Dependencias: httpx, imageio-ffmpeg (para extracción), sherpa-onnx, numpy (para transcripción), yt-dlp (resolución de plataformas genéricas); todo instalado en el mismo venv. Si falta alguna, el script lo indicará claramente. Sin yt-dlp, Bilibili sigue funcionando (con adaptador propio), pero otros sitios fallarán en la resolución.
  • ffmpeg: Viene con la construcción estática de imageio-ffmpeg, no requiere instalación en el sistema. Para especificar otro, usa la variable de entorno RV_FFMPEG o --ffmpeg.
  • Caché del modelo ASR: ~/.workbuddy/cache/read-video/models (~240 MB, descarga automática en la primera ejecución). Si el archivo existe y el número de bytes coincide, se salta la descarga.

Límites conocidos

  • La CDN a menudo verifica el origen: La CDN de vídeos de sitios como Bilibili requiere Referer; sin él, da 403. Usar resolve.py o --page lo añade automáticamente; al pasar enlaces directos manualmente, recuerda --header 'Referer: https://www.bilibili.com' (los tres scripts lo soportan, se puede repetir).
  • Los enlaces directos caducan: Los enlaces directos de los sitios llevan firma y fecha de caducidad; úsalos dentro de la misma tanda de operaciones. Si aparece un 403, vuelve a ejecutar resolve.py.
  • Cobertura de plataformas = cobertura de yt-dlp (más de 1700 sitios), no mantenida por separado. Si un sitio no se resuelve, primero usa --backend yt-dlp para ver el error original; los sitios que requieren inicio de sesión / Cookie fallarán; en ese caso, abandona o proporciona las cookies manualmente. yt-dlp necesita actualizarse periódicamente (las versiones antiguas fallan cuando los sitios cambian): pip install -U yt-dlp.
  • "Resuelve" no significa "se obtiene": La capa de resolución solo proporciona la URL. Si la extracción falla, primero distingue si es un fallo de resolución o de red: en pruebas, la conexión local a YouTube / Vimeo falló (proxy 502), mientras que TikTok / Weibo / Xigua Video eran accesibles.
  • HLS / m3u8: No es un solo archivo, el análisis de caja de probe.py fallará (tendrá un campo error). En ese caso, grab.py sigue funcionando; ffmpeg extraerá los segmentos necesarios por sí mismo; content_length estará vacío, por lo que no se puede calcular el porcentaje.
  • DASH con audio y vídeo separados: Muchos sitios (incluido Bilibili) tienen flujos de vídeo y audio como dos URL independientes. Usa el flujo de vídeo para fotogramas y el de audio para transcripción; si los usas al revés, obtendrás silenciosamente un resultado vacío.
  • La precisión de -ss está limitada por el GOP: Para tomar un fotograma en un segundo específico, debes decodificar desde el fotograma clave más cercano, por lo que la unidad mínima de datos es "desde el fotograma clave hasta el fotograma objetivo", no un rango de un solo byte.
  • Homófonos en ASR: SenseVoiceSmall funciona bien con chino, pero los nombres de personas, lugares y palabras antiguas suelen tener errores de homófonos (medido: "阪泉之野" → "版圈之也", "涿鹿" → "涿陆"). Corrige según el contexto antes de citar nombres propios, no tomes el texto transcrito como un hecho.
  • Dependencias: httpx + imageio-ffmpeg (extracción) + sherpa-onnx + numpy (transcripción). Si falta alguna, el script dará un error claro indicando cuál instalar.

Para más detalles sobre el principio, problemas encontrados y datos de prueba, consulta references/methodology.md. La relación entre esta habilidad y el plugin dsh dsh-readvideo se describe en references/dsh-dual-form.md.

Skills relacionados