修改所有bug

This commit is contained in:
xiao12feng8 2025-12-13 13:36:18 +08:00
parent c01d45b200
commit dfb1c4a7b7
17 changed files with 767 additions and 743 deletions

View File

@ -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("");

View File

@ -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)

View File

@ -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:
# 从数据源开关/默认关闭 # 从数据源开关/默认关闭

View File

@ -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;
/** 班级名称(用于按名称匹配班级,可选) */ /** 班级名称(用于按名称匹配班级,可选) */

View File

@ -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格式包含详细评测结果 */

View File

@ -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;

View File

@ -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>

View File

@ -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)

View File

@ -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': '困难' }

View File

@ -1,6 +1,4 @@
# 语音评测新方案 - 快速开始
## ✅ 已完成
1. ✅ **禁用 UTS 插件** - 不再依赖本地编译 1. ✅ **禁用 UTS 插件** - 不再依赖本地编译
2. ✅ **创建录音工具** - `utils/speech-recorder.js` 2. ✅ **创建录音工具** - `utils/speech-recorder.js`

View File

@ -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
View 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. 检查 ffmpegWhisper需要
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)

View File

@ -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)

View File

@ -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;
*/

View 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;

View File

@ -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.