马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
×
做嵌入式智能装备、离线语音音箱、单片机交互项目标朋侪,大概率都绕不开语音合成 TTS。之前做一个AI语音对话呆板人时间,利用的是讯飞云TTS模子,感觉非常慢,于是换到了火山引擎豆包语音模子上,快了不少,本日突发奇想没对比一下两个模子,拿ESP32
不掺水分、不吹不黑,全程真实跑分,本日把实测结果和选型干货一次性分享给各人。
一、测试情况和驱动代码
测试情况如下:
- 硬件:ESP32 主控 + I2S 音频输出
- 运行情况:MicroPython v1.27.0
- 采样规格:同一 16000Hz 单声道 PCM
- 测试模式:同时跑流式及时播放、非流式整段缓存两种商用常用模式
- 核心观测指标:首包延长 TTFB、帧播放流通度、网络净延长、握手耗时、合成服从、运行乐成率
驱动包在upypi上:
二、测试用关键指标
代码中界说的关键指标直接反映了 TTS 服务的体验与性能,需先明白其寄义:
指标寄义(联合测试逻辑)体验影响均匀首包延长(TTFB)从哀求发起到收到第一个音频包的时间(流式体验的核心)决定用户听到声音的等待时间,>1000ms 会有显着延长感均匀净延长总耗时 - 音频播放时长(打扫音频自己的时间,反映网络 + 合成的额外开销)越低阐明合成 / 传输服从越高,流式模式下直接影响播放流通度均匀帧隔断相邻两个音频包的吸取隔断(反映服务端推包频率)越小越稳固,>300ms 易导致播放断音(需依靠 I2S 缓冲区增补)帧隔断标准差帧隔断的颠簸幅度越小阐明传输越稳固,无忽快忽慢的卡顿感合成速率(非流式)(总耗时 / 音频时长)×100%,反映合成 + 传输服从<100% 阐明合成速率快于播放速率,>150% 则合成服从偏低均匀字节率每秒吸取的音频字节数,反映传输服从越靠近理论码率(16kHz×16bit=32000 B/s),传输服从越高三、测试对比
3.1 最扎心对比:首包延长,直接拉开差距
做语音交互装备,首包延长 TTFB 就是生命线。用户按下按键,多久能听到声音,直接决定产物体验优劣,高出 1.5 秒就会有显着卡顿等待感。
实测下来差距特别显着:
- ✅ 火山引擎:均匀首包延长仅700 多毫秒,险些无感,开口即出音
- ❌ 讯飞 TTS:不管流式照旧非流式,首包延长稳固卡在1500ms 以上,足足慢了一倍
这也是很多自研智能装备用讯飞后,总以为反应痴钝的核心缘故原由,不是代码写得差,是服务端自己相应就慢一截。
3.2 播放流通度:帧隔断稳固性高下立判
很多人忽略一个细节:TTS 不是一次性发完音频,是分帧一连推送。帧隔断越小、颠簸越低,播放越顺滑,不会出现断断续续、忽快忽慢的破音标题。
实测数据很直观:
- 火山引擎均匀帧隔断仅 122ms,颠簸标准差极低,推包节奏特别稳
- 讯飞均匀帧隔断高达 330ms 以上,隔断颠簸大,嵌入式弱缓冲区下很容易断音卡顿
尤其是做长文本播报、一连对话场景,火山的流通度上风直接拉满,讯飞在帧推送节奏上显着落后。
3.3 合成 & 传输服从:非流式模式差距更大
非流式适当提前缓存语音、当地播报的场景,拼的是合成速率 + 传输服从。
测试结果太真实:
- 火山引擎合成速率仅 116%,合成速率险些追上音频播放速率,服从拉满
- 讯飞合成速率高达 158%,意味着要多花近 60% 的时间才华完成整段合成
同时字节传输速率上,火山也远超讯飞,更贴近 16kHz 音频理论码率,网络带宽利用率更高,在弱网情况下容错性更好。
四、对比结果具体数据
4.1 流式模式(XF-S vs VC-S)
指标讯飞(XF-S)火山(VC-S)差异分析均匀总耗时11910ms11086ms火山快 7%,重要源于首包延长与帧隔断的优化均匀首包延长(TTFB)1509ms798ms火山直接砍半!讯飞 >1500ms 的延长会让用户产生显着等待感,火山 <800ms 险些无感知均匀净延长6368ms5677ms火山低 11%,合成 + 传输的额外开销更低均匀帧隔断339ms122ms火山快 64%,服务端推包频率更高,共同 I2S 缓冲区更难断音帧隔断标准差121ms57ms火山颠簸小 53%,播放流通度明显优于讯飞最大帧隔断584ms244ms火山断音风险低落 58%,非常情况下的稳固性更强均匀字节率14884 B/s15610 B/s火山略高,传输服从差异不大核心结论:火山流式 TTS 在首包延长、帧稳固性上全面碾压讯飞,用户体验差距明显。
4.2 非流式模式(XF-N vs VC-N)
指标讯飞(XF-N)火山(VC-N)差异分析均匀总耗时8774ms6341ms火山快 27%,合成 + 传输服从差距拉大均匀首包延长(TTFB)1503ms774ms火山仍快一倍,讯飞的首包相应速率在非流式下无改善均匀净延长3233ms932ms火山低 71%!讯飞的额外开销是火山的 3.5 倍,合成服从严肃偏低均匀帧隔断321ms122ms火山仍保持高频推包,讯飞的包隔断颠簸依然显着帧隔断标准差113ms55ms火山稳固性上风一连保持均匀字节率20313 B/s28113 B/s火山高 38%,传输服从靠近理论码率(32000 B/s),讯飞仅达理论值的 63%合成速率158%116%火山仅需音频时长的 1.16 倍即可完成合成,讯飞需 1.58 倍,服从差距巨大核心结论:火山非流式 TTS 服从靠近 “及时合成”,讯飞则必要额外 58% 的时间才华完成,在缓存 / 离线场景下火山上风碾压。
4.3 纵向对比:流式 vs 非流式(同厂商下)
4.3.1 讯飞(XF-S vs XF-N)
- 非流式总耗时(8774ms)比流式(11910ms)快 26%,重要由于流式需边收边播放,受 I2S 写入与缓冲区等待影响
- 首包延长两者无差异(≈1500ms),阐明讯飞的服务端相应速率与模式无关,是共性瓶颈
- 净延长非流式(3233ms)比流式(6368ms)低一倍,由于非流式无播放同步开销,可一次性吸取全部包
- 字节率非流式(20313 B/s)比流式(14884 B/s)高 36%,一次性传输服从高于边收边播
结论:讯飞的非流式模式更适当缓存场景,流式模式的体验受限于首包延长与推包频率。
4.3.2 火山(VC-S vs VC-N)
- 非流式总耗时(6341ms)比流式(11086ms)快 43%,提拔幅度宏大于讯飞,阐明火山的流式模式受播放同步影响更大
- 首包延长两者均 <800ms,无显着差异,服务端相应速率不受模式影响
- 净延长非流式(932ms)比流式(5677ms)低 83%,流式模式的播放等待开销(ibuf_ms + 200)被放大
- 字节率非流式(28113 B/s)比流式(15610 B/s)高 80%,一次性传输服从靠近理论码率
结论:火山的非流式模式服从极高,流式模式虽受播放开销影响,但核心的首包延长与推包稳固性依然良好。
五、总结
- 寻求低延长、高流通度、弱网适配:闭眼选火山引擎 TTS,全维度碾压
- 有生态兼容、业务欺压要求:只能生存讯飞,需做好延长高、帧卡顿的适配优化
- 流式适当及时交互,非流式适当缓存离线,火山两种模式体现都远优于讯飞
六、讯飞 TTS vs 火山引擎 TTS 性能差异深度缘故原由分析
前置阐明:TTS 引擎与声音模子层面分析仅为基于实测体现的公道推测,无官方架构佐证;协议、认证、网络基建部门完全基于代码实现、测试数据与公开技能特性实证分析。
分析严格从协议计划、服务端架构、网络根本办法三大维度睁开,匹配本次 40 条语料 Benchmark 实测数据。
6.1 协议计划差异
6.1.1 技能实现情势
- 讯飞云:接纳WebSocket 文本协议,音频数据嵌套在 JSON 布局体中,音频 PCM 数据额外做 Base64 编码
- 传输链路:WebSocket 文本帧 → JSON 字符串分析 → Base64 解码 → 原始 PCM 音频
- 火山引擎:接纳 WebSocket 自界说二进制协议
- 传输链路:WebSocket 二进制帧 → 4 字节定长帧头分析 → 直接读取原始 PCM 字节流
6.1.2 额外开销对比
额外开销项讯飞云火山引擎对测试数据的直接影响传输体积Base64 编码自然膨胀33%,数据包更大原生二进制无编码膨胀,传输体积最小讯飞单帧传输耗时更长每帧分析开销必须实行 json.loads 全量分析 + Base64 解码仅分析 4 字节定长头部,无复杂解码单帧处理惩罚耗时多出 130ms+帧布局复杂度JSON 动态字段,分析逻辑繁琐固定二进制帧格式,分析极简帧隔断颠簸更大、标准差更高6.1.3 数据印证
实测均匀帧隔断:讯飞 339ms,火山 122ms;帧隔断标准差:讯飞 121ms,火山 57ms。
本质缘故原由就是:讯飞每帧都负担 JSON 分析 + Base64 解码双重 CPU 开销,火山无冗余分析,原生二进制直传音频,帧推送与处理惩罚服从碾压。
6.2 认证机制 & 服务端架构差异
6.2.1 技能实现情势
- 讯飞云:接纳 URL 嵌入 HMAC-SHA256 动态署名
每次 WebSocket 握手都要客户端及时天生署名 URL,服务端需重新做哈希校验、鉴权盘算,运算开销大。
- 火山引擎:接纳 HTTP Header 静态 Bearer Token
Token 恒久有效,握手时仅做 Header 身份校验,无复杂加密运算,服务端鉴权逻辑轻量化。
数据印证:均匀握手耗时讯飞明显高于火山,单次握手多出约 130ms 固定延长,直接拉高首包延长基线。
6.2.2 服务端推包与引擎架构(模子层面仅为推测)
- 推包计谋火山服务端接纳小帧、高频次流式推送,碎片化及时下发音频包;讯飞接纳大帧、批量推送,需等待合成足量音频数据后才下发单帧,自然拉长首包等待时间与帧隔断。
- TTS 声音模子与合成架构(仅推测)火山 BV701_streaming 从定名可见原生为流式场景优化,神经 TTS 架构支持边文本分析、边及时合成、边分片推送;讯飞 x4_xiaoyan 为传统参数化 TTS 模子,架构偏整段文本合成,需攒足文本块再批量天生音频,无法做到极致流式输出。
数据印证:非流式合成速率讯飞 158%、火山 116%;讯飞合成耗时比音频实际时长多 58%,火山靠近及时合成,服务端合成服从差距显着。
6.3 网络根本办法差异
- 链路摆设讯飞域名 tts-api.xfyun.cn:依靠科大讯飞自建 IDC 机房,节点覆盖与调治本领有限;火山域名 openspeech.bytedance.com:复用字节跳动全域 CDN + 分布式机房,国内骨干网链路优化更完满。
- 网络体现影响字节网络基建规模、链路冗余、跨网调治本领远优于讯飞,实测网络净延长火山远低于讯飞,传输抖动更小、RTT 更低;同时火山均匀字节率 28113B/s,靠近 16kHz 理论码率,讯飞仅 20313B/s,网络传输利用率差距明显。
6.4 核心性能差距量化归因
- 首包耽毛病距(差值约 710ms)
- 认证握手开销:贡献约 130ms
- 协议分析冗余:贡献约 170ms
- 服务端合成 + 推包计谋:贡献约 410ms
- 帧隔断差距(差值约 217ms)
- Base64 解码单帧固定开销:约 50ms
- JSON 布局体分析开销:约 80ms
- 服务端大帧批量下发计谋:剩余 87ms
完备源代码
完备源代码如下所示:- # Python env : MicroPython v1.23.0
- # -*- coding: utf-8 -*-
- # @Time : 2026/05/14
- # @Author : leeqingsui
- # @File : benchmark_tts_full.py
- # @Description : 讯飞 vs 火山引擎 TTS 全面性能测试(40条语料,流式+非流式,多指标)
- import sys
- sys.modules.pop('volcengine_tts_v1_ws', None)
- sys.modules.pop('xfyun_tts', None)
- import asyncio
- import network
- import time
- import ntptime
- import gc
- from machine import I2S, Pin
- # ======================================== 全局变量 ============================================
- WIFI_SSID = ""
- WIFI_PASS = ""
- XF_APPID = ""
- XF_KEY = ""
- XF_SECRET = ""
- XF_RATE = 16000
- VOLC_APP_ID = ""
- VOLC_ACCESS_TOKEN = ""
- VOLC_RATE = 16000
- VOLC_SPEED = 1.45
- TEST_CORPUS = [
- "今天天气真不错,阳光明媚,微风轻拂,非常适合出门散步。",
- "你好,请问有什么可以帮助你的吗?我会尽力为你解答。",
- "晚上想吃什么呢?我们可以去吃火锅,也可以点外卖。",
- "周末打算去哪里玩?听说最近有个新开的游乐园很不错。",
- "这个问题确实比较复杂,我需要仔细思考一下才能回答。",
- "谢谢你的帮助,如果没有你,我可能无法完成这个任务。",
- "明天早上八点我们在公司门口集合,记得不要迟到哦。",
- "最近工作压力有点大,需要找个时间好好放松一下。",
- "你看过那部电影吗?评价很高,我打算这周末去看。",
- "这家餐厅的菜品味道不错,价格也比较实惠,值得推荐。",
- "据最新消息,本市将在下周启动新一轮的城市绿化工程。",
- "今日股市收盘,上证指数上涨百分之零点五,创业板指数下跌。",
- "气象台发布暴雨预警,提醒市民注意防范强降雨天气。",
- "教育部宣布将进一步推进义务教育均衡发展政策落实。",
- "科技公司发布最新产品,搭载人工智能芯片性能提升显著。",
- "交通部门提示,节假日期间高速公路将实行免费通行政策。",
- "卫生部门呼吁市民积极参与疫苗接种,共同构筑免疫屏障。",
- "文化部将举办传统文化展览,展示中华五千年文明成果。",
- "环保部门加强监管力度,严厉打击违法排污企业行为。",
- "体育赛事精彩纷呈,国家队在国际比赛中取得优异成绩。",
- "人工智能技术正在快速发展,语音合成已经非常自然流畅。",
- "这个算法的时间复杂度是O(n log n),空间复杂度是O(n)。",
- "系统采用分布式架构设计,可以支持高并发访问请求。",
- "数据库优化后查询速度提升了百分之五十,性能显著改善。",
- "前端框架使用React开发,后端采用Node.js构建服务。",
- "云计算平台提供弹性扩展能力,可根据负载自动调整资源。",
- "机器学习模型训练完成后,准确率达到百分之九十五以上。",
- "网络安全防护系统实时监控
异常流量,及时拦截攻击行为。", - "移动应用支持iOS和Android双平台,用户体验良好。",
- "区块链技术保证数据不可篡改,提高了系统的可信度。",
- "您的订单已经发货,预计三到五个工作日内送达。",
- "感谢您的反馈,我们会尽快处理您提出的问题。",
- "为了更好地为您服务,请您提供订单号以便查询。",
- "您可以通过官方网站或者手机应用查看订单详情。",
- "退换货政策请参考购买页面的相关说明,感谢理解。",
- "我们的营业时间是每天上午九点到晚上六点。",
- "如需人工服务,请按一;查询订单,请按二。",
- "感谢您的耐心等待,我们会尽快为您解决问题。",
- ]
- # ======================================== 功能函数 ============================================
- def connect_wifi():
- sta = network.WLAN(network.STA_IF)
- if not sta.isconnected():
- sta.active(True)
- sta.connect(WIFI_SSID, WIFI_PASS)
- for _ in range(20):
- if sta.isconnected():
- break
- time.sleep(0.5)
- print("WiFi:", sta.ifconfig()[0])
- try:
- ntptime.settime()
- print("NTP synced")
- except:
- pass
- def calc_stats(times):
- if not times:
- return 0, 0, 0, 0
- n = len(times)
- mean = sum(times) // n
- if n == 1:
- return mean, 0, times[0], times[0]
- variance = sum((t - mean) ** 2 for t in times) // n
- return mean, int(variance ** 0.5), max(times), min(times)
- def _make_result(t_start, t_end, t_first_frame, t_after_handshake, t_before_handshake,
- total_bytes, frame_times, frame_count, rate, streaming):
- handshake_ms = time.ticks_diff(t_after_handshake, t_before_handshake)
- ttfb_ms = time.ticks_diff(t_first_frame, t_start) if t_first_frame else 0
- total_ms = time.ticks_diff(t_end, t_start)
- audio_ms = total_bytes * 1000 // (rate * 2) if total_bytes else 0
- net_delay_ms = total_ms - audio_ms
- frame_mean, frame_std, frame_max, frame_min = calc_stats(frame_times)
- synth_ratio = total_ms * 100 // audio_ms if (not streaming and audio_ms > 0) else 0
- return {
- "total_ms": total_ms,
- "audio_ms": audio_ms,
- "net_delay_ms": net_delay_ms,
- "handshake_ms": handshake_ms,
- "ttfb_ms": ttfb_ms,
- "total_bytes": total_bytes,
- "frame_count": frame_count,
- "frame_mean_ms":frame_mean,
- "frame_std_ms": frame_std,
- "frame_max_ms": frame_max,
- "frame_min_ms": frame_min,
- "bps": total_bytes * 1000 // total_ms if total_ms > 0 else 0,
- "synth_ratio": synth_ratio,
- }
- async def bench_xf_stream(tts_xf, text, audio_out, amp_sd):
- from xfyun_tts import _WsClient
- import json, binascii
- t_start = time.ticks_ms()
- url = tts_xf._build_auth_url()
- try:
- await tts_xf._ws.close()
- except:
- pass
- tts_xf._ws = _WsClient(ms_delay_for_read=5)
- t0h = time.ticks_ms()
- try:
- await asyncio.wait_for(tts_xf._ws.handshake(url, cert_reqs=0), 10)
- except:
- return None
- t1h = time.ticks_ms()
- await tts_xf._ws.send(tts_xf._build_request(text))
- amp_sd.value(1)
- total_bytes = 0; frame_count = 0; frame_times = []; t_first = None
- swriter = asyncio.StreamWriter(audio_out)
- try:
- while await tts_xf._ws.open():
- t_br = time.ticks_ms()
- msg = await asyncio.wait_for(tts_xf._ws.recv(), 10)
- if msg is None: break
- try: resp = json.loads(msg)
- except: break
- if resp.get("code", -1) != 0: break
- sec = resp.get("data", {})
- b64 = sec.get("audio", "")
- if b64:
- t_ar = time.ticks_ms()
- chunk = binascii.a2b_base64(b64)
- swriter.write(chunk); await swriter.drain()
- total_bytes += len(chunk); frame_count += 1
- if t_first is None: t_first = t_ar
- else: frame_times.append(time.ticks_diff(t_ar, t_br))
- if sec.get("status", 0) == 2: break
- finally:
- pass
- await tts_xf._ws.close()
- ibuf_ms = total_bytes * 1000 // (XF_RATE * 2)
- await asyncio.sleep_ms(ibuf_ms + 200)
- amp_sd.value(0)
- await asyncio.sleep_ms(300)
- return _make_result(t_start, time.ticks_ms(), t_first, t1h, t0h,
- total_bytes, frame_times, frame_count, XF_RATE, True)
- async def bench_xf_nonstream(tts_xf, text):
- from xfyun_tts import _WsClient
- import json, binascii
- t_start = time.ticks_ms()
- url = tts_xf._build_auth_url()
- try:
- await tts_xf._ws.close()
- except:
- pass
- tts_xf._ws = _WsClient(ms_delay_for_read=5)
- t0h = time.ticks_ms()
- try:
- await asyncio.wait_for(tts_xf._ws.handshake(url, cert_reqs=0), 10)
- except:
- return None
- t1h = time.ticks_ms()
- await tts_xf._ws.send(tts_xf._build_request(text))
- total_bytes = 0; frame_count = 0; frame_times = []; t_first = None
- f = open("xf_ns.pcm", "wb")
- try:
- while await tts_xf._ws.open():
- t_br = time.ticks_ms()
- msg = await asyncio.wait_for(tts_xf._ws.recv(), 10)
- if msg is None: break
- try: resp = json.loads(msg)
- except: break
- if resp.get("code", -1) != 0: break
- sec = resp.get("data", {})
- b64 = sec.get("audio", "")
- if b64:
- t_ar = time.ticks_ms()
- chunk = binascii.a2b_base64(b64)
- f.write(chunk)
- total_bytes += len(chunk); frame_count += 1
- if t_first is None: t_first = t_ar
- else: frame_times.append(time.ticks_diff(t_ar, t_br))
- if sec.get("status", 0) == 2: break
- finally:
- f.close()
- await tts_xf._ws.close()
- return _make_result(t_start, time.ticks_ms(), t_first, t1h, t0h,
- total_bytes, frame_times, frame_count, XF_RATE, False)
- async def bench_vc_stream(tts_vc, text, audio_out, amp_sd):
- from async_websocketclient import AsyncWebsocketClient
- from volcengine_tts_v1_ws import _gen_uuid
- t_start = time.ticks_ms()
- reqid = _gen_uuid()
- payload = {
- "app": {"appid": tts_vc._app_id, "token": "access_token", "cluster": "volcano_tts"},
- "user": {"uid": "u_" + reqid[:8]},
- "audio": {"voice_type": tts_vc._default_voice_type, "encoding": "pcm",
- "speed_ratio": VOLC_SPEED, "volume_ratio": tts_vc._default_volume,
- "pitch_ratio": tts_vc._default_pitch, "sample_rate": VOLC_RATE,
- "language": tts_vc._default_language},
- "request": {"reqid": reqid, "text": text, "text_type": "plain", "operation": "submit"},
- }
- ws = AsyncWebsocketClient(ms_delay_for_read=5)
- headers = [("Authorization", "Bearer; {}".format(tts_vc._access_token))]
- t0h = time.ticks_ms()
- try:
- await ws.handshake(tts_vc._WS_URL, headers=headers, cert_reqs=0)
- except:
- return None
- t1h = time.ticks_ms()
- await ws.send(tts_vc._build_frame(payload))
- amp_sd.value(1)
- total_bytes = 0; frame_count = 0; frame_times = []; t_first = None
- swriter = asyncio.StreamWriter(audio_out)
- try:
- while await ws.open():
- t_br = time.ticks_ms()
- data = await ws.recv()
- if data is None: break
- msg_type, sequence, content = tts_vc._parse_response(data)
- if msg_type == tts_vc._MSG_TYPE_ERROR: break
- if msg_type == tts_vc._MSG_TYPE_AUDIO_ONLY and content:
- t_ar = time.ticks_ms()
- swriter.write(content); await swriter.drain()
- total_bytes += len(content); frame_count += 1
- if t_first is None: t_first = t_ar
- else: frame_times.append(time.ticks_diff(t_ar, t_br))
- if msg_type == tts_vc._MSG_TYPE_AUDIO_ONLY and sequence < 0: break
- finally:
- await ws.close()
- ibuf_ms = total_bytes * 1000 // (VOLC_RATE * 2)
- await asyncio.sleep_ms(ibuf_ms + 200)
- amp_sd.value(0)
- await asyncio.sleep_ms(300)
- return _make_result(t_start, time.ticks_ms(), t_first, t1h, t0h,
- total_bytes, frame_times, frame_count, VOLC_RATE, True)
- async def bench_vc_nonstream(tts_vc, text):
- from async_websocketclient import AsyncWebsocketClient
- from volcengine_tts_v1_ws import _gen_uuid
- t_start = time.ticks_ms()
- reqid = _gen_uuid()
- payload = {
- "app": {"appid": tts_vc._app_id, "token": "access_token", "cluster": "volcano_tts"},
- "user": {"uid": "u_" + reqid[:8]},
- "audio": {"voice_type": tts_vc._default_voice_type, "encoding": "pcm",
- "speed_ratio": VOLC_SPEED, "volume_ratio": tts_vc._default_volume,
- "pitch_ratio": tts_vc._default_pitch, "sample_rate": VOLC_RATE,
- "language": tts_vc._default_language},
- "request": {"reqid": reqid, "text": text, "text_type": "plain", "operation": "submit"},
- }
- ws = AsyncWebsocketClient(ms_delay_for_read=5)
- headers = [("Authorization", "Bearer; {}".format(tts_vc._access_token))]
- t0h = time.ticks_ms()
- try:
- await ws.handshake(tts_vc._WS_URL, headers=headers, cert_reqs=0)
- except:
- return None
- t1h = time.ticks_ms()
- await ws.send(tts_vc._build_frame(payload))
- total_bytes = 0; frame_count = 0; frame_times = []; t_first = None
- f = open("vc_ns.pcm", "wb")
- try:
- while await ws.open():
- t_br = time.ticks_ms()
- data = await ws.recv()
- if data is None: break
- msg_type, sequence, content = tts_vc._parse_response(data)
- if msg_type == tts_vc._MSG_TYPE_ERROR: break
- if msg_type == tts_vc._MSG_TYPE_AUDIO_ONLY and content:
- t_ar = time.ticks_ms()
- f.write(content)
- total_bytes += len(content); frame_count += 1
- if t_first is None: t_first = t_ar
- else: frame_times.append(time.ticks_diff(t_ar, t_br))
- if msg_type == tts_vc._MSG_TYPE_AUDIO_ONLY and sequence < 0: break
- finally:
- f.close()
- await ws.close()
- return _make_result(t_start, time.ticks_ms(), t_first, t1h, t0h,
- total_bytes, frame_times, frame_count, VOLC_RATE, False)
- def _avg(results, key):
- v = [r[key] for r in results if r]
- return sum(v) // len(v) if v else 0
- def print_summary(label, results, n_total, streaming):
- n = len(results)
- print("\n" + "="*62)
- print(" {} ({}/{} samples)".format(label, n, n_total))
- print("="*62)
- if not results:
- print(" No data")
- return
- fmt = " {:<22}: {:>8}"
- print(fmt.format("平均总耗时", "{} ms".format(_avg(results, "total_ms"))))
- print(fmt.format("平均首包延迟", "{} ms".format(_avg(results, "ttfb_ms"))))
- print(fmt.format("平均净延迟", "{} ms".format(_avg(results, "net_delay_ms"))))
- print(fmt.format("平均握手耗时", "{} ms".format(_avg(results, "handshake_ms"))))
- print(fmt.format("平均帧间隔", "{} ms".format(_avg(results, "frame_mean_ms"))))
- print(fmt.format("帧间隔标准差", "{} ms".format(_avg(results, "frame_std_ms"))))
- print(fmt.format("最大帧间隔(avg)", "{} ms".format(_avg(results, "frame_max_ms"))))
- print(fmt.format("平均字节率", "{} B/s".format(_avg(results, "bps"))))
- if not streaming:
- print(fmt.format("合成速率(avg)", "{}%".format(_avg(results, "synth_ratio"))))
- print(fmt.format("成功率", "{}%".format(n * 100 // n_total)))
- def print_compare_table(label, r_a, name_a, r_b, name_b, n_total):
- n = len(TEST_CORPUS)
- print("\n" + "="*62)
- print(" {}".format(label))
- print("="*62)
- fmt = " {:<22}: {:>10} {:>10}"
- print(fmt.format("指标", name_a, name_b))
- print("-"*62)
- keys = [
- ("平均总耗时(ms)", "total_ms"),
- ("平均首包延迟(ms)", "ttfb_ms"),
- ("平均净延迟(ms)", "net_delay_ms"),
- ("平均握手耗时(ms)", "handshake_ms"),
- ("平均帧间隔(ms)", "frame_mean_ms"),
- ("帧间隔标准差(ms)", "frame_std_ms"),
- ("最大帧间隔avg(ms)", "frame_max_ms"),
- ("平均字节率(B/s)", "bps"),
- ]
- for name, key in keys:
- va = _avg(r_a, key) if r_a else 0
- vb = _avg(r_b, key) if r_b else 0
- print(fmt.format(name, va, vb))
- print(fmt.format("成功率(%)",
- "{}%".format(len(r_a)*100//n),
- "{}%".format(len(r_b)*100//n)))
- # ======================================== 初始化配置 ==========================================
- time.sleep(3)
- print("=== TTS Full Benchmark v2 (40 samples, 4 modes) ===")
- connect_wifi()
- amp_sd = Pin(17, Pin.OUT, value=0)
- audio_out = I2S(
- 1,
- sck=Pin(14), ws=Pin(15), sd=Pin(16),
- mode=I2S.TX, bits=16, format=I2S.MONO,
- rate=XF_RATE, ibuf=40000,
- )
- from xfyun_tts import XfyunTTS
- from volcengine_tts_v1_ws import VolcengineTTSV1WS
- tts_xf = XfyunTTS(
- app_id=XF_APPID, api_key=XF_KEY, api_secret=XF_SECRET,
- vcn="x4_xiaoyan", aue="raw", auf="audio/L16;rate=16000",
- )
- tts_vc = VolcengineTTSV1WS(
- app_id=VOLC_APP_ID, access_token=VOLC_ACCESS_TOKEN,
- voice_type=VolcengineTTSV1WS.VOICE_BV701_STREAMING,
- volume=1.0, speed=VOLC_SPEED, pitch=1.0,
- )
- # ======================================== 主程序 ===========================================
- async def main():
- xf_s = []; xf_n = []; vc_s = []; vc_n = []
- for i, text in enumerate(TEST_CORPUS):
- print("\n[{}/{}] {}...".format(i+1, len(TEST_CORPUS), text[:15]))
- r = await bench_xf_stream(tts_xf, text, audio_out, amp_sd)
- if r: xf_s.append(r); print(" XF-S: {}ms TTFB:{}ms frames:{}".format(r["total_ms"], r["ttfb_ms"], r["frame_count"]))
- else: print(" XF-S: FAILED")
- await asyncio.sleep_ms(500); gc.collect()
- r = await bench_xf_nonstream(tts_xf, text)
- if r: xf_n.append(r); print(" XF-N: {}ms TTFB:{}ms ratio:{}%".format(r["total_ms"], r["ttfb_ms"], r["synth_ratio"]))
- else: print(" XF-N: FAILED")
- await asyncio.sleep_ms(500); gc.collect()
- r = await bench_vc_stream(tts_vc, text, audio_out, amp_sd)
- if r: vc_s.append(r); print(" VC-S: {}ms TTFB:{}ms frames:{}".format(r["total_ms"], r["ttfb_ms"], r["frame_count"]))
- else: print(" VC-S: FAILED")
- await asyncio.sleep_ms(500); gc.collect()
- r = await bench_vc_nonstream(tts_vc, text)
- if r: vc_n.append(r); print(" VC-N: {}ms TTFB:{}ms ratio:{}%".format(r["total_ms"], r["ttfb_ms"], r["synth_ratio"]))
- else: print(" VC-N: FAILED")
- await asyncio.sleep_ms(500); gc.collect()
- n = len(TEST_CORPUS)
- # 各模式独立汇总
- print_summary("讯飞 流式 (XF-S)", xf_s, n, True)
- print_summary("讯飞 非流式(XF-N)", xf_n, n, False)
- print_summary("火山 流式 (VC-S)", vc_s, n, True)
- print_summary("火山 非流式(VC-N)", vc_n, n, False)
- # 对比表1:两模型流式对比
- print_compare_table("两模型流式对比 XF-S vs VC-S", xf_s, "XF-S", vc_s, "VC-S", n)
- # 对比表2:两模型非流式对比
- print_compare_table("两模型非流式对比 XF-N vs VC-N", xf_n, "XF-N", vc_n, "VC-N", n)
- # 对比表3:讯飞流式 vs 非流式
- print_compare_table("讯飞 流式 vs 非流式 XF-S vs XF-N", xf_s, "XF-S", xf_n, "XF-N", n)
- # 对比表4:火山引擎流式 vs 非流式
- print_compare_table("火山 流式 vs 非流式 VC-S vs VC-N", vc_s, "VC-S", vc_n, "VC-N", n)
- audio_out.deinit()
- print("\n=== Benchmark done ===")
- try:
- asyncio.run(main())
- except KeyboardInterrupt:
- print("Interrupted")
- except Exception as e:
- print("Error:", e)
- import sys; sys.print_exception(e)
- finally:
- audio_out.deinit()
- print("Exited")
复制代码 运行前必要先用下面指令安装驱动包:- mpremote mip install https://upypi.net/pkgs/volcengine_tts_v1_ws/1.0.0
- mpremote mip install https://upypi.net/pkgs/xfyun_tts/1.1.0
复制代码
|