feat(music-lab): Phase 3 백엔드 — 업로드커버, 업로드확장, 보컬추가, 인스트추가, 뮤직비디오
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,7 @@ from .local_provider import run_local_generation
|
|||||||
from .suno_provider import (
|
from .suno_provider import (
|
||||||
run_suno_generation, run_suno_extend, run_vocal_removal,
|
run_suno_generation, run_suno_extend, run_vocal_removal,
|
||||||
run_cover_image, run_wav_convert, run_stem_split,
|
run_cover_image, run_wav_convert, run_stem_split,
|
||||||
|
run_upload_cover, run_upload_extend, run_add_vocals, run_add_instrumental, run_video_generate,
|
||||||
generate_lyrics, get_credits, get_timestamped_lyrics, generate_style_boost,
|
generate_lyrics, get_credits, get_timestamped_lyrics, generate_style_boost,
|
||||||
SUNO_API_KEY, SUNO_MODELS,
|
SUNO_API_KEY, SUNO_MODELS,
|
||||||
)
|
)
|
||||||
@@ -498,6 +499,134 @@ def style_boost(req: StyleBoostRequest):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
# ── Phase 3: 업로드 + 커버 ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class UploadCoverRequest(BaseModel):
|
||||||
|
upload_url: str
|
||||||
|
model: str = "V4"
|
||||||
|
custom_mode: bool = True
|
||||||
|
instrumental: bool = False
|
||||||
|
prompt: str = ""
|
||||||
|
style: str = ""
|
||||||
|
title: str = ""
|
||||||
|
vocal_gender: Optional[str] = None
|
||||||
|
negative_tags: Optional[str] = None
|
||||||
|
style_weight: Optional[float] = None
|
||||||
|
audio_weight: Optional[float] = None
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/music/upload-cover")
|
||||||
|
def upload_cover(req: UploadCoverRequest, background_tasks: BackgroundTasks):
|
||||||
|
"""외부 오디오를 Suno 스타일로 리메이크."""
|
||||||
|
if not SUNO_API_KEY:
|
||||||
|
raise HTTPException(status_code=400, detail="Suno API 키가 설정되지 않았습니다")
|
||||||
|
task_id = str(uuid.uuid4())
|
||||||
|
params = req.model_dump()
|
||||||
|
create_task(task_id, params, provider="suno")
|
||||||
|
background_tasks.add_task(run_upload_cover, task_id, params)
|
||||||
|
return {"task_id": task_id, "provider": "suno"}
|
||||||
|
|
||||||
|
|
||||||
|
# ── Phase 3: 업로드 + 확장 ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class UploadExtendRequest(BaseModel):
|
||||||
|
upload_url: str
|
||||||
|
model: str = "V4"
|
||||||
|
default_param_flag: bool = True
|
||||||
|
continue_at: Optional[float] = None
|
||||||
|
prompt: str = ""
|
||||||
|
style: str = ""
|
||||||
|
title: str = ""
|
||||||
|
instrumental: bool = False
|
||||||
|
vocal_gender: Optional[str] = None
|
||||||
|
negative_tags: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/music/upload-extend")
|
||||||
|
def upload_extend(req: UploadExtendRequest, background_tasks: BackgroundTasks):
|
||||||
|
"""외부 오디오를 이어서 확장."""
|
||||||
|
if not SUNO_API_KEY:
|
||||||
|
raise HTTPException(status_code=400, detail="Suno API 키가 설정되지 않았습니다")
|
||||||
|
task_id = str(uuid.uuid4())
|
||||||
|
params = req.model_dump()
|
||||||
|
create_task(task_id, params, provider="suno")
|
||||||
|
background_tasks.add_task(run_upload_extend, task_id, params)
|
||||||
|
return {"task_id": task_id, "provider": "suno"}
|
||||||
|
|
||||||
|
|
||||||
|
# ── Phase 3: 보컬 추가 ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class AddVocalsRequest(BaseModel):
|
||||||
|
upload_url: str
|
||||||
|
prompt: str
|
||||||
|
title: str
|
||||||
|
style: str
|
||||||
|
negative_tags: str = ""
|
||||||
|
vocal_gender: Optional[str] = None
|
||||||
|
model: str = "V4_5PLUS"
|
||||||
|
style_weight: Optional[float] = None
|
||||||
|
audio_weight: Optional[float] = None
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/music/add-vocals")
|
||||||
|
def add_vocals(req: AddVocalsRequest, background_tasks: BackgroundTasks):
|
||||||
|
"""인스트루멘탈에 AI 보컬 추가."""
|
||||||
|
if not SUNO_API_KEY:
|
||||||
|
raise HTTPException(status_code=400, detail="Suno API 키가 설정되지 않았습니다")
|
||||||
|
task_id = str(uuid.uuid4())
|
||||||
|
params = req.model_dump()
|
||||||
|
create_task(task_id, params, provider="suno")
|
||||||
|
background_tasks.add_task(run_add_vocals, task_id, params)
|
||||||
|
return {"task_id": task_id, "provider": "suno"}
|
||||||
|
|
||||||
|
|
||||||
|
# ── Phase 3: 인스트루멘탈 추가 ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
class AddInstrumentalRequest(BaseModel):
|
||||||
|
upload_url: str
|
||||||
|
title: str
|
||||||
|
tags: str
|
||||||
|
negative_tags: str = ""
|
||||||
|
vocal_gender: Optional[str] = None
|
||||||
|
model: str = "V4_5PLUS"
|
||||||
|
style_weight: Optional[float] = None
|
||||||
|
audio_weight: Optional[float] = None
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/music/add-instrumental")
|
||||||
|
def add_instrumental(req: AddInstrumentalRequest, background_tasks: BackgroundTasks):
|
||||||
|
"""보컬에 AI 반주 추가."""
|
||||||
|
if not SUNO_API_KEY:
|
||||||
|
raise HTTPException(status_code=400, detail="Suno API 키가 설정되지 않았습니다")
|
||||||
|
task_id = str(uuid.uuid4())
|
||||||
|
params = req.model_dump()
|
||||||
|
create_task(task_id, params, provider="suno")
|
||||||
|
background_tasks.add_task(run_add_instrumental, task_id, params)
|
||||||
|
return {"task_id": task_id, "provider": "suno"}
|
||||||
|
|
||||||
|
|
||||||
|
# ── Phase 3: 뮤직비디오 생성 ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class VideoRequest(BaseModel):
|
||||||
|
suno_task_id: str
|
||||||
|
suno_id: str
|
||||||
|
author: str = ""
|
||||||
|
domain_name: str = ""
|
||||||
|
track_id: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/music/video")
|
||||||
|
def video_generate(req: VideoRequest, background_tasks: BackgroundTasks):
|
||||||
|
"""뮤직비디오(MP4) 생성."""
|
||||||
|
if not SUNO_API_KEY:
|
||||||
|
raise HTTPException(status_code=400, detail="Suno API 키가 설정되지 않았습니다")
|
||||||
|
task_id = str(uuid.uuid4())
|
||||||
|
params = req.model_dump()
|
||||||
|
create_task(task_id, params, provider="suno")
|
||||||
|
background_tasks.add_task(run_video_generate, task_id, params)
|
||||||
|
return {"task_id": task_id, "provider": "suno"}
|
||||||
|
|
||||||
|
|
||||||
# ── 저장된 가사 CRUD API ────────────────────────────────────────────────────
|
# ── 저장된 가사 CRUD API ────────────────────────────────────────────────────
|
||||||
|
|
||||||
class LyricsSave(BaseModel):
|
class LyricsSave(BaseModel):
|
||||||
|
|||||||
@@ -757,3 +757,280 @@ def generate_style_boost(content: str) -> Optional[dict]:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning("Style boost error: %s", e)
|
logger.warning("Style boost error: %s", e)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# ── 오디오 업로드 + 커버 ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def run_upload_cover(task_id: str, params: dict) -> None:
|
||||||
|
"""외부 오디오를 Suno 스타일로 리메이크."""
|
||||||
|
try:
|
||||||
|
if not SUNO_API_KEY:
|
||||||
|
update_task(task_id, "failed", 0, "", error="SUNO_API_KEY가 설정되지 않았습니다.")
|
||||||
|
return
|
||||||
|
|
||||||
|
update_task(task_id, "processing", 5, "AI Cover 요청 중...")
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"uploadUrl": params["upload_url"],
|
||||||
|
"customMode": params.get("custom_mode", True),
|
||||||
|
"instrumental": params.get("instrumental", False),
|
||||||
|
"model": params.get("model", "V4"),
|
||||||
|
"callBackUrl": "https://example.com/noop",
|
||||||
|
}
|
||||||
|
for key, api_key in [("prompt", "prompt"), ("style", "style"), ("title", "title"),
|
||||||
|
("vocal_gender", "vocalGender"), ("negative_tags", "negativeTags"),
|
||||||
|
("style_weight", "styleWeight"), ("audio_weight", "audioWeight")]:
|
||||||
|
if params.get(key):
|
||||||
|
payload[api_key] = params[key]
|
||||||
|
|
||||||
|
resp = requests.post(f"{SUNO_BASE_URL}/generate/upload-cover", headers=_headers(), json=payload, timeout=30)
|
||||||
|
if resp.status_code != 200:
|
||||||
|
update_task(task_id, "failed", 0, "", error=f"Upload Cover API 오류: {resp.text[:300]}")
|
||||||
|
return
|
||||||
|
body = resp.json()
|
||||||
|
if body.get("code") != 200:
|
||||||
|
update_task(task_id, "failed", 0, "", error=f"Upload Cover 거부: {body.get('msg', 'unknown')}")
|
||||||
|
return
|
||||||
|
|
||||||
|
suno_task_id = body.get("data", {}).get("taskId", "")
|
||||||
|
if not suno_task_id:
|
||||||
|
update_task(task_id, "failed", 0, "", error="Upload Cover 응답에 taskId 없음")
|
||||||
|
return
|
||||||
|
update_task(task_id, "processing", 15, "AI Cover 생성 중...")
|
||||||
|
|
||||||
|
response = _poll_suno_record("/generate/record-info", suno_task_id, task_id)
|
||||||
|
if not response:
|
||||||
|
return
|
||||||
|
completed_tracks = response.get("sunoData") or []
|
||||||
|
if not completed_tracks:
|
||||||
|
update_task(task_id, "failed", 0, "", error="AI Cover 생성 완료했으나 트랙 없음")
|
||||||
|
return
|
||||||
|
|
||||||
|
track = _download_and_register(task_id=task_id, song=completed_tracks[0], params=params, filename_suffix="")
|
||||||
|
if track:
|
||||||
|
update_task(task_id, "succeeded", 100, "AI Cover 완료", audio_url=track["audio_url"])
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("Upload cover error for task %s", task_id)
|
||||||
|
update_task(task_id, "failed", 0, "", error=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
# ── 오디오 업로드 + 확장 ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def run_upload_extend(task_id: str, params: dict) -> None:
|
||||||
|
"""외부 오디오를 이어서 확장."""
|
||||||
|
try:
|
||||||
|
if not SUNO_API_KEY:
|
||||||
|
update_task(task_id, "failed", 0, "", error="SUNO_API_KEY가 설정되지 않았습니다.")
|
||||||
|
return
|
||||||
|
|
||||||
|
update_task(task_id, "processing", 5, "Upload Extend 요청 중...")
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"uploadUrl": params["upload_url"],
|
||||||
|
"defaultParamFlag": params.get("default_param_flag", True),
|
||||||
|
"model": params.get("model", "V4"),
|
||||||
|
"callBackUrl": "https://example.com/noop",
|
||||||
|
}
|
||||||
|
for key, api_key in [("prompt", "prompt"), ("style", "style"), ("title", "title"),
|
||||||
|
("continue_at", "continueAt"), ("instrumental", "instrumental"),
|
||||||
|
("vocal_gender", "vocalGender"), ("negative_tags", "negativeTags")]:
|
||||||
|
if params.get(key) is not None:
|
||||||
|
payload[api_key] = params[key]
|
||||||
|
|
||||||
|
resp = requests.post(f"{SUNO_BASE_URL}/generate/upload-extend", headers=_headers(), json=payload, timeout=30)
|
||||||
|
if resp.status_code != 200:
|
||||||
|
update_task(task_id, "failed", 0, "", error=f"Upload Extend API 오류: {resp.text[:300]}")
|
||||||
|
return
|
||||||
|
body = resp.json()
|
||||||
|
if body.get("code") != 200:
|
||||||
|
update_task(task_id, "failed", 0, "", error=f"Upload Extend 거부: {body.get('msg', 'unknown')}")
|
||||||
|
return
|
||||||
|
|
||||||
|
suno_task_id = body.get("data", {}).get("taskId", "")
|
||||||
|
if not suno_task_id:
|
||||||
|
update_task(task_id, "failed", 0, "", error="Upload Extend 응답에 taskId 없음")
|
||||||
|
return
|
||||||
|
update_task(task_id, "processing", 15, "Upload Extend 생성 중...")
|
||||||
|
|
||||||
|
response = _poll_suno_record("/generate/record-info", suno_task_id, task_id)
|
||||||
|
if not response:
|
||||||
|
return
|
||||||
|
completed_tracks = response.get("sunoData") or []
|
||||||
|
if not completed_tracks:
|
||||||
|
update_task(task_id, "failed", 0, "", error="Upload Extend 완료했으나 트랙 없음")
|
||||||
|
return
|
||||||
|
|
||||||
|
track = _download_and_register(task_id=task_id, song=completed_tracks[0], params=params, filename_suffix="")
|
||||||
|
if track:
|
||||||
|
update_task(task_id, "succeeded", 100, "Upload Extend 완료", audio_url=track["audio_url"])
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("Upload extend error for task %s", task_id)
|
||||||
|
update_task(task_id, "failed", 0, "", error=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
# ── 보컬 추가 ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def run_add_vocals(task_id: str, params: dict) -> None:
|
||||||
|
"""인스트루멘탈에 AI 보컬을 추가."""
|
||||||
|
try:
|
||||||
|
if not SUNO_API_KEY:
|
||||||
|
update_task(task_id, "failed", 0, "", error="SUNO_API_KEY가 설정되지 않았습니다.")
|
||||||
|
return
|
||||||
|
|
||||||
|
update_task(task_id, "processing", 5, "보컬 추가 요청 중...")
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"uploadUrl": params["upload_url"],
|
||||||
|
"prompt": params.get("prompt", ""),
|
||||||
|
"title": params.get("title", ""),
|
||||||
|
"style": params.get("style", ""),
|
||||||
|
"negativeTags": params.get("negative_tags", ""),
|
||||||
|
"callBackUrl": "https://example.com/noop",
|
||||||
|
}
|
||||||
|
for key, api_key in [("vocal_gender", "vocalGender"), ("model", "model"),
|
||||||
|
("style_weight", "styleWeight"), ("audio_weight", "audioWeight")]:
|
||||||
|
if params.get(key) is not None:
|
||||||
|
payload[api_key] = params[key]
|
||||||
|
|
||||||
|
resp = requests.post(f"{SUNO_BASE_URL}/generate/add-vocals", headers=_headers(), json=payload, timeout=30)
|
||||||
|
if resp.status_code != 200:
|
||||||
|
update_task(task_id, "failed", 0, "", error=f"Add Vocals API 오류: {resp.text[:300]}")
|
||||||
|
return
|
||||||
|
body = resp.json()
|
||||||
|
if body.get("code") != 200:
|
||||||
|
update_task(task_id, "failed", 0, "", error=f"Add Vocals 거부: {body.get('msg', 'unknown')}")
|
||||||
|
return
|
||||||
|
|
||||||
|
suno_task_id = body.get("data", {}).get("taskId", "")
|
||||||
|
if not suno_task_id:
|
||||||
|
update_task(task_id, "failed", 0, "", error="Add Vocals 응답에 taskId 없음")
|
||||||
|
return
|
||||||
|
update_task(task_id, "processing", 15, "AI 보컬 생성 중...")
|
||||||
|
|
||||||
|
response = _poll_suno_record("/generate/record-info", suno_task_id, task_id)
|
||||||
|
if not response:
|
||||||
|
return
|
||||||
|
completed_tracks = response.get("sunoData") or []
|
||||||
|
if not completed_tracks:
|
||||||
|
update_task(task_id, "failed", 0, "", error="보컬 추가 완료했으나 트랙 없음")
|
||||||
|
return
|
||||||
|
|
||||||
|
track = _download_and_register(task_id=task_id, song=completed_tracks[0], params=params, filename_suffix="")
|
||||||
|
if track:
|
||||||
|
update_task(task_id, "succeeded", 100, "보컬 추가 완료", audio_url=track["audio_url"])
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("Add vocals error for task %s", task_id)
|
||||||
|
update_task(task_id, "failed", 0, "", error=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
# ── 인스트루멘탈 추가 ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def run_add_instrumental(task_id: str, params: dict) -> None:
|
||||||
|
"""보컬에 AI 반주를 추가."""
|
||||||
|
try:
|
||||||
|
if not SUNO_API_KEY:
|
||||||
|
update_task(task_id, "failed", 0, "", error="SUNO_API_KEY가 설정되지 않았습니다.")
|
||||||
|
return
|
||||||
|
|
||||||
|
update_task(task_id, "processing", 5, "인스트루멘탈 추가 요청 중...")
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"uploadUrl": params["upload_url"],
|
||||||
|
"title": params.get("title", ""),
|
||||||
|
"tags": params.get("tags", ""),
|
||||||
|
"negativeTags": params.get("negative_tags", ""),
|
||||||
|
"callBackUrl": "https://example.com/noop",
|
||||||
|
}
|
||||||
|
for key, api_key in [("vocal_gender", "vocalGender"), ("model", "model"),
|
||||||
|
("style_weight", "styleWeight"), ("audio_weight", "audioWeight")]:
|
||||||
|
if params.get(key) is not None:
|
||||||
|
payload[api_key] = params[key]
|
||||||
|
|
||||||
|
resp = requests.post(f"{SUNO_BASE_URL}/generate/add-instrumental", headers=_headers(), json=payload, timeout=30)
|
||||||
|
if resp.status_code != 200:
|
||||||
|
update_task(task_id, "failed", 0, "", error=f"Add Instrumental API 오류: {resp.text[:300]}")
|
||||||
|
return
|
||||||
|
body = resp.json()
|
||||||
|
if body.get("code") != 200:
|
||||||
|
update_task(task_id, "failed", 0, "", error=f"Add Instrumental 거부: {body.get('msg', 'unknown')}")
|
||||||
|
return
|
||||||
|
|
||||||
|
suno_task_id = body.get("data", {}).get("taskId", "")
|
||||||
|
if not suno_task_id:
|
||||||
|
update_task(task_id, "failed", 0, "", error="Add Instrumental 응답에 taskId 없음")
|
||||||
|
return
|
||||||
|
update_task(task_id, "processing", 15, "AI 반주 생성 중...")
|
||||||
|
|
||||||
|
response = _poll_suno_record("/generate/record-info", suno_task_id, task_id)
|
||||||
|
if not response:
|
||||||
|
return
|
||||||
|
completed_tracks = response.get("sunoData") or []
|
||||||
|
if not completed_tracks:
|
||||||
|
update_task(task_id, "failed", 0, "", error="인스트루멘탈 추가 완료했으나 트랙 없음")
|
||||||
|
return
|
||||||
|
|
||||||
|
track = _download_and_register(task_id=task_id, song=completed_tracks[0], params=params, filename_suffix="")
|
||||||
|
if track:
|
||||||
|
update_task(task_id, "succeeded", 100, "인스트루멘탈 추가 완료", audio_url=track["audio_url"])
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("Add instrumental error for task %s", task_id)
|
||||||
|
update_task(task_id, "failed", 0, "", error=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
# ── 뮤직비디오 생성 ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def run_video_generate(task_id: str, params: dict) -> None:
|
||||||
|
"""곡의 뮤직비디오(MP4) 생성."""
|
||||||
|
try:
|
||||||
|
if not SUNO_API_KEY:
|
||||||
|
update_task(task_id, "failed", 0, "", error="SUNO_API_KEY가 설정되지 않았습니다.")
|
||||||
|
return
|
||||||
|
|
||||||
|
update_task(task_id, "processing", 5, "뮤직비디오 생성 요청 중...")
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"taskId": params["suno_task_id"],
|
||||||
|
"audioId": params["suno_id"],
|
||||||
|
"callBackUrl": "https://example.com/noop",
|
||||||
|
}
|
||||||
|
if params.get("author"):
|
||||||
|
payload["author"] = params["author"][:50]
|
||||||
|
if params.get("domain_name"):
|
||||||
|
payload["domainName"] = params["domain_name"][:50]
|
||||||
|
|
||||||
|
resp = requests.post(f"{SUNO_BASE_URL}/mp4/generate", headers=_headers(), json=payload, timeout=30)
|
||||||
|
if resp.status_code != 200:
|
||||||
|
update_task(task_id, "failed", 0, "", error=f"Video API 오류: {resp.text[:300]}")
|
||||||
|
return
|
||||||
|
body = resp.json()
|
||||||
|
if body.get("code") != 200:
|
||||||
|
update_task(task_id, "failed", 0, "", error=f"Video 생성 거부: {body.get('msg', 'unknown')}")
|
||||||
|
return
|
||||||
|
|
||||||
|
video_task_id = body.get("data", {}).get("taskId", params.get("suno_task_id", ""))
|
||||||
|
update_task(task_id, "processing", 15, "뮤직비디오 렌더링 중...")
|
||||||
|
|
||||||
|
response = _poll_suno_record(
|
||||||
|
"/mp4/record-info", video_task_id, task_id,
|
||||||
|
max_attempts=60, interval=10,
|
||||||
|
progress_msg_map={"PENDING": "비디오 렌더링 대기 중...", "GENERATING": "비디오 렌더링 중..."},
|
||||||
|
)
|
||||||
|
if not response:
|
||||||
|
return
|
||||||
|
|
||||||
|
video_url = ""
|
||||||
|
suno_data = response.get("sunoData") or []
|
||||||
|
if suno_data and isinstance(suno_data, list) and isinstance(suno_data[0], dict):
|
||||||
|
video_url = suno_data[0].get("videoUrl") or suno_data[0].get("video_url", "")
|
||||||
|
if not video_url:
|
||||||
|
video_url = response.get("video_url") or response.get("videoUrl", "")
|
||||||
|
|
||||||
|
update_task(task_id, "succeeded", 100, "뮤직비디오 생성 완료", audio_url=video_url)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("Video generate error for task %s", task_id)
|
||||||
|
update_task(task_id, "failed", 0, "", error=str(e))
|
||||||
|
|||||||
Reference in New Issue
Block a user