Compare commits
	
		
			No commits in common. "75c2bc7fd1e0cf83a90e37e60d434000c84425ba" and "e4fed25f954376fdefeaa89ad2421e573d974077" have entirely different histories.
		
	
	
		
			75c2bc7fd1
			...
			e4fed25f95
		
	
		
							
								
								
									
										97
									
								
								server.js
									
									
									
									
									
								
							
							
						
						
									
										97
									
								
								server.js
									
									
									
									
									
								
							@ -8,17 +8,7 @@ const { MessageHistory } = require('./src/message_history.js');
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
const app = express();
 | 
					const app = express();
 | 
				
			||||||
const server = http.createServer(app);
 | 
					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();
 | 
					const messageHistory = new MessageHistory();
 | 
				
			||||||
@ -131,66 +121,32 @@ function saveSceneState() {
 | 
				
			|||||||
let currentSceneIndex = 0;
 | 
					let currentSceneIndex = 0;
 | 
				
			||||||
const scenes = [
 | 
					const scenes = [
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    name: '起床-坐在床上',
 | 
					    name: '起床',
 | 
				
			||||||
    defaultVideo: '8-5-qc-bd-female.mp4',
 | 
					    defaultVideo: '8-5-qc-bd-female.mp4',
 | 
				
			||||||
    interactionVideo: '8-5-qc-sh-female.mp4',
 | 
					    interactionVideo: '8-5-qc-sh-female.mp4',
 | 
				
			||||||
    tag: 'wakeup',
 | 
					    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',
 | 
					    defaultVideo: '8-5-sb-bd-female.mp4',
 | 
				
			||||||
    interactionVideo: '8-5-sb-sh-female.mp4',
 | 
					    interactionVideo: '8-5-sb-sh-female.mp4',
 | 
				
			||||||
    tag: 'walk',
 | 
					    tag: 'driving',
 | 
				
			||||||
    apiKey: 'bot-20250703161810-sgkrh', // 开车场景的API key
 | 
					    apiKey: 'bot-20250703161810-sgkrh' // 开车场景的API key
 | 
				
			||||||
    openingLines: [
 | 
					 | 
				
			||||||
      '宝宝,看到你那边阳光好好哦。我这边有点风,吹着挺舒服的,要是你在旁边牵着手就更好了。',
 | 
					 | 
				
			||||||
      '宝宝,我快到公司啦。今天太阳真好,晒得人暖暖的... 你那边天气咋样?',
 | 
					 | 
				
			||||||
      '宝宝~ 我刚出地铁站就看到你啦!吃早餐没?',
 | 
					 | 
				
			||||||
      // '看到那个大钟楼没?每次路过都想你。想着要是能牵着你的手,站在最高层往下看该多好。',
 | 
					 | 
				
			||||||
      '宝宝,我快到公司了,你到公司了吗?',
 | 
					 | 
				
			||||||
      '宝宝,今天太阳真舒服!想起昨晚说的周末想去湖边散步,要是这天气保持住就好啦!',
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    name: '在公司楼下喝咖啡',
 | 
					    name: '喝茶',
 | 
				
			||||||
    defaultVideo: '8-8-hc-bd-2.mp4',
 | 
					    defaultVideo: '8-8-hc-bd-2.mp4',
 | 
				
			||||||
    interactionVideo: '8-5-hc-sh-female.mp4',
 | 
					    interactionVideo: '8-5-hc-sh-female.mp4',
 | 
				
			||||||
    tag: 'coffee',
 | 
					    tag: 'tea',
 | 
				
			||||||
    apiKey: 'bot-20250805140055-ccdr6', // 喝茶场景的API key
 | 
					    apiKey: 'bot-20250805140055-ccdr6' // 喝茶场景的API key
 | 
				
			||||||
    openingLines: [
 | 
					 | 
				
			||||||
      '宝宝,今天这杯拿铁拉花好丑哦... 不过没关系,看着你的帅脸就平衡啦,你比咖啡提神!',
 | 
					 | 
				
			||||||
      '嗯... 咖啡香香的,宝宝的声音也好好听。好想时间停在这一小会儿,就我们俩。',
 | 
					 | 
				
			||||||
      '哇,今天换了个口味,燕麦拿铁!宝宝你肯定嫌没味道,就爱喝你那美式... 苦死了。',
 | 
					 | 
				
			||||||
      '宝宝,刚才店员问我糖浆加多少,我脱口而出‘和我男朋友一样’,说完自己都脸红了...',
 | 
					 | 
				
			||||||
      '对了宝宝,昨天说帮你找的资料,我存手机了,喝完这杯咖啡就发你哈!记得看。',
 | 
					 | 
				
			||||||
      // '这杯好苦…但一看到你,就自动回甘了。比加十包糖都管用。你说你是不是我的专属甜味剂?'
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    name: '敷面膜-准备睡觉',
 | 
					    name: '睡觉',
 | 
				
			||||||
    defaultVideo: '8-8-sj-bd.mp4',
 | 
					    defaultVideo: '8-8-sj-bd.mp4',
 | 
				
			||||||
    interactionVideo: '8-8-sj-sh.mp4',
 | 
					    interactionVideo: '8-8-sj-sh.mp4',
 | 
				
			||||||
    tag: 'sleep',
 | 
					    tag: 'sleep',
 | 
				
			||||||
    apiKey: 'bot-20250808120020-jfkmk', // 睡觉场景的API key
 | 
					    apiKey: 'bot-20250808120020-jfkmk' // 吃饭场景的API key
 | 
				
			||||||
    openingLines: [
 | 
					 | 
				
			||||||
      '宝宝~ 敷着面膜和你视频,感觉像在做双倍美容,心里也美美的。',
 | 
					 | 
				
			||||||
      '嗯?宝宝打来啦... 刚躺下贴上面膜,你就来了,像算准时间一样,真贴心。',
 | 
					 | 
				
			||||||
      '哈,宝宝,选了个清洁面膜,有点刺刺的。你用的啥洗面奶来着?忘了...',
 | 
					 | 
				
			||||||
      '早...哦不,晚安宝宝!敷面膜呢,你看我像不像外星人?哈哈...,不许笑我!',
 | 
					 | 
				
			||||||
      '宝宝,你那边也躺下了?我弄完面膜就睡。今天累不累?',
 | 
					 | 
				
			||||||
      '宝宝... 这面膜说要敷15分钟,正好陪你唠会儿。不过我得小声,怕长皱纹!',
 | 
					 | 
				
			||||||
      '好啦宝宝,面膜快干了,得去洗了。你先睡?... 嗯,梦里见呗。'
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
];
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -307,37 +263,6 @@ 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 连接处理
 | 
					// Socket.IO 连接处理
 | 
				
			||||||
io.on('connection', (socket) => {
 | 
					io.on('connection', (socket) => {
 | 
				
			||||||
  console.log('用户连接:', socket.id);
 | 
					  console.log('用户连接:', socket.id);
 | 
				
			||||||
 | 
				
			|||||||
@ -26,13 +26,12 @@ async function initializeHistoryMessage(recentCount = 5) {
 | 
				
			|||||||
        const data = await response.json();
 | 
					        const data = await response.json();
 | 
				
			||||||
        historyMessage = data.messages || [];
 | 
					        historyMessage = data.messages || [];
 | 
				
			||||||
        isInitialized = true;
 | 
					        isInitialized = true;
 | 
				
			||||||
        console.log("历史消息初始化完成:", historyMessage.length, "条消息", historyMessage);
 | 
					        console.log("历史消息初始化完成:", historyMessage.length, "条消息");
 | 
				
			||||||
 | 
					 | 
				
			||||||
        return historyMessage;
 | 
					        return historyMessage;
 | 
				
			||||||
    } catch (error) {
 | 
					    } catch (error) {
 | 
				
			||||||
        console.error('获取历史消息失败,使用默认格式:', error);
 | 
					        console.error('获取历史消息失败,使用默认格式:', error);
 | 
				
			||||||
        historyMessage = [
 | 
					        historyMessage = [
 | 
				
			||||||
            // { role: 'system', content: 'You are a helpful assistant.' }
 | 
					            { role: 'system', content: 'You are a helpful assistant.' }
 | 
				
			||||||
        ];
 | 
					        ];
 | 
				
			||||||
        isInitialized = true;
 | 
					        isInitialized = true;
 | 
				
			||||||
        return historyMessage;
 | 
					        return historyMessage;
 | 
				
			||||||
@ -43,7 +42,7 @@ async function initializeHistoryMessage(recentCount = 5) {
 | 
				
			|||||||
function getCurrentHistoryMessage() {
 | 
					function getCurrentHistoryMessage() {
 | 
				
			||||||
    if (!isInitialized) {
 | 
					    if (!isInitialized) {
 | 
				
			||||||
        console.warn('历史消息未初始化,返回默认消息');
 | 
					        console.warn('历史消息未初始化,返回默认消息');
 | 
				
			||||||
        return [];
 | 
					        return [{ role: 'system', content: 'You are a helpful assistant.' }];
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return [...historyMessage]; // 返回副本,避免外部修改
 | 
					    return [...historyMessage]; // 返回副本,避免外部修改
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -61,15 +60,16 @@ function updateHistoryMessage(userInput, assistantResponse) {
 | 
				
			|||||||
    );
 | 
					    );
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    // 可选:限制历史消息数量,保持最近的对话
 | 
					    // 可选:限制历史消息数量,保持最近的对话
 | 
				
			||||||
    const maxMessages = 20; // 保留最近10轮对话(20条消息)
 | 
					    // const maxMessages = 20; // 保留最近10轮对话(20条消息)
 | 
				
			||||||
    if (historyMessage.length > maxMessages) {
 | 
					    // if (historyMessage.length > maxMessages) {
 | 
				
			||||||
        // 保留系统消息和最近的对话
 | 
					    //     // 保留系统消息和最近的对话
 | 
				
			||||||
        const systemMessages = historyMessage.filter(msg => msg.role === 'system');
 | 
					    //     const systemMessages = historyMessage.filter(msg => msg.role === 'system');
 | 
				
			||||||
        const recentMessages = historyMessage.slice(-maxMessages + systemMessages.length);
 | 
					    //     const recentMessages = historyMessage.slice(-maxMessages + systemMessages.length);
 | 
				
			||||||
        historyMessage = [...systemMessages, ...recentMessages.filter(msg => msg.role !== 'system')];
 | 
					    //     historyMessage = [...systemMessages, ...recentMessages.filter(msg => msg.role !== 'system')];
 | 
				
			||||||
    }
 | 
					    // }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 保存消息到服务端
 | 
				
			||||||
// 保存消息到服务端
 | 
					// 保存消息到服务端
 | 
				
			||||||
async function saveMessage(userInput, assistantResponse) {
 | 
					async function saveMessage(userInput, assistantResponse) {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
@ -197,7 +197,7 @@ async function chatWithAudioStream(userInput) {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 导出初始化函数,供外部调用
 | 
					// 导出初始化函数,供外部调用
 | 
				
			||||||
export { chatWithAudioStream, initializeHistoryMessage, getCurrentHistoryMessage, saveMessage, updateHistoryMessage };
 | 
					export { chatWithAudioStream, initializeHistoryMessage, getCurrentHistoryMessage };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 处理音频播放队列
 | 
					// 处理音频播放队列
 | 
				
			||||||
async function processAudioQueue() {
 | 
					async function processAudioQueue() {
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										171
									
								
								src/index.js
									
									
									
									
									
								
							
							
						
						
									
										171
									
								
								src/index.js
									
									
									
									
									
								
							@ -1,8 +1,7 @@
 | 
				
			|||||||
console.log('视频文件:');
 | 
					console.log('视频文件:');
 | 
				
			||||||
// WebRTC 音视频通话应用
 | 
					// WebRTC 音视频通话应用
 | 
				
			||||||
// import { chatWithAudioStream } from './chat_with_audio.js';
 | 
					// import { chatWithAudioStream } from './chat_with_audio.js';
 | 
				
			||||||
import { chatWithAudioStream, initializeHistoryMessage, updateHistoryMessage } from './chat_with_audio.js';
 | 
					import { chatWithAudioStream, initializeHistoryMessage } from './chat_with_audio.js';
 | 
				
			||||||
 | 
					 | 
				
			||||||
import { AudioProcessor } from './audio_processor.js';
 | 
					import { AudioProcessor } from './audio_processor.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 在应用初始化时调用
 | 
					// 在应用初始化时调用
 | 
				
			||||||
@ -75,10 +74,6 @@ class WebRTCChat {
 | 
				
			|||||||
        this.preloadVideoResources();
 | 
					        this.preloadVideoResources();
 | 
				
			||||||
        this.bindEvents();
 | 
					        this.bindEvents();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // 添加开场白相关属性
 | 
					 | 
				
			||||||
        this.openingAudioData = null;
 | 
					 | 
				
			||||||
        this.isOpeningAudioReady = false;
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        // 在初始化完成后预加载常用视频
 | 
					        // 在初始化完成后预加载常用视频
 | 
				
			||||||
        // setTimeout(() => {
 | 
					        // setTimeout(() => {
 | 
				
			||||||
        //     this.logMessage('开始预加载常用视频...', 'info');
 | 
					        //     this.logMessage('开始预加载常用视频...', 'info');
 | 
				
			||||||
@ -232,117 +227,12 @@ class WebRTCChat {
 | 
				
			|||||||
    async initializeHistory() {
 | 
					    async initializeHistory() {
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
            await initializeHistoryMessage(100);
 | 
					            await initializeHistoryMessage(100);
 | 
				
			||||||
 | 
					 | 
				
			||||||
            console.log('历史消息初始化完成');
 | 
					            console.log('历史消息初始化完成');
 | 
				
			||||||
        } catch (error) {
 | 
					        } catch (error) {
 | 
				
			||||||
            console.error('历史消息初始化失败:', 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() {
 | 
					    async loadVideoMapping() {
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
            const response = await fetch('/api/video-mapping');
 | 
					            const response = await fetch('/api/video-mapping');
 | 
				
			||||||
@ -541,7 +431,6 @@ class WebRTCChat {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    // 预创建重要视频流
 | 
					    // 预创建重要视频流
 | 
				
			||||||
    async precreateImportantVideos() {
 | 
					    async precreateImportantVideos() {
 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (this.isInitialized) return;
 | 
					        if (this.isInitialized) return;
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        console.log('开始预创建重要流...', 'info');
 | 
					        console.log('开始预创建重要流...', 'info');
 | 
				
			||||||
@ -1262,9 +1151,6 @@ class WebRTCChat {
 | 
				
			|||||||
            // 切换到通话中图标
 | 
					            // 切换到通话中图标
 | 
				
			||||||
            this.switchToCallingIcon();
 | 
					            this.switchToCallingIcon();
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
            // 在初始化完成后生成开场白音频
 | 
					 | 
				
			||||||
            await this.initializeOpeningAudio();
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
            // 现在才开始显示视频
 | 
					            // 现在才开始显示视频
 | 
				
			||||||
            await this.startDefaultVideoStream();
 | 
					            await this.startDefaultVideoStream();
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
@ -1277,54 +1163,35 @@ class WebRTCChat {
 | 
				
			|||||||
            console.log('麦克风权限获取成功');
 | 
					            console.log('麦克风权限获取成功');
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
            await this.createPeerConnection();
 | 
					            await this.createPeerConnection();
 | 
				
			||||||
 | 
					            await this.startVoiceRecording();
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
            this.startButton.disabled = true;
 | 
					            this.startButton.disabled = true;
 | 
				
			||||||
        this.startButton.style.opacity = '0.5'
 | 
					            this.startButton.style.opacity = '0.5'
 | 
				
			||||||
        this.stopButton.disabled = false;
 | 
					            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.stopButton.style.display = 'block';
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
        this.updateAudioStatus('已连接', 'connected');
 | 
					            this.updateAudioStatus('已连接', 'connected');
 | 
				
			||||||
        this.logMessage('音频通话已开始', 'success');
 | 
					            this.logMessage('音频通话已开始', 'success');
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
        // 确保视频映射已加载
 | 
					            // 确保视频映射已加载
 | 
				
			||||||
        if (Object.keys(this.videoMapping).length === 0) {
 | 
					            if (Object.keys(this.videoMapping).length === 0) {
 | 
				
			||||||
            await this.loadVideoMapping();
 | 
					                await this.loadVideoMapping();
 | 
				
			||||||
        }
 | 
					            }
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
        this.logMessage(`视频映射已加载: ${Object.keys(this.videoMapping).length} 个映射`, 'info');
 | 
					            this.logMessage(`视频映射已加载: ${Object.keys(this.videoMapping).length} 个映射`, 'info');
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
        // 通知服务器通话开始
 | 
					            // 通知服务器通话开始
 | 
				
			||||||
        this.socket.emit('call-started');
 | 
					            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();
 | 
					            // await this.precreateImportantVideos();
 | 
				
			||||||
 | 
				
			|||||||
@ -1,35 +1,5 @@
 | 
				
			|||||||
// 以流式方式请求LLM大模型接口,并打印流式返回内容
 | 
					// 以流式方式请求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 }) {
 | 
					async function requestLLMStream({ apiKey, model, messages, onSegment }) {
 | 
				
			||||||
  const response = await fetch('https://ark.cn-beijing.volces.com/api/v3/bots/chat/completions', {
 | 
					  const response = await fetch('https://ark.cn-beijing.volces.com/api/v3/bots/chat/completions', {
 | 
				
			||||||
    method: 'POST',
 | 
					    method: 'POST',
 | 
				
			||||||
@ -84,14 +54,7 @@ async function requestLLMStream({ apiKey, model, messages, onSegment }) {
 | 
				
			|||||||
            // 处理最后的待处理文本(无论长度是否大于5个字)
 | 
					            // 处理最后的待处理文本(无论长度是否大于5个字)
 | 
				
			||||||
            if (pendingText.trim() && onSegment) {
 | 
					            if (pendingText.trim() && onSegment) {
 | 
				
			||||||
              console.log('处理最后的待处理文本:', pendingText.trim());
 | 
					              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;
 | 
					            continue;
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
@ -102,15 +65,12 @@ async function requestLLMStream({ apiKey, model, messages, onSegment }) {
 | 
				
			|||||||
              const deltaContent = obj.choices[0].delta.content;
 | 
					              const deltaContent = obj.choices[0].delta.content;
 | 
				
			||||||
              content += deltaContent;
 | 
					              content += deltaContent;
 | 
				
			||||||
              pendingText += deltaContent;
 | 
					              pendingText += deltaContent;
 | 
				
			||||||
              console.log('【未过滤】LLM内容片段:', pendingText);
 | 
					              console.log('LLM内容片段:', deltaContent);
 | 
				
			||||||
              
 | 
					              
 | 
				
			||||||
              // 先过滤旁白,再检查分段分隔符
 | 
					              // 检查是否包含分段分隔符
 | 
				
			||||||
              const filteredPendingText = filterNarration(pendingText);
 | 
					              if (segmentDelimiters.test(pendingText)) {
 | 
				
			||||||
              
 | 
					                // 按分隔符分割文本
 | 
				
			||||||
              // 检查过滤后的文本是否包含分段分隔符
 | 
					                const segments = pendingText.split(segmentDelimiters);
 | 
				
			||||||
              if (segmentDelimiters.test(filteredPendingText)) {
 | 
					 | 
				
			||||||
                // 按分隔符分割已过滤的文本
 | 
					 | 
				
			||||||
                const segments = filteredPendingText.split(segmentDelimiters);
 | 
					 | 
				
			||||||
                
 | 
					                
 | 
				
			||||||
                // 重新组合处理:只处理足够长的完整段落
 | 
					                // 重新组合处理:只处理足够长的完整段落
 | 
				
			||||||
                let accumulatedText = '';
 | 
					                let accumulatedText = '';
 | 
				
			||||||
@ -121,30 +81,25 @@ async function requestLLMStream({ apiKey, model, messages, onSegment }) {
 | 
				
			|||||||
                  if (segment) {
 | 
					                  if (segment) {
 | 
				
			||||||
                    accumulatedText += segment;
 | 
					                    accumulatedText += segment;
 | 
				
			||||||
                    // 找到分隔符
 | 
					                    // 找到分隔符
 | 
				
			||||||
                    const delimiterMatch = filteredPendingText.match(segmentDelimiters);
 | 
					                    const delimiterMatch = pendingText.match(segmentDelimiters);
 | 
				
			||||||
                    if (delimiterMatch) {
 | 
					                    if (delimiterMatch) {
 | 
				
			||||||
                      accumulatedText += delimiterMatch[0];
 | 
					                      accumulatedText += delimiterMatch[0];
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                    
 | 
					                    
 | 
				
			||||||
                    // 如果累积文本长度大于5个字,处理它
 | 
					                    // 如果累积文本长度大于5个字,处理它
 | 
				
			||||||
                    if (accumulatedText.length > 8 && onSegment) {
 | 
					                    if (accumulatedText.length > 6 && onSegment) {
 | 
				
			||||||
                      console.log('【已过滤】检测到完整段落:', accumulatedText);
 | 
					                      console.log('检测到完整段落:', accumulatedText);
 | 
				
			||||||
                      // 文本已经过滤过旁白,直接使用
 | 
					                      await onSegment(accumulatedText, false);
 | 
				
			||||||
                      if (accumulatedText.trim()) {
 | 
					 | 
				
			||||||
                        console.log('处理过滤后的文本:', accumulatedText);
 | 
					 | 
				
			||||||
                        await onSegment(accumulatedText, false);
 | 
					 | 
				
			||||||
                      }
 | 
					 | 
				
			||||||
                      hasProcessed = true;
 | 
					                      hasProcessed = true;
 | 
				
			||||||
                      accumulatedText = ''; // 重置
 | 
					                      accumulatedText = ''; // 重置
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                  }
 | 
					                  }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                
 | 
					                
 | 
				
			||||||
                // 更新pendingText - 使用原始文本但需要相应调整
 | 
					                // 更新pendingText
 | 
				
			||||||
                if (hasProcessed) {
 | 
					                if (hasProcessed) {
 | 
				
			||||||
                  // 计算已处理的原始文本长度,更新pendingText
 | 
					                  // 保留未处理的累积文本和最后一个不完整段落
 | 
				
			||||||
                  const processedLength = pendingText.length - (segments[segments.length - 1] || '').length;
 | 
					                  pendingText = accumulatedText + (segments[segments.length - 1] || '');
 | 
				
			||||||
                  pendingText = pendingText.substring(processedLength);
 | 
					 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
              }
 | 
					              }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
				
			|||||||
@ -96,7 +96,7 @@ async function processAudioQueue() {
 | 
				
			|||||||
  // await new Promise(resolve => setTimeout(resolve, 300));
 | 
					  // await new Promise(resolve => setTimeout(resolve, 300));
 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
  const text = 'default'
 | 
					  const text = 'default'
 | 
				
			||||||
  console.log("音频结束------------------------:", window.webrtcApp.currentVideoTag, isPlaying)
 | 
						  console.log("音频结束------------------------:", window.webrtcApp.currentVideoTag, isPlaying)
 | 
				
			||||||
  if (window.webrtcApp.currentVideoTag != text && !isPlaying) {
 | 
					  if (window.webrtcApp.currentVideoTag != text && !isPlaying) {
 | 
				
			||||||
    isFirstChunk = true
 | 
					    isFirstChunk = true
 | 
				
			||||||
    window.webrtcApp.currentVideoTag = text
 | 
					    window.webrtcApp.currentVideoTag = text
 | 
				
			||||||
@ -431,4 +431,4 @@ function generateUUID() {
 | 
				
			|||||||
  });
 | 
					  });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export { requestMinimaxi, requestVolcanTTS, addAudioToQueue };
 | 
					export { requestMinimaxi, requestVolcanTTS };
 | 
				
			||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user