영상 생성시 이미지 url 전송 -> task_id로 직접 검색으로 변경

insta
bluebamus 2025-12-29 12:15:44 +09:00
parent c6d9edbb42
commit 95d90dcb50
2 changed files with 66 additions and 112 deletions

View File

@ -14,7 +14,9 @@ Video API Router
app.include_router(router, prefix="/api/v1") 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 import func, select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@ -23,13 +25,12 @@ from app.dependencies.pagination import (
PaginationParams, PaginationParams,
get_pagination_params, get_pagination_params,
) )
from app.home.models import Project from app.home.models import Image, Project
from app.lyric.models import Lyric from app.lyric.models import Lyric
from app.song.models import Song from app.song.models import Song
from app.video.models import Video from app.video.models import Video
from app.video.schemas.video_schema import ( from app.video.schemas.video_schema import (
DownloadVideoResponse, DownloadVideoResponse,
GenerateVideoRequest,
GenerateVideoResponse, GenerateVideoResponse,
PollingVideoResponse, PollingVideoResponse,
VideoListItem, VideoListItem,
@ -43,23 +44,23 @@ from app.utils.pagination import PaginatedResponse
router = APIRouter(prefix="/video", tags=["video"]) router = APIRouter(prefix="/video", tags=["video"])
@router.post( @router.get(
"/generate/{task_id}", "/generate/{task_id}",
summary="영상 생성 요청", summary="영상 생성 요청",
description=""" description="""
Creatomate API를 통해 영상 생성을 요청합니다. Creatomate API를 통해 영상 생성을 요청합니다.
## 경로 파라미터 ## 경로 파라미터
- **task_id**: Project/Lyric/Songtask_id (필수) - 연관된 프로젝트, 가사, 노래조회하는 사용 - **task_id**: Project/Lyric/Song/Imagetask_id (필수) - 연관된 프로젝트, 가사, 노래, 이미지조회하는 사용
## 요청 필드 ## 쿼리 파라미터
- **orientation**: 영상 방향 (horizontal: 가로형, vertical: 세로형, 기본값: vertical) - 선택 - **orientation**: 영상 방향 (horizontal: 가로형, vertical: 세로형, 기본값: vertical) - 선택
- **image_urls**: 영상에 사용할 이미지 URL 목록 (필수)
## 자동 조회 정보 (Song 테이블에서 task_id 기준 가장 최근 생성된 노래 사용) ## 자동 조회 정보
- **music_url**: song_result_url 사용 - **image_urls**: Image 테이블에서 task_id로 조회 (img_order 순서로 정렬)
- **duration**: 노래의 duration 사용 - **music_url**: Song 테이블의 song_result_url 사용
- **lyrics**: song_prompt (가사) 사용 - **duration**: Song 테이블의 duration 사용
- **lyrics**: Song 테이블의 song_prompt (가사) 사용
## 반환 정보 ## 반환 정보
- **success**: 요청 성공 여부 - **success**: 요청 성공 여부
@ -69,33 +70,12 @@ Creatomate API를 통해 영상 생성을 요청합니다.
## 사용 예시 ## 사용 예시
``` ```
POST /video/generate/019123ab-cdef-7890-abcd-ef1234567890 GET /video/generate/0694b716-dbff-7219-8000-d08cb5fce431
{ GET /video/generate/0694b716-dbff-7219-8000-d08cb5fce431?orientation=horizontal
"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": [...]
}
``` ```
## 참고 ## 참고
- 이미지는 task_id로 Image 테이블에서 자동 조회됩니다 (img_order 순서).
- 배경 음악(music_url), 영상 길이(duration), 가사(lyrics) task_id로 Song 테이블을 조회하여 자동으로 가져옵니다. - 배경 음악(music_url), 영상 길이(duration), 가사(lyrics) task_id로 Song 테이블을 조회하여 자동으로 가져옵니다.
- 같은 task_id로 여러 Song이 있을 경우 **가장 최근 생성된 노래** 사용합니다. - 같은 task_id로 여러 Song이 있을 경우 **가장 최근 생성된 노래** 사용합니다.
- Song의 song_result_url과 song_prompt가 있어야 영상 생성이 가능합니다. - Song의 song_result_url과 song_prompt가 있어야 영상 생성이 가능합니다.
@ -105,24 +85,27 @@ POST /video/generate/019123ab-cdef-7890-abcd-ef1234567890
response_model=GenerateVideoResponse, response_model=GenerateVideoResponse,
responses={ responses={
200: {"description": "영상 생성 요청 성공"}, 200: {"description": "영상 생성 요청 성공"},
400: {"description": "Song의 음악 URL 또는 가사(song_prompt)가 없음"}, 400: {"description": "Song의 음악 URL, 가사(song_prompt) 또는 이미지가 없음"},
404: {"description": "Project, Lyric 또는 Song을 찾을 수 없음"}, 404: {"description": "Project, Lyric, Song 또는 Image를 찾을 수 없음"},
500: {"description": "영상 생성 요청 실패"}, 500: {"description": "영상 생성 요청 실패"},
}, },
) )
async def generate_video( async def generate_video(
task_id: str, task_id: str,
request_body: GenerateVideoRequest, orientation: Literal["horizontal", "vertical"] = Query(
default="vertical",
description="영상 방향 (horizontal: 가로형, vertical: 세로형)",
),
session: AsyncSession = Depends(get_session), session: AsyncSession = Depends(get_session),
) -> GenerateVideoResponse: ) -> GenerateVideoResponse:
"""Creatomate API를 통해 영상을 생성합니다. """Creatomate API를 통해 영상을 생성합니다.
1. task_id로 Project, Lyric, Song 조회 1. task_id로 Project, Lyric, Song, Image 조회
2. Video 테이블에 초기 데이터 저장 (status: processing) 2. Video 테이블에 초기 데이터 저장 (status: processing)
3. Creatomate API 호출 (orientation에 따른 템플릿 자동 선택) 3. Creatomate API 호출 (orientation에 따른 템플릿 자동 선택)
4. creatomate_render_id 업데이트 응답 반환 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: try:
# 1. task_id로 Project 조회 # 1. task_id로 Project 조회
project_result = await session.execute( 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] 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)}") 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( video = Video(
project_id=project.id, project_id=project.id,
lyric_id=lyric.id, lyric_id=lyric.id,
@ -202,29 +203,29 @@ async def generate_video(
await session.flush() # ID 생성을 위해 flush await session.flush() # ID 생성을 위해 flush
print(f"[generate_video] Video saved (processing) - task_id: {task_id}") 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}") print(f"[generate_video] Creatomate API generation started - task_id: {task_id}")
# orientation에 따른 템플릿 선택, duration은 Song에서 가져옴 (없으면 config 기본값 사용) # orientation에 따른 템플릿 선택, duration은 Song에서 가져옴 (없으면 config 기본값 사용)
creatomate_service = CreatomateService( creatomate_service = CreatomateService(
orientation=request_body.orientation, orientation=orientation,
target_duration=song.duration, # Song의 duration 사용 (None이면 config 기본값) 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})") 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) template = await creatomate_service.get_one_template_data_async(creatomate_service.template_id)
print(f"[generate_video] Template fetched - task_id: {task_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( modifications = creatomate_service.elements_connect_resource_blackbox(
elements=template["source"]["elements"], elements=template["source"]["elements"],
image_url_list=request_body.image_urls, image_url_list=image_urls,
lyric=lyrics, lyric=lyrics,
music_url=music_url, music_url=music_url,
) )
print(f"[generate_video] Modifications created - task_id: {task_id}") print(f"[generate_video] Modifications created - task_id: {task_id}")
# 5-3. elements 수정 # 6-3. elements 수정
new_elements = creatomate_service.modify_element( new_elements = creatomate_service.modify_element(
template["source"]["elements"], template["source"]["elements"],
modifications, modifications,
@ -232,14 +233,14 @@ async def generate_video(
template["source"]["elements"] = new_elements template["source"]["elements"] = new_elements
print(f"[generate_video] Elements modified - task_id: {task_id}") 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( final_template = creatomate_service.extend_template_duration(
template, template,
creatomate_service.target_duration, creatomate_service.target_duration,
) )
print(f"[generate_video] Duration extended to {creatomate_service.target_duration}s - task_id: {task_id}") 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( render_response = await creatomate_service.make_creatomate_custom_call_async(
final_template["source"], final_template["source"],
) )
@ -253,7 +254,7 @@ async def generate_video(
else: else:
creatomate_render_id = None creatomate_render_id = None
# 6. creatomate_render_id 업데이트 # 7. creatomate_render_id 업데이트
video.creatomate_render_id = creatomate_render_id video.creatomate_render_id = creatomate_render_id
await session.commit() await session.commit()
print(f"[generate_video] SUCCESS - task_id: {task_id}, creatomate_render_id: {creatomate_render_id}") print(f"[generate_video] SUCCESS - task_id: {task_id}, creatomate_render_id: {creatomate_render_id}")

View File

@ -5,59 +5,9 @@ Video API Schemas
""" """
from datetime import datetime from datetime import datetime
from typing import Any, Dict, List, Literal, Optional from typing import Any, Dict, Literal, Optional
from pydantic import BaseModel, Field from pydantic import BaseModel, ConfigDict, 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 목록")
# ============================================================================= # =============================================================================
@ -69,19 +19,22 @@ class GenerateVideoResponse(BaseModel):
"""영상 생성 응답 스키마 """영상 생성 응답 스키마
Usage: Usage:
POST /video/generate/{task_id} GET /video/generate/{task_id}
Returns the task IDs for tracking video generation. 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="요청 성공 여부") success: bool = Field(..., description="요청 성공 여부")
task_id: Optional[str] = Field(None, description="내부 작업 ID (Project task_id)") task_id: Optional[str] = Field(None, description="내부 작업 ID (Project task_id)")
creatomate_render_id: Optional[str] = Field(None, description="Creatomate 렌더 ID") creatomate_render_id: Optional[str] = Field(None, description="Creatomate 렌더 ID")