Publish Sophon Realtime calling tool protocol and latency guide
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
.git
|
||||
dist
|
||||
node_modules
|
||||
*.log
|
||||
work
|
||||
.env*
|
||||
@@ -0,0 +1,6 @@
|
||||
dist/
|
||||
node_modules/
|
||||
.DS_Store
|
||||
.env*
|
||||
work/
|
||||
*.log
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
FROM node:22-alpine AS build
|
||||
WORKDIR /app
|
||||
COPY package.json build.mjs index.html styles.css app.js model.js favicon.svg robots.txt release.json ./
|
||||
RUN npm run build
|
||||
|
||||
FROM nginx:1.28-alpine
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
COPY --from=build --chmod=0644 /app/dist/ /usr/share/nginx/html/
|
||||
EXPOSE 8080
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 CMD wget -q -O /dev/null http://127.0.0.1:8080/healthz || exit 1
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 K1412
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,15 @@
|
||||
# Sophon Realtime 调用与工具机制研究
|
||||
|
||||
独立研究网页,提供模型构建、调用入口、工具运行流程、公开指标、时延口径与协议覆盖的源代码核对。
|
||||
|
||||
公开地址为 https://sophon-realtime.k1412.top/ 。原模型项目为 https://github.com/adventists-ai/sophon-realtime 。
|
||||
|
||||
## 开发
|
||||
|
||||
使用 Node.js 20 或更新版本,运行 `npm run check` 与 `npm run build`。运行 `npm run dev` 后可在终端显示的本地地址预览。
|
||||
|
||||
页面全部使用本地静态资源。交互生成启动步骤与协议片段,工具过程使用显式标注的合成示例。GPU推理与真实业务工具执行的评测见正文验收边界。
|
||||
|
||||
## 许可
|
||||
|
||||
本站原创代码、文字与绘图代码采用 MIT 许可。外部模型、代码与数据遵循其自身许可和声明,正文列出了需要澄清的上游声明差异。
|
||||
@@ -0,0 +1,150 @@
|
||||
import { builds, callModes, makeRecipe, toolSteps } from "./model.js";
|
||||
|
||||
const buildSelect = document.getElementById("build-select");
|
||||
const modeSelect = document.getElementById("mode-select");
|
||||
let currentBuild = "flash";
|
||||
let currentStep = 0;
|
||||
let currentPart = "startup";
|
||||
|
||||
function renderBuild() {
|
||||
const selected = builds[currentBuild];
|
||||
const flow = document.getElementById("build-flow");
|
||||
flow.replaceChildren();
|
||||
selected.flow.forEach((label, index) => {
|
||||
if (index) {
|
||||
const arrow = document.createElement("b");
|
||||
arrow.textContent = "→";
|
||||
arrow.setAttribute("aria-hidden", "true");
|
||||
flow.append(arrow);
|
||||
}
|
||||
const node = document.createElement("span");
|
||||
node.textContent = label;
|
||||
flow.append(node);
|
||||
});
|
||||
document.getElementById("build-detail").textContent = selected.detail;
|
||||
document.querySelectorAll("[data-build]").forEach((button) => {
|
||||
button.setAttribute(
|
||||
"aria-pressed",
|
||||
String(button.dataset.build === currentBuild),
|
||||
);
|
||||
});
|
||||
buildSelect.value = currentBuild;
|
||||
const previousMode = modeSelect.value;
|
||||
modeSelect.replaceChildren();
|
||||
for (const key of selected.modes) {
|
||||
const option = document.createElement("option");
|
||||
option.value = key;
|
||||
option.textContent = callModes[key].title;
|
||||
modeSelect.append(option);
|
||||
}
|
||||
modeSelect.value = selected.modes.includes(previousMode)
|
||||
? previousMode
|
||||
: selected.modes[0];
|
||||
renderRecipe();
|
||||
}
|
||||
|
||||
function renderRecipe() {
|
||||
const mode = callModes[modeSelect.value];
|
||||
document.getElementById("endpoint-kind").textContent = mode.kind;
|
||||
document.getElementById("endpoint-value").textContent = mode.endpoint;
|
||||
document.getElementById("endpoint-detail").textContent = mode.detail;
|
||||
document.getElementById("recipe-label").textContent =
|
||||
`${builds[currentBuild].title} · ${mode.title}`;
|
||||
document.getElementById("call-recipe").textContent = makeRecipe(
|
||||
currentBuild,
|
||||
modeSelect.value,
|
||||
)[currentPart];
|
||||
document.querySelectorAll("[data-part]").forEach((button) => {
|
||||
button.setAttribute(
|
||||
"aria-pressed",
|
||||
String(button.dataset.part === currentPart),
|
||||
);
|
||||
});
|
||||
document.getElementById("copy-status").textContent = "";
|
||||
}
|
||||
|
||||
function renderStep() {
|
||||
const selected = toolSteps[currentStep];
|
||||
document.getElementById("step-number").textContent = String(
|
||||
currentStep + 1,
|
||||
).padStart(2, "0");
|
||||
document.getElementById("step-title").textContent = selected.title;
|
||||
document.getElementById("step-explanation").textContent =
|
||||
selected.explanation;
|
||||
document.getElementById("step-code").textContent = selected.code;
|
||||
document.getElementById("step-progress").textContent =
|
||||
`${currentStep + 1} / ${toolSteps.length}`;
|
||||
document.getElementById("previous-step").disabled = currentStep === 0;
|
||||
document.getElementById("next-step").disabled =
|
||||
currentStep === toolSteps.length - 1;
|
||||
document.querySelectorAll("[data-step]").forEach((button) => {
|
||||
button.setAttribute(
|
||||
"aria-pressed",
|
||||
String(Number(button.dataset.step) === currentStep),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const stepList = document.getElementById("tool-steps");
|
||||
toolSteps.forEach((step, index) => {
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.dataset.step = String(index);
|
||||
button.textContent = `${index + 1} ${step.title}`;
|
||||
button.addEventListener("click", () => {
|
||||
currentStep = index;
|
||||
renderStep();
|
||||
});
|
||||
stepList.append(button);
|
||||
});
|
||||
|
||||
document.querySelectorAll("[data-build]").forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
currentBuild = button.dataset.build;
|
||||
renderBuild();
|
||||
});
|
||||
});
|
||||
buildSelect.addEventListener("change", () => {
|
||||
currentBuild = buildSelect.value;
|
||||
renderBuild();
|
||||
});
|
||||
modeSelect.addEventListener("change", renderRecipe);
|
||||
document.querySelectorAll("[data-part]").forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
currentPart = button.dataset.part;
|
||||
renderRecipe();
|
||||
});
|
||||
});
|
||||
document.getElementById("previous-step").addEventListener("click", () => {
|
||||
currentStep -= 1;
|
||||
renderStep();
|
||||
});
|
||||
document.getElementById("next-step").addEventListener("click", () => {
|
||||
currentStep += 1;
|
||||
renderStep();
|
||||
});
|
||||
document.getElementById("copy-recipe").addEventListener("click", async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(
|
||||
document.getElementById("call-recipe").textContent,
|
||||
);
|
||||
document.getElementById("copy-status").textContent = "已复制调用示例。";
|
||||
} catch {
|
||||
document.getElementById("copy-status").textContent =
|
||||
"可以在上方代码中手动选择并复制。";
|
||||
}
|
||||
});
|
||||
|
||||
const themeButton = document.getElementById("theme-toggle");
|
||||
themeButton.addEventListener("click", () => {
|
||||
const theme =
|
||||
document.documentElement.dataset.theme === "dark" ? "light" : "dark";
|
||||
document.documentElement.dataset.theme = theme;
|
||||
themeButton.setAttribute(
|
||||
"aria-label",
|
||||
theme === "dark" ? "切换浅色主题" : "切换深色主题",
|
||||
);
|
||||
});
|
||||
|
||||
renderBuild();
|
||||
renderStep();
|
||||
@@ -0,0 +1,17 @@
|
||||
import { mkdirSync, copyFileSync } from "node:fs";
|
||||
const files = [
|
||||
"index.html",
|
||||
"styles.css",
|
||||
"app.js",
|
||||
"model.js",
|
||||
"favicon.svg",
|
||||
"robots.txt",
|
||||
"release.json",
|
||||
];
|
||||
mkdirSync(new URL("./dist/", import.meta.url), { recursive: true });
|
||||
for (const file of files)
|
||||
copyFileSync(
|
||||
new URL(file, import.meta.url),
|
||||
new URL(`dist/${file}`, import.meta.url),
|
||||
);
|
||||
console.log(`Built ${files.length} public assets.`);
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="12" fill="#287557"/><path d="M17 19v27m0-11 16-16m-13 13 15 14" stroke="#f7f6f2" stroke-width="5" fill="none" stroke-linecap="round"/><circle cx="45" cy="44" r="4" fill="#f7f6f2"/></svg>
|
||||
|
After Width: | Height: | Size: 280 B |
+796
@@ -0,0 +1,796 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<meta
|
||||
name="description"
|
||||
content="基于公开代码核对 Sophon Realtime 的自托管入口、工具调用机制、Live委派与时延,提供调用路径和可复核的协议示例。"
|
||||
/>
|
||||
<meta name="theme-color" content="#f4f3ef" />
|
||||
<meta
|
||||
property="og:title"
|
||||
content="Sophon Realtime 的调用、工具机制与时延"
|
||||
/>
|
||||
<meta property="og:type" content="article" />
|
||||
<link rel="canonical" href="https://sophon-realtime.k1412.top/" />
|
||||
<link rel="icon" type="image/svg+xml" href="favicon.svg" />
|
||||
<link rel="stylesheet" href="styles.css" />
|
||||
<script type="module" src="app.js"></script>
|
||||
<title>Sophon Realtime 的调用、工具机制与时延 · K1412 Research</title>
|
||||
</head>
|
||||
<body id="top">
|
||||
<a class="skip-link" href="#main">跳到正文</a>
|
||||
<header class="site-header">
|
||||
<a class="brand" href="https://blog.k1412.top"
|
||||
><b>k.</b><span>K1412 <i>RESEARCH</i></span></a
|
||||
>
|
||||
<nav aria-label="顶部导航">
|
||||
<a href="#calling">调用入口</a><a href="#tools">工具机制</a
|
||||
><a href="https://voice-duplex.k1412.top/">双工评测 ↗</a
|
||||
><button id="theme-toggle" type="button" aria-label="切换深浅主题">
|
||||
◐
|
||||
</button>
|
||||
</nav>
|
||||
</header>
|
||||
<div class="layout">
|
||||
<aside class="contents">
|
||||
<span class="eyebrow">MODEL STUDY / 02</span>
|
||||
<nav aria-label="文章目录">
|
||||
<a href="#readiness">01 可调用性</a
|
||||
><a href="#architecture">02 模型与构建</a
|
||||
><a href="#calling">03 接口与调用</a><a href="#tools">04 工具机制</a
|
||||
><a href="#capability">05 工具能力证据</a
|
||||
><a href="#speed">06 与 GPT-Live 的时延</a
|
||||
><a href="#limits">07 兼容性与验证</a><a href="#sources">08 来源</a>
|
||||
</nav>
|
||||
<div class="version-note">
|
||||
资料核对<br /><strong>2026.11.09</strong><br /><span
|
||||
>代码 b2754d47<br />GPU 推理待实测</span
|
||||
>
|
||||
</div>
|
||||
</aside>
|
||||
<main id="main">
|
||||
<section class="hero">
|
||||
<span class="eyebrow">PUBLIC MODEL · SOURCE AUDIT · API GUIDE</span>
|
||||
<h1>Sophon Realtime 的<br />调用、工具机制与时延</h1>
|
||||
<p class="deck">
|
||||
沿着模型、协议与执行程序,<br />核对一条语音请求怎样真正调用工具。
|
||||
</p>
|
||||
<div class="hero-meta">
|
||||
<span>模型研究</span><span>2026 年 11 月 9 日</span
|
||||
><a href="https://github.com/adventists-ai/sophon-realtime"
|
||||
>原始项目 ↗</a
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
class="hero-route"
|
||||
aria-label="语音经过模型生成结构化调用,外部工具执行后回到语音回应"
|
||||
>
|
||||
<span>语音</span><b>→</b><span>模型</span><b>→</b
|
||||
><code><tool_call></code><b>→</b><span>外部执行</span><b>→</b
|
||||
><span>语音回应</span>
|
||||
</div>
|
||||
<p class="lead">
|
||||
Sophon 已公开模型权重、服务代码和客户端示例。Tag 与 Flash
|
||||
提供自托管调用路径,Live
|
||||
网关连接前台语音与后台代理。实际接入需要同时核对<strong>模型能力、协议覆盖、外部执行和时延口径</strong>。
|
||||
</p>
|
||||
<p>
|
||||
本页以公开仓库提交
|
||||
<code>b2754d4753e20ecfa860db0cda4325e4479f4427</code>
|
||||
为代码依据。作者测量、代码观察和说明性示例分别标记,GPU
|
||||
端到端调用列为待验证项。
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="readiness" class="article-section">
|
||||
<div class="section-number">01 / READINESS</div>
|
||||
<h2>当前有哪些可调用路径</h2>
|
||||
<p>
|
||||
Tag、Flash 和 E2E 均有公开权重目录。前两种构建走 vLLM-Omni,E2E
|
||||
使用单独的研究预览引擎。仓库还提供客户端、启动脚本和网关,已经具备可检查的部署链路。<a
|
||||
class="ref"
|
||||
href="#source-root"
|
||||
>[1]</a
|
||||
>
|
||||
</p>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>构建</th>
|
||||
<th>作者文档中的资源</th>
|
||||
<th>主要入口</th>
|
||||
<th>当前定位</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Tag</th>
|
||||
<td>单张 80 GB NVIDIA GPU,实例约 65 GB 显存</td>
|
||||
<td>HTTP + Realtime WebSocket</td>
|
||||
<td>强调笑声、叹气等表现力</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Flash</th>
|
||||
<td>单张 80 GB NVIDIA GPU,实例约 55 GB 显存</td>
|
||||
<td>HTTP + Realtime WebSocket</td>
|
||||
<td>强调首音速度与并发</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>E2E</th>
|
||||
<td>两张 80 GB GPU</td>
|
||||
<td>独立网关 <code>/ws</code></td>
|
||||
<td>连续向量语音生成的研究预览</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p class="source-line">
|
||||
资源与入口来源
|
||||
<a
|
||||
href="https://github.com/adventists-ai/sophon-realtime/blob/b2754d4753e20ecfa860db0cda4325e4479f4427/tag/README.md"
|
||||
>Tag</a
|
||||
>、<a
|
||||
href="https://github.com/adventists-ai/sophon-realtime/blob/b2754d4753e20ecfa860db0cda4325e4479f4427/flash/README.md"
|
||||
>Flash</a
|
||||
>、<a
|
||||
href="https://github.com/adventists-ai/sophon-realtime/blob/b2754d4753e20ecfa860db0cda4325e4479f4427/docs/deploy.md"
|
||||
>E2E 部署文档</a
|
||||
>。
|
||||
</p>
|
||||
<div class="evidence-note">
|
||||
<strong>自托管与在线体验分别确认</strong>
|
||||
<p>
|
||||
官方模型卡给出了 ModelScope
|
||||
体验页。当前核对支持“模型和服务代码可取得”,可直接申请密钥的
|
||||
Sophon 托管 API
|
||||
尚缺公开调用资料,体验页的实际语音会话也需单独验证。PyPI 已有
|
||||
<code>sophon-realtime 1.0.0</code>,Tag / Flash 脚本使用 Python
|
||||
3.12。<a
|
||||
href="https://www.modelscope.cn/studios/AdventistsAI/Sophon-Realtime"
|
||||
>体验页 ↗</a
|
||||
>
|
||||
<a href="https://pypi.org/project/sophon-realtime/">PyPI ↗</a>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="architecture" class="article-section">
|
||||
<div class="section-number">02 / ARCHITECTURE</div>
|
||||
<h2>三种构建共享怎样的语音骨干</h2>
|
||||
<p>
|
||||
共同骨干由语音特征进入 32B 模型,承担理解、话轮决策和文本回答。Tag
|
||||
与 Flash 用不同语音模块朗读带表现标签的回答。E2E
|
||||
则把语音生成改为连续向量与专用解码。<a
|
||||
class="ref"
|
||||
href="#source-root"
|
||||
>[1]</a
|
||||
>
|
||||
</p>
|
||||
<div class="figure">
|
||||
<div class="figure-heading">
|
||||
<div>
|
||||
<span class="eyebrow">BUILD EXPLORER</span>
|
||||
<h3>选择构建,查看生成路径</h3>
|
||||
</div>
|
||||
<span class="tag audit">代码与文档核对</span>
|
||||
</div>
|
||||
<div class="segmented" role="group" aria-label="选择 Sophon 构建">
|
||||
<button type="button" data-build="flash" aria-pressed="true">
|
||||
Flash</button
|
||||
><button type="button" data-build="tag" aria-pressed="false">
|
||||
Tag</button
|
||||
><button type="button" data-build="e2e" aria-pressed="false">
|
||||
E2E 预览
|
||||
</button>
|
||||
</div>
|
||||
<div id="build-flow" class="flow" aria-live="polite"></div>
|
||||
<div
|
||||
id="build-detail"
|
||||
class="selected-detail"
|
||||
aria-live="polite"
|
||||
></div>
|
||||
</div>
|
||||
<p>
|
||||
Tag 的 NOTICE 列出 Qwen3-VL-32B、DuplexJev-32B-Para、Qwen3-ASR
|
||||
编码器及 tag-tts
|
||||
等来源。当前构建把语言、声音和话轮决策组合起来,“一个服务”与内部推理阶段数需要分别描述。<a
|
||||
class="ref"
|
||||
href="#source-notice"
|
||||
>[2]</a
|
||||
>
|
||||
</p>
|
||||
<p>
|
||||
Flash 的 80 ms 描述语音生成帧。持续听说的响应还受
|
||||
VAD、话轮判断、生成调度和播放缓存影响。配置中仍可见外部语音活动检测,模型话轮判断与工程控制共同参与交互。<a
|
||||
class="ref"
|
||||
href="#source-flash"
|
||||
>[3]</a
|
||||
>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="calling" class="article-section">
|
||||
<div class="section-number">03 / CALLING</div>
|
||||
<h2>从语音接口到工具接口</h2>
|
||||
<p>
|
||||
先确认语音引擎正常返回,再检查连续听说,最后接真实工具和后台。以下示例含可复制的启动步骤与协议片段。模型权重应固定
|
||||
revision,并预先放入相应 <code>MODEL_DIR</code>。
|
||||
</p>
|
||||
<div class="figure">
|
||||
<div class="figure-heading">
|
||||
<div>
|
||||
<span class="eyebrow">REQUEST GUIDE</span>
|
||||
<h3>按任务选择调用入口</h3>
|
||||
</div>
|
||||
<span class="tag example">调用示例</span>
|
||||
</div>
|
||||
<div class="call-controls">
|
||||
<label for="build-select"
|
||||
>模型构建<select id="build-select">
|
||||
<option value="flash">Flash</option>
|
||||
<option value="tag">Tag</option>
|
||||
<option value="e2e">E2E 预览</option>
|
||||
</select></label
|
||||
><label for="mode-select"
|
||||
>接口路径<select id="mode-select"></select
|
||||
></label>
|
||||
</div>
|
||||
<div class="endpoint">
|
||||
<span id="endpoint-kind">HTTP</span
|
||||
><code id="endpoint-value"></code>
|
||||
</div>
|
||||
<p id="endpoint-detail" class="small"></p>
|
||||
<div class="recipe-parts" role="group" aria-label="示例内容">
|
||||
<button type="button" data-part="startup" aria-pressed="true">
|
||||
启动与验证</button
|
||||
><button type="button" data-part="request" aria-pressed="false">
|
||||
协议消息
|
||||
</button>
|
||||
</div>
|
||||
<div class="code-heading">
|
||||
<span id="recipe-label"></span
|
||||
><button type="button" id="copy-recipe">复制示例</button>
|
||||
</div>
|
||||
<pre><code id="call-recipe"></code></pre>
|
||||
<p id="copy-status" class="small" aria-live="polite"></p>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>端口与角色</th>
|
||||
<th>协议</th>
|
||||
<th>接入要求</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>8000 基础引擎</th>
|
||||
<td>
|
||||
<code>/v1/chat/completions</code><br /><code
|
||||
>/v1/realtime</code
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
Tag / Flash
|
||||
语音生成与双工引擎。真实工具需显式选择客户端模式并接工具桥。
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>8700 本地工具桥</th>
|
||||
<td>
|
||||
<code>/bridge/<key>/events</code><br /><code
|
||||
>/bridge/<key>/result</code
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
默认由引擎启动脚本按
|
||||
<code>PORT + 700</code>
|
||||
创建,绑定本机,用于外层网关取调用与回填结果。
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>8780 外层网关</th>
|
||||
<td>
|
||||
<code>/v1/realtime</code><br /><code
|
||||
>/v1/live/sessions</code
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
分别适配函数调用事件与 Live 客户端委派。额外 ASR 约占 2 GB
|
||||
GPU 显存。
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>E2E 独立网关</th>
|
||||
<td><code>/ws</code></td>
|
||||
<td>
|
||||
使用自身的配置、音频、打断与结束事件,按研究预览部署文档接入。
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p class="source-line">
|
||||
接口依据
|
||||
<a
|
||||
href="https://github.com/adventists-ai/sophon-realtime/blob/b2754d4753e20ecfa860db0cda4325e4479f4427/live/sophon_live/gateway.py"
|
||||
>gateway.py</a
|
||||
>、<a
|
||||
href="https://github.com/adventists-ai/sophon-realtime/blob/b2754d4753e20ecfa860db0cda4325e4479f4427/live/sophon_live/adapters.py"
|
||||
>adapters.py</a
|
||||
>
|
||||
与各构建文档。
|
||||
</p>
|
||||
<h3>HTTP 音频流有自己的数据形状</h3>
|
||||
<p>
|
||||
公开客户端先读 <code>GET /v1/models</code>,使用官方系统提示和
|
||||
<code>input_audio</code> 发送完整 WAV。响应按 SSE 流式返回,顶层
|
||||
<code>modality == "audio"</code> 时,音频数据位于
|
||||
<code>choices[].delta.content</code>,内容为 base64 WAV 块,24
|
||||
kHz。接入 SDK 时应按该实际字段解码。<a
|
||||
class="ref"
|
||||
href="#source-client"
|
||||
>[4]</a
|
||||
>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="tools" class="article-section">
|
||||
<div class="section-number">04 / TOOL CALLING</div>
|
||||
<h2>提示词、调用标记和外部执行怎样配合</h2>
|
||||
<p>
|
||||
运行时把函数 schema 送入 chat template,模型按 Qwen/Hermes
|
||||
格式输出函数名与参数。解析器读取
|
||||
<code><tool_call></code>
|
||||
分隔符,语音模块只接收前面的口语内容。外部程序执行函数后,结果回到模型上下文中,模型再生成语音回应。<a
|
||||
class="ref"
|
||||
href="#source-tools"
|
||||
>[5]</a
|
||||
>
|
||||
</p>
|
||||
<div class="figure">
|
||||
<div class="figure-heading">
|
||||
<div>
|
||||
<span class="eyebrow">CALL TRACE</span>
|
||||
<h3>逐步查看一条工具请求</h3>
|
||||
</div>
|
||||
<span class="tag example">合成示例</span>
|
||||
</div>
|
||||
<div
|
||||
id="tool-steps"
|
||||
class="step-list"
|
||||
role="group"
|
||||
aria-label="工具调用步骤"
|
||||
></div>
|
||||
<div class="step-head">
|
||||
<span id="step-number"></span>
|
||||
<h4 id="step-title"></h4>
|
||||
</div>
|
||||
<p id="step-explanation"></p>
|
||||
<pre><code id="step-code"></code></pre>
|
||||
<div class="step-controls">
|
||||
<button type="button" id="previous-step">上一步</button
|
||||
><span id="step-progress" aria-live="polite"></span
|
||||
><button type="button" id="next-step">下一步</button>
|
||||
</div>
|
||||
</div>
|
||||
<h3>“特殊 token”需要看两个层次</h3>
|
||||
<p>
|
||||
已核对的 Tag tokenizer 中,<code><tool_call></code> 与
|
||||
<code></tool_call></code> 对应独立词表 ID
|
||||
<code>151657</code> 和 <code>151658</code>,其
|
||||
<code>special</code> 字段均为
|
||||
<code>false</code>。服务代码再按分隔符字符串解析
|
||||
JSON。标记负责表示调用,函数执行由运行时和客户端完成。<a
|
||||
class="ref"
|
||||
href="#source-tokenizer"
|
||||
>[6]</a
|
||||
>
|
||||
</p>
|
||||
<div class="two-paths">
|
||||
<div>
|
||||
<span class="eyebrow">DIRECT TOOLS</span>
|
||||
<h3>前台生成业务工具调用</h3>
|
||||
<p>
|
||||
给模型具体函数定义,例如
|
||||
<code>get_weather(city)</code>。模型填写函数名和参数,网关转换成
|
||||
<code>function_call</code> 事件,客户端执行并回传
|
||||
<code>function_call_output</code>。
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<span class="eyebrow">LIVE DELEGATION</span>
|
||||
<h3>前台把任务交给后台</h3>
|
||||
<p>
|
||||
前台只拿到 <code>delegate(task)</code>。网关发出
|
||||
<code>session.delegation.created</code
|
||||
>,后台代理使用业务工具,结果经
|
||||
<code>session.commentary.append</code> 返回语音前台。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
启用 <code>SOPHON_LIVE_BACKEND</code> 后,代码会把前台工具替换为
|
||||
<code>delegate</code
|
||||
>,由配置的后台模型持有具体工具。该配置的表现应归于前后台系统。<a
|
||||
class="ref"
|
||||
href="#source-backend"
|
||||
>[7]</a
|
||||
>
|
||||
</p>
|
||||
<div class="evidence-note">
|
||||
<strong>真实工具执行要显式接通</strong>
|
||||
<p>
|
||||
基础引擎的默认工具模式是
|
||||
mock。未提供匹配模拟结果时,代码仍可返回含
|
||||
<code>ok: true</code> 和
|
||||
<code>note: "mock result"</code> 的对象。真实调用需要
|
||||
<code>tool_mode: "client"</code>、会话工具桥及执行端。8780
|
||||
网关适配器会设置客户端模式。<a
|
||||
href="https://github.com/adventists-ai/sophon-realtime/blob/b2754d4753e20ecfa860db0cda4325e4479f4427/tag/plugin/sophon_realtime/duplex.py"
|
||||
>引擎代码 ↗</a
|
||||
>
|
||||
<a
|
||||
href="https://github.com/adventists-ai/sophon-realtime/blob/b2754d4753e20ecfa860db0cda4325e4479f4427/live/sophon_live/adapters.py"
|
||||
>适配器 ↗</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
<p>
|
||||
公开微调样例也采用函数 schema、助手调用标记和
|
||||
<code>role: tool</code>
|
||||
结果。这展示了工具格式的训练路径,完整工具训练集及发布成绩的全部运行配置仍需更多资料。<a
|
||||
class="ref"
|
||||
href="#source-finetune"
|
||||
>[8]</a
|
||||
>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="capability" class="article-section">
|
||||
<div class="section-number">05 / EVIDENCE</div>
|
||||
<h2>工具能力有哪些公开指标</h2>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>证据</th>
|
||||
<th>作者报告</th>
|
||||
<th>解释范围</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>FDB-v3 工具任务</th>
|
||||
<td>
|
||||
Pass@1 <strong>48%</strong><br />回应率
|
||||
<strong>100%</strong>
|
||||
</td>
|
||||
<td>
|
||||
作者自测,并注明从多次运行中取最佳。与原论文其他系统的历史成绩分别看待。
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Live 小样本委派</th>
|
||||
<td>Tag / Flash 需委派项均为 <strong>7/7</strong></td>
|
||||
<td>
|
||||
14 条录音,7 条应委派、7 条应直接回答,使用 mock 后台。
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>避免不必要委派</th>
|
||||
<td>
|
||||
Tag <strong>6/7</strong><br />Flash <strong>5/7</strong>
|
||||
</td>
|
||||
<td>说明部分可直接回答的问题仍会交给后台。样本规模有限。</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p class="source-line">
|
||||
来源
|
||||
<a
|
||||
href="https://github.com/adventists-ai/sophon-realtime/blob/b2754d4753e20ecfa860db0cda4325e4479f4427/README.md"
|
||||
>作者评测说明</a
|
||||
>
|
||||
与
|
||||
<a
|
||||
href="https://github.com/adventists-ai/sophon-realtime/blob/b2754d4753e20ecfa860db0cda4325e4479f4427/live/README.md"
|
||||
>Live 测试说明</a
|
||||
>。
|
||||
</p>
|
||||
<p>
|
||||
完整接入还需要分开测量工具选择、参数正确、外部执行结果、重复调用、任务终态和正确告知。前台、后台、提示规则与参数校验配置都影响结果,应记录为同一份系统配置。
|
||||
</p>
|
||||
<p>
|
||||
网关包含工具名检查与参数来源检查,结合 ASR、先前工具结果、指令及
|
||||
schema
|
||||
决定是否要求模型重做。当前代码还给拒绝重试设置了次数上限。公开的
|
||||
FDB-v3 成绩没有充分说明这些配置的开启情况。<a
|
||||
class="ref"
|
||||
href="#source-grounding"
|
||||
>[9]</a
|
||||
>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="speed" class="article-section">
|
||||
<div class="section-number">06 / LATENCY</div>
|
||||
<h2>回复速度与 GPT-Live 怎样比较</h2>
|
||||
<p>
|
||||
Flash 的作者报告显示较短的 HTTP 首包路径。Tag 的双工报告值和
|
||||
GPT-Live
|
||||
的官方轮替值接近,整体体验仍需统一环境、输入与计时终点后比较。
|
||||
</p>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>配置</th>
|
||||
<th>报告值</th>
|
||||
<th>计时对象</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Sophon Flash</th>
|
||||
<td><strong>0.31 s</strong> 中位数,P90 0.34 s</td>
|
||||
<td>
|
||||
一张 H100 80 GB、单条 HTTP 音频请求,客户端 POST
|
||||
前开始计时,到首个音频块。
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Sophon Tag</th>
|
||||
<td><strong>约 0.7 s</strong></td>
|
||||
<td>作者文档的全双工“用户语音结束到首音”口径。</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>GPT-Live-1</th>
|
||||
<td><strong>0.798 s</strong></td>
|
||||
<td>官方发布图中的 FDB-v1 轮替时延。</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p class="source-line">
|
||||
来源
|
||||
<a
|
||||
href="https://github.com/adventists-ai/sophon-realtime/blob/b2754d4753e20ecfa860db0cda4325e4479f4427/flash/README.md"
|
||||
>Flash 测量条件</a
|
||||
>、<a
|
||||
href="https://github.com/adventists-ai/sophon-realtime/blob/b2754d4753e20ecfa860db0cda4325e4479f4427/flash/client_example.py"
|
||||
>客户端计时</a
|
||||
>、<a
|
||||
href="https://github.com/adventists-ai/sophon-realtime/blob/b2754d4753e20ecfa860db0cda4325e4479f4427/tag/README.md"
|
||||
>Tag</a
|
||||
>、<a
|
||||
href="https://openai.com/index/introducing-gpt-live-1-in-the-api/"
|
||||
>GPT-Live 发布材料</a
|
||||
>。
|
||||
</p>
|
||||
<div class="clock-boundaries">
|
||||
<div>
|
||||
<span>完整交互时间轴</span
|
||||
><strong
|
||||
>用户说完 → 上行与话轮判断 → 模型 / 工具 → 解码 →
|
||||
实际播放</strong
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<span>Flash 公开客户端的计时</span
|
||||
><strong>提交已读入的 WAV → 收到并解码首块音频</strong>
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
单看这些数值还无法给出同场速度排名。工具请求又有三个不同时间点,首次承接、工具派发、首个正确结果。先说“我查一下”可以很快,结果仍取决于后台模型与实际工具。
|
||||
</p>
|
||||
<h3>并发需要附带音频连续性</h3>
|
||||
<p>
|
||||
Flash 作者在 40 路请求、10 秒内到达时,报告首音中位数 0.58 s、P90
|
||||
0.67 s,stall 中位数 0.37 s。Tag 在 16 路时报告首音中位数 0.75
|
||||
s、最大 1.07
|
||||
s。并发数、尾时延和播放停顿一起报告,才有明确服务含义。<a
|
||||
class="ref"
|
||||
href="#source-flash"
|
||||
>[3]</a
|
||||
>
|
||||
<a
|
||||
class="ref"
|
||||
href="https://github.com/adventists-ai/sophon-realtime/blob/b2754d4753e20ecfa860db0cda4325e4479f4427/tag/README.md"
|
||||
>Tag 测量</a
|
||||
>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="limits" class="article-section">
|
||||
<div class="section-number">07 / INTEGRATION</div>
|
||||
<h2>接入时需要核对的实际边界</h2>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>观察项</th>
|
||||
<th>当前代码或资料</th>
|
||||
<th>验收动作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Realtime 取消</th>
|
||||
<td><code>response.cancel</code> 当前为空操作。</td>
|
||||
<td>
|
||||
验证调用方取消能否停止生成与播放,并单测后台任务状态。
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>历史截断</th>
|
||||
<td>
|
||||
<code>conversation.item.truncate</code>
|
||||
当前主要回确认事件,截断尚未传播到引擎。
|
||||
</td>
|
||||
<td>用已播放、未播放的事实分别测试后续上下文。</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>文本输入</th>
|
||||
<td>
|
||||
普通文本
|
||||
<code>conversation.item.create</code>
|
||||
会被记录为忽略,工具结果输入单独处理。
|
||||
</td>
|
||||
<td>按实际客户端所用的消息类型逐项验证。</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>参数事件</th>
|
||||
<td>
|
||||
完整参数 JSON 可一次放进
|
||||
<code>arguments.delta</code>,随后发 done。
|
||||
</td>
|
||||
<td>记录实际派发时间,区分事件命名与逐 token 执行。</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>预热与首会话</th>
|
||||
<td>脚本包含预热,Live 文档记录首次会话可能无回应。</td>
|
||||
<td>区分冷启动、预热和稳定服务样本,保留失败。</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>E2E 预览</th>
|
||||
<td>模型目录与部署文档更新节奏有差异。</td>
|
||||
<td>固定实际权重 revision、运行入口与引擎版本。</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>许可材料</th>
|
||||
<td>
|
||||
Sophon NOTICE 标为 Apache-2.0,上游 Para 模型说明含 CC BY-NC
|
||||
4.0。
|
||||
</td>
|
||||
<td>适用权重许可范围需要发布方澄清。</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p class="source-line">
|
||||
代码与声明
|
||||
<a
|
||||
href="https://github.com/adventists-ai/sophon-realtime/blob/b2754d4753e20ecfa860db0cda4325e4479f4427/live/sophon_live/realtime.py"
|
||||
>Realtime 适配器</a
|
||||
>、<a
|
||||
href="https://github.com/adventists-ai/sophon-realtime/blob/b2754d4753e20ecfa860db0cda4325e4479f4427/tag/NOTICE"
|
||||
>Sophon NOTICE</a
|
||||
>、<a href="https://github.com/adventists-ai/duplexjev#license"
|
||||
>上游许可说明</a
|
||||
>。
|
||||
</p>
|
||||
<p>
|
||||
当前材料支持继续开展自托管和接口验证。模型效果、真实工具执行、API
|
||||
协议覆盖与语音体验分别验收。接入与配置变更后的结果,应重新建立固定版本、固定场景的证据。
|
||||
</p>
|
||||
<a class="text-link" href="https://voice-duplex.k1412.top/#evaluation"
|
||||
>完整的双工评测标准与指标方案 ↗</a
|
||||
>
|
||||
</section>
|
||||
|
||||
<section id="sources" class="article-section">
|
||||
<div class="section-number">08 / SOURCES</div>
|
||||
<h2>来源与核对范围</h2>
|
||||
<p>
|
||||
截至 2026 年 11 月 9
|
||||
日。模型指标采用作者报告值,接口行为采用固定提交的代码观察,交互步骤使用合成示例。GPU
|
||||
推理、真实声学链路与外部业务执行尚待实测。
|
||||
</p>
|
||||
<ol class="source-list">
|
||||
<li id="source-root">
|
||||
<a
|
||||
href="https://github.com/adventists-ai/sophon-realtime/tree/b2754d4753e20ecfa860db0cda4325e4479f4427"
|
||||
>Sophon Realtime 原始项目</a
|
||||
><span
|
||||
>构建与公开评测说明。本文独立解读,代码版本固定为
|
||||
b2754d47。</span
|
||||
>
|
||||
</li>
|
||||
<li id="source-notice">
|
||||
<a
|
||||
href="https://github.com/adventists-ai/sophon-realtime/blob/b2754d4753e20ecfa860db0cda4325e4479f4427/tag/NOTICE"
|
||||
>Tag NOTICE</a
|
||||
><span>骨干、音频编码器与语音模块来源。</span>
|
||||
</li>
|
||||
<li id="source-flash">
|
||||
<a
|
||||
href="https://github.com/adventists-ai/sophon-realtime/blob/b2754d4753e20ecfa860db0cda4325e4479f4427/flash/README.md"
|
||||
>Flash README</a
|
||||
><span>80 ms 声学帧、HTTP 首包与并发测量条件。</span>
|
||||
</li>
|
||||
<li id="source-client">
|
||||
<a
|
||||
href="https://github.com/adventists-ai/sophon-realtime/blob/b2754d4753e20ecfa860db0cda4325e4479f4427/tag/client_example.py"
|
||||
>Tag 客户端</a
|
||||
><span>模型 ID 获取、请求体与音频流解码。</span>
|
||||
</li>
|
||||
<li id="source-tools">
|
||||
<a
|
||||
href="https://github.com/adventists-ai/sophon-realtime/blob/b2754d4753e20ecfa860db0cda4325e4479f4427/tag/plugin/sophon_realtime/tools.py"
|
||||
>工具解析与口语流裁剪</a
|
||||
><span>提示模板、JSON 解析、mock 与工具标记。</span>
|
||||
</li>
|
||||
<li id="source-tokenizer">
|
||||
<a
|
||||
href="https://huggingface.co/adventists-ai/Sophon-Realtime-32B-Tag/blob/9a9ed240ab7a4ed7494ec3f45caf1242c1dace2a/thinker/tokenizer_config.json"
|
||||
>Tag tokenizer 元信息</a
|
||||
><span
|
||||
>固定权重元信息 revision 9a9ed240,核对分隔符 ID 与 special
|
||||
字段。</span
|
||||
>
|
||||
</li>
|
||||
<li id="source-backend">
|
||||
<a
|
||||
href="https://github.com/adventists-ai/sophon-realtime/blob/b2754d4753e20ecfa860db0cda4325e4479f4427/live/sophon_live/backend.py"
|
||||
>后台工具循环</a
|
||||
><span>delegate、后台模型配置与真实工具调用。</span>
|
||||
</li>
|
||||
<li id="source-finetune">
|
||||
<a
|
||||
href="https://github.com/adventists-ai/sophon-realtime/blob/b2754d4753e20ecfa860db0cda4325e4479f4427/finetune/README.md"
|
||||
>微调说明与样例</a
|
||||
><span>函数 schema、调用格式与 tool 结果消息。</span>
|
||||
</li>
|
||||
<li id="source-grounding">
|
||||
<a
|
||||
href="https://github.com/adventists-ai/sophon-realtime/blob/b2754d4753e20ecfa860db0cda4325e4479f4427/live/sophon_live/realtime.py"
|
||||
>Realtime 网关</a
|
||||
><span>函数调用事件、参数来源检查、取消与截断路径。</span>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://github.com/adventists-ai/sophon-realtime/blob/b2754d4753e20ecfa860db0cda4325e4479f4427/live/README.md"
|
||||
>Live 文档</a
|
||||
><span>网关部署、委派事件、ASR、14 条 mock 后台测试。</span>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://openai.com/index/introducing-gpt-live-1-in-the-api/"
|
||||
>OpenAI GPT-Live-1 API 发布材料</a
|
||||
><span
|
||||
>FDB-v1
|
||||
轮替时延及后台配置应分别解读,完整补充见通用评测站。</span
|
||||
>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
<footer>
|
||||
<span>K1412 RESEARCH · MODEL STUDY</span>
|
||||
<div>
|
||||
<a href="https://blog.k1412.top">研究目录 ↗</a
|
||||
><a href="https://git.k1412.top/wuyang/sophon-realtime-guide"
|
||||
>网页源码 ↗</a
|
||||
><a href="#top">回到顶部 ↑</a>
|
||||
</div>
|
||||
<small>Release sophon-realtime-20261109-v1</small>
|
||||
</footer>
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,208 @@
|
||||
export const builds = {
|
||||
flash: {
|
||||
title: "Flash",
|
||||
directory: "flash",
|
||||
modelDirectory: "Sophon-Realtime-32B-Flash",
|
||||
flow: ["用户语音", "共享 32B 理解与文本", "Flash 语音头", "24 kHz 语音"],
|
||||
detail:
|
||||
"声音侧以每 80 ms 帧一次前向为设计。作者报告单 H100 HTTP 首包中位数 0.31 s,实例约 55 GB 显存。",
|
||||
modes: ["http", "engine", "realtime", "live"],
|
||||
},
|
||||
tag: {
|
||||
title: "Tag",
|
||||
directory: "tag",
|
||||
modelDirectory: "Sophon-Realtime-32B-Tag",
|
||||
flow: [
|
||||
"用户语音",
|
||||
"共享 32B 与声音标签",
|
||||
"tag-tts 语音模型",
|
||||
"Flow + HiFT 语音",
|
||||
],
|
||||
detail:
|
||||
"模型写出笑声、叹气等声音标签,由专用语音模块表现。作者报告单 80 GB GPU、实例约 65 GB 显存。",
|
||||
modes: ["http", "engine", "realtime", "live"],
|
||||
},
|
||||
e2e: {
|
||||
title: "E2E 预览",
|
||||
directory: "",
|
||||
modelDirectory: "Sophon-Realtime-32B-E2E",
|
||||
flow: [
|
||||
"用户语音",
|
||||
"32B Thinker 与 Talker",
|
||||
"连续语音向量",
|
||||
"E2 解码与语音",
|
||||
],
|
||||
detail:
|
||||
"采用独立推理引擎与 /ws 网关,文档要求两张 80 GB GPU。当前作为研究预览,部署文档与模型目录版本需逐项固定。",
|
||||
modes: ["e2e"],
|
||||
},
|
||||
};
|
||||
|
||||
export const callModes = {
|
||||
http: {
|
||||
title: "HTTP 音频生成",
|
||||
kind: "POST",
|
||||
endpoint: "http://HOST:8000/v1/chat/completions",
|
||||
detail:
|
||||
"先用完整 WAV 验证语音生成。官方客户端先取 /v1/models,再流式解码文本与音频。",
|
||||
},
|
||||
engine: {
|
||||
title: "基础引擎双工",
|
||||
kind: "WebSocket",
|
||||
endpoint: "ws://HOST:8000/v1/realtime",
|
||||
detail:
|
||||
"持续推送音频,模型参与话轮决策。原生工具桥需要自行对接,默认 mock 与真实执行分别核验。",
|
||||
},
|
||||
realtime: {
|
||||
title: "网关函数调用",
|
||||
kind: "WebSocket",
|
||||
endpoint: "ws://HOST:8780/v1/realtime",
|
||||
detail:
|
||||
"启动 Live 网关后,使用其 Realtime 适配器接收 function_call 与回填结果。当前协议覆盖有边界。",
|
||||
},
|
||||
live: {
|
||||
title: "Live 客户端委派",
|
||||
kind: "WebSocket",
|
||||
endpoint: "ws://HOST:8780/v1/live/sessions",
|
||||
detail:
|
||||
"前台生成 delegate(task),客户端接委派事件,并把后台结果经 commentary.append 返回。",
|
||||
},
|
||||
e2e: {
|
||||
title: "E2E 自有协议",
|
||||
kind: "WebSocket",
|
||||
endpoint: "ws://HOST:8000/ws",
|
||||
detail:
|
||||
"按 E2E 文档使用 config、二进制 PCM16、text、barge_in、done 等事件。",
|
||||
},
|
||||
};
|
||||
|
||||
export function makeRecipe(buildKey, modeKey) {
|
||||
const build = builds[buildKey];
|
||||
if (!build || !build.modes.includes(modeKey)) {
|
||||
throw new RangeError(
|
||||
"The selected build does not support this documented path.",
|
||||
);
|
||||
}
|
||||
if (modeKey === "e2e") {
|
||||
return {
|
||||
startup: `# 在已准备 Linux / CUDA / 权重的环境中
|
||||
SKIP_DOWNLOAD=1 MODEL_DIR=/path/to/${build.modelDirectory} bash scripts/install.sh
|
||||
SOPHON_MODEL_DIR=/path/to/${build.modelDirectory} THINKER_GPU=0 TALKER_GPU=1 bash scripts/start.sh`,
|
||||
request: `连接 ws://HOST:8000/ws 后先发送
|
||||
{"type":"config","lang":"zh","history":true}
|
||||
|
||||
随后持续发送 16 kHz、单声道 PCM16 二进制音频。
|
||||
接收 text / 二进制 24 kHz 音频 / barge_in / metrics / done。`,
|
||||
};
|
||||
}
|
||||
const startup = `# 在官方仓库 ${build.directory}/ 中,权重须预先准备
|
||||
SKIP_DOWNLOAD=1 bash install.sh
|
||||
MODEL_DIR=/path/to/${build.modelDirectory} GPU=0 PORT=8000 bash start.sh
|
||||
# 等待 READY 与预热完成`;
|
||||
if (modeKey === "http") {
|
||||
return {
|
||||
startup: `${startup}
|
||||
|
||||
python client_example.py --url http://HOST:8000 --wav question.wav --lang zh --voice chelsie`,
|
||||
request: `先从 GET /v1/models 取得模型 ID。
|
||||
POST /v1/chat/completions
|
||||
{
|
||||
"model": "<返回的模型 ID>",
|
||||
"stream": true,
|
||||
"modalities": ["text", "audio"],
|
||||
"language": "zh",
|
||||
"messages": [
|
||||
{"role":"system","content":"<config/system_prompt.json 的 zh 字段>"},
|
||||
{"role":"user","content":[
|
||||
{"type":"text","text":"<|audio|>"},
|
||||
{"type":"input_audio","input_audio":{"data":"<WAV_BASE64>","format":"wav"}}
|
||||
]}
|
||||
]
|
||||
}`,
|
||||
};
|
||||
}
|
||||
if (modeKey === "engine") {
|
||||
return {
|
||||
startup,
|
||||
request: `连接 ws://HOST:8000/v1/realtime
|
||||
{"type":"session.update","session":{"extra_body":{"auto_response":true,"language":"zh"}}}
|
||||
{"type":"input_audio_buffer.append","audio":"<持续发送的 PCM16_BASE64>"}
|
||||
|
||||
接真实工具时,额外配置 tools / tool_mode=client / bridge_key。
|
||||
工具桥默认在引擎本机 PORT+700,需配套执行与回填逻辑。`,
|
||||
};
|
||||
}
|
||||
const gatewayStartup = `${startup}
|
||||
|
||||
# 同一环境启动外层网关
|
||||
cd ../live
|
||||
SOPHON_LIVE_ASR=/path/to/Qwen3-ASR-0.6B-hf BUILD=${build.directory} PORT=8000 LIVE_PORT=8780 bash start_live.sh`;
|
||||
if (modeKey === "realtime") {
|
||||
return {
|
||||
startup: gatewayStartup,
|
||||
request: `连接 ws://HOST:8780/v1/realtime 后配置工具
|
||||
{"type":"session.update","session":{"instructions":"简短回答用户","tools":[
|
||||
{"type":"function","name":"get_weather","description":"查询城市天气",
|
||||
"parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"]}}
|
||||
]}}
|
||||
|
||||
收到 function_call 后,由客户端执行真实函数,再回填
|
||||
{"type":"conversation.item.create","item":{"type":"function_call_output","call_id":"<返回的 call_id>","output":"<真实结果>"}}
|
||||
{"type":"response.create"}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
startup: gatewayStartup,
|
||||
request: `连接 ws://HOST:8780/v1/live/sessions
|
||||
{"type":"session.start","session":{"instructions":"用中文简短回答","delegation":{"type":"client"}}}
|
||||
{"type":"session.input_audio.append","audio":"<PCM16_BASE64>"}
|
||||
|
||||
接收 session.delegation.created,后台完成后回填
|
||||
{"type":"session.commentary.append","delegation_id":"<返回的委派 ID>","content":"<真实后台结果>"}`,
|
||||
};
|
||||
}
|
||||
|
||||
export const toolSteps = [
|
||||
{
|
||||
title: "提供函数定义",
|
||||
explanation:
|
||||
"调用方声明函数名、说明与参数 schema。运行时经 chat template 把这些定义提供给模型。",
|
||||
code: '{"type":"function","name":"get_weather","parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"]}}',
|
||||
},
|
||||
{
|
||||
title: "模型生成调用",
|
||||
explanation:
|
||||
"模型可以先给一句承接语,再输出工具名与参数。下面的天气任务为合成示例。",
|
||||
code: '我帮你查一下。\n<tool_call>\n{"name":"get_weather","arguments":{"city":"上海"}}\n</tool_call>',
|
||||
},
|
||||
{
|
||||
title: "口语流与控制流分开",
|
||||
explanation:
|
||||
"cut_tool 保留调用标记前的口语。完整或尚未写完的工具标记会被拦住,避免进入语音合成。",
|
||||
code: '送往语音模块\n我帮你查一下。\n\n留给工具解析器\n<tool_call>{"name":"get_weather","arguments":{"city":"上海"}}</tool_call>',
|
||||
},
|
||||
{
|
||||
title: "解析与桥接",
|
||||
explanation:
|
||||
"Thinker 写完后,服务解析函数调用,发出带 call_id、name 和 arguments 的工具桥事件。此时承接语音可能仍在播放。",
|
||||
code: '{"type":"tool_call","call_id":"call_demo","name":"get_weather","arguments":{"city":"上海"}}',
|
||||
},
|
||||
{
|
||||
title: "客户端执行真实函数",
|
||||
explanation:
|
||||
"真实执行端取出函数和参数,调用业务系统。mock 模式则返回模拟值。两类证据必须分开记录。",
|
||||
code: 'result = await actual_weather_service(city="上海")\n# 记录真实调用 ID、参数、返回、耗时与最终状态',
|
||||
},
|
||||
{
|
||||
title: "回填结果",
|
||||
explanation:
|
||||
"结果经客户端工具桥,或网关的 function_call_output / commentary.append 返回。下方内容显式标为示意。",
|
||||
code: '{"type":"function_call_output","call_id":"call_demo","output":"示例结果:天气晴朗"}',
|
||||
},
|
||||
{
|
||||
title: "模型继续回应",
|
||||
explanation:
|
||||
"结果被加入工具历史,后续语音从该结果继续生成。正确选择函数、真实执行和正确告知各自评分。",
|
||||
code: "role: tool\n示例结果:天气晴朗\n\nassistant\n查到了,示例结果显示天气晴朗。",
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,35 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { builds, callModes, makeRecipe, toolSteps } from "./model.js";
|
||||
|
||||
test("every documented build and mode has distinct startup and request examples", () => {
|
||||
for (const [key, build] of Object.entries(builds)) {
|
||||
for (const mode of build.modes) {
|
||||
assert.ok(callModes[mode]);
|
||||
const example = makeRecipe(key, mode);
|
||||
assert.ok(example.startup.includes("SKIP_DOWNLOAD=1"));
|
||||
assert.ok(example.request.length > 30);
|
||||
}
|
||||
}
|
||||
});
|
||||
test("E2E cannot select an undocumented OpenAI-style entry", () => {
|
||||
assert.throws(() => makeRecipe("e2e", "http"), RangeError);
|
||||
assert.equal(callModes.e2e.endpoint, "ws://HOST:8000/ws");
|
||||
});
|
||||
test("Live and Realtime gateways retain their distinct event formats", () => {
|
||||
assert.ok(
|
||||
makeRecipe("flash", "live").request.includes("session.commentary.append"),
|
||||
);
|
||||
assert.ok(
|
||||
makeRecipe("tag", "realtime").request.includes("function_call_output"),
|
||||
);
|
||||
assert.ok(
|
||||
makeRecipe("flash", "http").startup.includes("python client_example.py"),
|
||||
);
|
||||
});
|
||||
test("tool walkthrough distinguishes model output and actual execution", () => {
|
||||
assert.equal(toolSteps.length, 7);
|
||||
assert.ok(toolSteps[1].code.includes("<tool_call>"));
|
||||
assert.ok(toolSteps[4].code.includes("actual_weather_service"));
|
||||
assert.ok(toolSteps[5].code.includes("示例结果"));
|
||||
});
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
server {
|
||||
listen 8080;
|
||||
server_name _;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
charset utf-8;
|
||||
add_header X-Content-Type-Options nosniff always;
|
||||
add_header Referrer-Policy strict-origin-when-cross-origin always;
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; object-src 'none'; base-uri 'self'; frame-ancestors 'self'" always;
|
||||
location = /healthz { access_log off; default_type text/plain; return 200 "ok\n"; }
|
||||
location / { try_files $uri $uri/ =404; }
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "sophon-realtime-research-guide",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"check": "node --check app.js && node --check model.js && node --test model.test.mjs && node validate.mjs",
|
||||
"build": "node build.mjs",
|
||||
"dev": "python3 -m http.server 4174 --bind 127.0.0.1 --directory dist"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"release": "sophon-realtime-20261109-v1",
|
||||
"researchCutoff": "2026-11-09",
|
||||
"canonicalUrl": "https://sophon-realtime.k1412.top/",
|
||||
"codeAudited": "b2754d4753e20ecfa860db0cda4325e4479f4427",
|
||||
"gpuInferenceExecuted": false
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
+931
@@ -0,0 +1,931 @@
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--paper: #f4f3ef;
|
||||
--surface: #fffefa;
|
||||
--ink: #202b29;
|
||||
--muted: #69756f;
|
||||
--line: #d5dcd3;
|
||||
--green: #2d755c;
|
||||
--green-soft: #e2ece5;
|
||||
--blue: #3d68a3;
|
||||
--code: #eaece7;
|
||||
--amber: #94652b;
|
||||
--amber-soft: #f1e8d7;
|
||||
}
|
||||
html[data-theme="dark"] {
|
||||
color-scheme: dark;
|
||||
--paper: #18201d;
|
||||
--surface: #202a24;
|
||||
--ink: #e2e9df;
|
||||
--muted: #a6b1a5;
|
||||
--line: #3c4c40;
|
||||
--green: #9ac6a6;
|
||||
--green-soft: #2d4434;
|
||||
--blue: #9fbfe8;
|
||||
--code: #28362c;
|
||||
--amber: #dfb16e;
|
||||
--amber-soft: #443b2c;
|
||||
}
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
scroll-padding-top: 100px;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--paper);
|
||||
color: var(--ink);
|
||||
font-family:
|
||||
-apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC",
|
||||
"Microsoft YaHei", sans-serif;
|
||||
line-height: 1.85;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover {
|
||||
color: var(--green);
|
||||
}
|
||||
button,
|
||||
select {
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
button:disabled {
|
||||
opacity: 0.35;
|
||||
cursor: default;
|
||||
}
|
||||
a:focus-visible,
|
||||
button:focus-visible,
|
||||
select:focus-visible {
|
||||
outline: 3px solid var(--blue);
|
||||
outline-offset: 4px;
|
||||
}
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
code,
|
||||
pre {
|
||||
font-family: "SFMono-Regular", Consolas, monospace;
|
||||
}
|
||||
p code,
|
||||
td code {
|
||||
font-size: 0.87em;
|
||||
overflow-wrap: anywhere;
|
||||
color: var(--green);
|
||||
}
|
||||
.skip-link {
|
||||
position: absolute;
|
||||
top: -80px;
|
||||
left: 20px;
|
||||
background: var(--ink);
|
||||
color: var(--paper);
|
||||
padding: 8px 15px;
|
||||
z-index: 20;
|
||||
}
|
||||
.skip-link:focus {
|
||||
top: 15px;
|
||||
}
|
||||
.site-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 80px;
|
||||
background: var(--paper);
|
||||
border-bottom: 1px solid var(--line);
|
||||
padding: 0 5vw;
|
||||
}
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 17px;
|
||||
}
|
||||
.brand b {
|
||||
font:
|
||||
37px Georgia,
|
||||
serif;
|
||||
letter-spacing: -4px;
|
||||
}
|
||||
.brand > span {
|
||||
font-size: 11px;
|
||||
letter-spacing: 1.8px;
|
||||
}
|
||||
.brand i {
|
||||
font-style: normal;
|
||||
color: var(--muted);
|
||||
margin-left: 9px;
|
||||
}
|
||||
.site-header nav {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
}
|
||||
#theme-toggle {
|
||||
border: 1px solid var(--line);
|
||||
background: transparent;
|
||||
border-radius: 50%;
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
padding: 0;
|
||||
font-size: 20px;
|
||||
}
|
||||
.layout {
|
||||
display: grid;
|
||||
grid-template-columns: 180px minmax(0, 900px);
|
||||
gap: 70px;
|
||||
max-width: 1300px;
|
||||
margin: 0 auto;
|
||||
padding: 0 45px;
|
||||
}
|
||||
.contents {
|
||||
position: sticky;
|
||||
top: 80px;
|
||||
align-self: start;
|
||||
padding-top: 63px;
|
||||
}
|
||||
.eyebrow {
|
||||
color: var(--muted);
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 1.6px;
|
||||
}
|
||||
.contents nav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
margin-top: 32px;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.version-note {
|
||||
margin-top: 32px;
|
||||
padding-top: 21px;
|
||||
border-top: 1px solid var(--line);
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
line-height: 1.9;
|
||||
}
|
||||
.version-note strong {
|
||||
color: var(--ink);
|
||||
font-size: 13px;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
.version-note span {
|
||||
display: block;
|
||||
margin-top: 11px;
|
||||
}
|
||||
.hero {
|
||||
padding-top: 60px;
|
||||
}
|
||||
h1 {
|
||||
font-size: clamp(35px, 4.1vw, 56px);
|
||||
line-height: 1.27;
|
||||
font-weight: 600;
|
||||
letter-spacing: -1.3px;
|
||||
margin: 20px 0 22px;
|
||||
}
|
||||
.deck {
|
||||
font-size: 17px;
|
||||
color: var(--muted);
|
||||
line-height: 1.9;
|
||||
}
|
||||
.hero-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px 20px;
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
margin: 23px 0 33px;
|
||||
}
|
||||
.hero-meta a {
|
||||
color: var(--green);
|
||||
}
|
||||
.hero-route {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
border-top: 1px solid var(--line);
|
||||
border-bottom: 1px solid var(--line);
|
||||
padding: 26px 0;
|
||||
margin-bottom: 31px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.hero-route b {
|
||||
color: var(--muted);
|
||||
font-weight: 400;
|
||||
}
|
||||
.hero-route code {
|
||||
color: var(--green);
|
||||
font-size: 15px;
|
||||
background: var(--green-soft);
|
||||
padding: 7px 12px;
|
||||
}
|
||||
main p {
|
||||
font-size: 14px;
|
||||
line-height: 1.95;
|
||||
}
|
||||
p + p {
|
||||
margin-top: 17px;
|
||||
}
|
||||
main .lead {
|
||||
font-size: 18px;
|
||||
line-height: 1.95;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.hero > p:last-child {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
.hero > p:last-child code {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.article-section {
|
||||
border-top: 1px solid var(--line);
|
||||
padding-top: 37px;
|
||||
margin-top: 58px;
|
||||
scroll-margin-top: 105px;
|
||||
}
|
||||
.section-number {
|
||||
font-size: 10px;
|
||||
color: var(--green);
|
||||
letter-spacing: 1.8px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
h2 {
|
||||
font-size: 28px;
|
||||
line-height: 1.5;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.3px;
|
||||
margin-bottom: 23px;
|
||||
}
|
||||
h3 {
|
||||
font-size: 19px;
|
||||
font-weight: 600;
|
||||
line-height: 1.6;
|
||||
margin: 28px 0 13px;
|
||||
}
|
||||
.ref {
|
||||
font-size: 11px;
|
||||
color: var(--green);
|
||||
margin-left: 6px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.table-wrap {
|
||||
margin: 25px 0;
|
||||
overflow-x: auto;
|
||||
border-top: 1px solid var(--line);
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
min-width: 610px;
|
||||
font-size: 12px;
|
||||
line-height: 1.9;
|
||||
}
|
||||
th,
|
||||
td {
|
||||
padding: 14px 12px;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
th {
|
||||
font-weight: 600;
|
||||
}
|
||||
thead th {
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
}
|
||||
tbody th {
|
||||
width: 20%;
|
||||
min-width: 100px;
|
||||
}
|
||||
tr:last-child > * {
|
||||
border-bottom: 0;
|
||||
}
|
||||
th:first-child,
|
||||
td:first-child {
|
||||
padding-left: 0;
|
||||
}
|
||||
th:last-child,
|
||||
td:last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
.source-line {
|
||||
color: var(--muted);
|
||||
}
|
||||
main .source-line {
|
||||
font-size: 11px;
|
||||
line-height: 1.9;
|
||||
}
|
||||
.source-line a {
|
||||
color: var(--green);
|
||||
}
|
||||
.evidence-note {
|
||||
border-left: 2px solid var(--green);
|
||||
padding: 13px 0 13px 20px;
|
||||
margin: 25px 0;
|
||||
}
|
||||
.evidence-note > strong {
|
||||
font-size: 14px;
|
||||
}
|
||||
.evidence-note p {
|
||||
margin-top: 9px;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.evidence-note a {
|
||||
color: var(--green);
|
||||
}
|
||||
.figure {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 5px;
|
||||
padding: 26px;
|
||||
margin: 26px 0;
|
||||
}
|
||||
.figure-heading {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
.figure-heading h3 {
|
||||
font-size: 20px;
|
||||
margin: 5px 0 22px;
|
||||
}
|
||||
.tag {
|
||||
font-size: 10px;
|
||||
white-space: nowrap;
|
||||
padding: 4px 7px;
|
||||
border-radius: 3px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.audit {
|
||||
background: var(--green-soft);
|
||||
color: var(--green);
|
||||
}
|
||||
.example {
|
||||
background: var(--amber-soft);
|
||||
color: var(--amber);
|
||||
}
|
||||
.segmented {
|
||||
display: inline-flex;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.segmented button {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
padding: 8px 22px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.segmented button[aria-pressed="true"] {
|
||||
background: var(--ink);
|
||||
color: var(--paper);
|
||||
}
|
||||
.flow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
justify-content: space-between;
|
||||
margin: 30px 0 24px;
|
||||
}
|
||||
.flow > span {
|
||||
flex: 1;
|
||||
background: var(--code);
|
||||
text-align: center;
|
||||
padding: 15px 8px;
|
||||
font-size: 12px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
.flow > span:first-child {
|
||||
background: var(--green-soft);
|
||||
}
|
||||
.flow > b {
|
||||
font-weight: 400;
|
||||
color: var(--muted);
|
||||
}
|
||||
.selected-detail {
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
min-height: 45px;
|
||||
border-top: 1px solid var(--line);
|
||||
padding-top: 17px;
|
||||
line-height: 1.9;
|
||||
}
|
||||
.call-controls {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 24px;
|
||||
}
|
||||
.call-controls label {
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
}
|
||||
select {
|
||||
display: block;
|
||||
width: 100%;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 3px;
|
||||
background: var(--paper);
|
||||
padding: 9px 12px;
|
||||
margin-top: 7px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.endpoint {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin: 22px 0 12px;
|
||||
}
|
||||
.endpoint > span {
|
||||
font-size: 10px;
|
||||
color: var(--green);
|
||||
font-weight: 600;
|
||||
}
|
||||
.endpoint code {
|
||||
font-size: 13px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
main .small {
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
line-height: 1.9;
|
||||
}
|
||||
.code-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 20px 0 11px;
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.recipe-parts {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 19px;
|
||||
}
|
||||
.recipe-parts button {
|
||||
background: transparent;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 3px;
|
||||
padding: 5px 12px;
|
||||
font-size: 11px;
|
||||
}
|
||||
.recipe-parts button[aria-pressed="true"] {
|
||||
background: var(--green-soft);
|
||||
color: var(--green);
|
||||
}
|
||||
.code-heading button,
|
||||
.step-controls button {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 3px;
|
||||
background: transparent;
|
||||
padding: 5px 12px;
|
||||
font-size: 11px;
|
||||
}
|
||||
pre {
|
||||
background: var(--code);
|
||||
margin: 0 0 11px;
|
||||
padding: 20px;
|
||||
font-size: 11px;
|
||||
line-height: 1.9;
|
||||
overflow-x: auto;
|
||||
tab-size: 2;
|
||||
}
|
||||
#copy-status {
|
||||
min-height: 21px;
|
||||
}
|
||||
.step-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.step-list button {
|
||||
background: transparent;
|
||||
border: 1px solid var(--line);
|
||||
padding: 6px 9px;
|
||||
font-size: 10px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.step-list button[aria-pressed="true"] {
|
||||
background: var(--green-soft);
|
||||
color: var(--green);
|
||||
border-color: var(--green);
|
||||
}
|
||||
.step-head {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.step-head > span {
|
||||
font:
|
||||
28px Georgia,
|
||||
serif;
|
||||
color: var(--green);
|
||||
}
|
||||
.step-head h4 {
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
}
|
||||
#step-explanation {
|
||||
min-height: 59px;
|
||||
margin-bottom: 19px;
|
||||
font-size: 13px;
|
||||
}
|
||||
#step-code {
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.step-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 18px;
|
||||
}
|
||||
.step-controls > span {
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.two-paths {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 31px;
|
||||
margin: 28px 0;
|
||||
}
|
||||
.two-paths > div {
|
||||
border-top: 1px solid var(--line);
|
||||
padding-top: 15px;
|
||||
}
|
||||
.two-paths h3 {
|
||||
font-size: 17px;
|
||||
margin: 8px 0 12px;
|
||||
}
|
||||
.two-paths p {
|
||||
font-size: 12px;
|
||||
}
|
||||
.clock-boundaries {
|
||||
margin: 26px 0;
|
||||
}
|
||||
.clock-boundaries > div {
|
||||
padding: 15px 0;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
.clock-boundaries span {
|
||||
color: var(--muted);
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
margin-bottom: 7px;
|
||||
}
|
||||
.clock-boundaries strong {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.text-link {
|
||||
display: inline-block;
|
||||
margin-top: 22px;
|
||||
color: var(--green);
|
||||
font-size: 13px;
|
||||
}
|
||||
.source-list {
|
||||
margin: 26px 0;
|
||||
padding-left: 24px;
|
||||
}
|
||||
.source-list li {
|
||||
border-top: 1px solid var(--line);
|
||||
padding: 13px 0 13px 6px;
|
||||
scroll-margin-top: 105px;
|
||||
}
|
||||
.source-list li::marker {
|
||||
font-size: 11px;
|
||||
color: var(--green);
|
||||
}
|
||||
.source-list a {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.source-list span {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
margin-top: 4px;
|
||||
}
|
||||
footer {
|
||||
margin-top: 55px;
|
||||
padding: 26px 0 35px;
|
||||
border-top: 1px solid var(--line);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
color: var(--muted);
|
||||
font-size: 10px;
|
||||
}
|
||||
footer > div {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
}
|
||||
footer small {
|
||||
flex-basis: 100%;
|
||||
font-size: 9px;
|
||||
}
|
||||
@media (max-width: 1050px) {
|
||||
.layout {
|
||||
grid-template-columns: 145px minmax(0, 1fr);
|
||||
gap: 35px;
|
||||
padding: 0 28px;
|
||||
}
|
||||
.contents nav {
|
||||
font-size: 11px;
|
||||
}
|
||||
h1 {
|
||||
font-size: 44px;
|
||||
}
|
||||
.figure {
|
||||
padding: 22px;
|
||||
}
|
||||
.flow > span {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 780px) {
|
||||
.site-header {
|
||||
height: 68px;
|
||||
padding: 0 22px;
|
||||
}
|
||||
.site-header nav {
|
||||
gap: 17px;
|
||||
font-size: 11px;
|
||||
}
|
||||
.brand {
|
||||
gap: 10px;
|
||||
}
|
||||
.brand > span {
|
||||
font-size: 10px;
|
||||
}
|
||||
.brand i {
|
||||
display: none;
|
||||
}
|
||||
.layout {
|
||||
display: block;
|
||||
padding: 0 24px;
|
||||
}
|
||||
.contents {
|
||||
position: static;
|
||||
padding: 20px 0 0;
|
||||
}
|
||||
.contents > .eyebrow,
|
||||
.version-note {
|
||||
display: none;
|
||||
}
|
||||
.contents nav {
|
||||
margin: 0;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 7px 17px;
|
||||
padding-bottom: 17px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
font-size: 10px;
|
||||
}
|
||||
.contents nav a:nth-child(n + 5) {
|
||||
display: none;
|
||||
}
|
||||
.hero {
|
||||
padding-top: 35px;
|
||||
}
|
||||
h1 {
|
||||
font-size: 39px;
|
||||
}
|
||||
.deck {
|
||||
font-size: 15px;
|
||||
}
|
||||
.article-section {
|
||||
margin-top: 47px;
|
||||
padding-top: 29px;
|
||||
}
|
||||
h2 {
|
||||
font-size: 25px;
|
||||
}
|
||||
.hero-route {
|
||||
font-size: 11px;
|
||||
gap: 6px;
|
||||
}
|
||||
.hero-route code {
|
||||
font-size: 12px;
|
||||
padding: 6px 9px;
|
||||
}
|
||||
.flow {
|
||||
gap: 7px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
.site-header {
|
||||
padding: 0 17px;
|
||||
}
|
||||
.site-header nav > a:first-child {
|
||||
display: none;
|
||||
}
|
||||
.site-header nav {
|
||||
gap: 14px;
|
||||
}
|
||||
.brand b {
|
||||
font-size: 32px;
|
||||
}
|
||||
.layout {
|
||||
padding: 0 19px;
|
||||
}
|
||||
.hero > .eyebrow {
|
||||
font-size: 8px;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
h1 {
|
||||
font-size: clamp(28px, 7.9vw, 31px);
|
||||
letter-spacing: -0.6px;
|
||||
line-height: 1.35;
|
||||
margin-top: 16px;
|
||||
}
|
||||
.hero-meta {
|
||||
font-size: 10px;
|
||||
gap: 7px 13px;
|
||||
}
|
||||
.hero-route {
|
||||
flex-wrap: nowrap;
|
||||
justify-content: space-between;
|
||||
gap: 3px;
|
||||
padding: 19px 0;
|
||||
}
|
||||
.hero-route > span {
|
||||
font-size: 10px;
|
||||
}
|
||||
.hero-route code {
|
||||
font-size: 10px;
|
||||
padding: 5px 6px;
|
||||
}
|
||||
main .lead {
|
||||
font-size: 16px;
|
||||
}
|
||||
main p {
|
||||
font-size: 13px;
|
||||
}
|
||||
h2 {
|
||||
font-size: clamp(21px, 5.9vw, 23px);
|
||||
}
|
||||
h3 {
|
||||
font-size: 18px;
|
||||
}
|
||||
.figure {
|
||||
padding: 18px 14px;
|
||||
margin: 23px -3px;
|
||||
}
|
||||
.figure-heading {
|
||||
display: block;
|
||||
}
|
||||
.figure-heading h3 {
|
||||
font-size: 17px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.tag {
|
||||
font-size: 8px;
|
||||
padding: 4px 5px;
|
||||
display: inline-block;
|
||||
margin-bottom: 17px;
|
||||
}
|
||||
.eyebrow {
|
||||
font-size: 9px;
|
||||
}
|
||||
.segmented {
|
||||
width: 100%;
|
||||
}
|
||||
.segmented button {
|
||||
flex: 1;
|
||||
padding: 8px 10px;
|
||||
font-size: 11px;
|
||||
}
|
||||
.flow {
|
||||
flex-direction: column;
|
||||
gap: 7px;
|
||||
margin: 22px 0;
|
||||
}
|
||||
.flow > span {
|
||||
width: 100%;
|
||||
padding: 11px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.flow > b {
|
||||
line-height: 1;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
.selected-detail {
|
||||
font-size: 11px;
|
||||
min-height: 73px;
|
||||
}
|
||||
.call-controls {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 14px;
|
||||
}
|
||||
.endpoint {
|
||||
align-items: flex-start;
|
||||
gap: 9px;
|
||||
}
|
||||
.endpoint code {
|
||||
font-size: 11px;
|
||||
}
|
||||
pre {
|
||||
padding: 14px;
|
||||
font-size: 10px;
|
||||
}
|
||||
.step-list button {
|
||||
font-size: 9px;
|
||||
padding: 6px 7px;
|
||||
}
|
||||
.step-head h4 {
|
||||
font-size: 15px;
|
||||
}
|
||||
#step-explanation {
|
||||
font-size: 12px;
|
||||
min-height: 91px;
|
||||
}
|
||||
.two-paths {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 24px;
|
||||
}
|
||||
.two-paths h3 {
|
||||
font-size: 16px;
|
||||
}
|
||||
.clock-boundaries strong {
|
||||
font-size: 12px;
|
||||
}
|
||||
.source-list a {
|
||||
font-size: 12px;
|
||||
}
|
||||
footer {
|
||||
display: block;
|
||||
font-size: 9px;
|
||||
}
|
||||
footer > div {
|
||||
margin: 16px 0;
|
||||
gap: 15px;
|
||||
}
|
||||
footer small {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
html {
|
||||
scroll-behavior: auto;
|
||||
}
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation: none !important;
|
||||
transition: none !important;
|
||||
}
|
||||
}
|
||||
@media print {
|
||||
.site-header,
|
||||
.contents,
|
||||
button,
|
||||
select {
|
||||
display: none;
|
||||
}
|
||||
.layout {
|
||||
display: block;
|
||||
padding: 0;
|
||||
}
|
||||
.figure {
|
||||
break-inside: avoid;
|
||||
}
|
||||
.table-wrap {
|
||||
overflow: visible;
|
||||
}
|
||||
table {
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const html = readFileSync(new URL("./index.html", import.meta.url), "utf8");
|
||||
const ids = [...html.matchAll(/\bid="([^"]+)"/g)].map((match) => match[1]);
|
||||
assert.equal(ids.length, new Set(ids).size);
|
||||
for (const match of html.matchAll(/href="#([^"]+)"/g)) {
|
||||
assert.ok(ids.includes(match[1]), `Missing anchor ${match[1]}`);
|
||||
}
|
||||
assert.ok(html.includes("GPU 推理待实测"));
|
||||
assert.ok(html.includes("mock"));
|
||||
assert.ok(html.includes("special"));
|
||||
console.log(`Validated ${ids.length} anchors and model evidence labels.`);
|
||||
Reference in New Issue
Block a user