playreel/backend/models/longcut.py
Haewon Kam 0e53fa294b fix(compose): 상세페이지 스크롤을 롱이미지로 되돌린다
조각을 화면에 얹는 방식이라 스크롤이 한 번도 일어나지 않았다.
심규선 상세페이지 실측 — 19조각 중 1920px를 넘는 것이 0개라
scroll_scene의 span이 항상 0이었다. 화면의 13~44%만 조각이고
나머지는 블러 포스터로 남았으며, 같은 정지컷이 6초 넘게 이어졌다.

정본(tech_spec §5)에 있던 extend_to_height가 이식에서 빠진 것이 원인이다.
그것이 없으니 조각이 짧을 때 블러 배경에 상단 정렬로 얹는 수밖에 없었다.

- to_surface: 롱이미지를 폭 1080으로. trim_blank를 태우지 않는다 —
  위를 깎으면 섹션 y좌표가 전부 어긋난다
- extend_to_height + blank_band: 글자 없는 배경 띠를 거울 타일링,
  이음매 페더 40px. 프레임보다 짧아도 풀프레임을 채운다
- scroll_scene: 배경 합성 없이 surface.crop() — 블러가 섞일 여지를 없앤다
- build_scene_plan: 섹션은 훑기 시작점, 큐 길이가 훑는 거리.
  섹션이 떨어지면 앞 씬이 멈춘 자리에서 이어 읽는다(커서)
- SCROLL_TAG_PRIORITY에서 notice·crossbanner 제거 — 약관을 읽히는 영상이 아니다
- final_checks: 씬 종류가 scroll이라고 적힌 것이 아니라 실제 이동 거리를 본다.
  이 검사가 물러서 0픽셀 스크롤이 게이트를 통과했다

실측(심규선 자산 전체 합성, 569프레임 23.72초):
  콘텐츠 비율 t=8s 23%→99% · t=11s 13%→92% · t=17s 44%→99%
  프레임 간 변화량 0.01(정지) → 22~31

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 15:24:58 +09:00

40 lines
1.2 KiB
Python

from dataclasses import dataclass
from typing import Literal
from PIL import Image
from pydantic import BaseModel
SceneKind = Literal["clip", "stills", "scroll", "poster"]
class Scene(BaseModel):
"""나레이션 큐 하나에 대응하는 화면."""
cue: int
kind: SceneKind
section: str | None = None # scroll 시작점이 된 섹션 이름
tag: str | None = None # 그 섹션의 태그. 왜 골랐는지 남긴다
# scroll은 조각이 아니라 롱이미지 위에서 일어난다. 아래 셋이 그 좌표다
source: str | None = None # 훑을 롱이미지 (detail_01 …)
y0: float | None = None # 시작 위치. 롱이미지 높이 대비 0~1
y1: float | None = None # 끝 위치
class LongcutPlan(BaseModel):
scenes: list[Scene]
class EndBandInfo(BaseModel):
"""엔드밴드 두 줄. 위가 공연명, 아래가 일시·장소."""
title: str
detail: str
@dataclass
class LongcutResult:
video: bytes
thumbnail: bytes # 첫 프레임. 0초가 포스터 원본이라 그대로 정지컷이 된다
duration: float
frames: int
plan: LongcutPlan
proof: Image.Image | None = None