8210806632
1. 修复 Worker 启动入口: celery_app → worker_app.celery_app - 旧 celery_app.py 不导入任何任务模块,导致 Worker 注册 0 个任务 - 删除旧版 apps/worker/celery_app.py,统一使用 worker_app/celery_app.py 2. 为缺少装饰器的任务补充 @celery_app.task: - classification.py: classify_asset() 添加装饰器 - generation.py: generate_video() 添加装饰器,修正签名匹配 API 调用方式 3. 确保 voice_extraction 任务被正确注册: - 添加 voice_extraction 到 celery_app imports - 修复 voice_extraction.py 中错误的相对导入 (.celery_app → worker_app.celery_app) - 修复 dedup.py 中指向已删除模块的导入 4. 修复 worker_app/celery_app.py: - 添加 broker_connection_retry_on_startup=True - imports 中添加 voice_extraction 和 dedup 模块 5. 修复 Dockerfile: - CMD 改为 celery -A worker_app.celery_app - 添加非 root 用户 celery 运行 Worker 6. 新建 packages/shared/config.py 和 storage.py 兼容层 - 为 worker 任务模块提供统一的 config/storage 访问入口
119 lines
5.2 KiB
Python
119 lines
5.2 KiB
Python
"""Voice extraction tasks - extract voice tracks and background music from videos."""
|
|
import json
|
|
import logging
|
|
import os
|
|
import subprocess
|
|
import tempfile
|
|
from typing import Optional
|
|
|
|
from celery import Task
|
|
from sqlalchemy.orm import Session
|
|
|
|
from packages.adapters.sqlalchemy_impl.asset_repository import SQLAlchemyAssetRepository
|
|
from packages.shared.storage import get_storage_service
|
|
|
|
from worker_app.celery_app import celery_app
|
|
from worker_app.db import SessionLocal
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class VoiceExtractor:
|
|
"""Extract voice tracks and background music from videos using FFmpeg."""
|
|
|
|
@staticmethod
|
|
def _run_ffmpeg(cmd: list[str]) -> subprocess.CompletedProcess:
|
|
"""Run FFmpeg command and return result."""
|
|
logger.info(f"Running FFmpeg: {chr(39).join(cmd)}")
|
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
if result.returncode != 0:
|
|
logger.error(f"FFmpeg error: {result.stderr}")
|
|
raise RuntimeError(f"FFmpeg failed: {result.stderr}")
|
|
return result
|
|
|
|
def extract_voice(self, input_path: str, output_path: str, highpass: int = 200, bandpass_freq: int = 300, bandpass_width: int = 3000, noise_reduction: int = 20) -> str:
|
|
"""Extract voice track from video using FFmpeg."""
|
|
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
|
cmd = ["ffmpeg", "-y", "-i", input_path, "-af", f"highpass=f={highpass},afftdn=bn={noise_reduction},bandpass=f={bandpass_freq}:width_type=h:width={bandpass_width},loudnorm", "-vn", "-acodec", "libmp3lame", "-q:a", "2", output_path]
|
|
self._run_ffmpeg(cmd)
|
|
logger.info(f"Voice extracted to: {output_path}")
|
|
return output_path
|
|
|
|
def extract_background(self, input_path: str, output_path: str, lowpass: int = 200) -> str:
|
|
"""Extract background music from video."""
|
|
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
|
cmd = ["ffmpeg", "-y", "-i", input_path, "-af", f"lowpass=f={lowpass},loudnorm", "-vn", "-acodec", "libmp3lame", "-q:a", "2", output_path]
|
|
self._run_ffmpeg(cmd)
|
|
logger.info(f"Background extracted to: {output_path}")
|
|
return output_path
|
|
|
|
|
|
@celery_app.task(bind=True, max_retries=3, name="worker.extract_voice")
|
|
def extract_voice_task(self: Task, asset_id: str) -> dict:
|
|
session = SessionLocal()
|
|
temp_dir = tempfile.mkdtemp()
|
|
try:
|
|
asset_repo = SQLAlchemyAssetRepository(session)
|
|
storage_service = get_storage_service()
|
|
extractor = VoiceExtractor()
|
|
asset = asset_repo.get(asset_id)
|
|
if asset is None:
|
|
raise ValueError(f"Asset {asset_id} not found")
|
|
local_video_path = os.path.join(temp_dir, f"{asset_id}.mp4")
|
|
storage_service.download_file(asset.storage_key, local_video_path)
|
|
voice_output_path = os.path.join(temp_dir, f"{asset_id}_voice.mp3")
|
|
extractor.extract_voice(local_video_path, voice_output_path)
|
|
voice_storage_key = f"assets/{asset_id}/voice.mp3"
|
|
storage_service.upload_file(voice_output_path, voice_storage_key)
|
|
voice_url = storage_service.get_url(voice_storage_key)
|
|
if asset.metadata is None:
|
|
asset.metadata = {}
|
|
asset.metadata["voice_url"] = voice_url
|
|
asset_repo.update(asset)
|
|
session.commit()
|
|
logger.info(f"Voice extraction completed for asset {asset_id}: {voice_url}")
|
|
return {"ok": True, "asset_id": asset_id, "voice_url": voice_url}
|
|
except Exception as e:
|
|
logger.error(f"Voice extraction failed for {asset_id}: {str(e)}")
|
|
session.rollback()
|
|
raise self.retry(exc=e, countdown=60)
|
|
finally:
|
|
session.close()
|
|
import shutil
|
|
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
|
|
|
|
@celery_app.task(bind=True, max_retries=3, name="worker.extract_background")
|
|
def extract_background_task(self: Task, asset_id: str) -> dict:
|
|
session = SessionLocal()
|
|
temp_dir = tempfile.mkdtemp()
|
|
try:
|
|
asset_repo = SQLAlchemyAssetRepository(session)
|
|
storage_service = get_storage_service()
|
|
extractor = VoiceExtractor()
|
|
asset = asset_repo.get(asset_id)
|
|
if asset is None:
|
|
raise ValueError(f"Asset {asset_id} not found")
|
|
local_video_path = os.path.join(temp_dir, f"{asset_id}.mp4")
|
|
storage_service.download_file(asset.storage_key, local_video_path)
|
|
bg_output_path = os.path.join(temp_dir, f"{asset_id}_background.mp3")
|
|
extractor.extract_background(local_video_path, bg_output_path)
|
|
bg_storage_key = f"assets/{asset_id}/background.mp3"
|
|
storage_service.upload_file(bg_output_path, bg_storage_key)
|
|
bg_url = storage_service.get_url(bg_storage_key)
|
|
if asset.metadata is None:
|
|
asset.metadata = {}
|
|
asset.metadata["background_url"] = bg_url
|
|
asset_repo.update(asset)
|
|
session.commit()
|
|
logger.info(f"Background extraction completed for asset {asset_id}: {bg_url}")
|
|
return {"ok": True, "asset_id": asset_id, "background_url": bg_url}
|
|
except Exception as e:
|
|
logger.error(f"Background extraction failed for {asset_id}: {str(e)}")
|
|
session.rollback()
|
|
raise self.retry(exc=e, countdown=60)
|
|
finally:
|
|
session.close()
|
|
import shutil
|
|
shutil.rmtree(temp_dir, ignore_errors=True)
|