ASS 작성 / libass 합성 실전 매뉴얼
1. 파일 헤더 템플릿 (그대로 사용)
[Script Info]
ScriptType: v4.00+
PlayResX: 2560
PlayResY: 1080
WrapStyle: 2
ScaledBorderAndShadow: yes
YCbCr Matrix: TV.709
[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: X,Microsoft YaHei,100,&H00F4F1EA,&H00F4F1EA,&H00140F11,&HFF000000,0,0,0,0,100,100,0,0,1,0,0,5,30,30,30,1
[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
Dialogue: 0,0:00:00.39,0:00:03.75,X,,0,0,0,,{\an5\pos(500,300)\fnAnton\fs150\1c&H00F4F1EA&\bord3\3c&H00140F11&\frz-4\fscx118\fscy118\t(0,130,\fscx100\fscy100)\fad(100,100)}WALK
2. 네 가지 치명적인 형식 함정 (이 프로젝트에서 모두 경험)
- 타임스탬프는 반드시
H:MM:SS.cc형식- 올바름:
0:00:01.50,0:01:02.30 - 틀림:
0:01.50(libass에서Bad timestamp오류 발생, 해당 줄이 무시됨 → 전체 비디오에 자막 없음, 하지만 ffmpeg는 0 반환)
- 올바름:
- Style 행 색상 끝에
&를 붙이지 말 것- 올바름:
&H00F4F1EA,&H00F4F1EA,&H00140F11,&HFF000000 - 틀림:
&H00F4F1EA&,...→ Style 행 파싱 실패 → 마찬가지로 '성공했지만 자막 없음' - 이벤트 내 인라인 태그 색상은
&를 포함:\1c&H00F4F1EA&
- 올바름:
- 글꼴 디렉토리
fontsdir는 상대 경로 + 작업 디렉토리 사용- 출력 디렉토리에
fonts/폴더를 만들고, 해당 디렉토리로cd한 후ass=x.ass:fontsdir=fonts사용. - 절대 경로는 드라이브 문자와 콜론이 필터그래프(filtergraph)에서 문제를 일으킬 수 있음; 상대 경로(중국어 포함)는 괜찮음(cwd는 중국어 가능하지만 필터 파라미터는 ASCII 권장).
- 출력 디렉토리에
- ASS 파일은
utf-8-sig로 저장, 파일명은 ASCII 사용- 중국어 파일명은 필터 파라미터에서 불안정함;
_s4.ass와 같은 형식 사용.
- 중국어 파일명은 필터 파라미터에서 불안정함;
진단 명령어 (libass가 실제로 읽는 내용 확인):
& $ff -hide_banner -v verbose -f lavfi -i "color=c=0x303040:s=2560x1080:r=30:d=2" `
-vf "ass=_s4.ass:fontsdir=fonts" -c:v libx264 -crf 22 -pix_fmt yuv420p _t.mp4 2>&1 | Select-String 'Bad timestamp|Style|font file'
3. 색상 / 알파(alpha) 의미
- 색상:
&HBBGGRR&(파랑-초록-빨강 순서), 예: 흰색&H00F4F1EA&, 먹색&H00140F11&, 분홍&H00B35CFF&, 라벤더&H00FFA4C4&, 크림&H00D7EEF8&, 금색&H0054C9FF&. - 투명도
\1a:&H00&완전 불투명;&HFF&완전 투명 (16진수 값이 높을수록 더 투명). 초기 버전에서 깜빡임 효과가 실패한 원인은 알파 의미가 반대였기 때문. - Style 행의 PrimaryColour는
&HAABBGGRR(AA가 앞에 오고, 끝에 & 없음).
4. 글꼴
- libass는 fontconfig/directwrite를 사용하므로 family name이 정확해야 함. PIL로 확인:
from PIL import ImageFont
ImageFont.truetype(r"...\fonts\Anton-Regular.ttf", 40).getname() # ('Anton','Regular')
- 이 프로젝트 사용 글꼴: Anton / Archivo Black / Bebas Neue / Permanent Marker / Silkscreen / Rubik Glitch / Bungee / Monoton / Pirata One / UnifrakturCook; 시스템 Georgia, Microsoft YaHei는
\fnGeorgia,\fnMicrosoft YaHei로 직접 사용 가능. - Permanent Marker는 libass에서 실제 크기가 약 0.79×fs이므로, 이 계수만큼 글자 크기/색상 블록 크기를 키워야 함.
- 글리프 누락(예: ♥ ★) 시 자동으로 ArialMT로 대체되어 표시는 되지만 글리프가 다름; 정확하게 표시하려면
\fnMicrosoft YaHei사용.
5. 애니메이션 태그 빠른 참조
| 태그 | 사용법 |
|---|---|
\fad(in,out) | 밀리초 단위 페이드 인/아웃 |
\move(x1,y1,x2,y2,t1,t2) | 위치 이동 (\pos와 함께 사용 불가, move 우선) |
\t(t1,t2,\tag...) | 시간 기반 보간, 예: \t(0,130,\fscx100\fscy100\blur(0)) |
\frz / \fscx / \fscy | 회전/크기 조정 (음수 값으로 뒤집기 가능) |
\blur(n) | 흐림 효과 (큰 글자가 나타날 때 흐림→선명 효과에 유용) |
\clip(x1,y1,x2,y2) | 직사각형 클리핑 (조각/스캔라이트/나타내기 효과) |
\org(x,y) | 회전/크기 조정의 중심점 설정 |
\p1 + 그리기 | m 0 0 l w 0 w h 0 h 직사각형 등 |
주의: 동일 속성에 연속된 두 개의 \t는 하나만 적용됨 (예: 규칙 선 수축 애니메이션). 두 개의 이벤트/두 번의 다시 그리기로 해결.
주의: \p1 그리기 좌표는 왼쪽 위 모서리 앵커이며, libass는 블록 전체를 높이의 h/2만큼 위로 이동시킴; \an7\pos(왼쪽위) + 절대 좌표 사용이 가장 안정적이며, 회전/스윕 효과는 \org로 원점 설정.
주의: 하이라이트 펜 색상 블록을 단어 뒤에 '스윕'하려면 그리기 레이어에서 \fscx0 → \t(0,150,\fscx100) 사용, 원점은 \org 사용.
6. 렌더링 명령어
6.1 직접 합성 (가장 자주 사용)
cd 출력_디렉토리
& $ff -v error -y -i _base_30s.mp4 -vf "ass=_s4.ass:fontsdir=fonts" `
-c:v libx264 -preset medium -crf 16 -pix_fmt yuv420p -movflags +faststart OUT.mp4
30초 / 2560×1080 / 중국어 자막량 기준 약 1–3분 소요.
6.2 투명 오버레이 (편집 소프트웨어 사용)
& $ff -v error -y -f lavfi -i "[email protected]:s=2560x1080:r=30,format=rgba" `
-vf "fps=30,ass=_sub.ass:alpha=1" -t 154.6 `
-c:v libvpx-vp9 -pix_fmt yuva420p -b:v 0 -crf 30 -row-mt 1 -auto-alt-ref 0 `
-metadata:s:v:0 alpha_mode=1 OUT_alpha.webm
- 디코딩은 반드시
-c:v libvpx-vp9사용; 기본 vp9 디코더는 알파 채널을 잃음. - 호환성 우선 '스크린 합성' mp4: 검은 배경 + 밝은 글자, 편집 소프트웨어에서 Screen/Lighten 혼합 모드 사용.
6.3 프레임 추출 검증 (렌더링 로그 대신 이미지 확인)
& $ff -v error -ss 12.4 -i OUT.mp4 -frames:v 1 -q:v 2 -y _check.jpg
참고:
-ss를-i앞에 (입력 seek) 두면, 특정 상황에서 ASS 타임라인이 출력 기준으로 리디렉션되어 미리보기가 비어 보일 수 있음; 검증 프레임 추출은 출력 측 seek (-i OUT.mp4 -ss 12.4) 또는 전체 구간을 추출한 후 선택하는 것이 좋음.
7. 문제 해결 빠른 참조표
| 현상 | 원인 | 해결 방법 |
|---|---|---|
| 전체 영상에 글자 없음 | 타임스탬프 형식 / Style 색상 끝 & | H:MM:SS.cc 형식으로 변경; Style 색상 끝 & 제거 |
| 일부 문장 없음 | 해당 문장 타임스탬프 오류 또는 start>end | 문장별로 확인 |
| 글자가 매우 작거나 글꼴 틀림 | 글꼴 family 이름 오류 | PIL getname()으로 확인 |
\p1 색상 블록이 위/아래로 치우침 | 그리기 앵커 + libass 상단 이동 h/2 | \an7 + 왼쪽 위 절대 좌표 + 실제 측정 기준 프레임 사용 |
| 회전 중심이 맞지 않음 | 기본 중심이 원하는 위치가 아님 | \org(x,y) 추가 |
| 페이드 아웃 효과 없음/깜빡임 반대 | 알파 의미 반대 | &H00 불투명, &HFF 투명 |
| 30초 렌더링에 처음 4문장만 표시됨 | 루프가 처음 몇 문장만 처리 | 이벤트 생성 루프 범위 확인 |
| 중국어 경로/파일명 렌더링 실패 | 필터 파라미터에 비ASCII 문자 포함 | ASS와 소스 파일은 ASCII 파일명 사용 + 중국어 cwd는 허용 |
8. 생성기 스켈레톤 (Python)
def ts(sec):
h=int(sec//3600); m=int(sec%3600//60)
return "%d:%02d:%05.2f" % (h,m,sec-h*3600-m*60)
class Ass:
def __init__(self,path,w=2560,h=1080): self.ev=[]; self.path=path; self.w=w; self.h=h
def add(self,layer,t0,t1,tags,text): self.ev.append((layer,t0,t1,tags,text))
def write(self):
head = open("template_header.ass",encoding="utf-8").read() # 또는 코드에 직접 포함
lines=[]
for layer,t0,t1,tags,text in sorted(self.ev,key=lambda e:(e[0],e[1])):
lines.append("Dialogue: %d,%s,%s,X,,0,0,0,,{%s}%s"%(layer,ts(t0),ts(t1),tags,text))
open(self.path,"w",encoding="utf-8-sig",newline="\n").write(head+"\n".join(lines)+"\n")
참고 구현: 字幕/build/styles_anim.py (S2/S4/S6), flare_wide.py, neon_wide.py, avoid_side2.py.