This commit is contained in:
2025-08-31 12:43:43 +08:00
parent 67feea9443
commit 67f76ab49f
4 changed files with 5 additions and 296 deletions

View File

@@ -6,6 +6,11 @@ from datetime import datetime
from fastapi import APIRouter, Request, File, UploadFile, WebSocket, WebSocketDisconnect from fastapi import APIRouter, Request, File, UploadFile, WebSocket, WebSocketDisconnect
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from Util.XueBanUtil import get_xueban_response_async
from Util.ASRClient import ASRClient
from Util.ObsUtil import ObsUploader
from Util.TTS_Pipeline import stream_and_split_text, StreamingVolcanoTTS
# 创建路由路由器 # 创建路由路由器
router = APIRouter(prefix="/api", tags=["学伴"]) router = APIRouter(prefix="/api", tags=["学伴"])
@@ -13,13 +18,6 @@ router = APIRouter(prefix="/api", tags=["学伴"])
# 配置日志 # 配置日志
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# 导入学伴工具函数、ASR客户端和OBS上传工具
from Util.XueBanUtil import get_xueban_response_async
from Util.ASRClient import ASRClient
from Util.ObsUtil import ObsUploader
# 导入TTS管道
from Util.TTS_Pipeline import stream_and_split_text, StreamingVolcanoTTS
# 保留原有的HTTP接口用于向后兼容 # 保留原有的HTTP接口用于向后兼容
@router.post("/xueban/upload-audio") @router.post("/xueban/upload-audio")
async def upload_audio(file: UploadFile = File(...)): async def upload_audio(file: UploadFile = File(...)):

View File

@@ -109,14 +109,6 @@
</style> </style>
</head> </head>
<body> <body>
<div class="model-selector">
<label for="model-select">选择学伴:</label>
<select id="model-select">
<option value="shizuku">小智</option>
<option value="koharu">小荷</option>
<option value="wanko">汪喵</option>
</select>
</div>
<div class="recording-indicator" id="recordingIndicator"> <div class="recording-indicator" id="recordingIndicator">
<div class="recording-dot"></div> <div class="recording-dot"></div>
@@ -191,286 +183,5 @@
<option value="wanko">汪喵</option> <option value="wanko">汪喵</option>
</select> </select>
</div> </div>
// 模型配置 - 使用与Sample.html相同的CDN链接
const models = {
shizuku: { jsonPath: "https://unpkg.com/live2d-widget-model-shizuku@1.0.5/assets/shizuku.model.json", name: "小智" },
koharu: { jsonPath: "https://unpkg.com/live2d-widget-model-koharu@1.0.5/assets/koharu.model.json", name: "小荷" },
wanko: { jsonPath: "https://unpkg.com/live2d-widget-model-wanko@1.0.5/assets/wanko.model.json", name: "汪喵" }
};
// 录音相关变量
let mediaRecorder; let audioChunks = []; let isRecording = false;
// 音频播放相关变量
let audioElement = null; let isPlaying = false;
// 获取URL参数
function getUrlParam(name) {
const reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)');
const r = window.location.search.substr(1).match(reg);
return r ? unescape(r[2]) : null;
}
// 开始录音
function startRecording() {
if (isRecording) return;
console.log("尝试开始录音");
navigator.mediaDevices.getUserMedia({ audio: true })
.then(stream => {
mediaRecorder = new MediaRecorder(stream);
audioChunks = [];
mediaRecorder.ondataavailable = event => {
if (event.data.size > 0) audioChunks.push(event.data);
};
mediaRecorder.onstop = () => {
const audioBlob = new Blob(audioChunks, { type: 'audio/wav' });
console.log("录音完成,音频数据大小:", audioBlob.size);
const audioUrl = URL.createObjectURL(audioBlob);
console.log("录音URL:", audioUrl);
// 这里可以调用ASR服务
uploadAudioToServer(audioBlob);
};
mediaRecorder.start();
isRecording = true;
document.getElementById('recordingIndicator').style.display = 'flex';
document.getElementById('startRecordBtn').style.display = 'none';
document.getElementById('stopRecordBtn').style.display = 'flex';
console.log("开始录音成功");
// 设置最长录音时间为60秒
setTimeout(stopRecording, 60000);
})
.catch(error => {
console.error("获取麦克风权限失败:", error);
alert("请授权麦克风权限以使用录音功能");
});
}
// 停止录音
function stopRecording() {
if (!isRecording || !mediaRecorder) return;
mediaRecorder.stop();
isRecording = false;
document.getElementById('recordingIndicator').style.display = 'none';
document.getElementById('startRecordBtn').style.display = 'flex';
document.getElementById('stopRecordBtn').style.display = 'none';
console.log("停止录音");
if (mediaRecorder.stream) {
mediaRecorder.stream.getTracks().forEach(track => track.stop());
}
}
// 上传音频到服务器
function uploadAudioToServer(audioBlob) {
console.log("开始上传音频到服务器");
// 显示思考中动画
document.getElementById('thinkingIndicator').style.display = 'flex';
const formData = new FormData();
formData.append('file', audioBlob, 'recording.wav');
fetch('/api/xueban/upload-audio', {
method: 'POST',
body: formData
})
.then(response => {
if (!response.ok) {
throw new Error('服务器响应错误');
}
return response.json();
})
.then(data => {
console.log("处理结果:", data);
// 隐藏思考中动画
document.getElementById('thinkingIndicator').style.display = 'none';
if (data.success) {
showResults(data.data);
} else {
alert('音频处理失败: ' + data.message);
}
})
.catch(error => {
console.error("上传音频失败:", error);
// 隐藏思考中动画
document.getElementById('thinkingIndicator').style.display = 'none';
alert('上传音频失败: ' + error.message);
});
}
// 显示ASR识别结果和反馈
function showResults(data) {
// 更新结果显示容器
const resultContainer = document.getElementById('resultContainer');
resultContainer.style.display = 'flex';
// 显示ASR结果
document.getElementById('asrResultText').textContent = data.asr_text || '未识别到内容';
// 显示反馈文本
document.getElementById('feedbackResultText').textContent = data.feedback_text || '无反馈内容';
// 准备音频播放
if (data.audio_url) {
if (audioElement) {
audioElement.pause();
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;
};
}
}
// 切换音频播放/暂停
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的工作版本
function initL2Dwidget() {
const modelId = getUrlParam('id') || 'koharu';
const model = models[modelId] || models.koharu;
document.getElementById('model-select').value = modelId;
console.log('加载模型:', model.name, model.jsonPath);
// 初始化模型 - 与Sample.html相同的配置
L2Dwidget.init({
"model": { "jsonPath": model.jsonPath, "scale": 1 },
"display": {
"position": "right",
"width": 150,
"height": 300,
"hOffset": 0, // 重置水平偏移
"vOffset": -20 // 重置垂直偏移
},
"mobile": { "show": true, "scale": 0.5 },
"react": { "opacityDefault": 0.8, "opacityOnHover": 1 },
"dialog": { "enable": true, "script": {
'tap body': `你好啊,我是${model.name}。`,
'tap face': '有什么问题或者烦心事都可以和我聊聊~'
}}
});
}
// 页面加载完成后初始化
window.onload = function() {
// 直接初始化看板娘,不添加额外延迟
initL2Dwidget();
// 监听下拉框变化使用独立JS暴露的接口
document.getElementById('model-select').addEventListener('change', function() {
window.switchL2DModel(this.value);
});
// 绑定录音按钮事件
document.getElementById('startRecordBtn').addEventListener('click', startRecording);
document.getElementById('stopRecordBtn').addEventListener('click', stopRecording);
// 页面加载时请求麦克风权限
navigator.mediaDevices.getUserMedia({ audio: true })
.then(stream => {
console.log("麦克风权限已授予");
// 立即停止流,只获取权限
stream.getTracks().forEach(track => track.stop());
})
.catch(error => {
console.error("获取麦克风权限失败:", error);
alert("请授权麦克风权限以使用录音功能");
});
};
</body> </body>
</html> </html>