'commit'
This commit is contained in:
Binary file not shown.
@@ -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(...)):
|
||||||
|
Binary file not shown.
@@ -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>
|
Reference in New Issue
Block a user