feat: 实现 KPL 系统功能改进计划
Some checks failed
continuous-integration/drone/push Build is failing

1. 课程学习进度追踪
   - 新增 UserCourseProgress 和 UserMaterialProgress 模型
   - 新增 /api/v1/progress/* 进度追踪 API
   - 更新 admin.py 使用真实课程完成率数据

2. 路由权限检查完善
   - 新增前端 permissionChecker.ts 权限检查工具
   - 更新 router/guard.ts 实现团队和课程权限验证
   - 新增后端 permission_service.py

3. AI 陪练音频转文本
   - 新增 speech_recognition.py 语音识别服务
   - 新增 /api/v1/speech/* API
   - 更新 ai-practice-coze.vue 支持语音输入

4. 双人对练报告生成
   - 更新 practice_room_service.py 添加报告生成功能
   - 新增 /rooms/{room_code}/report API
   - 更新 duo-practice-report.vue 调用真实 API

5. 学习提醒推送
   - 新增 notification_service.py 通知服务
   - 新增 scheduler_service.py 定时任务服务
   - 支持钉钉、企微、站内消息推送

6. 智能学习推荐
   - 新增 recommendation_service.py 推荐服务
   - 新增 /api/v1/recommendations/* API
   - 支持错题、能力、进度、热门多维度推荐

7. 安全问题修复
   - DEBUG 默认值改为 False
   - 添加 SECRET_KEY 安全警告
   - 新增 check_security_settings() 检查函数

8. 证书 PDF 生成
   - 更新 certificate_service.py 添加 PDF 生成
   - 添加 weasyprint、Pillow、qrcode 依赖
   - 更新下载 API 支持 PDF 和 PNG 格式
This commit is contained in:
yuliang_guo
2026-01-30 14:22:35 +08:00
parent 9793013a56
commit 64f5d567fa
66 changed files with 18067 additions and 14330 deletions

View File

@@ -0,0 +1,145 @@
"""
语音识别 API
"""
from typing import Optional
from fastapi import APIRouter, Depends, File, UploadFile, Form, HTTPException
from pydantic import BaseModel
from app.core.database import get_db
from app.api.deps import get_current_user
from app.models.user import User
from app.services.speech_recognition import (
get_speech_recognition_service,
SpeechRecognitionError,
)
router = APIRouter()
class SpeechRecognitionRequest(BaseModel):
"""语音识别请求(文本形式)"""
text: str
session_id: Optional[int] = None
class SpeechRecognitionResponse(BaseModel):
"""语音识别响应"""
code: int = 200
message: str = "识别成功"
data: dict
@router.post("/recognize/text", response_model=SpeechRecognitionResponse)
async def recognize_text(
request: SpeechRecognitionRequest,
current_user: User = Depends(get_current_user),
):
"""
处理前端已识别的语音文本
用于 Web Speech API 识别后的文本传输
"""
service = get_speech_recognition_service("simple")
try:
text = await service.recognize_text(request.text)
return SpeechRecognitionResponse(
code=200,
message="识别成功",
data={
"text": text,
"session_id": request.session_id,
}
)
except SpeechRecognitionError as e:
raise HTTPException(status_code=400, detail=str(e))
@router.post("/recognize/audio", response_model=SpeechRecognitionResponse)
async def recognize_audio(
audio: UploadFile = File(...),
format: str = Form(default="wav"),
sample_rate: int = Form(default=16000),
engine: str = Form(default="aliyun"),
current_user: User = Depends(get_current_user),
):
"""
识别音频文件
支持的音频格式wav, pcm, mp3, ogg, opus
支持的识别引擎aliyun, xunfei
"""
# 读取音频数据
audio_data = await audio.read()
if len(audio_data) == 0:
raise HTTPException(status_code=400, detail="音频文件为空")
if len(audio_data) > 10 * 1024 * 1024: # 10MB 限制
raise HTTPException(status_code=400, detail="音频文件过大,最大支持 10MB")
service = get_speech_recognition_service(engine)
try:
text = await service.recognize_audio(audio_data, format, sample_rate)
return SpeechRecognitionResponse(
code=200,
message="识别成功",
data={
"text": text,
"format": format,
"sample_rate": sample_rate,
"engine": engine,
}
)
except SpeechRecognitionError as e:
raise HTTPException(status_code=400, detail=str(e))
except NotImplementedError as e:
raise HTTPException(status_code=501, detail=str(e))
@router.get("/engines")
async def get_available_engines(
current_user: User = Depends(get_current_user),
):
"""
获取可用的语音识别引擎列表
"""
import os
engines = [
{
"id": "simple",
"name": "浏览器语音识别",
"description": "使用浏览器内置的 Web Speech API 进行语音识别",
"available": True,
},
{
"id": "aliyun",
"name": "阿里云智能语音",
"description": "使用阿里云 NLS 服务进行高精度语音识别",
"available": all([
os.getenv("ALIYUN_ACCESS_KEY_ID"),
os.getenv("ALIYUN_ACCESS_KEY_SECRET"),
os.getenv("ALIYUN_NLS_APP_KEY"),
]),
},
{
"id": "xunfei",
"name": "讯飞语音识别",
"description": "使用讯飞 IAT 服务进行语音识别",
"available": all([
os.getenv("XUNFEI_APP_ID"),
os.getenv("XUNFEI_API_KEY"),
os.getenv("XUNFEI_API_SECRET"),
]),
},
]
return {
"code": 200,
"message": "获取成功",
"data": {
"engines": engines,
"default": "simple",
}
}