import asyncio import logging from typing import Optional import httpx from app.conf.app_config import ASRConfig, app_config logger = logging.getLogger(__name__) class Qwen3ASRClient: def __init__(self, config: ASRConfig): self.config = config self._session: Optional[httpx.AsyncClient] = None @property def session(self) -> httpx.AsyncClient: if self._session is None: timeout = httpx.Timeout(120.0, connect=10.0, read=60.0) headers = {} if self.config.api_key and self.config.api_key.strip(): headers["Authorization"] = f"Bearer {self.config.api_key.strip()}" self._session = httpx.AsyncClient( timeout=timeout, follow_redirects=True, headers=headers ) return self._session def _get_submit_url(self) -> str: return f"{self.config.base_url}/api/v1/services/audio/asr/transcription" def _get_task_url(self, task_id: str) -> str: return f"{self.config.base_url}/api/v1/tasks/{task_id}" async def _submit_task(self, audio_url: str) -> str: url = self._get_submit_url() payload = { "model": "qwen3-asr-flash-filetrans", "input": { "file_url": audio_url }, "parameters": { "enable_itn": False } } headers = { "Content-Type": "application/json", "X-DashScope-Async": "enable" } response = await self.session.post(url, json=payload, headers=headers) if response.status_code >= 400: logger.error(f"ASR任务提交失败,状态码: {response.status_code}, 响应: {response.text}") response.raise_for_status() result = response.json() if "output" in result and "task_id" in result["output"]: task_id = result["output"]["task_id"] logger.info(f"ASR任务提交成功,task_id: {task_id}") return task_id else: logger.error(f"ASR任务提交失败,响应: {result}") raise ValueError(f"Failed to submit ASR task: {result}") async def _poll_task_result(self, task_id: str, max_attempts: int = 60, interval: float = 2.0) -> str: url = self._get_task_url(task_id) for attempt in range(max_attempts): response = await self.session.get(url) response.raise_for_status() result = response.json() task_status = result.get("output", {}).get("task_status", "") if task_status == "SUCCEEDED": logger.info(f"ASR任务完成,task_id: {task_id}") output = result.get("output", {}) result_data = output.get("result", {}) transcription_url = result_data.get("transcription_url", "") if transcription_url: logger.info(f"下载识别结果: {transcription_url}") trans_response = await self.session.get(transcription_url) trans_response.raise_for_status() trans_data = trans_response.json() texts = [] if "transcripts" in trans_data: for item in trans_data["transcripts"]: if "text" in item: texts.append(item["text"]) elif "text" in trans_data: texts.append(trans_data["text"]) text = "\n".join(texts).strip() else: text = str(output) logger.info(f"识别结果长度: {len(text)}") return text elif task_status in ("FAILED", "UNKNOWN"): error_msg = result.get("output", {}).get("message", f"Task {task_status}") logger.error(f"ASR任务失败,task_id: {task_id}, 错误: {error_msg}") raise RuntimeError(f"ASR task failed: {error_msg}") logger.debug(f"ASR任务进行中,task_id: {task_id}, 状态: {task_status}, 第{attempt + 1}次轮询") await asyncio.sleep(interval) raise TimeoutError(f"ASR task timed out after {max_attempts} attempts") async def recognize_from_url_direct(self, audio_url: str) -> str: logger.info(f"开始ASR URL直传识别,URL: {audio_url}") try: task_id = await self._submit_task(audio_url) result = await self._poll_task_result(task_id) return result except Exception as e: logger.error(f"ASR URL直传识别失败: {str(e)}", exc_info=True) raise async def recognize(self, audio_data: bytes, format: str = "wav", **kwargs) -> str: raise NotImplementedError("Audio data upload not implemented yet, use recognize_from_url_direct") async def recognize_from_file(self, file_path: str) -> str: raise NotImplementedError("Local file upload not implemented yet, use recognize_from_url_direct") async def close(self): if self._session: await self._session.aclose() self._session = None asr_client = Qwen3ASRClient(app_config.asr)