playreel/backend/tables/task.py
2026-09-15 16:00:54 +09:00

196 lines
8.6 KiB
Python

"""두 파이프라인의 잡 테이블
단계별 산출물을 컬럼으로 들고 있음
이미지·오디오·영상은 blob에 올린 뒤 URL만 저장
"""
import uuid
from datetime import datetime
from sqlalchemy import JSON, Boolean, DateTime, Enum, Float, Integer, String, Text, func
from sqlalchemy.orm import Mapped, mapped_column
from models.pipeline_state import PlayreelState, PosterAliveState
from tables.user import UNASSIGNED_USER_ID, USER_ID_LENGTH
from utils.database import Base
URL_LENGTH = 512
TASK_ID_LENGTH = 36
def new_task_id() -> str:
return str(uuid.uuid4())
def state_column(enum_type):
return Enum(enum_type, values_callable=lambda e: [member.value for member in e])
class TaskBase(Base):
"""두 테이블이 같이 쓰는 실행 메타
state는 진행도, status는 그 시점의 실행 상태로 축이 다름
"""
__abstract__ = True
id: Mapped[str] = mapped_column(String(TASK_ID_LENGTH), primary_key=True,
default=new_task_id)
# 만든 사람의 구글 sub. 모르면 UNASSIGNED_USER_ID가 들어간다
user_id: Mapped[str] = mapped_column(String(USER_ID_LENGTH), index=True,
default=UNASSIGNED_USER_ID)
name: Mapped[str] = mapped_column(String(255), default="")
status: Mapped[str] = mapped_column(String(32), default="queued", index=True)
# 스테퍼가 그리는 단계별 시각
# {stage: {"status": ..., "started": ..., "ended": ...}}
stage_timings: Mapped[dict] = mapped_column(JSON, default=dict)
error_stage: Mapped[str | None] = mapped_column(String(32))
error_detail: Mapped[str | None] = mapped_column(Text)
credits_used: Mapped[float] = mapped_column(Float, default=0.0)
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now(),
onupdate=func.now())
class PosterAliveTask(TaskBase):
"""포스터 한 장 → 8초 숏폼"""
__tablename__ = "poster_alive_task"
state: Mapped[PosterAliveState] = mapped_column(
state_column(PosterAliveState), default=PosterAliveState.DETECT, index=True)
# 검수 게이트를 건너뛰고 끝까지 돌린다
skip_review: Mapped[bool] = mapped_column(Boolean, default=False)
# 입력
poster_url: Mapped[str] = mapped_column(String(URL_LENGTH))
poster_width: Mapped[int | None] = mapped_column(Integer)
poster_height: Mapped[int | None] = mapped_column(Integer)
# i2v가 명시적으로 거절하는 하드 게이트
is_low_resolution: Mapped[bool] = mapped_column(Boolean, default=False)
# ① detect
detect_regions: Mapped[dict | None] = mapped_column(JSON) # Regions
detect_grid_url: Mapped[str | None] = mapped_column(String(URL_LENGTH))
detect_check_url: Mapped[str | None] = mapped_column(String(URL_LENGTH))
# ② narration_text
narration_lines: Mapped[list | None] = mapped_column(JSON) # 3문장
narration_voice: Mapped[str | None] = mapped_column(String(32))
bgm_style: Mapped[dict | None] = mapped_column(JSON) # BgmStyle
poster_metadata: Mapped[dict | None] = mapped_column(JSON) # NarrationMetadata
# ③ motion
motion_plan: Mapped[dict | None] = mapped_column(JSON) # MotionPlan
# ④ tts
narration_timeline: Mapped[dict | None] = mapped_column(JSON) # NarrationTimeline
narration_audio_url: Mapped[str | None] = mapped_column(String(URL_LENGTH))
# ⑤ bgm
bgm_audio_url: Mapped[str | None] = mapped_column(String(URL_LENGTH))
bgm_task_id: Mapped[str | None] = mapped_column(String(64))
bgm_target_seconds: Mapped[int | None] = mapped_column(Integer)
bgm_duration: Mapped[float | None] = mapped_column(Float)
# ⑥ i2v
clip_url: Mapped[str | None] = mapped_column(String(URL_LENGTH))
clip_model: Mapped[str | None] = mapped_column(String(64))
clip_credits: Mapped[float | None] = mapped_column(Float)
title_gate: Mapped[dict | None] = mapped_column(JSON) # TitleGateVerdict
# ⑦ render
video_url: Mapped[str | None] = mapped_column(String(URL_LENGTH))
thumbnail_url: Mapped[str | None] = mapped_column(String(URL_LENGTH))
video_duration: Mapped[float | None] = mapped_column(Float)
video_frames: Mapped[int | None] = mapped_column(Integer)
video_width: Mapped[int | None] = mapped_column(Integer)
video_height: Mapped[int | None] = mapped_column(Integer)
class PlayreelTask(TaskBase):
"""공연 상품페이지 URL → 30초 롱컷"""
__tablename__ = "playreel_task"
state: Mapped[PlayreelState] = mapped_column(
state_column(PlayreelState), default=PlayreelState.FETCH, index=True)
# 검수 대기 중인 게이트. 승인하면 비워짐
gate: Mapped[str | None] = mapped_column(String(32))
# 최종 승인마다 올라감. 재작업은 새 버전이 됨
version: Mapped[int] = mapped_column(Integer, default=0)
# 입력
source_url: Mapped[str] = mapped_column(String(URL_LENGTH))
goods_id: Mapped[str] = mapped_column(String(32), index=True)
slug: Mapped[str] = mapped_column(String(64))
# ① fetch
nol_meta: Mapped[dict | None] = mapped_column(JSON) # NolMeta
poster_url: Mapped[str | None] = mapped_column(String(URL_LENGTH))
# 750px이면 검수 화면이 저해상 경고를 띄운다
poster_width: Mapped[int | None] = mapped_column(Integer)
poster_height: Mapped[int | None] = mapped_column(Integer)
detail_image_urls: Mapped[list | None] = mapped_column(JSON)
fetch_failed_urls: Mapped[list | None] = mapped_column(JSON)
# ② split
# [{name, source, index, y0, y1, tag, url, selected}]
detail_sections: Mapped[list | None] = mapped_column(JSON)
section_sheet_url: Mapped[str | None] = mapped_column(String(URL_LENGTH))
# ③ upscale
upscaled_poster_url: Mapped[str | None] = mapped_column(String(URL_LENGTH))
upscale_credits: Mapped[int | None] = mapped_column(Integer)
upscale_variant: Mapped[str | None] = mapped_column(String(64))
# ④ analyze
analysis_grid_url: Mapped[str | None] = mapped_column(String(URL_LENGTH))
movable_elements: Mapped[list | None] = mapped_column(JSON) # [MotionChoice]
fixed_layers: Mapped[list | None] = mapped_column(JSON) # [FixedLayer]
i2v_model: Mapped[str | None] = mapped_column(String(64))
has_ip_risk: Mapped[bool | None] = mapped_column(Boolean)
has_qr: Mapped[bool | None] = mapped_column(Boolean)
# ⑤ motion
motion_plan: Mapped[dict | None] = mapped_column(JSON) # MotionPlan
# ⑥ narration
cast_extraction: Mapped[dict | None] = mapped_column(JSON) # CastExtraction
narration_lines: Mapped[list | None] = mapped_column(JSON) # [{slot, text}]
# ⑦ tts
narration_timeline: Mapped[dict | None] = mapped_column(JSON) # NarrationTimeline
narration_audio_url: Mapped[str | None] = mapped_column(String(URL_LENGTH))
narration_voice_id: Mapped[str | None] = mapped_column(String(64))
# ⑧ bgm
bgm_audio_url: Mapped[str | None] = mapped_column(String(URL_LENGTH))
bgm_task_id: Mapped[str | None] = mapped_column(String(64))
bgm_target_seconds: Mapped[int | None] = mapped_column(Integer)
bgm_duration: Mapped[float | None] = mapped_column(Float)
# ⑨ i2v
clip_url: Mapped[str | None] = mapped_column(String(URL_LENGTH))
clip_frames_url: Mapped[str | None] = mapped_column(String(URL_LENGTH))
clip_model: Mapped[str | None] = mapped_column(String(64))
clip_credits: Mapped[float | None] = mapped_column(Float)
title_gate: Mapped[dict | None] = mapped_column(JSON) # TitleGateVerdict
# ⑩ hybrid
layer_config: Mapped[dict | None] = mapped_column(JSON) # LayerConfig
hybrid_clip_url: Mapped[str | None] = mapped_column(String(URL_LENGTH))
hybrid_proof_url: Mapped[str | None] = mapped_column(String(URL_LENGTH))
hybrid_frames: Mapped[int | None] = mapped_column(Integer)
hybrid_duration: Mapped[float | None] = mapped_column(Float)
# ⑪ compose
video_url: Mapped[str | None] = mapped_column(String(URL_LENGTH))
scene_plan: Mapped[dict | None] = mapped_column(JSON) # LongcutPlan
compose_proof_url: Mapped[str | None] = mapped_column(String(URL_LENGTH))
video_duration: Mapped[float | None] = mapped_column(Float)
video_frames: Mapped[int | None] = mapped_column(Integer)
# ⑫ review
review_checks: Mapped[list | None] = mapped_column(JSON) # [{key, label, ok}]