137 lines
5.2 KiB
Python
137 lines
5.2 KiB
Python
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) |