基于ElevenLabs API的AI助手语音合成集成实践

基于ElevenLabs API的AI助手语音合成集成实践 1. 项目概述为AI工作助手注入“声音”最近在折腾我的AI工作工具时我干了一件事给它加了个“嘴巴”。准确来说是集成了 ElevenLabs 的语音合成能力让原本只能默默打字、在屏幕上显示文字的AI助手能够开口说话了。这个功能听起来简单但实际做下来你会发现它彻底改变了人机交互的体验。想象一下当你正在写代码、整理文档或者进行数据分析时你的AI伙伴不仅能理解你的文字指令还能用清晰、自然、甚至带点情绪的声音把分析结果、代码建议或者提醒事项“说”给你听。这不再是冰冷的文本输出而是一种更接近真人协作的体验。我之所以选择 ElevenLabs 作为语音层核心原因在于其合成语音的质量和自然度在目前的市场上是公认的第一梯队。它提供的不仅仅是“文字转语音”而是“文本转情感丰富的语音”。这对于一个工作工具来说至关重要——你肯定不希望听到一个机械的、毫无波澜的声音在你耳边念着复杂的逻辑或重要的数据。我需要的是听起来像一位专注、专业的同事在和你同步信息。这个项目就是围绕如何将 ElevenLabs 强大的语音合成 API无缝、稳定、高效地集成到现有的 AI 工作流中并处理好音频流的实时播放、错误重试、上下文关联等一系列工程细节。接下来我会详细拆解整个实现过程从架构设计到每一行关键代码以及我踩过的那些坑。2. 核心架构设计与技术选型2.1 为什么是 ElevenLabs在决定为工具添加语音功能时市面上可选的服务不少比如 Google Cloud TTS、Amazon Polly、微软 Azure TTS以及一些开源方案如 Coqui TTS。我的选择标准非常明确自然度优先延迟可接受API 稳定且开发者友好。经过一番实测和对比ElevenLabs 在自然度和情感表达上优势明显。它的“语音克隆”和“语音库”功能虽然强大但在这个项目中我主要使用的是其预置的、经过优化的“播音员”风格声音例如Rachel、Domi、Bella。这些声音在播报技术内容、数据报告时清晰、沉稳略带专业感非常契合工作场景。相比之下许多其他服务的“新闻播音”风格要么过于正式要么在长句连贯性和情感细微变化上稍逊一筹。从技术集成角度看ElevenLabs 提供了简洁明了的 REST API 和 WebSocket 流式接口。对于需要实时反馈的场景比如AI一边思考一边“说”流式接口是必选项。它的文档清晰提供了多种语言的 SDK虽然我选择直接调用 HTTP API 以获得更大控制权并且有相对慷慨的免费额度供开发和测试使用。成本方面按字符数计费的模式也易于预估和控制。注意ElevenLabs 的语音模型对标点符号和文本结构非常敏感。一个常见的误区是直接把 AI 返回的原始 Markdown 或代码块文本扔给它这会导致奇怪的停顿和语调。预处理文本是集成前必不可少的一步。2.2 整体架构思路我的 AI 工作工具本身是一个本地运行的桌面应用核心是一个与大型语言模型LLM交互的客户端。添加语音模式意味着要在现有的“用户输入 - AI 模型 - 文本输出”链条中插入一个“语音合成与播放”的并行环节。我设计的架构是“异步旁路输出”模式主流程不变用户提问工具调用 LLM API如 OpenAI GPT、Claude 或本地模型获取流式的文本回复。语音旁路触发当开始接收到 LLM 的文本流时同时启动一个语音合成任务。文本预处理与缓冲将流式到达的文本进行清洗去除 Markdown 符号、代码块、特殊字符、断句根据句号、问号、分号等并放入一个缓冲区。流式合成与播放一旦缓冲区积累了一个完整的句子或达到一定长度就将其发送给 ElevenLabs 的流式 TTS API。接收到音频流如 MP3 或 PCM 数据后立即交给系统的音频播放器进行播放。双流同步文本在界面中实时显示语音几乎同步播出。由于网络请求和音频生成需要时间语音会比文本显示略有延迟但通过合理的缓冲和预发送可以做到延迟感知不明显体验流畅。这种架构的优势在于非侵入性。语音模块作为一个独立的服务或线程运行不会阻塞主界面的渲染和用户交互。即使 ElevenLabs API 临时出现波动或网络不佳也只会影响语音输出核心的文本交互功能依然完好。2.3 技术栈明细核心应用基于 Electron React 的桌面应用也可类比为 PyQt、Tauri 等框架。LLM 交互使用 OpenAI Node.js SDK 进行流式调用。语音合成层ElevenLabs Text-to-Speech API主要使用/v1/text-to-speech/{voice_id}/stream端点实现低延迟流式输出。音频处理Node.js 环境下的speaker库用于播放 PCM 流或fluent-ffmpeg/howler.js用于播放 MP3 等格式。在渲染进程前端中可以使用 Web Audio API。文本预处理自定义的 JavaScript/TypeScript 函数利用正则表达式和分词库如natural进行文本清洗和断句。状态与通信使用 React Context 或 Zustand 进行应用状态管理通过 Electron 的 IPC进程间通信在主进程和渲染进程之间传递音频数据和播放指令。3. 关键实现步骤与代码拆解3.1 环境准备与 API 密钥配置首先你需要在 ElevenLabs 官网注册账号并在 Profile 页面获取你的 API Key。安全地管理这个密钥至关重要绝不能硬编码在客户端代码中。对于 Electron 应用我推荐将敏感配置存储在本地加密文件或使用主进程的环境变量中。这里展示一个简单的配置模块// config.js (在主进程中) import { app } from electron; import fs from fs/promises; import path from path; import crypto from crypto; const CONFIG_PATH path.join(app.getPath(userData), config.enc); // 一个简单的加密/解密函数生产环境应考虑更安全的方案如 keytar const ENCRYPTION_KEY process.env.ENCRYPTION_KEY || your-very-secure-dev-key; // 应从安全的地方注入 function encrypt(text) { const cipher crypto.createCipher(aes-256-cbc, ENCRYPTION_KEY); let encrypted cipher.update(text, utf8, hex); encrypted cipher.final(hex); return encrypted; } function decrypt(encryptedText) { const decipher crypto.createDecipher(aes-256-cbc, ENCRYPTION_KEY); let decrypted decipher.update(encryptedText, hex, utf8); decrypted decipher.final(utf8); return decrypted; } export async function getConfig() { try { const data await fs.readFile(CONFIG_PATH, utf8); const decrypted decrypt(data); return JSON.parse(decrypted); } catch (error) { // 文件不存在或损坏返回默认配置 return { elevenLabsApiKey: }; } } export async function saveConfig(config) { const encrypted encrypt(JSON.stringify(config)); await fs.writeFile(CONFIG_PATH, encrypted, utf8); } // 在应用初始化时可以提供一个界面让用户输入并保存 API Key3.2 文本预处理从 AI 原始输出到可朗读文本这是影响语音质量最关键的一步。LLM 的回复通常包含Markdown 格式**加粗**、# 标题、代码、[链接](url)。代码块python ...。特殊字符和表情符号。不完整的句子或思维链如“让我们想一想...”、“首先”。我们的目标是生成干净、符合口语习惯的文本。我的预处理管道如下// textProcessor.js import { SentenceTokenizer } from natural; // 一个简单的断句库 const sentenceTokenizer new SentenceTokenizer(); export function cleanTextForTTS(rawText) { let cleaned rawText; // 1. 移除 Markdown 代码块包括语言标识 cleaned cleaned.replace(/[\s\S]*?/g, ); // 2. 移除行内代码标记 cleaned cleaned.replace(/([^])/g, $1); // 保留代码内容去掉反引号 // 3. 移除 Markdown 链接只保留文本 cleaned cleaned.replace(/\[([^\]])\]\([^)]\)/g, $1); // 4. 移除加粗、斜体等标记 cleaned cleaned.replace(/(\*\*|__)(.*?)\1/g, $2); // 加粗 cleaned cleaned.replace(/(\*|_)(.*?)\1/g, $2); // 斜体 // 5. 移除标题标记 cleaned cleaned.replace(/^#\s/gm, ); // 6. 将多个换行/空格合并为单个空格或句点根据上下文 cleaned cleaned.replace(/\n\s*\n/g, . ); // 双换行可能表示段落结束 cleaned cleaned.replace(/\s/g, ).trim(); // 7. 处理一些AI常见的“口头禅”或无意义输出 const filters [ /^当然/i, /^让我们/i, /^首先/i, /^其次/i, /^另外/i, /^总的来说/i, /^请注意/i, ]; filters.forEach(regex { cleaned cleaned.replace(regex, ); }); return cleaned.trim(); } export function splitIntoSentences(text) { // 使用 natural 库进行基本断句也可用更复杂的规则 const sentences sentenceTokenizer.tokenize(text); // 过滤掉过短的“句子”可能是标点或残留碎片 return sentences.filter(s s.length 5 /[a-zA-Z0-9]/.test(s)); } // 综合处理函数 export function processAIResponseForSpeech(aiStreamChunk) { const cleaned cleanTextForTTS(aiStreamChunk); // 注意对于流式输入我们可能不会立即断句而是积累到一个缓冲区 return cleaned; }3.3 集成 ElevenLabs 流式 TTS APIElevenLabs 的流式端点允许我们发送文本并几乎实时接收音频流。我们需要管理文本缓冲区并在合适的时机如遇到句号、达到最大长度触发合成请求。// elevenlabsService.js import fetch from node-fetch; // 在 Node.js 主进程中 import { EventEmitter } from events; class ElevenLabsTTS extends EventEmitter { constructor(apiKey, voiceId 21m00Tcm4TlvDq8ikWAM, // 例如 Rachel modelId eleven_monolingual_v1, stability 0.5, similarityBoost 0.75) { super(); this.apiKey apiKey; this.voiceId voiceId; this.modelId modelId; this.stability stability; this.similarityBoost similarityBoost; this.textBuffer ; this.isSpeaking false; this.sentenceEndRegex /[.!?。]\s*$/; // 判断句子结束 } // 接收来自 AI 的文本流片段 feedText(chunk) { this.textBuffer chunk; // 检查缓冲区末尾是否形成了一个完整的句子 if (this.sentenceEndRegex.test(this.textBuffer) || this.textBuffer.length 150) { this._synthesizeAndPlay(this.textBuffer); this.textBuffer ; // 清空缓冲区 } } // 强制刷新缓冲区当AI回复结束时调用 flush() { if (this.textBuffer.trim().length 0) { this._synthesizeAndPlay(this.textBuffer); this.textBuffer ; } } async _synthesizeAndPlay(text) { if (!text.trim() || this.isSpeaking) { // 可以加入一个播放队列这里为了简单如果正在播放则跳过 return; } this.isSpeaking true; this.emit(speechStart, text); const url https://api.elevenlabs.io/v1/text-to-speech/${this.voiceId}/stream; const requestBody { text: text, model_id: this.modelId, voice_settings: { stability: this.stability, similarity_boost: this.similarityBoost, }, }; try { const response await fetch(url, { method: POST, headers: { Accept: audio/mpeg, // 接收MP3格式 Content-Type: application/json, xi-api-key: this.apiKey, }, body: JSON.stringify(requestBody), }); if (!response.ok) { const errorText await response.text(); throw new Error(ElevenLabs TTS failed: ${response.status} ${errorText}); } // 获取音频流 const audioStream response.body; // 这里需要将音频流传给播放器 // 例如使用 speaker 库播放需要 PCM 数据可能需要先解码 MP3 // 或者将流保存为临时文件然后用音频播放器播放 this.emit(audioStream, audioStream, text); // 假设我们有一个 playAudioStream 函数来处理流 await this.playAudioStream(audioStream); } catch (error) { console.error(TTS Synthesis error:, error); this.emit(error, error); } finally { this.isSpeaking false; this.emit(speechEnd); } } async playAudioStream(readableStream) { // 这是一个简化示例。实际中你需要 // 1. 可能要将 MP3 流解码为 PCM使用 like lame 或 ffmpeg // 2. 通过系统的音频接口播放在 Electron 主进程可以用 speaker // 3. 或者在渲染进程通过 Web Audio API 播放需要将流传输到前端 // 这里以主进程使用 speaker 为例需安装 speaker 和 node-lame const { default: Speaker } await import(speaker); const { default: lame } await import(node-lame); // 用于解码MP3 const decoder new lame.Decoder(); readableStream.pipe(decoder).on(format, (format) { const speaker new Speaker(format); decoder.pipe(speaker); speaker.on(close, () { console.log(Playback finished.); }); }); } } export default ElevenLabsTTS;3.4 前端渲染进程与音频播放集成在主进程处理音频流可能带来延迟和复杂性。更优的方案是将音频流直接发送到渲染进程利用 Web Audio API 在浏览器环境中播放这样更灵活且能更好地与 UI 状态同步。我们可以通过 Electron 的 IPC 将音频数据块如 ArrayBuffer从主进程传递到渲染进程。// 在主进程中 (main.js) import { ipcMain } from electron; import ElevenLabsTTS from ./elevenlabsService; let ttsEngine null; ipcMain.handle(init-tts, (event, apiKey, voiceId) { ttsEngine new ElevenLabsTTS(apiKey, voiceId); ttsEngine.on(audioStream, async (stream, text) { // 将音频流转换为 Buffer 块通过 IPC 发送到渲染进程 const chunks []; for await (const chunk of stream) { chunks.push(chunk); // 可以分块发送以减少延迟 event.sender.send(audio-chunk, chunk); } // 或者一次性发送整个音频数据适用于短句子 // const audioBuffer Buffer.concat(chunks); // event.sender.send(audio-data, { buffer: audioBuffer, text }); }); ttsEngine.on(error, (err) { event.sender.send(tts-error, err.message); }); }); ipcMain.handle(feed-text, (event, textChunk) { if (ttsEngine) { ttsEngine.feedText(textChunk); } }); ipcMain.handle(flush-tts, (event) { if (ttsEngine) { ttsEngine.flush(); } });// 在 React 组件中 (渲染进程) import React, { useEffect, useRef, useState } from react; const { ipcRenderer } window.require(electron); const VoicePlayer () { const audioContextRef useRef(null); const [isPlaying, setIsPlaying] useState(false); useEffect(() { // 初始化 AudioContext audioContextRef.current new (window.AudioContext || window.webkitAudioContext)(); // 监听来自主进程的音频数据块 ipcRenderer.on(audio-chunk, async (event, chunk) { await playAudioChunk(chunk); }); ipcRenderer.on(tts-error, (event, errorMsg) { console.error(TTS Error:, errorMsg); setIsPlaying(false); }); return () { ipcRenderer.removeAllListeners(audio-chunk); ipcRenderer.removeAllListeners(tts-error); if (audioContextRef.current audioContextRef.current.state ! closed) { audioContextRef.current.close(); } }; }, []); const playAudioChunk async (chunkBuffer) { if (!audioContextRef.current || audioContextRef.current.state closed) { audioContextRef.current new (window.AudioContext || window.webkitAudioContext)(); } if (audioContextRef.current.state suspended) { await audioContextRef.current.resume(); } setIsPlaying(true); try { // 解码 MP3 数据块这里假设 chunkBuffer 是完整的 MP3 帧实际情况可能更复杂 // 更稳健的做法是收集所有块解码完整的 MP3 文件或使用 MediaSource Extensions 流式播放 // 以下为简化示例 const audioBuffer await audioContextRef.current.decodeAudioData(chunkBuffer.buffer); const source audioContextRef.current.createBufferSource(); source.buffer audioBuffer; source.connect(audioContextRef.current.destination); source.start(); source.onended () { setIsPlaying(false); }; } catch (error) { console.error(Audio playback error:, error); setIsPlaying(false); } }; // 在接收 AI 流式文本时调用此函数 const handleAIStreamChunk (textChunk) { // 1. 预处理文本 const cleanedChunk processAIResponseForSpeech(textChunk); // 2. 发送到主进程进行 TTS ipcRenderer.invoke(feed-text, cleanedChunk); }; const handleAIStreamEnd () { ipcRenderer.invoke(flush-tts); }; return ( div {/* UI 状态指示器 */} div语音状态: {isPlaying ? 播放中... : 静音}/div /div ); }; export default VoicePlayer;4. 性能优化与用户体验打磨4.1 降低感知延迟预合成与智能缓冲流式 TTS 的延迟主要来自网络请求和音频生成。为了减少用户“等待语音”的感觉我采用了两种策略句子级预合成不要等到 AI 回复完一整段再开始合成。如前述代码所示一旦检测到一个完整的句子以句号、问号等结尾就立即将其从缓冲区取出发送给 ElevenLabs。这样当 AI 还在“思考”下一句时上一句的语音可能已经开始播放或正在合成。动态缓冲阈值对于较长的、没有标点的技术描述如一段代码解释如果缓冲区字符数超过一个阈值如 150 字符即使没有句子结束符也强制触发一次合成避免长时间等待。4.2 音频播放队列与打断机制当用户快速连续提问或者 AI 回复速度很快时可能会产生多个 TTS 请求。我们需要一个播放队列来管理这些任务并支持打断当前播放当用户提出新问题时。class TTSPlaybackQueue { constructor() { this.queue []; this.isPlaying false; this.currentSource null; } enqueue(audioData, text, priority false) { const item { audioData, text }; if (priority) { this.queue.unshift(item); // 插队到最前 this._interruptCurrent(); // 打断当前播放 } else { this.queue.push(item); } this._processQueue(); } _interruptCurrent() { if (this.currentSource) { this.currentSource.stop(); // 停止播放当前音频 this.currentSource null; this.isPlaying false; } } async _processQueue() { if (this.isPlaying || this.queue.length 0) { return; } this.isPlaying true; const { audioData, text } this.queue.shift(); try { // 播放 audioData (使用 Web Audio API 或其它播放器) await this._playAudioBuffer(audioData); } catch (error) { console.error(Playback failed:, error); } finally { this.isPlaying false; this.currentSource null; // 继续播放下一个 setTimeout(() this._processQueue(), 100); } } clear() { this.queue []; this._interruptCurrent(); } }当用户发送新消息时调用ttsQueue.clear()和ttsQueue.enqueue(newAudioData, newText, true)即可实现语音的即时打断和切换。4.3 语音设置与个性化ElevenLabs API 提供了stability稳定性和similarity_boost相似度增强等参数来微调语音。经过测试对于工作场景stability设置在0.4~0.6之间比较合适。太低会导致声音波动太大像在窃窃私语太高则显得过于单调机械。similarity_boost设置在0.7~0.8之间可以在保持声音特征清晰的同时不至于过度夸张。此外可以为不同的使用场景预设不同的“声音角色”。例如代码审查模式使用声音Domistability稍高0.6语调更平稳、严谨。创意脑暴模式使用声音Bellastability稍低0.45语调更富有变化和活力。数据播报模式使用声音Rachelstability中等0.5清晰、匀速适合念数字和列表。可以在应用设置中让用户自由选择并保存这些预设。5. 遇到的坑与解决方案实录5.1 坑一文本中的特殊符号导致语音中断或怪调问题早期版本中AI 回复里的 Markdown 代码块如const x 10或 URL 链接会导致 ElevenLabs 的语音突然中断、发出“滴”声或奇怪的电子音。排查通过将发送给 API 的文本日志记录下来发现未清洗的文本中包含反引号、方括号、星号等符号。ElevenLabs 的模型会尝试“读出”这些符号或者将其解释为控制字符。解决如3.2节所述建立严格的文本预处理管道。特别是对于代码我选择完全移除代码块因为用语音听大段代码效率很低对于行内代码则去掉反引号并在前后稍作停顿在文本中插入“代码”、“结束”等提示语或者直接跳过不读由用户自行查看高亮显示的文本。5.2 坑二流式播放的音频卡顿与杂音问题使用 Web Audio API 直接播放从 IPC 传过来的零碎 ArrayBuffer 时经常出现“噼啪”声、卡顿或音频不连贯。排查原因有两个1) IPC 传输和音频解码、播放的速度不匹配缓冲区下溢。2) 接收到的音频数据块可能不是完整的 MP3 帧导致解码错误。解决在主进程进行音频缓冲不再一个数据块一发而是在主进程将 ElevenLabs 返回的整个音频流收集完转换成一个完整的 MP3 Buffer再一次性发送给渲染进程。虽然增加了少量延迟但保证了音频完整性彻底消除了卡顿。对于长文本可以按句子为单位进行这种“缓冲-完整发送”的操作。使用成熟的音频播放库在前端放弃直接使用decodeAudioData处理原始流转而使用howler.js库。它内部有更稳健的缓冲和播放队列管理只需将完整的 MP3 数据作为 Blob 或 Base64 URL 提供给 Howler 即可。// 改进后的前端播放代码片段 import { Howl } from howler; // 在主进程将完整的音频 Buffer 转换为 Base64 const audioData Buffer.concat(chunks); const base64Audio audioData.toString(base64); const dataUrl data:audio/mpeg;base64,${base64Audio}; event.sender.send(audio-ready, { dataUrl, text }); // 在渲染进程 ipcRenderer.on(audio-ready, (event, { dataUrl, text }) { const sound new Howl({ src: [dataUrl], format: [mp3], onplay: () setIsPlaying(true), onend: () setIsPlaying(false), onloaderror: (id, err) console.error(Howl load error:, err), }); sound.play(); });5.3 坑三网络不稳定导致 TTS 请求失败问题在较差的网络环境下向 ElevenLabs API 发起的 POST 请求可能会超时或失败导致某段文本没有语音破坏了连续性。解决实现简单的重试机制和降级处理。重试对于失败的 TTS 请求最多重试 2 次每次间隔指数递增1秒2秒。降级如果重试后仍失败则将这段文本记录到日志并在 UI 上给出一个轻微的视觉提示比如文本颜色变淡或旁边显示一个静音图标告知用户此段语音缺失。同时继续处理后续的文本避免整个语音流程中断。离线缓存高级对于常见的、固定的提示语或命令反馈如“正在思考”、“操作完成”可以预先合成好音频文件存储在本地。当网络不佳或需要极低延迟反馈时直接播放本地缓存。5.4 坑四多语言混合文本的支持问题我的 AI 工具有时会处理包含中文、英文、甚至日文术语的混合文本。ElevenLabs 的单一模型在处理这种混合文本时发音可能会非常奇怪尤其是中文字符会被逐个读成英文字母。解决这是一个尚未完美解决的挑战。目前的策略是文本检测与分割使用简单的正则或语言检测库如franc将文本按语言片段大致分割。分语言合成如果支持ElevenLabs 有支持多语言的模型如eleven_multilingual_v1但需要指定目标语言。对于明确的中文段落可以尝试用该模型合成但音色可能与英文部分不统一。降级处理目前更实用的做法是在预处理阶段将明显的非英文字符如中日韩文字替换为其英文描述或直接跳过。例如将“请打开config.yaml文件”中的中文部分“请打开”在发送给 TTS 前移除只合成“Open theconfig.yamlfile”。这需要权衡信息丢失和语音可懂度。6. 效果评估与未来扩展方向集成 ElevenLabs 语音层后我的 AI 工作工具的交互体验有了质的提升。最明显的感受是在进行长时间、复杂任务时如调试一段错误、阅读长篇文档摘要听觉通道的加入显著降低了认知疲劳。我可以一边听它分析一边做其他事情或者更专注地思考它提出的问题。从技术指标看在良好网络下从 AI 输出第一个词到听到对应语音延迟可以控制在 1-2 秒以内对于非实时对话场景完全可以接受。语音的自然度很高长时间聆听也不会觉得烦躁。可能的扩展方向语音输入STT闭环目前只有“文本-语音”的输出。自然的下一步是加入“语音-文本”的输入实现全语音交互。可以利用浏览器的 Web Speech API识别精度一般或集成如 Whisper、Google Speech-to-Text 等更专业的服务。上下文感知语音让语音语调根据 AI 回复的内容情感自动调整。例如当 AI 输出“错误”或“警告”时使用更急促、强调的语气当输出“成功”或“完成”时使用更轻松、愉悦的语气。这需要从 AI 回复中提取情感关键词并动态调整 ElevenLabs 的voice_settings甚至切换不同的预置声音。离线 TTS 引擎备用完全依赖在线 API 存在服务依赖和隐私顾虑。可以集成一个本地的、轻量级的 TTS 引擎如 Coqui TTS 或系统 TTS作为备用选项当网络不可用或用户选择隐私模式时自动切换。自定义语音克隆利用 ElevenLabs 的语音克隆功能训练一个属于自己的、独一无二的助手声音让协作感更强。但这需要提供高质量的录音样本并考虑相关的伦理和隐私问题。这个项目让我深刻体会到一个看似简单的“加个语音”功能背后涉及到前后端协同、流式处理、音频工程、用户体验设计等多个层面的考量。每一步的优化都让这个工具离“智能工作伙伴”的愿景更近了一步。如果你也在构建类似的 AI 应用不妨从最基础的句子级流式合成开始亲自体验一下“能听会说”带来的不同。