"""④ analyze — 검수 게이트에 올릴 재료를 만든다. 여기서 사람이 움직일 요소를 확정하고, 그 승인이 14크레딧 지출을 확정한다. 실제 프롬프트는 ⑤ motion에서 사람이 고른 키로 다시 쓴다. """ import re from pathlib import Path from PIL import Image from models.analysis import FixedLayer, MotionChoice, PosterAnalysis from services.motion import plan_motion from utils.higgsfield import KLING_3_0 from utils.image import review_grid_overlay MOTION_LABELS = { "ray": "광선", "snow": "눈", "fog": "안개", "glow": "발광", "light": "조명", "star": "별", "cloud": "구름", "smoke": "연기", "water": "물", "wave": "파도", "moon": "달", "sun": "해", "flag": "깃발", "foliage": "나뭇잎", "vessel": "배", "crowd": "사람 무리", "wheel": "바퀴·관람차", "firework": "불꽃놀이", } FIXED_LABELS = {"title": "제목", "logo": "로고", "date": "일시", "person": "인물", "sponsor": "후원 바"} # veo·seedance가 디즈니 IP 작품을 거부한 이력이 있어, 크레딧을 쓰기 전에 경고만 띄운다. # 한국어 표기 몇 개만 보는 어림짐작이라 판정이 아니다. IP_RISK_PATTERN = re.compile(r"디즈니|겨울왕국|라이온킹|알라딘|마블") def motion_choices(kept, dropped) -> list[MotionChoice]: """VLM이 고른 것은 켜고, 확신 없어 탈락한 것은 꺼서 함께 보여준다.""" chosen = {item.motion for item in kept} choices = [MotionChoice(key=item.motion, label=MOTION_LABELS.get(item.motion, item.motion), what=item.what, on=True) for item in kept] choices += [MotionChoice(key=item.motion, label=MOTION_LABELS.get(item.motion, item.motion), what=item.what, on=False) for item in dropped if item.motion not in chosen] return choices async def analyze_poster(poster: Path | Image.Image, title: str = "") -> PosterAnalysis: source = (Image.open(poster) if isinstance(poster, Path) else poster).convert("RGB") plan = await plan_motion(source) return PosterAnalysis( grid=review_grid_overlay(source), movable=motion_choices(plan.kept, plan.dropped), fixed=[FixedLayer(key=key, label=label) for key, label in FIXED_LABELS.items()], model=KLING_3_0, ip_risk=bool(IP_RISK_PATTERN.search(title)), has_qr=plan.has_qr, plan=plan, )