영상 생성시 이미지 url 전송 -> task_id로 직접 검색으로 변경
parent
c6d9edbb42
commit
95d90dcb50
|
|
@ -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/Song의 task_id (필수) - 연관된 프로젝트, 가사, 노래를 조회하는 데 사용
|
- **task_id**: Project/Lyric/Song/Image의 task_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}")
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue