Files
wecom_it_smart_desk/backend/app/api/voice_asr.py
T

207 lines
7.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# =============================================================================
# 企微IT智能服务台 — 百度语音识别(ASR)API
# =============================================================================
# 说明:接收前端上传的 PCM 音频数据,调用百度 ASR API 转换为文字
# 1. POST /api/voice/asr — 语音转文字(上传PCM音频,返回识别文字)
#
# 音频格式要求:PCM, 16kHz, 16-bit, 单声道
# 百度ASR凭证通过环境变量配置(BAIDU_ASR_APP_ID/API_KEY/SECRET_KEY
# access_token 缓存在 Rediskey=baidu:asr:tokenTTL=30天)
# =============================================================================
import logging
from typing import Optional
import httpx
from fastapi import APIRouter, File, HTTPException, UploadFile
from app.config import settings
from app.utils.response import success_response, error_response
router = APIRouter(prefix="/voice", tags=["语音识别"])
logger = logging.getLogger(__name__)
# --------------------------------------------------------------------------
# 百度 ASR 配置常量
# --------------------------------------------------------------------------
# 百度 ASR 极速版 API 端点(raw binary POST 方式)
BAIDU_ASR_API_URL = "https://vop.baidu.com/pro_api"
# 百度 OAuth2 token 获取端点
BAIDU_TOKEN_URL = "https://aip.baidubce.com/oauth/2.0/token"
# dev_pid=80001 表示普通话极速版
BAIDU_ASR_DEV_PID = "80001"
# Redis 缓存 key
BAIDU_TOKEN_CACHE_KEY = "baidu:asr:token"
# token 缓存 TTL30天,秒)— 与百度 token有效期一致
BAIDU_TOKEN_TTL = 2592000
async def get_baidu_token() -> str:
"""获取百度 ASR access_token,带 Redis 缓存。
优先从 Redis 读取缓存的 tokenTTL 30天);
缓存不存在时调用百度 OAuth2 接口获取新 token 并缓存。
Returns:
str: 百度 ASR access_token
Raises:
HTTPException: 获取 token 失败时抛出 500 错误
"""
# 防御性检查:确保百度 ASR 凭证已配置
if not settings.baidu_asr_api_key or not settings.baidu_asr_secret_key:
logger.error(
"[BaiduASR] 配置缺失:BAIDU_ASR_API_KEY 或 SECRET_KEY 未设置,"
"无法获取 access_token"
)
raise HTTPException(
status_code=500,
detail="百度ASR配置缺失:BAIDU_ASR_API_KEY或SECRET_KEY未设置",
)
redis = settings.create_redis_client()
try:
# 1. 尝试从 Redis 缓存读取 token
cached_token: Optional[str] = await redis.get(BAIDU_TOKEN_CACHE_KEY)
if cached_token:
logger.debug("[BaiduASR] 使用缓存的 access_token")
return cached_token
# 2. 缓存不存在,调用百度 OAuth2 获取新 token
logger.info("[BaiduASR] 缓存未命中,请求新 access_token")
async with httpx.AsyncClient(timeout=10) as client:
resp = await client.post(
BAIDU_TOKEN_URL,
params={
"grant_type": "client_credentials",
"client_id": settings.baidu_asr_api_key,
"client_secret": settings.baidu_asr_secret_key,
},
)
data = resp.json()
token = data.get("access_token")
if not token:
error_msg = data.get("error_description", "未知错误")
logger.error(f"[BaiduASR] 获取 access_token 失败: {error_msg}")
raise HTTPException(
status_code=500,
detail=f"获取百度ASR token失败: {error_msg}",
)
# 3. 缓存 token 到 RedisTTL 30天)
await redis.set(BAIDU_TOKEN_CACHE_KEY, token, ex=BAIDU_TOKEN_TTL)
logger.info("[BaiduASR] access_token 已缓存到 RedisTTL=30天)")
return token
finally:
# 关闭 Redis 连接(每次创建新客户端,用完即关)
await redis.aclose()
@router.post("/asr")
async def transcribe_audio(
audio: UploadFile = File(..., description="PCM 音频数据(16kHz, 16-bit, mono"),
):
"""语音转文字 — 上传 PCM 音频,返回百度 ASR 识别结果。
处理流程:
1. 接收前端上传的 PCM 音频数据(multipart/form-data,字段名 audio
2. 获取百度 access_tokenRedis 缓存,TTL 30天)
3. 调用百度 ASR APIraw binary POST 方式)
4. 解析返回结果,返回识别文字
**请求格式**: multipart/form-data
**字段**: audio — PCM 音频文件(16kHz, 16-bit, 单声道)
**成功响应**: {code: 0, data: {text: "识别的文字"}, message: "success"}
**错误响应**: {code: 3001, data: null, message: "百度ASR错误: xxx"}
Args:
audio: FastAPI UploadFile 对象,包含 PCM 音频数据
Returns:
Dict: 统一响应格式
"""
# 1. 读取 PCM 音频数据
pcm_data = await audio.read()
if not pcm_data:
raise HTTPException(status_code=400, detail="未收到音频数据")
logger.info(
f"[BaiduASR] 收到音频数据: {len(pcm_data)} bytes, "
f"filename={audio.filename}"
)
# 防御性检查:确保百度 ASR AppID 已配置
# cuid 为空时百度会返回 "url param cuid error"
if not settings.baidu_asr_app_id:
logger.error(
"[BaiduASR] 配置缺失:BAIDU_ASR_APP_ID 未设置,"
"cuid 将为空,百度会返回 url param cuid error"
)
return error_response(
code=3001,
message="百度ASR配置缺失:BAIDU_ASR_APP_ID未设置",
)
logger.debug(
f"[BaiduASR] 配置检查通过: app_id={settings.baidu_asr_app_id}, "
f"api_key={'已设置' if settings.baidu_asr_api_key else '未设置'}, "
f"secret_key={'已设置' if settings.baidu_asr_secret_key else '未设置'}"
)
# 2. 获取百度 access_token
try:
token = await get_baidu_token()
except HTTPException:
raise
except Exception as e:
logger.error(f"[BaiduASR] 获取 token 异常: {e}")
return error_response(code=3001, message=f"获取百度ASR token失败: {str(e)}")
# 3. 调用百度 ASR APIraw binary POST
try:
async with httpx.AsyncClient(timeout=30) as client:
resp = await client.post(
BAIDU_ASR_API_URL,
params={
"dev_pid": BAIDU_ASR_DEV_PID,
"cuid": settings.baidu_asr_app_id,
"token": token,
},
content=pcm_data,
headers={"Content-Type": "audio/pcm;rate=16000"},
)
result = resp.json()
# 4. 解析返回结果
# 百度 ASR 返回格式: {"err_no": 0, "result": ["识别的文字"]}
# err_no=0 表示成功,非0表示错误
if result.get("err_no") != 0:
err_msg = result.get("err_msg", "未知错误")
err_no = result.get("err_no", -1)
logger.error(f"[BaiduASR] 识别失败: err_no={err_no}, err_msg={err_msg}")
return error_response(
code=3001,
message=f"百度ASR错误: {err_msg}",
)
# 成功:提取识别文字
text_list = result.get("result", [])
recognized_text = text_list[0] if text_list else ""
logger.info(
f"[BaiduASR] 识别成功: text='{recognized_text[:50]}...' "
f"(len={len(recognized_text)})"
)
return success_response(data={"text": recognized_text})
except httpx.TimeoutException:
logger.error("[BaiduASR] 调用百度 ASR API 超时(30秒)")
return error_response(code=3001, message="百度ASR识别超时,请重试")
except Exception as e:
logger.error(f"[BaiduASR] 调用百度 ASR API 异常: {e}")
return error_response(
code=3001,
message=f"语音识别失败: {str(e)}",
)