自定义动态音源
通过 JavaScript 脚本自定义 TTS 合成逻辑,支持 HTTP、WebSocket、持久化存储等能力
动态音源允许你通过编写 JavaScript 脚本来实现完全自定义的 TTS 合成逻辑。相比自定义 HTTP 合成,动态音源支持多步骤请求、WebSocket 流式合成、Token 缓存、认证签名等复杂场景。
快速开始
- 进入 设置 → TTS 引擎
- 点击右上角菜单 → 新建动态音源
- 填写名称,选择一个内置模板
- 根据需要修改脚本和配置项
- 保存后试听验证
脚本协议
脚本必须定义一个 synthesize(ctx) 异步函数,将合成的音频保存到 ctx.outputPath:
async function synthesize(ctx) {
// ctx.text - 待合成的文本
// ctx.voice - 语音标识
// ctx.config - 配置项(键值对)
// ctx.outputPath - 音频文件保存路径
}可用 API
网络请求
$http(options) — 发起 HTTP 请求
var resp = await $http({
url: 'https://api.example.com/token',
method: 'POST', // 默认 GET
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ key: 'value' }),
responseType: 'base64' // 可选,设为 'base64' 返回原始二进制的 base64 编码
});
// resp.status - HTTP 状态码
// resp.headers - 响应头
// resp.body - 响应内容(默认文本,responseType 为 'base64' 时为 base64 编码字符串)responseType 说明:
- 不传或传其他值:
resp.body为文本内容 'base64':以二进制方式接收响应,resp.body为 base64 编码字符串,可直接传给$writeBinary()写入文件
$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
new $WebSocket(url, options) — 创建 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() 关闭连接。
文件写入
$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' }
]);Base64 字节操作
$base64Slice(base64Data, start, end) — 对 base64 数据做字节切片,返回新的 base64 字符串
// 从第 44 字节开始截取到末尾(跳过 WAV 文件头)
var audioData = $base64Slice(rawBase64, 44);
// 截取第 10 到第 20 字节
var chunk = $base64Slice(rawBase64, 10, 20);start:起始字节偏移(从 0 开始)end:结束字节偏移(可选,默认到末尾)- 如果
start超出数据长度,返回空字符串
$base64ReadByte(base64Data, index) — 读取 base64 数据中指定位置的字节值(0-255)
// 读取第 0 个字节判断音频格式
var firstByte = $base64ReadByte(rawBase64, 0);
if (firstByte === 0xFF) {
$log('MP3 格式');
}index:字节位置(从 0 开始)- 如果
index越界,返回0
音频转换
$pcmToWav(base64Pcm, options) — 将 PCM 原始音频数据转换为 WAV 格式(base64 输入,base64 输出)
// 默认参数:16000Hz 采样率、单声道、16bit
var wavBase64 = $pcmToWav(pcmBase64);
// 自定义参数
var wavBase64 = $pcmToWav(pcmBase64, {
sampleRate: 24000, // 采样率,默认 16000
numChannels: 1, // 声道数,默认 1
bitDepth: 16 // 位深,默认 16
});
// 转换后写入文件
$writeBinary(ctx.outputPath, wavBase64);适用场景:部分 TTS 服务返回的是无文件头的 PCM 裸数据,需要转换为 WAV 格式才能正常播放。
持久化存储
$store — 跨调用的 key-value 持久化存储,适合缓存 Token、Session 等数据。
$store.set('token', 'xxx'); // 写入
var token = $store.get('token'); // 读取(不存在返回 null)
$store.remove('token'); // 删除
$store.clear(); // 清空所有数据
var keys = $store.keys(); // 获取所有 key注意事项:
- 所有动态音源共享同一个存储空间,建议使用前缀区分来源(如
azure_token、ali_session) - 值以字符串形式存储,复杂对象请用
JSON.stringify()/JSON.parse()处理
全部函数一览
| 函数 | 类型 | 说明 |
|---|---|---|
$http(options) | 异步 | HTTP 请求,返回 {status, headers, body},支持 responseType: 'base64' 获取二进制响应 |
$download(options) | 异步 | 下载文件到 options.savePath |
new $WebSocket(url, options) | 异步 | 创建 WebSocket 连接,事件驱动 |
$writeBinary(path, base64Data) | 同步 | 追加写入 base64 二进制数据 |
$writeSubtitle(audioPath, subtitles) | 同步 | 保存字幕文件(逐字高亮) |
$store.get(key) | 同步 | 读取持久化存储,不存在返回 null |
$store.set(key, value) | 同步 | 写入持久化存储(值为字符串) |
$store.remove(key) | 同步 | 删除持久化存储中的 key |
$store.clear() | 同步 | 清空持久化存储 |
$store.keys() | 同步 | 获取持久化存储所有 key |
$uuid() | 同步 | 生成 UUID |
$md5(str) | 同步 | MD5 哈希 |
$sha256(str) | 同步 | SHA256 哈希 |
$hmacSha256(key, data) | 同步 | HMAC-SHA256 签名 |
$base64Encode(str) | 同步 | Base64 编码 |
$base64Decode(str) | 同步 | Base64 解码 |
$base64Slice(base64Data, start, end) | 同步 | 对 base64 数据做字节切片,返回新的 base64 |
$base64ReadByte(base64Data, index) | 同步 | 读取 base64 数据中指定字节值(0-255) |
$pcmToWav(base64Pcm, options) | 同步 | PCM 转 WAV,支持自定义采样率/声道/位深 |
$timestamp() | 同步 | 当前时间戳(秒) |
$log(msg) | 同步 | 输出日志(试听时可查看) |
示例
简单 HTTP 请求
适用于一个 POST 请求直接返回音频的 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
}),
savePath: ctx.outputPath
});
}配置项:apiUrl、apiKey
带认证的多步骤请求
先获取 Token 再合成音频,使用 $store 缓存 Token 避免重复鉴权:
async function synthesize(ctx) {
var token = $store.get('auth_token');
var expire = $store.get('auth_token_expire');
var now = $timestamp();
if (!token || !expire || now >= Number(expire)) {
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 data = JSON.parse(resp.body);
token = data.access_token;
$store.set('auth_token', token);
$store.set('auth_token_expire', String(now + data.expires_in - 60));
}
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 + 字幕 + Token 缓存)
使用 Azure Speech Service 的 WebSocket 接口,支持逐字高亮字幕。通过 $store 缓存 Token 避免每次合成都请求鉴权接口:
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);
// 获取或刷新 Token(有效期 10 分钟,提前 1 分钟刷新)
var token = $store.get('azure_token');
var tokenExpire = $store.get('azure_token_expire');
var now = $timestamp();
if (!token || !tokenExpire || now >= Number(tokenExpire)) {
var resp = await $http({
url: 'https://' + region + '.api.cognitive.microsoft.com/sts/v1.0/issueToken',
method: 'POST',
headers: { 'Ocp-Apim-Subscription-Key': subscriptionKey, 'Content-Length': '0' }
});
if (resp.status !== 200) throw new Error('获取 Token 失败: ' + resp.body);
token = resp.body;
$store.set('azure_token', token);
$store.set('azure_token_expire', String(now + 540));
}
// XML 转义
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 wsUrl = 'wss://' + region
+ '.tts.speech.microsoft.com/cognitiveservices/websocket/v1'
+ '?Authorization=Bearer ' + token;
var ws = new $WebSocket(wsUrl, {
headers: { 'X-ConnectionId': $uuid().replace(/-/g, '') }
});
ws.onopen = function() {
// 音频格式 + word boundary 配置
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"}}}}'
);
// 发送 SSML
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、服务地址等需要灵活修改的参数。
使用方式:
- 在声音编辑页面定义需要的配置 key(如
subscriptionKey、region) - 在引擎选择器旁点击设置按钮,打开分组配置弹窗填写实际的值
- 同一分组下的所有声音共享这些配置值
加密分享
支持加密导出,保护脚本不被查看:
- 导出时设置密码,生成加密文件
- 接收方输入密码后导入,声音可正常使用但不可编辑
- 配置项的值不包含在导出文件中,接收方需自行填写
- 加密导入的声音不可二次导出
调试
- 使用
$log('消息')在脚本中输出日志 - 试听时若出错,错误信息会包含完整的执行日志
- 点击复制按钮可复制完整日志用于排查
Reeden