ReedenReeden

自定义动态音源

通过 JavaScript 脚本自定义 TTS 合成逻辑,支持 HTTP、WebSocket、持久化存储等能力

动态音源允许你通过编写 JavaScript 脚本来实现完全自定义的 TTS 合成逻辑。相比自定义 HTTP 合成,动态音源支持多步骤请求、WebSocket 流式合成、Token 缓存、认证签名等复杂场景。

快速开始

  1. 进入 设置 → TTS 引擎
  2. 点击右上角菜单 → 新建动态音源
  3. 填写名称,选择一个内置模板
  4. 根据需要修改脚本和配置项
  5. 保存后试听验证

脚本协议

脚本必须定义一个 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_tokenali_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
  });
}

配置项:apiUrlapiKey

带认证的多步骤请求

先获取 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
  });
}

配置项:authUrlappKeysecretttsUrl

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); };
  });
}

配置项:wsUrlapiKey

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, '&amp;').replace(/</g, '&lt;')
    .replace(/>/g, '&gt;').replace(/"/g, '&quot;');

  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); };
  });
}

配置项:subscriptionKeyregion

配置项

配置项是脚本中通过 ctx.config.xxx 访问的键值对,适合存放 API Key、服务地址等需要灵活修改的参数。

使用方式:

  1. 在声音编辑页面定义需要的配置 key(如 subscriptionKeyregion
  2. 在引擎选择器旁点击设置按钮,打开分组配置弹窗填写实际的值
  3. 同一分组下的所有声音共享这些配置值

加密分享

支持加密导出,保护脚本不被查看:

  • 导出时设置密码,生成加密文件
  • 接收方输入密码后导入,声音可正常使用但不可编辑
  • 配置项的值不包含在导出文件中,接收方需自行填写
  • 加密导入的声音不可二次导出

调试

  • 使用 $log('消息') 在脚本中输出日志
  • 试听时若出错,错误信息会包含完整的执行日志
  • 点击复制按钮可复制完整日志用于排查