Merge branch 'new_female' into kehu_female

This commit is contained in:
宋居成 2025-08-13 20:57:00 +08:00
commit cae92aac52
5 changed files with 317 additions and 64 deletions

View File

@ -8,7 +8,17 @@ const { MessageHistory } = require('./src/message_history.js');
const app = express();
const server = http.createServer(app);
const io = socketIo(server);
const io = socketIo(server, {
pingTimeout: 300000, // 60秒超时
pingInterval: 25000, // 25秒心跳间隔
upgradeTimeout: 30000, // 30秒升级超时
allowEIO3: true, // 允许Engine.IO v3客户端
transports: ['websocket', 'polling'], // 支持多种传输方式
cors: {
origin: "*",
methods: ["GET", "POST"]
}
});
// 创建消息历史管理器
const messageHistory = new MessageHistory();
@ -121,32 +131,66 @@ function saveSceneState() {
let currentSceneIndex = 0;
const scenes = [
{
name: '起床',
name: '起床-坐在床上',
defaultVideo: '8-5-qc-bd-female.mp4',
interactionVideo: '8-5-qc-sh-female.mp4',
tag: 'wakeup',
apiKey: 'bot-20250730213756-l627w' // 起床场景的API key
apiKey: 'bot-20250730213756-l627w', // 起床场景的API key
openingLines: [
'哈啊~ 头发是不是超乱?别笑话我哦... 不过,能看到你的脸,就算乱糟糟的我也好开心。',
"刚醒就看到你,比闹钟好使一百倍。",
// "昨晚梦里喊你名字喊醒了,现在视频看到你,心跳比梦里还烫。",
// "你穿我T恤的样子...让我想咬住衣领不放。",
"宝宝早安!偷偷告诉你...我昨晚梦见你啦,所以醒来第一件事就是找你~",
// "喉咙里有咖啡味...要不要来尝尝?记得上次你咬我锁骨时,咖啡还温着。",
"早,宝宝。吃早餐没?我还没... 好想有人送早餐来床上哦。"
]
},
{
name: '走路',
name: '走路去公司的路上',
defaultVideo: '8-5-sb-bd-female.mp4',
interactionVideo: '8-5-sb-sh-female.mp4',
tag: 'driving',
apiKey: 'bot-20250703161810-sgkrh' // 开车场景的API key
tag: 'walk',
apiKey: 'bot-20250703161810-sgkrh', // 开车场景的API key
openingLines: [
'宝宝,看到你那边阳光好好哦。我这边有点风,吹着挺舒服的,要是你在旁边牵着手就更好了。',
'宝宝,我快到公司啦。今天太阳真好,晒得人暖暖的... 你那边天气咋样?',
'宝宝~ 我刚出地铁站就看到你啦!吃早餐没?',
// '看到那个大钟楼没?每次路过都想你。想着要是能牵着你的手,站在最高层往下看该多好。',
'宝宝,我快到公司了,你到公司了吗?',
'宝宝,今天太阳真舒服!想起昨晚说的周末想去湖边散步,要是这天气保持住就好啦!',
]
},
{
name: '喝茶',
name: '在公司楼下喝咖啡',
defaultVideo: '8-8-hc-bd-2.mp4',
interactionVideo: '8-5-hc-sh-female.mp4',
tag: 'tea',
apiKey: 'bot-20250805140055-ccdr6' // 喝茶场景的API key
tag: 'coffee',
apiKey: 'bot-20250805140055-ccdr6', // 喝茶场景的API key
openingLines: [
'宝宝,今天这杯拿铁拉花好丑哦... 不过没关系,看着你的帅脸就平衡啦,你比咖啡提神!',
'嗯... 咖啡香香的,宝宝的声音也好好听。好想时间停在这一小会儿,就我们俩。',
'哇,今天换了个口味,燕麦拿铁!宝宝你肯定嫌没味道,就爱喝你那美式... 苦死了。',
'宝宝,刚才店员问我糖浆加多少,我脱口而出‘和我男朋友一样’,说完自己都脸红了...',
'对了宝宝,昨天说帮你找的资料,我存手机了,喝完这杯咖啡就发你哈!记得看。',
// '这杯好苦…但一看到你,就自动回甘了。比加十包糖都管用。你说你是不是我的专属甜味剂?'
]
},
{
name: '睡觉',
name: '敷面膜-准备睡觉',
defaultVideo: '8-8-sj-bd.mp4',
interactionVideo: '8-8-sj-sh.mp4',
tag: 'sleep',
apiKey: 'bot-20250808120020-jfkmk' // 吃饭场景的API key
apiKey: 'bot-20250808120020-jfkmk', // 睡觉场景的API key
openingLines: [
'宝宝~ 敷着面膜和你视频,感觉像在做双倍美容,心里也美美的。',
'嗯?宝宝打来啦... 刚躺下贴上面膜,你就来了,像算准时间一样,真贴心。',
'哈,宝宝,选了个清洁面膜,有点刺刺的。你用的啥洗面奶来着?忘了...',
'早...哦不,晚安宝宝!敷面膜呢,你看我像不像外星人?哈哈...嘶,不能笑!',
'宝宝,你那边也躺下了?我弄完面膜就睡。今天累不累?',
'宝宝... 这面膜说要敷15分钟正好陪你唠会儿。不过我得小声怕长皱纹',
'好啦宝宝,面膜快干了,得去洗了。你先睡?... 嗯,梦里见呗。'
]
}
];
@ -263,6 +307,37 @@ app.get('/api/default-video', (req, res) => {
});
});
// 在现有的API接口后添加
app.get('/api/current-scene/opening-line', (req, res) => {
try {
const currentScene = getCurrentScene();
if (currentScene && currentScene.openingLines && currentScene.openingLines.length > 0) {
// 随机选择一个开场白
const randomIndex = Math.floor(Math.random() * currentScene.openingLines.length);
const selectedOpeningLine = currentScene.openingLines[randomIndex];
res.json({
success: true,
openingLine: selectedOpeningLine,
sceneName: currentScene.name,
sceneTag: currentScene.tag
});
} else {
res.json({
success: false,
message: '当前场景没有配置开场白'
});
}
} catch (error) {
console.error('获取开场白失败:', error);
res.status(500).json({
success: false,
message: '获取开场白失败',
error: error.message
});
}
});
// Socket.IO 连接处理
io.on('connection', (socket) => {
console.log('用户连接:', socket.id);

View File

@ -26,12 +26,13 @@ async function initializeHistoryMessage(recentCount = 5) {
const data = await response.json();
historyMessage = data.messages || [];
isInitialized = true;
console.log("历史消息初始化完成:", historyMessage.length, "条消息");
console.log("历史消息初始化完成:", historyMessage.length, "条消息", historyMessage);
return historyMessage;
} catch (error) {
console.error('获取历史消息失败,使用默认格式:', error);
historyMessage = [
{ role: 'system', content: 'You are a helpful assistant.' }
// { role: 'system', content: 'You are a helpful assistant.' }
];
isInitialized = true;
return historyMessage;
@ -42,7 +43,7 @@ async function initializeHistoryMessage(recentCount = 5) {
function getCurrentHistoryMessage() {
if (!isInitialized) {
console.warn('历史消息未初始化,返回默认消息');
return [{ role: 'system', content: 'You are a helpful assistant.' }];
return [];
}
return [...historyMessage]; // 返回副本,避免外部修改
}
@ -60,16 +61,15 @@ function updateHistoryMessage(userInput, assistantResponse) {
);
// 可选:限制历史消息数量,保持最近的对话
// const maxMessages = 20; // 保留最近10轮对话20条消息
// if (historyMessage.length > maxMessages) {
// // 保留系统消息和最近的对话
// const systemMessages = historyMessage.filter(msg => msg.role === 'system');
// const recentMessages = historyMessage.slice(-maxMessages + systemMessages.length);
// historyMessage = [...systemMessages, ...recentMessages.filter(msg => msg.role !== 'system')];
// }
const maxMessages = 20; // 保留最近10轮对话20条消息
if (historyMessage.length > maxMessages) {
// 保留系统消息和最近的对话
const systemMessages = historyMessage.filter(msg => msg.role === 'system');
const recentMessages = historyMessage.slice(-maxMessages + systemMessages.length);
historyMessage = [...systemMessages, ...recentMessages.filter(msg => msg.role !== 'system')];
}
}
// 保存消息到服务端
// 保存消息到服务端
async function saveMessage(userInput, assistantResponse) {
try {
@ -197,7 +197,7 @@ async function chatWithAudioStream(userInput) {
}
// 导出初始化函数,供外部调用
export { chatWithAudioStream, initializeHistoryMessage, getCurrentHistoryMessage };
export { chatWithAudioStream, initializeHistoryMessage, getCurrentHistoryMessage, saveMessage, updateHistoryMessage };
// 处理音频播放队列
async function processAudioQueue() {
@ -322,4 +322,4 @@ async function playAudioStreamNode(audioHex) {
// export { chatWithAudioStream, playAudioStream, playAudioStreamNode, initializeHistoryMessage, getCurrentHistoryMessage };
// export { chatWithAudioStream, playAudioStream, playAudioStreamNode, initializeHistoryMessage, getCurrentHistoryMessage };

View File

@ -1,7 +1,8 @@
console.log('视频文件:');
// WebRTC 音视频通话应用
// import { chatWithAudioStream } from './chat_with_audio.js';
import { chatWithAudioStream, initializeHistoryMessage } from './chat_with_audio.js';
import { chatWithAudioStream, initializeHistoryMessage, updateHistoryMessage } from './chat_with_audio.js';
import { AudioProcessor } from './audio_processor.js';
// 在应用初始化时调用
@ -74,6 +75,10 @@ class WebRTCChat {
this.preloadVideoResources();
this.bindEvents();
// 添加开场白相关属性
this.openingAudioData = null;
this.isOpeningAudioReady = false;
// 在初始化完成后预加载常用视频
// setTimeout(() => {
// this.logMessage('开始预加载常用视频...', 'info');
@ -227,11 +232,116 @@ class WebRTCChat {
async initializeHistory() {
try {
await initializeHistoryMessage(100);
console.log('历史消息初始化完成');
} catch (error) {
console.error('历史消息初始化失败:', error);
}
}
// 新增方法:初始化开场白音频
async initializeOpeningAudio() {
try {
console.log('开始初始化开场白音频...');
// 获取当前场景的开场白
const response = await fetch('/api/current-scene/opening-line');
const data = await response.json();
if (data.success && data.openingLine) {
console.log(`获取到开场白: ${data.openingLine}`);
// 生成开场白音频
await this.generateOpeningAudio(data.openingLine);
this.logMessage(`开场白音频已准备就绪: ${data.openingLine}`, 'success');
} else {
console.warn('未获取到开场白:', data.message);
}
} catch (error) {
console.error('初始化开场白音频失败:', error);
this.logMessage(`开场白音频初始化失败: ${error.message}`, 'error');
}
}
// 新增方法:生成开场白音频
async generateOpeningAudio(text) {
try {
// 动态导入 minimaxi_stream 模块
const { requestMinimaxi } = await import('./minimaxi_stream.js');
const { getMinimaxiConfig, getAudioConfig, getLLMConfigByScene } = await import('./config.js');
const { saveMessage } = await import('./chat_with_audio.js');
const minimaxiConfig = getMinimaxiConfig();
const audioConfig = getAudioConfig();
const llmConfig = await getLLMConfigByScene();
const requestBody = {
model: audioConfig.model,
text: text,
voice_setting: audioConfig.voiceSetting,
audio_setting: audioConfig.audioSetting,
language_boost: 'auto',
output_format: 'hex'
};
console.log('开始生成开场白音频...');
// 生成音频数据
const audioHexData = await requestMinimaxi({
apiKey: minimaxiConfig.apiKey,
groupId: minimaxiConfig.groupId,
body: requestBody,
stream: false, // 非流式,一次性获取完整音频
textPlay: false
});
if (audioHexData && audioHexData.data && audioHexData.data.audio) {
this.openingAudioData = audioHexData.data.audio;
this.isOpeningAudioReady = true;
console.log('开场白音频生成成功');
}
// 先更新本地历史消息
updateHistoryMessage(`场景切换-${llmConfig.sceneName}`, text);
await saveMessage(`场景切换-${llmConfig.sceneName}`,text);
} catch (error) {
console.error('生成开场白音频失败:', error);
throw error;
}
}
// 新增方法:播放开场白音频
async playOpeningAudio() {
if (!this.isOpeningAudioReady || !this.openingAudioData) {
console.warn('开场白音频未准备就绪');
return;
}
try {
// 动态导入 addAudioToQueue 函数
const { addAudioToQueue } = await import('./minimaxi_stream.js');
console.log('将开场白音频添加到队列');
await addAudioToQueue(this.openingAudioData);
this.logMessage('开场白音频已开始播放', 'success');
} catch (error) {
console.error('播放开场白音频失败:', error);
this.logMessage(`播放开场白音频失败: ${error.message}`, 'error');
}
}
// 新增方法:获取开场白音频时长
getOpeningAudioDuration() {
// 估算开场白音频时长,可以根据实际情况调整
// 这里假设平均每个字符对应100ms的音频时长
if (this.openingAudioData) {
// 简单估算假设开场白大约3-5秒
return 4000; // 4秒
}
return 3000; // 默认3秒
}
async loadVideoMapping() {
try {
@ -431,6 +541,7 @@ class WebRTCChat {
// 预创建重要视频流
async precreateImportantVideos() {
if (this.isInitialized) return;
console.log('开始预创建重要流...', 'info');
@ -1150,6 +1261,9 @@ class WebRTCChat {
this.showConnectionWaiting();
// 切换到通话中图标
this.switchToCallingIcon();
// 在初始化完成后生成开场白音频
await this.initializeOpeningAudio();
// 现在才开始显示视频
await this.startDefaultVideoStream();
@ -1163,35 +1277,54 @@ class WebRTCChat {
console.log('麦克风权限获取成功');
await this.createPeerConnection();
await this.startVoiceRecording();
this.startButton.disabled = true;
this.startButton.style.opacity = '0.5'
this.stopButton.disabled = false;
this.startButton.style.opacity = '0.5'
this.stopButton.disabled = false;
// 隐藏头像,显示视频
if (this.videoContainer) {
// 隐藏头像,显示视频
if (this.videoContainer) {
this.videoContainer.classList.add('calling');
}
this.videoContainer.classList.add('calling');
}
// 显示结束通话按钮
this.stopButton.style.display = 'block';
this.updateAudioStatus('已连接', 'connected');
this.logMessage('音频通话已开始', 'success');
// 确保视频映射已加载
if (Object.keys(this.videoMapping).length === 0) {
await this.loadVideoMapping();
}
this.logMessage(`视频映射已加载: ${Object.keys(this.videoMapping).length} 个映射`, 'info');
// 通知服务器通话开始
this.socket.emit('call-started');
// 播放开场白,然后启动语音录制
if (this.isOpeningAudioReady) {
console.log('播放开场白音频...');
await this.playOpeningAudio();
// 显示结束通话按钮
this.stopButton.style.display = 'block';
this.updateAudioStatus('已连接', 'connected');
this.logMessage('音频通话已开始', 'success');
// 确保视频映射已加载
if (Object.keys(this.videoMapping).length === 0) {
await this.loadVideoMapping();
}
this.logMessage(`视频映射已加载: ${Object.keys(this.videoMapping).length} 个映射`, 'info');
// 通知服务器通话开始
this.socket.emit('call-started');
// 等待开场白播放完成后再启动语音录制
setTimeout(async () => {
console.log('开场白播放完成,启动语音录制...');
await this.startVoiceRecording();
this.logMessage('语音录制已启动,可以开始对话', 'success');
}, this.getOpeningAudioDuration() + 1000); // 开场白时长 + 1秒缓冲
} else {
console.warn('开场白音频尚未准备就绪,延迟启动语音录制');
// 如果没有开场白延迟500ms后启动录制
setTimeout(async () => {
await this.startVoiceRecording();
this.logMessage('语音录制已启动,可以开始对话', 'success');
}, 500);
}
// 开始播放当前场景的默认视频
// await this.precreateImportantVideos();

View File

@ -1,5 +1,35 @@
// 以流式方式请求LLM大模型接口并打印流式返回内容
// 过滤旁白内容的函数
function filterNarration(text) {
if (!text) return text;
// 匹配各种括号内的旁白内容
// 包括:()、【】、[]、{}、〈〉、《》等
const narrationPatterns = [
/[^]*/g, // 中文圆括号
/\([^)]*\)/g, // 英文圆括号
/【[^】]*】/g, // 中文方括号
/\[[^\]]*\]/g, // 英文方括号
/\{[^}]*\}/g, // 花括号
/〈[^〉]*〉/g, // 中文尖括号
/《[^》]*》/g, // 中文书名号
/<[^>]*>/g // 英文尖括号
];
let filteredText = text;
// 逐个应用过滤规则
narrationPatterns.forEach(pattern => {
filteredText = filteredText.replace(pattern, '');
});
// 清理多余的空格和换行
filteredText = filteredText.replace(/\s+/g, ' ').trim();
return filteredText;
}
async function requestLLMStream({ apiKey, model, messages, onSegment }) {
const response = await fetch('https://ark.cn-beijing.volces.com/api/v3/bots/chat/completions', {
method: 'POST',
@ -54,7 +84,14 @@ async function requestLLMStream({ apiKey, model, messages, onSegment }) {
// 处理最后的待处理文本无论长度是否大于5个字
if (pendingText.trim() && onSegment) {
console.log('处理最后的待处理文本:', pendingText.trim());
await onSegment(pendingText.trim(), true);
// 过滤旁白内容
const filteredText = filterNarration(pendingText.trim());
if (filteredText.trim()) {
console.log('过滤旁白后的最后文本:', filteredText);
await onSegment(filteredText, true);
} else {
console.log('最后的文本被完全过滤,跳过');
}
}
continue;
}
@ -65,12 +102,15 @@ async function requestLLMStream({ apiKey, model, messages, onSegment }) {
const deltaContent = obj.choices[0].delta.content;
content += deltaContent;
pendingText += deltaContent;
console.log('LLM内容片段:', deltaContent);
console.log('【未过滤】LLM内容片段:', pendingText);
// 检查是否包含分段分隔符
if (segmentDelimiters.test(pendingText)) {
// 按分隔符分割文本
const segments = pendingText.split(segmentDelimiters);
// 先过滤旁白,再检查分段分隔符
const filteredPendingText = filterNarration(pendingText);
// 检查过滤后的文本是否包含分段分隔符
if (segmentDelimiters.test(filteredPendingText)) {
// 按分隔符分割已过滤的文本
const segments = filteredPendingText.split(segmentDelimiters);
// 重新组合处理:只处理足够长的完整段落
let accumulatedText = '';
@ -81,25 +121,30 @@ async function requestLLMStream({ apiKey, model, messages, onSegment }) {
if (segment) {
accumulatedText += segment;
// 找到分隔符
const delimiterMatch = pendingText.match(segmentDelimiters);
const delimiterMatch = filteredPendingText.match(segmentDelimiters);
if (delimiterMatch) {
accumulatedText += delimiterMatch[0];
}
// 如果累积文本长度大于5个字处理它
if (accumulatedText.length > 6 && onSegment) {
console.log('检测到完整段落:', accumulatedText);
await onSegment(accumulatedText, false);
if (accumulatedText.length > 8 && onSegment) {
console.log('【已过滤】检测到完整段落:', accumulatedText);
// 文本已经过滤过旁白,直接使用
if (accumulatedText.trim()) {
console.log('处理过滤后的文本:', accumulatedText);
await onSegment(accumulatedText, false);
}
hasProcessed = true;
accumulatedText = ''; // 重置
}
}
}
// 更新pendingText
// 更新pendingText - 使用原始文本但需要相应调整
if (hasProcessed) {
// 保留未处理的累积文本和最后一个不完整段落
pendingText = accumulatedText + (segments[segments.length - 1] || '');
// 计算已处理的原始文本长度更新pendingText
const processedLength = pendingText.length - (segments[segments.length - 1] || '').length;
pendingText = pendingText.substring(processedLength);
}
}
}

View File

@ -96,7 +96,7 @@ async function processAudioQueue() {
// await new Promise(resolve => setTimeout(resolve, 300));
const text = 'default'
console.log("音频结束------------------------", window.webrtcApp.currentVideoTag, isPlaying)
console.log("音频结束------------------------", window.webrtcApp.currentVideoTag, isPlaying)
if (window.webrtcApp.currentVideoTag != text && !isPlaying) {
isFirstChunk = true
window.webrtcApp.currentVideoTag = text
@ -431,4 +431,4 @@ function generateUUID() {
});
}
export { requestMinimaxi, requestVolcanTTS };
export { requestMinimaxi, requestVolcanTTS, addAudioToQueue };