크레아토 완료

insta
bluebamus 2025-12-26 17:20:35 +09:00
parent 586dd5ccc9
commit 52520d770b
6 changed files with 362 additions and 60 deletions

View File

@ -252,7 +252,8 @@ async def get_song_status(
)
song = song_result.scalar_one_or_none()
if song:
if song and song.status != "completed":
# 이미 완료된 경우 백그라운드 작업 중복 실행 방지
# project_id로 Project 조회하여 store_name 가져오기
project_result = await session.execute(
select(Project).where(Project.id == song.project_id)
@ -269,6 +270,8 @@ async def get_song_status(
audio_url=audio_url,
store_name=store_name,
)
elif song and song.status == "completed":
print(f"[get_song_status] SKIPPED - Song already completed, suno_task_id: {suno_task_id}")
print(f"[get_song_status] SUCCESS - suno_task_id: {suno_task_id}")
return parsed_response
@ -289,9 +292,9 @@ async def get_song_status(
@router.get(
"/download/{task_id}",
summary="노래 다운로드 상태 조회",
summary="노래 생성 URL 조회",
description="""
task_id를 기반으로 Song 테이블의 상태를 polling하고,
task_id를 기반으로 Song 테이블의 상태를 조회하고,
completed인 경우 Project 정보와 노래 URL을 반환합니다.
## 경로 파라미터

View File

@ -25,10 +25,15 @@ response = creatomate.make_creatomate_call(template_id, modifications)
"""
import copy
from typing import Literal
import httpx
from config import apikey_settings
from config import apikey_settings, creatomate_settings
# Orientation 타입 정의
OrientationType = Literal["horizontal", "vertical"]
class CreatomateService:
@ -36,13 +41,40 @@ class CreatomateService:
BASE_URL = "https://api.creatomate.com"
def __init__(self, api_key: str | None = None):
# 템플릿 설정 (config에서 가져옴)
TEMPLATE_CONFIG = {
"horizontal": {
"template_id": creatomate_settings.TEMPLATE_ID_HORIZONTAL,
"duration": creatomate_settings.TEMPLATE_DURATION_HORIZONTAL,
},
"vertical": {
"template_id": creatomate_settings.TEMPLATE_ID_VERTICAL,
"duration": creatomate_settings.TEMPLATE_DURATION_VERTICAL,
},
}
def __init__(
self,
api_key: str | None = None,
orientation: OrientationType = "vertical",
target_duration: float | None = None,
):
"""
Args:
api_key: Creatomate API (Bearer token으로 사용)
None일 경우 config에서 자동으로 가져옴
orientation: 영상 방향 ("horizontal" 또는 "vertical", 기본값: "vertical")
target_duration: 목표 영상 길이 ()
None일 경우 orientation에 해당하는 기본값 사용
"""
self.api_key = api_key or apikey_settings.CREATOMATE_API_KEY
self.orientation = orientation
# orientation에 따른 템플릿 설정 가져오기
config = self.TEMPLATE_CONFIG.get(orientation, self.TEMPLATE_CONFIG["vertical"])
self.template_id = config["template_id"]
self.target_duration = target_duration if target_duration is not None else config["duration"]
self.headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {self.api_key}",
@ -62,6 +94,14 @@ class CreatomateService:
response.raise_for_status()
return response.json()
async def get_one_template_data_async(self, template_id: str) -> dict:
"""특정 템플릿 ID로 템플릿 정보를 비동기로 조회합니다."""
url = f"{self.BASE_URL}/v1/templates/{template_id}"
async with httpx.AsyncClient() as client:
response = await client.get(url, headers=self.headers, timeout=30.0)
response.raise_for_status()
return response.json()
def parse_template_component_name(self, template_source: list) -> dict:
"""템플릿 정보를 파싱하여 리소스 이름을 추출합니다."""
@ -209,6 +249,18 @@ class CreatomateService:
response.raise_for_status()
return response.json()
async def make_creatomate_custom_call_async(self, source: dict):
"""템플릿 없이 Creatomate에 비동기로 커스텀 렌더링 요청을 보냅니다.
Note:
response에 요청 정보가 있으니 폴링 필요
"""
url = f"{self.BASE_URL}/v2/renders"
async with httpx.AsyncClient() as client:
response = await client.post(url, json=source, headers=self.headers, timeout=60.0)
response.raise_for_status()
return response.json()
def get_render_status(self, render_id: str) -> dict:
"""렌더링 작업의 상태를 조회합니다.
@ -232,6 +284,30 @@ class CreatomateService:
response.raise_for_status()
return response.json()
async def get_render_status_async(self, render_id: str) -> dict:
"""렌더링 작업의 상태를 비동기로 조회합니다.
Args:
render_id: Creatomate 렌더 ID
Returns:
렌더링 상태 정보
Note:
상태 :
- planned: 예약됨
- waiting: 대기
- transcribing: 트랜스크립션
- rendering: 렌더링
- succeeded: 성공
- failed: 실패
"""
url = f"{self.BASE_URL}/v1/renders/{render_id}"
async with httpx.AsyncClient() as client:
response = await client.get(url, headers=self.headers, timeout=30.0)
response.raise_for_status()
return response.json()
def calc_scene_duration(self, template: dict) -> float:
"""템플릿의 전체 장면 duration을 계산합니다."""
total_template_duration = 0.0

View File

@ -35,7 +35,7 @@ from app.video.schemas.video_schema import (
VideoListItem,
VideoRenderData,
)
from app.video.worker.video_task import download_and_save_video
from app.video.worker.video_task import download_and_upload_video_to_blob
from app.utils.creatomate import CreatomateService
from app.utils.pagination import PaginatedResponse
@ -53,7 +53,7 @@ Creatomate API를 통해 영상 생성을 요청합니다.
- **task_id**: Project/Lyric/Song의 task_id (필수) - 연관된 프로젝트, 가사, 노래를 조회하는 사용
## 요청 필드
- **template_id**: Creatomate 템플릿 ID (필수)
- **orientation**: 영상 방향 (horizontal: 가로형, vertical: 세로형, 기본값: vertical) - 선택
- **image_urls**: 영상에 사용할 이미지 URL 목록 (필수)
- **lyrics**: 영상에 표시할 가사 (필수)
- **music_url**: 배경 음악 URL (필수)
@ -68,8 +68,29 @@ Creatomate API를 통해 영상 생성을 요청합니다.
```
POST /video/generate/019123ab-cdef-7890-abcd-ef1234567890
{
"template_id": "abc123...",
"image_urls": ["https://...", "https://..."],
"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"
],
"lyrics": "가사 내용...",
"music_url": "https://ado2mediastoragepublic.blob.core.windows.net/ado2-media-public-access/ado2-media-original/0694e2d8-7ae2-730c-8000-308aacaa582d/song/스테이 머뭄.mp3"
}
```
## 가로형 영상 생성 예시
```
POST /video/generate/019123ab-cdef-7890-abcd-ef1234567890
{
"orientation": "horizontal",
"image_urls": [...],
"lyrics": "가사 내용...",
"music_url": "https://..."
}
@ -95,10 +116,10 @@ async def generate_video(
1. task_id로 Project, Lyric, Song 조회
2. Video 테이블에 초기 데이터 저장 (status: processing)
3. Creatomate API 호출
3. Creatomate API 호출 (orientation에 따른 템플릿 자동 선택)
4. creatomate_render_id 업데이트 응답 반환
"""
print(f"[generate_video] START - task_id: {task_id}, template_id: {request_body.template_id}")
print(f"[generate_video] START - task_id: {task_id}, orientation: {request_body.orientation}")
try:
# 1. task_id로 Project 조회
project_result = await session.execute(
@ -158,23 +179,43 @@ async def generate_video(
await session.flush() # ID 생성을 위해 flush
print(f"[generate_video] Video saved (processing) - task_id: {task_id}")
# 5. Creatomate API 호출
# 5. Creatomate API 호출 (POC 패턴 적용)
print(f"[generate_video] Creatomate API generation started - task_id: {task_id}")
creatomate_service = CreatomateService()
# orientation에 따른 템플릿과 duration 자동 설정
creatomate_service = CreatomateService(orientation=request_body.orientation)
print(f"[generate_video] Using template_id: {creatomate_service.template_id}, duration: {creatomate_service.target_duration}")
# 템플릿에 리소스 매핑
modifications = creatomate_service.template_connect_resource_blackbox(
template_id=request_body.template_id,
# 5-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에서 리소스 매핑 생성
modifications = creatomate_service.elements_connect_resource_blackbox(
elements=template["source"]["elements"],
image_url_list=request_body.image_urls,
lyric=request_body.lyrics,
music_url=request_body.music_url,
)
print(f"[generate_video] Modifications created - task_id: {task_id}")
# 렌더링 요청
render_response = creatomate_service.make_creatomate_call(
template_id=request_body.template_id,
modifications=modifications,
# 5-3. elements 수정
new_elements = creatomate_service.modify_element(
template["source"]["elements"],
modifications,
)
template["source"]["elements"] = new_elements
print(f"[generate_video] Elements modified - task_id: {task_id}")
# 5-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. 커스텀 렌더링 요청 (비동기)
render_response = await creatomate_service.make_creatomate_custom_call_async(
final_template["source"],
)
print(f"[generate_video] Creatomate API response - task_id: {task_id}, response: {render_response}")
@ -265,7 +306,7 @@ async def get_video_status(
print(f"[get_video_status] START - creatomate_render_id: {creatomate_render_id}")
try:
creatomate_service = CreatomateService()
result = creatomate_service.get_render_status(creatomate_render_id)
result = await creatomate_service.get_render_status_async(creatomate_render_id)
print(f"[get_video_status] Creatomate API response - creatomate_render_id: {creatomate_render_id}, status: {result.get('status')}")
status = result.get("status", "unknown")
@ -293,7 +334,8 @@ async def get_video_status(
)
video = video_result.scalar_one_or_none()
if video:
if video and video.status != "completed":
# 이미 완료된 경우 백그라운드 작업 중복 실행 방지
# task_id로 Project 조회하여 store_name 가져오기
project_result = await session.execute(
select(Project).where(Project.id == video.project_id)
@ -302,14 +344,16 @@ async def get_video_status(
store_name = project.store_name if project else "video"
# 백그라운드 태스크로 MP4 다운로드 및 DB 업데이트
# 백그라운드 태스크로 MP4 다운로드 → Blob 업로드 → DB 업데이트 → 임시 파일 삭제
print(f"[get_video_status] Background task args - task_id: {video.task_id}, video_url: {video_url}, store_name: {store_name}")
background_tasks.add_task(
download_and_save_video,
download_and_upload_video_to_blob,
task_id=video.task_id,
video_url=video_url,
store_name=store_name,
)
elif video and video.status == "completed":
print(f"[get_video_status] SKIPPED - Video already completed, creatomate_render_id: {creatomate_render_id}")
render_data = VideoRenderData(
id=result.get("id"),
@ -344,7 +388,7 @@ async def get_video_status(
@router.get(
"/download/{task_id}",
summary="영상 다운로드 상태 조회",
summary="영상 생성 URL 조회",
description="""
task_id를 기반으로 Video 테이블의 상태를 polling하고,
completed인 경우 Project 정보와 영상 URL을 반환합니다.

View File

@ -5,7 +5,7 @@ Video API Schemas
"""
from datetime import datetime
from typing import Any, Dict, List, Optional
from typing import Any, Dict, List, Literal, Optional
from pydantic import BaseModel, Field
@ -34,18 +34,29 @@ class GenerateVideoRequest(BaseModel):
model_config = {
"json_schema_extra": {
"example": {
"template_id": "abc123-template-id",
"orientation": "vertical",
"image_urls": [
"https://example.com/image1.jpg",
"https://example.com/image2.jpg",
"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",
],
"lyrics": "인스타 감성의 스테이 머뭄, 머물러봐요\n군산 신흥동 말랭이 마을의 마음 힐링",
"music_url": "https://example.com/song.mp3",
"music_url": "https://ado2mediastoragepublic.blob.core.windows.net/ado2-media-public-access/ado2-media-original/0694e2d8-7ae2-730c-8000-308aacaa582d/song/스테이 머뭄.mp3",
}
}
}
template_id: str = Field(..., description="Creatomate 템플릿 ID")
orientation: Literal["horizontal", "vertical"] = Field(
default="vertical",
description="영상 방향 (horizontal: 가로형, vertical: 세로형, 기본값: vertical)",
)
image_urls: List[str] = Field(..., description="영상에 사용할 이미지 URL 목록")
lyrics: str = Field(..., description="영상에 표시할 가사")
music_url: str = Field(..., description="배경 음악 URL")

View File

@ -4,7 +4,6 @@ Video Background Tasks
영상 생성 관련 백그라운드 태스크를 정의합니다.
"""
from datetime import date
from pathlib import Path
import aiofiles
@ -13,27 +12,25 @@ from sqlalchemy import select
from app.database.session import AsyncSessionLocal
from app.video.models import Video
from app.utils.common import generate_task_id
from config import prj_settings
from app.utils.upload_blob_as_request import AzureBlobUploader
async def download_and_save_video(
async def download_and_upload_video_to_blob(
task_id: str,
video_url: str,
store_name: str,
) -> None:
"""백그라운드에서 영상을 다운로드하고 Video 테이블을 업데이트합니다.
"""백그라운드에서 영상을 다운로드하고 Azure Blob Storage에 업로드한 뒤 Video 테이블을 업데이트합니다.
Args:
task_id: 프로젝트 task_id
video_url: 다운로드할 영상 URL
store_name: 저장할 파일명에 사용할 업체명
"""
print(f"[download_and_save_video] START - task_id: {task_id}, store_name: {store_name}")
print(f"[download_and_upload_video_to_blob] START - task_id: {task_id}, store_name: {store_name}")
temp_file_path: Path | None = None
try:
# 저장 경로 생성: media/{날짜}/{uuid7}/{store_name}.mp4
today = date.today().isoformat()
unique_id = await generate_task_id()
# 파일명에 사용할 수 없는 문자 제거
safe_store_name = "".join(
c for c in store_name if c.isalnum() or c in (" ", "_", "-")
@ -41,27 +38,32 @@ async def download_and_save_video(
safe_store_name = safe_store_name or "video"
file_name = f"{safe_store_name}.mp4"
# 절대 경로 생성
media_dir = Path("media") / today / unique_id
media_dir.mkdir(parents=True, exist_ok=True)
file_path = media_dir / file_name
print(f"[download_and_save_video] Directory created - path: {file_path}")
# 임시 저장 경로 생성
temp_dir = Path("media") / "temp" / task_id
temp_dir.mkdir(parents=True, exist_ok=True)
temp_file_path = temp_dir / file_name
print(f"[download_and_upload_video_to_blob] Temp directory created - path: {temp_file_path}")
# 영상 파일 다운로드
print(f"[download_and_save_video] Downloading video - task_id: {task_id}, url: {video_url}")
print(f"[download_and_upload_video_to_blob] Downloading video - task_id: {task_id}, url: {video_url}")
async with httpx.AsyncClient() as client:
response = await client.get(video_url, timeout=120.0) # 영상은 더 큰 timeout
response = await client.get(video_url, timeout=180.0)
response.raise_for_status()
async with aiofiles.open(str(file_path), "wb") as f:
async with aiofiles.open(str(temp_file_path), "wb") as f:
await f.write(response.content)
print(f"[download_and_save_video] File saved - task_id: {task_id}, path: {file_path}")
print(f"[download_and_upload_video_to_blob] File downloaded - task_id: {task_id}, path: {temp_file_path}")
# 프론트엔드에서 접근 가능한 URL 생성
relative_path = f"/media/{today}/{unique_id}/{file_name}"
base_url = f"http://{prj_settings.PROJECT_DOMAIN}"
file_url = f"{base_url}{relative_path}"
print(f"[download_and_save_video] URL generated - task_id: {task_id}, url: {file_url}")
# Azure Blob Storage에 업로드
uploader = AzureBlobUploader(task_id=task_id)
upload_success = await uploader.upload_video(file_path=str(temp_file_path))
if not upload_success:
raise Exception("Azure Blob Storage 업로드 실패")
# SAS 토큰이 제외된 public_url 사용
blob_url = uploader.public_url
print(f"[download_and_upload_video_to_blob] Uploaded to Blob - task_id: {task_id}, url: {blob_url}")
# Video 테이블 업데이트 (새 세션 사용)
async with AsyncSessionLocal() as session:
@ -76,17 +78,16 @@ async def download_and_save_video(
if video:
video.status = "completed"
video.result_movie_url = file_url
video.result_movie_url = blob_url
await session.commit()
print(f"[download_and_save_video] SUCCESS - task_id: {task_id}, status: completed")
print(f"[download_and_upload_video_to_blob] SUCCESS - task_id: {task_id}, status: completed")
else:
print(f"[download_and_save_video] Video NOT FOUND in DB - task_id: {task_id}")
print(f"[download_and_upload_video_to_blob] Video NOT FOUND in DB - task_id: {task_id}")
except Exception as e:
print(f"[download_and_save_video] EXCEPTION - task_id: {task_id}, error: {e}")
print(f"[download_and_upload_video_to_blob] EXCEPTION - task_id: {task_id}, error: {e}")
# 실패 시 Video 테이블 업데이트
async with AsyncSessionLocal() as session:
# 여러 개 있을 경우 가장 최근 것 선택
result = await session.execute(
select(Video)
.where(Video.task_id == task_id)
@ -98,4 +99,144 @@ async def download_and_save_video(
if video:
video.status = "failed"
await session.commit()
print(f"[download_and_save_video] FAILED - task_id: {task_id}, status updated to failed")
print(f"[download_and_upload_video_to_blob] FAILED - task_id: {task_id}, status updated to failed")
finally:
# 임시 파일 삭제
if temp_file_path and temp_file_path.exists():
try:
temp_file_path.unlink()
print(f"[download_and_upload_video_to_blob] Temp file deleted - path: {temp_file_path}")
except Exception as e:
print(f"[download_and_upload_video_to_blob] Failed to delete temp file: {e}")
# 임시 디렉토리 삭제 시도
temp_dir = Path("media") / "temp" / task_id
if temp_dir.exists():
try:
temp_dir.rmdir()
except Exception:
pass # 디렉토리가 비어있지 않으면 무시
async def download_and_upload_video_by_creatomate_render_id(
creatomate_render_id: str,
video_url: str,
store_name: str,
) -> None:
"""creatomate_render_id로 Video를 조회하여 영상을 다운로드하고 Azure Blob Storage에 업로드한 뒤 Video 테이블을 업데이트합니다.
Args:
creatomate_render_id: Creatomate API 렌더 ID
video_url: 다운로드할 영상 URL
store_name: 저장할 파일명에 사용할 업체명
"""
print(f"[download_and_upload_video_by_creatomate_render_id] START - creatomate_render_id: {creatomate_render_id}, store_name: {store_name}")
temp_file_path: Path | None = None
task_id: str | None = None
try:
# creatomate_render_id로 Video 조회하여 task_id 가져오기
async with AsyncSessionLocal() as session:
result = await session.execute(
select(Video)
.where(Video.creatomate_render_id == creatomate_render_id)
.order_by(Video.created_at.desc())
.limit(1)
)
video = result.scalar_one_or_none()
if not video:
print(f"[download_and_upload_video_by_creatomate_render_id] Video NOT FOUND - creatomate_render_id: {creatomate_render_id}")
return
task_id = video.task_id
print(f"[download_and_upload_video_by_creatomate_render_id] Video found - creatomate_render_id: {creatomate_render_id}, task_id: {task_id}")
# 파일명에 사용할 수 없는 문자 제거
safe_store_name = "".join(
c for c in store_name if c.isalnum() or c in (" ", "_", "-")
).strip()
safe_store_name = safe_store_name or "video"
file_name = f"{safe_store_name}.mp4"
# 임시 저장 경로 생성
temp_dir = Path("media") / "temp" / task_id
temp_dir.mkdir(parents=True, exist_ok=True)
temp_file_path = temp_dir / file_name
print(f"[download_and_upload_video_by_creatomate_render_id] Temp directory created - path: {temp_file_path}")
# 영상 파일 다운로드
print(f"[download_and_upload_video_by_creatomate_render_id] Downloading video - creatomate_render_id: {creatomate_render_id}, url: {video_url}")
async with httpx.AsyncClient() as client:
response = await client.get(video_url, timeout=180.0)
response.raise_for_status()
async with aiofiles.open(str(temp_file_path), "wb") as f:
await f.write(response.content)
print(f"[download_and_upload_video_by_creatomate_render_id] File downloaded - creatomate_render_id: {creatomate_render_id}, path: {temp_file_path}")
# Azure Blob Storage에 업로드
uploader = AzureBlobUploader(task_id=task_id)
upload_success = await uploader.upload_video(file_path=str(temp_file_path))
if not upload_success:
raise Exception("Azure Blob Storage 업로드 실패")
# SAS 토큰이 제외된 public_url 사용
blob_url = uploader.public_url
print(f"[download_and_upload_video_by_creatomate_render_id] Uploaded to Blob - creatomate_render_id: {creatomate_render_id}, url: {blob_url}")
# Video 테이블 업데이트 (새 세션 사용)
async with AsyncSessionLocal() as session:
result = await session.execute(
select(Video)
.where(Video.creatomate_render_id == creatomate_render_id)
.order_by(Video.created_at.desc())
.limit(1)
)
video = result.scalar_one_or_none()
if video:
video.status = "completed"
video.result_movie_url = blob_url
await session.commit()
print(f"[download_and_upload_video_by_creatomate_render_id] SUCCESS - creatomate_render_id: {creatomate_render_id}, status: completed")
else:
print(f"[download_and_upload_video_by_creatomate_render_id] Video NOT FOUND in DB - creatomate_render_id: {creatomate_render_id}")
except Exception as e:
print(f"[download_and_upload_video_by_creatomate_render_id] EXCEPTION - creatomate_render_id: {creatomate_render_id}, error: {e}")
# 실패 시 Video 테이블 업데이트
if task_id:
async with AsyncSessionLocal() as session:
result = await session.execute(
select(Video)
.where(Video.creatomate_render_id == creatomate_render_id)
.order_by(Video.created_at.desc())
.limit(1)
)
video = result.scalar_one_or_none()
if video:
video.status = "failed"
await session.commit()
print(f"[download_and_upload_video_by_creatomate_render_id] FAILED - creatomate_render_id: {creatomate_render_id}, status updated to failed")
finally:
# 임시 파일 삭제
if temp_file_path and temp_file_path.exists():
try:
temp_file_path.unlink()
print(f"[download_and_upload_video_by_creatomate_render_id] Temp file deleted - path: {temp_file_path}")
except Exception as e:
print(f"[download_and_upload_video_by_creatomate_render_id] Failed to delete temp file: {e}")
# 임시 디렉토리 삭제 시도
if task_id:
temp_dir = Path("media") / "temp" / task_id
if temp_dir.exists():
try:
temp_dir.rmdir()
except Exception:
pass # 디렉토리가 비어있지 않으면 무시

View File

@ -142,6 +142,32 @@ class AzureBlobSettings(BaseSettings):
model_config = _base_config
class CreatomateSettings(BaseSettings):
"""Creatomate 템플릿 설정"""
# 세로형 템플릿 (기본값)
TEMPLATE_ID_VERTICAL: str = Field(
default="e8c7b43f-de4b-4ba3-b8eb-5df688569193",
description="Creatomate 세로형 템플릿 ID",
)
TEMPLATE_DURATION_VERTICAL: float = Field(
default=90.0,
description="세로형 템플릿 기본 duration (초)",
)
# 가로형 템플릿
TEMPLATE_ID_HORIZONTAL: str = Field(
default="0f092a6a-f526-4ef0-9181-d4ad4426b9e7",
description="Creatomate 가로형 템플릿 ID",
)
TEMPLATE_DURATION_HORIZONTAL: float = Field(
default=30.0,
description="가로형 템플릿 기본 duration (초)",
)
model_config = _base_config
prj_settings = ProjectSettings()
apikey_settings = APIKeySettings()
db_settings = DatabaseSettings()
@ -150,3 +176,4 @@ notification_settings = NotificationSettings()
cors_settings = CORSSettings()
crawler_settings = CrawlerSettings()
azure_blob_settings = AzureBlobSettings()
creatomate_settings = CreatomateSettings()