Files
xiaoxia-saas/apps/worker/worker_app/tasks/voice_extraction.py
T
CI Bot 1a57878f76
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Failing after 1m12s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 2m17s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 1m33s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 3m14s
CI/CD Pipeline / Build & Push Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Build Production Runtime Images (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
chore(backend): Phase 3 清理 — 未使用依赖删除 + pyflakes 警告清零 + 测试文件冗余清理
1. 未使用依赖清理:
   - 从 requirements-base.txt 移除 cryptography 和 pyOpenSSL

2. pyflakes 警告清零 (apps/ + packages/ + tests/):
   - 移除 17 处未使用的 import (F401)
   - 修复 26 处未使用的局部变量 (F841):
     * 有副作用的赋值转为裸调用
     * 无副作用的赋值直接删除
   - 修复 1 处未使用的异常变量 (F841)
   - 修复 1 处空 except 块

3. 测试文件冗余清理:
   - 删除 tests/integration/test_project_management.py (模块级 skip,测试不存在的模块)
   - 删除 tests/integration/fixtures/duplication_routes_fixed.py (未被引用)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 16:14:46 +08:00

152 lines
5.5 KiB
Python

"""Voice extraction tasks - extract voice tracks and background music from videos."""
import logging
import os
import subprocess
import tempfile
from celery import Task
from worker_app.celery_app import celery_app
from worker_app.db import SessionLocal
from packages.adapters.sqlalchemy_impl.asset_repository import SQLAlchemyAssetRepository
from packages.shared.storage import get_storage_service
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)