chore: update dependencies and remove unused files
refactor: migrate from nodemon to http-server for development build: add type module and update package.json scripts
This commit is contained in:
305
script.js
305
script.js
@@ -1,31 +1,36 @@
|
||||
// 导入 Transformers.js 的 pipeline
|
||||
import { pipeline } from 'https://cdn.jsdelivr.net/npm/@xenova/transformers@2.17.1';
|
||||
import BellaAI from './core.js';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
// --- 加载屏幕处理 ---
|
||||
document.addEventListener('DOMContentLoaded', async function() {
|
||||
// --- Get all necessary DOM elements first ---
|
||||
const transcriptDiv = document.getElementById('transcript');
|
||||
const loadingScreen = document.getElementById('loading-screen');
|
||||
const video1 = document.getElementById('video1');
|
||||
const video2 = document.getElementById('video2');
|
||||
const micButton = document.getElementById('mic-button');
|
||||
|
||||
|
||||
// --- AI Core Initialization ---
|
||||
let bellaAI;
|
||||
micButton.disabled = true;
|
||||
transcriptDiv.textContent = '正在唤醒贝拉的核心...';
|
||||
try {
|
||||
bellaAI = await BellaAI.getInstance();
|
||||
micButton.disabled = false;
|
||||
transcriptDiv.textContent = '贝拉已准备好,请点击麦克风开始对话。';
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize Bella AI:', error);
|
||||
transcriptDiv.textContent = '唤醒贝拉时遇到问题,请检查控制台信息。';
|
||||
// Keep the app running with visual features even if AI fails
|
||||
}
|
||||
|
||||
// --- Loading screen handling ---
|
||||
setTimeout(() => {
|
||||
loadingScreen.style.opacity = '0';
|
||||
// 在动画结束后将其隐藏,以防它阻碍交互
|
||||
// Hide it after the animation to prevent it from blocking interactions
|
||||
setTimeout(() => {
|
||||
loadingScreen.style.display = 'none';
|
||||
}, 500); // 这个时间应该匹配 CSS 中的 transition 时间
|
||||
}, 1500); // 1.5秒后开始淡出
|
||||
|
||||
// 获取需要的 DOM 元素
|
||||
let video1 = document.getElementById('video1');
|
||||
let video2 = document.getElementById('video2');
|
||||
const micButton = document.getElementById('mic-button');
|
||||
const favorabilityBar = document.getElementById('favorability-bar');
|
||||
const floatingButton = document.getElementById('floating-button');
|
||||
const menuContainer = document.getElementById('menu-container');
|
||||
const menuItems = document.querySelectorAll('.menu-item');
|
||||
|
||||
// --- 情感分析元素 ---
|
||||
const sentimentInput = document.getElementById('sentiment-input');
|
||||
const analyzeButton = document.getElementById('analyze-button');
|
||||
const sentimentResult = document.getElementById('sentiment-result');
|
||||
}, 500); // This time should match the transition time in CSS
|
||||
}, 1500); // Start fading out after 1.5 seconds
|
||||
|
||||
let activeVideo = video1;
|
||||
let inactiveVideo = video2;
|
||||
@@ -91,7 +96,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
recognition.lang = 'zh-CN'; // 设置语言为中文
|
||||
recognition.interimResults = true; // 获取临时结果
|
||||
|
||||
recognition.onresult = (event) => {
|
||||
recognition.onresult = async (event) => {
|
||||
const transcriptContainer = document.getElementById('transcript');
|
||||
let final_transcript = '';
|
||||
let interim_transcript = '';
|
||||
@@ -103,13 +108,41 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
interim_transcript += event.results[i][0].transcript;
|
||||
}
|
||||
}
|
||||
|
||||
// 显示最终识别结果
|
||||
transcriptContainer.textContent = final_transcript || interim_transcript;
|
||||
|
||||
// 基于关键词的情感分析和视频切换
|
||||
if (final_transcript) {
|
||||
analyzeAndReact(final_transcript);
|
||||
|
||||
// Update interim results
|
||||
transcriptContainer.textContent = `你: ${final_transcript || interim_transcript}`;
|
||||
|
||||
// Once we have a final result, process it with the AI
|
||||
if (final_transcript && bellaAI) {
|
||||
const userText = final_transcript.trim();
|
||||
transcriptContainer.textContent = `你: ${userText}`;
|
||||
|
||||
try {
|
||||
// Let Bella think
|
||||
const thinkingText = document.createElement('p');
|
||||
thinkingText.textContent = '贝拉正在思考...';
|
||||
transcriptContainer.appendChild(thinkingText);
|
||||
|
||||
const response = await bellaAI.think(userText);
|
||||
|
||||
transcriptContainer.removeChild(thinkingText);
|
||||
const bellaText = document.createElement('p');
|
||||
bellaText.textContent = `贝拉: ${response}`;
|
||||
transcriptContainer.appendChild(bellaText);
|
||||
|
||||
// Let Bella speak
|
||||
const audioData = await bellaAI.speak(response);
|
||||
const blob = new Blob([audioData], { type: 'audio/wav' });
|
||||
const audioUrl = URL.createObjectURL(blob);
|
||||
const audio = new Audio(audioUrl);
|
||||
audio.play();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Bella AI processing error:', error);
|
||||
const errorText = document.createElement('p');
|
||||
errorText.textContent = '贝拉处理时遇到问题。';
|
||||
transcriptContainer.appendChild(errorText);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -145,218 +178,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
});
|
||||
|
||||
|
||||
// --- 悬浮按钮交互 ---
|
||||
floatingButton.addEventListener('click', (event) => {
|
||||
event.stopPropagation(); // 防止事件冒泡到 document
|
||||
menuContainer.classList.toggle('hidden');
|
||||
});
|
||||
|
||||
menuItems.forEach(item => {
|
||||
item.addEventListener('click', function() {
|
||||
const videoSrc = this.getAttribute('data-video');
|
||||
playSpecificVideo(videoSrc);
|
||||
menuContainer.classList.add('hidden');
|
||||
});
|
||||
});
|
||||
|
||||
// 点击菜单外部区域关闭菜单
|
||||
document.addEventListener('click', () => {
|
||||
if (!menuContainer.classList.contains('hidden')) {
|
||||
menuContainer.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
// 阻止菜单自身的点击事件冒泡
|
||||
menuContainer.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
});
|
||||
|
||||
|
||||
function playSpecificVideo(videoSrc) {
|
||||
const currentVideoSrc = activeVideo.querySelector('source').getAttribute('src');
|
||||
if (videoSrc === currentVideoSrc) return;
|
||||
|
||||
inactiveVideo.querySelector('source').setAttribute('src', videoSrc);
|
||||
inactiveVideo.load();
|
||||
|
||||
inactiveVideo.addEventListener('canplaythrough', function onCanPlayThrough() {
|
||||
inactiveVideo.removeEventListener('canplaythrough', onCanPlayThrough);
|
||||
activeVideo.pause(); // 暂停当前视频,防止其 'ended' 事件触发切换
|
||||
inactiveVideo.play().catch(error => console.error("Video play failed:", error));
|
||||
activeVideo.classList.remove('active');
|
||||
inactiveVideo.classList.add('active');
|
||||
[activeVideo, inactiveVideo] = [inactiveVideo, activeVideo];
|
||||
activeVideo.addEventListener('ended', switchVideo, { once: true });
|
||||
}, { once: true });
|
||||
}
|
||||
|
||||
// --- 情感分析与反应 ---
|
||||
const positiveWords = ['开心', '高兴', '喜欢', '太棒了', '你好', '漂亮'];
|
||||
const negativeWords = ['难过', '生气', '讨厌', '伤心'];
|
||||
|
||||
const positiveVideos = [
|
||||
'视频资源/jimeng-2025-07-16-1043-笑着优雅的左右摇晃,过一会儿手扶着下巴,保持微笑.mp4',
|
||||
'视频资源/jimeng-2025-07-16-4437-比耶,然后微笑着优雅的左右摇晃.mp4',
|
||||
'视频资源/生成加油视频.mp4',
|
||||
'视频资源/生成跳舞视频.mp4'
|
||||
];
|
||||
const negativeVideo = '视频资源/负面/jimeng-2025-07-16-9418-双手叉腰,嘴巴一直在嘟囔,表情微微生气.mp4';
|
||||
|
||||
// --- 本地模型情感分析 ---
|
||||
let classifier;
|
||||
analyzeButton.addEventListener('click', async () => {
|
||||
const text = sentimentInput.value;
|
||||
if (!text) return;
|
||||
|
||||
sentimentResult.textContent = '正在分析中...';
|
||||
|
||||
// 第一次点击时,初始化分类器
|
||||
if (!classifier) {
|
||||
try {
|
||||
classifier = await pipeline('sentiment-analysis');
|
||||
} catch (error) {
|
||||
console.error('模型加载失败:', error);
|
||||
sentimentResult.textContent = '抱歉,模型加载失败了。';
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 进行情感分析
|
||||
try {
|
||||
const result = await classifier(text);
|
||||
// 显示最主要的情绪和分数
|
||||
const primaryEmotion = result[0];
|
||||
sentimentResult.textContent = `情绪: ${primaryEmotion.label}, 分数: ${primaryEmotion.score.toFixed(2)}`;
|
||||
} catch (error) {
|
||||
console.error('情感分析失败:', error);
|
||||
sentimentResult.textContent = '分析时出错了。';
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// --- 本地语音识别 --- //
|
||||
const localMicButton = document.getElementById('local-mic-button');
|
||||
const localAsrResult = document.getElementById('local-asr-result');
|
||||
|
||||
let recognizer = null;
|
||||
let mediaRecorder = null;
|
||||
let isRecording = false;
|
||||
|
||||
const handleRecord = async () => {
|
||||
// 状态切换:如果正在录音,则停止
|
||||
if (isRecording) {
|
||||
mediaRecorder.stop();
|
||||
isRecording = false;
|
||||
localMicButton.textContent = '开始本地识别';
|
||||
localMicButton.classList.remove('recording');
|
||||
return;
|
||||
}
|
||||
|
||||
// 初始化模型(仅一次)
|
||||
if (!recognizer) {
|
||||
localAsrResult.textContent = '正在加载语音识别模型...';
|
||||
try {
|
||||
recognizer = await pipeline('automatic-speech-recognition', 'Xenova/whisper-tiny');
|
||||
localAsrResult.textContent = '模型加载完毕,请开始说话...';
|
||||
} catch (error) {
|
||||
console.error('模型加载失败:', error);
|
||||
localAsrResult.textContent = '抱歉,模型加载失败了。';
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 开始录音
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
mediaRecorder = new MediaRecorder(stream);
|
||||
const audioChunks = [];
|
||||
|
||||
mediaRecorder.addEventListener("dataavailable", event => {
|
||||
audioChunks.push(event.data);
|
||||
});
|
||||
|
||||
mediaRecorder.addEventListener("stop", async () => {
|
||||
const audioBlob = new Blob(audioChunks, { type: mediaRecorder.mimeType });
|
||||
const arrayBuffer = await audioBlob.arrayBuffer();
|
||||
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||
|
||||
// 检查音频数据是否为空
|
||||
if (arrayBuffer.byteLength === 0) {
|
||||
localAsrResult.textContent = '没有录制到音频,请重试。';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
|
||||
const rawAudio = audioBuffer.getChannelData(0);
|
||||
|
||||
localAsrResult.textContent = '正在识别...';
|
||||
const output = await recognizer(rawAudio);
|
||||
localAsrResult.textContent = output.text || '未能识别出任何内容。';
|
||||
} catch(e) {
|
||||
console.error('音频解码或识别失败:', e);
|
||||
localAsrResult.textContent = '处理音频时出错,请再试一次。';
|
||||
}
|
||||
});
|
||||
|
||||
mediaRecorder.start();
|
||||
isRecording = true;
|
||||
localMicButton.textContent = '正在录音... 点击停止';
|
||||
localMicButton.classList.add('recording');
|
||||
|
||||
} catch (error) {
|
||||
console.error('语音识别失败:', error);
|
||||
localAsrResult.textContent = '无法访问麦克风或识别出错。';
|
||||
isRecording = false; // 重置状态
|
||||
localMicButton.textContent = '开始本地识别';
|
||||
localMicButton.classList.remove('recording');
|
||||
}
|
||||
};
|
||||
|
||||
localMicButton.addEventListener('click', handleRecord);
|
||||
|
||||
|
||||
function analyzeAndReact(text) {
|
||||
let reaction = 'neutral'; // 默认为中性
|
||||
|
||||
if (positiveWords.some(word => text.includes(word))) {
|
||||
reaction = 'positive';
|
||||
} else if (negativeWords.some(word => text.includes(word))) {
|
||||
reaction = 'negative';
|
||||
}
|
||||
|
||||
if (reaction !== 'neutral') {
|
||||
switchVideoByEmotion(reaction);
|
||||
}
|
||||
}
|
||||
|
||||
function switchVideoByEmotion(emotion) {
|
||||
let nextVideoSrc;
|
||||
if (emotion === 'positive') {
|
||||
const randomIndex = Math.floor(Math.random() * positiveVideos.length);
|
||||
nextVideoSrc = positiveVideos[randomIndex];
|
||||
} else { // negative
|
||||
nextVideoSrc = negativeVideo;
|
||||
}
|
||||
|
||||
// 避免重复播放同一个视频
|
||||
const currentVideoSrc = activeVideo.querySelector('source').getAttribute('src');
|
||||
if (nextVideoSrc === currentVideoSrc) return;
|
||||
|
||||
// --- 以下逻辑与 switchVideo 函数类似,用于切换视频 ---
|
||||
inactiveVideo.querySelector('source').setAttribute('src', nextVideoSrc);
|
||||
inactiveVideo.load();
|
||||
|
||||
inactiveVideo.addEventListener('canplaythrough', function onCanPlayThrough() {
|
||||
inactiveVideo.removeEventListener('canplaythrough', onCanPlayThrough);
|
||||
activeVideo.pause(); // 暂停当前视频,防止其 'ended' 事件触发切换
|
||||
inactiveVideo.play().catch(error => console.error("Video play failed:", error));
|
||||
activeVideo.classList.remove('active');
|
||||
inactiveVideo.classList.add('active');
|
||||
[activeVideo, inactiveVideo] = [inactiveVideo, activeVideo];
|
||||
// 情感触发的视频播放结束后,回归随机播放
|
||||
activeVideo.addEventListener('ended', switchVideo, { once: true });
|
||||
}, { once: true });
|
||||
}
|
||||
|
||||
});
|
||||
Reference in New Issue
Block a user