""" 音频上传后台处理服务 将上传后的重操作放到后台线程执行,避免请求长时间阻塞: 1. 音频预处理 2. 更新音频文件记录 3. 启动转录 4. 启动自动总结监控 """ from __future__ import annotations import json import os from pathlib import Path from typing import Optional from app.core.config import BACKGROUND_TASK_CONFIG, BASE_DIR from app.services.async_transcription_service import AsyncTranscriptionService from app.services.audio_preprocess_service import audio_preprocess_service from app.services.audio_service import handle_audio_upload from app.services.background_task_runner import KeyedBackgroundTaskRunner upload_task_runner = KeyedBackgroundTaskRunner( max_workers=max(1, int(BACKGROUND_TASK_CONFIG.get("upload_workers", 2))), thread_name_prefix="imeeting-audio-upload", ) class AudioUploadTaskService: def __init__(self): self.transcription_service = AsyncTranscriptionService() def enqueue_upload_processing( self, *, meeting_id: int, original_file_path: str, current_user: dict, auto_summarize: bool, prompt_id: Optional[int] = None, model_code: Optional[str] = None, ) -> str: task_id = self.transcription_service.create_local_processing_task( meeting_id=meeting_id, status="processing", progress=5, ) upload_task_runner.submit( f"audio-upload:{task_id}", self._process_uploaded_audio, task_id, meeting_id, original_file_path, current_user, auto_summarize, prompt_id, model_code, ) return task_id def _process_uploaded_audio( self, task_id: str, meeting_id: int, original_file_path: str, current_user: dict, auto_summarize: bool, prompt_id: Optional[int], model_code: Optional[str], ) -> None: source_absolute_path = BASE_DIR / original_file_path.lstrip("/") processed_absolute_path: Optional[Path] = None handoff_to_audio_service = False try: self.transcription_service.update_local_processing_task(task_id, "processing", 15, None) preprocess_result = audio_preprocess_service.preprocess(source_absolute_path) processed_absolute_path = preprocess_result.file_path audio_duration = preprocess_result.metadata.duration_seconds file_path = "/" + str(processed_absolute_path.relative_to(BASE_DIR)) print( f"[AudioUploadTaskService] 音频预处理完成: source={source_absolute_path.name}, " f"target={processed_absolute_path.name}, duration={audio_duration}s, " f"applied={preprocess_result.applied}" ) self.transcription_service.update_local_processing_task(task_id, "processing", 40, None) handoff_to_audio_service = True result = handle_audio_upload( file_path=file_path, file_name=preprocess_result.file_name, file_size=preprocess_result.file_size, meeting_id=meeting_id, current_user=current_user, auto_summarize=auto_summarize, background_tasks=None, prompt_id=prompt_id, model_code=model_code, duration=audio_duration, transcription_task_id=task_id, ) if not result["success"]: raise RuntimeError(self._extract_response_message(result["response"])) if preprocess_result.applied and processed_absolute_path != source_absolute_path and source_absolute_path.exists(): try: os.remove(source_absolute_path) except OSError: pass except Exception as exc: error_message = str(exc) print(f"[AudioUploadTaskService] 音频后台处理失败, task_id={task_id}, meeting_id={meeting_id}: {error_message}") self.transcription_service.update_local_processing_task(task_id, "failed", 0, error_message) if handoff_to_audio_service: return cleanup_targets = [] if processed_absolute_path: cleanup_targets.append(processed_absolute_path) if source_absolute_path.exists(): cleanup_targets.append(source_absolute_path) deduped_targets: list[Path] = [] for target in cleanup_targets: if target not in deduped_targets: deduped_targets.append(target) for target in deduped_targets: if target.exists(): try: os.remove(target) except OSError: pass @staticmethod def _extract_response_message(response) -> str: body = getattr(response, "body", None) if not body: return "音频处理失败" try: payload = json.loads(body.decode("utf-8")) return payload.get("message") or "音频处理失败" except Exception: return "音频处理失败" audio_upload_task_service = AudioUploadTaskService()