From 4028e54629917f261e79eaca842f1a3c7c5118e3 Mon Sep 17 00:00:00 2001 From: HuangHai <10402852@qq.com> Date: Fri, 5 Sep 2025 22:01:55 +0800 Subject: [PATCH] 'commit' --- .../XunFeiAudioEvaluator.cpython-310.pyc | Bin 7475 -> 7486 bytes dsLightRag/Routes/XunFeiRoute.py | 101 ++- .../__pycache__/XunFeiRoute.cpython-310.pyc | Bin 4358 -> 5789 bytes .../static/XunFei/audio_evaluation.html | 825 +++++++++++++++++- .../static/XunFei/js/audio_evaluation.js | 205 ----- ...o_0b3f382e-865a-487c-ac18-e8d6ecaca28d.wav | Bin 0 -> 44 bytes ...o_777ae5af-d41f-4cf2-ab72-91204622626f.wav | Bin 0 -> 44 bytes 7 files changed, 903 insertions(+), 228 deletions(-) delete mode 100644 dsLightRag/static/XunFei/js/audio_evaluation.js create mode 100644 dsLightRag/static/audio/eval_audio_0b3f382e-865a-487c-ac18-e8d6ecaca28d.wav create mode 100644 dsLightRag/static/audio/eval_audio_777ae5af-d41f-4cf2-ab72-91204622626f.wav diff --git a/dsLightRag/KeDaXunFei/__pycache__/XunFeiAudioEvaluator.cpython-310.pyc b/dsLightRag/KeDaXunFei/__pycache__/XunFeiAudioEvaluator.cpython-310.pyc index eda476bd88a0c6613f344e5cfd3babd355422aa2..5cc0845c7979286214cef1688e0a1ed929bd458b 100644 GIT binary patch delta 169 zcmdmNwa-dBpO=@50SMxr@5(UMW?*;>;vfSyAjbiSi@7FhuVZYUcuUGHg}a$GN;*|~ zfy_dN7^W!MD7jSm6rN^AMut>*ps2z^1_(QaH&t#n!(1jPj}JtrFb0FMCjVv&MlC6x pTkNTM={cFj8CAwylQ&8mY@RATfss#unTMH!nTL^&k%y7x4FKF?B(eYi delta 158 zcmdmIwb@ELpO=@50SL05?aI*7W?*;>;vfT7AjbiSi#aB0uVZYQcuUHqnI%d(ReFKU zLWUToDA_2vRQVL{W=2MaRC%DN!a@cJJB24zZZ^YQCMb^=M5izYgRv&xW-CT5Dds9; euE~YchMN~k_cQYGG4n8UF!M0-G4e37ya50p;~+2q diff --git a/dsLightRag/Routes/XunFeiRoute.py b/dsLightRag/Routes/XunFeiRoute.py index 43c6421d..48a075ad 100644 --- a/dsLightRag/Routes/XunFeiRoute.py +++ b/dsLightRag/Routes/XunFeiRoute.py @@ -7,6 +7,9 @@ from fastapi import APIRouter, HTTPException, BackgroundTasks, Query, UploadFile from pydantic import BaseModel from typing import Optional import tempfile + +from starlette.websockets import WebSocket + from Util.ObsUtil import ObsUploader from Config.Config import OBS_BUCKET, OBS_SERVER, XF_APPID, XF_APISECRET, XF_APIKEY from fastapi.responses import StreamingResponse @@ -72,7 +75,15 @@ async def evaluate_audio( # 添加async关键字 # 创建临时文件保存上传的音频 # 修改临时文件处理逻辑 - with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as temp_audio: + import os + import uuid + # 创建持久化保存目录 + output_dir = os.path.join(os.path.dirname(__file__), '../static/audio') + os.makedirs(output_dir, exist_ok=True) + # 生成唯一文件名 + temp_audio_path = os.path.join(output_dir, f"eval_audio_{uuid.uuid4()}.wav") + + with open(temp_audio_path, 'wb') as temp_audio: # 先读取音频内容 audio_content = await audio_file.read() # 再进行格式转换 @@ -82,8 +93,6 @@ async def evaluate_audio( # 添加async关键字 wf.setsampwidth(2) wf.setframerate(16000) wf.writeframes(audio_content) - # 移除冗余的temp_audio.write(audio_content),避免重复写入 - temp_audio_path = temp_audio.name # 创建评测器实例 evaluator = XunFeiAudioEvaluator( @@ -122,8 +131,8 @@ async def evaluate_audio( # 添加async关键字 executor, evaluator.run_evaluation ) - # 清理临时文件 - os.unlink(temp_audio_path) + # 清理临时文件 - 注释掉此行以便保留文件 + # os.unlink(temp_audio_path) # 生成评测ID evaluation_id = str(uuid.uuid4()) @@ -160,3 +169,85 @@ async def get_evaluation_result(evaluation_id: str): "message": "请实现结果存储逻辑" } + +@router.websocket("/xunfei/streaming-evaluate") +async def streaming_evaluate(websocket: WebSocket): + await websocket.accept() + logger.info("讯飞语音评测WebSocket连接已建立") + + # 生成唯一音频文件名 + output_dir = os.path.join(os.path.dirname(__file__), '../static/audio') + os.makedirs(output_dir, exist_ok=True) + temp_audio_path = os.path.join(output_dir, f"eval_audio_{uuid.uuid4()}.wav") + + try: + # 初始化WAV文件 + with wave.open(temp_audio_path, 'wb') as wf: + wf.setnchannels(1) + wf.setsampwidth(2) + wf.setframerate(16000) + + # 持续接收音频流 + while True: + data = await websocket.receive_bytes() + if not data: # 结束标记 + break + wf.writeframes(data) + + # 执行评测(复用现有逻辑) + evaluator = XunFeiAudioEvaluator( + appid=XUNFEI_CONFIG["appid"], + api_key=XUNFEI_CONFIG["api_key"], + api_secret=XUNFEI_CONFIG["api_secret"], + audio_file=temp_audio_path, + language=await websocket.receive_json().get("language", "chinese") + ) + + # 根据语言设置不同的评测参数 + if language == "chinese": + # 中文评测配置 + evaluator.business_params = { + "category": "read_chapter", + "ent": "cn_vip", + "group": group, + "check_type": check_type, + "grade": grade, + } + else: + # 英文评测配置 + evaluator.business_params = { + "category": "read_chapter", + "ent": "en_vip", + "group": group, # 添加缺失参数 + "check_type": check_type, # 添加缺失参数 + "grade": grade, # 添加缺失参数 + } + + # 运行评测 + from concurrent.futures import ThreadPoolExecutor + executor = ThreadPoolExecutor(max_workers=4) + results, eval_time = await asyncio.get_event_loop().run_in_executor( + executor, evaluator.run_evaluation + ) + + # 清理临时文件 - 注释掉此行以便保留文件 + # os.unlink(temp_audio_path) + + # 生成评测ID + evaluation_id = str(uuid.uuid4()) + + return AudioEvaluationResponse( + evaluation_id=evaluation_id, + status="success", + results=results, + evaluation_time=eval_time.total_seconds() if eval_time else None + ) + + except Exception as e: + logger.error(f"评测失败: {str(e)}") + await websocket.send_json({ + "status": "error", + "message": str(e) + }) + await websocket.close() + diff --git a/dsLightRag/Routes/__pycache__/XunFeiRoute.cpython-310.pyc b/dsLightRag/Routes/__pycache__/XunFeiRoute.cpython-310.pyc index a60943b2544f915f77c387233f375ac9d435c23e..d278a8220b97e52e58b65d67a72d6f5d6f789910 100644 GIT binary patch delta 3499 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 DaAi~6 delta 2109 zcmZWqO>7%Q6rNeH*XzHHlR9>sIKRzLozk?3NQjD`N~I~4N(w3^utKt&%-G#@ZAY`? zx~@-+MDV zZ{C|XGantgK4a9A$ryoe{`S}BYa{O%c`DyI_rCIOi&}e=XIt0c^_;ddpaEZH)`k>|NW*2jUXtP<_VsK%U~|ak0dU5? zQGM6wyw?dgY0ewb7U`IGTsut1Jx6;ig-aBHGd(}NZ?ydI5Gc{~cR1CoLda|M$}R6V z?YXw3n+l8L{_QAl9cyWBxD)DuEtVb<*j@x>P9;n8|+d_ zfJN9)pXqF0pGDmWvNf1RB`VntQBu<%CeC^jv{+t->S~*@Ry+2;L2~aP8J7u`&Jn=j z05dt_#$3v>JKQgBYTU!I%TCKrkL zQ>Hg3J5y*1`2(Z5U9)VK&F%iq=66DCM1w#wqc!;IAezRuNt>jw;5{2%r5W!=bRAOT z@91erjnlCckSy=Uo^G#PQJ0#fiv`8k7`M6PzUDN_W$V(appYKE(p9hn)97Z?-Pze7=A(pW#NW2U%Ga-116*tb8wj>v(woxlyw6+RiU*9N6K3%Dc%YKO0 zs!2LCLAtn;7+Z^Z6}bNQgy3oF?l|a%A&MXHbi_17qgLb>OLn6qt$NK@>W;59YPMCo5ZoTV z+GtqJ4w85Y&E~QVk`oD+Y}PB|-D|bS?jfVtCv*@=z?C2q}s}vBao;Pf_)IsYse; zlo(a8(gYp4uU}RE5=HM@;{aXo?ie@Q57NPpK+v61rMJWa?d|^Vtr{$MxWeV0Xcq-{ z8n%27SB)1WtXfC=+I_JOV&R}4vFnwJRjY{SK^A8Ld^L#2D$+>V`zd|m39*7g;+(YX zr8Daewjz$IFv^7p9+Nc~ z^g(_+K4|V - 语音评测系统 - + 在线录音机 +
-

语音评测系统

- -
- - + + +
-
- - +
+

录音设置

+
+ +
+ + 50% +
+
-
准备就绪
+
+ +
- - + - + \ No newline at end of file diff --git a/dsLightRag/static/XunFei/js/audio_evaluation.js b/dsLightRag/static/XunFei/js/audio_evaluation.js deleted file mode 100644 index 71a877b9..00000000 --- a/dsLightRag/static/XunFei/js/audio_evaluation.js +++ /dev/null @@ -1,205 +0,0 @@ -let mediaRecorder; -let audioChunks = []; -let audioBlob; - -// 获取DOM元素 -const languageSelect = document.getElementById('language'); -const textInput = document.getElementById('text'); -const recordBtn = document.getElementById('recordBtn'); -const stopBtn = document.getElementById('stopBtn'); -const statusDiv = document.getElementById('status'); -const resultDiv = document.getElementById('result'); -const resultContent = document.getElementById('resultContent'); - - -// 开始录音 -recordBtn.addEventListener('click', async () => { - try { - statusDiv.textContent = '正在获取麦克风权限...'; - statusDiv.className = 'status'; - - // 获取麦克风权限并处理可能的错误 - const stream = await navigator.mediaDevices.getUserMedia({ audio: true }) - .catch(err => { - if (err.name === 'NotAllowedError') { - throw new Error('用户拒绝了麦克风访问权限。请在浏览器设置中允许麦克风访问,或刷新页面重试。'); - } else if (err.name === 'NotFoundError') { - throw new Error('未检测到麦克风设备,请确保您的设备已正确连接麦克风。'); - } else if (err.name === 'NotReadableError') { - throw new Error('麦克风设备正在被其他程序占用,请关闭其他可能使用麦克风的程序后重试。'); - } else { - throw err; - } - }); - - // 检测浏览器支持的录制格式 - const getSupportedMimeType = () => { - const options = [ - 'audio/webm; codecs=opus', - 'audio/webm', - 'audio/mp4', - '' // 空字符串表示使用浏览器默认格式 - ]; - for (const option of options) { - if (MediaRecorder.isTypeSupported(option)) return option; - } - return ''; - }; - - // 使用检测到的格式初始化 - mediaRecorder = new MediaRecorder(stream, { - mimeType: getSupportedMimeType(), - audioBitsPerSecond: 16000 - }); - - // 同时修正Blob类型(确保前后一致) - mediaRecorder.onstop = () => { - audioBlob = new Blob(audioChunks, { type: 'audio/webm' }); // 与录制格式匹配 - statusDiv.textContent = '录音完成,正在自动提交评测...'; - submitEvaluation(); - // 停止所有音轨以释放麦克风 - stream.getTracks().forEach(track => track.stop()); - }; - - // 设置60秒自动停止录音 - const maxRecordingTime = 60000; // 60秒 - const timeoutId = setTimeout(() => { - if (mediaRecorder && mediaRecorder.state === 'recording') { - alert('已达到最大录音时长(60秒)'); - mediaRecorder.stop(); - recordBtn.disabled = false; - stopBtn.disabled = true; - } - }, maxRecordingTime); - - mediaRecorder.start(); - statusDiv.textContent = '正在录音...(最多60秒)'; - recordBtn.disabled = true; - stopBtn.disabled = false; - - } catch (error) { - console.error('录音初始化失败:', error); - alert('录音失败: ' + error.message); - statusDiv.textContent = '录音失败: ' + error.message; - statusDiv.className = 'status error'; - recordBtn.disabled = false; - stopBtn.disabled = true; - } -}); - -// 停止录音 -stopBtn.addEventListener('click', () => { - if (mediaRecorder && mediaRecorder.state === 'recording') { - mediaRecorder.stop(); - recordBtn.disabled = false; - stopBtn.disabled = true; - } -}); - -// 提交评测 -async function submitEvaluation() { - if (!audioBlob) { - statusDiv.textContent = '请先完成录音'; - statusDiv.className = 'status error'; - return; - } - - // 移除文本验证 - // if (!textInput.value.trim()) { - // statusDiv.textContent = '请输入评测文本内容'; - // statusDiv.className = 'status error'; - // return; - // } - - try { - statusDiv.textContent = '正在提交评测...'; - statusDiv.className = 'status'; - - const formData = new FormData(); - formData.append('audio_file', audioBlob, 'recording.webm'); - formData.append('language', languageSelect.value); - // 移除文本参数 - // formData.append('text', textInput.value.trim()); - formData.append('group', 'adult'); - formData.append('check_type', 'common'); - formData.append('grade', 'middle'); - - const response = await fetch('/api/xunFei/evaluate-audio', { - method: 'POST', - body: formData - }); - - if (!response.ok) { - throw new Error(`服务器错误: HTTP状态码 ${response.status}`); - } - - const result = await response.json(); - - if (result.status === 'success') { - displayResults(result.results); - statusDiv.textContent = '评测成功完成'; - statusDiv.className = 'status success'; - } else { - throw new Error(result.error_message || '评测失败,服务器返回未知错误'); - } - - } catch (error) { - console.error('评测提交失败:', error); - statusDiv.textContent = '评测失败: ' + error.message; - statusDiv.className = 'status error'; - } -} - -// 显示评测结果 -function displayResults(results) { - resultDiv.style.display = 'block'; - - if (!results) { - resultContent.innerHTML = '

暂无评测结果

'; - return; - } - - let html = '
'; - - // 显示总分 - if (results.total_score !== undefined) { - html += `

总分: ${results.total_score.toFixed(4)} / 5.0

`; - } - - // 显示各项评分 - if (results.accuracy_score !== undefined) { - html += `

准确度: ${results.accuracy_score.toFixed(4)}

`; - } - // 添加结果对象有效性检查 - if (!results) { - showError("评测结果格式错误"); - return; - } - - if (results.fluency_score !== undefined) { - html += `

流利度: ${results.fluency_score.toFixed(4)}

`; - } else { - html += `

流利度: 未获取

`; // 添加默认值 - } - - if (results.completeness_score !== undefined) { - html += `

完整度: ${results.completeness_score.toFixed(4)}

`; - } else { - html += `

完整度: 未获取

`; // 添加默认值 - } - - html += '
'; - - // 显示单词级评分 - if (results.words && results.words.length > 0) { - html += '

单词评分:

    '; - results.words.forEach(word => { - // 为单词评分添加空值检查 - const score = word.score !== undefined ? word.score.toFixed(4) : '无'; - html += `
  • ${word.content}: ${score}
  • `; - }); - html += '
'; - } - - resultContent.innerHTML = html; -} \ No newline at end of file diff --git a/dsLightRag/static/audio/eval_audio_0b3f382e-865a-487c-ac18-e8d6ecaca28d.wav b/dsLightRag/static/audio/eval_audio_0b3f382e-865a-487c-ac18-e8d6ecaca28d.wav new file mode 100644 index 0000000000000000000000000000000000000000..d26b87bdbb5a1d3bd5a9b12a2e0b6a4107616f9d GIT binary patch literal 44 tcmWIYbaPW-U|