From a2e3ceb4caa28c8943b66574bad197dcae5d233c Mon Sep 17 00:00:00 2001 From: wuyang6 Date: Mon, 9 Nov 2026 10:56:46 +0800 Subject: [PATCH] Publish voice duplex architecture latency training and evaluation research --- .dockerignore | 6 + .gitignore | 6 + Dockerfile | 10 + LICENSE | 21 + README.md | 23 + app.js | 194 ++++++ build.mjs | 17 + favicon.svg | 1 + index.html | 1319 +++++++++++++++++++++++++++++++++++++++ model.js | 61 ++ model.test.mjs | 30 + nginx.conf | 12 + package.json | 14 + release.json | 6 + robots.txt | 2 + styles.css | 1625 ++++++++++++++++++++++++++++++++++++++++++++++++ validate.mjs | 24 + 17 files changed, 3371 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 README.md create mode 100644 app.js create mode 100644 build.mjs create mode 100644 favicon.svg create mode 100644 index.html create mode 100644 model.js create mode 100644 model.test.mjs create mode 100644 nginx.conf create mode 100644 package.json create mode 100644 release.json create mode 100644 robots.txt create mode 100644 styles.css create mode 100644 validate.mjs diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..05ea36d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +.git +dist +node_modules +*.log +work +.env* diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8cbbe6b --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +dist/ +node_modules/ +.DS_Store +.env* +work/ +*.log diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..338b229 --- /dev/null +++ b/Dockerfile @@ -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 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..436a541 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..6105fec --- /dev/null +++ b/README.md @@ -0,0 +1,23 @@ +# 语音双工系统的架构、时延与评测 + +公开研究网页,比较三段式、两段式、原生语音与混合设计,解释时间片、计算耗时与用户交互时延,并整理双工训练和系统评测方法。 + +访问 [研究网页](https://voice-duplex.k1412.top/)。 + +## 本地运行 + +使用 Node.js 20 或更新版本及 Python 3。运行 `npm run check`、`npm run build`,随后运行 `npm run dev` 并访问终端显示的本地地址。 + +网页使用本地静态资源。队列实验的纯计算模型位于 `model.js`,对应回归用例位于 `model.test.mjs`。原始论文与官方资料见页面来源章节。来源报告值、工程推断和说明性模型分别标注。 + +## 内容与日期 + +资料核对日期为 2026 年 11 月 9 日。评测数据保留原始版本和条件,完整系统评测方法按方案呈现。 + +## 容器 + +Dockerfile 构建静态资源并使用 Nginx 在 8080 端口提供服务,健康检查路径为 `/healthz`。 + +## 许可 + +原创代码、文字和绘图代码采用 MIT 许可。外部链接中的论文、商标与内容遵循各自权利声明。 diff --git a/app.js b/app.js new file mode 100644 index 0000000..37a2a59 --- /dev/null +++ b/app.js @@ -0,0 +1,194 @@ +import { queueSteps, architectures, benchmarks } from "./model.js"; + +const chunkSlider = document.getElementById("chunk-slider"); +const computeSlider = document.getElementById("compute-slider"); +const queueChart = document.getElementById("queue-chart"); + +function renderQueue() { + const chunkMs = Number(chunkSlider.value); + const computeMs = Number(computeSlider.value); + const steps = queueSteps(chunkMs, computeMs); + const totalMs = Math.max(steps.at(-1).end, chunkMs * 9); + const percentage = (value) => `${(value / totalMs) * 100}%`; + document.getElementById("chunk-value").textContent = `${chunkMs} ms`; + document.getElementById("compute-value").textContent = `${computeMs} ms`; + const ratio = document.getElementById("rtf-value"); + ratio.textContent = `${(computeMs / chunkMs).toFixed(2)}×`; + ratio.classList.toggle("overloaded", computeMs >= chunkMs); + document.getElementById("backlog-value").textContent = + `${steps.at(-1).wait} ms`; + document.getElementById("growth-value").textContent = + `${Math.max(0, computeMs - chunkMs)} ms`; + queueChart.replaceChildren(); + const ticks = document.createElement("div"); + ticks.className = "queue-ticks"; + for (const fraction of [0, 0.25, 0.5, 0.75, 1]) { + const tick = document.createElement("span"); + tick.textContent = `${Math.round(totalMs * fraction)}${fraction === 1 ? " ms" : ""}`; + ticks.append(tick); + } + queueChart.append(ticks); + for (const step of steps) { + const row = document.createElement("div"); + row.className = "queue-row"; + row.setAttribute( + "aria-label", + `第${step.index}块在${step.arrival}毫秒就绪,等待${step.wait}毫秒,在${step.end}毫秒计算完成`, + ); + const label = document.createElement("span"); + label.className = "queue-label"; + label.textContent = `块 ${step.index}`; + const arrival = document.createElement("span"); + arrival.className = "queue-arrival"; + arrival.style.left = percentage(step.arrival); + const wait = document.createElement("span"); + wait.className = "queue-wait"; + wait.style.left = percentage(step.arrival); + wait.style.width = percentage(step.wait); + const compute = document.createElement("span"); + compute.className = "queue-compute"; + compute.style.left = percentage(step.start); + compute.style.width = percentage(computeMs); + row.append(label, wait, compute, arrival); + queueChart.append(row); + } + const explanation = document.getElementById("queue-explanation"); + if (computeMs < chunkMs) { + explanation.textContent = `每块留有 ${chunkMs - computeMs} ms 计算余量。此理想条件下等待为零,单块 ${computeMs} ms 的计算延迟仍然存在。`; + } else if (computeMs === chunkMs) { + explanation.textContent = + "理想条件下刚好赶上输入节奏。出现额外计算或调度抖动时,队列就会开始积压。"; + } else { + explanation.textContent = `每块增加 ${computeMs - chunkMs} ms 等待,第 8 块已等待 ${steps.at(-1).wait} ms。需要减少计算、增加处理能力或调整处理策略。`; + } +} + +function selectArchitecture(key) { + const selected = architectures[key]; + const flow = document.getElementById("architecture-flow"); + flow.replaceChildren(); + selected.nodes.forEach(([title, subtitle, type], index) => { + if (index) { + const arrow = document.createElement("span"); + arrow.className = "flow-arrow"; + arrow.textContent = "→"; + arrow.setAttribute("aria-hidden", "true"); + flow.append(arrow); + } + const node = document.createElement("div"); + node.className = `flow-node ${type || ""}`; + const strong = document.createElement("strong"); + strong.textContent = title; + const small = document.createElement("small"); + small.textContent = subtitle; + node.append(strong, small); + flow.append(node); + }); + document.getElementById("architecture-detail").innerHTML = selected.detail; + document.querySelectorAll("[data-architecture]").forEach((button) => { + button.setAttribute( + "aria-pressed", + String(button.dataset.architecture === key), + ); + }); +} + +function renderBenchmarks() { + const chart = document.getElementById("benchmark-chart"); + for (const model of benchmarks) { + const row = document.createElement("div"); + row.className = "benchmark-row"; + const label = document.createElement("span"); + label.className = "benchmark-label"; + label.textContent = model.name; + const tracks = document.createElement("div"); + tracks.className = "benchmark-tracks"; + for (const [key, className] of [ + ["first", "first"], + ["answer", "answer"], + ]) { + const bar = document.createElement("div"); + bar.className = `benchmark-bar ${className}`; + bar.style.width = `${(model[key] / 12) * 100}%`; + const value = document.createElement("span"); + value.className = "benchmark-value"; + value.textContent = `${model[key].toFixed(2)} s`; + bar.append(value); + tracks.append(bar); + } + row.append(label, tracks); + chart.append(row); + } + const axis = document.createElement("div"); + axis.className = "benchmark-axis"; + for (const value of ["0", "3", "6", "9", "12 s"]) { + const tick = document.createElement("span"); + tick.textContent = value; + axis.append(tick); + } + chart.append(axis); +} + +chunkSlider.addEventListener("input", renderQueue); +computeSlider.addEventListener("input", renderQueue); +document.querySelectorAll("[data-preset]").forEach((button) => { + button.addEventListener("click", () => { + const [chunk, compute] = button.dataset.preset.split(","); + chunkSlider.value = chunk; + computeSlider.value = compute; + renderQueue(); + }); +}); +document.querySelectorAll("[data-architecture]").forEach((button) => { + button.addEventListener("click", () => + selectArchitecture(button.dataset.architecture), + ); +}); + +const themeToggle = document.getElementById("theme-toggle"); +try { + const preference = localStorage.getItem("voice-duplex-theme"); + if (preference === "dark" || preference === "light") + document.documentElement.dataset.theme = preference; +} catch { + /* The page remains usable when storage is unavailable. */ +} +themeToggle.addEventListener("click", () => { + const theme = + document.documentElement.dataset.theme === "dark" ? "light" : "dark"; + document.documentElement.dataset.theme = theme; + themeToggle.setAttribute( + "aria-label", + theme === "dark" ? "切换浅色主题" : "切换深色主题", + ); + try { + localStorage.setItem("voice-duplex-theme", theme); + } catch { + /* Theme changes still apply for this visit. */ + } +}); + +const navigationLinks = [...document.querySelectorAll(".contents nav a")]; +const observer = new IntersectionObserver( + (entries) => { + const visible = entries + .filter((entry) => entry.isIntersecting) + .sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top); + if (!visible.length) return; + const currentId = visible[0].target.id; + navigationLinks.forEach((link) => { + const active = link.hash === `#${currentId}`; + link.classList.toggle("active", active); + if (active) link.setAttribute("aria-current", "location"); + else link.removeAttribute("aria-current"); + }); + }, + { rootMargin: "-110px 0px -55% 0px", threshold: 0 }, +); +document + .querySelectorAll(".article-section") + .forEach((section) => observer.observe(section)); + +selectArchitecture("cascade"); +renderQueue(); +renderBenchmarks(); diff --git a/build.mjs b/build.mjs new file mode 100644 index 0000000..9d0f685 --- /dev/null +++ b/build.mjs @@ -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.`); diff --git a/favicon.svg b/favicon.svg new file mode 100644 index 0000000..2c18936 --- /dev/null +++ b/favicon.svg @@ -0,0 +1 @@ + diff --git a/index.html b/index.html new file mode 100644 index 0000000..a5d6b81 --- /dev/null +++ b/index.html @@ -0,0 +1,1319 @@ + + + + + + + + + + + + + + 语音双工系统的架构、时延与评测 · K1412 Research + + + + + +
+ +
+
+
SYSTEM ARCHITECTURE · REALTIME INTERACTION
+

语音双工系统的
架构、时延与评测

+

+ 从一句话怎样被听见,到一次任务怎样被完成。
沿着信息流与时间轴,判断一个语音系统好在哪里。 +

+
+ 技术研究2026 年 11 月 9 日起点 · Higgs Realtime Duplex ↗ +
+
+
+ 听见理解回应继续听 +
+
+ 用户 + + 持续输入 +
+
+ 系统 + + 按需输出 +
+
+ 任务 +
异步执行结果返回
+ 独立推进 +
+
+ 概念示意 · 三条轨道在同一时间轴上推进 +
+
+

+ 架构决定信息如何流动,以及有哪些优化空间。系统的价值需要同时用任务正确率、交互行为、用户端时延和完整成本来证明。 +

+

+ 一次“帮我订周五的票,嗯……改成周六”的请求,同时考验语音理解、句中停顿、参数修正、工具执行与回应时机。只看首次发声速度,会遗漏这条链路中的大部分问题。 +

+
+ +
+
01 / ARCHITECTURE
+

段数、原生语音与双工分别描述什么

+

+ 这里按推理时的信息流区分三段式、两段式和原生 + S2S。两段式在业界存在不同用法,本页采用“语音理解模型输出回答文本,再接语音合成”的工作定义。带 + Thinker、Talker 和共享隐藏状态的模型单列为混合设计。 +

+

+ ASR 或 STT 表示语音识别,LLM 表示语言模型,TTS 表示语音合成。S2S + 表示从语音输入到语音输出,codec 是声音的编解码器。 +

+
+
+
+ 信息流比较 +

语音在哪一步变成了文字

+
+ 工程分析 +
+
+ +
+
+
+
+
+
+

流式处理

+

输入或输出尚未结束,就可以开始处理、传输和播放。

+
+
+

全双工交互

+

+ 系统说话期间,持续利用新输入,判断继续说、让出话轮或修正任务。 +

+
+
+

原生语音建模

+

+ 语音特征直接进入模型,或由模型生成语音表示。具体系统仍需验证双工行为。 +

+
+
+

+ DuplexCascade 把流式 ASR、文本 LLM 和流式 TTS + 用微轮次连接,提供了级联全双工的研究实例。段数与双工能力因此需要分别记录。语音编码器、codec + 和波形解码器也可以存在于端到端模型中,直接清点模块数容易得到含混分类。[2] +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ 三类信息流的工程取舍。以下为机制推断,具体性能由实现与训练决定。 +
维度三段式两段式原生 S2S
输入声学线索仅传 ASR 文本时,语气、重叠和非语言声音难以传递可保留在音频特征中,取决于模型与训练可直接联合建模,需要相应数据支持
首音关键路径残余识别、话轮判断、LLM 首段、TTS 首块语音理解生成、话轮判断、TTS 首块听说决策、音频表示生成、波形解码
知识与工具易接入成熟文本模型和工具生态依赖语音适配后模型的能力保留依赖语音训练与工具训练,常配合后台模型
语音输出韵律和表现力主要由 TTS 决定文本接口与 TTS 约束仍然存在可把内容与声学表现联合生成
调试与控制文本接口明确,分段替换、审计和定位较直接输出文本清楚,输入理解过程更紧耦合需要音频对齐、事件和播放状态等观测
持续双工需流式调度与话轮策略需输入持续更新与调度策略需因果听说训练与可持续的实时推理
总成本 + 统一计算监听、前后台模型、编解码、工具、网络和所需并发。段数本身不足以给出成本排名。 +
+
+
+ +
+
02 / METRICS
+

主要指标围绕完整交互组织

+

+ 主指标负责回答“系统有没有把事情做好”,诊断指标负责定位问题发生在哪个环节。每项统计都需要固定起止事件、分母、超时规则和系统边界。 +

+

+ P50 为中位数,P95 表示 95% 样本不超过的值。Pass@1 + 是一次尝试的成功率。 WER、CER 分别衡量词和字的识别错误,MOS + 是主观质量评分的均值。 +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
主要问题建议主指标测量与诊断
多久开始回应用户端首音时延 P50 / P95 + 从标注的用户话轮结束,到客户端实际播出的首段非静音语音。另报端点判断耗时。 +
多久得到有用结果有效答案时延、任务完成时延 + “我查一下”与包含所需信息的答案分别计时。工具最终状态与语音告知分别验收。 +
该停时能否停真打断检出率、停止说话时延 P95 + 从有效打断开始到扬声器停止,包含取消传播、在途音频和播放缓冲。 +
该继续时能否继续误打断率、附和与旁人对话处理正确率 + 分别测试“嗯、对”、对旁人说话、背景广播。按情境判断期望行为。 +
说话时有没有听懂双讲条件下的信息保留与修正成功率 + 模型输出期间插入新的编号、约束或改口,检查后续回答和工具参数。 +
任务有没有完成Pass@1、正确工具链与最终状态 + 包含无响应、超时、错参数、过期结果和取消失败。WER、CER + 与实体准确率用于归因。 +
任务是否高效完成有效完成前提下的轮次、冗余步骤、无效工具调用与返工 + 在相同任务与质量门槛下比较,保留必要澄清的收益,并报告全部尝试的成本。 +
声音是否可用真人盲评偏好、可懂度、关键信息读对率 + 内容正确性、韵律、发音与音质分开评分。自动 MOS 可作补充。 +
能否稳定长时间工作按会话长度分层的成功率、断流与积压 + 测试长会话、回声、远场、丢包、上下文压缩和后台任务交错。 +
能否承担规模与成本满足质量及时延门槛的并发、每有效任务成本 + 同时报告每分钟整机成本、监听占比、前后台调用和 GPU 占用。 +
+
+

+ Full-Duplex-Bench v1.5 + 把重叠场景分成打断、附和、旁人对话和背景语音,并测量行为及停止、重入时间。这组情境适合构成交互测试的基础。[10] +

+
+ +
+
03 / TIME
+

160 ms 是怎样的一段时间

+

+ Higgs 把连续媒体和工作流按 160 ms + 时间片组织。模型读入当前片段,再生成下一片段的输出,思考和工具调用可以跨多个片段持续。这种设计定义了更新节奏。用户端时延还涉及采集、计算、决策、网络和播放。[1] +

+
+
+ 网络包 +

运输的单位,常比模型时间片更小。

+
+
+ 音频帧 +

编码和解码的时间单位。

+
+
+ 模型时间片 Δ +

一次更新覆盖的媒体时长。

+
+
+ 单步耗时 C +

完成计算实际经过的墙钟时间。

+
+
+

每块都要在 160 ms 内算完吗

+

+ 对单会话、串行关键阶段,要持续按 160 ms + 推进,就需要及时产出每个下一块。长期平均计算耗时低于 160 ms + 是基本条件,还需要给抖动留余量,关注尾延迟、超期比例和缓冲。多阶段流水线可以把吞吐摊开计算,关键路径上的固定延迟仍然存在。 +

+

+ 用一个简化的单工作者模型说明。当 Δ = 160 ms、C = 80 + ms,计算有余量。当 C = 220 ms,每处理一个新块,等待队列便增加约 60 + ms。Higgs 博客没有披露单步耗时的 P50 / P95 + 或超期比例,无法据分块长度确认其实际计算分布。 +

+
+
+
+ 交互实验台 +

改变处理节奏,观察积压

+
+ 说明性模型 +
+

+ 单会话、单工作者、固定耗时、无流水线。数值由下方参数计算。 +

+
+ +
+
+ +
+
+
+ 处理 / 媒体时长0.50× +
+
+ 第 8 块等待计算0 ms +
+
+ 每块新增长期积压0 ms +
+
+ +
+ 输入块就绪等待计算实际计算 +
+

+

+ Bk+1 = max(0, Bk + C − Δ) +

+
+

分块越短,交互会怎样变化

+

+ 事件发生在一个时间片内部时,模型需要等到该事件进入可用输入。事件均匀落在固定边界之间、并且必须等整块时,残余等待平均约 + Δ/2,最大接近 Δ。实际系统还受编码器前视和接收策略影响。 +

+

+ 缩短 Δ + 可以更频繁地更新感知,也会增加调用、调度和控制开销,并改变每步可用的信息量。MiniCPM-o + 4.5 的作者比较了 1.0 s、0.2 s、0.1 s,在其模型和训练设置下选择 1.0 s + 作为折中。时间片大小要和训练方式、语义稳定性一起评估。[6] +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ 公开资料中的时间粒度与时延。各行测量对象不同,分别保留原口径。 +
实现时间粒度公开时延及其含义
Higgs Realtime Duplex160 ms 统一时间片 + 博客披露分块设计,端到端首音分布与逐块计算分布待补充。[1] +
TML Interaction Small200 ms 微轮次 + 作者在 FDB v1 报告轮替时延 0.40 s,属于特定评测设置。[7] +
MoshiMimi 音频帧 80 ms + 80 ms 帧与额外声学延迟构成 160 ms 理论路径,作者报告实践约 + 200 ms。[4] +
MiniCPM-o 4.5论文选用 1.0 s 时间片 + 论文的分块消融说明其设计取舍,用户端响应需结合部署测量。[6] +
Qwen3-Omni输入音频特征约 80 ms / 步 + 作者估算音频首包在并发 1 / 4 / 6 时为 234 / 728 / 1172 + ms。该值属于服务端理论首包口径。[5] +
+
+

+ 时间数据来源 + Higgs、TML、Moshi、MiniCPM-o、Qwen3-Omni。 +

+

用户听到回答的时间取决于关键路径

+
+ 边界后残余采集与上行→话轮或语义决策→关键路径计算→下行与实际播放 +
+

+ 流式系统会把部分识别、理解与生成提前到用户仍在说话时。核算时只计事件发生后尚未完成的部分,并保留阶段重叠。首次开口、给出有效内容、完成工具动作和停止播放,分别使用自己的起止事件。 +

+

+ 声音已经生成后,还可能在网络或播放器中排队。Google 的 Live API + 指南明确要求收到打断事件时清空待播音频。客户端缓冲因此会影响用户感知的停止时延。[14] +

+
+ +
+
04 / IMPLEMENTATIONS
+

用代表实现建立架构地图

+

+ 下面以公开结构或接口描述定位代表路线。闭源模型的内部模块、实际训练数据和服务调度仍有未知项。版本明确的论文适合解释机制,当前系统选型需要固定具体模型 + ID 再测。 +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
代表实现公开结构比较时应核对的边界
LiveKit 级联管线独立 STT、文本 LLM、TTS,应用控制话轮与打断 + 流式程度、提前生成、端点策略和播放取消共同决定体验。官方文档 ↗ +
DuplexCascadeASR–LLM–TTS,以约 0.6 s 微轮次和控制 token 组织双向流 + 级联双工的研究样例,文本瓶颈与交互策略分别测量。论文 ↗ +
Ultravox 0.7 + TTS音频特征经投影进入 LLM,核心输出流式文本 + 本页按两段式归类。外接 + TTS、话轮控制和具体适配配置须纳入整机边界。官方仓库 ↗ +
Moshi / PersonaPlex用户与系统多路语音流,结合文字和声学 token + 同时听说、语音内容能力、角色控制和上下文窗口分别考察。Moshi ↗ + PersonaPlex ↗ +
Qwen3-Omni + Thinker–Talker 分工,支持多模态特征、流式文本与语音生成 + + 作为模块化原生音频路线比较。持续输入能否影响正在进行的输出,须在具体接口验证。论文 ↗ +
MiniCPM-o 4.5 / Freeze-Omni + 语言骨干连接语音输入和专用语音输出模块,并训练时间对齐或状态控制 + + 属于语义骨干与语音头紧耦合的路线,各模式的双工行为与能力保留需单独测量。MiniCPM-o ↗ + Freeze-Omni ↗ +
GPT-Realtime / GPT-Live / Gemini Live + 公开提供原生语音交互接口,各系列的轮次、工具和持续听说能力有差别 + + OpenAI 文档把 GPT-Live + 的持续听说与后台代理协作单独说明。能力按模型和 API + 版本记录。OpenAI ↗ + Google ↗ +
Higgs / TML Interaction统一时间片更新输入输出,并行推进交互和后台任务 + 比较时记录前台独立能力、后台模型配置、委派比例与完整成本。Higgs ↗ + TML ↗ +
+
+
+ +
+
05 / EVIDENCE
+

同场数据怎样揭示不同取舍

+

+ Full-Duplex-Bench v3 的 2026 年 4 月 v1 研究,用 100 条真人录音、12 + 名说话者,在统一 LiveKit 框架与本地 mock + 工具下比较多种配置。下面保留其中四项,展示三类信息流的系统表现。这组结果属于该版本和样本的评测快照。[11] +

+
+
+
+ FDB v3 · 2026-04 · 作者报告 +

首次开口与有效答案的距离

+
+ 来源报告值 +
+ +
+ 首次开口有效答案 +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
配置任务成功率首次开口有效答案应答率抢话率
Whisper → GPT-4o → TTS45.0%8.78 s10.12 s100.0%33.0%
Ultravox 0.741.0%3.88 s8.40 s96.0%47.9%
GPT-Realtime 1.560.0%6.36 s6.89 s96.0%13.5%
Gemini Live 3.154.0%3.95 s4.25 s78.0%19.2%
+
+

+ 首音与有效答案为论文平均时延。抢话沿用论文的负话轮间隔口径,重叠是否合适仍需按情境判断。论文表 2 与表 6 ↗ +

+
+

+ 两段式在这批配置中更早开口,成功率仍低于该级联基线。不同原生语音系统也表现出速度与应答覆盖率的取舍。论文将 + Ultravox 广义归入 + S2S,本页依据其公开核心的音频到文本结构作两段式定位,TTS + 适配的披露有限。系统级差异需要结合模型能力、话轮和部署配置解释。 +

+
+ +
+
06 / HIGGS
+

Higgs 的论证如何组织

+

+ Higgs + 以自然交互、智能和成本组织设计。其前台模型按时间片持续接收信息,自己思考、调工具,并按需委派后台。博客用交互、问答与任务等多组成绩呈现取舍,FDB + v1.5 得分 75.5、τ-Voice 69.8,同表仍有更高配置。[1] +

+
+
+ 需求自然交互 · 办成任务 · 可承担成本 +
+
+ 机制时间对齐 · 异步执行 · 按需委派 +
+
+ 证据具体场景 · 固定维度 · 多组基准 +
+
+

+ 这种比较方法可以转化为评测纪律。每个系统都交代任务范围、完整模型配置、时间预算和成本边界。Demo + 解释机制,测试集衡量覆盖,配对消融说明改善来自哪项设计。 +

+

+ 前台和后台的分工尤其影响解读。TML + 的表格注明,部分推理与工具成绩启用了后台代理。比较“模型能力”与“整机能力”时,应分别报告前台独立配置和完整系统配置。MoE + 总参数量、音频 token 速率、后台调用和实际价格也需要分别记录。[7] +

+
+ +
+
07 / TRAINING
+

全双工如何进入架构与训练

+

+ 全双工涉及三项设计。输入在输出期间怎样持续进入,听与说怎样按时间对齐,模型怎样学习何时继续、停下或行动。下列组织方式可以组合使用。 +

+
+
+ 多流联合建模 +

分别表示双方语音

+

+ Moshi + 用用户流、系统流和时间对齐文本联合建模,保留重叠。PersonaPlex + 沿用其架构与权重,加入角色和声音控制。[4][8] +

+
+
+ 时间对齐序列 +

持续读写微轮次

+

+ Higgs、TML + 将输入输出按时间切分并交错处理。外部交互连续进行,内部计算遵循因果顺序。[1][7] +

+
+
+ 语义骨干与语音头 +

分担内容和声学生成

+

+ MiniCPM-o 用骨干的文本和隐藏状态驱动轻量语音模块。Freeze-Omni + 接入语音模块与逐块状态预测。[6][9] +

+
+
+ 级联微轮次 +

同步独立模块

+

+ DuplexCascade 用微轮次和控制 token 协调 ASR、LLM 与 + TTS,训练等待、回应和附和等行为。[2] +

+
+
+

基于语言模型继续预训练,具体改了什么

+

+ 文本模型已学会知识、推理和语言生成。语音系统在此基础上接入声音表示,使语音特征能进入骨干,并让生成端输出语音 + token + 或驱动专用语音头。训练时加入文本与声音对应关系、语音连续性,以及带时间的交互轨迹。 +

+

+ 骨干可以继续做统一序列预测,音频编码器和波形解码器按各自目标训练。不同项目会选择冻结已有组件、先训新增模块,再逐步联合训练,或始终保留冻结的语言骨干。 +

+
    +
  1. + 01 +
    +

    建立声音表示与模态对齐

    +

    + 利用语音、转写与配对数据,把音频特征映射到语言表示,训练语音生成模块。编码帧率与压缩影响计算预算和声学信息。 +

    +
    +
  2. +
  3. + 02 +
    +

    混合继续预训练

    +

    + 混合文本、音频与可选视频数据,学习跨模态预测并保留语言能力。知识问答和文本能力需要在语音训练前后配对评估。 +

    +
    +
  4. +
  5. + 03 +
    +

    按时间训练双工行为

    +

    + 把双声道、静默、停顿、附和、重叠和改口保留在时间轴上。训练中限制未来信息访问,让输出只依赖当时已到达的输入。 +

    +
    +
  6. +
  7. + 04 +
    +

    交互微调与强化学习

    +

    + 监督微调(SFT)示范合适动作,强化学习(RL) + 可依据正确答案、指令遵循、行动效果或交互行为优化。具体采用哪些奖励,需要逐模型查证。 +

    +
    +
  8. +
  9. + 05 +
    +

    蒸馏与仿真迭代

    +

    + 部分系统把专长教师的能力蒸馏到交互模型,并从仿真失败中合成训练数据。模拟器的说话时机、工具返回和噪声会影响学到的行为。 +

    +
    +
  10. +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ 代表项目公开的训练路线 +
项目初始化与训练理解重点
Moshi + 用文本 Helium 初始化时间 + Transformer,混合音频与文本预训练,加入双流对话及合成语音指令数据。 + + 文本能力迁移、音频生成与双流交互分阶段建立。训练章节 ↗ +
MiniCPM-o 4.5 + 先冻结已有组件训练新增语音模块,再解冻联合预训练,之后做监督微调(SFT) + 与组相对策略优化(GRPO)。 + + 模态对齐、联合表征、行为与推理逐步训练。训练章节 ↗ +
Freeze-Omni + 保留冻结语言骨干,训练语音输入输出模块,并用逐块状态预测学习双工控制。 + + 冻结骨干提供另一种适配选择,整体效果仍须测量。模型与训练 ↗ +
Higgs + 作者报告从文本模型继续训练约 2T 多模态 + token,以强化学习(RL) + 训练专长教师后蒸馏到学生,并用仿真失败数据迭代。 + + 继续预训练、行为后训练与仿真数据机制分别核验。作者说明 ↗ +
+
+

+ 语音转写和音质测试覆盖了模态能力的一部分。持续双工还需要验证新输入能否改变当前生成与后续行动,尤其是模型已经开始回答、工具仍在执行时的参数修正。 +

+
+ +
+
08 / EVALUATION
+

怎样证明自己的系统更好

+

+ 先确定目标场景及成功条件,再比较质量、交互、时延和成本的共同边界。可以把通过任务正确、交互规则和时间门槛的对话记为“有效完成”,并同时报告各原始指标,保留失败原因。 +

+
+ 满足交互与时限的有效完成率满足全部预设门槛的对话数 ÷ 全部测试对话数每个有效任务成本整轮测试全部系统成本 ÷ 有效完成任务数 +
+

+ 这是可选的项目级验收口径,需要预先确定门槛与超时规则。有效完成数为零时,成本指标记录为不可用并保留完整支出。 +

+

从评测标准推导可视化

+

+ 先写清楚怎样判定系统好,再确定需要采集哪些事件,最后选择图形。交互时机、任务完成与效率、时间、成本构成评价主线。模块活跃与工具异步轨迹用来解释这些结果。 +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ 评测目标、可判定证据与视图的映射。属于项目方案,可按业务裁剪。 +
评价维度判据与分母所需事件或状态对应视图
交互时机 + 在标注窗口内执行合适行为的事件数,除以全部应决策事件。过早、过晚、遗漏分别统计。 + 语义意图、说话对象、期望时间窗、实际听说与停播时间。 + 期望窗口与实际动作叠加的对话时间线,打断与误打断权衡。 +
任务完成 + 满足用户约束和最终状态的任务数,除以全部任务。超时和无响应计入。 + + 任务规范、约束修订、工具参数、结果、最终环境状态与语音告知。 + 任务状态轨迹、约束检查表、配置间配对成功与失败对照。
完成效率 + 在同一质量门槛下比较总轮次、冗余澄清、重复动作、无效调用和返工,同时呈现成功率。 + + 对话轮次、动作因果、工具调用目的、撤销与重试、必要步骤基线。 + 任务路径与返工支路,成功任务分层与全量尝试开销。
时间 + 首音、有效答案、任务完成、停播的 + P50/P95,附超时覆盖率与会话分层。 + + 采集、网络、排队、模型、工具、解码、播放的对齐时间戳。 + 关键路径、阶段重叠、延迟分布与尾部样本复盘。
成本 + 完整系统支出、每分钟成本、每个有效完成任务成本,以及满足门槛的并发。 + + 监听时长、前后台用量、工具费用、编解码资源与账单边界。 + 质量—时延—成本配置曲线,成本构成与并发负载曲线。
+
+

+ 结果层回答是否达到门槛,诊断层解释因果链,展示层帮助读者理解过程。单次回放、两配置配对与样本集聚合使用相同事件定义。图中的“计算请求在途”按观测事件标记,内部思考状态需要对应的公开事件才能呈现。 +

+

先把测试条件固定

+
    +
  • + 固定模型 + ID、服务版本、系统提示、工具和后台模型,记录推理预算与委派次数。 +
  • +
  • + 使用相同输入和设备条件,按实际时间流入音频,在系统实际播放时触发重叠事件。 +
  • +
  • + 同时记录客户端采集、发送、服务计算、工具、返回与实际播放,标明时钟对齐误差。 +
  • +
  • + 无响应和超时保留在成功率分母中,时延分布同时标注完成覆盖率。 +
  • +
  • + 按对话配对比较,多次重复并按会话聚类计算置信区间,避免把一个长对话的多个片段当作独立样本。 +
  • +
  • + 在相同成本或质量门槛下比较配置,另做前台独立与整机两套结果。 +
  • +
+

最小情境集覆盖八类问题

+
+
+ 01正常轮替与句中停顿 +

+ 测试不同长度的自然停顿,检查抢话、等待和端点判断。用户最终意图保持一致,避免换题造成混淆。 +

+
+
+ 02真打断与参数改口 +

+ 系统说话时更新日期、目的地或数量,核对停说、旧任务取消、后续工具参数与最终状态。 +

+
+
+ 03附和与延续 +

+ 插入“嗯、对、你继续”,观察是否误停、重复已经说过的内容或丢失上下文。 +

+
+
+ 04旁人对话与背景语音 +

+ 控制说话对象与远近声源,检查应答对象判断,按场景标注期望继续、暂停或安静。 +

+
+
+ 05双讲时的信息保留 +

+ 系统输出期间提供一个新编号或限制条件,再测试能否正确使用,区分收到音频与理解信息。 +

+
+
+ 06异步工具与任务变化 +

+ 模拟工具延迟、部分结果、失败和过期结果,在等待时加入新问题、修改或撤销请求。 +

+
+
+ 07中文实体与复杂任务 +

+ 覆盖中文人名、地址、数字、专业词、口音和中英混说,同时保留简单与多步工具任务。 +

+
+
+ 08长会话与真实音频链路 +

+ 覆盖长时间监听、上下文压缩、回声、丢包和播放积压。按设备、网络与会话时长分层报告。 +

+
+
+

+ 可先用少量案例核验采集与评分,再扩充独立测试集。样本量应由要检测的差异与置信度确定。开发案例、公开基准和最终保留测试集分别维护。 +

+

基准各自承担一个判断任务

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
证据面主要回答的问题需要配合的证据
Full-Duplex-Bench v1 / v1.5轮替、打断、附和和干扰时怎样行动真实设备链路与业务正确性。[10]
Full-Duplex-Bench v3真人不流利语音下的工具调用、行为和时延最新配置、真实工具异常和更广场景。[11]
Audio MultiChallenge多轮约束保持、记忆、自洽与语音改口 + 其固定上下文协议只评分最终回答,持续闭环交互另测。[12] +
τ-Voice在可核验环境中是否完成任务,语音与文本能力差多少真人与目标语言、设备的实测。[13]
项目保留集与真人盲评目标业务、中文和实际交互体验与公开基准分别展示,保留录音、状态与评分依据。
+
+

用消融解释改进来源

+

+ 一次只改变一项设计,比较提前处理开关、双工话轮控制、后台委派、播放器缓冲等设置。对时间片大小的实验,需要使用模型支持且训练条件相容的配置。若骨干、数据和服务都不同,证据支持的是整机配置之间的差异。 +

+

+ 发布结果时给出四组图。任务成功率与有效答案时延的配置曲线,真打断检出与误打断的权衡,质量及时延门槛下的成本和并发,典型成功及失败对话的因果时间线。每个更优结论都应附带适用场景与代价。 +

+
+ 可复核的结论表达 +

+ 在预先固定的场景、版本、预算和测试集上,配置 A + 在达到任务质量门槛的同时,改善了有效答案时延或交互行为。给出配对差异、置信区间、整机成本,以及退化场景。 +

+
+
+ +
+
09 / REFERENCES
+

来源与证据边界

+

+ 资料核对日期为 2026 年 11 月 9 + 日。性能数字来自注明版本的论文和官方报告。页面中的队列实验与概念图用于解释机制,尚待执行的项目评测按方案呈现。原始图表均由本页代码重绘,未复制第三方媒体。 +

+
    +
  1. + Boson AI · Higgs Realtime Duplex2026-11-05。时间片、模型设计、训练与作者比较表。 +
  2. +
  3. + DuplexCascade2026-03,v1。级联流式微轮次架构。 +
  4. +
  5. + Fixie · Ultravox官方仓库,检索日读取。音频输入、流式文本输出及开源版本。 +
  6. +
  7. + Moshi2024-10,v2。多流、音频帧、理论时延与训练过程。 +
  8. +
  9. + Qwen3-Omni Technical Report2025-09,v1。Thinker–Talker 结构与并发首包估算。 +
  10. +
  11. + MiniCPM-o 4.52026-04,v1。Omni-Flow、分块消融、模型结构与分阶段训练。 +
  12. +
  13. + Thinking Machines Lab · Interaction Models检索日读取。200 ms 微轮次、前后台分工与评测配置。 +
  14. +
  15. + NVIDIA · PersonaPlex官方仓库,检索日读取。基于 Moshi 的角色与声音控制。 +
  16. +
  17. + Freeze-Omni2024-11,v1。冻结骨干、语音模块与逐块状态预测。 +
  18. +
  19. + Full-Duplex-Bench v1.52025-07,v1。四类重叠情境与行为、时延测量。 +
  20. +
  21. + Full-Duplex-Bench v32026-04,v1。真人语音、工具任务、表 2 与表 6 的报告值。 +
  22. +
  23. + Scale · Audio MultiChallenge检索日读取。固定上下文、最终回答与 APR 评价协议。 +
  24. +
  25. + τ-Voice2026-03,v1。语音任务、环境状态与文本能力比较。 +
  26. +
  27. + Google · Live API Best Practices检索日读取。网络发送块与客户端打断播放处理。 +
  28. +
  29. + OpenAI · Audio and Voice检索日读取。实时语音工作流与 GPT-Live 公开能力。 +
  30. +
  31. + LiveKit · Pipeline Types检索日读取。级联、轮次式与持续双工管线的接口说明。 +
  32. +
+
+ +
+
+ + diff --git a/model.js b/model.js new file mode 100644 index 0000000..3c8bda1 --- /dev/null +++ b/model.js @@ -0,0 +1,61 @@ +export function queueSteps(chunkMs, computeMs, count = 8) { + if ( + !Number.isFinite(chunkMs) || + !Number.isFinite(computeMs) || + chunkMs <= 0 || + computeMs <= 0 + ) { + throw new RangeError("Time values must be finite and positive."); + } + let previousEnd = 0; + return Array.from({ length: count }, (_, index) => { + const arrival = (index + 1) * chunkMs; + const start = Math.max(arrival, previousEnd); + const end = start + computeMs; + previousEnd = end; + return { index: index + 1, arrival, start, end, wait: start - arrival }; + }); +} + +export const architectures = { + cascade: { + nodes: [ + ["用户语音", "声音输入", "input"], + ["ASR", "识别成文本"], + ["文本 LLM", "生成回答"], + ["TTS", "合成声音"], + ["系统语音", "播放输出", "output"], + ], + detail: + "

两个独立的文本接口。ASR 把用户语音交给文本模型,TTS 再把回答读出。模块可以独立替换。仅传文字时,输入声学信息会在接口处被压缩。

持续双工需要在这些模块之间保持新输入、话轮状态、取消与播放进度的一致。

", + }, + two: { + nodes: [ + ["用户语音", "声音输入", "input"], + ["语音理解 LLM", "直接读取音频特征"], + ["回答文本", "内容接口"], + ["TTS", "合成声音"], + ["系统语音", "播放输出", "output"], + ], + detail: + "

输入端直接理解声音。模型接收音频特征并生成回答文字,之后由独立 TTS 合成。输入端可以保留语气等线索,输出端仍通过文本接口组织。

本页以 Ultravox 核心加外接 TTS 作为这一工作定义的例子。

", + }, + native: { + nodes: [ + ["用户语音", "声音输入", "input"], + ["语音表示", "编码或特征"], + ["联合交互模型", "声音与内容建模"], + ["语音解码", "还原波形"], + ["系统语音", "播放输出", "output"], + ], + detail: + "

语音直接参与内容与声学生成。编码器、音频 token、文字辅助流和波形解码器可以共同存在,具体组织依模型而异。

持续听说依赖输入更新方式、因果训练与实时服务。原生语音接口与全双工行为分别核验。

", + }, +}; + +export const benchmarks = [ + { name: "级联管线", first: 8.78, answer: 10.12 }, + { name: "Ultravox 0.7", first: 3.88, answer: 8.4 }, + { name: "GPT-Realtime 1.5", first: 6.36, answer: 6.89 }, + { name: "Gemini Live 3.1", first: 3.95, answer: 4.25 }, +]; diff --git a/model.test.mjs b/model.test.mjs new file mode 100644 index 0000000..e0ee897 --- /dev/null +++ b/model.test.mjs @@ -0,0 +1,30 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { queueSteps, benchmarks } from "./model.js"; + +test("faster processing leaves no waiting queue", () => { + const steps = queueSteps(160, 80); + assert.equal(steps[0].arrival, 160); + assert.equal(steps[0].end, 240); + assert.ok(steps.every((step) => step.wait === 0)); +}); +test("equal processing and media intervals leave zero ideal headroom", () => { + assert.ok(queueSteps(160, 160).every((step) => step.wait === 0)); +}); +test("overload adds sixty milliseconds per subsequent input block", () => { + const steps = queueSteps(160, 220); + assert.equal(steps[0].wait, 0); + assert.equal(steps[7].wait, 420); + assert.equal(steps[7].end, 1920); +}); +test("invalid media times fail explicitly", () => { + assert.throws(() => queueSteps(0, 80), RangeError); + assert.throws(() => queueSteps(160, Number.NaN), RangeError); +}); +test("benchmark ordering and source values are preserved", () => { + assert.deepEqual( + benchmarks.map((model) => model.answer), + [10.12, 8.4, 6.89, 4.25], + ); + assert.ok(benchmarks.every((model) => model.first <= model.answer)); +}); diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..abd4e3e --- /dev/null +++ b/nginx.conf @@ -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; } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..1d09e9b --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "name": "voice-duplex-research", + "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 4173 --bind 127.0.0.1 --directory dist" + }, + "engines": { + "node": ">=20" + } +} diff --git a/release.json b/release.json new file mode 100644 index 0000000..5a22c2d --- /dev/null +++ b/release.json @@ -0,0 +1,6 @@ +{ + "release": "voice-duplex-20261109-v1", + "researchCutoff": "2026-11-09", + "canonicalUrl": "https://voice-duplex.k1412.top/", + "evidence": "published primary sources and clearly labeled explanatory simulations" +} diff --git a/robots.txt b/robots.txt new file mode 100644 index 0000000..c2a49f4 --- /dev/null +++ b/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Allow: / diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..0fc8e7f --- /dev/null +++ b/styles.css @@ -0,0 +1,1625 @@ +:root { + color-scheme: light; + --paper: #f7f6f2; + --surface: #fffefa; + --ink: #202922; + --muted: #72766d; + --line: #d9ddd2; + --blue: #3167c6; + --blue-soft: #e5edf9; + --green: #287557; + --green-soft: #e3eee5; + --amber: #b97625; + --amber-soft: #f8ead3; + --code: #eff0e9; + --header: rgba(247, 246, 242, 0.94); +} +html[data-theme="dark"] { + color-scheme: dark; + --paper: #171c19; + --surface: #202722; + --ink: #e1e7dd; + --muted: #a5afa1; + --line: #3c483d; + --blue: #8bb2f5; + --blue-soft: #263b55; + --green: #8bc5a0; + --green-soft: #294235; + --amber: #e4b16c; + --amber-soft: #493a25; + --code: #2a322b; + --header: rgba(23, 28, 25, 0.94); +} +* { + box-sizing: border-box; +} +html { + scroll-behavior: smooth; + scroll-padding-top: 104px; +} +body { + margin: 0; + background: var(--paper); + color: var(--ink); + font-family: + -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", + "Microsoft YaHei", sans-serif; + font-size: 16px; + line-height: 1.85; + -webkit-font-smoothing: antialiased; +} +a { + color: inherit; + text-decoration: none; +} +a:hover { + color: var(--green); +} +button, +input { + font: inherit; +} +button { + cursor: pointer; +} +button:focus-visible, +a:focus-visible, +input:focus-visible, +summary:focus-visible { + outline: 3px solid var(--blue); + outline-offset: 4px; +} +button { + color: var(--ink); +} +h1, +h2, +h3, +p { + margin: 0; +} +h1, +h2, +h3 { + font-weight: 600; +} +p + p { + margin-top: 17px; +} +strong { + font-weight: 600; +} +::selection { + background: var(--green-soft); +} +.skip-link { + position: absolute; + top: -90px; + left: 20px; + padding: 8px 16px; + background: var(--ink); + color: var(--paper); + z-index: 10; +} +.skip-link:focus { + top: 12px; +} +.site-header { + height: 85px; + position: sticky; + top: 0; + z-index: 5; + background: var(--header); + backdrop-filter: blur(14px); + display: flex; + align-items: center; + justify-content: space-between; + border-bottom: 1px solid var(--line); + padding: 0 5vw; +} +.brand { + display: flex; + align-items: center; + gap: 14px; + font-size: 12px; + letter-spacing: 1.8px; +} +.brand-mark { + font-family: Georgia, serif; + font-size: 37px; + line-height: 1; + letter-spacing: -4px; + margin-right: 3px; +} +.brand b { + font-weight: 500; + color: var(--muted); + margin-left: 8px; +} +.site-header nav { + display: flex; + align-items: center; + gap: 27px; + font-size: 13px; +} +.icon-button { + background: none; + border: 1px solid var(--line); + border-radius: 50%; + width: 36px; + height: 36px; + padding: 0; + font-size: 20px; + line-height: 1; +} +.page-layout { + max-width: 1370px; + margin: 0 auto; + display: grid; + grid-template-columns: 190px minmax(0, 1fr); + gap: 70px; + padding: 0 48px; +} +.contents { + padding-top: 66px; + position: sticky; + top: 85px; + align-self: start; + height: calc(100vh - 85px); + max-height: 790px; +} +.eyebrow { + font-size: 10px; + letter-spacing: 1.7px; + font-weight: 600; + color: var(--muted); +} +.contents nav { + display: flex; + flex-direction: column; + margin-top: 30px; + gap: 13px; + font-size: 12px; +} +.contents nav a { + color: var(--muted); + position: relative; + padding-left: 11px; + border-left: 2px solid transparent; + transition: color 0.15s; +} +.contents nav a.active { + color: var(--ink); + border-color: var(--green); +} +.contents-note { + border-top: 1px solid var(--line); + margin-top: 32px; + padding: 22px 0 0 13px; + font-size: 11px; + color: var(--muted); + line-height: 1.8; +} +.contents-note strong { + font-size: 12px; + letter-spacing: 1px; + color: var(--ink); +} +.contents-note span:last-child { + display: block; + margin-top: 10px; +} +.status-dot { + display: inline-block; + width: 5px; + height: 5px; + border-radius: 50%; + background: var(--green); + margin-right: 6px; +} +.hero { + padding-top: 60px; +} +.hero h1 { + font-size: clamp(38px, 4.1vw, 59px); + line-height: 1.25; + letter-spacing: -1.8px; + margin: 19px 0 21px; + font-weight: 600; +} +.hero-deck { + font-size: 17px; + line-height: 1.85; + color: var(--muted); +} +.hero-meta { + display: flex; + flex-wrap: wrap; + gap: 8px 20px; + margin: 25px 0 36px; + font-size: 11px; + color: var(--muted); +} +.hero-meta > a { + color: var(--green); +} +.hero-visual { + border-top: 1px solid var(--line); + border-bottom: 1px solid var(--line); + padding: 19px 0 14px; + margin-bottom: 35px; +} +.hero-axis { + display: flex; + justify-content: space-between; + margin: 0 100px 19px 70px; + font-size: 10px; + color: var(--muted); +} +.signal-row { + display: grid; + grid-template-columns: 54px 1fr 66px; + gap: 14px; + align-items: center; + font-size: 11px; + margin: 10px 0; +} +.signal-label { + font-size: 10px; + color: var(--muted); +} +.signal { + height: 28px; + background-size: 8px 100%; + border-radius: 3px; + position: relative; +} +.input-signal { + background-color: var(--blue-soft); + background-image: repeating-linear-gradient( + 90deg, + transparent 0 2px, + var(--blue) 2px 4px, + transparent 4px 8px + ); + clip-path: polygon( + 0 25%, + 2% 15%, + 4% 40%, + 6% 8%, + 8% 38%, + 10% 23%, + 12% 43%, + 14% 16%, + 16% 40%, + 18% 5%, + 20% 28%, + 22% 10%, + 24% 42%, + 26% 20%, + 28% 43%, + 30% 18%, + 32% 31%, + 34% 10%, + 36% 43%, + 38% 18%, + 40% 42%, + 42% 3%, + 44% 27%, + 46% 7%, + 48% 41%, + 50% 17%, + 52% 32%, + 54% 7%, + 56% 40%, + 58% 20%, + 60% 40%, + 62% 8%, + 64% 35%, + 66% 18%, + 68% 40%, + 70% 9%, + 72% 35%, + 74% 18%, + 76% 34%, + 78% 9%, + 80% 43%, + 82% 22%, + 84% 44%, + 86% 8%, + 88% 33%, + 90% 18%, + 92% 41%, + 94% 5%, + 96% 35%, + 98% 21%, + 100% 38%, + 100% 65%, + 98% 78%, + 96% 65%, + 94% 95%, + 92% 59%, + 90% 82%, + 88% 67%, + 86% 92%, + 84% 56%, + 82% 78%, + 80% 57%, + 78% 91%, + 76% 66%, + 74% 82%, + 72% 65%, + 70% 91%, + 68% 60%, + 66% 82%, + 64% 65%, + 62% 92%, + 60% 60%, + 58% 80%, + 56% 60%, + 54% 93%, + 52% 68%, + 50% 83%, + 48% 59%, + 46% 93%, + 44% 73%, + 42% 97%, + 40% 58%, + 38% 82%, + 36% 57%, + 34% 90%, + 32% 69%, + 30% 82%, + 28% 57%, + 26% 80%, + 24% 58%, + 22% 90%, + 20% 72%, + 18% 95%, + 16% 60%, + 14% 84%, + 12% 57%, + 10% 77%, + 8% 62%, + 6% 92%, + 4% 60%, + 2% 85%, + 0 75% + ); +} +.output-signal { + background-image: repeating-linear-gradient( + 90deg, + var(--green) 0 3px, + transparent 3px 7px + ); + height: 23px; + opacity: 0.88; + mask-image: linear-gradient( + 90deg, + transparent 0 14%, + black 14% 25%, + transparent 25% 50%, + black 50% 80%, + transparent 80% 85%, + black 85% + ); +} +.task-signal { + display: flex; + margin-left: 30%; + gap: 0; + font-size: 10px; + height: 26px; +} +.task-signal i { + font-style: normal; + background: var(--code); + border: 1px dashed var(--muted); + padding: 0 35px; + white-space: nowrap; +} +.task-signal b { + font-weight: 500; + background: var(--green-soft); + padding: 0 12px; + white-space: nowrap; +} +.visual-caption { + font-size: 10px; + color: var(--muted); + margin: 18px 0 0 68px; +} +.lead { + font-size: 19px; + line-height: 1.9; + margin-bottom: 18px; +} +.article-section { + border-top: 1px solid var(--line); + margin-top: 62px; + padding-top: 38px; + scroll-margin-top: 108px; +} +.section-number { + color: var(--green); + font-size: 10px; + letter-spacing: 1.8px; + margin-bottom: 12px; + font-weight: 600; +} +h2 { + font-size: 28px; + line-height: 1.5; + letter-spacing: -0.4px; + margin-bottom: 23px; +} +h3 { + font-size: 18px; + line-height: 1.6; + margin: 29px 0 12px; +} +main p { + font-size: 15px; + line-height: 1.95; +} +.citation { + font-size: 11px; + color: var(--green); + white-space: nowrap; + margin-left: 5px; +} +.terminology { + color: var(--muted); +} +main .terminology { + font-size: 12px; +} +.interactive-figure { + margin: 27px 0 25px; + background: var(--surface); + border: 1px solid var(--line); + padding: 25px 28px; + border-radius: 6px; +} +.figure-heading { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 15px; +} +.figure-label { + font-size: 10px; + letter-spacing: 1px; + color: var(--muted); +} +.figure-heading h3 { + font-size: 20px; + margin: 4px 0 20px; +} +.evidence-tag { + border: 1px solid var(--line); + font-size: 10px; + line-height: 1.5; + padding: 4px 7px; + white-space: nowrap; + border-radius: 3px; +} +.simulated { + background: var(--amber-soft); + color: var(--amber); + border-color: transparent; +} +.reported { + background: var(--blue-soft); + color: var(--blue); + border-color: transparent; +} +.inference { + background: var(--code); + color: var(--muted); + border-color: transparent; +} +.segmented { + display: inline-flex; + flex-wrap: wrap; + border: 1px solid var(--line); + border-radius: 4px; + overflow: hidden; + margin-bottom: 28px; +} +.segmented button { + border: 0; + background: transparent; + padding: 7px 20px; + font-size: 12px; +} +.segmented button[aria-pressed="true"] { + background: var(--ink); + color: var(--paper); +} +.architecture-flow { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + min-height: 88px; +} +.flow-node { + padding: 11px 9px; + border: 1px solid var(--line); + border-radius: 4px; + text-align: center; + flex: 1; + min-width: 0; +} +.flow-node strong { + display: block; + font-size: 12px; +} +.flow-node small { + font-size: 10px; + color: var(--muted); + display: block; + margin-top: 3px; +} +.flow-node.input { + background: var(--blue-soft); + border-color: transparent; +} +.flow-node.output { + background: var(--green-soft); + border-color: transparent; +} +.flow-arrow { + font-size: 17px; + color: var(--muted); +} +.architecture-detail { + border-top: 1px solid var(--line); + margin-top: 24px; + padding-top: 18px; + min-height: 105px; +} +.architecture-detail p { + font-size: 13px; +} +.architecture-detail strong { + color: var(--green); +} +.three-concepts { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 24px; + margin: 30px 0; +} +.three-concepts h3 { + margin: 0 0 8px; + font-size: 15px; +} +.three-concepts p { + font-size: 12px; + line-height: 1.8; + color: var(--muted); +} +.table-wrap { + overflow-x: auto; + margin: 24px 0 22px; + border-top: 1px solid var(--line); + border-bottom: 1px solid var(--line); + -webkit-overflow-scrolling: touch; +} +.table-wrap table { + border-collapse: collapse; + width: 100%; + font-size: 12px; + line-height: 1.85; + min-width: 580px; +} +.table-wrap th, +.table-wrap td { + text-align: left; + vertical-align: top; + padding: 13px 12px; + border-bottom: 1px solid var(--line); +} +.table-wrap th:first-child, +.table-wrap td:first-child { + padding-left: 0; +} +.table-wrap th:last-child, +.table-wrap td:last-child { + padding-right: 0; +} +.table-wrap th { + font-weight: 600; +} +.table-wrap thead th { + color: var(--muted); + font-size: 11px; + white-space: nowrap; +} +.table-wrap tbody tr:last-child > * { + border-bottom: 0; +} +.table-wrap tbody th { + width: 18%; + min-width: 85px; +} +.table-wrap a { + color: var(--green); + white-space: nowrap; + font-size: 11px; +} +.table-wrap caption { + text-align: left; + color: var(--muted); + font-size: 11px; + padding: 12px 0; +} +.reading-note { + border-left: 2px solid var(--green); + padding: 10px 0 10px 18px; + color: var(--muted); +} +main .reading-note { + font-size: 13px; +} +.time-vocabulary { + display: grid; + grid-template-columns: repeat(4, 1fr); + margin: 28px 0; + gap: 18px; +} +.time-vocabulary > div { + border-top: 2px solid var(--line); + padding-top: 12px; +} +.time-vocabulary span { + font-size: 13px; + font-weight: 600; +} +.time-vocabulary p { + font-size: 11px; + color: var(--muted); + line-height: 1.8; + margin-top: 7px; +} +.figure-intro { + color: var(--muted); +} +main .figure-intro { + font-size: 11px; + line-height: 1.85; +} +.lab-controls { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 30px; + margin: 23px 0 13px; +} +.lab-controls label { + font-size: 12px; + font-weight: 500; +} +.lab-controls output { + float: right; + font-variant-numeric: tabular-nums; + font-weight: 600; + color: var(--green); +} +input[type="range"] { + display: block; + width: 100%; + margin: 13px 0; + accent-color: var(--green); +} +.lab-presets { + display: flex; + flex-wrap: wrap; + gap: 8px; +} +.lab-presets button { + background: transparent; + border: 1px solid var(--line); + font-size: 10px; + padding: 4px 10px; + border-radius: 3px; +} +.lab-presets button:hover { + background: var(--code); +} +.lab-stats { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 12px; + margin: 25px 0; +} +.lab-stats > div { + border-top: 1px solid var(--line); + padding-top: 11px; +} +.lab-stats span { + display: block; + font-size: 10px; + color: var(--muted); +} +.lab-stats strong { + display: block; + font-size: 23px; + font-weight: 500; + font-variant-numeric: tabular-nums; + line-height: 1.6; + margin-top: 3px; +} +.lab-stats .overloaded { + color: var(--amber); +} +.queue-chart { + height: 246px; + width: 100%; + position: relative; + padding: 25px 0 10px; +} +.queue-row { + position: relative; + height: 24px; + margin-left: 45px; + border-bottom: 1px solid var(--line); +} +.queue-label { + position: absolute; + right: calc(100% + 10px); + font-size: 9px; + color: var(--muted); + top: 1px; + white-space: nowrap; +} +.queue-arrival { + position: absolute; + width: 5px; + height: 13px; + top: 4px; + background: var(--blue); + transform: translateX(-2px); + z-index: 2; +} +.queue-compute, +.queue-wait { + height: 10px; + top: 5px; + position: absolute; + border-radius: 2px; +} +.queue-compute { + background: var(--green); + opacity: 0.82; +} +.queue-wait { + background: var(--amber); + opacity: 0.5; +} +.queue-ticks { + position: absolute; + left: 45px; + right: 0; + top: 0; + display: flex; + justify-content: space-between; + font-size: 9px; + color: var(--muted); + font-variant-numeric: tabular-nums; +} +.chart-legend { + display: flex; + flex-wrap: wrap; + gap: 18px; + color: var(--muted); + font-size: 10px; + margin: 10px 0 17px; +} +.chart-legend span { + display: flex; + align-items: center; + gap: 6px; +} +.chart-legend i { + width: 14px; + height: 6px; + display: inline-block; + border-radius: 1px; +} +.legend-input { + background: var(--blue); +} +.legend-compute { + background: var(--green); +} +.legend-wait { + background: var(--amber); + opacity: 0.55; +} +main .selected-detail { + font-size: 12px; + min-height: 46px; +} +main .formula { + font-family: Georgia, serif; + font-size: 16px; + line-height: 1.7; + color: var(--muted); + margin-top: 10px; +} +.source-line { + color: var(--muted); +} +main .source-line { + font-size: 11px; +} +.source-line a { + color: var(--green); +} +.latency-path { + display: flex; + align-items: center; + gap: 9px; + margin: 23px 0 20px; +} +.latency-path span { + background: var(--code); + padding: 10px 12px; + font-size: 11px; + flex: 1; + text-align: center; + min-height: 60px; + display: flex; + align-items: center; + justify-content: center; +} +.latency-path b { + font-weight: 400; + color: var(--muted); +} +.benchmark-chart { + margin: 9px 0 22px; +} +.benchmark-row { + display: grid; + grid-template-columns: 120px 1fr; + gap: 15px; + align-items: center; + margin: 15px 0; +} +.benchmark-label { + font-size: 11px; +} +.benchmark-tracks { + position: relative; + height: 48px; + padding-right: 45px; +} +.benchmark-bar { + height: 11px; + position: relative; + min-width: 2px; + border-radius: 2px; + margin: 7px 0; +} +.benchmark-bar.first { + background: var(--blue); +} +.benchmark-bar.answer { + background: var(--green); +} +.benchmark-value { + position: absolute; + left: calc(100% + 7px); + font-size: 9px; + white-space: nowrap; + line-height: 1; + top: 1px; + font-variant-numeric: tabular-nums; +} +.benchmark-axis { + display: flex; + margin-left: 135px; + margin-right: 45px; + justify-content: space-between; + font-size: 9px; + color: var(--muted); +} +.benchmark-table td { + white-space: nowrap; + font-variant-numeric: tabular-nums; +} +.argument-chain { + display: flex; + gap: 20px; + margin: 28px 0; +} +.argument-chain > div { + flex: 1; + padding: 12px 0; + border-top: 1px solid var(--green); +} +.argument-chain span { + font-size: 10px; + color: var(--green); + display: block; +} +.argument-chain strong { + font-size: 12px; + font-weight: 500; + line-height: 1.8; + display: block; + margin-top: 8px; +} +.design-routes { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 26px 30px; + margin: 27px 0; +} +.design-routes > div { + border-top: 1px solid var(--line); + padding-top: 13px; +} +.design-routes span { + font-size: 10px; + color: var(--green); +} +.design-routes h3 { + font-size: 17px; + margin: 7px 0 10px; +} +.design-routes p { + font-size: 12px; + color: var(--muted); +} +.training-steps { + list-style: none; + padding: 0; + margin: 30px 0; +} +.training-steps li { + display: flex; + gap: 23px; + padding: 19px 0; + border-top: 1px solid var(--line); +} +.training-steps li > span { + font-family: Georgia, serif; + color: var(--green); + font-size: 22px; + min-width: 32px; +} +.training-steps h3 { + font-size: 16px; + margin: 0 0 8px; +} +.training-steps p { + font-size: 13px; +} +.evaluation-equation { + margin: 25px 0; + padding: 21px 0; + border-top: 1px solid var(--green); + border-bottom: 1px solid var(--line); +} +.evaluation-equation span { + display: block; + color: var(--muted); + font-size: 11px; + margin: 0 0 6px; +} +.evaluation-equation strong { + font-size: 16px; + font-weight: 500; + display: block; +} +.evaluation-equation strong + span { + margin-top: 18px; +} +.checklist { + padding-left: 20px; + margin: 20px 0; + font-size: 14px; +} +.checklist li { + padding: 5px 0 5px 6px; +} +.checklist li::marker { + color: var(--green); +} +.scenario-list { + margin: 23px 0; +} +.scenario-list details { + border-top: 1px solid var(--line); + padding: 15px 0; +} +.scenario-list details:last-child { + border-bottom: 1px solid var(--line); +} +.scenario-list summary { + cursor: pointer; + font-size: 14px; + font-weight: 500; + list-style: none; + position: relative; + padding-right: 25px; +} +.scenario-list summary::-webkit-details-marker { + display: none; +} +.scenario-list summary::after { + content: "+"; + position: absolute; + right: 3px; + color: var(--muted); +} +.scenario-list details[open] summary::after { + content: "−"; +} +.scenario-list summary span { + font-size: 10px; + color: var(--green); + margin-right: 20px; + font-variant-numeric: tabular-nums; +} +.scenario-list p { + margin: 10px 30px 0 36px; + font-size: 13px; + color: var(--muted); +} +.claim-template { + background: var(--green-soft); + margin-top: 25px; + padding: 20px 23px; +} +.claim-template p { + font-size: 14px; + margin-top: 10px; +} +.source-list { + padding-left: 25px; + margin: 28px 0 0; +} +.source-list li { + padding: 12px 0 12px 7px; + border-top: 1px solid var(--line); + scroll-margin-top: 110px; +} +.source-list li::marker { + font-size: 10px; + color: var(--green); +} +.source-list a { + font-size: 13px; + font-weight: 500; +} +.source-list span { + display: block; + color: var(--muted); + font-size: 11px; + margin-top: 4px; + line-height: 1.8; +} +.article-footer { + margin: 60px 0 0; + padding: 26px 0 34px; + border-top: 1px solid var(--line); + font-size: 10px; + color: var(--muted); + display: flex; + gap: 15px; + justify-content: space-between; + flex-wrap: wrap; +} +.article-footer > div { + display: flex; + gap: 20px; +} +.article-footer small { + font-size: 9px; + flex-basis: 100%; + letter-spacing: 0.4px; +} +@media (min-width: 1400px) { + .page-layout { + gap: 85px; + grid-template-columns: 190px minmax(0, 910px); + } +} +@media (max-width: 1050px) { + .page-layout { + grid-template-columns: 155px minmax(0, 1fr); + gap: 32px; + padding: 0 30px; + } + .contents nav { + font-size: 11px; + } + .interactive-figure { + padding: 22px; + } + .flow-node small { + font-size: 9px; + } + .flow-node strong { + font-size: 11px; + } + .hero h1 { + font-size: 46px; + } + .time-vocabulary { + gap: 12px; + } + .figure-heading h3 { + font-size: 18px; + } +} +@media (max-width: 780px) { + .site-header { + height: 68px; + padding: 0 23px; + } + .site-header nav { + gap: 18px; + } + .site-header nav > a:first-child { + display: none; + } + .brand { + font-size: 10px; + gap: 9px; + } + .brand b { + margin-left: 3px; + } + .brand-mark { + font-size: 32px; + } + .page-layout { + display: block; + padding: 0 24px; + } + .contents { + position: static; + height: auto; + max-height: none; + padding-top: 22px; + } + .contents > .eyebrow, + .contents-note { + display: none; + } + .contents nav { + margin: 0; + flex-direction: row; + flex-wrap: wrap; + gap: 4px 16px; + padding-bottom: 16px; + border-bottom: 1px solid var(--line); + } + .contents nav a { + border-left: 0; + padding: 3px 0; + font-size: 10px; + } + .contents nav a:nth-child(n + 5) { + display: none; + } + .hero { + padding-top: 35px; + } + .hero h1 { + font-size: 42px; + letter-spacing: -1.3px; + } + .hero-deck { + font-size: 15px; + } + .hero-meta { + margin-top: 21px; + font-size: 10px; + } + .article-section { + margin-top: 48px; + padding-top: 29px; + scroll-margin-top: 90px; + } + h2 { + font-size: 25px; + } + .interactive-figure { + padding: 21px; + } + .hero-axis { + margin-right: 70px; + } + .signal-row { + grid-template-columns: 35px 1fr 52px; + gap: 8px; + } + .signal-label { + font-size: 9px; + } + .visual-caption { + margin-left: 43px; + } + .task-signal { + margin-left: 20%; + } + .task-signal i { + padding: 0 18px; + } + .architecture-flow { + gap: 5px; + } + .flow-node { + padding: 10px 5px; + } + .flow-node strong { + font-size: 10px; + } + .flow-node small { + font-size: 9px; + } + .flow-arrow { + font-size: 13px; + } + .argument-chain { + gap: 14px; + } + .design-routes { + gap: 23px; + } + .benchmark-row { + grid-template-columns: 105px 1fr; + gap: 10px; + } + .benchmark-label { + font-size: 10px; + } + .benchmark-axis { + margin-left: 115px; + } + .article-footer { + font-size: 9px; + } + .table-wrap table { + font-size: 11px; + } + .table-wrap th, + .table-wrap td { + padding: 12px 9px; + } + main p { + font-size: 14px; + } +} +@media (max-width: 480px) { + body { + line-height: 1.8; + } + .site-header { + padding: 0 18px; + } + .brand b { + display: none; + } + .site-header nav { + font-size: 11px; + gap: 15px; + } + .page-layout { + padding: 0 19px; + } + .hero { + padding-top: 29px; + } + .hero > .eyebrow { + font-size: 8px; + letter-spacing: 1px; + } + .hero h1 { + font-size: 35px; + line-height: 1.3; + margin-top: 15px; + } + .hero-deck { + font-size: 14px; + } + .hero-meta { + gap: 4px 13px; + margin-bottom: 25px; + } + .hero-visual { + margin-bottom: 25px; + } + .hero-axis { + margin: 0 50px 14px 42px; + font-size: 9px; + } + .lead { + font-size: 17px; + } + .three-concepts { + grid-template-columns: 1fr; + gap: 18px; + margin: 25px 0; + } + .three-concepts > div { + display: grid; + grid-template-columns: 95px 1fr; + gap: 10px; + } + .three-concepts h3 { + font-size: 13px; + margin: 0; + } + .three-concepts p { + font-size: 11px; + } + .interactive-figure { + padding: 17px 14px; + margin: 23px -3px; + } + .figure-heading { + gap: 8px; + } + .figure-heading h3 { + font-size: 17px; + margin-bottom: 18px; + } + .figure-label { + font-size: 9px; + } + .evidence-tag { + font-size: 8px; + padding: 4px 5px; + } + .segmented { + margin-bottom: 20px; + width: 100%; + } + .segmented button { + padding: 7px 8px; + flex: 1; + font-size: 11px; + } + .architecture-flow { + flex-wrap: wrap; + justify-content: center; + gap: 5px 4px; + } + .flow-node { + flex: 0 0 26%; + padding: 9px 3px; + } + .flow-node strong { + font-size: 10px; + } + .flow-node small { + font-size: 8px; + } + .architecture-flow .flow-arrow { + font-size: 12px; + } + .architecture-detail { + margin-top: 20px; + min-height: 150px; + } + .architecture-detail p { + font-size: 12px; + } + .time-vocabulary { + grid-template-columns: 1fr 1fr; + gap: 16px 20px; + } + .time-vocabulary span { + font-size: 12px; + } + .time-vocabulary p { + font-size: 11px; + } + .lab-controls { + grid-template-columns: 1fr; + gap: 12px; + margin-top: 20px; + } + .lab-controls label { + font-size: 11px; + } + .lab-stats { + gap: 7px; + } + .lab-stats span { + font-size: 9px; + min-height: 30px; + } + .lab-stats strong { + font-size: 20px; + } + .queue-chart { + height: 242px; + } + .queue-row { + margin-left: 32px; + } + .queue-ticks { + left: 32px; + font-size: 8px; + } + .queue-label { + font-size: 8px; + right: calc(100% + 8px); + } + .chart-legend { + gap: 10px; + font-size: 9px; + } + .latency-path { + flex-wrap: wrap; + gap: 6px; + } + .latency-path span { + flex: 0 0 calc(50% - 13px); + font-size: 10px; + min-height: 58px; + } + .latency-path b:nth-child(4) { + visibility: hidden; + width: 0; + } + .latency-path b { + font-size: 12px; + } + .benchmark-row { + grid-template-columns: 81px 1fr; + gap: 8px; + } + .benchmark-label { + font-size: 9px; + } + .benchmark-value { + font-size: 8px; + left: calc(100% + 5px); + } + .benchmark-tracks { + padding-right: 36px; + } + .benchmark-axis { + margin-left: 89px; + margin-right: 36px; + font-size: 8px; + } + .argument-chain { + flex-direction: column; + gap: 8px; + } + .argument-chain > div { + display: grid; + grid-template-columns: 40px 1fr; + gap: 12px; + align-items: center; + padding: 10px 0; + } + .argument-chain strong { + margin: 0; + font-size: 11px; + } + .design-routes { + grid-template-columns: 1fr; + gap: 17px; + } + .design-routes h3 { + font-size: 16px; + } + .design-routes p { + font-size: 12px; + } + .training-steps li { + gap: 16px; + } + .training-steps li > span { + font-size: 20px; + min-width: 25px; + } + .training-steps h3 { + font-size: 15px; + } + .training-steps p { + font-size: 12px; + } + .evaluation-equation strong { + font-size: 14px; + } + .checklist { + font-size: 13px; + padding-left: 16px; + } + .scenario-list summary { + font-size: 13px; + } + .scenario-list p { + margin-left: 28px; + font-size: 12px; + } + .scenario-list summary span { + margin-right: 13px; + } + .claim-template { + padding: 17px; + } + .claim-template p { + font-size: 13px; + } + h2 { + font-size: 23px; + } + h3 { + font-size: 17px; + } + .source-list a { + font-size: 12px; + } + .source-list span { + font-size: 10px; + } + .article-footer { + display: block; + } + .article-footer > div { + margin: 15px 0; + gap: 18px; + } + .article-footer small { + display: block; + } +} +@media (prefers-reduced-motion: reduce) { + html { + scroll-behavior: auto; + } + *, + *::before, + *::after { + transition: none !important; + animation: none !important; + } +} +@media print { + .site-header, + .contents, + .lab-controls, + .lab-presets, + #theme-toggle { + display: none; + } + .page-layout { + display: block; + padding: 0; + } + .article-section { + break-inside: auto; + } + .interactive-figure, + .training-steps li { + break-inside: avoid; + } + body { + background: #fff; + color: #111; + } + .table-wrap { + overflow: visible; + } + .table-wrap table { + min-width: 0; + } + a { + text-decoration: underline; + } +} diff --git a/validate.mjs b/validate.mjs new file mode 100644 index 0000000..f0d4a83 --- /dev/null +++ b/validate.mjs @@ -0,0 +1,24 @@ +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(new Set(ids).size, ids.length, "HTML IDs must be unique"); +for (const match of html.matchAll(/href="#([^"]+)"/g)) { + assert.ok(ids.includes(match[1]), `Missing anchor ${match[1]}`); +} +for (const pattern of [ + /\/Users\//, + /git\.n\.xiaomi/, + /100\.94\./, + /human-authorization/, + /Bearer\s+[A-Za-z0-9]/, +]) { + assert.ok(!pattern.test(html), `Private content matched ${pattern}`); +} +assert.ok(html.includes("说明性模型")); +assert.ok(html.includes("来源报告值")); +assert.ok(html.includes("2026 年 11 月 9 日")); +console.log( + `Validated ${ids.length} unique anchors, public-content boundaries and evidence labels.`, +);