# -*- coding: utf-8 -*- """P2V 요청/응답 스키마. 응답 형태는 P2V 서버의 잡 JSON 과 최대한 호환되게 유지한다 — 프론트엔드 (o2o-castad-frontend p2vApi.ts) 가 이미 그 형태(artifacts 딕셔너리, stages 등)에 맞춰 작성돼 있어, 필드명을 바꾸면 P2V 직접 호출 → castad 프록시 전환 때 갈아엎어야 할 코드가 배로 늘기 때문이다. 단 `id` 는 castad 잡 id(정수)다 — P2V 잡 id 는 절대 밖으로 내보내지 않는다. 검증 범위·기본값은 여기서만 강제한다. DB 모델에 두면 진실이 두 곳에 생긴다. """ from typing import Literal, Optional from pydantic import BaseModel, Field F1StatusLiteral = Literal[ "queued", "running", "awaiting_review", "archiving", "done", "failed" ] F2StatusLiteral = Literal["queued", "running", "archiving", "done", "failed"] FormatLiteral = Literal["poster", "story", "feed", "square"] # ============================================================================= # 공통 # ============================================================================= class P2vJobCreateResponse(BaseModel): """생성 요청 접수. `id` 로 폴링한다.""" id: int = Field(..., description="castad 잡 ID (P2V 서버 식별자가 아니다)") status: F1StatusLiteral = Field(..., description="접수 직후 상태") poll_interval_seconds: int = Field(..., description="권장 폴링 간격(초)") class P2vJobError(BaseModel): """실패 정보 — P2V 의 {stage, detail} 형태를 그대로 중계한다""" stage: str = Field(..., description="실패한 스테이지") detail: str = Field(..., description="실패 사유") class P2vMetadata(BaseModel): """행사 메타데이터 (검수 대상)""" event_name: str = Field(default="", description="행사명") date_text: str = Field(default="", description="일시 표기") place: str = Field(default="", description="장소") category: Optional[str] = Field(None, description="분류") keywords: Optional[list[str]] = Field(None, description="키워드 목록") region_guess: Optional[str] = Field(None, description="추정 지역") class P2vDeleteResponse(BaseModel): """삭제 결과""" id: int = Field(..., description="삭제한 castad 잡 ID") removed: bool = Field(..., description="삭제 여부") # ============================================================================= # F1 — 무빙 포스터 # ============================================================================= class P2vF1StatusResponse(BaseModel): """F1 잡 상태 (폴링 응답). 진행 중에는 P2V 실시간 값을, archived 이후에는 castad DB 미러를 내려준다. artifacts 값은 전부 Azure Blob 공개 URL 또는 castad 프록시 경로(/p2v/files/...)다. """ id: int = Field(..., description="castad 잡 ID") status: F1StatusLiteral = Field(..., description="진행 상태") stage: Optional[str] = Field(None, description="현재 스테이지 (진행 중에만)") stages: Optional[dict] = Field( None, description="스테이지별 상태 (P2V 원형 그대로 — 진행 중에만)" ) name: Optional[str] = Field(None, description="행사명") narration: Optional[list[str]] = Field(None, description="나레이션 3문장 (검수 대상)") metadata: Optional[P2vMetadata] = Field(None, description="행사 메타데이터 (검수 대상)") motion_elements: Optional[list[str]] = Field(None, description="채택된 모션 요소") duration: Optional[float] = Field(None, description="완성 영상 길이(초)") queue_size: Optional[int] = Field(None, description="P2V 대기열 크기 (진행 중에만)") error: Optional[P2vJobError] = Field(None, description="실패 정보") artifacts: dict[str, str] = Field( default_factory=dict, description="산출물 URL — video/thumbnail 은 Blob, check_jpg 는 프록시 경로", ) class NarrationUpdateRequest(BaseModel): """검수 중 나레이션 수정""" narration: list[str] = Field( ..., min_length=1, max_length=5, description="수정한 나레이션 문장 목록" ) class MetadataUpdateRequest(BaseModel): """검수 중 행사 메타데이터 수정""" event_name: str = Field(..., max_length=200, description="행사명") date_text: str = Field(default="", max_length=100, description="일시 표기") place: str = Field(..., max_length=200, description="장소") class ApproveRequest(BaseModel): """검수 승인/재시도. motions 를 주면 모션 선정을 덮어쓴다""" motions: Optional[list[str]] = Field( None, description="모션 키 목록 (None 이면 서버 선정 유지)" ) # ============================================================================= # F2 — 포스터 스타일링 # ============================================================================= class P2vF2StatusResponse(BaseModel): """F2 잡 상태 (폴링 응답)""" id: int = Field(..., description="castad 잡 ID") status: F2StatusLiteral = Field(..., description="진행 상태") stage: Optional[str] = Field(None, description="현재 스테이지 (진행 중에만)") stages: Optional[dict] = Field(None, description="스테이지별 상태 (진행 중에만)") template_id: Optional[str] = Field(None, description="사용한 템플릿 id") queue_size: Optional[int] = Field(None, description="P2V 대기열 크기 (진행 중에만)") error: Optional[P2vJobError] = Field(None, description="실패 정보") artifacts: dict[str, str] = Field( default_factory=dict, description="산출물 URL — image 는 Blob 공개 URL" ) class F2TemplateResponse(BaseModel): """스타일 템플릿 (P2V 목록 중계 — thumb_url 은 castad 프록시 경로로 재작성됨)""" id: str = Field(..., description="템플릿 id") name_ko: str = Field(..., description="한국어 이름") thumb_url: str = Field(..., description="썸네일 (castad 프록시 경로 /p2v/files/...)") category: str = Field(..., description="카테고리 id") license: str = Field(..., description="배포 등급 (public-domain/internal-only/user-uploaded)") attribution: str = Field(default="", description="출처 표기") license_note: str = Field(default="", description="등급 설명") removable: bool = Field(default=False, description="사용자 레퍼런스 여부 (삭제 가능)") small_ref: bool = Field(default=False, description="저해상 레퍼런스 경고 여부") class F2CategoryResponse(BaseModel): """템플릿 카테고리""" id: str = Field(..., description="카테고리 id") label: str = Field(..., description="표시 이름") class F2FormatResponse(BaseModel): """출력 포맷""" id: str = Field(..., description="포맷 id") label: str = Field(..., description="표시 이름") class F2UploadHintResponse(BaseModel): """사용자 레퍼런스 업로드 안내""" enabled: bool = Field(..., description="업로드 허용 여부") min_long_edge: int = Field(..., description="권장 최소 긴 변(px)") ref_long_edge: Optional[int] = Field(None, description="분석 시 리사이즈 기준(px)") __all__ = [ "F1StatusLiteral", "F2StatusLiteral", "FormatLiteral", "P2vJobCreateResponse", "P2vJobError", "P2vMetadata", "P2vDeleteResponse", "P2vF1StatusResponse", "NarrationUpdateRequest", "MetadataUpdateRequest", "ApproveRequest", "P2vF2StatusResponse", "F2TemplateResponse", "F2CategoryResponse", "F2FormatResponse", "F2UploadHintResponse", ]