Skip to content

58 语音合成系统

模块目标

理解 OpenClaw 的 TTS 系统:多 provider 优先级选择、自动 fallback 循环、auto-mode 判断、通道格式适配。

核心文件

文件职责
src/tts/tts.ts主编排模块(provider 选择、主循环、auto-mode)
src/tts/tts-core.tsProvider 底层实现(Edge / OpenAI / ElevenLabs)
src/tts/prepare-text.ts文本预处理(Markdown 剥离、LLM 摘要)

一、getTtsProvider(provider 优先级,精确源码)

ts
// src/tts/tts.ts

export function getTtsProvider(config: ResolvedTtsConfig, prefsPath: string): TtsProvider {
  // 优先级 1:用户偏好文件(tts.json)
  const prefs = readPrefs(prefsPath);
  if (prefs.tts?.provider) {
    return prefs.tts.provider;
  }

  // 优先级 2:配置文件显式指定
  if (config.providerSource === "config") {
    return config.provider;
  }

  // 优先级 3:自动推断(按 API Key 可用性)
  if (resolveTtsApiKey(config, "openai")) {
    return "openai";
  }
  if (resolveTtsApiKey(config, "elevenlabs")) {
    return "elevenlabs";
  }

  // 优先级 4:默认(Edge TTS,无需 API Key)
  return "edge";
}

优先级总结:

用户偏好文件 > 配置文件显式指定 > OpenAI(有key) > ElevenLabs(有key) > Edge(默认)

二、textToSpeech(主函数,provider 循环 + fallback)

ts
// src/tts/tts.ts

export async function textToSpeech(params: {
  text: string;
  cfg: OpenClawConfig;
  prefsPath?: string;
  channel?: string;
  overrides?: TtsDirectiveOverrides;
}): Promise<TtsResult>

执行流程:

ts
// 1. 解析配置和通道输出格式
const config = resolveTtsConfig(params.cfg);
const output = resolveOutputFormat(channelId);

// 2. 文本长度检查
if (params.text.length > config.maxTextLength) {
  return { success: false, error: `Text too long (${params.text.length} chars, max ${config.maxTextLength})` };
}

// 3. provider 循环(自动 fallback)
for (const provider of providers) {
  const providerStart = Date.now();
  try {
    if (provider === "edge") { /* Edge TTS 逻辑 */ }
    if (provider === "openai") { /* OpenAI TTS 逻辑 */ }
    if (provider === "elevenlabs") { /* ElevenLabs TTS 逻辑 */ }
  } catch (err) {
    lastError = String(err);
    continue;   // 失败 → 尝试下一个 provider
  }
}

// 4. 全部失败
return { success: false, error: lastError ?? "all providers failed" };

三、三大 Provider 详情

Edge TTS(免费,默认)

ts
if (provider === "edge") {
  if (!config.edge.enabled) {
    lastError = "edge: disabled";
    continue;
  }

  const tempDir = mkdtempSync(path.join(tmpdir(), "tts-"));
  let edgeOutputFormat = resolveEdgeOutputFormat(config);
  const fallbackEdgeOutputFormat =
    edgeOutputFormat !== DEFAULT_EDGE_OUTPUT_FORMAT ? DEFAULT_EDGE_OUTPUT_FORMAT : undefined;

  const attemptEdgeTts = async (outputFormat: string) => {
    const extension = inferEdgeExtension(outputFormat);
    const audioPath = path.join(tempDir, `voice-${Date.now()}${extension}`);
    await edgeTTS({
      text: params.text,
      outputPath: audioPath,
      config: { ...config.edge, outputFormat },
      timeoutMs: config.timeoutMs,
    });
    return { audioPath, outputFormat };
  };
  // 主格式失败时回退到 DEFAULT_EDGE_OUTPUT_FORMAT
}

ElevenLabs TTS

ts
if (provider === "elevenlabs") {
  const output = TELEPHONY_OUTPUT.elevenlabs;
  const audioBuffer = await elevenLabsTTS({
    text: params.text,
    apiKey,
    baseUrl: config.elevenlabs.baseUrl,
    voiceId: config.elevenlabs.voiceId,
    modelId: config.elevenlabs.modelId,
    outputFormat: output.format,
    seed: config.elevenlabs.seed,
    applyTextNormalization: config.elevenlabs.applyTextNormalization,
    languageCode: config.elevenlabs.languageCode,
    voiceSettings: config.elevenlabs.voiceSettings,
    timeoutMs: config.timeoutMs,
  });

  return {
    success: true,
    audioBuffer,
    latencyMs: Date.now() - providerStart,
    provider,
    outputFormat: output.format,
    sampleRate: output.sampleRate,
  };
}

四、通道格式适配

通道输出格式采样率
TelegramOpus @ 64kbps48kHz(语音消息兼容)
电话(OpenAI)PCM24kHz
电话(ElevenLabs)PCM22.05kHz
默认MP3 @ 128kbps44.1kHz

五、resolveTtsAutoMode(auto-mode 判断,三级优先级)

ts
// src/tts/tts.ts

export function resolveTtsAutoMode(params: {
  config: ResolvedTtsConfig;
  prefsPath: string;
  sessionAuto?: string;   // 来自当前会话参数
}): TtsAutoMode {
  // 优先级 1:session 参数(最高优先级)
  const sessionAuto = normalizeTtsAutoMode(params.sessionAuto);
  if (sessionAuto) return sessionAuto;

  // 优先级 2:用户偏好文件
  const prefsAuto = resolveTtsAutoModeFromPrefs(readPrefs(params.prefsPath));
  if (prefsAuto) return prefsAuto;

  // 优先级 3:配置文件默认值(最低优先级)
  return params.config.auto;
}

auto-mode 取值:

模式行为
off不自动转语音(默认)
always所有回复都转语音
inbound仅当用户发送语音时才回复语音
tagged仅当 Agent 使用 [[tts]] 指令时才转语音

六、maybeApplyTtsToPayload(自动 TTS 附加)

ts
// src/tts/tts.ts

export async function maybeApplyTtsToPayload(params: {
  payload: ReplyPayload;
  cfg: OpenClawConfig;
  channel?: string;
  kind?: "tool" | "block" | "final";   // 消息类型
  inboundAudio?: boolean;               // 用户是否发送了语音
  ttsAuto?: string;                     // session 级 auto 设置
}): Promise<ReplyPayload> {
  const config = resolveTtsConfig(params.cfg);
  const prefsPath = resolveTtsPrefsPath(config);
  const autoMode = resolveTtsAutoMode({
    config, prefsPath, sessionAuto: params.ttsAuto,
  });

  // 判断是否应该应用 TTS
  const shouldApply =
    autoMode === "always" ||
    (autoMode === "inbound" && params.inboundAudio) ||
    (autoMode === "tagged" && payloadHasTtsTag(params.payload));

  if (!shouldApply) return params.payload;

  // 执行文本预处理 + TTS 转换
  const text = prepareTextForTts(params.payload.text ?? "");
  const result = await textToSpeech({ text, cfg: params.cfg, channel: params.channel });

  if (!result.success) return params.payload;  // TTS 失败降级为文本

  return { ...params.payload, audio: result.audioBuffer, audioFormat: result.outputFormat };
}

七、文本预处理链

原始 payload.text


Markdown 剥离 → 纯文本


[[tts:...]] 指令解析(provider/voice 覆盖)


[可选] LLM 摘要(text.length > config.summarizeThreshold)


传入 textToSpeech(text)

支持行内指令覆盖:[[tts:provider=openai,voice=alloy]]

八、用户偏好持久化

偏好文件路径:~/.openclaw/settings/tts.json

json
{
  "tts": {
    "enabled": true,
    "auto": "inbound",
    "provider": "openai",
    "maxTextLength": 4000,
    "summarize": true
  }
}

tts.enable / tts.disable 方法修改此文件,tts.setProvider 设置首选 provider。

九、自检清单

  1. provider 优先级:用户偏好 > 配置显式 > OpenAI(有key) > ElevenLabs(有key) > Edge。
  2. auto-mode 优先级:session 参数 > 用户偏好文件 > 配置默认值。
  3. TTS 失败时 maybeApplyTtsToPayload 降级为纯文本,不报错给用户。
  4. Edge TTS 有输出格式回退:主格式失败时尝试 DEFAULT_EDGE_OUTPUT_FORMAT
  5. ElevenLabs 返回 audioBuffer(内存),Edge TTS 先写临时文件再读(文件路径)。
  6. kind 参数区分 tool/block/final,某些 auto-mode 可能只对 final 消息应用 TTS。

十、开发避坑

  1. Edge TTS 依赖外部进程:底层调用系统 TTS 工具,在无头环境(CI/容器)中可能不可用。
  2. ElevenLabs 的 seed 参数:相同 seed + 相同文本 + 相同 voiceId 理论上生成相同音频(可缓存)。
  3. 长文本自动摘要:超过 config.summarizeThreshold 的文本会通过 LLM 摘要后再转语音,摘要消耗额外 token。
  4. 通道格式硬编码:Telegram 必须是 Opus,电话必须是 PCM,不要在 config 中覆盖这些。

用工程视角拆解 AI 智能体框架