修改所有bug
This commit is contained in:
parent
c01d45b200
commit
dfb1c4a7b7
|
|
@ -275,7 +275,6 @@ public class StudyClassUserController extends BaseController
|
||||||
StudentImportData sample = new StudentImportData();
|
StudentImportData sample = new StudentImportData();
|
||||||
sample.setUserId("001");
|
sample.setUserId("001");
|
||||||
sample.setUserName("张三");
|
sample.setUserName("张三");
|
||||||
sample.setPrisonName("XX监狱");
|
|
||||||
sample.setPrisonArea("一监区");
|
sample.setPrisonArea("一监区");
|
||||||
sample.setClassName("一班");
|
sample.setClassName("一班");
|
||||||
sample.setSex("男");
|
sample.setSex("男");
|
||||||
|
|
|
||||||
|
|
@ -515,6 +515,20 @@ public class StudyVoiceEvaluationController extends BaseController
|
||||||
|
|
||||||
Map<String, Object> evaluationResult = voiceService.evaluateVoice(fileName, content, language, format);
|
Map<String, Object> evaluationResult = voiceService.evaluateVoice(fileName, content, language, format);
|
||||||
|
|
||||||
|
// ✅ 从resultDetail中提取recognizedText
|
||||||
|
String recognizedText = "";
|
||||||
|
String resultDetail = (String) evaluationResult.get("resultDetail");
|
||||||
|
if (resultDetail != null && resultDetail.contains("recognizedText")) {
|
||||||
|
try {
|
||||||
|
com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
|
||||||
|
Map<String, Object> detailMap = mapper.readValue(resultDetail, Map.class);
|
||||||
|
recognizedText = (String) detailMap.getOrDefault("recognizedText", "");
|
||||||
|
logger.info("✅ 提取识别文本: {}", recognizedText);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("解析resultDetail失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 保存评测记录
|
// 保存评测记录
|
||||||
StudyVoiceEvaluation voiceEvaluation = new StudyVoiceEvaluation();
|
StudyVoiceEvaluation voiceEvaluation = new StudyVoiceEvaluation();
|
||||||
voiceEvaluation.setStudentId(studentId);
|
voiceEvaluation.setStudentId(studentId);
|
||||||
|
|
@ -537,6 +551,8 @@ public class StudyVoiceEvaluationController extends BaseController
|
||||||
AjaxResult ajax = AjaxResult.success("评测完成");
|
AjaxResult ajax = AjaxResult.success("评测完成");
|
||||||
ajax.put("evaluation", voiceEvaluation);
|
ajax.put("evaluation", voiceEvaluation);
|
||||||
ajax.put("audioUrl", audioUrl);
|
ajax.put("audioUrl", audioUrl);
|
||||||
|
// ✅ 添加recognizedText到返回数据
|
||||||
|
ajax.put("recognizedText", recognizedText);
|
||||||
return ajax;
|
return ajax;
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ spring:
|
||||||
master:
|
master:
|
||||||
url: jdbc:mysql://127.0.0.1:3306/study?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true
|
url: jdbc:mysql://127.0.0.1:3306/study?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true
|
||||||
username: root
|
username: root
|
||||||
password: root
|
password: 123456
|
||||||
# 从库数据源
|
# 从库数据源
|
||||||
slave:
|
slave:
|
||||||
# 从数据源开关/默认关闭
|
# 从数据源开关/默认关闭
|
||||||
|
|
|
||||||
|
|
@ -20,12 +20,11 @@ public class StudentImportData
|
||||||
@Excel(name = "罪犯姓名", sort = 2, prompt = "必填项,如:张三")
|
@Excel(name = "罪犯姓名", sort = 2, prompt = "必填项,如:张三")
|
||||||
private String userName;
|
private String userName;
|
||||||
|
|
||||||
/** 监狱(可选) */
|
/** 监狱(不导出到模板,仅保留字段用于兼容旧数据导入) */
|
||||||
@Excel(name = "监狱", sort = 3, prompt = "可选,如:第一监狱")
|
|
||||||
private String prisonName;
|
private String prisonName;
|
||||||
|
|
||||||
/** 监区 - 支持多种列名匹配 */
|
/** 监区 - 支持多种列名匹配 */
|
||||||
@Excel(name = "监区", sort = 4, prompt = "必填项,如:第三监区")
|
@Excel(name = "监区", sort = 3, prompt = "必填项,如:第三监区")
|
||||||
private String prisonArea;
|
private String prisonArea;
|
||||||
|
|
||||||
/** 班级名称(用于按名称匹配班级,可选) */
|
/** 班级名称(用于按名称匹配班级,可选) */
|
||||||
|
|
|
||||||
|
|
@ -48,22 +48,27 @@ public class StudyVoiceEvaluation extends BaseEntity
|
||||||
|
|
||||||
/** 评分(总分,0-100) */
|
/** 评分(总分,0-100) */
|
||||||
@Excel(name = "总分", cellType = ColumnType.NUMERIC)
|
@Excel(name = "总分", cellType = ColumnType.NUMERIC)
|
||||||
|
@com.fasterxml.jackson.databind.annotation.JsonSerialize(using = com.fasterxml.jackson.databind.ser.std.ToStringSerializer.class)
|
||||||
private BigDecimal score;
|
private BigDecimal score;
|
||||||
|
|
||||||
/** 准确度(0-100) */
|
/** 准确度(0-100) */
|
||||||
@Excel(name = "准确度", cellType = ColumnType.NUMERIC)
|
@Excel(name = "准确度", cellType = ColumnType.NUMERIC)
|
||||||
|
@com.fasterxml.jackson.databind.annotation.JsonSerialize(using = com.fasterxml.jackson.databind.ser.std.ToStringSerializer.class)
|
||||||
private BigDecimal accuracy;
|
private BigDecimal accuracy;
|
||||||
|
|
||||||
/** 流畅度(0-100) */
|
/** 流畅度(0-100) */
|
||||||
@Excel(name = "流畅度", cellType = ColumnType.NUMERIC)
|
@Excel(name = "流畅度", cellType = ColumnType.NUMERIC)
|
||||||
|
@com.fasterxml.jackson.databind.annotation.JsonSerialize(using = com.fasterxml.jackson.databind.ser.std.ToStringSerializer.class)
|
||||||
private BigDecimal fluency;
|
private BigDecimal fluency;
|
||||||
|
|
||||||
/** 完整度(0-100) */
|
/** 完整度(0-100) */
|
||||||
@Excel(name = "完整度", cellType = ColumnType.NUMERIC)
|
@Excel(name = "完整度", cellType = ColumnType.NUMERIC)
|
||||||
|
@com.fasterxml.jackson.databind.annotation.JsonSerialize(using = com.fasterxml.jackson.databind.ser.std.ToStringSerializer.class)
|
||||||
private BigDecimal completeness;
|
private BigDecimal completeness;
|
||||||
|
|
||||||
/** 发音得分(0-100) */
|
/** 发音得分(0-100) */
|
||||||
@Excel(name = "发音得分", cellType = ColumnType.NUMERIC)
|
@Excel(name = "发音得分", cellType = ColumnType.NUMERIC)
|
||||||
|
@com.fasterxml.jackson.databind.annotation.JsonSerialize(using = com.fasterxml.jackson.databind.ser.std.ToStringSerializer.class)
|
||||||
private BigDecimal pronunciation;
|
private BigDecimal pronunciation;
|
||||||
|
|
||||||
/** 评测详情(JSON格式,包含详细评测结果) */
|
/** 评测详情(JSON格式,包含详细评测结果) */
|
||||||
|
|
|
||||||
|
|
@ -123,13 +123,27 @@ public class DeepSeekService {
|
||||||
return parseEvaluationResponse(response, recognizedText, standardText);
|
return parseEvaluationResponse(response, recognizedText, standardText);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理文本:去除标点符号和空格,只保留文字
|
||||||
|
*/
|
||||||
|
private String cleanText(String text) {
|
||||||
|
if (text == null) return "";
|
||||||
|
// 去除所有标点符号(\p{P})、符号(\p{S})和空格(\s),只保留中文、英文、数字
|
||||||
|
return text.replaceAll("[\\p{P}\\p{S}\\s]+", "").toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 构建评测提示词
|
* 构建评测提示词
|
||||||
*/
|
*/
|
||||||
private String buildEvaluationPrompt(String recognizedText, String standardText) {
|
private String buildEvaluationPrompt(String recognizedText, String standardText) {
|
||||||
|
// ✅ 清理标点符号,只评测文字内容
|
||||||
|
String cleanRecognized = cleanText(recognizedText);
|
||||||
|
String cleanStandard = cleanText(standardText);
|
||||||
|
|
||||||
return "你是一位专业的语音评测专家。请对以下语音识别结果进行评测:\n\n" +
|
return "你是一位专业的语音评测专家。请对以下语音识别结果进行评测:\n\n" +
|
||||||
"[标准文本] " + standardText + "\n" +
|
"[标准文本] " + cleanStandard + "\n" +
|
||||||
"[识别文本] " + recognizedText + "\n\n" +
|
"[识别文本] " + cleanRecognized + "\n\n" +
|
||||||
|
"注意:已去除标点符号,只评测文字内容。\n\n" +
|
||||||
"请从以下维度进行评分(每项0-100分):\n" +
|
"请从以下维度进行评分(每项0-100分):\n" +
|
||||||
"1. 准确度(accuracy): 文本是否与标准一致\n" +
|
"1. 准确度(accuracy): 文本是否与标准一致\n" +
|
||||||
"2. 完整度(completeness): 是否完整表达了标准内容\n" +
|
"2. 完整度(completeness): 是否完整表达了标准内容\n" +
|
||||||
|
|
@ -218,8 +232,9 @@ public class DeepSeekService {
|
||||||
* 计算文本相似度
|
* 计算文本相似度
|
||||||
*/
|
*/
|
||||||
private double calculateSimilarity(String text1, String text2) {
|
private double calculateSimilarity(String text1, String text2) {
|
||||||
String clean1 = text1.replaceAll("\\s+", "").toLowerCase();
|
// ✅ 使用 cleanText 方法,自动去除标点符号和空格
|
||||||
String clean2 = text2.replaceAll("\\s+", "").toLowerCase();
|
String clean1 = cleanText(text1);
|
||||||
|
String clean2 = cleanText(text2);
|
||||||
|
|
||||||
int maxLen = Math.max(clean1.length(), clean2.length());
|
int maxLen = Math.max(clean1.length(), clean2.length());
|
||||||
if (maxLen == 0) return 1.0;
|
if (maxLen == 0) return 1.0;
|
||||||
|
|
|
||||||
|
|
@ -261,7 +261,7 @@
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<!-- 查看对话框 -->
|
<!-- 查看对话框 -->
|
||||||
<el-dialog title="语音评测详情" :visible.sync="viewOpen" width="800px" append-to-body>
|
<el-dialog title="语音评测详情" :visible.sync="viewOpen" width="800px" append-to-body :close-on-click-modal="false" class="view-dialog">
|
||||||
<el-descriptions :column="2" border>
|
<el-descriptions :column="2" border>
|
||||||
<el-descriptions-item label="评测ID">{{ form.id }}</el-descriptions-item>
|
<el-descriptions-item label="评测ID">{{ form.id }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="学员姓名">{{ form.studentName }}</el-descriptions-item>
|
<el-descriptions-item label="学员姓名">{{ form.studentName }}</el-descriptions-item>
|
||||||
|
|
@ -271,19 +271,20 @@
|
||||||
<el-tag v-if="form.isSubmitted === 1" type="success" size="small">已提交</el-tag>
|
<el-tag v-if="form.isSubmitted === 1" type="success" size="small">已提交</el-tag>
|
||||||
<el-tag v-else type="info" size="small">未提交</el-tag>
|
<el-tag v-else type="info" size="small">未提交</el-tag>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="提交时间" v-if="form.submitTime">
|
<el-descriptions-item label="提交时间">
|
||||||
{{ parseTime(form.submitTime, '{y}-{m}-{d} {h}:{i}:{s}') }}
|
<span v-if="form.submitTime">{{ parseTime(form.submitTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
|
||||||
|
<span v-else style="color: #909399;">未提交</span>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="评测内容" :span="2">
|
<el-descriptions-item label="评测内容" :span="2">
|
||||||
<div style="max-height: 100px; overflow-y: auto;">{{ form.content }}</div>
|
<div style="max-height: 100px; overflow-y: auto;">{{ form.content }}</div>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="总分">
|
<el-descriptions-item label="总分" :span="2">
|
||||||
<span style="color: #409eff; font-weight: bold; font-size: 18px;">{{ form.score || 0 }}分</span>
|
<span style="color: #409eff; font-weight: bold; font-size: 18px;">{{ parseFloat(form.score || 0).toFixed(0) }}分</span>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="准确度">{{ form.accuracy || 0 }}分</el-descriptions-item>
|
<el-descriptions-item label="准确度">{{ parseFloat(form.accuracy || 0).toFixed(0) }}分</el-descriptions-item>
|
||||||
<el-descriptions-item label="流畅度">{{ form.fluency || 0 }}分</el-descriptions-item>
|
<el-descriptions-item label="完整度">{{ parseFloat(form.completeness || 0).toFixed(0) }}分</el-descriptions-item>
|
||||||
<el-descriptions-item label="完整度">{{ form.completeness || 0 }}分</el-descriptions-item>
|
<el-descriptions-item label="流畅度">{{ parseFloat(form.fluency || 0).toFixed(0) }}分</el-descriptions-item>
|
||||||
<el-descriptions-item label="发音">{{ form.pronunciation || 0 }}分</el-descriptions-item>
|
<el-descriptions-item label="发音">{{ parseFloat(form.pronunciation || 0).toFixed(0) }}分</el-descriptions-item>
|
||||||
<el-descriptions-item label="音频文件" :span="2">
|
<el-descriptions-item label="音频文件" :span="2">
|
||||||
<div style="margin-top: 10px;">
|
<div style="margin-top: 10px;">
|
||||||
<div v-if="form.audioPath">
|
<div v-if="form.audioPath">
|
||||||
|
|
@ -317,7 +318,7 @@
|
||||||
</div>
|
</div>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="详细结果" :span="2" v-if="form.resultDetail">
|
<el-descriptions-item label="详细结果" :span="2" v-if="form.resultDetail">
|
||||||
<pre style="max-height: 200px; overflow-y: auto; background-color: #f5f5f5; padding: 10px; border-radius: 4px;">{{ formatJson(form.resultDetail) }}</pre>
|
<pre class="result-detail-pre">{{ formatJson(form.resultDetail) }}</pre>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
<div slot="footer" class="dialog-footer">
|
<div slot="footer" class="dialog-footer">
|
||||||
|
|
@ -632,5 +633,38 @@ export default {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
/* 详细结果区域样式 */
|
||||||
|
.result-detail-pre {
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: none;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
font-family: Consolas, Monaco, 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* 详情弹窗样式(不使用scoped,确保覆盖element-ui默认样式) */
|
||||||
|
.view-dialog.el-dialog__wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
padding-top: 10vh;
|
||||||
|
}
|
||||||
|
.view-dialog .el-dialog {
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
.view-dialog .el-dialog__body {
|
||||||
|
padding: 20px;
|
||||||
|
max-height: 70vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,292 +0,0 @@
|
||||||
#!/usr/bin/env python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
本地Whisper语音识别服务
|
|
||||||
替代百度API,完全离线运行
|
|
||||||
|
|
||||||
安装:
|
|
||||||
pip install openai-whisper flask flask-cors
|
|
||||||
|
|
||||||
运行:
|
|
||||||
python whisper_server.py
|
|
||||||
|
|
||||||
优点:
|
|
||||||
1. 完全免费,无限次调用
|
|
||||||
2. 离线运行,不需要网络
|
|
||||||
3. 识别准确率高(接近百度API)
|
|
||||||
4. 支持中文、英文等多语言
|
|
||||||
"""
|
|
||||||
|
|
||||||
from flask import Flask, request, jsonify
|
|
||||||
from flask_cors import CORS
|
|
||||||
import whisper
|
|
||||||
import os
|
|
||||||
import tempfile
|
|
||||||
import logging
|
|
||||||
|
|
||||||
# 繁简体转换
|
|
||||||
try:
|
|
||||||
import zhconv
|
|
||||||
HAS_ZHCONV = True
|
|
||||||
except ImportError:
|
|
||||||
HAS_ZHCONV = False
|
|
||||||
print("⚠️ 未安装zhconv库,无法进行繁简体转换")
|
|
||||||
print(" 安装命令: pip install zhconv")
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
|
||||||
CORS(app)
|
|
||||||
|
|
||||||
# 配置日志
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
# 全局变量:Whisper模型
|
|
||||||
whisper_model = None
|
|
||||||
|
|
||||||
def load_whisper_model():
|
|
||||||
"""加载Whisper模型(懒加载)"""
|
|
||||||
global whisper_model
|
|
||||||
if whisper_model is None:
|
|
||||||
logger.info("正在加载Whisper模型...")
|
|
||||||
# 使用base模型(平衡速度和准确度)
|
|
||||||
# 可选: tiny, base, small, medium, large
|
|
||||||
# tiny: 最快,准确度一般
|
|
||||||
# base: 快速,准确度好 ✅ 推荐
|
|
||||||
# small: 较慢,准确度高
|
|
||||||
# medium/large: 很慢,准确度最高
|
|
||||||
whisper_model = whisper.load_model("base")
|
|
||||||
logger.info("✅ Whisper模型加载成功")
|
|
||||||
return whisper_model
|
|
||||||
|
|
||||||
def convert_to_simplified(text):
|
|
||||||
"""
|
|
||||||
将繁体中文转换为简体中文
|
|
||||||
|
|
||||||
参数:
|
|
||||||
text: 待转换的文本
|
|
||||||
|
|
||||||
返回:
|
|
||||||
简体中文文本
|
|
||||||
"""
|
|
||||||
if not text:
|
|
||||||
return text
|
|
||||||
|
|
||||||
if HAS_ZHCONV:
|
|
||||||
try:
|
|
||||||
# 使用zhconv进行繁简转换
|
|
||||||
simplified = zhconv.convert(text, 'zh-cn')
|
|
||||||
logger.info(f"繁简转换: {text} -> {simplified}")
|
|
||||||
return simplified
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"繁简转换失败: {e}")
|
|
||||||
return text
|
|
||||||
else:
|
|
||||||
# 如果没有安装zhconv,返回原文
|
|
||||||
return text
|
|
||||||
|
|
||||||
@app.route('/health', methods=['GET'])
|
|
||||||
def health():
|
|
||||||
"""健康检查"""
|
|
||||||
return jsonify({
|
|
||||||
"status": "ok",
|
|
||||||
"service": "Whisper语音识别服务",
|
|
||||||
"model": "base"
|
|
||||||
})
|
|
||||||
|
|
||||||
@app.route('/recognize', methods=['POST'])
|
|
||||||
def recognize():
|
|
||||||
"""
|
|
||||||
语音识别接口
|
|
||||||
|
|
||||||
参数:
|
|
||||||
- file: 音频文件(支持MP3, WAV, M4A等)
|
|
||||||
- language: 语言(可选,默认自动检测)
|
|
||||||
|
|
||||||
返回:
|
|
||||||
{
|
|
||||||
"code": 200,
|
|
||||||
"msg": "识别成功",
|
|
||||||
"data": {
|
|
||||||
"text": "识别的文本",
|
|
||||||
"language": "zh",
|
|
||||||
"confidence": 0.95
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# 检查是否有文件
|
|
||||||
if 'file' not in request.files:
|
|
||||||
return jsonify({
|
|
||||||
"code": 400,
|
|
||||||
"msg": "未找到音频文件",
|
|
||||||
"data": None
|
|
||||||
}), 400
|
|
||||||
|
|
||||||
audio_file = request.files['file']
|
|
||||||
language = request.form.get('language', 'zh') # 默认中文
|
|
||||||
|
|
||||||
# 保存临时文件
|
|
||||||
with tempfile.NamedTemporaryFile(delete=False, suffix='.mp3') as temp_file:
|
|
||||||
audio_file.save(temp_file.name)
|
|
||||||
temp_path = temp_file.name
|
|
||||||
|
|
||||||
try:
|
|
||||||
# 加载模型
|
|
||||||
model = load_whisper_model()
|
|
||||||
|
|
||||||
# 识别音频
|
|
||||||
logger.info(f"开始识别音频: {audio_file.filename}")
|
|
||||||
result = model.transcribe(
|
|
||||||
temp_path,
|
|
||||||
language=language,
|
|
||||||
task='transcribe', # transcribe=识别,translate=翻译成英文
|
|
||||||
fp16=False # CPU模式
|
|
||||||
)
|
|
||||||
|
|
||||||
recognized_text = result['text'].strip()
|
|
||||||
detected_language = result.get('language', language)
|
|
||||||
|
|
||||||
# ✅ 繁体转简体
|
|
||||||
if detected_language == 'zh':
|
|
||||||
recognized_text = convert_to_simplified(recognized_text)
|
|
||||||
|
|
||||||
logger.info(f"✅ 识别成功: {recognized_text}")
|
|
||||||
|
|
||||||
return jsonify({
|
|
||||||
"code": 200,
|
|
||||||
"msg": "识别成功",
|
|
||||||
"data": {
|
|
||||||
"text": recognized_text,
|
|
||||||
"language": detected_language,
|
|
||||||
"segments": len(result.get('segments', [])),
|
|
||||||
"duration": result.get('duration', 0)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
finally:
|
|
||||||
# 删除临时文件
|
|
||||||
if os.path.exists(temp_path):
|
|
||||||
os.remove(temp_path)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"识别失败: {str(e)}", exc_info=True)
|
|
||||||
return jsonify({
|
|
||||||
"code": 500,
|
|
||||||
"msg": f"识别失败: {str(e)}",
|
|
||||||
"data": None
|
|
||||||
}), 500
|
|
||||||
|
|
||||||
@app.route('/evaluate', methods=['POST'])
|
|
||||||
def evaluate():
|
|
||||||
"""
|
|
||||||
语音评测接口(完整功能)
|
|
||||||
|
|
||||||
参数:
|
|
||||||
- file: 音频文件
|
|
||||||
- text: 标准文本(用于对比)
|
|
||||||
|
|
||||||
返回:
|
|
||||||
{
|
|
||||||
"code": 200,
|
|
||||||
"msg": "评测成功",
|
|
||||||
"data": {
|
|
||||||
"text": "识别的文本",
|
|
||||||
"score": 95,
|
|
||||||
"accuracy": 98,
|
|
||||||
"fluency": 92,
|
|
||||||
"completeness": 95,
|
|
||||||
"pronunciation": 94
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
if 'file' not in request.files:
|
|
||||||
return jsonify({"code": 400, "msg": "未找到音频文件", "data": None}), 400
|
|
||||||
|
|
||||||
audio_file = request.files['file']
|
|
||||||
standard_text = request.form.get('text', '')
|
|
||||||
|
|
||||||
# 保存临时文件
|
|
||||||
with tempfile.NamedTemporaryFile(delete=False, suffix='.mp3') as temp_file:
|
|
||||||
audio_file.save(temp_file.name)
|
|
||||||
temp_path = temp_file.name
|
|
||||||
|
|
||||||
try:
|
|
||||||
# 1. 识别音频
|
|
||||||
model = load_whisper_model()
|
|
||||||
result = model.transcribe(temp_path, language='zh', fp16=False)
|
|
||||||
recognized_text = result['text'].strip()
|
|
||||||
|
|
||||||
# ✅ 繁体转简体
|
|
||||||
recognized_text = convert_to_simplified(recognized_text)
|
|
||||||
|
|
||||||
# 2. 计算评分
|
|
||||||
from difflib import SequenceMatcher
|
|
||||||
|
|
||||||
# 清理文本
|
|
||||||
clean_recognized = ''.join(recognized_text.split()).lower()
|
|
||||||
clean_standard = ''.join(standard_text.split()).lower()
|
|
||||||
|
|
||||||
# 相似度
|
|
||||||
similarity = SequenceMatcher(None, clean_recognized, clean_standard).ratio()
|
|
||||||
|
|
||||||
# 计算各项评分
|
|
||||||
accuracy = similarity * 100 # 准确度
|
|
||||||
completeness = min(len(clean_recognized) / max(len(clean_standard), 1) * 100, 100) # 完整度
|
|
||||||
fluency = accuracy * 0.95 # 流利度(基于准确度)
|
|
||||||
pronunciation = accuracy * 0.98 # 发音(基于准确度)
|
|
||||||
|
|
||||||
# 总分
|
|
||||||
total_score = (accuracy * 0.3 + completeness * 0.25 + fluency * 0.3 + pronunciation * 0.15)
|
|
||||||
|
|
||||||
logger.info(f"✅ 评测完成: {recognized_text} | 得分: {total_score:.0f}")
|
|
||||||
|
|
||||||
return jsonify({
|
|
||||||
"code": 200,
|
|
||||||
"msg": "评测成功",
|
|
||||||
"data": {
|
|
||||||
"text": recognized_text,
|
|
||||||
"score": round(total_score),
|
|
||||||
"accuracy": round(accuracy),
|
|
||||||
"fluency": round(fluency),
|
|
||||||
"completeness": round(completeness),
|
|
||||||
"pronunciation": round(pronunciation),
|
|
||||||
"similarity": round(similarity * 100, 2)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
finally:
|
|
||||||
if os.path.exists(temp_path):
|
|
||||||
os.remove(temp_path)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"评测失败: {str(e)}", exc_info=True)
|
|
||||||
return jsonify({"code": 500, "msg": f"评测失败: {str(e)}", "data": None}), 500
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
print("=" * 60)
|
|
||||||
print("🎤 本地Whisper语音识别服务")
|
|
||||||
print("=" * 60)
|
|
||||||
print("")
|
|
||||||
print("✅ 优势:")
|
|
||||||
print(" 1. 完全免费,无限次调用")
|
|
||||||
print(" 2. 离线运行,不需要网络")
|
|
||||||
print(" 3. 识别准确率高")
|
|
||||||
print(" 4. 数据完全私有")
|
|
||||||
print("")
|
|
||||||
print("📦 安装依赖:")
|
|
||||||
print(" pip install openai-whisper flask flask-cors")
|
|
||||||
print("")
|
|
||||||
print("🚀 启动服务:")
|
|
||||||
print(" python whisper_server.py")
|
|
||||||
print("")
|
|
||||||
print("📌 API接口:")
|
|
||||||
print(" 健康检查: GET http://localhost:5001/health")
|
|
||||||
print(" 语音识别: POST http://localhost:5001/recognize")
|
|
||||||
print(" 语音评测: POST http://localhost:5001/evaluate")
|
|
||||||
print("")
|
|
||||||
print("=" * 60)
|
|
||||||
print("")
|
|
||||||
|
|
||||||
# 启动服务
|
|
||||||
app.run(host='0.0.0.0', port=5001, debug=True)
|
|
||||||
|
|
@ -246,7 +246,7 @@ export default {
|
||||||
pageUnloaded: false, // 页面卸载标记
|
pageUnloaded: false, // 页面卸载标记
|
||||||
recordStartTime: 0, // 录音开始时间
|
recordStartTime: 0, // 录音开始时间
|
||||||
recordingFailCount: 0, // 录音失败次数
|
recordingFailCount: 0, // 录音失败次数
|
||||||
lastRecordedFilePath: null // ✅ 保存最后一次录音的文件路径
|
lastRecordedFilePath: null // ✅ 保存服务器返回的音频路径
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onLoad(options) {
|
onLoad(options) {
|
||||||
|
|
@ -506,37 +506,77 @@ export default {
|
||||||
throw new Error('录音文件获取失败')
|
throw new Error('录音文件获取失败')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 上传并评测(上传后会从服务器返回保存到数据库的路径)
|
// 2. ✅ 上传并评测(使用 uploadAndEvaluate 一次性完成识别+评测)
|
||||||
console.log('[Speech] 开始上传评测...')
|
console.log('[Speech] ========================================')
|
||||||
const { uploadAndRecognize } = await import('@/api/study/voiceEvaluation.js')
|
console.log('[Speech] 🎯 新版本代码 v2.0 - 统一评测模式')
|
||||||
const evalResult = await uploadAndRecognize(
|
console.log('[Speech] 准备调用 uploadAndEvaluate')
|
||||||
|
console.log('[Speech] 文件路径:', filePath)
|
||||||
|
console.log('[Speech] 文件类型:', typeof filePath)
|
||||||
|
console.log('[Speech] 评测内容:', this.selectedContent?.content)
|
||||||
|
console.log('[Speech] ========================================')
|
||||||
|
|
||||||
|
// 显示版本提示
|
||||||
|
uni.showToast({
|
||||||
|
title: '新版本v2.0-统一评测',
|
||||||
|
icon: 'none',
|
||||||
|
duration: 1500
|
||||||
|
})
|
||||||
|
|
||||||
|
const { uploadAndEvaluate } = await import('@/api/study/voiceEvaluation.js')
|
||||||
|
const evalResult = await uploadAndEvaluate(
|
||||||
filePath,
|
filePath,
|
||||||
this.selectedContent?.content || '测试文本'
|
this.selectedContent?.content || '测试文本',
|
||||||
|
null,
|
||||||
|
'zh-CN'
|
||||||
)
|
)
|
||||||
|
|
||||||
uni.hideLoading()
|
uni.hideLoading()
|
||||||
|
|
||||||
if (evalResult.code === 200 && evalResult.data) {
|
// 🔍 调试:输出完整返回数据
|
||||||
const data = evalResult.data
|
console.log('[Speech] 返回结果 code:', evalResult.code)
|
||||||
this.recognizedText = data.recognizedText || ''
|
console.log('[Speech] 返回结果:', JSON.stringify(evalResult))
|
||||||
this.scoreResult = {
|
console.log('[Speech] 是否有evaluation:', !!evalResult.evaluation)
|
||||||
score: data.totalScore || 0,
|
|
||||||
pronunciationScore: data.pronunciation || 0,
|
|
||||||
fluencyScore: data.fluency || 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// ✅ 使用服务器返回的相对路径(如 /profile/upload/voice/2025/12/11/xxx.mp3)
|
// ✅ 只检查 code === 200,不强制要求 evaluation
|
||||||
if (data.audioPath) {
|
if (evalResult.code === 200) {
|
||||||
this.lastRecordedFilePath = data.audioPath
|
// 获取评测数据(可能在 evaluation 或 data 中)
|
||||||
console.log('[Speech] 保存服务器音频路径:', data.audioPath)
|
const evaluation = evalResult.evaluation || evalResult.data || {}
|
||||||
|
const recognizedText = evalResult.recognizedText || evaluation.recognizedText || ''
|
||||||
|
|
||||||
|
// ✅ 设置识别文本
|
||||||
|
this.recognizedText = recognizedText
|
||||||
|
console.log('[Speech] 识别文本:', this.recognizedText)
|
||||||
|
|
||||||
|
// ✅ 设置评分结果(兼容多种数据格式)
|
||||||
|
this.scoreResult = {
|
||||||
|
totalScore: Number(evaluation.score) || Number(evaluation.totalScore) || 0,
|
||||||
|
accuracy: Number(evaluation.accuracy) || 0,
|
||||||
|
completeness: Number(evaluation.completeness) || 0,
|
||||||
|
fluency: Number(evaluation.fluency) || 0,
|
||||||
|
pronunciation: Number(evaluation.pronunciation) || 0
|
||||||
|
}
|
||||||
|
console.log('[Speech] 评分结果:', this.scoreResult)
|
||||||
|
|
||||||
|
// ✅ 保存评测ID和提交状态
|
||||||
|
this.currentEvaluationId = evaluation.id
|
||||||
|
const submitted = evaluation.isSubmitted
|
||||||
|
this.isSubmitted = submitted === 1 || submitted === true || submitted === '1'
|
||||||
|
|
||||||
|
// ✅ 保存服务器音频路径
|
||||||
|
if (evaluation.audioPath) {
|
||||||
|
this.lastRecordedFilePath = evaluation.audioPath
|
||||||
|
console.log('[Speech] 服务器音频路径:', evaluation.audioPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.hasFirstResult = true
|
this.hasFirstResult = true
|
||||||
this.statusText = '评测完成'
|
this.statusText = '评测完成'
|
||||||
this.debugInfo = `得分:${data.totalScore}分`
|
const finalScore = this.scoreResult.totalScore
|
||||||
|
this.debugInfo = `得分:${finalScore}分`
|
||||||
this.recordingFailCount = 0
|
this.recordingFailCount = 0
|
||||||
uni.showToast({ title: `得分:${data.totalScore}分`, icon: 'success' })
|
uni.showToast({ title: `得分:${finalScore}分`, icon: 'success' })
|
||||||
} else {
|
} else {
|
||||||
|
console.error('[Speech] ❌ 评测失败,返回码:', evalResult.code)
|
||||||
|
console.error('[Speech] evalResult:', evalResult)
|
||||||
throw new Error(evalResult.msg || '评测失败')
|
throw new Error(evalResult.msg || '评测失败')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -677,27 +717,14 @@ export default {
|
||||||
if (this.scrollTimer) { clearInterval(this.scrollTimer); this.scrollTimer = null }
|
if (this.scrollTimer) { clearInterval(this.scrollTimer); this.scrollTimer = null }
|
||||||
},
|
},
|
||||||
async evaluateScore() {
|
async evaluateScore() {
|
||||||
if (!this.recognizedText || !this.selectedContent) {
|
// ✅ 已经在停止录音时完成评分,这里不需要再次评分
|
||||||
uni.showToast({ title: '请先完成语音识别', icon: 'none' })
|
if (this.scoreResult && this.scoreResult.totalScore !== undefined) {
|
||||||
|
uni.showToast({ title: '已经评分完成', icon: 'none' })
|
||||||
|
this.$nextTick(() => { uni.pageScrollTo({ selector: '.score-section', duration: 300 }) })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.isEvaluating = true
|
|
||||||
try {
|
uni.showToast({ title: '请先录音', icon: 'none' })
|
||||||
const result = await evaluateSpeechRecognition(this.selectedContent.content, this.recognizedText)
|
|
||||||
if (result.code === 200 && result.data) {
|
|
||||||
this.scoreResult = result.data
|
|
||||||
await this.saveScoreToBackend(result.data)
|
|
||||||
uni.showToast({ title: '评分完成', icon: 'success', duration: 2000 })
|
|
||||||
this.$nextTick(() => { uni.pageScrollTo({ selector: '.score-section', duration: 300 }) })
|
|
||||||
} else {
|
|
||||||
throw new Error(result.msg || '评分失败')
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('评分失败', error)
|
|
||||||
uni.showToast({ title: error.message || '评分失败', icon: 'none', duration: 3000 })
|
|
||||||
} finally {
|
|
||||||
this.isEvaluating = false
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
async saveScoreToBackend(scoreData) {
|
async saveScoreToBackend(scoreData) {
|
||||||
if (!this.selectedContent || !this.recognizedText) return
|
if (!this.selectedContent || !this.recognizedText) return
|
||||||
|
|
@ -769,6 +796,7 @@ export default {
|
||||||
this.scoreResult = null
|
this.scoreResult = null
|
||||||
this.currentEvaluationId = null
|
this.currentEvaluationId = null
|
||||||
this.isSubmitted = false
|
this.isSubmitted = false
|
||||||
|
this.lastRecordedFilePath = null // ✅ 清空服务器路径
|
||||||
},
|
},
|
||||||
getDifficultyText(difficulty) {
|
getDifficultyText(difficulty) {
|
||||||
const map = { 'easy': '简单', 'medium': '中等', 'hard': '困难' }
|
const map = { 'easy': '简单', 'medium': '中等', 'hard': '困难' }
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
# 语音评测新方案 - 快速开始
|
|
||||||
|
|
||||||
## ✅ 已完成
|
|
||||||
|
|
||||||
1. ✅ **禁用 UTS 插件** - 不再依赖本地编译
|
1. ✅ **禁用 UTS 插件** - 不再依赖本地编译
|
||||||
2. ✅ **创建录音工具** - `utils/speech-recorder.js`
|
2. ✅ **创建录音工具** - `utils/speech-recorder.js`
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@
|
||||||
- **路径**: `Study-Vue-redis/`
|
- **路径**: `Study-Vue-redis/`
|
||||||
- **技术栈**: Spring Boot + MyBatis + Vue.js
|
- **技术栈**: Spring Boot + MyBatis + Vue.js
|
||||||
- **说明**: 包含后端API服务和管理后台前端
|
- **说明**: 包含后端API服务和管理后台前端
|
||||||
|
4-
|
||||||
### 2. APP项目
|
### 2. APP项目
|
||||||
- **路径**: `fronted_uniapp/`
|
- **路径**: `fronted_uniapp/`
|
||||||
- **技术栈**: uni-app (Vue.js)
|
- **技术栈**: uni-app (Vue.js)
|
||||||
|
|
|
||||||
555
log/whisper_server (1).py
Normal file
555
log/whisper_server (1).py
Normal file
|
|
@ -0,0 +1,555 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Whisper语音识别服务
|
||||||
|
支持语音识别和评测功能
|
||||||
|
多线程并发处理
|
||||||
|
作者: AI Assistant
|
||||||
|
版本: 2.0
|
||||||
|
"""
|
||||||
|
|
||||||
|
from flask import Flask, request, jsonify
|
||||||
|
from flask_cors import CORS
|
||||||
|
import whisper
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import logging
|
||||||
|
from logging.handlers import RotatingFileHandler
|
||||||
|
import traceback
|
||||||
|
from datetime import datetime
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# 日志配置
|
||||||
|
# ============================================
|
||||||
|
def setup_logging():
|
||||||
|
"""配置日志系统"""
|
||||||
|
# 创建logger
|
||||||
|
logger = logging.getLogger('whisper_server')
|
||||||
|
logger.setLevel(logging.INFO)
|
||||||
|
|
||||||
|
# 控制台输出
|
||||||
|
console_handler = logging.StreamHandler(sys.stdout)
|
||||||
|
console_handler.setLevel(logging.INFO)
|
||||||
|
console_formatter = logging.Formatter(
|
||||||
|
'%(asctime)s [%(levelname)s] %(message)s',
|
||||||
|
datefmt='%Y-%m-%d %H:%M:%S'
|
||||||
|
)
|
||||||
|
console_handler.setFormatter(console_formatter)
|
||||||
|
logger.addHandler(console_handler)
|
||||||
|
|
||||||
|
# 文件输出(可选)
|
||||||
|
try:
|
||||||
|
log_dir = 'logs'
|
||||||
|
if not os.path.exists(log_dir):
|
||||||
|
os.makedirs(log_dir)
|
||||||
|
|
||||||
|
file_handler = RotatingFileHandler(
|
||||||
|
os.path.join(log_dir, 'whisper_server.log'),
|
||||||
|
maxBytes=10*1024*1024, # 10MB
|
||||||
|
backupCount=5,
|
||||||
|
encoding='utf-8'
|
||||||
|
)
|
||||||
|
file_handler.setLevel(logging.INFO)
|
||||||
|
file_handler.setFormatter(console_formatter)
|
||||||
|
logger.addHandler(file_handler)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"无法创建日志文件: {e}")
|
||||||
|
|
||||||
|
return logger
|
||||||
|
|
||||||
|
# 初始化日志
|
||||||
|
logger = setup_logging()
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Flask应用配置
|
||||||
|
# ============================================
|
||||||
|
app = Flask(__name__)
|
||||||
|
CORS(app) # 允许跨域请求
|
||||||
|
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 最大16MB
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# 全局变量
|
||||||
|
# ============================================
|
||||||
|
whisper_model = None
|
||||||
|
MODEL_NAME = "tiny" # 可选: tiny, base, small, medium, large
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# 模型加载
|
||||||
|
# ============================================
|
||||||
|
def load_whisper_model():
|
||||||
|
"""加载Whisper模型(全局单例)"""
|
||||||
|
global whisper_model
|
||||||
|
if whisper_model is None:
|
||||||
|
logger.info(f"正在加载Whisper模型 ({MODEL_NAME})...")
|
||||||
|
try:
|
||||||
|
whisper_model = whisper.load_model(MODEL_NAME)
|
||||||
|
logger.info(f"✅ Whisper模型加载成功 ({MODEL_NAME})")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ 模型加载失败: {e}")
|
||||||
|
raise
|
||||||
|
return whisper_model
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# 工具函数
|
||||||
|
# ============================================
|
||||||
|
def convert_to_simplified(text):
|
||||||
|
"""将繁体中文转换为简体中文"""
|
||||||
|
if not text:
|
||||||
|
return text
|
||||||
|
|
||||||
|
# 方案1:使用zhconv(推荐,纯Python)
|
||||||
|
try:
|
||||||
|
import zhconv
|
||||||
|
result = zhconv.convert(text, 'zh-cn')
|
||||||
|
if result != text:
|
||||||
|
logger.info(f"繁简转换: {text} -> {result}")
|
||||||
|
return result
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 方案2:使用opencc
|
||||||
|
try:
|
||||||
|
from opencc import OpenCC
|
||||||
|
cc = OpenCC('t2s')
|
||||||
|
result = cc.convert(text)
|
||||||
|
if result != text:
|
||||||
|
logger.info(f"繁简转换(OpenCC): {text} -> {result}")
|
||||||
|
return result
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 都没安装,返回原文
|
||||||
|
logger.warning("繁简转换库未安装,请运行: pip install zhconv")
|
||||||
|
return text
|
||||||
|
|
||||||
|
def clean_text_strict(text):
|
||||||
|
"""
|
||||||
|
严格清理文本,只保留汉字、字母、数字
|
||||||
|
用于准确度计算
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
import unicodedata
|
||||||
|
|
||||||
|
if not text:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
# Unicode标准化
|
||||||
|
text = unicodedata.normalize('NFKC', text)
|
||||||
|
|
||||||
|
# 只保留汉字、字母、数字
|
||||||
|
text = re.sub(r'[^\u4e00-\u9fffa-zA-Z0-9]', '', text)
|
||||||
|
|
||||||
|
return text.lower()
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# API路由
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
@app.route('/health', methods=['GET'])
|
||||||
|
def health_check():
|
||||||
|
"""健康检查接口"""
|
||||||
|
return jsonify({
|
||||||
|
"status": "ok",
|
||||||
|
"service": "Whisper Speech Recognition",
|
||||||
|
"model": MODEL_NAME,
|
||||||
|
"version": "2.0",
|
||||||
|
"timestamp": datetime.now().isoformat()
|
||||||
|
})
|
||||||
|
|
||||||
|
@app.route('/recognize', methods=['POST'])
|
||||||
|
def recognize():
|
||||||
|
"""
|
||||||
|
语音识别接口
|
||||||
|
只识别,不评测
|
||||||
|
|
||||||
|
参数:
|
||||||
|
file: 音频文件 (multipart/form-data)
|
||||||
|
|
||||||
|
返回:
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "识别成功",
|
||||||
|
"data": {
|
||||||
|
"text": "识别的文本"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 检查文件
|
||||||
|
if 'file' not in request.files:
|
||||||
|
logger.warning("请求缺少音频文件")
|
||||||
|
return jsonify({"code": 400, "msg": "缺少音频文件", "data": None}), 400
|
||||||
|
|
||||||
|
audio_file = request.files['file']
|
||||||
|
if audio_file.filename == '':
|
||||||
|
logger.warning("文件名为空")
|
||||||
|
return jsonify({"code": 400, "msg": "文件名为空", "data": None}), 400
|
||||||
|
|
||||||
|
logger.info(f"收到识别请求: {audio_file.filename} ({len(audio_file.read())} bytes)")
|
||||||
|
audio_file.seek(0) # 重置文件指针
|
||||||
|
|
||||||
|
# 保存临时文件
|
||||||
|
with tempfile.NamedTemporaryFile(delete=False, suffix='.mp3') as temp_file:
|
||||||
|
temp_path = temp_file.name
|
||||||
|
audio_file.save(temp_path)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 加载模型
|
||||||
|
model = load_whisper_model()
|
||||||
|
|
||||||
|
# 识别音频
|
||||||
|
logger.info(f"开始识别: {audio_file.filename}")
|
||||||
|
result = model.transcribe(temp_path, language='zh', fp16=False)
|
||||||
|
recognized_text = result['text'].strip()
|
||||||
|
|
||||||
|
# 繁体转简体
|
||||||
|
recognized_text = convert_to_simplified(recognized_text)
|
||||||
|
|
||||||
|
logger.info(f"✅ 识别成功: {recognized_text}")
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"code": 200,
|
||||||
|
"msg": "识别成功",
|
||||||
|
"data": {
|
||||||
|
"text": recognized_text
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# 删除临时文件
|
||||||
|
try:
|
||||||
|
if os.path.exists(temp_path):
|
||||||
|
os.remove(temp_path)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"删除临时文件失败: {e}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"识别失败: {str(e)}")
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
return jsonify({
|
||||||
|
"code": 500,
|
||||||
|
"msg": f"识别失败: {str(e)}",
|
||||||
|
"data": None
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
@app.route('/evaluate', methods=['POST'])
|
||||||
|
def evaluate():
|
||||||
|
"""
|
||||||
|
语音评测接口
|
||||||
|
识别 + 评分
|
||||||
|
|
||||||
|
参数:
|
||||||
|
file: 音频文件 (multipart/form-data)
|
||||||
|
text: 标准文本 (form-data)
|
||||||
|
|
||||||
|
返回:
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "评测成功",
|
||||||
|
"data": {
|
||||||
|
"text": "识别的文本",
|
||||||
|
"score": 95,
|
||||||
|
"accuracy": 98,
|
||||||
|
"fluency": 95,
|
||||||
|
"completeness": 100,
|
||||||
|
"pronunciation": 96,
|
||||||
|
"similarity": 98.5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 检查文件
|
||||||
|
if 'file' not in request.files:
|
||||||
|
logger.warning("请求缺少音频文件")
|
||||||
|
return jsonify({"code": 400, "msg": "缺少音频文件", "data": None}), 400
|
||||||
|
|
||||||
|
audio_file = request.files['file']
|
||||||
|
standard_text = request.form.get('text', '')
|
||||||
|
|
||||||
|
if not standard_text:
|
||||||
|
logger.warning("请求缺少标准文本")
|
||||||
|
return jsonify({"code": 400, "msg": "缺少标准文本", "data": None}), 400
|
||||||
|
|
||||||
|
logger.info(f"收到评测请求: {audio_file.filename}, 标准文本: {standard_text}")
|
||||||
|
|
||||||
|
# 保存临时文件
|
||||||
|
with tempfile.NamedTemporaryFile(delete=False, suffix='.mp3') as temp_file:
|
||||||
|
temp_path = temp_file.name
|
||||||
|
audio_file.save(temp_path)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 1. 识别音频
|
||||||
|
model = load_whisper_model()
|
||||||
|
logger.info("开始识别音频...")
|
||||||
|
result = model.transcribe(temp_path, language='zh', fp16=False)
|
||||||
|
recognized_text = result['text'].strip()
|
||||||
|
recognized_text = convert_to_simplified(recognized_text)
|
||||||
|
|
||||||
|
# 2. 计算评分
|
||||||
|
from difflib import SequenceMatcher
|
||||||
|
|
||||||
|
# 清理文本
|
||||||
|
clean_recognized = clean_text_strict(recognized_text)
|
||||||
|
clean_standard = clean_text_strict(standard_text)
|
||||||
|
|
||||||
|
# 相似度
|
||||||
|
similarity = SequenceMatcher(None, clean_recognized, clean_standard).ratio()
|
||||||
|
|
||||||
|
# 调试日志
|
||||||
|
logger.info(f"📝 标准文本: {standard_text}")
|
||||||
|
logger.info(f"🎤 识别文本: {recognized_text}")
|
||||||
|
logger.info(f"🧹 清理后标准: {clean_standard}")
|
||||||
|
logger.info(f"🧹 清理后识别: {clean_recognized}")
|
||||||
|
logger.info(f"📊 相似度: {similarity:.4f} ({similarity*100:.2f}%)")
|
||||||
|
|
||||||
|
# 准确度计算(带阈值优化)
|
||||||
|
if similarity >= 0.98:
|
||||||
|
accuracy = 100
|
||||||
|
logger.info(f"✅ 相似度>=98%,准确度给满分: {accuracy}")
|
||||||
|
elif similarity >= 0.95:
|
||||||
|
accuracy = 99
|
||||||
|
logger.info(f"✅ 相似度>=95%,准确度给99分: {accuracy}")
|
||||||
|
else:
|
||||||
|
accuracy = similarity * 100
|
||||||
|
logger.info(f"📊 准确度: {accuracy:.2f}")
|
||||||
|
|
||||||
|
# 完整度
|
||||||
|
completeness = min(len(clean_recognized) / max(len(clean_standard), 1) * 100, 100)
|
||||||
|
|
||||||
|
# 流畅度和发音(如果准确度满分,其他也满分)
|
||||||
|
fluency = 100 if accuracy == 100 else accuracy * 0.95
|
||||||
|
pronunciation = 100 if accuracy == 100 else accuracy * 0.98
|
||||||
|
|
||||||
|
# 总分(加权)
|
||||||
|
total_score = (accuracy * 0.3 + completeness * 0.25 + fluency * 0.3 + pronunciation * 0.15)
|
||||||
|
|
||||||
|
logger.info(f"✅ 评测完成: 总分={total_score:.0f}, 准确度={accuracy:.0f}, 流畅度={fluency:.0f}, 完整度={completeness:.0f}, 发音={pronunciation:.0f}")
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"code": 200,
|
||||||
|
"msg": "评测成功",
|
||||||
|
"data": {
|
||||||
|
"text": recognized_text,
|
||||||
|
"score": round(total_score),
|
||||||
|
"accuracy": round(accuracy),
|
||||||
|
"fluency": round(fluency),
|
||||||
|
"completeness": round(completeness),
|
||||||
|
"pronunciation": round(pronunciation),
|
||||||
|
"similarity": round(similarity * 100, 2)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# 删除临时文件
|
||||||
|
try:
|
||||||
|
if os.path.exists(temp_path):
|
||||||
|
os.remove(temp_path)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"删除临时文件失败: {e}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"评测失败: {str(e)}")
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
return jsonify({
|
||||||
|
"code": 500,
|
||||||
|
"msg": f"评测失败: {str(e)}",
|
||||||
|
"data": None
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# 错误处理
|
||||||
|
# ============================================
|
||||||
|
@app.errorhandler(413)
|
||||||
|
def request_entity_too_large(error):
|
||||||
|
logger.warning("请求文件过大")
|
||||||
|
return jsonify({
|
||||||
|
"code": 413,
|
||||||
|
"msg": "文件太大,最大支持16MB",
|
||||||
|
"data": None
|
||||||
|
}), 413
|
||||||
|
|
||||||
|
@app.errorhandler(500)
|
||||||
|
def internal_error(error):
|
||||||
|
logger.error(f"服务器内部错误: {error}")
|
||||||
|
return jsonify({
|
||||||
|
"code": 500,
|
||||||
|
"msg": "服务器内部错误",
|
||||||
|
"data": None
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
@app.errorhandler(404)
|
||||||
|
def not_found(error):
|
||||||
|
return jsonify({
|
||||||
|
"code": 404,
|
||||||
|
"msg": "接口不存在",
|
||||||
|
"data": None
|
||||||
|
}), 404
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# 环境检查
|
||||||
|
# ============================================
|
||||||
|
def check_environment():
|
||||||
|
"""检查运行环境和依赖"""
|
||||||
|
logger.info("")
|
||||||
|
logger.info("=" * 70)
|
||||||
|
logger.info("🔍 环境检查")
|
||||||
|
logger.info("=" * 70)
|
||||||
|
|
||||||
|
all_ok = True
|
||||||
|
|
||||||
|
# 1. 检查 Python 版本
|
||||||
|
py_version = sys.version.split()[0]
|
||||||
|
logger.info(f" Python版本: {py_version} ✅")
|
||||||
|
|
||||||
|
# 2. 检查 whisper
|
||||||
|
try:
|
||||||
|
import whisper
|
||||||
|
logger.info(f" Whisper: 已安装 ✅")
|
||||||
|
except ImportError:
|
||||||
|
logger.error(f" Whisper: 未安装 ❌ (pip install openai-whisper)")
|
||||||
|
all_ok = False
|
||||||
|
|
||||||
|
# 3. 检查 Flask
|
||||||
|
try:
|
||||||
|
import flask
|
||||||
|
logger.info(f" Flask: {flask.__version__} ✅")
|
||||||
|
except ImportError:
|
||||||
|
logger.error(f" Flask: 未安装 ❌ (pip install flask)")
|
||||||
|
all_ok = False
|
||||||
|
|
||||||
|
# 4. 检查 flask-cors
|
||||||
|
try:
|
||||||
|
import flask_cors
|
||||||
|
logger.info(f" Flask-CORS: 已安装 ✅")
|
||||||
|
except ImportError:
|
||||||
|
logger.error(f" Flask-CORS: 未安装 ❌ (pip install flask-cors)")
|
||||||
|
all_ok = False
|
||||||
|
|
||||||
|
# 5. 检查 waitress
|
||||||
|
try:
|
||||||
|
import waitress
|
||||||
|
logger.info(f" Waitress: 已安装 ✅")
|
||||||
|
except ImportError:
|
||||||
|
logger.error(f" Waitress: 未安装 ❌ (pip install waitress)")
|
||||||
|
all_ok = False
|
||||||
|
|
||||||
|
# 6. 检查繁简转换库
|
||||||
|
zhconv_ok = False
|
||||||
|
opencc_ok = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
import zhconv
|
||||||
|
zhconv_ok = True
|
||||||
|
logger.info(f" zhconv: 已安装 ✅ (繁简转换)")
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
from opencc import OpenCC
|
||||||
|
opencc_ok = True
|
||||||
|
logger.info(f" OpenCC: 已安装 ✅ (繁简转换备用)")
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not zhconv_ok and not opencc_ok:
|
||||||
|
logger.warning(f" 繁简转换: 未安装 ⚠️ (pip install zhconv)")
|
||||||
|
logger.warning(f" 识别结果可能包含繁体字!")
|
||||||
|
|
||||||
|
# 7. 检查 ffmpeg(Whisper需要)
|
||||||
|
import shutil
|
||||||
|
if shutil.which('ffmpeg'):
|
||||||
|
logger.info(f" FFmpeg: 已安装 ✅")
|
||||||
|
else:
|
||||||
|
logger.warning(f" FFmpeg: 未找到 ⚠️ (某些音频格式可能无法处理)")
|
||||||
|
|
||||||
|
logger.info("=" * 70)
|
||||||
|
|
||||||
|
if all_ok:
|
||||||
|
logger.info("✅ 环境检查通过!")
|
||||||
|
else:
|
||||||
|
logger.error("❌ 缺少必要依赖,请先安装后再启动!")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
logger.info("")
|
||||||
|
return all_ok
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# 启动服务(多线程模式)
|
||||||
|
# ============================================
|
||||||
|
if __name__ == '__main__':
|
||||||
|
try:
|
||||||
|
# 环境检查
|
||||||
|
check_environment()
|
||||||
|
|
||||||
|
from waitress import serve
|
||||||
|
|
||||||
|
# 打印启动信息
|
||||||
|
logger.info("=" * 70)
|
||||||
|
logger.info("🚀 正在启动Whisper语音识别服务...")
|
||||||
|
logger.info("=" * 70)
|
||||||
|
|
||||||
|
# 预加载模型
|
||||||
|
load_whisper_model()
|
||||||
|
|
||||||
|
# 启动信息
|
||||||
|
logger.info("")
|
||||||
|
logger.info("=" * 70)
|
||||||
|
logger.info("✅ Whisper服务启动成功!")
|
||||||
|
logger.info("=" * 70)
|
||||||
|
logger.info(f"📍 本地地址: http://127.0.0.1:5001")
|
||||||
|
logger.info(f"📍 局域网地址: http://0.0.0.0:5001")
|
||||||
|
logger.info(f"📍 访问地址: http://192.168.0.106:5001")
|
||||||
|
logger.info("=" * 70)
|
||||||
|
logger.info(f"⚙️ 运行模式: 多线程并发")
|
||||||
|
logger.info(f"⚙️ Whisper模型: {MODEL_NAME}")
|
||||||
|
logger.info(f"⚙️ 工作线程: 8 个")
|
||||||
|
logger.info(f"⚙️ 并发能力: 40-60 人同时使用")
|
||||||
|
logger.info(f"⚙️ 超时时间: 300 秒")
|
||||||
|
logger.info(f"⚙️ 最大连接: 100 个")
|
||||||
|
logger.info(f"⚙️ 最大文件: 16 MB")
|
||||||
|
logger.info("=" * 70)
|
||||||
|
logger.info("")
|
||||||
|
logger.info("📌 API接口列表:")
|
||||||
|
logger.info(" [GET] /health - 健康检查")
|
||||||
|
logger.info(" [POST] /recognize - 语音识别(只识别)")
|
||||||
|
logger.info(" [POST] /evaluate - 语音评测(识别+评分)")
|
||||||
|
logger.info("=" * 70)
|
||||||
|
logger.info("")
|
||||||
|
logger.info("💡 使用示例:")
|
||||||
|
logger.info(" 健康检查: curl http://192.168.0.106:5001/health")
|
||||||
|
logger.info(" 语音识别: curl -F 'file=@audio.mp3' http://192.168.0.106:5001/recognize")
|
||||||
|
logger.info(" 语音评测: curl -F 'file=@audio.mp3' -F 'text=你好' http://192.168.0.106:5001/evaluate")
|
||||||
|
logger.info("=" * 70)
|
||||||
|
logger.info("")
|
||||||
|
logger.info("✨ 服务已就绪,等待请求...")
|
||||||
|
logger.info("✨ 按 Ctrl+C 停止服务")
|
||||||
|
logger.info("")
|
||||||
|
|
||||||
|
# 使用waitress启动(支持多线程)
|
||||||
|
serve(
|
||||||
|
app,
|
||||||
|
host='0.0.0.0',
|
||||||
|
port=5001,
|
||||||
|
threads=8, # 8个工作线程,支持40-60人并发
|
||||||
|
channel_timeout=300, # 单个请求超时5分钟
|
||||||
|
connection_limit=100, # 最多100个并发连接
|
||||||
|
backlog=64, # 连接队列长度
|
||||||
|
recv_bytes=65536, # 接收缓冲区 64KB
|
||||||
|
send_bytes=65536, # 发送缓冲区 64KB
|
||||||
|
url_scheme='http'
|
||||||
|
)
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
logger.info("")
|
||||||
|
logger.info("=" * 70)
|
||||||
|
logger.info("⏹️ 收到停止信号,正在关闭服务...")
|
||||||
|
logger.info("=" * 70)
|
||||||
|
logger.info("👋 服务已停止")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("=" * 70)
|
||||||
|
logger.error(f"❌ 服务启动失败: {e}")
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
logger.error("=" * 70)
|
||||||
|
sys.exit(1)
|
||||||
|
|
@ -1,294 +0,0 @@
|
||||||
#!/usr/bin/env python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
本地Whisper语音识别服务
|
|
||||||
替代百度API,完全离线运行
|
|
||||||
|
|
||||||
安装:
|
|
||||||
pip install openai-whisper flask flask-cors
|
|
||||||
|
|
||||||
运行:
|
|
||||||
python whisper_server.py
|
|
||||||
|
|
||||||
优点:
|
|
||||||
1. 完全免费,无限次调用
|
|
||||||
2. 离线运行,不需要网络
|
|
||||||
3. 识别准确率高(接近百度API)
|
|
||||||
4. 支持中文、英文等多语言
|
|
||||||
"""
|
|
||||||
|
|
||||||
from flask import Flask, request, jsonify
|
|
||||||
from flask_cors import CORS
|
|
||||||
import whisper
|
|
||||||
import os
|
|
||||||
import tempfile
|
|
||||||
import logging
|
|
||||||
|
|
||||||
# 繁简体转换
|
|
||||||
try:
|
|
||||||
import zhconv
|
|
||||||
HAS_ZHCONV = True
|
|
||||||
except ImportError:
|
|
||||||
HAS_ZHCONV = False
|
|
||||||
print("⚠️ 未安装zhconv库,无法进行繁简体转换")
|
|
||||||
print(" 安装命令: pip install zhconv")
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
|
||||||
CORS(app)
|
|
||||||
|
|
||||||
# 配置日志
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
# 全局变量:Whisper模型
|
|
||||||
whisper_model = None
|
|
||||||
|
|
||||||
def load_whisper_model():
|
|
||||||
"""加载Whisper模型(懒加载)"""
|
|
||||||
global whisper_model
|
|
||||||
if whisper_model is None:
|
|
||||||
logger.info("正在加载Whisper模型...")
|
|
||||||
# 使用base模型(平衡速度和准确度)
|
|
||||||
# 可选: tiny, base, small, medium, large
|
|
||||||
# tiny: 最快,准确度一般
|
|
||||||
# base: 快速,准确度好 ✅ 推荐
|
|
||||||
# small: 较慢,准确度高
|
|
||||||
# medium/large: 很慢,准确度最高
|
|
||||||
whisper_model = whisper.load_model("base")
|
|
||||||
logger.info("✅ Whisper模型加载成功")
|
|
||||||
return whisper_model
|
|
||||||
|
|
||||||
def convert_to_simplified(text):
|
|
||||||
"""
|
|
||||||
将繁体中文转换为简体中文
|
|
||||||
|
|
||||||
参数:
|
|
||||||
text: 待转换的文本
|
|
||||||
|
|
||||||
返回:
|
|
||||||
简体中文文本
|
|
||||||
"""
|
|
||||||
if not text:
|
|
||||||
return text
|
|
||||||
|
|
||||||
if HAS_ZHCONV:
|
|
||||||
try:
|
|
||||||
# 使用zhconv进行繁简转换
|
|
||||||
simplified = zhconv.convert(text, 'zh-cn')
|
|
||||||
logger.info(f"繁简转换: {text} -> {simplified}")
|
|
||||||
return simplified
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"繁简转换失败: {e}")
|
|
||||||
return text
|
|
||||||
else:
|
|
||||||
# 如果没有安装zhconv,返回原文
|
|
||||||
return text
|
|
||||||
|
|
||||||
@app.route('/health', methods=['GET'])
|
|
||||||
def health():
|
|
||||||
"""健康检查"""
|
|
||||||
return jsonify({
|
|
||||||
"status": "ok",
|
|
||||||
"service": "Whisper语音识别服务",
|
|
||||||
"model": "base"
|
|
||||||
})
|
|
||||||
|
|
||||||
@app.route('/recognize', methods=['POST'])
|
|
||||||
def recognize():
|
|
||||||
"""
|
|
||||||
语音识别接口
|
|
||||||
|
|
||||||
参数:
|
|
||||||
- file: 音频文件(支持MP3, WAV, M4A等)
|
|
||||||
- language: 语言(可选,默认自动检测)
|
|
||||||
|
|
||||||
返回:
|
|
||||||
{
|
|
||||||
"code": 200,
|
|
||||||
"msg": "识别成功",
|
|
||||||
"data": {
|
|
||||||
"text": "识别的文本",
|
|
||||||
"language": "zh",
|
|
||||||
"confidence": 0.95
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# 检查是否有文件
|
|
||||||
if 'file' not in request.files:
|
|
||||||
return jsonify({
|
|
||||||
"code": 400,
|
|
||||||
"msg": "未找到音频文件",
|
|
||||||
"data": None
|
|
||||||
}), 400
|
|
||||||
|
|
||||||
audio_file = request.files['file']
|
|
||||||
language = request.form.get('language', 'zh') # 默认中文
|
|
||||||
|
|
||||||
# 保存临时文件
|
|
||||||
with tempfile.NamedTemporaryFile(delete=False, suffix='.mp3') as temp_file:
|
|
||||||
audio_file.save(temp_file.name)
|
|
||||||
temp_path = temp_file.name
|
|
||||||
|
|
||||||
try:
|
|
||||||
# 加载模型
|
|
||||||
model = load_whisper_model()
|
|
||||||
|
|
||||||
# 识别音频
|
|
||||||
logger.info(f"开始识别音频: {audio_file.filename}")
|
|
||||||
result = model.transcribe(
|
|
||||||
temp_path,
|
|
||||||
language=language,
|
|
||||||
task='transcribe', # transcribe=识别,translate=翻译成英文
|
|
||||||
fp16=False # CPU模式
|
|
||||||
)
|
|
||||||
|
|
||||||
recognized_text = result['text'].strip()
|
|
||||||
detected_language = result.get('language', language)
|
|
||||||
|
|
||||||
# ✅ 繁体转简体
|
|
||||||
if detected_language == 'zh':
|
|
||||||
recognized_text = convert_to_simplified(recognized_text)
|
|
||||||
|
|
||||||
logger.info(f"✅ 识别成功: {recognized_text}")
|
|
||||||
|
|
||||||
return jsonify({
|
|
||||||
"code": 200,
|
|
||||||
"msg": "识别成功",
|
|
||||||
"data": {
|
|
||||||
"text": recognized_text,
|
|
||||||
"language": detected_language,
|
|
||||||
"segments": len(result.get('segments', [])),
|
|
||||||
"duration": result.get('duration', 0)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
finally:
|
|
||||||
# 删除临时文件
|
|
||||||
if os.path.exists(temp_path):
|
|
||||||
os.remove(temp_path)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"识别失败: {str(e)}", exc_info=True)
|
|
||||||
return jsonify({
|
|
||||||
"code": 500,
|
|
||||||
"msg": f"识别失败: {str(e)}",
|
|
||||||
"data": None
|
|
||||||
}), 500
|
|
||||||
|
|
||||||
@app.route('/evaluate', methods=['POST'])
|
|
||||||
def evaluate():
|
|
||||||
"""
|
|
||||||
语音评测接口(完整功能)
|
|
||||||
|
|
||||||
参数:
|
|
||||||
- file: 音频文件
|
|
||||||
- text: 标准文本(用于对比)
|
|
||||||
|
|
||||||
返回:
|
|
||||||
{
|
|
||||||
"code": 200,
|
|
||||||
"msg": "评测成功",
|
|
||||||
"data": {
|
|
||||||
"text": "识别的文本",
|
|
||||||
"score": 95,
|
|
||||||
"accuracy": 98,
|
|
||||||
"fluency": 92,
|
|
||||||
"completeness": 95,
|
|
||||||
"pronunciation": 94
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
if 'file' not in request.files:
|
|
||||||
return jsonify({"code": 400, "msg": "未找到音频文件", "data": None}), 400
|
|
||||||
|
|
||||||
audio_file = request.files['file']
|
|
||||||
standard_text = request.form.get('text', '')
|
|
||||||
|
|
||||||
# 保存临时文件
|
|
||||||
with tempfile.NamedTemporaryFile(delete=False, suffix='.mp3') as temp_file:
|
|
||||||
audio_file.save(temp_file.name)
|
|
||||||
temp_path = temp_file.name
|
|
||||||
|
|
||||||
try:
|
|
||||||
# 1. 识别音频
|
|
||||||
model = load_whisper_model()
|
|
||||||
result = model.transcribe(temp_path, language='zh', fp16=False)
|
|
||||||
recognized_text = result['text'].strip()
|
|
||||||
|
|
||||||
# ✅ 繁体转简体
|
|
||||||
recognized_text = convert_to_simplified(recognized_text)
|
|
||||||
|
|
||||||
# 2. 计算评分
|
|
||||||
from difflib import SequenceMatcher
|
|
||||||
import re
|
|
||||||
|
|
||||||
# 清理文本:去除所有标点符号和空格,只保留汉字、字母、数字
|
|
||||||
# \w 匹配字母、数字、下划线、汉字
|
|
||||||
clean_recognized = re.sub(r'[^\w]', '', recognized_text, flags=re.UNICODE).lower()
|
|
||||||
clean_standard = re.sub(r'[^\w]', '', standard_text, flags=re.UNICODE).lower()
|
|
||||||
|
|
||||||
# 相似度
|
|
||||||
similarity = SequenceMatcher(None, clean_recognized, clean_standard).ratio()
|
|
||||||
|
|
||||||
# 计算各项评分
|
|
||||||
accuracy = similarity * 100 # 准确度
|
|
||||||
completeness = min(len(clean_recognized) / max(len(clean_standard), 1) * 100, 100) # 完整度
|
|
||||||
fluency = accuracy * 0.95 # 流利度(基于准确度)
|
|
||||||
pronunciation = accuracy * 0.98 # 发音(基于准确度)
|
|
||||||
|
|
||||||
# 总分
|
|
||||||
total_score = (accuracy * 0.3 + completeness * 0.25 + fluency * 0.3 + pronunciation * 0.15)
|
|
||||||
|
|
||||||
logger.info(f"✅ 评测完成: {recognized_text} | 得分: {total_score:.0f}")
|
|
||||||
|
|
||||||
return jsonify({
|
|
||||||
"code": 200,
|
|
||||||
"msg": "评测成功",
|
|
||||||
"data": {
|
|
||||||
"text": recognized_text,
|
|
||||||
"score": round(total_score),
|
|
||||||
"accuracy": round(accuracy),
|
|
||||||
"fluency": round(fluency),
|
|
||||||
"completeness": round(completeness),
|
|
||||||
"pronunciation": round(pronunciation),
|
|
||||||
"similarity": round(similarity * 100, 2)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
finally:
|
|
||||||
if os.path.exists(temp_path):
|
|
||||||
os.remove(temp_path)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"评测失败: {str(e)}", exc_info=True)
|
|
||||||
return jsonify({"code": 500, "msg": f"评测失败: {str(e)}", "data": None}), 500
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
print("=" * 60)
|
|
||||||
print("🎤 本地Whisper语音识别服务")
|
|
||||||
print("=" * 60)
|
|
||||||
print("")
|
|
||||||
print("✅ 优势:")
|
|
||||||
print(" 1. 完全免费,无限次调用")
|
|
||||||
print(" 2. 离线运行,不需要网络")
|
|
||||||
print(" 3. 识别准确率高")
|
|
||||||
print(" 4. 数据完全私有")
|
|
||||||
print("")
|
|
||||||
print("📦 安装依赖:")
|
|
||||||
print(" pip install openai-whisper flask flask-cors")
|
|
||||||
print("")
|
|
||||||
print("🚀 启动服务:")
|
|
||||||
print(" python whisper_server.py")
|
|
||||||
print("")
|
|
||||||
print("📌 API接口:")
|
|
||||||
print(" 健康检查: GET http://localhost:5001/health")
|
|
||||||
print(" 语音识别: POST http://localhost:5001/recognize")
|
|
||||||
print(" 语音评测: POST http://localhost:5001/evaluate")
|
|
||||||
print("")
|
|
||||||
print("=" * 60)
|
|
||||||
print("")
|
|
||||||
|
|
||||||
# 启动服务
|
|
||||||
app.run(host='0.0.0.0', port=5001, debug=True)
|
|
||||||
|
|
@ -1,94 +0,0 @@
|
||||||
-- 1️⃣ 先查看要删除的用户(确认范围)
|
|
||||||
SELECT user_id, user_name, nick_name, register_type, create_time
|
|
||||||
FROM sys_user
|
|
||||||
WHERE user_id > 200 AND del_flag = '0'
|
|
||||||
ORDER BY user_id;
|
|
||||||
|
|
||||||
-- 2️⃣ 查看影响范围(统计)
|
|
||||||
SELECT
|
|
||||||
'用户数' AS 项目,
|
|
||||||
COUNT(*) AS 数量
|
|
||||||
FROM sys_user
|
|
||||||
WHERE user_id > 200 AND del_flag = '0'
|
|
||||||
UNION ALL
|
|
||||||
SELECT
|
|
||||||
'学习记录数',
|
|
||||||
COUNT(*)
|
|
||||||
FROM learning_record
|
|
||||||
WHERE student_id > 200
|
|
||||||
UNION ALL
|
|
||||||
SELECT
|
|
||||||
'学习详情数',
|
|
||||||
COUNT(*)
|
|
||||||
FROM learning_detail
|
|
||||||
WHERE student_id > 200
|
|
||||||
UNION ALL
|
|
||||||
SELECT
|
|
||||||
'考试成绩数',
|
|
||||||
COUNT(*)
|
|
||||||
FROM score
|
|
||||||
WHERE student_id > 200
|
|
||||||
UNION ALL
|
|
||||||
SELECT
|
|
||||||
'班级关联数',
|
|
||||||
COUNT(*)
|
|
||||||
FROM student_class
|
|
||||||
WHERE student_id > 200;
|
|
||||||
|
|
||||||
-- ============================================================
|
|
||||||
-- ⚠️ 确认无误后,执行以下删除操作
|
|
||||||
-- ============================================================
|
|
||||||
|
|
||||||
-- 3️⃣ 删除关联数据(按顺序)
|
|
||||||
|
|
||||||
-- 删除学习详情
|
|
||||||
DELETE FROM learning_detail
|
|
||||||
WHERE student_id > 200;
|
|
||||||
|
|
||||||
-- 删除学习记录
|
|
||||||
DELETE FROM learning_record
|
|
||||||
WHERE student_id > 200;
|
|
||||||
|
|
||||||
-- 删除考试成绩
|
|
||||||
DELETE FROM score
|
|
||||||
WHERE student_id > 200;
|
|
||||||
|
|
||||||
-- 删除班级关联
|
|
||||||
DELETE FROM student_class
|
|
||||||
WHERE student_id > 200;
|
|
||||||
|
|
||||||
-- 删除用户角色关联
|
|
||||||
DELETE FROM sys_user_role
|
|
||||||
WHERE user_id > 200;
|
|
||||||
|
|
||||||
-- 删除课程分配(如果用户是学生)
|
|
||||||
DELETE FROM course_assignment
|
|
||||||
WHERE student_id > 200;
|
|
||||||
|
|
||||||
-- 4️⃣ 最后删除用户表记录
|
|
||||||
DELETE FROM sys_user
|
|
||||||
WHERE user_id > 200 AND del_flag = '0';
|
|
||||||
|
|
||||||
-- 5️⃣ 验证删除结果
|
|
||||||
SELECT
|
|
||||||
'剩余用户数' AS 验证项,
|
|
||||||
COUNT(*) AS 数量
|
|
||||||
FROM sys_user
|
|
||||||
WHERE del_flag = '0';
|
|
||||||
|
|
||||||
-- ============================================================
|
|
||||||
-- 💡 如果想使用逻辑删除(推荐,可恢复),使用以下语句代替
|
|
||||||
-- ============================================================
|
|
||||||
|
|
||||||
/*
|
|
||||||
-- 逻辑删除用户(设置 del_flag = '2')
|
|
||||||
UPDATE sys_user
|
|
||||||
SET del_flag = '2',
|
|
||||||
update_time = NOW()
|
|
||||||
WHERE user_id > 200 AND del_flag = '0';
|
|
||||||
|
|
||||||
-- 验证逻辑删除
|
|
||||||
SELECT user_id, user_name, nick_name, del_flag
|
|
||||||
FROM sys_user
|
|
||||||
WHERE user_id > 200;
|
|
||||||
*/
|
|
||||||
55
log/数据库/delete_users_over_200.sql
Normal file
55
log/数据库/delete_users_over_200.sql
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
-- ============================================
|
||||||
|
-- 删除 user_id >= 100 的用户及其关联数据
|
||||||
|
-- 只删除:用户、学习记录、语音评测、课程分配
|
||||||
|
-- 保留:admin(1), ry(2) 等系统用户
|
||||||
|
-- 执行前请先备份数据库!
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- 关闭外键检查(加快删除速度)
|
||||||
|
SET FOREIGN_KEY_CHECKS = 0;
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 1. 删除学习记录
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- 删除学习详情记录
|
||||||
|
DELETE FROM learning_detail WHERE student_id >= 100;
|
||||||
|
|
||||||
|
-- 删除学习记录
|
||||||
|
DELETE FROM learning_record WHERE student_id >= 100;
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 2. 删除语音评测记录
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
DELETE FROM voice_evaluation WHERE student_id >= 100;
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 3. 删除课程分配(只删除该用户的分配,不影响课程本身)
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
DELETE FROM course_assignment WHERE student_id >= 100;
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 4. 删除用户
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
DELETE FROM sys_user WHERE user_id >= 100;
|
||||||
|
|
||||||
|
-- 恢复外键检查
|
||||||
|
SET FOREIGN_KEY_CHECKS = 1;
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 验证结果
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- 查看剩余用户数量
|
||||||
|
SELECT COUNT(*) AS '剩余用户数' FROM sys_user;
|
||||||
|
|
||||||
|
-- 查看剩余用户列表
|
||||||
|
SELECT user_id, user_name, nick_name FROM sys_user ORDER BY user_id;
|
||||||
|
|
||||||
|
-- 确认课件未受影响
|
||||||
|
SELECT COUNT(*) AS '课件数量' FROM courseware;
|
||||||
|
SELECT COUNT(*) AS '课程数量' FROM course;
|
||||||
|
SELECT COUNT(*) AS '评测内容数量' FROM voice_evaluation_content;
|
||||||
|
|
@ -178,9 +178,9 @@ def main():
|
||||||
print("="*60)
|
print("="*60)
|
||||||
|
|
||||||
# 配置参数
|
# 配置参数
|
||||||
count = 10
|
count = 3200
|
||||||
# 生成数据条数
|
# 生成数据条数
|
||||||
start_id = 500 # 起始信息编号
|
start_id = 200 # 起始信息编号
|
||||||
output_file = 'test_data.xlsx'
|
output_file = 'test_data.xlsx'
|
||||||
|
|
||||||
print(f"配置: 生成 {count} 条数据,起始编号 {start_id}")
|
print(f"配置: 生成 {count} 条数据,起始编号 {start_id}")
|
||||||
|
|
|
||||||
Binary file not shown.
Loading…
Reference in New Issue
Block a user