Merge branch 'new_female' into kehu_female
This commit is contained in:
commit
cae92aac52
97
server.js
97
server.js
@ -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);
|
||||
|
||||
@ -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() {
|
||||
|
||||
137
src/index.js
137
src/index.js
@ -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,12 +232,117 @@ 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 {
|
||||
const response = await fetch('/api/video-mapping');
|
||||
@ -431,6 +541,7 @@ class WebRTCChat {
|
||||
|
||||
// 预创建重要视频流
|
||||
async precreateImportantVideos() {
|
||||
|
||||
if (this.isInitialized) return;
|
||||
|
||||
console.log('开始预创建重要流...', 'info');
|
||||
@ -1151,6 +1262,9 @@ class WebRTCChat {
|
||||
// 切换到通话中图标
|
||||
this.switchToCallingIcon();
|
||||
|
||||
// 在初始化完成后生成开场白音频
|
||||
await this.initializeOpeningAudio();
|
||||
|
||||
// 现在才开始显示视频
|
||||
await this.startDefaultVideoStream();
|
||||
|
||||
@ -1163,7 +1277,6 @@ class WebRTCChat {
|
||||
console.log('麦克风权限获取成功');
|
||||
|
||||
await this.createPeerConnection();
|
||||
await this.startVoiceRecording();
|
||||
|
||||
this.startButton.disabled = true;
|
||||
this.startButton.style.opacity = '0.5'
|
||||
@ -1193,6 +1306,26 @@ class WebRTCChat {
|
||||
// 通知服务器通话开始
|
||||
this.socket.emit('call-started');
|
||||
|
||||
// 播放开场白,然后启动语音录制
|
||||
if (this.isOpeningAudioReady) {
|
||||
console.log('播放开场白音频...');
|
||||
await this.playOpeningAudio();
|
||||
|
||||
// 等待开场白播放完成后再启动语音录制
|
||||
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();
|
||||
|
||||
|
||||
@ -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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -431,4 +431,4 @@ function generateUUID() {
|
||||
});
|
||||
}
|
||||
|
||||
export { requestMinimaxi, requestVolcanTTS };
|
||||
export { requestMinimaxi, requestVolcanTTS, addAudioToQueue };
|
||||
Loading…
x
Reference in New Issue
Block a user