Communitygithub.com

자막 삽입 (ASS/FFmpeg)

실제 ASS 자막 파일과 FFmpeg의 libass 렌더러를 사용하여 비디오에 스타일이 적용되고 읽기 쉬운 자막/캡션을 삽입합니다. drawtext 해킹 대신 전문적인 자막 워크플로우를 제공합니다.

FFmpeg)란 무엇인가요?

이 스킬은 FFmpeg의 libass 렌더러를 사용하여 비디오에 전문적인 스타일의 자막을 삽입하는 방법을 설명합니다. 기본 FFmpeg는 drawtext/subtitles 필터가 없을 수 있으므로, Homebrew의 `ffmpeg-full`을 설치해야 합니다. ASS 형식을 사용하면 글꼴, 크기, 위치, 색상, 테두리, 그림자 등 고급 스타일링과 정확한 줄바꿈 제어가 가능합니다. 이 스킬은 실제 프로젝트 경험에서 얻은 여러 교훈을 포함합니다: 자동 줄바꿈 대신 자연스러운 말하기 호흡에 맞춰 수동으로 줄바꿈(`\N`)해야 하며, 자막이 인스타그램 등 플랫폼에서 잘리지 않도록 충분한 여백(MarginL/R)을 설정해야 합니다. 또한, 자막이 화면마다 움직이지 않도록 상단 정렬(Alignment 8)을 사용하고, 연속된 자막 사이에 0.12초의 간격을 두어 텍스트가 깜빡이는 현상을 방지합니다. 전체 렌더링 전에 테스트 프레임을 추출하여 오버플로우나 클리핑을 빠르게 확인할 수 있으며, 피드백은 ASS 파일을 직접 편집하고 빠른 프레임 체크 후 다시 렌더링하는 반복 작업으로 처리합니다.

지원 대상~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/Istok-Media-Tool/video-edit-skills/tree/HEAD/video-edit/skills/subtitle

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

자막 삽입 (Subtitle burning)

이 스킬이 존재하는 이유

실제 프로젝트에서 캡션이 제대로 보이도록 하는 데 여러 번의 수정 작업이 필요했으며, 각각의 교훈을 처음부터 다시 배우지 않도록 기록합니다:

  1. 시스템의 기본 Homebrew ffmpeg에는 drawtext/subtitles 필터가 전혀 없습니다 (libass/freetype으로 빌드되지 않음). 실제 스타일이 적용된 자막을 삽입하려면 다른 ffmpeg 빌드가 필요하며, 임시방편이 아닌 올바른 방법을 사용해야 합니다.
  2. 캡션 줄을 문자 수에 맞춰 분할하면 말도 안 되는 결과가 나옵니다. 줄바꿈은 사람이 실제로 문장을 말할 때 자연스럽게 멈추는 위치에 이루어져야 하며, 절대 구절 중간에 끊어서는 안 됩니다.
  3. 프레임 왼쪽/오른쪽 가장자리에 너무 가깝게 늘어선 줄은 게시 후 Instagram의 자체 크롭으로 잘릴 수 있습니다. 단순히 "프레임에 맞는다"가 아닌 실제 여백이 필요합니다.
  4. 글꼴 크기와 수직 위치는 프로젝트마다 다르므로 확인할 가치가 있지만, 전체 렌더링 전에 시각적으로 검증해야 합니다. 스틸 프레임 확인은 오버플로우/클리핑을 몇 초 만에 찾아내지만, 전체 재렌더링은 그렇지 않습니다.
  5. 캡션은 화면 간에 움직여서는 안 됩니다. 첫 번째 버전에서는 캡션을 하단에 고정(Alignment 2, MarginV 500)했기 때문에 3줄 캡션이 2줄이나 1줄 캡션보다 더 높게 시작되었고, 이전 캡션과 수백 분의 1초 겹치는 캡션은 libass의 충돌 회피 기능에 의해 전체 지속 시간 동안 위로 밀려 올라갔습니다. 사용자는 제목이 "위아래로 움직이는" 것을 보았습니다. 수정 규칙: 상단에 고정(Alignment 8, MarginV = 첫 번째 줄의 y 좌표; 1080x1920에서 1215는 3줄 캡션의 가장 낮은 줄이 이전 하단 고정 위치에 있도록 함)하여 모든 캡션의 첫 번째 줄이 같은 위치에 있고 추가 줄은 아래로 자라나도록 하며, 두 Dialogue 이벤트가 시간적으로 겹치지 않도록 합니다. 또한, 연속되는 캡션 사이에 짧은 빈 순간을 남깁니다: 각 이벤트를 다음 이벤트 시작보다 0.12초(25fps에서 3프레임) 일찍 종료합니다. 사용자가 명시적으로 요청한 것입니다. 이 짧은 빈 공간은 뇌에 캡션이 변경되었음을 알려줍니다. 연속된 캡션은 동일한 텍스트가 깜빡이는 것으로 읽힙니다. 이전 캡션의 종료 시간을 단축하고, 다음 시작 시간(음성에 동기화됨)을 절대 지연하지 마십시오.
  6. 캡션 텍스트가 반드시 문자 그대로의 대본일 필요는 없습니다. 문법/표현 문제(잘못 들은 단어, 어색한 구성)를 수정하고 사용자가 변경을 요청한 단어를 추가/제거하십시오. 이는 다른 사람이 읽을 텍스트를 교정할 때와 동일합니다.

1단계: FFmpeg가 실제로 자막을 삽입할 수 있는지 확인

ffmpeg -hide_banner -filters 2>&1 | grep -E "subtitles|^\s*\S*\s+ass\b"

일치하는 항목이 없으면 일반 ffmpeg로는 이 작업을 수행할 수 없습니다. 전체 빌드를 설치하십시오:

brew install ffmpeg-full

ffmpeg-full은 keg-only( PATH에 심볼릭 링크되지 않음)이므로 자막 삽입 단계에서 명시적으로 사용해야 합니다: /opt/homebrew/opt/ffmpeg-full/bin/ffmpeg.

실제 발생한 문제점: ffmpeg-full을 설치하면 새로운 공유 x265가 함께 설치되어 PATH에 있는 일반 ffmpeg/ffprobe가 손상될 수 있습니다(dyld "Library not loaded: libx265.*.dylib" 오류). 이전 버전에 링크되어 있었기 때문입니다. ffmpeg-full을 설치한 후 즉시 다음을 실행하십시오:

ffmpeg -version && ffprobe -version

둘 중 하나라도 오류가 발생하면 brew reinstall ffmpeg(일반 포뮬러, -full 아님)로 수정하고 계속하기 전에 두 명령어가 모두 제대로 작동하는지 다시 확인하십시오. 일반 ffmpeg가 설치에 영향을 받지 않았다고 가정하지 마십시오.

2단계: 정확한 캡션 텍스트 및 타이밍 확인

비디오가 기본 스킬의 컷팅 워크플로우에서 나온 경우, 세그먼트별로 유지된 정확한 텍스트와 원본 소스 타임스탬프를 이미 알고 있을 것입니다. 그러나 세그먼트가 컷팅되고 연결되면 타이밍은 새 비디오의 자체 타임라인으로 재설정됩니다. 최종 비디오에서 각 세그먼트의 위치를 계산하려면 실제 렌더링된 클립 지속 시간을 순서대로 합산하십시오:

ffprobe -v error -show_entries format=duration -of csv=p=0 clip_01.mp4

... 그리고 누적합니다. 각 세그먼트의 창 내에서 클립 자체의 시작/종료 시간에서 약간 안쪽으로 여백을 두어 음성이 실제로 시작/종료되는 지점과 일치시킵니다(클립에 컷 단계의 사전/사후 패딩이 포함된 경우 다시 빼냄). 이렇게 하면 캡션이 무음 구간에 나타났다 사라지지 않습니다.

3단계: ASS 파일 작성

다음 시작 템플릿을 사용하십시오. 실제 프로젝트에서 확인된 정확한 스타일입니다(Inter Bold, 흰색 텍스트, 불투명 검정 상자, 하단 중간 위치, 충분한 측면 여백):

[Script Info]
Title: <video name> - titlovi
ScriptType: v4.00+
WrapStyle: 2
ScaledBorderAndShadow: yes
PlayResX: 1080
PlayResY: 1920

[V4+ Styles]
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
Style: Default,Inter,70,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,-1,0,0,0,100,100,0,0,3,16,0,8,120,120,1215,1

[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
Dialogue: 0,0:00:00.15,0:00:05.93,Default,,0,0,0,,Zamisli univerzum koji postoji\Ntoliko dugo da su sve zvezde\Nodavno ugašene

삽입하기 전에 모든 이벤트가 다음 이벤트 시작보다 0.12초 일찍 종료되도록 설정하십시오(이전 이벤트의 End를 다음 Start - 0.12로 설정, Start는 절대 이동하지 않음) - 5번 교훈 참조.

PlayResX/PlayResY를 실제 비디오 해상도에 맞게 조정하십시오(이 예제는 1080x1920 세로). WrapStyle: 2는 libass가 텍스트를 자동으로 줄바꿈하지 않음을 의미합니다. 모든 줄바꿈은 Text 필드에 명시적인 \N이어야 합니다. 이는 의도적입니다. 렌더러가 추측하도록 두는 대신 모든 줄바꿈을 신중하게 선택하도록 강제하며(4단계 참조), 또한 넘치는 줄이 조용히 추가되는 대신 잘린 가장자리로 포착되도록 합니다.

글꼴/크기/색상/위치는 모두 시작하기 위한 기본값이며 고정 규칙이 아닙니다. 새 프로젝트인 경우 사용자와 확인하거나, 이 사용자에 대해 이미 알고 있는 마지막으로 확인된 값을 재사용하십시오.

실제 프로젝트에서 확인된 구두점 규칙

캡션 화면의 마지막 줄 끝에 있는 마침표 및 쉼표는 제거합니다(캡션은 산문처럼 마침표가 필요하지 않습니다. 해당 크기에서 시각적 잡음으로 읽힙니다). 이는 문장이 다음 캡션 화면으로 이어지는 경우에도 적용됩니다. 화면 변경 자체가 이미 중간 휴지로 읽히므로 "svoje znanje kroz praksu,"는 "svoje znanje kroz praksu"로 표시됩니다. 사용자가 명시적으로 요청했습니다(2026-11-05, "isto kao za tačku"). 물음표와 느낌표는 유지합니다. 화면 내부의 쉼표(줄 중간 또는 화면의 마지막 줄이 아닌 줄의 끝)는 유지합니다. 모든 비디오에 기본적으로 적용하며, 이중언어 캡션의 두 번째 언어 줄과 사용자가 계획 편집기에서 편집한 캡션 텍스트에도 적용합니다.

4단계: 사람이 실제로 말을 멈추는 위치에서 모든 줄 바꿈

이 부분이 가장 제대로 해야 할 부분이면서 가장 게을러서 틀리기 쉬운 부분입니다. 문장을 소리 내어 말하는 것처럼 읽고 자연스러운 호흡/구절 경계에서 줄바꿈하십시오. 절 경계, 완전한 생각 후, 실제 말하기 멈춤을 나타내는 쉼표에서 끊으십시오. 자연스러운 끊김 지점이 다른 곳에 있다면 줄이 "더 짧아져야" 한다고 해서 줄바꿈하지 마십시오. 대신 더 짧은 자연스러운 끊김 지점을 찾으십시오(긴 문장에는 거의 항상 유효한 멈춤 지점이 두 개 이상 있습니다). 너비 목표를 달성하기 위해 부자연스러운 끊김을 강요하지 마십시오.

실제 수정 작업의 구체적인 예 - 잘못된 경우(문자 수로 균형을 맞추고 구절 중간에 끊김):

Zamisli univerzum koji postoji toliko
dugo da su sve zvezde odavno ugasene

올바른 경우(자연스러운 말하기 멈춤에서 끊김):

Zamisli univerzum koji postoji
toliko dugo da su sve zvezde
odavno ugašene

1, 2, 또는 3줄 모두 괜찮습니다. 고정된 최대치는 없습니다. 중요한 것은 모든 줄바꿈이 화자가 실제로 멈출 위치여야 하며, 모든 줄이 여백 안에 들어맞아야 한다는 것입니다(다음 단계).

5단계: 실제 측면 여백에 대한 너비 확인, 원시 프레임 너비 아님

스타일의 MarginL/MarginR(위 템플릿에서 1080 너비 프레임에 각각 120px, 약 11%씩)은 비디오가 게시될 플랫폼(Instagram 등)에서 업로드 후 자체적인 약간의 크롭을 적용하기 때문에 존재합니다. 텍스트가 프레임 경계뿐만 아니라 이로 인해 잘릴 수 있습니다. 줄이 너무 긴지 판단할 때 PlayResX - MarginL - MarginR을 실제 사용 가능한 너비로 취급하고 전체 프레임 너비로 취급하지 마십시오.

WrapStyle: 2는 자동 줄바꿈을 비활성화하므로 실제로 너무 넓은 줄은 스스로 줄바꿈되지 않고 여백을 넘어 넘쳐흐르며, 이는 잘린/잘린 텍스트로 읽힙니다. 전체 렌더링에 들어가기 전에 6단계에서 이를 포착하십시오.

6단계: 전체 비디오를 굽기 전에 테스트 프레임 렌더링

가장 길거나 위험한 2-3개의 캡션 각각에서 타임스탬프를 선택하고 해당 프레임만 렌더링하십시오:

/opt/homebrew/opt/ffmpeg-full/bin/ffmpeg -nostdin -y -i video.mp4 -ss <t> -update 1 \
  -vf "subtitles=captions.ass" -frames:v 1 check.png -loglevel error

각각에서 다음을 확인하십시오: 여백을 넘는 텍스트, 의도보다 많은 줄(예상보다 넓은 줄을 나타내며 상위 단계에서 단어 수정/재분할이 필요함), 수직 위치가 실제로 "중간 아래"로 읽히는지(맨 아래 가장자리에 너무 가깝거나 얼굴을 가릴 정도로 높지 않은지). 수정하고 다음으로 넘어가기 전에 다시 확인하십시오. 이 루프는 빠르며(프레임당 몇 초), 전체 렌더링이 몇 분을 낭비할 오류를 정확히 포착합니다.

7단계: 최종 비디오 굽기

/opt/homebrew/opt/ffmpeg-full/bin/ffmpeg -nostdin -y -i video.mp4 \
  -vf "subtitles=captions.ass" -c:v libx264 -preset medium -crf 18 -pix_fmt yuv420p \
  -c:a copy output_titlovi.mp4 -loglevel error

오디오는 자막이 비디오 전용이므로 건드리지 않습니다(-c:a copy). 출력을 파일 크기 제한이 있는 도구에 전달해야 하는 경우, 마스터를 압축하는 대신 보조 압축 복사본을 만드십시오. 기본 및 줌 스킬과 동일한 패턴입니다:

ffmpeg -nostdin -y -i output_titlovi.mp4 -c:v libx264 -preset slow -crf 24 -pix_fmt yuv420p -c:a aac -b:a 128k output_titlovi_share.mp4 -loglevel error

피드백 반복

캡션 작업은 일반적으로 작고 구체적인 수정(글꼴 크기, 위치, 잘못된 단어, 구두점)의 여러 라운드를 거칩니다. 각 라운드는 변경된 부분만 건드려야 합니다. .ass 파일을 직접 편집하고(일반 텍스트입니다), 영향을 받은 부분에 대해 6단계의 빠른 프레임 확인을 다시 실행한 다음, 7단계로 다시 굽습니다. 기본 비디오 자체가 변경되지 않는 한 1-2단계를 다시 수행할 필요가 없습니다.

대화형 도구를 통한 잘못된 단어 수정

사용자는 동일한 계획 편집기 도구(plan-editor 스킬 - 사용자의 표준 편집기)에서 캡션 텍스트를 직접 수정하는 것을 선호합니다. "Izmeni tekst" 토글을 사용하면 모든 문장을 제자리에서 편집할 수 있습니다. 캡션이 존재할 때마다, 수정 사항을 글로 요청하는 대신 --ass captions.ass --durations ...(plan-editor 스킬 참조) 옵션과 함께 해당 도구를 표시하십시오. 또한 캡션만 확인이 필요하고 줌이 계획되지 않은 경우에도 마찬가지입니다(그런 다음 모든 클립을 와이드로 시작하고 줌 계획은 그대로 둡니다).

응답에는 변경된 화면만 포함된 CAPTIONS_JSON: {"<dialogue index>": "line 1\nline 2"}가 포함됩니다. 적용 방법:

python3 scripts/apply_caption_edits.py captions.ass caption_edits.json captions_fixed.ass

텍스트만 변경하고(타이밍과 스타일은 유지), 각 줄을 명시적인 \N 줄바꿈으로 바꿉니다. 그런 다음 편집된 화면에 대해 6단계의 프레임 확인을 다시 실행하고(더 긴 단어가 여백을 넘을 수 있음) 7단계로 다시 굽니다.

Individual skills in this repo

This repo contains 2 individual skills — each has its own dedicated page.

관련 스킬