// 以流式或非流式方式请求 minimaxi 大模型接口,并打印/返回内容 // import { text } from "express"; // 在文件顶部添加音频播放相关的变量和函数 let audioContext = null; let audioQueue = []; // 音频队列 let isPlaying = false; let isProcessingQueue = false; // 队列处理状态 let nextStartTime = 0; // 添加这行来声明 nextStartTime 变量 // 初始化音频上下文 function initAudioContext() { if (!audioContext) { audioContext = new (window.AudioContext || window.webkitAudioContext)(); } return audioContext; } // 将hex字符串转换为ArrayBuffer function hexToArrayBuffer(hex) { const bytes = new Uint8Array(hex.length / 2); for (let i = 0; i < hex.length; i += 2) { bytes[i / 2] = parseInt(hex.substr(i, 2), 16); } return bytes.buffer; } // 将音频添加到队列(不等待播放) async function addAudioToQueue(audioHex) { if (!audioHex || audioHex.length === 0) return; try { const ctx = initAudioContext(); const audioBuffer = hexToArrayBuffer(audioHex); const audioData = await ctx.decodeAudioData(audioBuffer); // 将解码后的音频数据添加到队列 audioQueue.push({ audioData, timestamp: Date.now() }); console.log(`音频已添加到队列,队列长度: ${audioQueue.length}`); // 启动队列处理器(如果还没有运行) if (!isProcessingQueue) { processAudioQueue(); } } catch (error) { console.error('音频解码失败:', error); } } let isFirstChunk = true; // 队列处理器 - 独立运行,按顺序播放音频 async function processAudioQueue() { if (isProcessingQueue) return; isProcessingQueue = true; while (audioQueue.length > 0 && !isPlaying) { console.log('开始处理音频队列'); // 如果当前没有音频在播放,且队列中有音频 if (!isPlaying && audioQueue.length > 0) { const audioItem = audioQueue.shift(); const sayName = '8-4-sh' const targetVideo = window.webrtcApp.interactionVideo // 如果是第一个音频片段,触发视频切换 if (isFirstChunk && sayName != window.webrtcApp.currentVideoTag && window.webrtcApp && window.webrtcApp.switchVideoStream) { try { console.log('--------------触发视频切换:', sayName); window.webrtcApp.switchVideoStream(targetVideo, 'audio', '8-4-sh'); isFirstChunk = false; window.webrtcApp.currentVideoTag = sayName; } catch (error) { console.error('视频切换失败:', error); } } await playAudioData(audioItem.audioData); } else { // 等待一小段时间再检查 await new Promise(resolve => setTimeout(resolve, 50)); } } isProcessingQueue = false; // 等待当前音频播放完成后再切换回默认视频 while (isPlaying) { await new Promise(resolve => setTimeout(resolve, 100)); } const text = 'default' console.log("音频结束------------------------:", window.webrtcApp.currentVideoTag, isPlaying) if (window.webrtcApp.currentVideoTag != text) { isFirstChunk = true window.webrtcApp.currentVideoTag = text window.webrtcApp.switchVideoStream(window.webrtcApp.defaultVideo, 'audio', text); } console.log('音频队列处理完成'); } // 播放单个音频数据 function playAudioData(audioData) { return new Promise((resolve) => { try { const ctx = initAudioContext(); const source = ctx.createBufferSource(); source.buffer = audioData; source.connect(ctx.destination); isPlaying = true; source.onended = () => { console.log('音频片段播放完成'); isPlaying = false; resolve(); }; // 超时保护 setTimeout(() => { if (isPlaying) { console.log('音频播放超时,强制结束'); isPlaying = false; resolve(); } }, (audioData.duration + 0.5) * 1000); source.start(0); console.log(`开始播放音频片段,时长: ${audioData.duration}秒`); } catch (error) { console.error('播放音频失败:', error); isPlaying = false; resolve(); } }); } // 修改原来的playAudioChunk函数,改为addAudioToQueue const playAudioChunk = addAudioToQueue; // 清空音频队列 function clearAudioQueue() { audioQueue.length = 0; console.log('音频队列已清空'); } // 获取队列状态 function getQueueStatus() { return { queueLength: audioQueue.length, isPlaying, isProcessingQueue }; } // 移除waitForCurrentAudioToFinish函数,不再需要 async function requestMinimaxi({ apiKey, groupId, body, stream = true , textPlay = false}) { const url = `https://api.minimaxi.com/v1/t2a_v2`; const reqBody = { ...body, stream }; isPlaying = textPlay // 添加这两行变量定义 let isFirstChunk = true; // const currentText = body.text; const response = await fetch(url, { method: 'POST', headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json', 'Accept': 'text/event-stream', 'Cache-Control': 'no-cache', }, body: JSON.stringify(reqBody), }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } if (!stream) { // 非流式,直接返回JSON const result = await response.json(); console.log(JSON.stringify(result, null, 2)); return result; } else { // 流式,解析每个chunk,实时播放音频 const reader = response.body.getReader(); const decoder = new TextDecoder('utf-8'); let done = false; let buffer = ''; let audioHex = ''; let lastFullResult = null; // 重置播放状态 nextStartTime = 0; if (audioContext) { nextStartTime = audioContext.currentTime; } while (!done) { const { value, done: doneReading } = await reader.read(); done = doneReading; if (value) { const chunk = decoder.decode(value, { stream: true }); buffer += chunk; // 处理SSE格式的数据(以\n分割) let lines = buffer.split('\n'); buffer = lines.pop(); // 最后一行可能是不完整的,留到下次 for (const line of lines) { if (!line.trim()) continue; // 检查是否是SSE格式的数据行 if (line.startsWith('data:')) { const jsonStr = line.substring(6); // 移除 'data: ' 前缀 if (jsonStr.trim() === '[DONE]') { console.log('SSE流结束'); continue; } try { const obj = JSON.parse(jsonStr); // 流式,解析每个chunk,实时播放音频 if (obj.data && obj.data.audio && obj.data.status === 1) { console.log('收到音频数据片段!', obj.data.audio.length); // audioHex += obj.data.audio; audioHex = obj.data.audio; // const sayName = 'say-5s-m-sw' // // 如果是第一个音频片段,触发视频切换 // if (isFirstChunk && sayName != window.webrtcApp.currentVideoName && window.webrtcApp && window.webrtcApp.handleTextInput) { // try { // await window.webrtcApp.handleTextInput(sayName); // isFirstChunk = false; // window.webrtcApp.currentVideoName = sayName; // } catch (error) { // console.error('视频切换失败:', error); // } // } // 立即播放这个音频片段 await playAudioChunk(obj.data.audio); } // status=2为最后一个chunk,记录完整结构 if (obj.data && obj.data.status === 2) { // const text = 'default' // await window.webrtcApp.socket.emit('text-input', { text }); // await window.webrtcApp.handleTextInput(text); lastFullResult = null; console.log('收到最终状态'); } } catch (e) { console.error('解析SSE数据失败:', e, '原始数据:', jsonStr); } } else if (line.startsWith('event: ') || line.startsWith('id: ') || line.startsWith('retry: ')) { // 忽略SSE的其他字段 console.log('忽略SSE字段:', line); continue; } else if (line.trim() && !line.startsWith('data:')) { // 尝试直接解析(兼容非SSE格式,但避免重复处理) console.log('尝试直接解析:', line); try { const obj = JSON.parse(line); if (obj.data && obj.data.audio) { console.log('收到无data:音频数据!', obj.data.audio.length); audioHex = obj.data.audio; // 立即播放这个音频片段 await playAudioChunk(obj.data.audio); } if (obj.data && obj.data.status === 2) { lastFullResult = obj; } console.log('直接解析成功:', JSON.stringify(obj)); } catch (e) { console.error('解析chunk失败:', e, line); } } } } } // 合成最终结构 console.log('音频数据总长度:', audioHex.length); if (lastFullResult) { lastFullResult.data.audio = audioHex; console.log('最终合成结果:', JSON.stringify(lastFullResult, null, 2)); return lastFullResult; } else { // 没有完整结构,返回合成的audio return { data: { audio: audioHex } }; } } } // 火山引擎TTS方法 async function requestVolcanTTS({ appId, accessKey, resourceId = 'volc.service_type.10029', appKey = 'aGjiRDfUWi', body, stream = true }) { const url = 'https://openspeech.bytedance.com/api/v3/tts/unidirectional'; // 生成请求ID const requestId = generateUUID(); const response = await fetch(url, { method: 'POST', headers: { 'X-Api-App-Id': appId, 'X-Api-Access-Key': accessKey, 'X-Api-Resource-Id': resourceId, 'X-Api-App-Key': appKey, 'X-Api-Request-Id': requestId, 'Content-Type': 'application/json', 'Accept': stream ? 'text/event-stream' : 'application/json', 'Cache-Control': 'no-cache', }, body: JSON.stringify(body), }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } if (!stream) { // 非流式,直接返回JSON const result = await response.json(); console.log('火山引擎TTS非流式结果:', JSON.stringify(result, null, 2)); return result; } else { // 流式,解析每个chunk,合并audio const reader = response.body.getReader(); const decoder = new TextDecoder('utf-8'); let done = false; let buffer = ''; let audioBase64 = ''; let lastFullResult = null; while (!done) { const { value, done: doneReading } = await reader.read(); done = doneReading; if (value) { const chunk = decoder.decode(value, { stream: true }); buffer += chunk; // 处理SSE格式的数据(以\n分割) let lines = buffer.split('\n'); buffer = lines.pop(); // 最后一行可能是不完整的,留到下次 for (const line of lines) { if (!line.trim()) continue; // 检查是否是SSE格式的数据行 if (line.startsWith('data:')) { const jsonStr = line.substring(6); // 移除 'data: ' 前缀 if (jsonStr.trim() === '[DONE]') { console.log('火山引擎TTS流结束'); continue; } try { const obj = JSON.parse(jsonStr); // 流式,解析每个chunk,合并audio base64数据 if (obj.data) { audioBase64 += obj.data; lastFullResult = obj; } // 实时打印每个chunk console.log('火山引擎TTS解析成功:', JSON.stringify(obj)); } catch (e) { console.error('解析火山引擎TTS数据失败:', e, '原始数据:', jsonStr); } } else if (line.startsWith('event: ') || line.startsWith('id: ') || line.startsWith('retry: ')) { // 忽略SSE的其他字段 console.log('忽略SSE字段:', line); continue; } else if (line.trim() && !line.startsWith('data:')) { // 尝试直接解析(兼容非SSE格式) try { const obj = JSON.parse(line); if (obj.data) { audioBase64 += obj.data; lastFullResult = obj; } console.log('火山引擎TTS直接解析成功:', JSON.stringify(obj)); } catch (e) { console.error('解析火山引擎TTS chunk失败:', e, line); } } } } } // 合成最终结构 console.log('火山引擎TTS音频数据总长度:', audioBase64.length); if (lastFullResult) { // 更新最终结果的音频数据 lastFullResult.data = audioBase64; console.log('火山引擎TTS最终合成结果:', JSON.stringify(lastFullResult, null, 2)); return lastFullResult; } else { // 没有完整结构,返回合成的audio return { code: 0, message: '', data: audioBase64 }; } } } // 生成UUID的辅助函数 function generateUUID() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { const r = Math.random() * 16 | 0; const v = c === 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); } export { requestMinimaxi, requestVolcanTTS };