update at 2026-06-04 14:09:16

This commit is contained in:
陈赣
2026-06-04 14:09:16 +08:00
parent 41bd03123c
commit 4603914e85
11 changed files with 692 additions and 68 deletions

View File

@@ -13,7 +13,10 @@ const timingFrameEl = document.querySelector("#timing-frame");
let selectedDevice = "";
let pendingDevice = "";
let queuedDevice = "";
let switching = false;
let devicesSignature = "";
let lastWsSignature = "";
function setConnection(online, text) {
connection.textContent = text;
@@ -84,16 +87,54 @@ function renderDetections(detections) {
.join("");
}
async function switchDevice(deviceNum) {
async function performSwitch(deviceNum) {
switching = true;
pendingDevice = deviceNum;
devicesSignature = "";
setConnection(false, "切换中");
const response = await fetch(`/devices/${encodeURIComponent(deviceNum)}`, { method: "POST" });
if (!response.ok) {
throw new Error("切换摄像头失败");
console.log("[device-switch] start", { deviceNum });
try {
const response = await fetch(`/devices/${encodeURIComponent(deviceNum)}`, { method: "POST" });
if (!response.ok) {
throw new Error("切换摄像头失败");
}
const result = await response.json();
const video = document.querySelector("#video");
if (video) {
video.src = `/video?t=${Date.now()}`;
}
document.querySelectorAll(".grid-video").forEach((item) => {
item.src = `${item.dataset.src}?t=${Date.now()}`;
});
console.log("[device-switch] requested", { deviceNum, version: result.version });
} catch (error) {
pendingDevice = "";
devicesSignature = "";
setConnection(false, "切换失败");
console.error("[device-switch] failed", { deviceNum, error });
} finally {
switching = false;
if (queuedDevice && queuedDevice !== deviceNum) {
const nextDevice = queuedDevice;
queuedDevice = "";
return performSwitch(nextDevice);
}
queuedDevice = "";
}
const video = document.querySelector("#video");
video.src = `/video?t=${Date.now()}`;
}
function switchDevice(deviceNum) {
if (switching) {
queuedDevice = deviceNum;
pendingDevice = deviceNum;
devicesSignature = "";
setConnection(false, "等待切换");
console.log("[device-switch] queued", { deviceNum });
return Promise.resolve();
}
return performSwitch(deviceNum);
}
function connectWebSocket() {
@@ -109,6 +150,18 @@ function connectWebSocket() {
errorEl.textContent = data.error || (data.connected ? "正常" : "未连接");
sourceEl.textContent = data.source || "-";
setConnection(Boolean(data.connected), data.connected ? "已连接" : "重连中");
const wsSignature = `${data.current_device_num}|${data.connected}|${data.frame_id}|${data.error || ""}`;
if (wsSignature !== lastWsSignature) {
lastWsSignature = wsSignature;
console.log("[device-switch] ws", {
currentDeviceNum: data.current_device_num,
pendingDevice,
queuedDevice,
connected: data.connected,
frameId: data.frame_id,
error: data.error,
});
}
if (pendingDevice && data.current_device_num === pendingDevice) {
pendingDevice = "";
deviceSelect.disabled = false;

View File

@@ -73,6 +73,30 @@ p {
color: var(--red);
}
.topbar-actions {
display: flex;
align-items: center;
gap: 12px;
}
.button-link {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 36px;
padding: 8px 14px;
border: 1px solid var(--line);
border-radius: 999px;
color: var(--text);
text-decoration: none;
background: var(--panel);
}
.button-link:hover {
border-color: var(--green);
color: var(--green);
}
.layout {
display: grid;
grid-template-columns: minmax(0, 1fr) 360px;
@@ -92,33 +116,6 @@ p {
overflow: hidden;
}
.pipeline {
display: flex;
align-items: center;
gap: 10px;
padding: 14px;
border-bottom: 1px solid var(--line);
overflow-x: auto;
}
.stage {
flex: 0 0 auto;
padding: 9px 12px;
border: 1px solid var(--line);
border-radius: 10px;
color: var(--muted);
background: var(--panel-2);
}
.stage.active {
border-color: rgba(46, 232, 135, 0.5);
color: var(--green);
}
.arrow {
color: var(--muted);
}
.video-wrap {
display: grid;
place-items: center;
@@ -126,7 +123,8 @@ p {
background: #05070b;
}
#video {
#video,
.grid-video {
display: block;
width: 100%;
height: auto;
@@ -134,6 +132,36 @@ p {
object-fit: contain;
}
.video-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
padding: 14px;
background: #05070b;
}
.video-grid-item {
overflow: hidden;
border: 1px solid var(--line);
border-radius: 14px;
background: var(--panel-2);
}
.video-grid-title {
padding: 10px 12px;
border-bottom: 1px solid var(--line);
color: var(--muted);
font-size: 13px;
}
.video-grid-wrap {
min-height: 240px;
}
.grid-video {
max-height: calc((100vh - 260px) / 2);
}
.side-card {
display: flex;
flex-direction: column;
@@ -166,13 +194,19 @@ p {
background: var(--panel-2);
}
.detections-panel {
padding: 16px;
border-top: 1px solid var(--line);
}
.detections {
display: flex;
flex-direction: column;
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 10px;
}
.detections.empty {
display: block;
color: var(--muted);
}
@@ -197,6 +231,133 @@ p {
font-size: 12px;
}
.tokenizer-page .tokenizer-layout {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 2fr);
grid-template-areas: "flow side";
align-items: start;
width: 100%;
gap: 18px;
padding: 18px;
}
.tokenizer-page .tokenizer-side {
display: grid;
grid-area: side;
min-width: 0;
gap: 18px;
}
.tokenizer-page .tokenizer-flow-card {
grid-area: flow;
min-width: 0;
min-height: calc(100vh - 122px);
}
.tokenizer-page .tokenizer-side .detections {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.tokenizer-card {
border: 1px solid var(--line);
border-radius: 18px;
padding: 18px;
background: rgba(21, 27, 38, 0.9);
box-shadow: 0 18px 40px rgba(0, 0, 0, 0.28);
}
.pipeline-steps {
display: grid;
gap: 10px;
}
.pipeline-step {
display: grid;
grid-template-columns: 34px minmax(0, 1fr);
gap: 10px;
align-items: start;
padding: 12px;
border: 1px solid var(--line);
border-radius: 12px;
background: var(--panel-2);
}
.step-index {
display: grid;
place-items: center;
width: 28px;
height: 28px;
border-radius: 999px;
color: #06100b;
font-weight: 700;
background: var(--green);
}
.step-title {
margin-bottom: 5px;
font-weight: 700;
}
.step-value,
.token-summary,
.selected-token {
color: var(--muted);
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 13px;
word-break: break-all;
}
.token-sequence {
display: grid;
grid-template-columns: repeat(12, minmax(0, 1fr));
gap: 8px;
margin-top: 14px;
}
.token-cell {
min-height: 50px;
border: 1px solid var(--line);
border-radius: 10px;
color: var(--text);
cursor: pointer;
background: var(--panel-2);
}
.token-cell span,
.token-cell small {
display: block;
}
.token-cell small {
margin-top: 3px;
color: var(--muted);
}
.token-cell.selected,
.token-cell:hover {
border-color: var(--green);
color: var(--green);
}
.token-detail-title {
margin-bottom: 10px;
color: var(--green);
font-weight: 700;
}
.token-vector {
padding: 12px;
border: 1px solid var(--line);
border-radius: 12px;
background: var(--panel-2);
}
@media (max-width: 1280px) {
.detections {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
}
@media (max-width: 980px) {
.layout {
grid-template-columns: 1fr;
@@ -206,4 +367,18 @@ p {
align-items: flex-start;
flex-direction: column;
}
.detections {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
@media (max-width: 640px) {
.video-grid {
grid-template-columns: 1fr;
}
.detections {
grid-template-columns: 1fr;
}
}

131
app/static/tokenizer.js Normal file
View File

@@ -0,0 +1,131 @@
const statusEl = document.querySelector("#tokenizer-status");
const pipelineEl = document.querySelector("#pipeline-steps");
const tokenSummaryEl = document.querySelector("#token-summary");
const tokenSequenceEl = document.querySelector("#token-sequence");
const selectedTokenEl = document.querySelector("#selected-token");
const detectionsEl = document.querySelector("#tokenizer-detections");
let selectedTokenIndex = null;
function formatShape(shape) {
if (!shape || !shape.length) {
return "-";
}
return `[${shape.join(", ")}]`;
}
function setStatus(ready, text) {
statusEl.textContent = text;
statusEl.classList.toggle("online", ready);
statusEl.classList.toggle("offline", !ready);
}
function renderPipeline(data) {
const steps = [
["OpenCV RGB 帧", `${data.image_size?.width ?? "-"} × ${data.image_size?.height ?? "-"}`],
["PIL Image", `${data.image_size?.width ?? "-"} × ${data.image_size?.height ?? "-"}`],
["DetrImageProcessor", `pixel_values ${formatShape(data.pixel_values_shape)} / pixel_mask ${formatShape(data.pixel_mask_shape)}`],
["ResNet-50 backbone", `feature map ${formatShape(data.feature_map_shape)}`],
["1×1 convolution", `projected ${formatShape(data.projected_feature_map_shape)}`],
["视觉 token embedding", `由 projected feature map flatten 得到 ${formatShape(data.visual_tokens_shape)}`],
["位置 embedding", `二维位置 embedding ${formatShape(data.position_encoding_shape)}`],
["Transformer Encoder", formatShape(data.encoder_last_hidden_state_shape)],
["Object query embedding + Decoder", `object query embedding 解码后 ${formatShape(data.decoder_last_hidden_state_shape)}`],
["类别 logits + boxes", `logits ${formatShape(data.logits_shape)} / boxes ${formatShape(data.pred_boxes_shape)}`],
["post_process_object_detection", `检测结果 ${data.detections?.length ?? 0}`],
];
pipelineEl.innerHTML = steps
.map(([title, value], index) => `
<div class="pipeline-step">
<div class="step-index">${index + 1}</div>
<div>
<div class="step-title">${title}</div>
<div class="step-value">${value}</div>
</div>
</div>
`)
.join("");
}
function renderTokens(data) {
const grid = data.token_grid || {};
tokenSummaryEl.textContent = `帧号 ${data.frame_id ?? "-"} · token 网格 ${grid.rows ?? "-"} × ${grid.cols ?? "-"},总数 ${grid.total ?? "-"},展示前 ${grid.shown ?? 0} 个 token每个显示前 8 维采样。`;
tokenSequenceEl.innerHTML = (data.token_sequence || [])
.map((token) => `
<button class="token-cell ${token.index === selectedTokenIndex ? "selected" : ""}" data-index="${token.index}">
<span>#${token.index}</span>
<small>(${token.row}, ${token.col})</small>
</button>
`)
.join("");
tokenSequenceEl.querySelectorAll(".token-cell").forEach((button) => {
button.addEventListener("click", () => {
selectedTokenIndex = Number(button.dataset.index);
renderSelectedToken(data);
renderTokens(data);
});
});
renderSelectedToken(data);
}
function renderSelectedToken(data) {
const tokens = data.token_sequence || [];
const token = tokens.find((item) => item.index === selectedTokenIndex) || tokens[0];
if (!token) {
selectedTokenEl.textContent = "暂无 token。";
return;
}
selectedTokenIndex = token.index;
selectedTokenEl.innerHTML = `
<div class="token-detail-title">Token #${token.index} · 网格位置 (${token.row}, ${token.col}) · L2 ${token.magnitude}</div>
<div class="token-vector">[${token.values.map((value) => Number(value).toFixed(4)).join(", ")}, ...]</div>
`;
}
function renderDetections(detections) {
if (!detections.length) {
detectionsEl.className = "detections empty";
detectionsEl.textContent = "暂无目标";
return;
}
detectionsEl.className = "detections";
detectionsEl.innerHTML = detections
.map((det) => `
<div class="det-item">
<div class="det-title">
<span>${det.label}</span>
<span>${(det.score * 100).toFixed(1)}%</span>
</div>
<div class="det-box">box: [${det.box.join(", ")}]</div>
</div>
`)
.join("");
}
async function refreshTokenizer() {
try {
const response = await fetch(`/tokenizer/state?t=${Date.now()}`);
const data = await response.json();
if (!data.ready) {
setStatus(false, data.error || "等待帧");
tokenSummaryEl.textContent = data.error || "等待视频帧";
return;
}
setStatus(Boolean(data.connected), data.connected ? "动态更新中" : "未连接");
renderPipeline(data);
renderTokens(data);
renderDetections(data.detections || []);
} catch (error) {
setStatus(false, "更新失败");
tokenSummaryEl.textContent = `更新失败:${error}`;
} finally {
setTimeout(refreshTokenizer, 1200);
}
}
refreshTokenizer();