영상 생성시 이미지 url 전송 -> task_id로 직접 검색으로 변경
parent
c6d9edbb42
commit
95d90dcb50
|
|
@ -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}")
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
Loading…
Reference in New Issue