""" Video API Schemas 영상 생성 관련 Pydantic 스키마를 정의합니다. """ from datetime import datetime from typing import Any, Dict, Literal, Optional from pydantic import BaseModel, ConfigDict, Field # ============================================================================= # Response Schemas # ============================================================================= class GenerateVideoResponse(BaseModel): """영상 생성 응답 스키마 Usage: GET /video/generate/{task_id} Returns the task IDs for tracking video generation. """ model_config = ConfigDict( json_schema_extra={ "example": { "success": True, "task_id": "0694b716-dbff-7219-8000-d08cb5fce431", "creatomate_render_id": "render-id-123456", "message": "영상 생성 요청이 접수되었습니다. creatomate_render_id로 상태를 조회하세요.", "error_message": None, } } ) success: bool = Field(..., description="요청 성공 여부") status: Optional[str] = Field(None, description="처리 상태 (subtitle_pending: 자막 미완료, completed: 정상 접수)") task_id: Optional[str] = Field(None, description="내부 작업 ID (Project task_id)") creatomate_render_id: Optional[str] = Field(None, description="Creatomate 렌더 ID") message: str = Field(..., description="응답 메시지") error_message: Optional[str] = Field(None, description="에러 메시지 (실패 시)") class VideoRenderData(BaseModel): """Creatomate 렌더링 결과 데이터""" id: Optional[str] = Field(None, description="렌더 ID") status: Optional[str] = Field(None, description="렌더 상태") url: Optional[str] = Field(None, description="영상 URL") snapshot_url: Optional[str] = Field(None, description="스냅샷 URL") video_id: Optional[int] = Field(None, description="Video id(DB)") class PollingVideoResponse(BaseModel): """영상 생성 상태 조회 응답 스키마 Usage: GET /video/status/{creatomate_render_id} Creatomate API 작업 상태를 조회합니다. Note: 상태 값: - planned: 예약됨 - waiting: 대기 중 - transcribing: 트랜스크립션 중 - rendering: 렌더링 중 - succeeded: 성공 - failed: 실패 Example Response (Success): { "success": true, "status": "succeeded", "message": "영상 생성이 완료되었습니다.", "render_data": { "id": "render-id", "status": "succeeded", "url": "https://...", "snapshot_url": "https://..." }, "raw_response": {...}, "error_message": null } """ success: bool = Field(..., description="조회 성공 여부") status: Optional[str] = Field( None, description="작업 상태 (planned, waiting, rendering, succeeded, failed)" ) message: str = Field(..., description="상태 메시지") render_data: Optional[VideoRenderData] = Field(None, description="렌더링 결과 데이터") raw_response: Optional[Dict[str, Any]] = Field(None, description="Creatomate API 원본 응답") error_message: Optional[str] = Field(None, description="에러 메시지 (실패 시)") class DownloadVideoResponse(BaseModel): """영상 다운로드 응답 스키마 Usage: GET /video/download/{task_id} Polls for video completion and returns project info with video URL. Note: 상태 값: - processing: 영상 생성 진행 중 (result_movie_url은 null) - completed: 영상 생성 완료 (result_movie_url 포함) - failed: 영상 생성 실패 - not_found: task_id에 해당하는 Video 없음 - error: 조회 중 오류 발생 Example Response (Completed): { "success": true, "status": "completed", "message": "영상 다운로드가 완료되었습니다.", "store_name": "스테이 머뭄", "region": "군산", "task_id": "019123ab-cdef-7890-abcd-ef1234567890", "result_movie_url": "http://localhost:8000/media/2025-01-15/video.mp4", "created_at": "2025-01-15T12:00:00", "error_message": null } """ success: bool = Field(..., description="다운로드 성공 여부") status: str = Field(..., description="처리 상태 (processing, completed, failed, not_found, error)") message: str = Field(..., description="응답 메시지") store_name: Optional[str] = Field(None, description="업체명") region: Optional[str] = Field(None, description="지역명") task_id: Optional[str] = Field(None, description="작업 고유 식별자") result_movie_url: Optional[str] = Field(None, description="영상 결과 URL") created_at: Optional[datetime] = Field(None, description="생성 일시") error_message: Optional[str] = Field(None, description="에러 메시지 (실패 시)") class VideoListItem(BaseModel): """영상 목록 아이템 스키마 Usage: GET /videos 응답의 개별 영상 정보 Example: { "video_id": 1, "store_name": "스테이 머뭄", "region": "군산", "task_id": "019123ab-cdef-7890-abcd-ef1234567890", "result_movie_url": "http://localhost:8000/media/2025-01-15/video.mp4", "created_at": "2025-01-15T12:00:00" } """ # ⚠️ `video_id` 는 type 안에서만 유일하다 — `video.id` 와 `ssul_content.id` 는 # 각각 1부터 시작하는 독립 시퀀스다. 식별·삭제·상세 열기 모두 # **`(type, video_id)` 쌍**으로 다뤄야 한다. # 특히 `DELETE /archive/videos/{id}` 는 `Video.id` 로 지우므로, # 썰박스 항목의 id 를 그대로 넘기면 **엉뚱한 ADO2 영상이 삭제된다.** type: Literal["video", "ssul"] = Field( default="video", description="콘텐츠 종류 (video: ADO2 영상, ssul: 썰박스)", ) video_id: int = Field(..., description="콘텐츠 고유 ID (type 안에서만 유일)") store_name: Optional[str] = Field(None, description="업체명") region: Optional[str] = Field(None, description="지역명") task_id: str = Field( default="", description="작업 고유 식별자 (ADO2 전용. 썰박스는 개념이 없어 빈 문자열)", ) result_movie_url: Optional[str] = Field(None, description="영상 결과 URL") created_at: Optional[datetime] = Field(None, description="생성 일시") like_count: int = Field(0, description="좋아요 수") comment_count: int = Field(0, description="댓글 수 (대댓글 포함)") class VideoThumbnailItem(BaseModel): """ADO2 콘텐츠 갤러리용 최소 영상 정보 (썸네일 표시 + 상세 페이지 이동용) Usage: GET /video/all 응답의 개별 영상 정보 """ # ⚠️ `video_id` 는 종류 안에서만 유일하다. `video.id` 와 `ssul_content.id` 가 # **둘 다 1부터 시작**하므로 식별자는 반드시 `(type, video_id)` 쌍으로 다뤄야 한다. # 한 곳이라도 id 만 쓰면 다른 종류의 콘텐츠가 열린다. type: Literal["video", "ssul"] = Field( default="video", description="콘텐츠 종류 (video: ADO2 영상, ssul: 썰박스). video_id 와 쌍으로 식별한다", ) video_id: int = Field(..., description="콘텐츠 고유 ID (type 안에서만 유일)") store_name: str = Field(..., description="업체명") result_movie_url: str = Field(..., description="영상 URL — 프론트에서