3dcba4c44c
Deploy / Staging E2E Tests (push) Has been skipped
Deploy / Build Production Runtime Images (push) Has been skipped
Deploy / Deploy Production (push) Has been skipped
Deploy / Production Browser E2E (push) Has been skipped
Deploy / Deploy Staging (push) Failing after 137h5m41s
CI/CD Pipeline / Frontend Lint (push) Failing after 137h5m49s
CI/CD Pipeline / Validate Code Quality And Tests (push) Failing after 137h5m53s
140 lines
5.0 KiB
Python
Executable File
140 lines
5.0 KiB
Python
Executable File
"""视频合成 Celery 任务 — Phase 8 任务 2.10.
|
|
|
|
使用 JobService 管理任务生命周期,集成 VideoComposeService 执行合成。
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import tempfile
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
|
|
from celery.utils.log import get_task_logger
|
|
from worker_app.celery_app import celery_app
|
|
from worker_app.db import SessionLocal
|
|
|
|
logger = get_task_logger(__name__)
|
|
|
|
|
|
def _get_job_service():
|
|
"""延迟导入 JobService,避免循环依赖。"""
|
|
from apps.api.app.services.job_service import JobService
|
|
from packages.adapters.sqlalchemy_impl.job_repository import SQLAlchemyJobRepository
|
|
|
|
db = SessionLocal()
|
|
repo = SQLAlchemyJobRepository(db)
|
|
return JobService(repo), db
|
|
|
|
|
|
@celery_app.task(
|
|
name="worker.compose_video",
|
|
bind=True,
|
|
max_retries=3,
|
|
default_retry_delay=60,
|
|
)
|
|
def compose_video(self, job_id: str, **kwargs):
|
|
"""视频合成任务。
|
|
|
|
Args:
|
|
job_id: JobService 中的任务 ID
|
|
**kwargs: 来自 Job.payload 的额外参数(plan_id, output_path 等)
|
|
"""
|
|
job_service, db = _get_job_service()
|
|
|
|
try:
|
|
job = job_service.get_job(job_id)
|
|
if job is None:
|
|
logger.error("Job not found: %s", job_id)
|
|
return {"status": "error", "message": f"Job {job_id} not found"}
|
|
|
|
plan_id = job.payload.get("plan_id", "")
|
|
if not plan_id:
|
|
job_service.fail_job(job_id, "Missing plan_id in job payload")
|
|
return {"status": "error", "message": "Missing plan_id"}
|
|
|
|
# 标记为 running
|
|
job_service.update_progress(job_id, progress=10.0, current_stage="初始化合成环境")
|
|
|
|
# 延迟导入 VideoComposeService
|
|
from apps.api.app.services.video_compose_service import VideoComposeService
|
|
|
|
compose_svc = VideoComposeService(db)
|
|
|
|
# 校验合成条件
|
|
job_service.update_progress(job_id, progress=20.0, current_stage="校验合成条件")
|
|
validation = compose_svc.validate_compose(plan_id)
|
|
if not validation.valid:
|
|
error_msg = "; ".join(validation.errors)
|
|
job_service.fail_job(job_id, f"合成校验失败: {error_msg}")
|
|
return {"status": "error", "message": error_msg}
|
|
|
|
# 构建合成命令
|
|
job_service.update_progress(job_id, progress=30.0, current_stage="构建 FFmpeg 命令")
|
|
_output_dir = os.environ.get("VIDEO_OUTPUT_DIR", os.path.join(tempfile.gettempdir(), "video_output"))
|
|
output_path = os.path.join(_output_dir, f"{job_id}.mp4")
|
|
compose_cmd = compose_svc.build_compose_command(plan_id, output_path)
|
|
|
|
# 执行 FFmpeg
|
|
job_service.update_progress(job_id, progress=50.0, current_stage="正在执行视频合成")
|
|
logger.info("Executing FFmpeg for job %s, plan %s", job_id, plan_id)
|
|
|
|
try:
|
|
subprocess.run(
|
|
compose_cmd.command,
|
|
check=True,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
text=True,
|
|
timeout=3600,
|
|
)
|
|
except subprocess.CalledProcessError as e:
|
|
job_service.fail_job(job_id, f"FFmpeg 执行失败: {e.stderr[:500]}")
|
|
raise
|
|
|
|
# 上传结果
|
|
job_service.update_progress(job_id, progress=80.0, current_stage="上传合成结果")
|
|
storage_key = f"rendered/{plan_id}/{job_id}.mp4"
|
|
|
|
from worker_app.tasks.edit_plan_generation import _upload_to_oss
|
|
|
|
output_url = _upload_to_oss(Path(output_path), storage_key)
|
|
|
|
# 更新 Job 状态为完成
|
|
result_data = {
|
|
"plan_id": plan_id,
|
|
"output_path": output_path,
|
|
"storage_key": storage_key,
|
|
"output_url": output_url or "",
|
|
"estimated_duration": compose_cmd.estimated_duration,
|
|
"clip_count": len(compose_cmd.clip_chains),
|
|
}
|
|
job_service.complete_job(job_id, result=result_data)
|
|
|
|
logger.info("视频合成完成: job_id=%s, plan_id=%s", job_id, plan_id)
|
|
return {"status": "completed", "job_id": job_id, "result": result_data}
|
|
|
|
except self.retry_exc as exc:
|
|
logger.warning("视频合成重试中: job_id=%s, exc=%s", job_id, exc)
|
|
raise
|
|
except Exception as exc:
|
|
logger.exception("视频合成异常: job_id=%s", job_id)
|
|
try:
|
|
job_service.fail_job(job_id, str(exc)[:500])
|
|
except Exception:
|
|
logger.exception("更新 Job 失败状态时出错")
|
|
raise self.retry(exc=exc, countdown=60)
|
|
finally:
|
|
db.close()
|
|
# 清理临时文件
|
|
try:
|
|
_output_dir = os.environ.get("VIDEO_OUTPUT_DIR", os.path.join(tempfile.gettempdir(), "video_output"))
|
|
output_path = os.path.join(_output_dir, f"{job_id}.mp4")
|
|
if Path(output_path).exists():
|
|
Path(output_path).unlink()
|
|
except Exception as e:
|
|
logger.warning(f"Operation failed in apps/worker/worker_app/tasks/compose_video.py: {e}", exc_info=True)
|