new commit
This commit is contained in:
		
							parent
							
								
									7703a266bf
								
							
						
					
					
						commit
						6f087fe874
					
				
							
								
								
									
										184
									
								
								src/chat_with_audio.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										184
									
								
								src/chat_with_audio.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,184 @@ | |||||||
|  | // 用户输入文本后,进行大模型回答,并且合成音频,流式播放
 | ||||||
|  | 
 | ||||||
|  | import { requestLLMStream } from './llm_stream.js'; | ||||||
|  | import { requestMinimaxi } from './minimaxi_stream.js'; | ||||||
|  | 
 | ||||||
|  | async function chatWithAudioStream({ userInput, llmApiKey, llmModel, minimaxiApiKey, minimaxiGroupId }) { | ||||||
|  |   console.log('用户输入:', userInput); | ||||||
|  |    | ||||||
|  |   // 1. 请求大模型回答
 | ||||||
|  |   console.log('\n=== 请求大模型回答 ==='); | ||||||
|  |   const llmResponse = await requestLLMStream({ | ||||||
|  |     apiKey: llmApiKey, | ||||||
|  |     model: llmModel, | ||||||
|  |     messages: [ | ||||||
|  |       { role: 'system', content: 'You are a helpful assistant.' }, | ||||||
|  |       { role: 'user', content: userInput }, | ||||||
|  |     ], | ||||||
|  |   }); | ||||||
|  |    | ||||||
|  |   // 提取大模型回答内容(假设返回的是JSON格式,包含content字段)
 | ||||||
|  |   let llmContent = ''; | ||||||
|  |   try { | ||||||
|  |     const llmData = JSON.parse(llmResponse); | ||||||
|  |     llmContent = llmData.choices?.[0]?.message?.content || llmResponse; | ||||||
|  |   } catch (e) { | ||||||
|  |     llmContent = llmResponse; | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   console.log('\n=== 大模型回答 ==='); | ||||||
|  |   console.log(llmContent); | ||||||
|  |    | ||||||
|  |   // 2. 合成音频
 | ||||||
|  |   console.log('\n=== 开始合成音频 ==='); | ||||||
|  |   const audioResult = await requestMinimaxi({ | ||||||
|  |     apiKey: minimaxiApiKey, | ||||||
|  |     groupId: minimaxiGroupId, | ||||||
|  |     body: { | ||||||
|  |       model: 'speech-02-hd', | ||||||
|  |       text: llmContent, | ||||||
|  |       stream: true, | ||||||
|  |       language_boost: 'auto', | ||||||
|  |       output_format: 'hex', | ||||||
|  |       voice_setting: { | ||||||
|  |         voice_id: 'male-qn-qingse', | ||||||
|  |         speed: 1, | ||||||
|  |         vol: 1, | ||||||
|  |         pitch: 0, | ||||||
|  |         emotion: 'happy', | ||||||
|  |       }, | ||||||
|  |       audio_setting: { | ||||||
|  |         sample_rate: 32000, | ||||||
|  |         bitrate: 128000, | ||||||
|  |         format: 'mp3', | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|  |     stream: true, | ||||||
|  |   }); | ||||||
|  |    | ||||||
|  |   // 3. 流式播放音频
 | ||||||
|  |   console.log('\n=== 开始流式播放音频 ==='); | ||||||
|  |   await playAudioStream(audioResult.data.audio); | ||||||
|  |    | ||||||
|  |   return { | ||||||
|  |     userInput, | ||||||
|  |     llmResponse: llmContent, | ||||||
|  |     audioResult, | ||||||
|  |   }; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 流式播放音频
 | ||||||
|  | async function playAudioStream(audioHex) { | ||||||
|  |   // 将hex转换为ArrayBuffer
 | ||||||
|  |   const audioBuffer = hexToArrayBuffer(audioHex); | ||||||
|  |    | ||||||
|  |   // 创建AudioContext
 | ||||||
|  |   const audioContext = new (window.AudioContext || window.webkitAudioContext)(); | ||||||
|  |    | ||||||
|  |   try { | ||||||
|  |     // 解码音频
 | ||||||
|  |     const audioData = await audioContext.decodeAudioData(audioBuffer); | ||||||
|  |      | ||||||
|  |     // 创建音频源
 | ||||||
|  |     const source = audioContext.createBufferSource(); | ||||||
|  |     source.buffer = audioData; | ||||||
|  |     source.connect(audioContext.destination); | ||||||
|  |      | ||||||
|  |     // 播放
 | ||||||
|  |     source.start(0); | ||||||
|  |      | ||||||
|  |     console.log('音频播放开始,时长:', audioData.duration, '秒'); | ||||||
|  |      | ||||||
|  |     // 等待播放完成
 | ||||||
|  |     return new Promise((resolve) => { | ||||||
|  |       source.onended = () => { | ||||||
|  |         console.log('音频播放完成'); | ||||||
|  |         resolve(); | ||||||
|  |       }; | ||||||
|  |     }); | ||||||
|  |   } catch (error) { | ||||||
|  |     console.error('音频播放失败:', error); | ||||||
|  |     throw error; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 将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; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 在Node.js环境下的音频播放(使用play-sound库)
 | ||||||
|  | async function playAudioStreamNode(audioHex) { | ||||||
|  |   const fs = require('fs'); | ||||||
|  |   const path = require('path'); | ||||||
|  |    | ||||||
|  |   // 将hex转换为buffer
 | ||||||
|  |   const audioBuffer = Buffer.from(audioHex, 'hex'); | ||||||
|  |    | ||||||
|  |   // 保存为临时文件
 | ||||||
|  |   const tempFile = path.join(process.cwd(), 'temp_audio.mp3'); | ||||||
|  |   fs.writeFileSync(tempFile, audioBuffer); | ||||||
|  |    | ||||||
|  |   try { | ||||||
|  |     // 使用系统默认播放器播放
 | ||||||
|  |     const { exec } = require('child_process'); | ||||||
|  |     const platform = process.platform; | ||||||
|  |      | ||||||
|  |     let command; | ||||||
|  |     if (platform === 'win32') { | ||||||
|  |       command = `start "" "${tempFile}"`; | ||||||
|  |     } else if (platform === 'darwin') { | ||||||
|  |       command = `open "${tempFile}"`; | ||||||
|  |     } else { | ||||||
|  |       command = `xdg-open "${tempFile}"`; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     exec(command, (error) => { | ||||||
|  |       if (error) { | ||||||
|  |         console.error('播放音频失败:', error); | ||||||
|  |       } else { | ||||||
|  |         console.log('音频播放开始'); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |      | ||||||
|  |     // 等待一段时间后删除临时文件
 | ||||||
|  |     setTimeout(() => { | ||||||
|  |       if (fs.existsSync(tempFile)) { | ||||||
|  |         fs.unlinkSync(tempFile); | ||||||
|  |       } | ||||||
|  |     }, 10000); | ||||||
|  |      | ||||||
|  |   } catch (error) { | ||||||
|  |     console.error('音频播放失败:', error); | ||||||
|  |     throw error; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 示例用法
 | ||||||
|  | if (require.main === module) { | ||||||
|  |   const llmApiKey = process.env.ARK_API_KEY; | ||||||
|  |   const llmModel = 'bot-20250720193048-84fkp'; | ||||||
|  |   const minimaxiApiKey = process.env.MINIMAXI_API_KEY; | ||||||
|  |   const minimaxiGroupId = process.env.MINIMAXI_GROUP_ID; | ||||||
|  |    | ||||||
|  |   if (!llmApiKey || !minimaxiApiKey || !minimaxiGroupId) { | ||||||
|  |     console.error('请设置环境变量: ARK_API_KEY, MINIMAXI_API_KEY, MINIMAXI_GROUP_ID'); | ||||||
|  |     process.exit(1); | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   const userInput = process.argv[2] || '你好,请介绍一下人工智能的发展历程'; | ||||||
|  |    | ||||||
|  |   chatWithAudioStream({ | ||||||
|  |     userInput, | ||||||
|  |     llmApiKey, | ||||||
|  |     llmModel, | ||||||
|  |     minimaxiApiKey, | ||||||
|  |     minimaxiGroupId, | ||||||
|  |   }).catch(console.error); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export { chatWithAudioStream, playAudioStream, playAudioStreamNode };  | ||||||
							
								
								
									
										59
									
								
								src/llm_stream.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								src/llm_stream.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,59 @@ | |||||||
|  | // 以流式方式请求LLM大模型接口,并打印流式返回内容
 | ||||||
|  | 
 | ||||||
|  | async function requestLLMStream({ apiKey, model, messages }) { | ||||||
|  |   const response = await fetch('https://ark.cn-beijing.volces.com/api/v3/bots/chat/completions', { | ||||||
|  |     method: 'POST', | ||||||
|  |     headers: { | ||||||
|  |       'Authorization': `Bearer ${apiKey}`, | ||||||
|  |       'Content-Type': 'application/json', | ||||||
|  |     }, | ||||||
|  |     body: JSON.stringify({ | ||||||
|  |       model, | ||||||
|  |       stream: true, | ||||||
|  |       stream_options: { include_usage: true }, | ||||||
|  |       messages, | ||||||
|  |     }), | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   if (!response.ok) { | ||||||
|  |     throw new Error(`HTTP error! status: ${response.status}`); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   const reader = response.body.getReader(); | ||||||
|  |   const decoder = new TextDecoder('utf-8'); | ||||||
|  |   let done = false; | ||||||
|  |   let buffer = ''; | ||||||
|  | 
 | ||||||
|  |   while (!done) { | ||||||
|  |     const { value, done: doneReading } = await reader.read(); | ||||||
|  |     done = doneReading; | ||||||
|  |     if (value) { | ||||||
|  |       const chunk = decoder.decode(value, { stream: true }); | ||||||
|  |       buffer += chunk; | ||||||
|  |       // 打印每次收到的内容
 | ||||||
|  |       process.stdout.write(chunk); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // 可选:返回完整内容
 | ||||||
|  |   return buffer; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 示例用法
 | ||||||
|  | if (require.main === module) { | ||||||
|  |   const apiKey = process.env.ARK_API_KEY; | ||||||
|  |   if (!apiKey) { | ||||||
|  |     console.error('请设置环境变量 ARK_API_KEY'); | ||||||
|  |     process.exit(1); | ||||||
|  |   } | ||||||
|  |   requestLLMStream({ | ||||||
|  |     apiKey, | ||||||
|  |     model: 'bot-20250720193048-84fkp', | ||||||
|  |     messages: [ | ||||||
|  |       { role: 'system', content: 'You are a helpful assistant.' }, | ||||||
|  |       { role: 'user', content: 'Hello!' }, | ||||||
|  |     ], | ||||||
|  |   }).catch(console.error); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export { requestLLMStream };  | ||||||
							
								
								
									
										116
									
								
								src/minimaxi_stream.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								src/minimaxi_stream.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,116 @@ | |||||||
|  | // 以流式或非流式方式请求 minimaxi 大模型接口,并打印/返回内容
 | ||||||
|  | 
 | ||||||
|  | async function requestMinimaxi({ apiKey, groupId, body, stream = true }) { | ||||||
|  |   const url = `https://api.minimaxi.com/v1/t2a_v2/${groupId}`; | ||||||
|  |   const reqBody = { ...body, stream }; | ||||||
|  |   const response = await fetch(url, { | ||||||
|  |     method: 'POST', | ||||||
|  |     headers: { | ||||||
|  |       'Authorization': `Bearer ${apiKey}`, | ||||||
|  |       'Content-Type': 'application/json', | ||||||
|  |     }, | ||||||
|  |     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,合并audio
 | ||||||
|  |     const reader = response.body.getReader(); | ||||||
|  |     const decoder = new TextDecoder('utf-8'); | ||||||
|  |     let done = false; | ||||||
|  |     let buffer = ''; | ||||||
|  |     let audioHex = ''; | ||||||
|  |     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; | ||||||
|  |         // 处理多条JSON(以\n分割)
 | ||||||
|  |         let lines = buffer.split('\n'); | ||||||
|  |         buffer = lines.pop(); // 最后一行可能是不完整的,留到下次
 | ||||||
|  |         for (const line of lines) { | ||||||
|  |           if (!line.trim()) continue; | ||||||
|  |           try { | ||||||
|  |             const obj = JSON.parse(line); | ||||||
|  |             if (obj.data && obj.data.audio) { | ||||||
|  |               audioHex += obj.data.audio; | ||||||
|  |             } | ||||||
|  |             // status=2为最后一个chunk,记录完整结构
 | ||||||
|  |             if (obj.data && obj.data.status === 2) { | ||||||
|  |               lastFullResult = obj; | ||||||
|  |             } | ||||||
|  |             // 实时打印每个chunk
 | ||||||
|  |             console.log('chunk:', JSON.stringify(obj)); | ||||||
|  |           } catch (e) { | ||||||
|  |             console.error('解析chunk失败:', e, line); | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     // 合成最终结构
 | ||||||
|  |     if (lastFullResult) { | ||||||
|  |       lastFullResult.data.audio = audioHex; | ||||||
|  |       console.log('最终合成结果:', JSON.stringify(lastFullResult, null, 2)); | ||||||
|  |       return lastFullResult; | ||||||
|  |     } else { | ||||||
|  |       // 没有完整结构,返回合成的audio
 | ||||||
|  |       return { data: { audio: audioHex } }; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 示例用法
 | ||||||
|  | if (require.main === module) { | ||||||
|  |   const apiKey = process.env.MINIMAXI_API_KEY; | ||||||
|  |   const groupId = process.env.MINIMAXI_GROUP_ID; | ||||||
|  |   if (!apiKey || !groupId) { | ||||||
|  |     console.error('请设置环境变量 MINIMAXI_API_KEY 和 MINIMAXI_GROUP_ID'); | ||||||
|  |     process.exit(1); | ||||||
|  |   } | ||||||
|  |   const baseBody = { | ||||||
|  |     model: 'speech-02-hd', | ||||||
|  |     text: '真正的危险不是计算机开始像人一样思考,而是人开始像计算机一样思考。计算机只是可以帮我们处理一些简单事务。', | ||||||
|  |     language_boost: 'auto', | ||||||
|  |     output_format: 'hex', | ||||||
|  |     voice_setting: { | ||||||
|  |       voice_id: 'male-qn-qingse', | ||||||
|  |       speed: 1, | ||||||
|  |       vol: 1, | ||||||
|  |       pitch: 0, | ||||||
|  |       emotion: 'happy', | ||||||
|  |     }, | ||||||
|  |     audio_setting: { | ||||||
|  |       sample_rate: 32000, | ||||||
|  |       bitrate: 128000, | ||||||
|  |       format: 'mp3', | ||||||
|  |     }, | ||||||
|  |   }; | ||||||
|  |   // 非流式
 | ||||||
|  |   requestMinimaxi({ | ||||||
|  |     apiKey, | ||||||
|  |     groupId, | ||||||
|  |     body: baseBody, | ||||||
|  |     stream: false, | ||||||
|  |   }).then(() => { | ||||||
|  |     // 流式
 | ||||||
|  |     return requestMinimaxi({ | ||||||
|  |       apiKey, | ||||||
|  |       groupId, | ||||||
|  |       body: baseBody, | ||||||
|  |       stream: true, | ||||||
|  |     }); | ||||||
|  |   }).catch(console.error); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export { requestMinimaxi };  | ||||||
							
								
								
									
										42
									
								
								src/video_audio_sync.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								src/video_audio_sync.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,42 @@ | |||||||
|  | import { requestMinimaxi } from './minimaxi_stream.js'; | ||||||
|  | 
 | ||||||
|  | export async function playVideoWithAudio(videoPath, text) { | ||||||
|  |   // 1. 初始化视频播放
 | ||||||
|  |   const video = document.createElement('video'); | ||||||
|  |   video.src = videoPath; | ||||||
|  |   document.body.appendChild(video); | ||||||
|  |    | ||||||
|  |   // 2. 启动音频合成流
 | ||||||
|  |   const audioStream = await requestMinimaxi({ | ||||||
|  |     apiKey: process.env.MINIMAXI_API_KEY, | ||||||
|  |     groupId: process.env.MINIMAXI_GROUP_ID, | ||||||
|  |     body: { | ||||||
|  |       model: 'speech-02-hd', | ||||||
|  |       text, | ||||||
|  |       output_format: 'hex', | ||||||
|  |       voice_setting: { | ||||||
|  |         voice_id: 'male-qn-qingse', | ||||||
|  |         speed: 1 | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     stream: true | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   // 3. 将音频hex转换为可播放格式
 | ||||||
|  |   const audioCtx = new AudioContext(); | ||||||
|  |   const audioBuffer = await audioCtx.decodeAudioData( | ||||||
|  |     hexToArrayBuffer(audioStream.data.audio) | ||||||
|  |   ); | ||||||
|  |    | ||||||
|  |   // 4. 同步播放
 | ||||||
|  |   const source = audioCtx.createBufferSource(); | ||||||
|  |   source.buffer = audioBuffer; | ||||||
|  |   source.connect(audioCtx.destination); | ||||||
|  |    | ||||||
|  |   video.play(); | ||||||
|  |   source.start(0); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function hexToArrayBuffer(hex) { | ||||||
|  |   // ... hex转ArrayBuffer实现
 | ||||||
|  | } | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Song367
						Song367