Publish voice duplex architecture latency training and evaluation research
This commit is contained in:
@@ -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();
|
||||
Reference in New Issue
Block a user