Publish voice duplex architecture latency training and evaluation research

This commit is contained in:
wuyang6
2026-11-09 10:56:46 +08:00
commit a2e3ceb4ca
17 changed files with 3371 additions and 0 deletions
+194
View File
@@ -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();