read-video —— 바이트 단위로 비디오를 읽는다
이 스킬은 어떤 문제를 해결하나
사용자가 비디오 링크를 주면서 내용을 이해하고 싶어 합니다. 간단한 방법은 비디오를 다운로드한 후 처리하는 것입니다. 하지만 이는 느리고, 저장 공간을 많이 차지하며, 저작권 위험이 있고, 종종 몇 초만 필요합니다. 이 스킬의 방식은 타겟 시간 근처의 작은 바이트 조각만 가져오는 것입니다. 오디오 트랙은 로컬에서 오프라인으로 텍스트로 변환되고( "무엇을 말하는지"에 대한 주요 방법), 비디오 트랙은 키 프레임을 추출하여 시각 모델에 전달해 화면을 보완합니다.
실측 기준 (249 MB / 9분 56초 MP4, faststart 아님):
| 동작 | 업스트림 바이트 | 전체 대비 비율 |
|---|---|---|
| 30초 프레임 캡처 | 2.0 MB | 0.839% |
| 8분 프레임 캡처 | 1.7 MB | 0.707% |
| 480초부터 20초 오디오 | 6.6 MB | 2.784% |
언제 사용 / 언제 사용하지 않을까
사용: 사용자가 비디오 직접 링크를 주거나, 비디오가 포함된 웹 페이지 링크를 준 경우 (bilibili 등 플랫폼은 resolve.py가 자동으로 파싱하며, 수동 API 호출이 필요 없습니다).
사용하지 않음: 로컬 파일 (직접 읽기), 초 단위로 완전히 이해해야 하는 긴 비디오 (비용이 여전히 높으므로 먼저 사용자에게 시간대를 물어보세요).
엄격한 원칙
- 전체 파일을 다운로드하지 않습니다. 어떤 단계에서도
wget/curl -O를 사용하여 전체 파일을 가져오지 마세요. - 먼저 탐색한 후 실행합니다. 탐색하지 않고 seek하면 Range를 지원하지 않는 서버에서 시간을 낭비할 수 있습니다.
-ss는 반드시-i앞에 위치해야 합니다. 뒤에 위치하면 출력 측 seek이 되어, 순차적으로 디코딩한 후에야 타겟 지점에 도달하게 되어 전체 파일을 가져오는 것과 같아집니다. 앞에 위치하면 입력 측 seek이 되어 디먹서가 직접 점프합니다.- 측정을 켜 둡니다.
--measure는 기본적으로 켜져 있습니다. 로컬 스트리밍 프록시를 통해 업스트림 바이트를 측정하며, 이것이 "실제로 요청 시 바이트를 가져왔다"는 유일한 증거이자 사용자에게 전체 파일을 몰래 다운로드하지 않았다는 증거입니다. - 웹 링크는 스크립트에 맡겨 파싱합니다. 페이지 URL ≠ 직접 링크이며, 직접 링크에는 서명이 있어 만료됩니다.
resolve.py를 사용하거나, 링크를 직접grab.py/transcribe.py에 전달하세요 (자동으로 파싱하고 요청 헤더를 추가합니다). 세션에서 직접 사이트 API 호출을 작성하지 마세요. 그런 코드는 매번 모양이 다르고 검토된 적도 없습니다.
작업 흐름
0단계: 입력 구분 —— 웹 링크인지 직접 링크인지
- 웹 링크 (bilibili, YouTube, TikTok, Weibo, Vimeo … 총 1700+ 사이트) → 먼저 직접 링크로 파싱합니다. 링크를
grab.py/transcribe.py에 직접 전달해도 됩니다. 자동으로 인식하고 파싱하며 필요한 요청 헤더를 추가합니다. - 직접 링크 (
.mp4/.m3u8등 미디어 확장자) → 파싱을 건너뛸 수 있습니다.
판단 규칙은 resolvers.is_page_url()에 있습니다. 미디어 확장자가 있으면 직접 링크로 간주하고, 나머지 http 링크는 파싱 계층으로 보냅니다.
1단계: 접근 가능한 스트림 얻기
python scripts/resolve.py "<웹 링크>"
출력: video_url (프레임 캡처용), audio_url (전사용, 전자와 다름), headers (CDN 요구 요청 헤더), 제목, 길이, 그리고 권장 프레임 캡처 시간대.
플랫폼 줄은 사용된 백엔드를 표시합니다: native (수제 어댑터, 현재는 bilibili) 또는 yt-dlp (범용, 1700+ 사이트). --backend native|yt-dlp|auto로 강제 지정할 수 있습니다.
전체 과정을 한 번에 실행 (파싱과 데이터 가져오기가 자동으로 연결됨, 권장):
python scripts/transcribe.py --url "<웹 링크>" --seconds 90
python scripts/grab.py "<웹 링크>" --times 14 29 44 58 73 --prefix shot --outdir ./out
2단계: 탐색 (직접 링크는 권장, 웹 링크는 건너뛸 수 있음)
python scripts/probe.py <직접 링크> --json
직접 링크가 CDN에서 온 경우 출처를 포함해야 합니다: --header 'Referer: https://www.bilibili.com'.
세 가지 필드를 확인:
seekable——false이면 중단합니다. 서버가 Range를 무시하면 프레임 캡처가 순차적 스트리밍으로 퇴화됩니다. "다운로드하면서 디코딩"(-i <url>에-ss없음, 스트림 앞부분의 프레임 가져오기)으로 전환하거나 사용자에게 불가능하다고 알립니다.container.moov_offset/faststart—— 인덱스가 헤더에 있으면 (faststart) 고정 오버헤드는 수십 KB입니다. 테일에 있으면 먼저 파일 끝 부분을 가져와야 합니다 (실측 약 441 KB). 이것이 "한 프레임을 가져오는" 고정 비용이며, 피할 수 없습니다.content_length—— 나중에 "전체 대비 비율"의 분모로 사용됩니다.
3단계: 사용자 의도에 따라 데이터 가져오기
사용자가 특정 시간대를 지정한 경우 → 정확한 증거 캡처:
python scripts/grab.py <url> --time 183.5 --out shot.jpg
사용자가 시간대를 지정하지 않은 경우 → 먼저 몇 프레임을 캡처하여 개요 파악:
python scripts/grab.py <url> --times 5 60 180 420 --prefix shot --outdir /tmp/rv
선택 포인트 팁: 콘텐츠 밀도에 따라 4~6 프레임을 캡처하고, 전체를 균일하게 덮지 마세요 (전체를 디코딩하는 것과 같습니다). 확실하지 않으면 먼저 3 프레임을 캡처하고, 확인 후 특정 부분을 자세히 볼지 결정하세요.
내용 이해에 초점을 맞출 경우 → 먼저 텍스트로 변환 (권장):
python scripts/transcribe.py --url <url> --segments 0:90 320:90 630:97
한 명령으로 "오디오 캡처 → 로컬 인식"을 완료하고 타임스탬프가 포함된 중국어 텍스트를 출력합니다. 외부 API나 네트워크 추론이 필요 없습니다. 첫 실행 시 SenseVoice 모델 (약 240 MB, hf-mirror에서 다운로드, 한 번만)을 자동으로 다운로드합니다.
긴 비디오의 경우 3~4개의 세그먼트를 각각 약 90초씩 캡처하면 주요 내용을 다룰 수 있으며, 전체를 전사할 필요가 없습니다. 이는 프레임별로 보는 것보다 훨씬 빠르며, "무엇을 말하는지"와 같은 질문에 더 적합합니다. 먼저 텍스트로 변환한 후 프레임으로 시각 정보를 보완합니다. 이 순서를 따르세요.
전사 후 생산성을 확인합니다. 일반적인 대화는 약 46자/초입니다. 스크립트 출력에 10 프레임)로 전환하여 화면 분석을 수행하세요. 소리에 정보가 없으면 답은 화면에만 있습니다.chars_per_second가 포함되어 있으며, 1자/초 미만이면 자동으로 알림을 표시합니다. 이런 경우 (순수 음악, 게임 플레이, 무음 데모) 적은 양의 텍스트를 내용으로 간주하지 말고, 더 조밀한 프레임 캡처 (8
오디오 파일 자체만 필요한 경우 (예: 다른 곳에 전사 의뢰):
python scripts/grab.py <url> --start 0 --seconds 60 --audio clip.wav
내장 자막 트랙이 있으면 자막을 우선 읽습니다 (ffmpeg -i <url>로 subtitle 스트림 확인). ASR보다 더 정확하고 빠릅니다.
4단계: 프레임을 시각 모델에 전달
grab.py는 픽셀을 로컬로 가져오기만 하고 내용을 이해하지 않습니다. 출력된 jpg 경로를 시각 모델에 전달하여 질문하세요 ("이 프레임에 무엇이 있나요?", "이 부분이 무엇을 말하나요?"). 여러 프레임의 경우 시간 순서대로 함께 전달하여 모델이 서술로 연결하도록 합니다.
출력 읽는 방법
grab.py는 JSON을 출력하며, 각 동작에 대한 바이트 계정을 포함합니다:
{
"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가 바로 "전체 대비 비율"입니다. 5%를 초과하면 중단하고 재검토하세요. 대부분-ss위치가 잘못되었거나 서버가 Range를 인식하지 못하는 경우입니다.requests가 많은 것은 정상입니다. ffmpeg가 여러 번에 걸쳐 가져와야 하기 때문입니다 (moov, 키 프레임에서 타겟 프레임까지, 오디오 헤더).ok: false인 경우err_tail을 확인하세요. 일반적인 원인은 시간 초과 및 5XX입니다. 스크립트에 재시도가 내장되어 있으며, 여전히 실패하면 다른 시간대로 다시 시도하세요.
transcribe.py의 출력:
{
"ok": true,
"text": "이것은 큰 강과 산맥으로 둘러싸인 땅입니다...",
"segments": [{"start": 0, "end": 25, "text": "..."}],
"audio_seconds": 276.7,
"model": "SenseVoiceSmall-int8"
}
text는 모든 세그먼트를 연결한 전체 텍스트입니다. segments는 타임스탬프를 포함하며, "X초에 무엇을 말했는지" 확인해야 할 때 참조합니다.
환경
- 인터프리터: 로컬 호스팅 venv의 python. Windows에서 경로는
~/.workbuddy/binaries/python/envs/default/Scripts/python.exe와 같습니다. **Scripts/**임을 유의하세요. Unix 스타일의bin/이 아닙니다 (문서대로bin/python을 작성하면 파일이 없다는 오류가 발생합니다). - 의존성:
httpx,imageio-ffmpeg(데이터 가져오기),sherpa-onnx,numpy(전사),yt-dlp(범용 플랫폼 파싱)가 모두 동일한 venv에 설치되어 있습니다. 누락되면 스크립트가 명확하게 오류를 표시합니다. yt-dlp가 설치되지 않은 경우 bilibili는 여전히 사용 가능 (수제 어댑터 사용)하지만 다른 사이트는 파싱에 실패합니다. - ffmpeg:
imageio-ffmpeg에서 제공하는 정적 빌드를 사용하므로 시스템 설치가 필요 없습니다. 다른 ffmpeg를 지정하려면 환경 변수RV_FFMPEG또는--ffmpeg를 사용하세요. - ASR 모델 캐시:
~/.workbuddy/cache/read-video/models(약 240 MB, 첫 실행 시 자동 다운로드). 파일이 존재하고 바이트 수가 일치하면 다운로드를 건너뜁니다.
알려진 경계
- CDN은 종종 출처를 확인합니다: bilibili 등 사이트의 비디오 CDN은
Referer를 요구하며, 없으면 403을 반환합니다.resolve.py또는--page를 사용하면 자동으로 포함됩니다. 직접 링크를 수동으로 전달할 때는--header 'Referer: https://www.bilibili.com'를 기억하세요 (세 스크립트 모두 지원하며, 반복 가능). - 직접 링크는 만료됩니다: 사이트의 직접 링크에는 서명과 기한이 있어 동일 작업 내에서 사용해야 합니다. 403이 발생하면
resolve.py를 다시 실행하세요. - 플랫폼 범위 = yt-dlp의 범위 (1700+ 사이트), 자체 유지 관리가 아닙니다. 파싱할 수 없는 사이트는 먼저
--backend yt-dlp를 사용하여 원본 오류를 확인하세요. 로그인 / Cookie가 필요한 사이트는 실패하며, 이런 경우 포기하거나 수동으로 Cookie를 제공해야 합니다. yt-dlp 자체는 정기적으로 업그레이드해야 합니다 (사이트 개편 후 이전 버전은 작동하지 않음) ——pip install -U yt-dlp. - "파싱 성공" ≠ "데이터 가져오기 성공": 파싱 계층은 URL만 제공합니다. 데이터 가져오기 실패 시 먼저 파싱 실패인지 네트워크 연결 불가인지 구분하세요. 실제 측정 결과, 로컬 환경에서 YouTube / Vimeo는 연결되지 않지만 (프록시 502), TikTok / Weibo / Xigua Video는 연결 가능합니다.
- HLS / m3u8: 단일 파일이 아니므로
probe.py의 박스 분석이 실패합니다 (error필드 있음). 이 경우grab.py는 여전히 사용 가능하며, ffmpeg가 필요한 세그먼트를 가져옵니다.content_length는 비어 있어 비율을 계산할 수 없습니다. - DASH 오디오/비디오 분리: 많은 사이트 (bilibili 등)는 비디오 스트림과 오디오 스트림이 두 개의 별도 URL입니다. 프레임 캡처에는 비디오 스트림을, 전사에는 오디오 스트림을 사용하세요. 잘못 사용하면 조용히 빈 결과를 얻을 수 있습니다.
-ss정밀도는 GOP에 의해 제한됩니다: 특정 초의 프레임을 가져오려면 가장 가까운 키 프레임부터 디코딩해야 하므로 최소 가져오기 단위는 "키 프레임에서 타겟 프레임까지"이며, "한 바이트 구간"이 아닙니다.- ASR 동음이의어: SenseVoiceSmall은 중국어에 대해 괜찮은 성능을 보이지만, 인명, 지명, 고전 단어에서 동음이의어 오류가 자주 발생합니다 (실측 "阪泉之野" → "판취안즈예"가 아닌 다른 글자로 오인). 전문 용어를 인용하기 전에 문맥에 따라 교정하고, 전사 텍스트를 사실 그대로 인용하지 마세요.
- 의존성:
httpx+imageio-ffmpeg(데이터 가져오기) +sherpa-onnx+numpy(전사). 누락 시 스크립트가 명확하게 오류를 표시하고 설치할 패키지를 알려줍니다.
더 자세한 원리, 문제 해결 기록 및 실제 측정 데이터는 references/methodology.md를 참조하세요.
이 스킬과 dsh 플러그인 dsh-readvideo의 관계는 references/dsh-dual-form.md를 참조하세요.