From 95d90dcb50714647cc74070b2707880344fd78dc Mon Sep 17 00:00:00 2001 From: bluebamus Date: Mon, 29 Dec 2025 12:15:44 +0900 Subject: [PATCH] =?UTF-8?q?=EC=98=81=EC=83=81=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=EC=8B=9C=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20url=20=EC=A0=84?= =?UTF-8?q?=EC=86=A1=20->=20task=5Fid=EB=A1=9C=20=EC=A7=81=EC=A0=91=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/video/api/routers/v1/video.py | 101 +++++++++++++++--------------- app/video/schemas/video_schema.py | 77 +++++------------------ 2 files changed, 66 insertions(+), 112 deletions(-) diff --git a/app/video/api/routers/v1/video.py b/app/video/api/routers/v1/video.py index f09e574..8cd1e79 100644 --- a/app/video/api/routers/v1/video.py +++ b/app/video/api/routers/v1/video.py @@ -14,7 +14,9 @@ Video API Router app.include_router(router, prefix="/api/v1") """ -from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException +from typing import Literal + +from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Query from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession @@ -23,13 +25,12 @@ from app.dependencies.pagination import ( PaginationParams, get_pagination_params, ) -from app.home.models import Project +from app.home.models import Image, Project from app.lyric.models import Lyric from app.song.models import Song from app.video.models import Video from app.video.schemas.video_schema import ( DownloadVideoResponse, - GenerateVideoRequest, GenerateVideoResponse, PollingVideoResponse, VideoListItem, @@ -43,23 +44,23 @@ from app.utils.pagination import PaginatedResponse router = APIRouter(prefix="/video", tags=["video"]) -@router.post( +@router.get( "/generate/{task_id}", summary="영상 생성 요청", description=""" Creatomate API를 통해 영상 생성을 요청합니다. ## 경로 파라미터 -- **task_id**: Project/Lyric/Song의 task_id (필수) - 연관된 프로젝트, 가사, 노래를 조회하는 데 사용 +- **task_id**: Project/Lyric/Song/Image의 task_id (필수) - 연관된 프로젝트, 가사, 노래, 이미지를 조회하는 데 사용 -## 요청 필드 +## 쿼리 파라미터 - **orientation**: 영상 방향 (horizontal: 가로형, vertical: 세로형, 기본값: vertical) - 선택 -- **image_urls**: 영상에 사용할 이미지 URL 목록 (필수) -## 자동 조회 정보 (Song 테이블에서 task_id 기준 가장 최근 생성된 노래 사용) -- **music_url**: song_result_url 사용 -- **duration**: 노래의 duration 사용 -- **lyrics**: song_prompt (가사) 사용 +## 자동 조회 정보 +- **image_urls**: Image 테이블에서 task_id로 조회 (img_order 순서로 정렬) +- **music_url**: Song 테이블의 song_result_url 사용 +- **duration**: Song 테이블의 duration 사용 +- **lyrics**: Song 테이블의 song_prompt (가사) 사용 ## 반환 정보 - **success**: 요청 성공 여부 @@ -69,33 +70,12 @@ Creatomate API를 통해 영상 생성을 요청합니다. ## 사용 예시 ``` -POST /video/generate/019123ab-cdef-7890-abcd-ef1234567890 -{ - "image_urls": [ - "https://naverbooking-phinf.pstatic.net/20240514_189/1715688030436xT14o_JPEG/1.jpg", - "https://naverbooking-phinf.pstatic.net/20240514_48/1715688030574wTtQd_JPEG/2.jpg", - "https://naverbooking-phinf.pstatic.net/20240514_92/17156880307484bvpH_JPEG/3.jpg", - "https://naverbooking-phinf.pstatic.net/20240514_7/1715688031000y8Y5q_JPEG/4.jpg", - "https://naverbooking-phinf.pstatic.net/20240514_259/17156880311809wCnY_JPEG/5.jpg", - "https://naverbooking-phinf.pstatic.net/20240514_64/1715688031601oGNsV_JPEG/6.jpg", - "https://naverbooking-phinf.pstatic.net/20240514_175/1715688031657oXc7l_JPEG/7.jpg", - "https://naverbooking-phinf.pstatic.net/20240514_192/1715688031798MbFDj_JPEG/8.jpg", - "https://naverbooking-phinf.pstatic.net/20240514_205/17156880318681JLwX_JPEG/9.jpg", - "https://naverbooking-phinf.pstatic.net/20240514_142/1715688031946hhxHz_JPEG/10.jpg" - ] -} -``` - -## 가로형 영상 생성 예시 -``` -POST /video/generate/019123ab-cdef-7890-abcd-ef1234567890 -{ - "orientation": "horizontal", - "image_urls": [...] -} +GET /video/generate/0694b716-dbff-7219-8000-d08cb5fce431 +GET /video/generate/0694b716-dbff-7219-8000-d08cb5fce431?orientation=horizontal ``` ## 참고 +- 이미지는 task_id로 Image 테이블에서 자동 조회됩니다 (img_order 순서). - 배경 음악(music_url), 영상 길이(duration), 가사(lyrics)는 task_id로 Song 테이블을 조회하여 자동으로 가져옵니다. - 같은 task_id로 여러 Song이 있을 경우 **가장 최근 생성된 노래**를 사용합니다. - Song의 song_result_url과 song_prompt가 있어야 영상 생성이 가능합니다. @@ -105,24 +85,27 @@ POST /video/generate/019123ab-cdef-7890-abcd-ef1234567890 response_model=GenerateVideoResponse, responses={ 200: {"description": "영상 생성 요청 성공"}, - 400: {"description": "Song의 음악 URL 또는 가사(song_prompt)가 없음"}, - 404: {"description": "Project, Lyric 또는 Song을 찾을 수 없음"}, + 400: {"description": "Song의 음악 URL, 가사(song_prompt) 또는 이미지가 없음"}, + 404: {"description": "Project, Lyric, Song 또는 Image를 찾을 수 없음"}, 500: {"description": "영상 생성 요청 실패"}, }, ) async def generate_video( task_id: str, - request_body: GenerateVideoRequest, + orientation: Literal["horizontal", "vertical"] = Query( + default="vertical", + description="영상 방향 (horizontal: 가로형, vertical: 세로형)", + ), session: AsyncSession = Depends(get_session), ) -> GenerateVideoResponse: """Creatomate API를 통해 영상을 생성합니다. - 1. task_id로 Project, Lyric, Song 조회 + 1. task_id로 Project, Lyric, Song, Image 조회 2. Video 테이블에 초기 데이터 저장 (status: processing) 3. Creatomate API 호출 (orientation에 따른 템플릿 자동 선택) 4. creatomate_render_id 업데이트 후 응답 반환 """ - print(f"[generate_video] START - task_id: {task_id}, orientation: {request_body.orientation}") + print(f"[generate_video] START - task_id: {task_id}, orientation: {orientation}") try: # 1. task_id로 Project 조회 project_result = await session.execute( @@ -189,7 +172,25 @@ async def generate_video( print(f"[generate_video] Song found - song_id: {song.id}, task_id: {task_id}, duration: {song.duration}") print(f"[generate_video] Music URL (from DB): {music_url}, Song duration: {song.duration}, Lyrics length: {len(lyrics)}") - # 4. Video 테이블에 초기 데이터 저장 + # 4. task_id로 Image 조회 (img_order 순서로 정렬) + image_result = await session.execute( + select(Image) + .where(Image.task_id == task_id) + .order_by(Image.img_order.asc()) + ) + images = image_result.scalars().all() + + if not images: + print(f"[generate_video] Image NOT FOUND - task_id: {task_id}") + raise HTTPException( + status_code=404, + detail=f"task_id '{task_id}'에 해당하는 이미지를 찾을 수 없습니다.", + ) + + image_urls = [img.img_url for img in images] + print(f"[generate_video] Images found - task_id: {task_id}, count: {len(image_urls)}") + + # 5. Video 테이블에 초기 데이터 저장 video = Video( project_id=project.id, lyric_id=lyric.id, @@ -202,29 +203,29 @@ async def generate_video( await session.flush() # ID 생성을 위해 flush print(f"[generate_video] Video saved (processing) - task_id: {task_id}") - # 5. Creatomate API 호출 (POC 패턴 적용) + # 6. Creatomate API 호출 (POC 패턴 적용) print(f"[generate_video] Creatomate API generation started - task_id: {task_id}") # orientation에 따른 템플릿 선택, duration은 Song에서 가져옴 (없으면 config 기본값 사용) creatomate_service = CreatomateService( - orientation=request_body.orientation, + orientation=orientation, target_duration=song.duration, # Song의 duration 사용 (None이면 config 기본값) ) print(f"[generate_video] Using template_id: {creatomate_service.template_id}, duration: {creatomate_service.target_duration} (song duration: {song.duration})") - # 5-1. 템플릿 조회 (비동기, CreatomateService에서 orientation에 맞는 template_id 사용) + # 6-1. 템플릿 조회 (비동기, CreatomateService에서 orientation에 맞는 template_id 사용) template = await creatomate_service.get_one_template_data_async(creatomate_service.template_id) print(f"[generate_video] Template fetched - task_id: {task_id}") - # 5-2. elements에서 리소스 매핑 생성 (music_url, lyrics는 DB에서 조회한 값 사용) + # 6-2. elements에서 리소스 매핑 생성 (music_url, lyrics는 DB에서 조회한 값 사용) modifications = creatomate_service.elements_connect_resource_blackbox( elements=template["source"]["elements"], - image_url_list=request_body.image_urls, + image_url_list=image_urls, lyric=lyrics, music_url=music_url, ) print(f"[generate_video] Modifications created - task_id: {task_id}") - # 5-3. elements 수정 + # 6-3. elements 수정 new_elements = creatomate_service.modify_element( template["source"]["elements"], modifications, @@ -232,14 +233,14 @@ async def generate_video( template["source"]["elements"] = new_elements print(f"[generate_video] Elements modified - task_id: {task_id}") - # 5-4. duration 확장 (target_duration: 영상 길이) + # 6-4. duration 확장 (target_duration: 영상 길이) final_template = creatomate_service.extend_template_duration( template, creatomate_service.target_duration, ) print(f"[generate_video] Duration extended to {creatomate_service.target_duration}s - task_id: {task_id}") - # 5-5. 커스텀 렌더링 요청 (비동기) + # 6-5. 커스텀 렌더링 요청 (비동기) render_response = await creatomate_service.make_creatomate_custom_call_async( final_template["source"], ) @@ -253,7 +254,7 @@ async def generate_video( else: creatomate_render_id = None - # 6. creatomate_render_id 업데이트 + # 7. creatomate_render_id 업데이트 video.creatomate_render_id = creatomate_render_id await session.commit() print(f"[generate_video] SUCCESS - task_id: {task_id}, creatomate_render_id: {creatomate_render_id}") diff --git a/app/video/schemas/video_schema.py b/app/video/schemas/video_schema.py index 3d3dbc1..e3f31eb 100644 --- a/app/video/schemas/video_schema.py +++ b/app/video/schemas/video_schema.py @@ -5,59 +5,9 @@ Video API Schemas """ from datetime import datetime -from typing import Any, Dict, List, Literal, Optional +from typing import Any, Dict, Literal, Optional -from pydantic import BaseModel, Field - - -# ============================================================================= -# Request Schemas -# ============================================================================= - - -class GenerateVideoRequest(BaseModel): - """영상 생성 요청 스키마 - - Usage: - POST /video/generate/{task_id} - Request body for generating a video via Creatomate API. - - Note: - - music_url, duration, lyrics(song_prompt)는 task_id로 Song 테이블에서 자동 조회됩니다. - - 같은 task_id로 여러 Song이 있을 경우 가장 최근 생성된 것을 사용합니다. - - Example Request: - { - "orientation": "vertical", - "image_urls": ["https://...", "https://..."] - } - """ - - model_config = { - "json_schema_extra": { - "example": { - "orientation": "vertical", - "image_urls": [ - "https://naverbooking-phinf.pstatic.net/20240514_189/1715688030436xT14o_JPEG/1.jpg", - "https://naverbooking-phinf.pstatic.net/20240514_48/1715688030574wTtQd_JPEG/2.jpg", - "https://naverbooking-phinf.pstatic.net/20240514_92/17156880307484bvpH_JPEG/3.jpg", - "https://naverbooking-phinf.pstatic.net/20240514_7/1715688031000y8Y5q_JPEG/4.jpg", - "https://naverbooking-phinf.pstatic.net/20240514_259/17156880311809wCnY_JPEG/5.jpg", - "https://naverbooking-phinf.pstatic.net/20240514_64/1715688031601oGNsV_JPEG/6.jpg", - "https://naverbooking-phinf.pstatic.net/20240514_175/1715688031657oXc7l_JPEG/7.jpg", - "https://naverbooking-phinf.pstatic.net/20240514_192/1715688031798MbFDj_JPEG/8.jpg", - "https://naverbooking-phinf.pstatic.net/20240514_205/17156880318681JLwX_JPEG/9.jpg", - "https://naverbooking-phinf.pstatic.net/20240514_142/1715688031946hhxHz_JPEG/10.jpg", - ], - } - } - } - - orientation: Literal["horizontal", "vertical"] = Field( - default="vertical", - description="영상 방향 (horizontal: 가로형, vertical: 세로형, 기본값: vertical)", - ) - image_urls: List[str] = Field(..., description="영상에 사용할 이미지 URL 목록") +from pydantic import BaseModel, ConfigDict, Field # ============================================================================= @@ -69,19 +19,22 @@ class GenerateVideoResponse(BaseModel): """영상 생성 응답 스키마 Usage: - POST /video/generate/{task_id} + GET /video/generate/{task_id} Returns the task IDs for tracking video generation. - - Example Response (Success): - { - "success": true, - "task_id": "019123ab-cdef-7890-abcd-ef1234567890", - "creatomate_render_id": "render-id-123", - "message": "영상 생성 요청이 접수되었습니다.", - "error_message": null - } """ + 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="요청 성공 여부") task_id: Optional[str] = Field(None, description="내부 작업 ID (Project task_id)") creatomate_render_id: Optional[str] = Field(None, description="Creatomate 렌더 ID")