跳转至

语音和音频

音频侧功能,提供商插件可随文本推理一起注册。属于构建提供商插件指南的一部分。

语音和音频功能

在 register(api) 中注册每项功能,与现有的 api.registerProvider(...) 调用一起。只需选择所需的选项卡:

import {
  assertOkOrThrowProviderError,
  postJsonRequest,
} from "openclaw/plugin-sdk/provider-http";

api.registerSpeechProvider({
  id: "acme-ai",
  label: "Acme Speech",
  defaultTimeoutMs: 120_000,
  isConfigured: ({ config }) => Boolean(config.messages?.tts),
  synthesize: async (req) => {
    const { response, release } = await postJsonRequest({
      url: "https://api.example.com/v1/speech",
      headers: new Headers({ "Content-Type": "application/json" }),
      body: { text: req.text },
      timeoutMs: req.timeoutMs,
      fetchFn: fetch,
      auditContext: "acme speech",
    });
    try {
      await assertOkOrThrowProviderError(response, "Acme Speech API error");
      return {
        audioBuffer: Buffer.from(await response.arrayBuffer()),
        outputFormat: "mp3",
        fileExtension: ".mp3",
        voiceCompatible: false,
      };
    } finally {
      await release();
    }
  },
});

对于提供商 HTTP 失败,请使用 assertOkOrThrowProviderError(...),以便插件共享有上限的错误正文读取、JSON 错误解析以及请求 ID 后缀。当请求携带凭据时,将 { requestHeaders: headers } 作为其第三个参数传入:这会在保留错误详情和元数据之前对反射的标头值进行脱敏。将同一选项传递给 readProviderJsonResponse(...),以省略不安全的解析器摘录。对于提供商特定的失败负载,请使用同一 SDK 入口点中的 redactProviderResponseErrorText(text, headers) 或限制大小的 readProviderResponseErrorText(response, limitBytes, headers) 辅助函数。

调用方可以将候选提供商 ID 作为可选的第二个参数传递给 listRealtimeTranscriptionProviders(cfg, providerIds)。这可以发现插件本地配置中指定的提供商,而不会扩大活动注册表或绕过插件启用以及允许/拒绝策略。

优先使用 createRealtimeTranscriptionWebSocketSession(...) —— 共享辅助函数会处理代理捕获、重连退避、关闭刷新、就绪握手、音频排队以及关闭事件诊断。你的插件只需映射上游事件。

api.registerRealtimeTranscriptionProvider({
  id: "acme-ai",
  label: "Acme Realtime Transcription",
  isConfigured: () => true,
  createSession: (req) => {
    const apiKey = String(req.providerConfig.apiKey ?? "");
    return createRealtimeTranscriptionWebSocketSession({
      providerId: "acme-ai",
      callbacks: req,
      url: "wss://api.example.com/v1/realtime-transcription",
      headers: { Authorization: `Bearer ${apiKey}` },
      onMessage: (event, transport) => {
        if (event.type === "session.created") {
          transport.sendJson({ type: "session.update" });
          transport.markReady();
          return;
        }
        if (event.type === "transcript.final") {
          req.onTranscript?.(event.text);
        }
      },
      sendAudio: (audio, transport) => {
        transport.sendJson({
          type: "audio.append",
          audio: audio.toString("base64"),
        });
      },
      onClose: (transport) => {
        transport.sendJson({ type: "audio.end" });
      },
    });
  },
});

通过 POST 发送 multipart 音频的批量 STT 提供商应使用来自 openclaw/plugin-sdk/provider-http 的 buildAudioTranscriptionFormData(...)。该辅助函数会规范化上传文件名,包括需要 M4A 风格文件名的 AAC 上传,以兼容相应的转录 API。

官方插件可以使用私有的 blob-runtime 辅助函数 bufferToBlobPart(buffer) 进行其他 multipart 上传。将其直接传递给 new Blob(...),以保留 Buffer 范围而不进行中间复制;共享底层数据会在需要时复制。在等待其他工作之前构造 Blob,以便立即对字节进行快照。

调用方可以将候选提供商 ID 作为可选的第二个参数传递给 listRealtimeVoiceProviders(cfg, providerIds)。省略该参数以进行常规目录发现;每次调用的候选项不会更改该目录。 自动实时语音和 Voice Call 转录选择使用已声明的别名配置作为默认值,优先选择较早的别名,并且规范值优先。 显式选择的别名仍然会覆盖规范配置,而不会继承其他别名的设置。

resolveConfig 会接收与 cfg 和 rawConfig 一起的可选主机上下文: agentId、surface(browser-session、gateway-relay 或 bridge)、 autoRespondToAudio 以及 requiredCapabilities.supportsVideoFrames。使用此 上下文来选择与账户和会话兼容的默认值,同时保留 显式模型。省略 surface 时保留 bridge 行为。浏览器会话 创建会为具备摄像头能力的调用方提供 supportsVideoFrames: true,为 仅音频调用方提供 false;目录发现会保持该要求 未指定。OpenAI 会按账户选择 GPT-Live,除非调用方要求 视频或手动响应。当 Gateway 中继策略控制响应时,Talk 会设置 autoRespondToAudio: false。talk.catalog 会为已配置的 Talk 代理和提供商设置解析提供商的发现默认值,不包括显式模型;就绪状态和功能使用有效的模型覆盖。 在不更改现有模型默认值的情况下添加新传输的调用方,可以向 resolveConfiguredRealtimeVoiceProvider(...) 传递 useProviderDefaultModel: true。这会在 surface 特定解析之前,从所选提供商的 defaultModel 填充缺失的模型;显式配置的模型和请求覆盖仍然优先。可选的 talk.catalog 输入 provider 和 model 会为特定的实时启动解析功能,而不更改已保存的配置。Gateway 音频调用方选择 gateway-relay surface,并使用 resolveConfiguredRealtimeVoiceProvider(...) 返回的 capabilities。该结果会将配置、身份验证就绪状态和功能绑定到同一个提供商规范化模型。浏览器调用方还会将其协商的 clientControl 传递给解析。将解析后的功能带入 resolveRealtimeVoiceSessionPolicy(...) 和共享 bridge/session 框架,而不是读取提供商的静态功能默认值。目录在检查候选项而不创建会话时仍可使用 resolveRealtimeVoiceProviderCapabilities(...)。例如,GPT-Live 负责代理委托和中断,但不支持主机强制的唤醒词门控,尽管 GA OpenAI Realtime 支持。

    api.registerRealtimeVoiceProvider({
      id: "acme-ai",
      label: "Acme Realtime Voice",
      capabilities: {
        transports: ["gateway-relay"],
        inputAudioFormats: [{ encoding: "pcm16", sampleRateHz: 24000, channels: 1 }],
        outputAudioFormats: [{ encoding: "pcm16", sampleRateHz: 24000, channels: 1 }],
        supportsBargeIn: true,
        handlesInputAudioBargeIn: true,
        supportsToolCalls: true,
      },
      isConfigured: ({ providerConfig }) => Boolean(providerConfig.apiKey),
      createBridge: (req) => ({
        // Set this only if the provider accepts multiple tool responses for
        // one call, for example an immediate "working" response followed by
        // the final result.
        supportsToolResultContinuation: false,
        connect: async () => {},
        sendAudio: () => {},
        setMediaTimestamp: () => {},
        handleBargeIn: () => {},
        submitToolResult: () => {},
        acknowledgeMark: () => {},
        close: () => {},
        isConnected: () => true,
      }),
    });

声明 capabilities,以便 talk.catalog 能够向浏览器和原生 Talk 客户端暴露有效的模式、传输、音频格式和功能标志。当传输能够检测到人类正在中断助手播放,并且提供商支持截断或清除当前音频响应时,实现 handleBargeIn。对于没有响应边界的流(例如 GPT-Live),设置 bridge.outputAudioMode: "continuous"。此时,宿主会立即播放短音频,在提供商清除后接受新音频,并将中断交给提供商处理。对于此模式,省略 handleBargeIn 并报告 supportsBargeIn: false;传入音频已经驱动原生中断。省略 outputAudioMode,或设置为 "response",会保留基于响应的播放。共享会话和测试框架会拒绝连续流或 supportsBargeIn: false 的宿主中断,包括回退输出清除。显式会话停止仍然是独立操作。传输必须让提供商能够获取参与者音频;包含注入的助手输出的麦克风输入必须在启用此行为之前进行隔离。共享浏览器会议适配器会出于该目的将远程播放与原生虚拟麦克风注入分开捕获。

当提供商按其采样率缓冲传入 PCM,并在麦克风写入之间提供静音时,设置 bridge.pacesInputAudio: true。这可防止 Discord 等传输在每个捕获边界追加额外的静音突发。GPT-Live Gateway WebRTC 和 WebSocket 桥接共享该输入时钟;关闭桥接会停止它。当原生音频事件标识出某个项时,将该标识与 PCM 一起作为 req.onAudio(audio, { itemId }) 传递;对于没有原生项 ID 的传输,省略元数据。如果提供,req.getPlaybackState() 会按播放顺序返回保留的项,并带有累计的、相对于项的 audioEndMs;排队中的项时长为零。在清除输出之前对这些偏移量进行快照,并使用提供商的原生取消和截断语义同步已丢弃的输出。空快照表示没有保留音频,即使新响应正在生成。没有播放测量的宿主应省略该回调,并保留现有的媒体时间戳和播放标记契约。

在发出 PCM 后,提供商可以调用 req.onMark?.(name, acknowledge),并传入一个绑定到该确切提供商连接的确认回调。该回调必须拒绝已被替换的连接和已退役的标记,同时如果较新的响应在较旧的播放排空之前开始,它仍应保持有效。传输应在消费关联的 PCM 后按顺序调用作用域回调,而不是在接收或编码时调用。取消和失败会分别使提供商标记所有权退役;被丢弃的 PCM 永远不会被报告为已播放。现有的 onMark(name) 和 bridge.acknowledgeMark(name) 契约仍然可用于远程传输和已安装的提供商。Discord 会为这些遗留的无作用域标记保留立即确认。onEvent 观察诊断事件。OpenAI 和 xAI 在将出站帧提交到本地套接字后报告它们;该回调既不确认远程接收,也不否决该帧。在观察者内部请求的控制操作会在该帧之后运行。submitToolResult 可以返回 void 表示同步提交,或返回 Promise<void> 表示提供商桥接可以暴露的异步完成边界。Gateway relay 会话在确认最终结果或清除关联运行之前会等待该 Promise;提交失败时拒绝它。close 可以返回 void 表示同步处置,或返回 Promise<void>,在提供商最终化和资源清理后结算。立即停止音频、工具和委托准入。最终转录回调可以排空直到完成;消费者必须在密封转录队列或报告逻辑会话关闭之前等待它。通过 onClose 报告提供商的终止原因,并在清理失败时拒绝该 Promise。会话外观保留同步处置。一旦提供商返回 Promise,重复的 close 调用会返回同一个待处理完成。在提供商调用期间的重入 close 调用是空操作;终止回调不得等待其自身处置。

连续单声道 PCM16/24 kHz 桥接可以实现 setAudioOutputPort(output),在连接之前绑定由 worker 拥有的播放接收端。RealtimeVoiceAudioOutputPort 携带一个可转移的 Node MessagePort 和一个共享关闭栅栏:其第一个 Int32 在打开时为 0,在撤销后永久为 1。绑定此接收端后,通过端口而不是 onAudio 和 onClearAudio 发送 PCM 和清除事件;将转录、委托和生命周期回调保留在宿主上。createRealtimeVoiceAudioPortSender 提供有界队列、复制的缓冲区所有权、一个未决音频消息以及有序的清除。接收方可以发送 { type: "flush", marker };发送方仅在其排队和未决 PCM 已被确认后回复 { type: "flushed", marker },包括没有音频的情况。较新的 flush 标记会取代较旧的待处理标记。这是本地接收端准入,而不是可听播放或未来提供商静音的证明。消费者使用它来将控制平面完成排序在已提交媒体之后。接收方使用 { type: "ack" } 确认音频,在接受音频前检查栅栏,并在端口关闭时关闭其播放资源。不要使用此路径绕过宿主响应或唤醒名称准入。接收端所有者会在异步拆除之前撤销栅栏,以便排队音频不能进入替换调用。

捆绑的懒加载提供者使用来自私有本地 openclaw/plugin-sdk/realtime-voice-provider 表面的 createLazyRealtimeVoiceBridgeLifecycle,以负责加载、回调围栏和等待式释放。它在调用提供者工厂之前声明一个代际,因此同步回调可以在工厂返回桥接之前关闭或替换桥接。提供者模块保留其输入队列、就绪策略、认证和重连行为;模块缓存仍保留在懒加载运行时辅助函数中。

该私有本地表面还导出宿主内部的浏览器会话请求、能力和提供者 API 类型。官方插件应导入这些类型,而不是重新声明进程私有钩子契约。这些仅类型导入不会加载宿主的会话或提供者注册表运行时。

当提供者无法遵守 options.suppressResponse 时,设置 supportsToolResultSuppression: false。OpenClaw 随后会避免对内部强制咨询和取消结果进行抑制,并拒绝直接的抑制结果请求,而不是静默地开始响应。createRealtimeVoiceBridgeSession 的消费者同样可以从 onToolCall 返回一个 Promise;同步抛出和拒绝会被路由到会话的 onError 回调。宿主可以在响应状态空闲时传递 sendUserMessage(text, { toolChoice }),以强制该响应使用一个命名函数;后续响应会返回到会话配置的 tool choice。当提供者拥有来自传入音频的中断时,设置 handlesInputAudioBargeIn。在可用时,通过 onClearAudio("barge-in") 转发提供者缓冲区清除事件;连续提供者可以在没有单独清除事件的情况下停止说话。宿主不得为这些提供者虚构本地中断。省略该标志的基于响应的提供者使用 OpenClaw 的本地输入音频回退检测。

浏览器会话请求的 clientControl: { owner: "gateway" } 记录显式协商的服务器拥有控制。请求类型要求带有该声明的 gatewayControl.bindControl;没有它的请求保留旧版回调形状。仅存在 gatewayControl 回调并不构成该协商:原生委托也可以将它们用于生命周期处理,同时浏览器保留其数据通道和转录报告。

对于协商控制,保持供应商认证和信令私有,使用 gatewayControl.bindControl(...) 绑定受支持的 submitToolResult 和 sendUserMessage 命令,并通过提供的回调转发提供者就绪状态、转录和终止事件。将实例方法绑定到其接收者。侧带无需虚构媒体方法或创建另一个音频对端。bindBridge(fullBridge) 仍可用于稳定的 2026.8.1 SDK 契约,并且仅在有版本号的 SDK 破坏时移除。Gateway 仍然是工具策略和运行生命周期的所有者;切勿从模型名称推断控制所有权,或重复客户端拥有的转录写入。

桥接请求和协商的浏览器 gatewayControl 可以提供 handleDelegationInput(rawText, respond): "control" | "consult"。在消费转录上下文、替换待处理工作或中止活动咨询之前,对原生委托输入调用此同步、有副作用的准入钩子。只有 consult 允许任务回退。control 结果会消费请求,包括拒绝或失败;不要启动任务或发送任务回执。即使空闲时,状态和取消也是控制;重定向和后续操作需要调用拥有的工作。普通空闲请求仍然回退到咨询。

宿主根据已解析的 handlesAgentConsult 能力准备委托所有权,而不是根据 supportsToolCalls: false 或回调存在。在此模式下,最终转录仅更新历史和可观测性。具备工具能力、未指定以及无工具的非委托提供者保留其现有转录行为。没有该钩子时,保留现有委托和确认策略。

createRealtimeVoiceBridgeSession 将宿主 runAgentConsult 转发到提供者拥有的委托桥接,并将其绑定到已准入的提供者连接。调用方提供其现有身份和工具策略所有者;Discord 使用原始发言者的正常代理路由和权限。关闭或连接替换会使该回调的权限失效,而不是将其转移给下一个发言者或连接。

宿主在 harness 策略准备后,将转向权限绑定到实际已准入的后端尝试。后备代理 harness 在注册其句柄时转发现有尝试指纹。实时语音提供者不计算权限,也不将目标指纹复制到传入用户输入中。调用方策略由宿主针对精确的实时注册进行投影,并且已关闭或已替换的所有者拒绝注入。普通回复拥有的尝试保留其原始权限快照和具体模型路由。仅借用回复操作进行生命周期管理的维护尝试则从其自身准备好的执行中获得权限。后端队列在异步输入准备后重新验证所有权,就在插入消息或回答待处理问题之前。

将 respond(message) 绑定到传入的控制委托和精确的调用/传输实例。最多提交一次,在首次发送尝试之前消费响应;多个线路块是一个响应。不要在发送失败时重试它,不要指向更新的委托/套接字,也不要在关闭/分离后投递。取消可以中止后备任务,而无需使其控制回复失效。将委托 ID 和线路编码保留在提供者内部;独立的宿主语音和任务回执使用会话上下文。提交不建立完成或可听交付。

会话门面在桥接采用后允许此钩子,包括在就绪之前,并在关闭后对操作和回复进行围栏。回调失败被隔离,不会导致任务穿透。onTranscript 保留其 void 回调契约,包括可赋值的异步处理程序和关闭时的最终转录刷新。

具有累积性临时转录的提供方可以将 { textMode: "snapshot" } 作为第四个 onTranscript 参数传入。网关中继会将其转发到浏览器,浏览器会就地替换临时文本。对于增量片段,请省略此元数据。在提供方的实际完成边界处,每个话语发布一个最终版本,而不是为每个临时快照发布。

宿主 runAgentConsult 中名为 AbortError 的拒绝表示取消,即使提供方自身的信号仍然有效。不要将其转换为失败任务或重试回复。TimeoutError 仍然是失败。关闭传输和取消已接受的宿主工作是独立的生命周期操作。

=== "媒体理解" {#media-understanding}

拥有自身凭据和端点契约的音频提供方可以实现 transcribeAudioWithContext(request)。宿主在加载每个音频文件后调用它。请求包括音频字节、文件名、模型、提示、语言、超时、传输设置、配置、agent 目录和所选配置文件。为该调用解析凭据;不要跨附件下载保留凭据。

转录完成后返回 { ok: true, value: { text, model } }。仅在上传音频之前认证或配置被拒绝时返回 { ok: false, error }。宿主会记录该错误,自动选择可能会尝试下一个提供方或本地后端。规范的提供方认证缺失会使自动候选不可用,而不产生失败尝试。上传和 HTTP 失败必须抛出异常:随后自动选择会停止,而不会将录音发送到另一个提供方。显式模型列表保留其编写的回退顺序。

已知时返回模型;否则宿主会在其结果中保留请求的模型。transcribeAudio 仍然可用于使用宿主拥有的 API 密钥解析和轮换的提供方。

捆绑的媒体提供方可以使用私有本地 openclaw/plugin-sdk/provider-http 入口点中的 openProviderWebSocket(...)。首先使用 resolveProviderHttpRequestConfigWithOriginTrust(...) 解析请求设置,然后将其 baseUrl、headers、dispatcherPolicy、allowPrivateNetwork 和 trustConfiguredBaseUrlOrigin 与 WebSocket url 一起传入。

已配置的代理路由保留已解析的目标地址检查。适用的环境 HTTP(S) 代理和 OpenClaw 管理的代理保留其现有 DNS 委派;NO_PROXY 绕过和单独的 ALL_PROXY 不会禁用目标地址检查。

代理连接使用共享的由 Proxyline 支持的 Node 代理。准备好的代理 DNS 查找和代理 TLS 设置通过 createNodeProxyAgent(...) 上的 proxyConnect 选项进行;目标 TLS 保持独立。Proxyline 拥有待处理的代理套接字,包括在 CONNECT 完成之前的清理,因此提供方不需要单独的 proxy-agent 依赖。

  • 该 Promise 在网络策略和代理准备完成后解决,而返回的套接字仍在连接中。立即附加 error、close 和 open 处理程序;在 open 之后发送帧。
  • timeoutMs 将 DNS 准备、代理 CONNECT 和 WebSocket 握手作为一个连接截止时间。连接计时器在 open 时停止;提供方拥有剩余的转录截止时间。
  • signal 取消准备,并在套接字的生命周期内保持活动。关闭或终止套接字也会取消待处理的代理连接。在操作的清理路径中释放套接字。
  • maxPayloadBytes 限制每条传入消息,默认值为 16 MiB。压缩已禁用。提供方负责音频缓冲、帧节奏、协议解析和转录大小限制。
api.registerMediaUnderstandingProvider({
  id: "acme-ai",
  capabilities: ["image", "audio"],
  describeImage: async (req) => ({ text: "A photo of..." }),
  transcribeAudio: async (req) => ({ text: "Transcript..." }),
});

有意不需要凭据的本地或自托管媒体提供方可以公开 resolveAuth 并返回 kind: "none"。OpenClaw 仍然为未明确选择加入的提供方保留常规认证关卡。现有提供方可以继续读取 req.apiKey;新提供方应优先使用 req.auth。

api.registerMediaUnderstandingProvider({
  id: "local-audio",
  capabilities: ["audio"],
  resolveAuth: () => ({
    kind: "none",
    source: "local-audio plugin no-auth",
  }),
  transcribeAudio: async (req) => ({ text: "Transcript..." }),
});

本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 cl/openclaw