web.edgeTextToSpeech 是一个只负责合成的 TTS 适配器。它提交一次完整文本,按顺序交付服务端返回的 MP3 字节,不负责播放、存文件或管理全局文本队列。
web.socket.client + dotNet.sslTunnel.client。web.SocketSharp、BASS 或浏览器播放器。task.wait()。import web.edgeTextToSpeech;
import sys.mediaFoundation.streamingMp3Player;
var player,err = sys.mediaFoundation.streamingMp3Player();
if(!player) return print(err);
var engine = web.edgeTextToSpeech({voice="zh-CN-XiaoxiaoNeural";rate=1;});
var task,err = engine.synthesize({text="这是一段流式语音,不必等完整音频才开始播放。";}, player);
if(!task){player.close();return print(err.message);}
var ok,err = player.wait(120000); // 合成失败也会传给播放器
if(!ok) print(err);
task.cancel();
player.close();
播放器直接作为第二个参数:收到的音频交给 player.write,合成成功后调用 player.finish(),失败时调用 player.finish(错误信息)。finish() 只表示没有更多输入,剩余声音继续播放;合成结束与播放结束是两件不同的事。
sys.mediaFoundation.streamingMp3Player 是 Windows 自带解码器实现的原生播放器,免费、不需要附带 DLL。
GUI 中通常不用 wait():保留 task 与 player,写一个 stop() 依次调用 task.cancel()、player.close(),停止按钮、窗口关闭、播放结束和出错都调用它。取消、关闭之后不会再有任何回调,不需要额外的状态变量。完整示例见 ~/examples/Media/Audio/edgeTTS-mediaFoundation.aardio。
synthesize 的第二个参数可以是任何带 write、finish 方法的对象:
| 情况 | 引擎的调用 |
|---|---|
| 收到音频 | output.write(data)。返回 false,err 或 null,err 表示拒绝,合成以 consumer_error 终止 |
| 合成成功 | output.finish() |
| 合成失败 | output.finish(err.message) |
调用 task.cancel() |
不调用 output,由调用者自己 close |
| 参数错误 | synthesize 返回 null,err,不调用 output |
可以直接传入:
sys.mediaFoundation.streamingMp3Player:原生边收边播;wsock.tcp.httpStreamMp3:把音频提供为本机 MP3 URL;bass.streamPlayer;edgeTTS-webView.aardio。输出对象是回调表的便捷写法。业务代码最少只依赖:
engine.synthesize(request, handlers) -> task, err
handlers.onAudio(data, format)
handlers.onBoundary(info) // 可选
handlers.onFinish(result, err)
task.cancel()
task.wait(timeoutMs) // 可选等待封装
engine.voices(locale) // 可选:列出音色,每项至少有 id、locale、gender
传入输出对象,等价于传入下面的回调表。只支持回调表的其他引擎,用同样几行代码就能接收输出对象:
var toHandlers = function(output){
return {
onAudio = function(data){ return output.write(data); };
onFinish = function(result,err){
if(!err) output.finish();
elseif(err.code!=="cancelled") output.finish(err.message);
};
};
}
替换供应商时,调整引擎创建代码、音色配置及需要使用的能力。保存和播放代码不需要知道 WebSocket、SSML、鉴权参数或 .NET 类型。
构造器 web.edgeTextToSpeech(config) 不联网。config 提供默认值,synthesize 的请求字段覆盖这些默认值。
| 字段 | 含义 |
|---|---|
text |
必填,UTF-8 文本,非 SSML |
voice |
音色 ID,默认 zh-CN-XiaoxiaoNeural,由具体供应商定义 |
rate |
速度倍率,默认 1,允许 0.1~3 |
volume |
合成音量倍率,默认 1,允许 0~2;0 不会被默认值覆盖 |
format |
此适配器仅支持 "mp3" |
boundary |
"sentence"(默认)、"word"、"none" |
connectTimeoutMs |
连接时限,默认 10000 毫秒 |
receiveTimeoutMs |
收到响应之间的空闲时限,默认 30000 毫秒 |
timeoutMs |
整个任务时限,默认 120000 毫秒 |
providerOptions |
Edge 专用表,可设 pitch="+0Hz"、language="en-US" |
速度与音量会转换为整数百分比。providerOptions 是供应商相关参数,不应作为跨引擎能力假定。当前传输未提供 HTTP 代理接口,传入 proxy 会明确失败。
本适配器限制清理并 XML 转义后的文本不超过 4096 个 UTF-8 字节。这是保守的客户端策略,不是对服务端硬限制的声明。不会自动切片、改写句子、去重或将多个独立 MP3 混成一个请求。
voice 决定是谁在说话:男声女声、播音腔还是活泼、普通话还是粤语或方言。engine.voices(locale) 联网取得服务端当前可用的音色(进程内缓存,第二次不联网;请求在工作线程执行,GUI 线程调用也不会卡住界面):
import web.edgeTextToSpeech;
var engine = web.edgeTextToSpeech();
var list,err = engine.voices("zh"); //按地区前缀筛选,忽略大小写;省略则返回全部(约 300 个)
for(i,v in list) print(v.id,v.gender,v.locale,v.localeName,string.join(v.personalities,"/"));
每项是 {id;locale;localeName;gender;name;categories;personalities},id 就是 synthesize 的 voice,gender 为小写的 "female"/"male"。写错音色 ID 时合成以 Unsupported voice 失败,可以用列表核对。
2026 年实测可用的中文音色(标签来自服务端):
| voice | 性别 | 口音 | 风格标签 |
|---|---|---|---|
zh-CN-XiaoxiaoNeural |
女 | 普通话 | 温暖(新闻、小说) |
zh-CN-XiaoyiNeural |
女 | 普通话 | 活泼(卡通、小说) |
zh-CN-YunxiNeural |
男 | 普通话 | 活泼、阳光 |
zh-CN-YunjianNeural |
男 | 普通话 | 激情(体育) |
zh-CN-YunyangNeural |
男 | 普通话 | 专业、可靠(新闻播音) |
zh-CN-YunxiaNeural |
男 | 普通话 | 可爱(童声) |
zh-CN-liaoning-XiaobeiNeural |
女 | 东北话 | 幽默 |
zh-CN-shaanxi-XiaoniNeural |
女 | 陕西话 | 明亮 |
zh-HK-HiuGaaiNeural、zh-HK-HiuMaanNeural |
女 | 粤语 | 友好 |
zh-HK-WanLungNeural |
男 | 粤语 | 友好 |
zh-TW-HsiaoChenNeural、zh-TW-HsiaoYuNeural |
女 | 台湾普通话 | 友好 |
zh-TW-YunJheNeural |
男 | 台湾普通话 | 友好 |
同一段文字换音色时长会不同(实测同一句 5.1~6.5 秒,粤语最长),需要对齐画面时以实际音频为准;各音色的原始响度也相差约 5dB,混音时宜逐句归一化。
onAudio(data,format) 按收到的顺序调用:
data 是稳定的编码字节串,可由调用者保存;不是 PCM。format 包含 codec="mp3"、mimeType="audio/mpeg"、sampleRate=24000、channels=1、bitrate=48000。false,err 或 null,err 表示拒绝,任务以 consumer_error 终结。核心不会积累整段音频。慢消费者应使用有界缓冲,并在超限时明确失败(例如原生播放器的 maxPcmBytes)。
onBoundary(info) 的 info 包含:
type:"sentence" 或 "word";text:边界文本,XML 实体已还原;offset、duration:相对本次音频起点的秒数,不是原始的 100ns tick。一个服务器元数据消息里的多项边界会全部按序交付。
synthesize 立即返回 null,err,没有任务,也不会触发回调。synthesize 返回后开始,成功/失败/取消恰好触发一次 onFinish。onFinish(result,null);result 包含 id,format,audioBytes,audioChunks,firstAudioMs,elapsedMs,不包含完整 MP3。onFinish(null,err);错误包含 code,message,stage,partial,retryable,以及可选 httpStatus。partial=true 表示已经向消费者交付过音频;不能盲目重试,否则可能重复朗读。cancel() 幂等,首次取消返回 true,已终结返回 false。它不会删除消费者已收到的音频,也不会替播放器按停止键。cancel() 会在内部同步触发一次 onFinish(null,err),err.code 为 cancelled,可以在这里关闭并删除临时文件;在停止函数里调用 cancel() 时,onFinish 开头判断 cancelled 直接返回即可,不需要计数器。使用输出对象时,取消不会调用它。之后都不会再有任何回调。wait(timeoutMs) 的等待超时返回 wait_timeout,不取消任务。任务自己的网络/总时限则会终结任务。onFinish 自身抛错记录为 task.callbackError,不会再次通知结束。import web.edgeTextToSpeech;
var path = io.tmpname(,".mp3");
var file,err = io.file(path,"w+b");
if(!file){print(err);return;}
var task,err = web.edgeTextToSpeech().synthesize({text="保存音频,不绑定播放器。";}, {
onAudio = function(data){return file.write(data);};
onFinish = function(result,err){file.close();};
});
if(!task){file.close();io.remove(path);print(err.message);return;}
var result,err = task.wait();
if(result) print(path);
else {io.remove(path);print(err.message);}
文件由调用者创建和关闭。正式程序若要替换已有文件,应先写临时文件,合成成功后再提交,失败时保留已有文件。
~/examples/Media/Audio/edgeTTS-stream.aardio:最小流式播放,原生播放器。~/examples/Media/Audio/edgeTTS-mediaFoundation.aardio:带朗读、停止按钮的原生播放界面。~/examples/Media/Audio/edgeTTS-http.aardio:音频写入本机 HTTP URL,播放器只需打开地址。~/examples/Media/Audio/edgeTTS-webView.aardio:自定义输出对象,把音频转交给 WebView2 的 MediaSource 与 <audio>。~/examples/Media/Audio/edgeTTS.aardio:大声朗读/翻译界面,可停止、缓存后重播;界面使用原生播放器。网页示例使用 wb.invoke 异步转交音频,串行处理 SourceBuffer.appendBuffer,所有块进入缓冲后才 endOfStream()。播放器自行探测 audio/mpeg 支持,失败时不会假装成功。JS 反向取消 aardio 任务时使用异步调用,避免双向同步等待。
这里不假定传统 web.form 和 WebView2 具有相同的媒体能力。
import string.sentences;
var sentences = string.sentences("第一句。第二句。最后一句。");
for i,sentence in sentences {
print(i,sentence);
}
string.sentences 不需要 LLM。只有文本确实需要分段时才使用它;已经完整且符合长度策略的文本,优先一次提交并流式消费音频。
如果后续文本尚未产生,可以在上层对每句创建独立任务、有限预取并按序播放。每段使用独立播放器/流生命周期,不把独立 MP3 文件当作同一编码流随意拼接,也不要用文本内容作为队列的唯一键(重复句子是合法输入)。本库不再提供隐式全局队列。
旧的 voiceOptions、ConnectAsync、SetProxy、OnResponseEnd、pushText/popSpeech 已移除,不提供兼容层。
synthesize({text=...}, handlers)。"-10%" 改为 0.9。synthesize(request, player),收到音频就开始播放。~/lib/web/edgeTextToSpeech/_.aardio。不要再添加同名的平铺 .aardio 文件遮蔽它。protocol.aardio;task.aardio 负责异步生命周期。业务不应引用内部模块。