'commit'
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -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:
|
||||||
|
Binary file not shown.
@@ -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
|
||||||
|
|
||||||
|
BIN
dsLightRag/Util/__pycache__/ASRClient.cpython-310.pyc
Normal file
BIN
dsLightRag/Util/__pycache__/ASRClient.cpython-310.pyc
Normal file
Binary file not shown.
BIN
dsLightRag/Util/__pycache__/TTSService.cpython-310.pyc
Normal file
BIN
dsLightRag/Util/__pycache__/TTSService.cpython-310.pyc
Normal file
Binary file not shown.
Binary file not shown.
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
resultElement.textContent = '识别结果: ' + text;
|
audioElement = new Audio(data.audio_url);
|
||||||
resultElement.style.display = 'block';
|
audioElement.onloadedmetadata = function() {
|
||||||
|
updateAudioTimeDisplay();
|
||||||
|
// 音频加载完成后自动播放
|
||||||
|
try {
|
||||||
|
audioElement.play();
|
||||||
|
isPlaying = true;
|
||||||
|
updatePlayButton();
|
||||||
|
} catch (e) {
|
||||||
|
console.error("自动播放失败:", e);
|
||||||
|
}
|
||||||
|
// 无论自动播放是否成功,都显示播放按钮
|
||||||
|
document.getElementById('playAudioBtn').style.display = 'flex';
|
||||||
|
};
|
||||||
|
|
||||||
// 5秒后自动隐藏
|
audioElement.ontimeupdate = function() {
|
||||||
setTimeout(() => {
|
updateAudioProgress();
|
||||||
resultElement.style.display = 'none';
|
updateAudioTimeDisplay();
|
||||||
}, 5000);
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换音频播放/暂停
|
||||||
|
function togglePlayAudio() {
|
||||||
|
if (!audioElement) return;
|
||||||
|
|
||||||
|
if (isPlaying) {
|
||||||
|
audioElement.pause();
|
||||||
|
} else {
|
||||||
|
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的工作版本
|
||||||
|
Reference in New Issue
Block a user