o2o-castad-backend/app/utils/thumbnail.py

198 lines
6.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

"""세로형 썸네일 정지 이미지 렌더용 컴포지션 및 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