192 lines
8.3 KiB
Python
192 lines
8.3 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 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)
|
|
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}]
|