动态音源 (JS 脚本)
通过 JavaScript 脚本自定义 TTS 合成逻辑,支持 HTTP、WebSocket 等任意协议
动态音源允许您通过编写 JavaScript 脚本来实现完全自定义的 TTS 合成逻辑。相比自定义 HTTP 合成,动态音源支持多步骤请求、WebSocket 流式合成、认证签名等复杂场景。
快速开始
- 进入 设置 → TTS 引擎
- 点击右上角菜单 → 新建动态音源
- 填写名称,选择一个内置模板
- 根据需要修改脚本和配置项
- 保存,试听验证
脚本协议
脚本必须定义 synthesize(ctx) 函数,完成音频合成后将结果保存到 ctx.outputPath:
async function synthesize(ctx) {
// ctx.text - 待合成的文本
// ctx.voice - 语音标识
// ctx.rate - 语速 (0.5 ~ 2.0)
// ctx.config - 配置项
// ctx.outputPath - 音频保存路径
}可用函数
网络请求
$http(options) — 发起 HTTP 请求,获取文本响应
var resp = await $http({
url: 'https://api.example.com/token',
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ key: 'value' })
});
// resp.status - 状态码
// resp.body - 响应内容$download(options) — 下载音频文件
await $download({
url: 'https://api.example.com/tts',
method: 'POST',
headers: { 'Authorization': 'Bearer token' },
body: JSON.stringify({ text: ctx.text }),
savePath: ctx.outputPath
});WebSocket
var ws = new $WebSocket('wss://api.example.com/tts', {
headers: { 'X-Api-Key': 'your-key' }
});
ws.onopen = function() {
ws.send(JSON.stringify({ text: ctx.text }));
};
ws.onmessage = function(event) {
// event.type: 'text' 或 'binary'
// event.data: 文本内容或 base64 编码的二进制数据
if (event.type === 'binary') {
$writeBinary(ctx.outputPath, event.data);
}
};
ws.onclose = function() { };
ws.onerror = function(err) { };
ws.send(data);
ws.close();文件写入
$writeAudio(path, base64Data)— 写入 base64 音频数据$writeBinary(path, base64Data)— 写入 base64 二进制数据$writeSubtitle(audioPath, subtitles)— 保存字幕(用于逐字高亮)
字幕格式:
$writeSubtitle(ctx.outputPath, [
{ audioOffset: 0, duration: 500, text: '你好', textOffset: 0, boundaryType: 'WordBoundary' },
{ audioOffset: 500, duration: 300, text: '世界', textOffset: 2, boundaryType: 'WordBoundary' }
]);工具函数
$uuid()— 生成 UUID$md5(str)— MD5 哈希$hmacSha256(key, data)— HMAC-SHA256 签名$base64Encode(str)/$base64Decode(str)— Base64 编解码$timestamp()— 当前时间戳(秒)$log(msg)— 输出日志(试听时可查看)
示例
简单 HTTP
适用于返回音频二进制的 API:
async function synthesize(ctx) {
await $download({
url: ctx.config.apiUrl,
method: 'POST',
headers: {
'Authorization': 'Bearer ' + ctx.config.apiKey,
'Content-Type': 'application/json'
},
body: JSON.stringify({
text: ctx.text,
voice: ctx.voice,
speed: ctx.rate
}),
savePath: ctx.outputPath
});
}配置项:apiUrl、apiKey
带认证(多步骤)
先获取 Token,再合成音频:
async function synthesize(ctx) {
var resp = await $http({
url: ctx.config.authUrl,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
appKey: ctx.config.appKey,
secret: ctx.config.secret
})
});
var token = JSON.parse(resp.body).access_token;
await $download({
url: ctx.config.ttsUrl,
method: 'POST',
headers: { 'Authorization': 'Bearer ' + token },
body: JSON.stringify({ text: ctx.text, voice: ctx.voice }),
savePath: ctx.outputPath
});
}配置项:authUrl、appKey、secret、ttsUrl
WebSocket 流式合成
适用于 WebSocket 推送音频流的服务:
async function synthesize(ctx) {
await new Promise(function(resolve, reject) {
var ws = new $WebSocket(ctx.config.wsUrl, {
headers: { 'X-Api-Key': ctx.config.apiKey }
});
ws.onopen = function() {
ws.send(JSON.stringify({
text: ctx.text,
voice: ctx.voice,
format: 'mp3'
}));
};
ws.onmessage = function(event) {
if (event.type === 'binary') {
$writeBinary(ctx.outputPath, event.data);
return;
}
var data = JSON.parse(event.data);
if (data.done) ws.close();
};
ws.onclose = function() { resolve(); };
ws.onerror = function(err) { reject(err); };
});
}配置项:wsUrl、apiKey
Azure TTS(WebSocket + 字幕)
使用 Azure 认知服务,支持逐字高亮:
async function synthesize(ctx) {
var subscriptionKey = ctx.config.subscriptionKey;
var region = ctx.config.region || 'eastus';
var voice = ctx.voice || 'zh-CN-XiaoxiaoNeural';
var lang = voice.substring(0, 5);
var wsUrl = 'wss://' + region
+ '.tts.speech.microsoft.com/cognitiveservices/websocket/v1'
+ '?Ocp-Apim-Subscription-Key=' + subscriptionKey;
var text = ctx.text
.replace(/&/g, '&').replace(/</g, '<')
.replace(/>/g, '>').replace(/"/g, '"');
var ssml = "<speak version='1.0' xmlns='http://www.w3.org/2001/10/synthesis'"
+ " xml:lang='" + lang + "'>"
+ "<voice name='" + voice + "'>" + text + "</voice></speak>";
var subtitles = [];
await new Promise(function(resolve, reject) {
var ws = new $WebSocket(wsUrl, {
headers: { 'X-ConnectionId': $uuid().replace(/-/g, '') }
});
ws.onopen = function() {
ws.send(
'X-Timestamp:' + new Date().toISOString() + '\r\n'
+ 'Content-Type:application/json; charset=utf-8\r\n'
+ 'Path:speech.config\r\n\r\n'
+ '{"context":{"synthesis":{"audio":{"metadataoptions":'
+ '{"sentenceBoundaryEnabled":false,"wordBoundaryEnabled":true},'
+ '"outputFormat":"audio-24khz-48kbitrate-mono-mp3"}}}}'
);
ws.send(
'X-RequestId:' + $uuid().replace(/-/g, '') + '\r\n'
+ 'Content-Type:application/ssml+xml\r\n'
+ 'X-Timestamp:' + new Date().toISOString() + '\r\n'
+ 'Path:ssml\r\n\r\n' + ssml
);
};
ws.onmessage = function(event) {
if (event.type === 'binary') {
$writeBinary(ctx.outputPath, event.data);
return;
}
var parts = event.data.split('\r\n\r\n');
if (parts.length < 2) return;
if (parts[0].indexOf('Path:audio.metadata') >= 0) {
try {
var items = JSON.parse(parts[1]).Metadata || [];
for (var i = 0; i < items.length; i++) {
var d = items[i].Data || items[i];
subtitles.push({
audioOffset: Math.floor((d.Offset || 0) / 10000),
duration: Math.floor((d.Duration || 0) / 10000),
text: (d.text && d.text.Text) || '',
textOffset: (d.text && d.text.Index) || 0,
boundaryType: items[i].Type
});
}
} catch(e) { $log('解析错误: ' + e); }
} else if (parts[0].indexOf('Path:turn.end') >= 0) {
if (subtitles.length > 0) $writeSubtitle(ctx.outputPath, subtitles);
ws.close();
}
};
ws.onclose = function() { resolve(); };
ws.onerror = function(err) { reject(err); };
});
}配置项:subscriptionKey、region
配置项
配置项是脚本中通过 ctx.config.xxx 访问的键值对,在编辑页面中添加。适合存放 API Key、服务地址等需要灵活修改的参数。
当同一分组下有多个动态音源使用相同的配置项时,可以通过引擎选择器旁的 ⚡ 按钮统一配置。
加密分享
支持加密导出,保护脚本不被查看:
- 导出时设置密码,生成加密文件
- 接收方输入密码后导入,声音可正常使用但不可编辑
- 配置项的值不包含在导出文件中,接收方需自行填写
- 加密导入的声音不可二次导出
调试
- 使用
$log('消息')输出日志 - 试听时,错误信息会包含完整的执行日志
- 出错时点击复制按钮可复制完整日志
Reeden