Skip to content

Commit 14be253

Browse files
authored
fix: scale subtitle fontSize 3x for ASS rendering
1 parent 2516f8b commit 14be253

1 file changed

Lines changed: 14 additions & 36 deletions

File tree

BE/backend-api/process.py

Lines changed: 14 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from pathlib import Path
55
from fastapi import APIRouter, HTTPException
66
from pydantic import BaseModel
7-
from typing import Optional, Union
7+
from typing import Optional
88

99
router = APIRouter()
1010

@@ -14,26 +14,22 @@
1414
OUTPUT_VIDEO_DIR.mkdir(parents=True, exist_ok=True)
1515
OUTPUT_ASS_DIR.mkdir(parents=True, exist_ok=True)
1616

17+
# FE 미리보기 픽셀값을 ASS 좌표(PlayResY=1920)로 변환할 때 곱하는 배수
18+
FONT_SIZE_SCALE = 3
1719

18-
# ── 매핑 테이블 ────────────────────────────
19-
# 한글 폰트명 → 시스템 설치된 폰트명 (개발은 일단 맑은 고딕으로 통일, 추후 교체 가능)
2020
FONT_MAP = {
2121
"통통체": "NanumGothic",
2222
"각진체": "NanumGothic",
2323
"얇은체": "NanumGothic",
2424
"고딕": "NanumGothic",
2525
}
2626

27-
# 한글 위치 → ASS Alignment (numpad 1~9)
2827
POSITION_MAP = {
29-
"상단": 8, # top center
30-
"중앙": 5, # middle center
31-
"하단": 2, # bottom center
28+
"상단": 8, "중앙": 5, "하단": 2,
3229
"top": 8, "middle": 5, "bottom": 2, "center": 5,
3330
}
3431

3532

36-
# ── Pydantic 스키마 ────────────────────────
3733
class Segment(BaseModel):
3834
start: float
3935
end: float
@@ -47,12 +43,10 @@ class Segment(BaseModel):
4743

4844
class ProcessRequest(BaseModel):
4945
video_id: str
50-
segments: list[Segment]
46+
subtitles: list[Segment]
5147

5248

53-
# ── 유틸 함수 ──────────────────────────────
5449
def hex_to_ass_color(hex_color: str) -> str:
55-
"""#RRGGBB → &H00BBGGRR (ASS는 BGR 순서)"""
5650
h = hex_color.lstrip("#").upper()
5751
if len(h) == 3:
5852
h = "".join(c * 2 for c in h)
@@ -63,7 +57,6 @@ def hex_to_ass_color(hex_color: str) -> str:
6357

6458

6559
def to_ass_time(sec: float) -> str:
66-
"""0.0 → 0:00:00.00 (센티초)"""
6760
h = int(sec // 3600)
6861
m = int((sec % 3600) // 60)
6962
s = int(sec % 60)
@@ -73,24 +66,21 @@ def to_ass_time(sec: float) -> str:
7366
return f"{h}:{m:02d}:{s:02d}.{cs:02d}"
7467

7568

76-
def build_ass(segments: list[Segment]) -> str:
77-
"""Segment 리스트 → ASS 파일 텍스트"""
69+
def build_ass(segments):
7870
header = (
7971
"[Script Info]\n"
8072
"ScriptType: v4.00+\n"
8173
"PlayResX: 1080\n"
8274
"PlayResY: 1920\n"
8375
"WrapStyle: 0\n"
84-
"ScaledBorderAndShadow: yes\n"
85-
"\n"
76+
"ScaledBorderAndShadow: yes\n\n"
8677
"[V4+ Styles]\n"
8778
"Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, "
8879
"OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, "
8980
"ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, "
9081
"Alignment, MarginL, MarginR, MarginV, Encoding\n"
91-
"Style: Default,Malgun Gothic,50,&H00FFFFFF,&H000000FF,&H00000000,"
92-
"&H80000000,0,0,0,0,100,100,0,0,1,2,1,2,40,40,80,1\n"
93-
"\n"
82+
"Style: Default,NanumGothic,60,&H00FFFFFF,&H000000FF,&H00000000,"
83+
"&H80000000,0,0,0,0,100,100,0,0,1,3,1,2,40,40,80,1\n\n"
9484
"[Events]\n"
9585
"Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, "
9686
"Effect, Text\n"
@@ -103,17 +93,13 @@ def build_ass(segments: list[Segment]) -> str:
10393
color_ass = hex_to_ass_color(seg.color or "#FFFFFF")
10494
font_name = FONT_MAP.get(seg.font or "고딕", "NanumGothic")
10595
alignment = POSITION_MAP.get(seg.position or "하단", 2)
106-
font_size = seg.fontSize or 50
10796

108-
# 인라인 오버라이드 태그로 segment별 스타일 적용
97+
# FE 픽셀값 → ASS 좌표로 스케일업
98+
font_size = (seg.fontSize or 20) * FONT_SIZE_SCALE
99+
109100
override = (
110-
f"{{\\an{alignment}"
111-
f"\\fn{font_name}"
112-
f"\\fs{font_size}"
113-
f"\\c{color_ass}"
114-
f"}}"
101+
f"{{\\an{alignment}\\fn{font_name}\\fs{font_size}\\c{color_ass}}}"
115102
)
116-
# 줄바꿈은 \\N (ASS 표준)
117103
text = seg.text.replace("\n", "\\N").replace("\r", "")
118104
lines.append(
119105
f"Dialogue: 0,{start_t},{end_t},Default,,0,0,0,,{override}{text}"
@@ -122,12 +108,8 @@ def build_ass(segments: list[Segment]) -> str:
122108
return "\n".join(lines) + "\n"
123109

124110

125-
# ── 메인 엔드포인트 ────────────────────────
126111
@router.post("/process")
127112
async def process_video(req: ProcessRequest):
128-
if "/" in req.video_id or "\\" in req.video_id or ".." in req.video_id:
129-
raise HTTPException(400, detail="Invalid video_id")
130-
131113
video_path = UPLOAD_DIR / f"{req.video_id}.mp4"
132114
if not video_path.exists():
133115
raise HTTPException(404, detail="VIDEO_NOT_FOUND")
@@ -136,12 +118,9 @@ async def process_video(req: ProcessRequest):
136118
output_ass = OUTPUT_ASS_DIR / f"{req.video_id}.ass"
137119

138120
try:
139-
# ① ASS 파일 생성
140-
ass_text = build_ass(req.segments)
121+
ass_text = build_ass(req.subtitles)
141122
output_ass.write_text(ass_text, encoding="utf-8")
142123

143-
# ② ffmpeg로 ASS + MP4 합성
144-
# subtitles 필터는 경로에 콜론/슬래시 escape 까다로움 → cwd 지정으로 우회
145124
ass_filename = output_ass.name
146125
cmd = [
147126
"ffmpeg", "-y",
@@ -164,6 +143,5 @@ async def process_video(req: ProcessRequest):
164143
return {"success": True, "video_id": req.video_id}
165144

166145
except Exception as e:
167-
# 실패 마커
168146
(OUTPUT_VIDEO_DIR / f"{req.video_id}.failed").touch()
169147
raise HTTPException(500, detail=f"PROCESS_FAILED: {str(e)}")

0 commit comments

Comments
 (0)