Files
xiaoxia-saas/apps/worker/worker_app/tasks/voice_extraction.py
T
Celery Worker Fix Agent 8210806632
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Lint (pull_request) Has been cancelled
fix: 修复 Celery Worker 任务注册和导入问题
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 访问入口
2026-06-27 19:31:07 +08:00

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)