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 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();
|
||||||
@ -121,32 +131,66 @@ 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: 'driving',
|
tag: 'walk',
|
||||||
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: 'tea',
|
tag: 'coffee',
|
||||||
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分钟,正好陪你唠会儿。不过我得小声,怕长皱纹!',
|
||||||
|
'好啦宝宝,面膜快干了,得去洗了。你先睡?... 嗯,梦里见呗。'
|
||||||
|
]
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -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 连接处理
|
// Socket.IO 连接处理
|
||||||
io.on('connection', (socket) => {
|
io.on('connection', (socket) => {
|
||||||
console.log('用户连接:', socket.id);
|
console.log('用户连接:', socket.id);
|
||||||
|
|||||||
@ -26,12 +26,13 @@ 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, "条消息");
|
console.log("历史消息初始化完成:", historyMessage.length, "条消息", historyMessage);
|
||||||
|
|
||||||
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;
|
||||||
@ -42,7 +43,7 @@ async function initializeHistoryMessage(recentCount = 5) {
|
|||||||
function getCurrentHistoryMessage() {
|
function getCurrentHistoryMessage() {
|
||||||
if (!isInitialized) {
|
if (!isInitialized) {
|
||||||
console.warn('历史消息未初始化,返回默认消息');
|
console.warn('历史消息未初始化,返回默认消息');
|
||||||
return [{ role: 'system', content: 'You are a helpful assistant.' }];
|
return [];
|
||||||
}
|
}
|
||||||
return [...historyMessage]; // 返回副本,避免外部修改
|
return [...historyMessage]; // 返回副本,避免外部修改
|
||||||
}
|
}
|
||||||
@ -60,16 +61,15 @@ 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 };
|
export { chatWithAudioStream, initializeHistoryMessage, getCurrentHistoryMessage, saveMessage, updateHistoryMessage };
|
||||||
|
|
||||||
// 处理音频播放队列
|
// 处理音频播放队列
|
||||||
async function processAudioQueue() {
|
async function processAudioQueue() {
|
||||||
|
|||||||
171
src/index.js
171
src/index.js
@ -1,7 +1,8 @@
|
|||||||
console.log('视频文件:');
|
console.log('视频文件:');
|
||||||
// WebRTC 音视频通话应用
|
// WebRTC 音视频通话应用
|
||||||
// import { chatWithAudioStream } from './chat_with_audio.js';
|
// 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';
|
import { AudioProcessor } from './audio_processor.js';
|
||||||
|
|
||||||
// 在应用初始化时调用
|
// 在应用初始化时调用
|
||||||
@ -74,6 +75,10 @@ class WebRTCChat {
|
|||||||
this.preloadVideoResources();
|
this.preloadVideoResources();
|
||||||
this.bindEvents();
|
this.bindEvents();
|
||||||
|
|
||||||
|
// 添加开场白相关属性
|
||||||
|
this.openingAudioData = null;
|
||||||
|
this.isOpeningAudioReady = false;
|
||||||
|
|
||||||
// 在初始化完成后预加载常用视频
|
// 在初始化完成后预加载常用视频
|
||||||
// setTimeout(() => {
|
// setTimeout(() => {
|
||||||
// this.logMessage('开始预加载常用视频...', 'info');
|
// this.logMessage('开始预加载常用视频...', 'info');
|
||||||
@ -227,12 +232,117 @@ 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');
|
||||||
@ -431,6 +541,7 @@ class WebRTCChat {
|
|||||||
|
|
||||||
// 预创建重要视频流
|
// 预创建重要视频流
|
||||||
async precreateImportantVideos() {
|
async precreateImportantVideos() {
|
||||||
|
|
||||||
if (this.isInitialized) return;
|
if (this.isInitialized) return;
|
||||||
|
|
||||||
console.log('开始预创建重要流...', 'info');
|
console.log('开始预创建重要流...', 'info');
|
||||||
@ -1151,6 +1262,9 @@ class WebRTCChat {
|
|||||||
// 切换到通话中图标
|
// 切换到通话中图标
|
||||||
this.switchToCallingIcon();
|
this.switchToCallingIcon();
|
||||||
|
|
||||||
|
// 在初始化完成后生成开场白音频
|
||||||
|
await this.initializeOpeningAudio();
|
||||||
|
|
||||||
// 现在才开始显示视频
|
// 现在才开始显示视频
|
||||||
await this.startDefaultVideoStream();
|
await this.startDefaultVideoStream();
|
||||||
|
|
||||||
@ -1163,35 +1277,54 @@ 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,5 +1,35 @@
|
|||||||
// 以流式方式请求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',
|
||||||
@ -54,7 +84,14 @@ 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;
|
||||||
}
|
}
|
||||||
@ -65,12 +102,15 @@ 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内容片段:', deltaContent);
|
console.log('【未过滤】LLM内容片段:', pendingText);
|
||||||
|
|
||||||
// 检查是否包含分段分隔符
|
// 先过滤旁白,再检查分段分隔符
|
||||||
if (segmentDelimiters.test(pendingText)) {
|
const filteredPendingText = filterNarration(pendingText);
|
||||||
// 按分隔符分割文本
|
|
||||||
const segments = pendingText.split(segmentDelimiters);
|
// 检查过滤后的文本是否包含分段分隔符
|
||||||
|
if (segmentDelimiters.test(filteredPendingText)) {
|
||||||
|
// 按分隔符分割已过滤的文本
|
||||||
|
const segments = filteredPendingText.split(segmentDelimiters);
|
||||||
|
|
||||||
// 重新组合处理:只处理足够长的完整段落
|
// 重新组合处理:只处理足够长的完整段落
|
||||||
let accumulatedText = '';
|
let accumulatedText = '';
|
||||||
@ -81,25 +121,30 @@ async function requestLLMStream({ apiKey, model, messages, onSegment }) {
|
|||||||
if (segment) {
|
if (segment) {
|
||||||
accumulatedText += segment;
|
accumulatedText += segment;
|
||||||
// 找到分隔符
|
// 找到分隔符
|
||||||
const delimiterMatch = pendingText.match(segmentDelimiters);
|
const delimiterMatch = filteredPendingText.match(segmentDelimiters);
|
||||||
if (delimiterMatch) {
|
if (delimiterMatch) {
|
||||||
accumulatedText += delimiterMatch[0];
|
accumulatedText += delimiterMatch[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果累积文本长度大于5个字,处理它
|
// 如果累积文本长度大于5个字,处理它
|
||||||
if (accumulatedText.length > 6 && onSegment) {
|
if (accumulatedText.length > 8 && 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
|
||||||
pendingText = accumulatedText + (segments[segments.length - 1] || '');
|
const processedLength = pendingText.length - (segments[segments.length - 1] || '').length;
|
||||||
|
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 };
|
export { requestMinimaxi, requestVolcanTTS, addAudioToQueue };
|
||||||
Loading…
x
Reference in New Issue
Block a user