Files
wecom_it_smart_desk/backend/app/api/voice_asr.py
Simon bea288e414 feat: 2026-07-11 全量更新 - 代办集成+会议室预定+知识迭代修复+UI统一+Bug修复
== 已部署上线 (9项) ==
- 代办事项真实数据源集成 (企微审批API 8bug修复链)
- H5/坐席端 Logo样式统一+绿色背景
- 视频引导页修复 (localStorage key v2)
- 坐席端 v9 Vue版本修复 (ElMessage._context)
- 截图按钮 v10 修复 (getDisplayMedia user gesture)
- 扫码样式恢复+H5扫码登录跳转修复
- H5截图快捷键提示

== 代码完成待部署 (3项) ==
- 知识迭代3Bug修复 (#8 POST端点/#7 MERGE幂等/#6 过期检查)
- 会议室预定-小鱼易联终端 (40文件, 40/40测试通过)
- IT资产升级审批推送 (asset_service.py)

== 需求文档 (2项) ==
- 坐席端AI辅助消息框-PRD (4项新功能确认)
- 坐席端布局优化建议 v2.0 (7天计划)

== 新增文档 ==
- 日报-2026-07-11.md
- 知识迭代Bug修复报告-20260711.md
- 会议室预定-部署指南.md
- CHANGELOG.md 更新

== 测试 ==
- test_todo_integration.py: 40/40
- test_meetingroom.py: 40/40
- test_bugfix_ki_suggestions.py: 21/21
2026-07-11 23:13:10 +08:00

178 lines
6.7 KiB
Python
Raw Permalink 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 错误
"""
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}"
)
# 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)}",
)