- 先调用
POST /api/v1/public/self-serve/signup,拿到tenantId和x-omnimodal-api-key。 - 先跑
POST /api/v1/multimodal-entries,确认鉴权、tenant 和最小业务入口是通的。 - 如果做异步文档链路,再跑
ingestions -> tasks -> result,最后再接租户回调。 - 如果做实时语音,先用本页联调台确认公网主链路通,再接你自己的前端和 WebSocket。
Developer Portal
拿到 key 后,按文档直接调。
这里给你一条最小可跑链路:自助开通、带鉴权请求、多模态入口、异步任务结果查询、实时语音会话。
Integration Pack
把网址、key、顺序、错误码和回调规则一次讲清楚。
如果你要把全模通交给另一个后端团队、前端团队或外部客户,优先把这一节发给他。
- 实时语音默认正式主链路是
pcm16 / 16000 / mono。 webm_opus仍是实验路径,不能默认当商用接法。- 实时音频分片字段名必须是
audioBase64,不是audio。 - 同一 realtime 会话里的
sequenceNo必须单调递增。
- 核心链路成功率建议
>= 85%,失败样本必须能追踪到sessionId、taskId或providerCode。 - 实时语音联调单次健康分建议
>= 90,多轮平均建议>= 85。 - 死信、失败回调、上游异常要能解释、能重试、能归档。
- 成本口径要能看到账单预估、provider 估算、套餐含量和超额用量。
- 必带:
tenantId、sessionId或taskId、测试时间、浏览器/设备/网络。 - 实时语音必带:
audio.append数、最后sequenceNo、最后 ACK、rms/peakAbs、final 文本。 - 如果识别不准,请同时给出“实际说的话”和“识别出来的话”。
- 如果是异步任务,请带
providerCode、任务状态、错误码、回调投递结果。
`API Base URL`、`tenantId`、`x-omnimodal-api-key`、推荐链路、当前稳定边界。如果没有这 4 个,外部团队很容易把平台问题、协议问题和业务问题混在一起排。
先用 `multimodal-entries` 或开发者页联调台确认公网链路,再按你自己的业务场景补异步任务、回调或 realtime,不要直接跳到复杂链路。
建议直接把 交付简报 一起发出去。开发者页负责“跑通”,交付简报负责“统一说法”,两者配合最好。
最值得先看这 8 个
UNAUTHORIZED、FORBIDDEN、INVALID_MULTIMODAL_ENTRY、INVALID_REALTIME_SESSION、REALTIME_CODEC_REQUIRES_PCM16、REALTIME_MESSAGE_FAILED、TASK_NOT_FOUND、RESULT_NOT_FOUND。
先按真实 secret 验签
优先用你租户回调配置里的真实签名密钥验 x-omnimodal-signature,不是直接拿 x-omnimodal-signature-ref 字符串做签名。
按 taskId 去重,按 deliveryId 记投递
回调可能重试,所以你的业务系统建议按 taskId 做最终状态幂等,按 deliveryId 记录每次投递尝试。
先公网验证,再业务嵌入
先在开发者页或 curl 跑通,再接到你的前后端。这样问题边界最清楚,返工最少。
Quick Start
先开通,再拿 key,再跑第一条请求。
自助开通租户
调用公开注册接口,返回 tenantId 和首个 integration_system key。
记录 API Base URL
统一请求入口是 `https://omnimodal.shenliu.cc/api`。
带鉴权头请求
所有租户业务请求统一使用 `x-omnimodal-api-key`。
先跑一条最小链路
建议先跑 `multimodal-entries` 或 `ingestions + tasks + result`。
curl -X POST https://omnimodal.shenliu.cc/api/v1/public/self-serve/signup \
-H 'content-type: application/json' \
-d '{
"companyName": "你的公司",
"contactName": "你的名字",
"phone": "13800000000",
"email": "you@example.com",
"desiredPlan": "starter",
"scenario": "public api test",
"message": "public api test"
}'
`data.tenant.tenantId` 是你的租户 ID,`data.apiKey.secret` 是真实可用的租户级 key。
后续所有业务调用都走 `https://omnimodal.shenliu.cc/api`,请求头带 `x-omnimodal-api-key`。
如果你配置了租户回调,回调 payload 会和 `GET /api/v1/tasks/{taskId}/result` 保持同一套业务结果语义,并额外带 `deliveryId`,方便你做幂等处理和补偿。
先在本页跑最小链路和 `Realtime Playground`,确认鉴权、ACK、partial、final 和服务端诊断都正常。
联调通过后,再把实时语音、多模态入口或异步任务接到你的业务系统,不要把协议问题和业务问题混在一起排。
要给客户或内部团队汇报时,用受控证据页而不是聊天截图,统一说明当前链路、量化结果和边界。
Trial Boundary
这页不只是“怎么调接口”,还要告诉你现在能讲到哪一步。
先讲清主链、证据和边界,后面的试点推进才不会失真。
公网可接入、主链可联调、Realtime Playground 可自助验收、主场景证据和商用门禁已形成统一材料。
不要直接讲“所有场景都已经达到最终商用品质”。真实效果仍取决于输入质量、行业词包、真实样本和当前稳定版窗口。
当你已经跑完联调、拿到一轮真实结果、需要对齐销售/客户/实施时,用证据页展示当前 Readiness、样板链路和边界最合适。
开发者页负责“跑通”,证据页负责“讲清楚”,后台负责“持续运营和治理”。三者不要混着替代。
Auth
对外调用统一使用租户级 API key。
Base URL: `https://omnimodal.shenliu.cc/api`
Header: `x-omnimodal-api-key: 你的 key`
建议由你的后端调用,不要把真实 key 长期暴露在浏览器前端。
`integration_system` 角色适合业务接入,但不能改平台/计费/权限配置。
Multimodal Entry
最简单的统一入口:直接提交文本、文件、图片或语音元数据。
curl -X POST https://omnimodal.shenliu.cc/api/v1/multimodal-entries \
-H 'content-type: application/json' \
-H 'x-omnimodal-api-key: YOUR_KEY' \
-d '{
"tenantId": "YOUR_TENANT_ID",
"scene": "assistant",
"intent": "lookup",
"mode": "text",
"text": "帮我查一下订单 12345 的状态",
"businessContext": {
"sourceSystem": "ziin"
}
}'
{
"success": true,
"data": {
"entryId": "entry_7ad5f81e",
"tenantId": "YOUR_TENANT_ID",
"routingStatus": "accepted",
"scene": "assistant",
"mode": "text",
"skillCode": "order_lookup",
"linkedIngestionIds": [],
"linkedTaskIds": [],
"createdAt": "2026-04-17T13:31:00.000Z"
}
}
Camera Capture
浏览器拍照也能直接接进全模通,但 PoC 和商用接法要分开。
现在开发者页里的相机联调台,已经按正式链路工作:先上传图片拿到受控 `fileUri`,再提交到 `multimodal-entries`。如果你自己的业务系统已有对象存储,也可以继续使用你自己的 `https fileUri + documentType`。
Preset Lane
先选场景,再拍照,参数就不容易填错。
当前是“通用验收”:只验证拍照上传和统一图片入口,不强制生成 OCR 任务。
浏览器相机拍照后,先调用 `POST /api/v1/uploads` 上传,再把返回的 `fileUri` 作为 `images[]` 提交给 `POST /api/v1/multimodal-entries`。
正式环境不要长期依赖超长 data URL。推荐一律先上传拿 `fileUri`,再提交,并补上 `documentType`,这样全模通才能继续自动建 ingestion / task。
当 `images[]` 同时带上 `fileUri + documentType`,并且你的租户对该 `documentType` 已配置可用 provider 路由时,全模通会继续自动生成 ingestion 和解析任务。
当前公开稳定合同已经包含 `POST /api/v1/uploads` 和 `multimodal-entries.images[]`。如果你要做完整“拍照即 OCR/识别”的商用链路,按这两步接就行,不需要再自己猜上传协议。
浏览器端如果已经做了亮度、清晰度、构图预检,建议一并放进 `businessContext`。这样日志、运营台、`GET /tasks/{taskId}/result` 返回体和回调接收方都能统一拿到“是否建议重拍”的信号。
curl -X POST https://omnimodal.shenliu.cc/api/v1/uploads \
-H 'content-type: application/json' \
-H 'x-omnimodal-api-key: YOUR_KEY' \
-d '{
"tenantId": "YOUR_TENANT_ID",
"fileName": "camera-capture.jpg",
"dataUrl": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQ...",
"purpose": "camera_capture",
"sourceSystem": "mobile_web"
}'
curl -X POST https://omnimodal.shenliu.cc/api/v1/multimodal-entries \
-H 'content-type: application/json' \
-H 'x-omnimodal-api-key: YOUR_KEY' \
-d '{
"tenantId": "YOUR_TENANT_ID",
"scene": "camera_capture",
"intent": "document_scan",
"mode": "image",
"images": [
{
"fileUri": "https://example.com/uploads/invoice-001.jpg",
"name": "invoice-001.jpg",
"mimeType": "image/jpeg",
"documentType": "finance.invoice"
}
],
"businessContext": {
"sourceSystem": "mobile_web",
"captureSource": "browser_camera",
"cameraQualityOverall": "suggest_submit",
"cameraBrightnessScore": "72",
"cameraSharpnessScore": "61",
"cameraCoverageScore": "84"
}
}'
只要你在入口侧提交了 `businessContext`,后续任务结果查询和租户回调都会原样带回 `sourceContext`。相机场景里这通常包括 `sourceSystem`、`captureSource`、`cameraQualityOverall` 以及亮度/清晰度/构图分数。
同一任务在下游超时、重试或人工补投时,回调投递会有不同 `deliveryId`。你的业务系统建议同时按 `taskId + deliveryId` 记录投递日志,按 `taskId` 做最终状态去重。
POST https://your-system.example.com/omnimodal/callback
content-type: application/json
x-omnimodal-signature: sha256=YOUR_SIGNATURE
x-omnimodal-signature-ref: vault://tenant_camera_loop_test_d19636/callback-signing
{
"deliveryId": "cb_0025",
"taskId": "task_1776528644197_ipo5t4",
"tenantId": "tenant_camera_loop_test_d19636",
"schemaType": "finance.receipt",
"schemaVersion": "1.0.0",
"confidenceLevel": "medium",
"payload": {
"documentType": "finance.receipt",
"quality": {
"overall": "submit_with_caution",
"missingFields": ["merchantName"],
"retakeAdvice": "请补充票据上沿并提高对焦清晰度。"
}
},
"rawPayloadRef": "result://task_1776528644197_ipo5t4/raw",
"sourceContext": {
"sourceSystem": "developer_portal_camera_receipt",
"captureSource": "browser_camera",
"cameraQualityOverall": "submit_with_caution",
"cameraBrightnessScore": "53",
"cameraSharpnessScore": "41",
"cameraCoverageScore": "77"
}
}
如果租户回调配置启用了 `hmac_sha256`,当前公网实现会对 `taskId` 做 HMAC-SHA256,放进 `x-omnimodal-signature`,格式是 `sha256=...`。`x-omnimodal-signature-ref` 会告诉你当前使用的是哪把签名密钥引用。
当前公开实现是对 `taskId` 签名,不是对整个请求体签名。也就是说,下游要先用你配置的回调密钥对 `taskId` 重新算 HMAC,再与请求头比对。后续如果升级成 body-based 签名,会在开发者页和 OpenAPI 一起公告。
import crypto from "node:crypto";
function verifyOmniModalSignature(headers, payload, signingSecret) {
const headerValue = headers["x-omnimodal-signature"] || "";
const expected = "sha256=" + crypto
.createHmac("sha256", signingSecret)
.update(String(payload.taskId))
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(headerValue),
Buffer.from(expected),
);
}
先开相机、拍一张、上传拿 `fileUri`,再提交到 `multimodal-entries`。这条链路适合你快速验证浏览器侧拍照入口、上传能力、鉴权和公网 API 是否都已打通。
拍照后评估
拍照后评估
拍照后评估
等待拍照后生成 payload。
拍照后会先上传,返回受控 fileUri。
提交后显示 entry / routing / linked ids。
如生成任务,这里会显示 task 状态。
如果生成结果,这里会把关键字段翻成更容易读的摘要。
如果生成结果,这里会把关键字段拆成卡片,方便一眼判断能不能商用。
如生成结果,这里会自动查询并展示解析结果。
Async Task
文件上传编排建议走异步链路:创建入口、发起任务、查询结果。
异步任务的 `providerCode` 必须使用当前租户已经开通并可路由的真实 provider,不要把示例里的占位值当成公网默认值。
对外文档里应固定一条你自己环境里已验证通过的 OCR/解析 provider 组合,再让客户照抄;否则任务可能进入 `dead_lettered`。
如果你希望某类任务更偏低延迟或更偏低成本,可以在 `POST /api/v1/tasks` 的 `sourcePayload.executionPreferences` 里显式传 `strategy`、`preferredProviderCodes`、`blockedProviderCodes`、`disableFallbackProviderCodes`。
curl -X POST https://omnimodal.shenliu.cc/api/v1/ingestions \
-H 'content-type: application/json' \
-H 'x-omnimodal-api-key: YOUR_KEY' \
-d '{
"tenantId": "YOUR_TENANT_ID",
"fileName": "invoice.pdf",
"mimeType": "application/pdf",
"fileUri": "https://example.com/invoice.pdf",
"documentType": "finance.invoice"
}'
curl -X POST https://omnimodal.shenliu.cc/api/v1/tasks \
-H 'content-type: application/json' \
-H 'x-omnimodal-api-key: YOUR_KEY' \
-d '{
"tenantId": "YOUR_TENANT_ID",
"ingestionId": "INGESTION_ID",
"taskType": "document.parse",
"providerCode": "YOUR_ENABLED_PROVIDER",
"sourcePayload": {
"executionPreferences": {
"strategy": "latency_first",
"preferredProviderCodes": ["YOUR_ENABLED_PROVIDER"],
"blockedProviderCodes": [],
"disableFallbackProviderCodes": []
}
}
}'
curl -H 'x-omnimodal-api-key: YOUR_KEY' \
https://omnimodal.shenliu.cc/api/v1/tasks/TASK_ID/result
{
"success": true,
"data": {
"resultId": "res_84f9d7a2",
"taskId": "TASK_ID",
"tenantId": "YOUR_TENANT_ID",
"schemaType": "finance.invoice",
"schemaVersion": "1.0.0",
"payload": {
"schemaType": "finance.invoice",
"schemaVersion": "1.0.0",
"extractionConfidence": "high",
"fields": {
"invoiceNo": "INV-2026-001",
"amount": "2088.00"
}
},
"rawPayloadRef": "mysql:provider_raw_outputs/raw_123",
"confidenceLevel": "high",
"createdAt": "2026-04-17T13:32:10.000Z"
}
}
Realtime Voice
实时语音先创建会话,再通过 WebSocket 持续送音频块。
如果你是第一次接 OmniModal realtime,不要先自己猜 WebSocket 协议。最稳的顺序是:先用本页联调台确认公网链路通,再按下面的 `pcm16 + audioBase64 + 递增 sequenceNo + audio.commit` 去接你的业务前端。
curl -X POST https://omnimodal.shenliu.cc/api/v1/realtime/sessions \
-H 'content-type: application/json' \
-H 'x-omnimodal-api-key: YOUR_KEY' \
-d '{
"tenantId": "YOUR_TENANT_ID",
"scene": "assistant",
"intent": "voice_query",
"mode": "voice",
"preflight": true,
"audioConfig": {
"codec": "pcm16",
"sampleRate": 16000,
"channelCount": 1
},
"tuning": {
"profile": "short_query"
},
"businessContext": {
"sourceSystem": "web"
}
}'
# 返回里会有 asrRouting 和 wsUrl
# asrRouting.status=ready 说明当前租户已解析到可用实时 ASR
# preflight=true 时会额外探测上游连通性,返回 upstreamConnectivity / preflightLatencyMs
# asrRouting.providerCode / adapter 会告诉你这次会话实际走哪条语音链路
# asrRouting.codecSupport / runtimeConfig.codecSupport 会告诉你当前 codec 是否可直接用于公网 realtime
# 然后通过 wss://omnimodal.shenliu.cc/api/v1/realtime/ws?... 建链
# 持续发送 audio.append / audio.commit / session.close
# 注意:audio.append 里的音频字段名必须是 audioBase64
实时语音会话要求 `audioConfig.codec`、`audioConfig.sampleRate`、`audioConfig.channelCount` 三个字段。
现在支持 `tuning.profile` 预设档位。`short_query` 适合短问答和命令式操作,优先更早收尾;`balanced` 适合默认通用场景;`rapid_speech` 适合急促语速和短停顿连续表达,避免过早断句;`dictation` 适合访谈、长句和弱声环境;`noisy_environment` 适合地铁、站台、路边、多人说话等嘈杂环境,会明显拉长收尾等待,避免噪声里过早断句。需要更细调时,再覆盖 `tuning.vadThreshold`、`tuning.silenceDurationMs`、`tuning.commitTimeoutMs`。`daily_life` 场景里,短动作命令如“从头播放 / 继续播放 / 挂断电话”更适合 `short_query`;唤醒词、导航词、音量词如“天猫精灵 / 上一首 / 音量调到50%”更稳的默认是 `balanced`。
生产接入前先检查返回里的 `asrRouting`。如果不是 `ready`,说明当前租户的实时语音提供方还没准备好;要更严格,可以把 `preflight` 设成 `true`。
接口返回的是相对 `wsUrl`,生产接入时拼成 `wss://omnimodal.shenliu.cc/api...` 再建链。
如果没有 `transcript.partial/final`,请查 `GET /api/v1/realtime/sessions/{sessionId}/events`,重点看是否有 `asr.upstream_event`。
如果 `audio.append` 事件里的 `byteLength/rms/peakAbs` 都是 0,说明客户端虽然在发包,但音频字节实际上是空的。
现在 `session.error`、`/metrics`、以及联调台的服务端诊断里会返回 `weakAudioDiagnosis`。如果 level 是 `silent`,优先检查麦克风权限、静音和采集链;如果是 `weak`,优先检查说话距离、增益和前端 PCM 转码强度,不要先怀疑 ASR。
如果前端显示发了很多片,但 `audio.ack.lastAckedSequenceNo` 一直不增长,多半不是 ASR 不准,而是 `sequenceNo`、断线补发或消息格式发错了。
如果你的场景是物流、客服、财务、电商、制造、医疗,建议在 `businessContext.industryCode` 里显式传 `logistics/customer_service/finance/ecommerce/manufacturing/healthcare`。如果是更高频的通用使用,也建议直接传 `daily_life/life_services/business_work`。当前 realtime final transcript 会在落库前按有效词库做一次纠正。
现在可以调用 `GET /api/v1/tenants/{tenantId}/realtime-accuracy-overview` 看最近 realtime 的 `lexiconRefinedCount`、`feedbackRefinedCount`、低置信度 final 和场景分布,不要只凭主观感觉判断词库是否有效。
`GET /api/v1/tenants/{tenantId}/lexicon-benefit-ranking` 可以看哪些场景词库在真正起作用,`GET /api/v1/tenants/{tenantId}/confusion-ranking` 可以看哪些错词最值得优先修。
`GET /api/v1/tenants/{tenantId}/transcript-feedback-candidates` 会返回达到阈值的错词候选,适合放进后台审核池,批量提升成租户词条。
Release Governance
公网可接入,而且仓库侧主场景证据与商用门禁已经跑通;正式放量仍要按稳定版、候选版和回退目标来接。
全模通现在不是“所有能力一次性全量开放然后听天由命”,而是按商用平台的方式管理:仓库侧先补齐场景证据、跑过商用门禁,再进入稳定版候选;生产默认走稳定主链路,候选版本先观察,再决定是否放量;一旦出现健康分回落、死信上升或回调失败放大,就应立即切回回退目标版本。
`daily_life / life_services / business_work` 三条主场景都已有真实 run 证据,`npm run quality:commercial` 已通过。这说明当前仓库状态已经具备下一稳定版候选条件,但不等同于所有公网节点已经自动切换到新 stable。
稳定版
适合正式生产流量。对外宣传时,应以稳定版当前能力、默认 codec 和已通过的验收线为准,不要把实验路径一起打包宣称。
候选版
适合少量租户、专项语料和场景打磨。可以联调、可以灰度,但不应默认作为所有客户的生产入口。
回退目标
一旦出现明显回归,优先回退到上一稳定结论对应的版本,而不是继续扩大放量或临时改接入协议。
审计归档
后台现在会记录观察结论、准入决定和回退依据。接入方做生产验收时,也应保留自己的放量、回退和异常记录。
推荐的商用放量顺序: 1. 先确认仓库侧或候选版本已通过 build / smoke / scene readiness / quality:commercial 2. 再用开发者页联调台验证当前租户 realtime 主链路是否稳定出 partial / final 3. 再看 health score、弱音频诊断、死信和失败回调是否处于健康区间 4. 满足验收线后,把当前版本作为稳定版放量 5. 新版本先作为 candidate 观察,不要直接替换稳定版 6. 如果出现回归,优先切回 rollback target,而不是临时修改协议或前端发包方式
当前公网默认商用 realtime 主链路仍然是 `pcm16 / 16000 / mono`。如果你的系统需要正式放量,建议把这条链路视为稳定默认值,`webm_opus` 只做专项观察,不要作为默认接入策略。
至少同时满足:仓库侧商用门禁已通过、联调台健康分达标、真实业务链路能稳定出 final、租户级回归里没有明显 unhealthy 样本、后台发布结论已记录为“准入稳定版”。
如果能出 final,但 health score 还在 75-89、final latency 较波动、弱音频占比偏高,或者只是某个行业词包刚上线,此时更适合维持 candidate,不要过早扩大放量。
如果同样输入下准确率明显回落、死信上升、回调失败放大,或用户稳定反馈“比上一轮差”,优先回到上一个稳定结论对应的版本,再继续排障和打磨。
SDK Samples
直接复制到项目里跑,先把第一条链路接通。
提交多模态入口
const API_BASE = "https://omnimodal.shenliu.cc/api";
const API_KEY = process.env.OMNIMODAL_API_KEY;
const TENANT_ID = process.env.OMNIMODAL_TENANT_ID;
const response = await fetch(`${API_BASE}/v1/multimodal-entries`, {
method: "POST",
headers: {
"content-type": "application/json",
"x-omnimodal-api-key": API_KEY,
},
body: JSON.stringify({
tenantId: TENANT_ID,
scene: "assistant",
intent: "lookup",
mode: "text",
text: "帮我查一下订单 12345 的状态",
businessContext: {
sourceSystem: "ziin",
},
}),
});
const result = await response.json();
console.log(result);
发起异步任务并查询结果
import os
import requests
API_BASE = "https://omnimodal.shenliu.cc/api"
API_KEY = os.environ["OMNIMODAL_API_KEY"]
TENANT_ID = os.environ["OMNIMODAL_TENANT_ID"]
headers = {
"content-type": "application/json",
"x-omnimodal-api-key": API_KEY,
}
ingestion = requests.post(
f"{API_BASE}/v1/ingestions",
headers=headers,
json={
"tenantId": TENANT_ID,
"fileName": "invoice.pdf",
"mimeType": "application/pdf",
"fileUri": "https://example.com/invoice.pdf",
"documentType": "finance.invoice",
},
).json()
ingestion_id = ingestion["data"]["ingestionId"]
task = requests.post(
f"{API_BASE}/v1/tasks",
headers=headers,
json={
"tenantId": TENANT_ID,
"ingestionId": ingestion_id,
"taskType": "document.parse",
"providerCode": "YOUR_ENABLED_PROVIDER",
},
).json()
task_id = task["data"]["taskId"]
result = requests.get(f"{API_BASE}/v1/tasks/{task_id}/result", headers=headers).json()
print(result)
如果你不想自己处理 `audioBase64`、重连、ACK 跟踪和补发逻辑,可以直接引入全模通提供的浏览器 SDK 文件。
<script src="https://omnimodal.shenliu.cc/assets/omnimodal-realtime-sdk.js"></script>
<script>
const client = new OmniModalRealtime.OmniModalRealtimeClient({
apiOrigin: "https://omnimodal.shenliu.cc",
apiKey: "YOUR_KEY",
tenantId: "YOUR_TENANT_ID",
sessionRequest: {
scene: "assistant",
intent: "voice_query",
mode: "voice",
preflight: true,
audioConfig: {
codec: "pcm16",
sampleRate: 16000,
channelCount: 1,
},
tuning: {
profile: "short_query",
},
businessContext: {
sourceSystem: "browser_sdk",
},
},
});
client.addEventListener("partial", (event) => {
console.log("partial", event.detail.text);
});
client.addEventListener("final", (event) => {
console.log("final", event.detail.text);
});
client.addEventListener("runtimeconfig", (event) => {
console.log("runtime config", event.detail.runtimeConfig);
});
await client.startSession();
await client.connect();
</script>
服务端现在支持会话级重连恢复,`session.ready.resume.lastAckedSequenceNo` 会告诉你已经确认到哪一片;客户端只要补发更大的 `sequenceNo` 即可。
同一会话里 `sequenceNo` 必须单调递增。重复补发老分片时,服务端会返回 `audio.ack.duplicate=true`,但客户端不应把所有分片重新从 1 开始发。
真实语料回归已经证明:当前 realtime 主链路应默认使用 `pcm16/16000/mono`。`webm_opus` 直推在真实中文语料上不稳定,只能作为实验路径;如果输入来自浏览器录音文件,建议先转成 pcm16 再推 realtime。当前公网节点如果不接受 `webm_opus/opus`,会在 `codecSupport` 和 `session.error` 里明确返回“需要先转 pcm16”。
截至 2026-04-19,我们已用历史浏览器 `m4a` 真实录音做过公网黑盒复测。结论是:`m4a -> pcm16/16000/mono -> realtime` 这条链路已经可用,partial/final 能稳定产出;当前更主要的问题是较长真实语音上的 `final latency` 仍偏高,而不是“压缩录音无法识别”。
这组真实 `m4a` 样本的 `commitToFinalLatencyMs` 实测平均只有约 238ms,说明 `audio.commit` 之后系统并没有再额外卡很久。当前更像是 realtime 仍按整句或整段语音收尾后再给 final,所以用户会感觉“比很早之前稳了,但 final 还是稍慢一点”。
先看 `session.ready.runtimeConfig.codecSupport.accepted` 和 `session.ready.asrProviderCode`,再看 `audio.ack.lastAckedSequenceNo` 是否持续增长,最后看 `/events` 里有没有 `asr.upstream_event` 和 `transcript.partial/final`。
点击联调台里的“刷新服务端诊断”后,优先看 `/metrics` 里的 `latestWeakAudioDiagnosis`、`latestFinalMetrics.weakAudioDiagnosis` 和 `latestFinalMetrics.audioSceneDiagnosis`。如果 `peakPercentMax`、`rmsPercentAvg` 很低,且 `weakRatio/silentRatio` 很高,说明问题主要在音频输入;如果 `audioSceneDiagnosis.noiseRiskLevel` 升高,则更像地铁广播、旁人说话或嘈杂环境混入。
联调台和 CLI 脚本现在共用同一套 `health score`。`90-100` 代表这次会话可以作为商用主链路验收;`75-89` 说明主链路基本健康但还有优化空间;`55-74` 代表可用但不建议直接宣称稳定商用;`55` 以下应先排障再放量。
如果你要把全模通作为外部产品的实时语音底座,建议至少满足这 3 条:`realtime-smoke` 单次健康分 `>= 90`、`realtime-benchmark` 平均健康分 `>= 85`、`realtime-commercial-regression` 各 suite 的 `unhealthyCount = 0`。这样才更接近“可商用、可放量、可交付”的状态。
如果你收到回调但验签总是不通过,先确认你用的是租户回调配置里的真实签名密钥,而不是 `signingSecretRef` 字符串本身。只有没有解析到真实密钥时,系统才会回退使用 ref 字符串。
如果头里有 `x-omnimodal-signature-ref`,但你下游没有多租户密钥管理,建议在自己的配置中心维护 `ref -> secret` 映射;不要硬编码到业务代码里。
如果回调重复投递,不要把它当成异常。先按 `taskId` 做最终结果幂等,再按 `deliveryId` 记录每次投递尝试,这样既能保留审计,又不会重复入账或重复落单。
Browser Mic
浏览器麦克风直连:PCM16 采集、100ms 分片、自动 commit。
`OmniModalPcm16Recorder` 已内置浏览器麦克风采集、优先 AudioWorklet、降采样到 16k、转 `pcm16`、按 100ms 切片。你不需要再自己处理 WAV 头或 Float32 转 Int16。
生产环境建议在用户按住说话或点击开始后,`recorder.start()` 和 `client.connect()` 同时开始;结束说话时先 `recorder.stop()`,再 `client.commit()`。
如果你的场景主要是“怎么查订单”“帮我新建客户”这种短句,可以在浏览器侧加 `OmniModalAutoCommitController`。它会在本地检测到一句话结束后的短静音窗口后自动 `commit`,体感会更利落。
<script src="https://omnimodal.shenliu.cc/assets/omnimodal-realtime-sdk.js"></script>
<script>
const client = new OmniModalRealtime.OmniModalRealtimeClient({
apiOrigin: "https://omnimodal.shenliu.cc",
apiKey: "YOUR_KEY",
tenantId: "YOUR_TENANT_ID",
});
const recorder = new OmniModalRealtime.OmniModalPcm16Recorder({
targetSampleRate: 16000,
chunkDurationMs: 100,
});
const autoCommit = new OmniModalRealtime.OmniModalAutoCommitController(
OmniModalRealtime.getRecommendedAutoCommitProfileConfig("short_query"),
);
recorder.addEventListener("chunk", (event) => {
const { pcm16Bytes, stats } = event.detail;
client.sendPcm16Chunk(pcm16Bytes);
autoCommit.observeChunk(stats);
console.log("chunk", stats.sampleCount, stats.rms, stats.peakAbs);
});
autoCommit.addEventListener("autocommit", async () => {
await recorder.stop();
client.commit();
});
client.addEventListener("ack", (event) => {
console.log("acked to", event.detail.lastAckedSequenceNo);
});
client.addEventListener("final", (event) => {
console.log("final", event.detail.text);
});
async function startTalking() {
await client.startSession();
await client.connect();
await recorder.start();
}
async function stopTalking() {
await recorder.stop();
client.commit();
}
</script>
如果你的项目不想直接引入浏览器 SDK,至少要自己做这 6 件事:创建 session、连接 ws、把浏览器采样降到 `16k`、转成 `pcm16`、把每片音频放进 `audioBase64`、结束时显式发送 `audio.commit`。
<script>
const API_BASE = "https://omnimodal.shenliu.cc/api";
const API_KEY = "YOUR_KEY";
let socket;
let mediaStream;
let audioContext;
let sourceNode;
let workletNode;
let chunkBuffer = [];
let chunkSampleCount = 0;
let chunkCount = 0;
let startedAt = 0;
function floatToPcm16(samples) {
const pcm = new Int16Array(samples.length);
for (let i = 0; i < samples.length; i += 1) {
const s = Math.max(-1, Math.min(1, samples[i]));
pcm[i] = s < 0 ? s * 0x8000 : s * 0x7fff;
}
return pcm;
}
function downsampleTo16k(samples, sourceRate) {
if (sourceRate === 16000) return samples;
const ratio = sourceRate / 16000;
const out = new Float32Array(Math.floor(samples.length / ratio));
for (let i = 0; i < out.length; i += 1) {
const start = Math.floor(i * ratio);
const end = Math.min(Math.floor((i + 1) * ratio), samples.length);
let sum = 0;
let count = 0;
for (let j = start; j < end; j += 1) {
sum += samples[j];
count += 1;
}
out[i] = count ? sum / count : 0;
}
return out;
}
function base64FromInt16(int16) {
const bytes = new Uint8Array(int16.buffer);
let binary = "";
const batchSize = 0x8000;
for (let i = 0; i < bytes.length; i += batchSize) {
binary += String.fromCharCode(...bytes.subarray(i, i + batchSize));
}
return btoa(binary);
}
async function createRealtimeSession() {
const response = await fetch(API_BASE + "/v1/realtime/sessions", {
method: "POST",
headers: {
"content-type": "application/json",
"x-omnimodal-api-key": API_KEY,
},
body: JSON.stringify({
tenantId: "YOUR_TENANT_ID",
scene: "assistant",
intent: "voice_query",
mode: "voice",
audioConfig: {
codec: "pcm16",
sampleRate: 16000,
channelCount: 1,
},
tuning: {
profile: "short_query",
},
}),
});
const payload = await response.json();
return payload.data;
}
function sendVoiceChunk(force = false) {
if (!socket || socket.readyState !== WebSocket.OPEN) return;
if (!force && chunkSampleCount < 1600) return; // 约 100ms
if (chunkSampleCount === 0) return;
const merged = new Float32Array(chunkSampleCount);
let offset = 0;
for (const chunk of chunkBuffer) {
merged.set(chunk, offset);
offset += chunk.length;
}
chunkBuffer = [];
chunkSampleCount = 0;
chunkCount += 1;
const pcm16 = floatToPcm16(merged);
const audioBase64 = base64FromInt16(pcm16);
socket.send(JSON.stringify({
type: "audio.append",
seq: chunkCount,
sequenceNo: chunkCount,
audioBase64,
}));
}
async function startTalking() {
const session = await createRealtimeSession();
const wsUrl = new URL("api" + session.wsUrl, window.location.origin);
wsUrl.protocol = location.protocol === "https:" ? "wss:" : "ws:";
socket = new WebSocket(wsUrl);
socket.addEventListener("message", (event) => {
const message = JSON.parse(event.data);
if (message.type === "transcript.partial") console.log("partial", message.text);
if (message.type === "transcript.final") console.log("final", message.text);
});
await new Promise((resolve, reject) => {
socket.addEventListener("open", resolve, { once: true });
socket.addEventListener("error", () => reject(new Error("ws connect failed")), { once: true });
});
startedAt = Date.now();
chunkBuffer = [];
chunkSampleCount = 0;
chunkCount = 0;
mediaStream = await navigator.mediaDevices.getUserMedia({
audio: {
channelCount: 1,
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true,
},
});
audioContext = new AudioContext();
sourceNode = audioContext.createMediaStreamSource(mediaStream);
const processorCode = `
class OmniModalPcmProcessor extends AudioWorkletProcessor {
process(inputs) {
const channel = inputs[0]?.[0];
if (channel?.length) this.port.postMessage(channel.slice(0));
return true;
}
}
registerProcessor("omnimodal-pcm-processor", OmniModalPcmProcessor);
`;
const blob = new Blob([processorCode], { type: "text/javascript" });
const url = URL.createObjectURL(blob);
await audioContext.audioWorklet.addModule(url);
URL.revokeObjectURL(url);
workletNode = new AudioWorkletNode(audioContext, "omnimodal-pcm-processor");
workletNode.port.onmessage = (event) => {
const downsampled = downsampleTo16k(event.data, audioContext.sampleRate);
chunkBuffer.push(downsampled);
chunkSampleCount += downsampled.length;
sendVoiceChunk(false);
};
sourceNode.connect(workletNode);
workletNode.connect(audioContext.destination);
}
async function stopTalking() {
sendVoiceChunk(true);
workletNode?.disconnect();
sourceNode?.disconnect();
await audioContext?.close();
mediaStream?.getTracks().forEach((track) => track.stop());
if (socket?.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify({
type: "audio.commit",
sequenceNo: Math.max(chunkCount, 1),
durationMs: Math.max(0, Date.now() - startedAt),
}));
setTimeout(() => {
if (socket?.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify({ type: "session.close" }));
}
}, 1200);
}
}
</script>
`audio.append` 里的音频字段名必须是 `audioBase64`,不是 `audio`、`data`、`payload.audio`。如果字段名错了,公网看起来像“ws 已连接、也在发包”,但服务端会把音频当空包处理。
当前公网默认商用 realtime 主链路是 `pcm16 / 16000 / mono`。浏览器真实语料回归已经证明,这条路稳定;`webm_opus` 只保留为实验兼容路径,不建议拿来做默认商用接入。
Accuracy Boost
要逼近豆包级体验,不能只靠一个 ASR 模型,要靠“行业词库 + final 修正 + 反馈回灌”。
全模通已内置通用中文词库,以及物流、客服、财务、企业助手、电商、制造、医疗行业词库。Realtime 目前先对 `final transcript` 做词库纠正,优先保守,避免影响实时 partial 的低延迟体验。
每个租户都可以维护自己的专属热词、错词、品牌词、业务词。推荐把组织名、产品名、模块名、客户名、单据名都沉淀进去。
如果用户发现 final transcript 有误,不要只在前端改字。把“原识别文本 -> 用户修正文本”提交回全模通,系统会形成高频混淆对,帮助你决定哪些词应该提升为租户词库。
curl -X POST https://omnimodal.shenliu.cc/api/v1/realtime/sessions \
-H 'content-type: application/json' \
-H 'x-omnimodal-api-key: YOUR_KEY' \
-d '{
"tenantId": "YOUR_TENANT_ID",
"scene": "assistant",
"intent": "voice_query",
"mode": "voice",
"audioConfig": {
"codec": "pcm16",
"sampleRate": 16000,
"channelCount": 1
},
"businessContext": {
"sourceSystem": "ziin",
"industryCode": "logistics"
}
}'
curl -X PUT https://omnimodal.shenliu.cc/api/v1/tenants/YOUR_TENANT_ID/lexicons \
-H 'content-type: application/json' \
-H 'x-omnimodal-api-key: YOUR_KEY' \
-d '{
"industryCode": "logistics",
"name": "物流高频词补丁",
"description": "补充我们真实业务里的高频词",
"priority": 70,
"terms": [
{
"canonical": "签收回单",
"aliases": ["签售回单", "签收回到"],
"category": "document",
"weight": 95,
"source": "manual",
"status": "active"
}
]
}'
curl -X POST https://omnimodal.shenliu.cc/api/v1/tenants/YOUR_TENANT_ID/transcript-feedback \
-H 'content-type: application/json' \
-H 'x-omnimodal-api-key: YOUR_KEY' \
-d '{
"sessionId": "rtsess_xxx",
"utteranceId": "utt_xxx",
"industryCode": "logistics",
"sourceSystem": "ziin",
"originalText": "签售回单怎么查询",
"correctedText": "签收回单怎么查询",
"note": "业务员口音导致误识别"
}'
curl -H 'x-omnimodal-api-key: YOUR_KEY' \ 'https://omnimodal.shenliu.cc/api/v1/tenants/YOUR_TENANT_ID/transcript-feedback?industryCode=logistics&limit=20'
Realtime Playground
直接在页面里开麦联调,验证公网实时语音主链路。
填入 `API key` 和 `tenantId` 后,直接开始录音。你会看到会话状态、ACK 推进、实时 partial/final、最近一片音频能量,以及服务端指标与事件。
1 分钟快速验收
给第一次接入的人看的最短流程,不需要懂协议。按顺序做,最后看“是否达到上线试用线”和“通过 / 有条件通过 / 不通过”即可。
- 填入 `tenantId` 和 `API key`,点击“开始录音”。
- 连续说 1 条完整短句,不要只说“喂喂喂”。
- 点击“停止并提交”,等待自动诊断结束。
- 确认是否出现 `transcript.final`,并看本地麦克风是否不是静音。
- 如果状态是“通过”或“有条件通过”,再继续做 5 条推荐短句回归。
尚未创建会话
provider: -
chunks: 0 / reconnect: 0
sampleCount / rms
等待语音输入...
等待最终结果...
开始录音后会判断本地 PCM16 是否接近静音、偏弱或可用。
等待服务端 metrics。拿到 final / weakAudioDiagnosis / 关键事件后会计算单次会话健康分。
创建会话并说一句完整短句后,这里会总结建链、ACK、VAD、partial 和 final 的推进情况。
speech_started=无 / speech_stopped=无 / commit_failed=无
avgVad=- / avgFinal=-
先完成一次录音联调。
报告会给使用者一个非技术结论:能不能继续接入、问题更像在哪一层、下一步该做什么。
建议下一步:说一句完整短句,停止并提交,然后等待自动诊断。
暂未判定
完成一次录音并拿到诊断后,这里会明确告诉你本次是否达到上线试用线。
建议下一步:先录一条完整短句,再看是否有 final、健康分和本地麦克风状态。
接入方自助验收清单
第一次接入时,不要随便说几句就下结论。先按这 5 条短句测,再对照右侧状态判断是否达到“可上线试用”的最低标准。
你好,请帮我查一下今天的订单。我想查询客户资料在哪里。怎么新建一个订单。帮我看一下这个运单状态。请告诉我刚才那条记录怎么修改。
- 结论为“通过”或“有条件通过”。
- 本地麦克风不是
silent。 - ACK 能持续推进,且至少出现一次
transcript.final。 - 健康分建议至少
85 / 100。 - 5 条里至少 4 条能稳定出 final,再讨论准确率调优。
请把 5 条推荐短句测完,再根据每条结果汇总本轮验收结论。
建议下一步:每测完一条短句,就手动标记本条结果,直到 5 条全部完成。
点击“刷新服务端诊断”后生成中文摘要。
当前还没有服务端诊断结果。
建议下一步:先创建会话并说一句完整短句,再刷新服务端诊断。
点击“刷新服务端诊断”后显示 metrics / events。
API Catalog
机器可读接口清单已经公开,而且带字段和示例。
公开接口统一返回 { "success": false, "error": { "code", "message" } }。接入时建议业务侧至少记录 error.code、HTTP 状态码、请求链路 ID 和当前 tenantId。
`UNAUTHORIZED` 通常是 key 不对;`FORBIDDEN` 通常是租户或角色越界;`INVALID_REALTIME_SESSION` 通常是 realtime 请求体不合规;`REALTIME_CODEC_REQUIRES_PCM16` 说明当前节点要求你先转 pcm16;`REALTIME_MESSAGE_FAILED` 更像上游 ASR 或实时消息阶段失败。
接入建议最少处理这些错误码: - UNAUTHORIZED: 没带有效 key,或 key 已停用 - FORBIDDEN: 当前 key 不能访问这个租户或这类接口 - INVALID_MULTIMODAL_ENTRY: 多模态入口请求体不合法 - INVALID_INGESTION_INPUT / INVALID_TASK_INPUT: 异步任务参数不合法 - INVALID_REALTIME_SESSION: realtime session 请求体不合法 - REALTIME_CODEC_REQUIRES_PCM16: 当前节点要求你先转成 pcm16 / 16k / mono - REALTIME_MESSAGE_FAILED: realtime 消息链路或上游 ASR 阶段失败 - TASK_NOT_FOUND / RESULT_NOT_FOUND: taskId/resultId 不存在或未准备好
如果你接租户回调,建议固定记录 x-omnimodal-signature、x-omnimodal-signature-ref、deliveryId、taskId。验签通过后再落业务结果,不通过要保留原始报文方便排查。
这一页适合“人看”,OpenAPI 适合“机器接”。推荐把这一页发给工程负责人,把 OpenAPI JSON 交给生成 SDK、校验或 API 网关的团队。