This commit is contained in:
2025-08-22 10:22:27 +08:00
parent 6dd778ac85
commit ee53250d00
9 changed files with 258 additions and 33 deletions

View File

@@ -17,6 +17,8 @@ logger = logging.getLogger(__name__)
from Util.XueBanUtil import get_xueban_response_async from Util.XueBanUtil import get_xueban_response_async
from Util.ASRClient import ASRClient from Util.ASRClient import ASRClient
from Util.ObsUtil import ObsUploader from Util.ObsUtil import ObsUploader
# 新增导入TTSService
from Util.TTSService import TTSService
@router.post("/xueban/upload-audio") @router.post("/xueban/upload-audio")
@@ -49,11 +51,36 @@ async def upload_audio(file: UploadFile = File(...)):
os.remove(temp_file_path) os.remove(temp_file_path)
logger.info(f"临时文件已删除: {temp_file_path}") logger.info(f"临时文件已删除: {temp_file_path}")
# 返回识别结果 # 使用大模型生成反馈
logger.info(f"使用大模型生成反馈,输入文本: {asr_result['text']}")
response_generator = get_xueban_response_async(asr_result['text'], stream=False)
feedback_text = ""
async for chunk in response_generator:
feedback_text += chunk
logger.info(f"大模型反馈生成完成: {feedback_text}")
# 使用TTS生成语音
tts_service = TTSService()
tts_temp_file = os.path.join(tempfile.gettempdir(), f"tts_{timestamp}.mp3")
success = tts_service.synthesize(feedback_text, output_file=tts_temp_file)
if not success:
raise Exception("TTS语音合成失败")
logger.info(f"TTS语音合成成功文件保存至: {tts_temp_file}")
# 上传TTS音频文件到OBS
tts_audio_url = upload_file_to_obs(tts_temp_file)
os.remove(tts_temp_file) # 删除临时TTS文件
logger.info(f"TTS文件已上传至OBS: {tts_audio_url}")
# 返回结果包含ASR文本和TTS音频URL
return JSONResponse(content={ return JSONResponse(content={
"success": True, "success": True,
"message": "音频识别成功", "message": "音频处理和语音反馈生成成功",
"data": asr_result "data": {
"asr_text": asr_result['text'],
"feedback_text": feedback_text,
"audio_url": tts_audio_url
}
}) })
except Exception as e: except Exception as e:

View File

@@ -2,8 +2,6 @@
import traceback import traceback
import logging import logging
import base64 # 添加base64模块导入 import base64 # 添加base64模块导入
import requests # 添加requests模块导入
from obs import ObsClient from obs import ObsClient
from obs import PutObjectHeader from obs import PutObjectHeader

Binary file not shown.

Binary file not shown.

View File

@@ -49,6 +49,63 @@
border-radius: 50%; margin-right: 8px; animation: pulse 1.5s infinite; border-radius: 50%; margin-right: 8px; animation: pulse 1.5s infinite;
} }
@keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.5; } 100% { opacity: 1; } } @keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.5; } 100% { opacity: 1; } }
/* 添加思考中动画样式 */
.thinking-indicator {
position: fixed; bottom: 20px; right: 120px; z-index: 1000;
padding: 10px 15px; background-color: rgba(0, 123, 255, 0.9);
color: white; border-radius: 20px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
display: none; align-items: center;
}
.thinking-dots {
display: flex; gap: 4px; margin-right: 8px;
}
.thinking-dot {
width: 8px; height: 8px; background-color: white;
border-radius: 50%;
}
.thinking-dot:nth-child(1) { animation: pulse 1.5s infinite 0s; }
.thinking-dot:nth-child(2) { animation: pulse 1.5s infinite 0.3s; }
.thinking-dot:nth-child(3) { animation: pulse 1.5s infinite 0.6s; }
/* 新增样式 */
.result-container {
position: fixed; bottom: 80px; left: 20px; z-index: 1000;
padding: 15px; background-color: rgba(255, 255, 255, 0.95);
border-radius: 8px; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.15);
max-width: 400px; width: 90%;
display: none; flex-direction: column; gap: 10px;
}
.result-header {
font-weight: bold; color: #495057; margin-bottom: 5px;
display: flex; align-items: center; gap: 5px;
}
.result-header.asr { color: #007bff; }
.result-header.feedback { color: #28a745; }
.result-text {
color: #333; line-height: 1.5; max-height: 200px; overflow-y: auto;
padding-right: 5px; word-break: break-all;
}
.audio-player-container {
margin-top: 10px; display: flex; align-items: center; gap: 10px;
}
.play-button {
background-color: #28a745; color: white; border: none;
border-radius: 50%; width: 40px; height: 40px;
cursor: pointer; display: flex; align-items: center; justify-content: center;
display: flex; /* 始终显示播放按钮 */
}
.play-button:hover { background-color: #218838; }
.audio-progress {
flex-grow: 1; height: 6px; background-color: #e9ecef; border-radius: 3px;
overflow: hidden; cursor: pointer;
}
.progress-bar {
height: 100%; background-color: #28a745; width: 0%;
}
.audio-time {
font-size: 12px; color: #6c757d;
}
</style> </style>
</head> </head>
<body> <body>
@@ -66,10 +123,56 @@
<span>正在录音...</span> <span>正在录音...</span>
</div> </div>
<!-- 添加思考中指示器 -->
<div class="thinking-indicator" id="thinkingIndicator">
<div class="thinking-dots">
<div class="thinking-dot"></div>
<div class="thinking-dot"></div>
<div class="thinking-dot"></div>
</div>
<span>学伴正在思考中...</span>
</div>
<!-- 在看板娘附近添加录音按钮 --> <!-- 在看板娘附近添加录音按钮 -->
<div class="recording-controls" style="position: fixed; right:37px; bottom: 230px; z-index: 998;"> <div class="recording-controls" style="position: fixed; right:37px; bottom: 230px; z-index: 998;">
<button class="record-button" id="startRecordBtn" style="font-size: 14px;">开始录音</button> <button class="record-button" id="startRecordBtn" style="font-size: 14px;">和我讲话</button>
<button class="stop-button" id="stopRecordBtn" style="font-size: 14px;">停止录音</button> <button class="stop-button" id="stopRecordBtn" style="font-size: 14px;">停止讲话</button>
</div>
<!-- 新增结果显示容器 -->
<div class="result-container" id="resultContainer">
<div>
<div class="result-header asr">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 1H4C2.89 1 2 1.9 2 3V13C2 14.1 2.89 15 4 15H12C13.11 15 14 14.1 14 13V3C14 1.9 13.11 1 12 1ZM12 13H4V3H12V13Z" fill="#007bff"/>
<path d="M8 4C6.34 4 5 5.34 5 7C5 8.66 6.34 10 8 10C9.66 10 11 8.66 11 7C11 5.34 9.66 4 8 4ZM8 8.5C7.17 8.5 6.5 7.83 6.5 7C6.5 6.17 7.17 5.5 8 5.5C8.83 5.5 9.5 6.17 9.5 7C9.5 7.83 8.83 8.5 8 8.5Z" fill="#007bff"/>
<path d="M8 11C5.79 11 4 12.79 4 15V16H12V15C12 12.79 10.21 11 8 11ZM8 13C9.1 13 10 13.9 10 15H6C6 13.9 6.9 13 8 13Z" fill="#007bff"/>
</svg>
你讲的话
</div>
<div class="result-text" id="asrResultText"></div>
</div>
<div>
<div class="result-header feedback">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 4C1 2.9 1.9 2 3 2H13C14.1 2 15 2.9 15 4V11C15 12.1 14.1 13 13 13H9L5 16V13H3C1.9 13 1 12.1 1 11V4Z" fill="#28a745"/>
<path d="M8 8H6V5H8V8ZM11 8H9V5H11V8ZM11 11H6V9H11V11Z" fill="#28a745"/>
</svg>
学伴回复
</div>
<div class="result-text" id="feedbackResultText"></div>
</div>
<div class="audio-player-container">
<button class="play-button" id="playAudioBtn">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 5V19L19 12L8 5Z" fill="white"/>
</svg>
</button>
<div class="audio-progress" id="audioProgress">
<div class="progress-bar" id="progressBar"></div>
</div>
<div class="audio-time" id="audioTime">00:00 / 00:00</div>
</div>
</div> </div>
<script src="https://l2dwidget.js.org/lib/L2Dwidget.min.js"></script> <script src="https://l2dwidget.js.org/lib/L2Dwidget.min.js"></script>
@@ -83,6 +186,8 @@
// 录音相关变量 // 录音相关变量
let mediaRecorder; let audioChunks = []; let isRecording = false; let mediaRecorder; let audioChunks = []; let isRecording = false;
// 音频播放相关变量
let audioElement = null; let isPlaying = false;
// 获取URL参数 // 获取URL参数
function getUrlParam(name) { function getUrlParam(name) {
@@ -149,6 +254,9 @@
// 上传音频到服务器 // 上传音频到服务器
function uploadAudioToServer(audioBlob) { function uploadAudioToServer(audioBlob) {
console.log("开始上传音频到服务器"); console.log("开始上传音频到服务器");
// 显示思考中动画
document.getElementById('thinkingIndicator').style.display = 'flex';
const formData = new FormData(); const formData = new FormData();
formData.append('file', audioBlob, 'recording.wav'); formData.append('file', audioBlob, 'recording.wav');
@@ -163,45 +271,137 @@
return response.json(); return response.json();
}) })
.then(data => { .then(data => {
console.log("ASR识别结果:", data); console.log("处理结果:", data);
// 隐藏思考中动画
document.getElementById('thinkingIndicator').style.display = 'none';
if (data.success) { if (data.success) {
showAsrResult(data.data.text); showResults(data.data);
} else { } else {
alert('音频识别失败: ' + data.message); alert('音频处理失败: ' + data.message);
} }
}) })
.catch(error => { .catch(error => {
console.error("上传音频失败:", error); console.error("上传音频失败:", error);
// 隐藏思考中动画
document.getElementById('thinkingIndicator').style.display = 'none';
alert('上传音频失败: ' + error.message); alert('上传音频失败: ' + error.message);
}); });
} }
// 显示ASR识别结果 // 显示ASR识别结果和反馈
function showAsrResult(text) { function showResults(data) {
// 检查是否已存在结果显示元素 // 更新结果显示容器
let resultElement = document.getElementById('asrResult'); const resultContainer = document.getElementById('resultContainer');
if (!resultElement) { resultContainer.style.display = 'flex';
resultElement = document.createElement('div');
resultElement.id = 'asrResult'; // 显示ASR结果
resultElement.style.position = 'fixed'; document.getElementById('asrResultText').textContent = data.asr_text || '未识别到内容';
resultElement.style.bottom = '80px';
resultElement.style.left = '20px'; // 显示反馈文本
resultElement.style.zIndex = '1000'; document.getElementById('feedbackResultText').textContent = data.feedback_text || '无反馈内容';
resultElement.style.padding = '10px 15px';
resultElement.style.backgroundColor = 'rgba(0, 123, 255, 0.9)'; // 准备音频播放
resultElement.style.color = 'white'; if (data.audio_url) {
resultElement.style.borderRadius = '5px'; if (audioElement) {
resultElement.style.boxShadow = '0 2px 10px rgba(0, 0, 0, 0.2)'; audioElement.pause();
document.body.appendChild(resultElement); audioElement = null;
}
audioElement = new Audio(data.audio_url);
audioElement.onloadedmetadata = function() {
updateAudioTimeDisplay();
// 音频加载完成后自动播放
try {
audioElement.play();
isPlaying = true;
updatePlayButton();
} catch (e) {
console.error("自动播放失败:", e);
}
// 无论自动播放是否成功,都显示播放按钮
document.getElementById('playAudioBtn').style.display = 'flex';
};
audioElement.ontimeupdate = function() {
updateAudioProgress();
updateAudioTimeDisplay();
};
audioElement.onended = function() {
isPlaying = false;
updatePlayButton();
};
// 绑定播放按钮事件
document.getElementById('playAudioBtn').onclick = togglePlayAudio;
// 绑定进度条点击事件
document.getElementById('audioProgress').onclick = function(e) {
if (!audioElement) return;
const progressBar = document.getElementById('audioProgress');
const rect = progressBar.getBoundingClientRect();
const clickPosition = (e.clientX - rect.left) / rect.width;
audioElement.currentTime = clickPosition * audioElement.duration;
};
} }
}
resultElement.textContent = '识别结果: ' + text; // 切换音频播放/暂停
resultElement.style.display = 'block'; function togglePlayAudio() {
if (!audioElement) return;
// 5秒后自动隐藏 if (isPlaying) {
setTimeout(() => { audioElement.pause();
resultElement.style.display = 'none'; } else {
}, 5000); audioElement.play();
}
isPlaying = !isPlaying;
updatePlayButton();
}
// 更新播放按钮状态
function updatePlayButton() {
const playButton = document.getElementById('playAudioBtn');
if (isPlaying) {
playButton.innerHTML = `
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 19H10V5H6V19ZM14 19H18V5H14V19Z" fill="white"/>
</svg>
`;
} else {
playButton.innerHTML = `
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 5V19L19 12L8 5Z" fill="white"/>
</svg>
`;
}
}
// 更新音频进度条
function updateAudioProgress() {
if (!audioElement || !audioElement.duration) return;
const progress = (audioElement.currentTime / audioElement.duration) * 100;
document.getElementById('progressBar').style.width = `${progress}%`;
}
// 更新音频时间显示
function updateAudioTimeDisplay() {
if (!audioElement || !audioElement.duration) return;
const currentTime = formatTime(audioElement.currentTime);
const duration = formatTime(audioElement.duration);
document.getElementById('audioTime').textContent = `${currentTime} / ${duration}`;
}
// 格式化时间为 MM:SS
function formatTime(seconds) {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
} }
// 初始化看板娘 - 简化为Sample.html的工作版本 // 初始化看板娘 - 简化为Sample.html的工作版本