o2o-castad-backend/app/p2v/schemas/p2v_schema.py
2026-08-26 13:52:18 +09:00

191 lines
7.6 KiB
Python

# -*- 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",
]