198 lines
6.9 KiB
Python
198 lines
6.9 KiB
Python
"""세로형 썸네일 정지 이미지 렌더용 컴포지션 및 source 조립.
|
||
|
||
CreatomateService에 의존하지 않는 코드 측 자산이다. 실제 렌더 요청·이미지 선택은
|
||
CreatomateService.render_thumbnail / select_thumbnail_image가 담당한다.
|
||
"""
|
||
|
||
import copy
|
||
|
||
# 세로형 썸네일(1080×1920) 정지 이미지 렌더용 컴포지션.
|
||
# 운영 템플릿에 의존하지 않는 코드 측 자산 — 디자인 수정 시 이 상수만 변경.
|
||
# 텍스트 슬롯 이름은 CreatomateService.make_thumbnail_modification()의 반환 키와 일치해야 한다.
|
||
THUMBNAIL_COMPOSITION_V: dict = {
|
||
"name": "thumbnail-composition",
|
||
"type": "composition",
|
||
"track": 1,
|
||
"time": 0,
|
||
"elements": [
|
||
{
|
||
"name": "thumb-background",
|
||
"type": "image",
|
||
"track": 1,
|
||
"time": 0,
|
||
"width": "105%",
|
||
"height": "105%",
|
||
"smart_crop": True,
|
||
"color_overlay": "rgba(0,0,0,0.22)",
|
||
},
|
||
{
|
||
"name": "thumb-badge-category",
|
||
"type": "text",
|
||
"track": 2,
|
||
"time": 0,
|
||
"y": "8%",
|
||
"width": "75%",
|
||
"height": "7%",
|
||
"x_alignment": "50%",
|
||
"y_alignment": "50%",
|
||
"font_family": "Pretendard",
|
||
"font_weight": "700",
|
||
"font_size_maximum": "4.8 vmin",
|
||
"letter_spacing": "12%",
|
||
"line_height": "110%",
|
||
"background_color": "#FFFFFF",
|
||
"background_x_padding": "50%",
|
||
"background_y_padding": "65%",
|
||
"background_border_radius": "100%",
|
||
"fill_color": "#1A1A1A",
|
||
},
|
||
{
|
||
"name": "thumb-headline-hook_claim-aspirational",
|
||
"type": "text",
|
||
"track": 3,
|
||
"time": 0,
|
||
"y": "45%",
|
||
"width": "85%",
|
||
"height": "22%",
|
||
"x_alignment": "50%",
|
||
"y_alignment": "50%",
|
||
"font_family": "Pretendard",
|
||
"font_weight": "900",
|
||
"font_size_maximum": "11 vmin",
|
||
"letter_spacing": "-2%",
|
||
"fill_color": "#FFFFFF",
|
||
"stroke_color": "rgba(0,0,0,0.65)",
|
||
"stroke_width": "0.8 vmin",
|
||
"shadow_color": "rgba(0,0,0,0.55)",
|
||
"shadow_blur": "2.5 vmin",
|
||
"shadow_y": "0.5 vmin",
|
||
},
|
||
{
|
||
"name": "thumb-subheadline-selling_point",
|
||
"type": "text",
|
||
"track": 4,
|
||
"time": 0,
|
||
"y": "64%",
|
||
"width": "88%",
|
||
"height": "9%",
|
||
"x_alignment": "50%",
|
||
"y_alignment": "50%",
|
||
"font_family": "Pretendard",
|
||
"font_weight": "500",
|
||
"font_size_maximum": "6 vmin",
|
||
"letter_spacing": "6%",
|
||
"line_height": "130%",
|
||
"fill_color": "#FFFFFF",
|
||
"stroke_color": "rgba(0,0,0,0.55)",
|
||
"stroke_width": "0.4 vmin",
|
||
},
|
||
{
|
||
"name": "thumb-brand-wordmark",
|
||
"type": "text",
|
||
"track": 5,
|
||
"time": 0,
|
||
"y": "89%",
|
||
"width": "75%",
|
||
"height": "9%",
|
||
"x_alignment": "50%",
|
||
"y_alignment": "50%",
|
||
"font_family": "Pretendard",
|
||
"font_weight": "900",
|
||
"font_size_maximum": "7.5 vmin",
|
||
"letter_spacing": "8%",
|
||
"line_height": "120%",
|
||
"fill_color": "#FFFFFF",
|
||
"stroke_color": "rgba(0,0,0,0.6)",
|
||
"stroke_width": "0.5 vmin",
|
||
"shadow_color": "rgba(0,0,0,0.45)",
|
||
"shadow_blur": "1.5 vmin",
|
||
"shadow_y": "0.3 vmin",
|
||
},
|
||
{
|
||
"name": "thumb-hashtag-primary",
|
||
"type": "text",
|
||
"track": 6,
|
||
"time": 0,
|
||
"y": "95.5%",
|
||
"width": "92%",
|
||
"height": "6%",
|
||
"x_alignment": "50%",
|
||
"y_alignment": "50%",
|
||
"font_family": "Pretendard",
|
||
"font_weight": "600",
|
||
"font_size_maximum": "4.2 vmin",
|
||
"letter_spacing": "4%",
|
||
"line_height": "120%",
|
||
"fill_color": "#FFFFFF",
|
||
"stroke_color": "rgba(0,0,0,0.5)",
|
||
"stroke_width": "0.3 vmin",
|
||
},
|
||
],
|
||
}
|
||
|
||
|
||
# 영상 첫 화면 썸네일 오버레이 노출 시간(초). 30fps 기준 3프레임 — SNS 첫 프레임
|
||
# 캡처에는 충분하면서 시청 흐름에는 거의 인지되지 않는 안전 하한선.
|
||
THUMBNAIL_INTRO_DURATION = 0.1
|
||
|
||
# 오버레이가 기존 최상위 트랙(1: 씬, 2: 오디오, 3: 자막, 4: 키워드)과
|
||
# 렌더 직전 append되는 가사 캡션(track 3~4) 위에 오도록 여유 있게 잡은 트랙 번호.
|
||
THUMBNAIL_INTRO_TRACK = 9
|
||
|
||
|
||
def _fill_thumbnail_slots(composition: dict, image_url: str, text_modifications: dict[str, str]) -> None:
|
||
"""썸네일 컴포지션의 이미지/텍스트 슬롯을 in-place로 채웁니다."""
|
||
for elem in composition["elements"]:
|
||
name = elem.get("name")
|
||
if elem["type"] == "image" and name == "thumb-background":
|
||
elem["source"] = image_url
|
||
elif elem["type"] == "text" and name in text_modifications:
|
||
elem["text"] = text_modifications[name]
|
||
|
||
|
||
def build_thumbnail_source(image_url: str, text_modifications: dict[str, str]) -> dict:
|
||
"""썸네일 렌더용 Creatomate source를 조립합니다.
|
||
|
||
Args:
|
||
image_url: 배경 이미지 URL
|
||
text_modifications: make_thumbnail_modification() 반환값 (thumb-* 텍스트 슬롯)
|
||
|
||
Returns:
|
||
1080×1920 정지 이미지 렌더 source 딕셔너리
|
||
"""
|
||
composition = copy.deepcopy(THUMBNAIL_COMPOSITION_V)
|
||
_fill_thumbnail_slots(composition, image_url, text_modifications)
|
||
return {
|
||
"width": 1080,
|
||
"height": 1920,
|
||
"elements": [composition],
|
||
}
|
||
|
||
|
||
def build_thumbnail_intro_element(
|
||
image_url: str,
|
||
text_modifications: dict[str, str],
|
||
duration: float = THUMBNAIL_INTRO_DURATION,
|
||
) -> dict:
|
||
"""영상 첫 화면(SNS 첫 프레임용)에 얹을 썸네일 오버레이 요소를 조립합니다.
|
||
|
||
duration 스케일링(extend_template_duration) 이후, 렌더 요청 직전에
|
||
final_template["source"]["elements"]에 append해야 노출 시간이 그대로 유지된다.
|
||
기존 씬·자막·오디오 타이밍은 건드리지 않는 순수 오버레이이다.
|
||
|
||
Args:
|
||
image_url: 배경 이미지 URL
|
||
text_modifications: make_thumbnail_modification() 반환값 (thumb-* 텍스트 슬롯)
|
||
duration: 오버레이 노출 시간(초)
|
||
|
||
Returns:
|
||
time=0, 최상위 트랙에 배치된 composition 요소 딕셔너리
|
||
"""
|
||
composition = copy.deepcopy(THUMBNAIL_COMPOSITION_V)
|
||
composition["name"] = "thumbnail-intro"
|
||
composition["track"] = THUMBNAIL_INTRO_TRACK
|
||
composition["time"] = 0
|
||
composition["duration"] = duration
|
||
_fill_thumbnail_slots(composition, image_url, text_modifications)
|
||
return composition
|