207 lines
7.9 KiB
Python
207 lines
7.9 KiB
Python
# =============================================================================
|
||
# 企微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 缓存在 Redis(key=baidu:asr:token,TTL=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 缓存 TTL(30天,秒)— 与百度 token有效期一致
|
||
BAIDU_TOKEN_TTL = 2592000
|
||
|
||
|
||
async def get_baidu_token() -> str:
|
||
"""获取百度 ASR access_token,带 Redis 缓存。
|
||
|
||
优先从 Redis 读取缓存的 token(TTL 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 到 Redis(TTL 30天)
|
||
await redis.set(BAIDU_TOKEN_CACHE_KEY, token, ex=BAIDU_TOKEN_TTL)
|
||
logger.info("[BaiduASR] access_token 已缓存到 Redis(TTL=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_token(Redis 缓存,TTL 30天)
|
||
3. 调用百度 ASR API(raw 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 API(raw 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)}",
|
||
)
|