read-video — Lire une vidéo en ne téléchargeant que les octets nécessaires
Quel problème cette compétence résout-elle ?
L'utilisateur envoie un lien vidéo et veut en comprendre le contenu. L'approche naïve consiste à télécharger la vidéo entière, puis à la traiter : lent, gourmand en stockage, risqué sur le plan des droits d'auteur, et souvent inutile (seules quelques secondes sont nécessaires). Cette compétence ne télécharge que le petit segment d'octets autour du point cible. La piste audio est transcrite localement en texte (principal moyen de répondre à « de quoi ça parle »), et les frames vidéo sont extraites pour être soumises à un modèle de vision.
Benchmark (249 MB / 9 min 56 s MP4, non faststart) :
| Action | Octets téléchargés | % du fichier total |
|---|---|---|
| Frame à 30 s | 2,0 MB | 0,839% |
| Frame à 8 min | 1,7 MB | 0,707% |
| Audio 480s + 20 s | 6,6 MB | 2,784% |
Quand l'utiliser / Quand ne pas l'utiliser
À utiliser : l'utilisateur fournit un lien direct vidéo ou un lien web contenant une vidéo (Bilibili, etc. sont automatiquement résolus par resolve.py, pas besoin d'appels API manuels).
À ne pas utiliser : pour les fichiers locaux (lire directement), ou pour les longues vidéos nécessitant une compréhension seconde par seconde (le coût reste élevé ; demander d'abord les points temporels à l'utilisateur).
Principes stricts
- Ne pas télécharger la vidéo entière. Aucune étape ne doit utiliser
wget/curl -Osur le fichier complet. - Toujours sonder avant d'agir. Ne pas chercher (seek) sans avoir vérifié le support du Range, sous peine d'attendre inutilement sur un serveur qui ne le supporte pas.
-ssdoit être placé avant-i. Placé après, c'est un seek côté sortie, ce qui équivaut à décoder séquentiellement jusqu'au point cible — autant télécharger la vidéo entière. Placé avant, c'est un seek côté entrée, le démultiplexeur saute directement.- Les mesures sont activées.
--measureest activé par défaut : il utilise un proxy de flux local pour compter les octets téléchargés, unique preuve que les données sont vraiment récupérées à la demande, et garantit qu'aucun téléchargement intégral n'a eu lieu. - Les liens web sont résolus par les scripts. URL de page ≠ lien direct, et les liens directs avec signature expirent. Utiliser
resolve.py, ou passer directement le lien àgrab.py/transcribe.py(ils résolvent automatiquement et ajoutent les en-têtes nécessaires). Ne pas écrire d'appels API spécifiques au site dans la conversation — ce code est toujours différent et jamais revu.
Flux de travail
Étape 0 : Déterminer le type d'entrée — lien web ou lien direct
- Lien web (Bilibili, YouTube, Douyin, Weibo, Vimeo… 1700+ sites) → d'abord résoudre en lien direct. Vous pouvez aussi passer le lien directement à
grab.py/transcribe.py; ils le reconnaissent, le résolvent et ajoutent automatiquement les en-têtes nécessaires. - Lien direct (extension média
.mp4/.m3u8…) → peut ignorer la résolution.
La règle de décision est dans resolvers.is_page_url() : toute URL avec une extension média est considérée comme lien direct ; les autres URLs HTTP sont envoyées au résolveur.
Première étape : Obtenir le flux accessible
python scripts/resolve.py "<lien web>"
Affiche video_url (pour les frames), audio_url (pour la transcription, différente de la précédente), headers (en-têtes requis par le CDN), plus le titre, la durée, et une série de points temporels suggérés pour l'extraction de frames.
La ligne Platform indique le backend utilisé : native (adaptateur dédié, actuellement Bilibili) ou yt-dlp (générique, 1700+ sites). Pour forcer l'utilisation : --backend native|yt-dlp|auto.
Commande complète en une ligne (résolution et extraction enchaînées automatiquement — recommandé) :
python scripts/transcribe.py --url "<lien web>" --seconds 90
python scripts/grab.py "<lien web>" --times 14 29 44 58 73 --prefix shot --outdir ./out
Deuxième étape : Sondage (recommandé pour les liens directs, optionnel pour les liens web)
python scripts/probe.py <lien direct> --json
Si le lien direct provient d'un CDN, inclure la source : --header 'Referer: https://www.bilibili.com'.
Vérifier trois champs :
seekable— sifalse, arrêter ; le serveur ignore Range, l'extraction de frame à un point précis dégénère en lecture séquentielle. Passer en mode « télécharger en décodant » (-i <url>sans-ss, prendre la frame la plus proche du début du flux) ou informer l'utilisateur que c'est impossible.container.moov_offset/faststart— si l'index est en tête (faststart), le coût fixe est de quelques dizaines de KB ; s'il est en queue, il faut d'abord télécharger la fin du fichier (environ 441 KB mesuré). C'est le coût incompressible pour extraire une frame.content_length— utilisé plus tard comme dénominateur pour calculer le pourcentage du fichier total téléchargé.
Troisième étape : Extraire les données selon l'intention de l'utilisateur
L'utilisateur donne un instant précis → extraction ponctuelle :
python scripts/grab.py <url> --time 183.5 --out shot.jpg
L'utilisateur ne donne pas d'instant précis → extraire d'abord quelques frames pour un aperçu :
python scripts/grab.py <url> --times 5 60 180 420 --prefix shot --outdir /tmp/rv
Astuce de sélection : prendre 4 à 6 frames selon la densité de contenu, ne pas les répartir uniformément sur toute la durée (cela reviendrait à décoder la vidéo entière). En cas de doute, prendre d'abord 3 frames, puis décider si un segment mérite un examen plus détaillé.
Pour comprendre le contenu parlé → transcrire directement en texte (recommandé en premier) :
python scripts/transcribe.py --url <url> --segments 0:90 320:90 630:97
Une seule commande pour « extraire l'audio → reconnaissance locale ». Résultat : texte chinois horodaté, sans API externe, sans inférence en ligne. La première exécution télécharge automatiquement le modèle SenseVoice (environ 240 Mo, depuis hf-mirror, une seule fois).
Pour les longues vidéos, prendre 3 à 4 segments de 90 secondes chacun suffit à couvrir la trame principale ; pas besoin de transcrire l'intégralité — c'est beaucoup plus rapide que d'analyser frame par frame, et mieux adapté aux questions comme « de quoi ça parle ». Transcrire d'abord, puis utiliser les frames pour compléter l'information visuelle — c'est l'ordre à suivre.
Vérifier le débit après la transcription. Un débit normal de parole est d'environ 4 à 6 caractères/seconde ; le script affiche chars_per_second. S'il est inférieur à 1 caractère/seconde, un avertissement est émis. Dans ce cas (musique pure, gameplay sans commentaire, démonstration silencieuse), ne pas considérer ce faible texte comme représentatif du contenu ; passer directement à une extraction plus dense (8 à 10 frames) pour une analyse visuelle — l'audio ne contient pas d'information, la réponse est uniquement dans l'image.
Pour obtenir uniquement le fichier audio (par exemple pour le transcrire ailleurs) :
python scripts/grab.py <url> --start 0 --seconds 60 --audio clip.wav
S'il y a une piste de sous-titres intégrée, la lire en priorité (ffmpeg -i <url> pour vérifier la présence d'un flux subtitle) — plus précise et plus rapide que l'ASR.
Quatrième étape : Soumettre les frames au modèle de vision
grab.py ne fait que récupérer les pixels localement ; il ne comprend pas le contenu. Passer le chemin des fichiers jpg produits au modèle de vision pour poser des questions (« que contient cette frame ? », « de quoi parle ce passage ? »). Pour plusieurs frames, les donner dans l'ordre temporel afin que le modèle puisse construire un récit.
Comment lire la sortie
grab.py produit un JSON avec, pour chaque action, un bilan des octets :
{
"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_lengthcorrespond au pourcentage du fichier téléchargé. S'il dépasse 5%, il faut vérifier : souvent,-ssest mal positionné ou le serveur ne reconnaît pas Range.- Un nombre élevé de
requestsest normal : ffmpeg doit effectuer plusieurs requêtes (moov, keyframe jusqu'à la frame cible, en-tête audio). - Si
ok: false, regardererr_tail; les causes fréquentes sont les timeouts et les erreurs 5XX — le script intègre déjà des tentatives automatiques ; en cas d'échec persistant, essayer un autre point temporel.
Sortie de transcribe.py :
{
"ok": true,
"text": "Il s'agit d'une terre entourée de grands fleuves et de montagnes…",
"segments": [{"start": 0, "end": 25, "text": "…"}],
"audio_seconds": 276.7,
"model": "SenseVoiceSmall-int8"
}
text est la concaténation de tous les segments ; segments est horodaté, utile pour une question comme « qu'a-t-il dit à la seconde X ? ».
Environnement
- Interpréteur : Python dans un venv local. Sous Windows, le chemin ressemble à
~/.workbuddy/binaries/python/envs/default/Scripts/python.exe. Notez bienScripts/(et nonbin/comme sous Unix — écrirebin/pythonproduira une erreur de fichier introuvable). - Dépendances :
httpx,imageio-ffmpeg(extraction),sherpa-onnx,numpy(transcription),yt-dlp(résolution générique). Tout est installé dans le même venv ; si une dépendance manque, le script l'indiquera clairement. Sans yt-dlp, Bilibili reste utilisable (via l'adaptateur dédié), mais les autres sites échoueront. - ffmpeg : fourni par la construction statique de
imageio-ffmpeg, pas besoin d'installation système. Pour en utiliser un autre, définir la variable d'environnementRV_FFMPEGou utiliser--ffmpeg. - Cache du modèle ASR :
~/.workbuddy/cache/read-video/models(environ 240 Mo, téléchargé automatiquement à la première exécution). Si le fichier existe et que sa taille correspond, le téléchargement est sauté.
Limitations connues
- Les CDN vérifient souvent l'origine : les CDN vidéo de Bilibili et autres exigent un
Referer, sans quoi ils répondent par un 403. L'utilisation deresolve.pyou de--pageajoute automatiquement l'en-tête ; pour les liens directs passés manuellement, penser à ajouter--header 'Referer: https://www.bilibili.com'(les trois scripts le supportent, répétable). - Les liens directs expirent : ils contiennent une signature et une date limite ; à utiliser dans la même session. En cas d'erreur 403, relancer
resolve.py. - Couverture des sites = couverture de yt-dlp (1700+ sites), pas une liste gérée indépendamment. Pour un site qui ne se résout pas, utiliser d'abord
--backend yt-dlppour voir l'erreur brute ; les sites nécessitant connexion/Cookie échoueront — soit les abandonner, soit fournir manuellement le Cookie. yt-dlp doit être mis à jour régulièrement (les sites changent, les anciennes versions échouent) —pip install -U yt-dlp. - « Résolu » ne signifie pas « accessible » : la couche de résolution ne fournit qu'une URL. En cas d'échec d'extraction, déterminer d'abord s'il s'agit d'un échec de résolution ou d'un problème réseau — testé : la machine n'atteint pas YouTube/Vimeo (proxy 502), mais Douyin/Weibo/Xigua sont accessibles.
- HLS / m3u8 : n'est pas un fichier unique ;
probe.pyéchouera à analyser le conteneur (champerror).grab.pypeut encore fonctionner, ffmpeg téléchargeant les segments nécessaires ;content_lengthsera vide, le pourcentage ne peut pas être calculé. - Séparation audio/vidéo DASH : de nombreux sites (Bilibili, etc.) fournissent des flux vidéo et audio séparés. Utiliser le flux vidéo pour les frames, le flux audio pour la transcription — les mélanger donnera un résultat vide silencieux.
- Précision de
-sslimitée par le GOP : pour obtenir une frame à une seconde donnée, il faut décoder à partir de la keyframe la plus proche. L'unité minimale de données téléchargées est donc l'intervalle entre la keyframe et la frame cible, pas un intervalle d'un octet. - Homophones de l'ASR : SenseVoiceSmall fonctionne bien pour le chinois, mais les noms propres, noms de lieux et termes anciens sont souvent mal retranscrits (par exemple « 阪泉之野 » → « 版圈之也 », « 涿鹿 » → « 涿陆 »). Corriger la transcription avant de citer un nom propre ; ne pas prendre la sortie brute comme un fait.
- Dépendances :
httpx+imageio-ffmpeg(extraction) +sherpa-onnx+numpy(transcription). Si une dépendance manque, le script l'indiquera clairement et suggérera l'installation.
Pour plus de détails sur les principes, les pièges et les mesures, voir references/methodology.md. La relation avec le plugin dsh dsh-readvideo est décrite dans references/dsh-dual-form.md.