WebRtc_QingGan/src/minimaxi_stream.js
Song367 226ec68525
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 2m51s
弃用webrtc 切换视频
2025-08-07 21:39:08 +08:00

431 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 以流式或非流式方式请求 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 };