localizing-video은(는) 무엇을 하나요?
외국어 비디오를 영어로 변환합니다 — 동기화된 더빙 + 영어 자막 + 번역된 화면 내 텍스트 — Apple Silicon에서 100% 로컬/무료로 제공합니다. AI 에이전트가 yt-dlp, whisper, ffmpeg, scene-detect, mux 등의 도구를 사용하여 파이프라인을 구축할 수 있지만, 이 스킬은 재구축 시 잘못 선택하거나 간과하기 쉬운 도구 선택과 숨은 문제(footgun) 를 방지하기 위한 검증된 참조 구현체입니다.
참조 구현 (여기서 시작하세요)
이 저장소에는 작동하는 파이프라인이 포함되어 있습니다: engine/ 은 일반적인 실행 가능 엔진(build_dub.py — 청크 앵커링 기반, 드리프트 없음 (더빙 함정 참조), make_ass.py, dense_ocr.py, dedup_spans.py, render_overlay.py, run.sh)이고, example/ 은 완전한 작업 데이터셋입니다. engine/SETUP.md + example/README.md를 읽고 엔진을 실행하세요 — 처음부터 작성하지 마십시오. 비디오별로 새로 해야 할 작업은 en.srt + translations.json을 생성하는 것(번역 단계)뿐입니다. 엔진은 1920×1080 소스를 가정합니다.
파이프라인 및 각 단계의 결정
- 다운로드
yt-dlp(403 오류 시--extractor-args "youtube:player_client=web_safari,ios,tv"시도; DASH가 DRM인 경우 HLS 형식이 종종 작동). - ASR — 하지만
yt-dlp --list-subs를 먼저 실행 (함정 참조): 수동/크리에이터 트랙이 무료로 Whisper보다 더 나은 결과를 낼 수 있고,-orig태그는 실제 소스 언어를 알려줍니다(16분 동안 Whisper를 돌릴 필요 없이 "이미 대상 언어임"을 파악).-orig자동 캡션은 무료 제2 ASR이므로 교차 검증에 사용할 수 있습니다. 이후 openai-whisperwhisper source.mp4 --model large-v3-turbo --language <zh/ja> --word_timestamps True --output_format all(JSON 유지). 언어를 고정하세요. (torch는 py3.14 휠이 없으므로 py3.12 venv에서 whisper를 실행하세요.) - 직접 (에이전트가 루프 내에서) TRANSLATE transcript — 무료, 톤 인식, 전문 용어 일관성 유지. 1:1 세그먼트↔타임스탬프 매핑을 유지하세요. 고유 명사는 지식을 바탕으로 수정하세요(ASR이 잘못 인식함). 말장난을 그대로 살리세요, 단어가 아닌 — 말장난은 직역으로 살아남는 경우가 드물기 때문에, 동일한 메커니즘을 지닌 영어 말장난을 트랜스-크리에이트하고, 그 효과를 자막에서 전달하세요(말장난의 주요 전달 매체 — 더빙은 원본 오디오 덕킹 위에 올라탐). 동음이의어 말장난은 ASR에서 보이지 않습니다 (함정 참조). 따라서 자막을 OCR하여 찾아내야 합니다.
- 더빙 — Kokoro-onnx TTS (함정 참조). 문장 단위 합성, 각 청크를 실제 타임스탬프에 앵커링(세그먼트별 드리프트 추적이 아님 — 누적 지연 발생), 시간 맞춤, 믹싱. 더빙을 따라가기 위해 시각적 타이밍을 조정합니다(더빙을 시계에만 맞추지 말고) — Time-density 참조.
- 자막 — 영어 자막을 레터박스 바에 입히고, 프레임 위에 겹치지 않도록 합니다 (함정 참조).
- 시각적 OCR —
ocrmac(Apple Vision), 1fps 밀집 처리, 시간 범위로 중복 제거. - 직접 시각적 문자열을 번역; 중국어 전용 박스 위에 영어를 오버레이.
- 먹싱 —
ffmpeg-full(libass), 오버레이 → 레터박스 → 자막 버닝 → 더빙 오디오.
함정 — 이 스킬이 존재하는 실제 이유
| 함정 | 현실 / 해결책 |
|---|---|
ffmpeg에 subtitles 필터 없음 | 기본 brew ffmpeg는 libass가 없습니다. /opt/homebrew/opt/ffmpeg-full/bin/ffmpeg를 사용하세요. (스킬: hardsubbing-video) |
TTS = say는 평평하고 로봇 같음 | 기본값으로 Kokoro-onnx (무료, 신경망, onnxruntime을 통해 py3.14 지원). 남성 am_michael 등. |
Kokoro가 조용히 실패: phontab ... No such file | 번들된 espeak 데이터 경로가 깨짐. brew install espeak-ng를 실행한 후, 스크립트 내에서 espeakng_loader.get_data_path/get_library_path를 재정의하여 /opt/homebrew/share/espeak-ng-data + /opt/homebrew/lib/libespeak-ng.dylib를 사용하세요. |
| 영어가 중국어/일본어보다 말하는 데 밀도가 낮음 → 더빙이 늦게 끝남 | atempo를 사용하지 마세요(>1.25×는 보코더 아티팩트 생성) — Kokoro의 네이티브 speed 로 재합성. 실행 커서로 드리프트를 추적하지 마세요(cursor-start catch-up): 경험적으로 밀집된 연속 음성의 경우 동영상 중간까지 ~+5초 누적. 대신: (a) 각 발화를 실제 시작점에 앵커링(=자막/비디오 타임라인)하여 드리프트가 누적될 수 없도록 함; (b) 인접한 ASR 세그먼트를 ~문장 단위 발화로 청킹하여 긴 영어 줄이 짧은 이웃에서 시간을 빌려올 수 있도록 함 — 세그먼트별 피팅은 1.9× 한도에서 59% 라인이 잘렸지만, 문장 청킹은 이를 ~0으로 줄였습니다. 앵커링은 드리프트를 잡지만, 오버런을 잡지 못합니다 — 오디오가 할당된 슬롯을 초과하는 청크는 여전히 다음 앵커와 충돌합니다. 오버플로우 폴백 순서: (1) 간결한 번역, (2) ~1.9× 한도 내에서 더 빠르게 재합성("압축" = 네이티브 speed 재합성 또는 더 짧은 텍스트 — atempo 아님), (3) 그 아래 슬라이드를 프리즈 확장(Time-density 참조), (4) 밀도가 낮은 이웃의 여유 시간을 빌림; 최후의 수단으로, 하드캡 + 30ms 페이드아웃(자막은 여전히 전체 줄을 전달). |
| 영어 자막이 비디오 위에 크게 렌더링됨 | SRT→libass가 PlayResY=288로 기본 설정되어 글꼴/여백이 ~4.4배 확대됨. 명시적인 PlayResY가 있는 ASS를 생성해야 함 = 패딩된 높이. 그리고 레터박스(하단 막대 패딩, Alignment=2)를 사용하여 영어가 소스 자체의 내장 자막과 충돌하지 않도록 함(해당 자막은 제거할 수 없음). |
| Vision OCR "사용자 정의 스크립트가 필요함" | 아니요 — pip install ocrmac; ocrmac.OCR(f, language_preference=["zh-Hans","en-US"]).recognize()는 정규화된 (text, conf, (x,y,w,h))를 반환하며, 원점은 왼쪽 하단. |
| ASR 대 화면 자막 조정 | 하단 자막은 내레이션에 대한 권위 있는 정답입니다(좋은 ASR 교차 검증). 하지만 제작자는 개그 자막을 넣습니다(횡설수설 ≠ 실제 대사); ASR을 OCR로 맹목적으로 대체하지 마세요. 차이점을 식별하고 각각 판단하세요. |
| ASR이 동음이의어 말장난을 조용히 평탄화함 | 말장난 자체가 동음이의어일 수 있습니다: 人工智能(AI)를 人工痔能("치질 기술")로 발음하거나, 搁置("프로젝트 보류")를 割痔疮("치질 제거")로 발음. Whisper는 희귀한 말장난을 일반적인 단어로 "수정"하므로 개그가 대본에 도달하지 못함 — 고유 명사 수정으로는 복구할 수 없습니다. 제작자는 일반적으로 내장 자막에 말장난을 철자하므로, 여기서 OCR은 교차 검증이 아니라 일반적으로 유일한 신뢰할 수 있는 복구 경로입니다(철자된 동음이의어의 경우 자막이 우선하며, 위의 개그 자막 경우와 반대입니다). 징후: 한 줄이 이상하게 직역된 경우, 또는 반응/시각 효과가 단어와 일치하지 않는 경우 → 번역 전에 해당 자막을 OCR하세요. |
| YouTube 자막을 먼저 확인하지 않고 Whisper 실행 | yt-dlp --list-subs는 5초가 걸리며 세 가지 이점을 제공합니다. (1) <lang>-orig 태그 = 비디오의 실제 소스 언어; 이미 대상 언어라면(예: en-orig) 전체 파이프라인은 무효 — 다운로드+Whisper 전에 중단. (2) 수동/크리에이터 트랙(소스 언어)은 종종 Whisper보다 우수: 실제 구두점, 올바른 고유 명사, 삽입된 [음악] 없음. (3) -orig 자동 캡션은 무료 제2 ASR — Whisper와 비교하여 정확히 인간 수정이 필요한 줄을 찾을 수 있습니다. 16분 영어 강연에서 측정된 결과: 98.2% 단어 일치, 그러나 두 엔진은 Arthur Andersen(YT, 정답) vs Anderson(Whisper, 오답)에서 갈렸고 둘 다 브랜드명(Saily → "Saylite"/"Saley")을 잘못 처리함 — 교차 검증이 이를 식별; 어느 엔진 혼자서는 잡을 수 없습니다. 함정: --list-subs에 나오는 다른 100개 이상의 언어는 YouTube 자동 번역이며, 크리에이터 트랙이 아닙니다 — 거기에 zh-Hant 행이 있다고 해서 실제 중국어 소스로 착각하지 마세요. |
| 모든 중국어 박스에 오버레이 | 해설 비디오에서 화면상의 많은 "내용"(기사, 포함된 강연, 트윗)은 이미 영어이고 중국어가 위에 덧씌워져 있음 — 오버레이는 중복되어 지저분함. 중국어 전용 요소로 범위를 좁히세요. |
| 스크롤 텍스트에 정적 오버레이 | 스팬별(평균 위치) 오버레이는 연속 스크롤을 추적할 수 없음 — 중국어가 계속 가려짐. 프레임별 OCR + 박스 추적이 필요하며, 일반적으로 가치가 없음(해당 콘텐츠는 이미 영어 종류). |
| 원본 영어 클립을 들리게 유지 | 소스가 실제 영어 클립(인터뷰/데모)을 재생하는 경우, 원본 오디오를 전체 볼륨으로 그대로 전달하고 더빙 아래에서만 덕킹(약 0.1)하세요. |
| 새로운 비디오에서 스크립트 재사용 | 적응하되 하드코딩된 길이/경로가 있는지 확인하세요. render_overlay.py는 하드코딩된 프레임 수에서 오버레이 지속 시간을 파생함 — 더 긴 비디오에서 오버레이가 조용히 중간에 멈춤. len(glob('f1/f_*.png'))에서 지속 시간을 파생하고 상수는 사용하지 마세요. 비디오별로 en.srt + translations.json을 다시 번역하세요; 인덱스 페어링 어설션과 병렬 확인으로 번역/키 정렬 오류를 잡을 수 있음. |
Time-density: 더빙뿐만 아니라 타임라인 재조정
영어는 중국어/일본어보다 초당 약 1.5~2배 적은 비트를 전달하므로, 충실한 더빙은 밀집된 소스에서 항상 시간과 싸우게 됩니다. 더빙을 압축하는 것(네이티브 speed)은 하나의 지렛대이며, ~1.9배 한도가 넘으면 서둘러 말하는 것처럼 들립니다. 더 큰 지렛대는: 시각적 타임라인은 탄력적입니다 — 더빙에 맞게 재조정하세요. 세 가지 지렛대, 비용이 적게 드는 순서:
- 먼저 간결한 번역(밀집→더 짧게). 가장 밀집된 줄을 더 짧게 바꾸세요. 무료이며 시청자가 보는 것에 아무것도 잃지 않으며, 더빙이 조금 줄이더라도 자막은 여전히 전체 의미를 전달합니다. 이것이 talking-head 샷의 주요 지렛대입니다(움직이는 얼굴을 잡으면 떨림 발생; B-roll로 재편집은 일반적으로 제자리 현지화 작업 범위를 벗어남).
- 정적 슬라이드 프리즈 확장(압박이 있는 곳에서 시간 확보). 내레이터가 스크린샷/슬라이드를 읽을 때 더빙이 가장 빨리 달리며 — 해당 프레임은 정적이므로, 자연스러운 일시 정지 지점에서 약간 더 길게 유지하여 시간을 확보하세요. 세 가지 조건을 지킨다면 거의 눈에 띄지 않습니다: (i) 타임라인 편집입니다 — 모든 다운스트림 항목이 유지 시간만큼 이동하므로, 프리즈를 삽입하고 유지된 타임라인에 대해 자막/더빙/오버레이 타이밍을 다시 생성하거나(또는 먹싱 시 모든 트랙을 함께 유지), 원래 시간에 앵커링하지 마세요; (ii) 오디오 배경음도 일시 정지하는 곳에서만 프리즈 — 연속 음악/룸톤/원본 오디오 위에서 유지하면 배경음이 동기화 해제됨(확장/크로스페이드하거나, 배경음이 덕킹/무음인 더빙 아래에서만 프리즈); (iii) "정적"은 진정으로 정적이어야 함 — 커서, 깜박이는 캐럿, 레이저 포인터 또는 프레임의 웹캠 인셋은 환상을 깨뜨립니다. 많은 작은 유지를 하고, 긴 드래그는 하지 마세요(몇 초 유지는 버퍼링/정지로 읽힘).
- 밀도가 낮은 구간에서 여유 시간 회수(역방향 — 덜 밀집→밀집). 인트로, 사인오프, 느린 팬에는 여유 공간이 있습니다; 더빙이 거기서 숨을 쉬게 하거나, 그 여유를 밀집된 구간으로 앞당겨 빌리세요.
이것이 작동하는 이유: 밀도와 프리즈 가능성은 상관관계가 있습니다. 내레이션이 밀집된 이유는 슬라이드를 설명하기 때문이며 — 슬라이드는 정확히 프리즈 가능한 것입니다. 따라서 대부분의 시간 압박은 흡수할 수 있는 곳에 발생하는 경향이 있습니다. 추정하지 말고 측정하세요: 각 초를 프레임 간 움직임(평균|프레임 − 이전|을 다운스케일된 그레이스케일에서)으로 정적으로 표시하세요 — 그러나 임계값은 비디오별 보정이 필요합니다(코덱 노이즈, 비트레이트, fps, 그레인이 모두 영향을 줌; ≈4/255가 한 비디오의 값이었음), 그리고 전체 프레임 평균은 국지적 움직임을 놓칩니다(커서/캐럿/포인터) → 자막 대역을 마스킹하고 전체 프레임이 아닌 정적 영역을 확인하세요. 그런 다음 정적 마스크와 빠른 더빙 청크를 교차하여 압박이 얼마나 프리즈 가능한지 확인하세요. 한 10분 설명자 비디오에서 측정된 결과, 차분한 1.4× 한도는 ~14초를 회수해야 했고 74%가 프리즈 가능한 정적 슬라이드 위에 있었습니다(26%는 프리즈 불가능한 talking-head 위 → 간결한 표현) — 그러나 이 비율은 비디오마다 다릅니다. 소스 런타임을 초과하는 것은 독립 실행형 파일에는 괜찮습니다; 외부에서 동기화되는 것이 없기 때문입니다.
운영자와 논의할 결정 사항
- 시각적 범위: 타겟팅(중국어 전용, 권장) vs 전체(기사 단락 포함 — 스크롤 시 저하).
- 더빙 음성: 사용 가능한 무료 Kokoro 음성; 유료 (ElevenLabs)는 청구 제약을 완화하는 경우에만 제안.
- 오디오: 전체 교체 vs 더빙 아래에 원본을 덕킹하여 유지.
- 페이싱 노력: 빠른 더빙 수용(자막이 전체 의미 전달) vs 차분한 한도를 위한 시간 재조정(간결한 표현 + 슬라이드 프리즈 확장 — 측정 + 재먹싱 비용 발생). 결정할 수 있도록 측정값을 제공하세요.
검증 (신뢰하지 말고 — 확인하세요)
최종 mp4를 프레임 샘플링(JPG 읽기)하여 내레이션 순간, 슬라이드/오버레이 순간, 영어 클립 간격, 마지막 줄에서 확인하세요 — 자막 동기화, 오버레이 배치, 레터박스, A/V 지속 시간 일치를 확인하세요. 픽셀은 영구적으로 타버립니다. 특히 밀집된 슬라이드 순간을 샘플링하세요(더빙이 빨라지는 곳) — 그곳이 동기화/드리프트가 먼저 깨지는 곳입니다; 해당 타임스탬프의 번인된 중국어 자막이 동기화의 기준입니다. (오디오를 직접 듣고 확인할 수 없습니다 — 동기화는 앵커된 타임라인을 통해 구조적입니다; 그렇게 말하고 운영자에게 최종 청취를 요청하세요.)
다음과 함께 사용
hardsubbing-video (ffmpeg/libass 번인 함정 — 이 스킬의 8단계).