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