[diffusion] misc: add realtime-webui (#26959)

This commit is contained in:
Mick
2026-06-02 14:13:02 +08:00
committed by GitHub
parent 2fc548f250
commit 3b26644bc4
8 changed files with 2480 additions and 1 deletions
+2 -1
View File
@@ -177,7 +177,8 @@ killall_sglang = "sglang.cli.killall:main"
[tool.setuptools.package-data]
"sglang" = [
"srt/**/*",
"jit_kernel/**/*"
"jit_kernel/**/*",
"multimodal_gen/apps/realtime_webui/**/*"
]
[tool.setuptools.packages.find]
@@ -0,0 +1,24 @@
# SGLang Diffusion Realtime WebUI
Standalone browser demo for `/v1/realtime_video/generate`.
Open `index.html` directly in a browser, point it at an SGLang Diffusion server,
and generate. The app sends msgpack init / event messages and renders lossless
raw RGB frame batches on a canvas.
The first version is intentionally static: no npm install, no build step, and no
server-side dependencies. Presets are UI-side templates for prompt, LingBot
example images, album artwork references, and session parameters. The default
preset preloads a reference image so the demo can be tested without a file
upload.
By default, `Continuous session` is enabled for long-running camera control.
Keyboard and pointer controls send state transitions instead of scripted preset
actions. The telemetry `Chunk wait` measures request-to-chunk arrival time, not
client-side RGB decode time. Continuous playback adapts to the measured chunk
production rate so the canvas does not play a chunk at target FPS and then sit
on the last frame while waiting for the next chunk.
The interface shape follows camera-control-first video playgrounds such as
Reactor LingBot: reference image, scene prompt, enhancement, clip controls,
move/look camera controls, recordings history, and model telemetry.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,162 @@
const RAW_RGB_CONTENT_TYPE = "application/x-raw-rgb";
const RAW_RGB_DELTA_GZIP_CONTENT_TYPE = "application/x-raw-rgb-delta-gzip";
const RAW_RGBA_DELTA_GZIP_CONTENT_TYPE = "application/x-raw-rgba-delta-gzip";
const WEBP_FRAME_CONTENT_TYPE = "image/webp";
const JPEG_FRAME_CONTENT_TYPE = "image/jpeg";
let lastFrame = null;
function reset() {
lastFrame = null;
}
async function gunzipBytes(payload) {
if (typeof DecompressionStream === "undefined") {
throw new Error("This browser does not support gzip stream decoding");
}
const stream = new Blob([payload]).stream().pipeThrough(new DecompressionStream("gzip"));
return new Uint8Array(await new Response(stream).arrayBuffer());
}
async function restoreDeltaGzipFrames(header, payload) {
const frameBytes = Number(header.bytes_per_frame);
const count = Number(header.num_frames);
const expectedSize = frameBytes * count;
const restored = await gunzipBytes(payload);
if (restored.length !== expectedSize) {
throw new Error(`delta payload size mismatch: expected ${expectedSize}, got ${restored.length}`);
}
let previous = header.delta_reference === "previous-frame" ? lastFrame : null;
if (header.delta_reference === "previous-frame") {
if (!previous) throw new Error("Missing previous frame for delta payload");
if (previous.byteLength !== frameBytes) {
throw new Error("Previous frame size does not match current delta payload");
}
}
for (let f = 0; f < count; f++) {
const offset = f * frameBytes;
if (previous) {
for (let i = 0; i < frameBytes; i++) restored[offset + i] ^= previous[i];
}
previous = restored.slice(offset, offset + frameBytes);
}
lastFrame = previous;
return restored;
}
function rawFramesToRgbaBuffers(header, payload) {
const width = Number(header.width);
const height = Number(header.height);
const channels = Number(header.channels);
const count = Number(header.num_frames);
const frameBytes = Number(header.bytes_per_frame);
const pixels = width * height;
const buffers = [];
for (let f = 0; f < count; f++) {
const offset = f * frameBytes;
if (channels === 4) {
buffers.push(payload.buffer.slice(
payload.byteOffset + offset,
payload.byteOffset + offset + frameBytes,
));
continue;
}
const rgba = new Uint8ClampedArray(pixels * 4);
let src = offset;
let dst = 0;
for (let p = 0; p < pixels; p++) {
rgba[dst++] = payload[src++];
rgba[dst++] = payload[src++];
rgba[dst++] = payload[src++];
src += channels - 3;
rgba[dst++] = 255;
}
buffers.push(rgba.buffer);
}
return buffers;
}
async function encodedFrameToRgbaBuffers(header, payload) {
if (typeof createImageBitmap === "undefined" || typeof OffscreenCanvas === "undefined") {
throw new Error("This browser does not support worker image decoding");
}
const blob = new Blob([payload], { type: header.content_type });
const bitmap = await createImageBitmap(blob);
const width = bitmap.width;
const height = bitmap.height;
const canvas = new OffscreenCanvas(width, height);
const ctx = canvas.getContext("2d", { alpha: false });
ctx.drawImage(bitmap, 0, 0);
const image = ctx.getImageData(0, 0, width, height);
bitmap.close?.();
return {
width,
height,
frames: [image.data.buffer],
};
}
async function decode(header, payload) {
let rawPayload;
if (
header.content_type === WEBP_FRAME_CONTENT_TYPE ||
header.content_type === JPEG_FRAME_CONTENT_TYPE
) {
const decoded = await encodedFrameToRgbaBuffers(header, payload);
return {
id: header.__decode_id,
width: decoded.width,
height: decoded.height,
chunk: Number(header.chunk_index),
frames: decoded.frames,
};
} else if (header.content_type === RAW_RGB_CONTENT_TYPE) {
rawPayload = new Uint8Array(payload);
const frameBytes = Number(header.bytes_per_frame);
const count = Number(header.num_frames);
lastFrame = count > 0
? rawPayload.slice((count - 1) * frameBytes, count * frameBytes)
: null;
} else if (
header.content_type === RAW_RGB_DELTA_GZIP_CONTENT_TYPE ||
header.content_type === RAW_RGBA_DELTA_GZIP_CONTENT_TYPE
) {
rawPayload = await restoreDeltaGzipFrames(header, payload);
} else {
throw new Error(`Unsupported content type ${header.content_type}`);
}
return {
id: header.__decode_id,
width: Number(header.width),
height: Number(header.height),
chunk: Number(header.chunk_index),
frames: rawFramesToRgbaBuffers(header, rawPayload),
};
}
self.onmessage = async (event) => {
const message = event.data;
try {
if (message.type === "reset") {
reset();
return;
}
const result = await decode(message.header, message.payload);
self.postMessage(
{ type: "decoded", ...result },
result.frames,
);
} catch (error) {
self.postMessage({
type: "error",
id: message.header?.__decode_id,
message: error.message || "decode failed",
});
}
};
@@ -0,0 +1,129 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>sglang-diffusion Realtime Studio</title>
<link rel="stylesheet" href="./styles.css?v=realtime-fixes-v25" />
</head>
<body>
<main class="shell">
<section class="panel controls" aria-label="Session controls">
<div class="brand">
<span>sglang-diffusion</span>
<strong>Realtime Studio</strong>
</div>
<label>Server<input id="serverUrl" value="ws://127.0.0.1:30000/v1/realtime_video/generate" /></label>
<label>Model<input id="model" value="" placeholder="auto from /v1/models" /></label>
<div class="section-title">Reference</div>
<label class="reference-upload">
<input id="firstFrame" type="file" accept="image/*" />
<canvas id="referencePreview" width="320" height="180"></canvas>
<span id="referenceName">Preset reference</span>
</label>
<div class="section-title">Generate the scene</div>
<label>Prompt<textarea id="prompt" rows="4">A cinematic handheld shot of a quiet city street at dusk, soft reflections, natural motion.</textarea></label>
<button id="enhanceBtn" class="wide">Enhance</button>
<div class="split">
<label>Size<input id="size" value="832x480" /></label>
<label>FPS<input id="fps" type="number" value="25" min="1" max="60" /></label>
</div>
<div class="split">
<label>Frames<input id="numFrames" type="number" value="9" min="5" step="4" /></label>
<label>Seed<input id="seed" type="number" value="42" /></label>
</div>
<div class="split">
<label>Steps<input id="steps" type="number" value="4" min="1" /></label>
<label>Guidance<input id="guidance" type="number" value="1" step="0.1" /></label>
</div>
<div class="split">
<label>Sink<input id="sinkSize" type="number" value="9" min="0" /></label>
<label>Window<input id="windowFrames" type="number" value="18" min="1" /></label>
</div>
<div class="split">
<label>Transport
<select id="transportFormat">
<option value="webp" selected>WebP preview</option>
<option value="jpeg">JPEG preview</option>
<option value="">Lossless delta</option>
<option value="raw">Raw RGB</option>
</select>
</label>
<label>Quality<input id="transportQuality" type="number" value="95" min="1" max="100" /></label>
</div>
<label class="toggle-row"><input id="frameInterpolation" type="checkbox" />Smooth 2x frames</label>
<label class="toggle-row"><input id="continuous" type="checkbox" checked />Continuous session</label>
<div class="actions">
<button id="connectBtn" class="primary">Generate</button>
<button id="stopBtn">Close session</button>
</div>
<button id="sendPromptBtn" class="wide">Send prompt update</button>
</section>
<section class="stage" aria-label="Realtime preview">
<div class="topbar">
<span id="statusDot" class="dot"></span>
<span id="statusText">Idle</span>
<span id="chunkText">chunk -</span>
<span class="topbar-spacer"></span>
<span class="stage-stat">render <b id="renderFps">0</b> fps</span>
<span class="stage-stat">theoretical <b id="theoreticalFpsText">-</b></span>
<span class="stage-stat">wait <b id="stageLatencyText">-</b></span>
</div>
<canvas id="viewport" width="1280" height="720"></canvas>
<div class="stage-controls" aria-label="Camera controls">
<div class="control-cluster" aria-label="Move camera">
<span class="control-title">Move</span>
<div class="camera-pad move-pad">
<span></span>
<button data-action="w" data-key="W">Forward</button>
<span></span>
<button data-action="a" data-key="A">Left</button>
<button data-action="s" data-key="S">Back</button>
<button data-action="d" data-key="D">Right</button>
</div>
</div>
<div class="control-cluster" aria-label="Look around">
<span class="control-title">Look</span>
<div class="camera-pad look-pad">
<span></span>
<button data-action="i" data-key="↑">Pitch +</button>
<span></span>
<button data-action="j" data-key="←">Yaw -</button>
<button data-action="k" data-key="↓">Pitch -</button>
<button data-action="l" data-key="→">Yaw +</button>
</div>
</div>
</div>
<div class="timeline">
<span id="queueText">queue 0</span>
<span id="frameText">frames 0</span>
<span id="byteText">0 MB</span>
</div>
<div class="telemetry stage-telemetry">
<span>Payload<b id="payloadMode">webp</b></span>
<span>Server send<b id="serverSendText">-</b></span>
<span>Chunk bytes<b id="chunkPayloadText">-</b></span>
<span>Chunk wait<b id="latencyText">-</b></span>
<span>Decode<b id="decodeText">-</b></span>
<span>Display lag<b id="displayLagText">-</b></span>
</div>
</section>
<section class="panel presets" aria-label="Presets and camera">
<div class="section-title">LingBot</div>
<div class="spec-grid">
<span><b>25 fps</b> target</span>
<span><b>chunked</b> stream</span>
<span><b>480p/720p</b></span>
<span><b>Cam + Act</b></span>
</div>
<div class="section-title">Presets</div>
<div id="presetList" class="preset-list"></div>
<div class="section-title">History</div>
<div id="historyList" class="history-list"></div>
</section>
</main>
<script src="./app.js?v=realtime-model-info-v31"></script>
</body>
</html>
@@ -0,0 +1,508 @@
:root {
--paper: #eef1ec;
--panel: #fbfaf5;
--ink: #171a16;
--muted: #687164;
--line: #cbd2c4;
--accent: #b9543c;
--green: #4d765f;
--blue: #3f607c;
--pressed: #8c9288;
--pressed-border: #aeb4aa;
--pressed-ring: rgba(238, 241, 236, 0.2);
--shadow: 0 18px 60px rgba(23, 26, 22, 0.12);
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
background:
linear-gradient(90deg, rgba(23, 26, 22, 0.035) 1px, transparent 1px),
linear-gradient(180deg, rgba(23, 26, 22, 0.035) 1px, transparent 1px),
var(--paper);
background-size: 28px 28px;
color: var(--ink);
font-family: ui-sans-serif, "Avenir Next", "Helvetica Neue", sans-serif;
}
button, input, textarea, select { font: inherit; }
button:disabled { cursor: wait; opacity: 0.64; transform: none; }
.shell {
display: grid;
grid-template-columns: minmax(260px, 320px) minmax(420px, 1fr) minmax(260px, 320px);
gap: 18px;
min-height: 100vh;
padding: 18px;
}
.panel {
background: color-mix(in oklch, var(--panel), white 20%);
border: 1px solid var(--line);
border-radius: 8px;
box-shadow: var(--shadow);
padding: 18px;
}
.brand {
display: flex;
align-items: baseline;
gap: 10px;
margin-bottom: 22px;
}
.brand span {
color: var(--panel);
background: var(--ink);
border-radius: 4px;
padding: 3px 7px;
font-size: 12px;
letter-spacing: 0;
}
.brand strong { font-size: 18px; font-weight: 650; }
label {
display: grid;
gap: 7px;
margin: 12px 0;
color: var(--muted);
font-size: 12px;
}
.label-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.help-tooltip {
position: relative;
display: inline-grid;
place-items: center;
width: 18px;
height: 18px;
border: 1px solid var(--line);
border-radius: 50%;
color: var(--muted);
background: #fffdf7;
cursor: help;
font-size: 11px;
line-height: 1;
}
.help-tooltip::after {
content: attr(aria-label);
position: absolute;
right: 0;
bottom: calc(100% + 8px);
z-index: 20;
width: 280px;
max-width: min(280px, calc(100vw - 48px));
padding: 9px 10px;
border-radius: 6px;
background: var(--ink);
box-shadow: 0 12px 36px rgba(23, 26, 22, 0.24);
color: var(--panel);
font-size: 11px;
font-weight: 400;
line-height: 1.4;
opacity: 0;
pointer-events: none;
transform: translateY(4px);
transition: opacity 120ms ease, transform 120ms ease;
}
.help-tooltip:hover::after,
.help-tooltip:focus-visible::after {
opacity: 1;
transform: translateY(0);
}
input, textarea, select {
width: 100%;
border: 1px solid var(--line);
border-radius: 6px;
background: #fffdf7;
color: var(--ink);
padding: 10px 11px;
outline: none;
}
textarea { resize: vertical; line-height: 1.45; }
input:focus, textarea:focus, select:focus { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(185, 84, 60, 0.12); }
.split { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.actions { display: grid; grid-template-columns: 1fr 0.7fr; gap: 10px; margin-top: 16px; }
.toggle-row {
display: flex;
align-items: center;
gap: 9px;
margin-top: 14px;
}
.toggle-row input {
width: 16px;
height: 16px;
accent-color: var(--ink);
}
button {
border: 1px solid var(--line);
border-radius: 6px;
color: var(--ink);
background: #fffdf7;
min-height: 38px;
padding: 0 12px;
cursor: pointer;
transition:
background-color 120ms ease,
border-color 120ms ease,
box-shadow 120ms ease,
color 120ms ease,
transform 120ms ease;
}
button:hover:not(:disabled) {
border-color: var(--ink);
background: color-mix(in oklch, #fffdf7, var(--green) 10%);
box-shadow: 0 8px 18px rgba(23, 26, 22, 0.08);
transform: translateY(-1px);
}
button:active:not(:disabled),
button.is-pressed:not(:disabled) {
border-color: var(--pressed-border);
background: var(--pressed);
color: #fffdf7;
box-shadow:
inset 0 0 0 1px rgba(255, 253, 247, 0.18),
inset 0 2px 7px rgba(23, 26, 22, 0.16);
transform: translateY(0);
}
button.is-key-active:not(:disabled) {
border-color: var(--pressed-border);
background: var(--pressed);
color: #fffdf7;
box-shadow:
inset 0 0 0 1px rgba(255, 253, 247, 0.22),
0 0 0 3px var(--pressed-ring),
0 10px 22px rgba(23, 26, 22, 0.18);
transform: none;
}
button:focus-visible {
outline: none;
box-shadow: 0 0 0 3px rgba(185, 84, 60, 0.18);
}
.primary { background: var(--ink); color: var(--panel); border-color: var(--ink); }
.primary:hover:not(:disabled) {
background: color-mix(in oklch, var(--ink), var(--green) 18%);
color: var(--panel);
}
.primary:active:not(:disabled),
.primary.is-pressed:not(:disabled) {
background: var(--pressed);
border-color: var(--pressed-border);
color: var(--panel);
}
.wide { width: 100%; margin-top: 10px; }
.stage {
position: relative;
display: grid;
grid-template-rows: auto auto auto auto auto;
align-self: start;
justify-self: center;
width: 100%;
max-width: 1040px;
overflow: hidden;
border: 1px solid #11140f;
border-radius: 8px;
background: #11140f;
box-shadow: var(--shadow);
}
.topbar, .timeline {
display: flex;
align-items: center;
gap: 14px;
height: 44px;
padding: 0 14px;
color: #e8eadf;
background: rgba(17, 20, 15, 0.88);
font-size: 12px;
}
.topbar-spacer { flex: 1; }
#statusText {
display: inline-block;
min-width: 92px;
}
#chunkText {
display: inline-block;
min-width: 70px;
}
.stage-stat {
display: inline-flex;
align-items: baseline;
gap: 5px;
min-width: max-content;
color: rgba(232, 234, 223, 0.72);
}
.stage-stat b {
color: #fffdf7;
font-weight: 650;
}
.timeline { justify-content: flex-end; border-top: 1px solid rgba(232, 234, 223, 0.12); }
.dot { width: 8px; height: 8px; border-radius: 50%; background: var(--muted); }
.dot.live { background: #8ecf9d; box-shadow: 0 0 0 4px rgba(142, 207, 157, 0.14); }
.dot.error { background: var(--accent); }
#viewport {
display: block;
width: 100%;
height: auto;
max-height: min(56vh, 560px);
min-height: 0;
object-fit: contain;
image-rendering: auto;
}
.stage-controls {
display: grid;
grid-template-columns: repeat(2, minmax(180px, 1fr));
gap: 12px;
padding: 12px 14px 13px;
border-top: 1px solid rgba(232, 234, 223, 0.12);
background: #151912;
}
.control-cluster {
display: grid;
grid-template-columns: 46px 1fr;
gap: 10px;
align-items: center;
}
.control-title {
color: rgba(232, 234, 223, 0.62);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.stage-controls .camera-pad {
margin: 0;
}
.stage-controls .camera-pad button {
position: relative;
border-color: rgba(232, 234, 223, 0.18);
background: #eef1ec;
color: #11140f;
}
.stage-controls .camera-pad button:active:not(:disabled),
.stage-controls .camera-pad button.is-pressed:not(:disabled),
.stage-controls .camera-pad button.is-key-active:not(:disabled) {
border-color: var(--pressed-border);
background: var(--pressed);
color: #fffdf7;
box-shadow:
inset 0 0 0 1px rgba(255, 253, 247, 0.22),
0 0 0 3px var(--pressed-ring),
0 10px 22px rgba(23, 26, 22, 0.18);
}
.stage-controls .camera-pad button::after {
content: attr(data-key);
position: absolute;
right: 7px;
top: 5px;
color: color-mix(in oklch, var(--muted), var(--ink) 18%);
font-size: 10px;
font-weight: 650;
}
.stage-controls .camera-pad button:active::after,
.stage-controls .camera-pad button.is-pressed::after,
.stage-controls .camera-pad button.is-key-active::after {
color: rgba(255, 253, 247, 0.78);
}
.section-title {
margin: 16px 0 10px;
color: var(--muted);
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.reference-upload {
margin-top: 0;
}
.reference-upload input {
border: 1px dashed var(--line);
}
#referencePreview {
display: block;
width: 100%;
aspect-ratio: 16 / 9;
min-height: 0;
border: 1px solid var(--line);
border-radius: 8px;
background: #e5e7df;
}
#referenceName {
min-height: 18px;
color: var(--muted);
font-size: 11px;
}
.spec-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
margin-bottom: 12px;
}
.spec-grid span {
display: grid;
gap: 2px;
min-height: 46px;
align-content: center;
border: 1px solid var(--line);
border-radius: 8px;
background: #fffdf7;
padding: 9px;
color: var(--muted);
font-size: 11px;
}
.spec-grid b {
color: var(--ink);
font-size: 14px;
}
.presets {
position: sticky;
top: 18px;
max-height: calc(100vh - 36px);
overflow: auto;
scrollbar-gutter: stable;
}
.preset-list {
display: grid;
gap: 7px;
min-height: 320px;
max-height: min(54vh, 560px);
margin-bottom: 12px;
overflow: auto;
padding-right: 3px;
}
.preset {
display: grid;
grid-template-columns: 72px minmax(0, 1fr);
gap: 4px 9px;
align-items: center;
padding: 8px;
border: 1px solid var(--line);
border-radius: 8px;
background: #fffdf7;
text-align: left;
}
.preset-thumb {
display: block;
grid-row: span 2;
width: 72px;
height: 46px;
min-height: 0;
object-fit: cover;
border-radius: 5px;
border: 1px solid color-mix(in oklch, var(--line), var(--ink) 8%);
}
.preset b { min-width: 0; font-size: 13px; }
.preset span { min-width: 0; color: var(--muted); font-size: 11px; line-height: 1.25; }
.preset[data-tone="green"] { border-left: 4px solid var(--green); }
.preset[data-tone="blue"] { border-left: 4px solid var(--blue); }
.preset[data-tone="accent"] { border-left: 4px solid var(--accent); }
.preset:hover:not(:disabled) {
background: color-mix(in oklch, #fffdf7, var(--blue) 9%);
}
.camera-pad {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 6px;
margin-bottom: 8px;
}
.camera-pad span { min-height: 36px; }
.camera-pad button { min-height: 36px; font-size: 12px; }
.telemetry { display: grid; gap: 7px; margin-top: 10px; }
.telemetry span {
display: flex;
justify-content: space-between;
border-bottom: 1px solid var(--line);
padding-bottom: 8px;
color: var(--muted);
font-size: 12px;
}
.telemetry b { color: var(--ink); font-weight: 650; }
.stage-telemetry {
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0;
margin-top: 0;
border-top: 1px solid rgba(232, 234, 223, 0.12);
background: #11140f;
}
.stage-telemetry span {
min-height: 36px;
align-items: center;
gap: 8px;
border-right: 1px solid rgba(232, 234, 223, 0.1);
border-bottom: 1px solid rgba(232, 234, 223, 0.1);
padding: 0 14px;
color: rgba(232, 234, 223, 0.62);
font-size: 11px;
}
.stage-telemetry b {
color: #fffdf7;
font-size: 12px;
}
.history-list {
display: grid;
gap: 7px;
max-height: 76px;
overflow: auto;
}
.history-list span {
display: block;
border-left: 3px solid var(--blue);
background: #fffdf7;
padding: 8px 9px;
color: var(--muted);
font-size: 12px;
}
@media (max-width: 980px) {
.shell { grid-template-columns: 1fr; }
.presets { position: static; max-height: none; overflow: visible; }
.preset-list { min-height: 260px; max-height: 420px; }
#viewport { max-height: 420px; }
.stage-controls { grid-template-columns: 1fr; }
.stage-telemetry { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.topbar { flex-wrap: wrap; height: auto; min-height: 44px; padding: 10px 14px; }
.topbar-spacer { display: none; }
}
@@ -11,6 +11,7 @@ from typing import TYPE_CHECKING
import httpx
import torch
from fastapi import APIRouter, FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from sglang.multimodal_gen.configs.sample.sampling_params import SamplingParams
from sglang.multimodal_gen.runtime.entrypoints.openai import (
@@ -387,6 +388,13 @@ def create_app(server_args: ServerArgs):
Create and configure the FastAPI application instance.
"""
app = FastAPI(lifespan=lifespan)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.middleware("http")
async def wait_for_server_warmup(request: Request, call_next):
@@ -0,0 +1,79 @@
# SPDX-License-Identifier: Apache-2.0
from pathlib import Path
def test_realtime_webui_presets_do_not_emit_camera_scripts():
repo_root = Path(__file__).resolve().parents[6]
app_js = (
repo_root / "python/sglang/multimodal_gen/apps/realtime_webui/app.js"
).read_text()
index_html = (
repo_root / "python/sglang/multimodal_gen/apps/realtime_webui/index.html"
).read_text()
styles_css = (
repo_root / "python/sglang/multimodal_gen/apps/realtime_webui/styles.css"
).read_text()
assert "preset.actions" not in app_js
assert "repeatActions" not in app_js
assert 'id="eventFrames"' not in index_html
assert "ControlStateController" in app_js
assert 'const DEFAULT_PREVIEW_OUTPUT_FORMAT = "webp";' in app_js
assert 'id="transportFormat"' in index_html
assert 'id="fps" type="number" value="25"' in index_html
assert 'id="frameInterpolation" type="checkbox" />' in index_html
assert (
'id="serverUrl" value="ws://127.0.0.1:30000/v1/realtime_video/generate"'
in index_html
)
assert '<option value="webp" selected>WebP preview</option>' in index_html
assert 'id="serverSendText"' in index_html
assert 'id="theoreticalFpsText"' in index_html
assert 'id="renderFps"' in index_html
assert 'id="stageRenderFps"' not in index_html
assert "sglang-diffusion Realtime Studio" in index_html
assert "SGLD" not in index_html
assert 'class="tabs"' not in index_html
assert "Recordings" not in index_html
assert "API" not in index_html
assert "Info" not in index_html
assert 'id="steps" type="number" value="4"' in index_html
assert 'id="guidance" type="number" value="1"' in index_html
assert "styles.css?v=realtime-fixes-v25" in index_html
assert "app.js?v=realtime-fixes-v30" in index_html
assert 'const DECODER_WORKER_URL = "./decoder_worker.js?v=rgb-worker-v6";' in app_js
assert "const DEFAULT_TARGET_FPS = 25;" in app_js
assert "const DEFAULT_FRAME_INTERPOLATION_EXP = 1;" in app_js
assert "const DEFAULT_FRAME_INTERPOLATION_SCALE = 1.0;" in app_js
assert "enable_frame_interpolation: true" in app_js
assert "frame_interpolation_exp: DEFAULT_FRAME_INTERPOLATION_EXP" in app_js
assert "frame_interpolation_scale: DEFAULT_FRAME_INTERPOLATION_SCALE" in app_js
assert "elapsedMs % targetMs" in app_js
assert "liveQueueFrameFloor(header, chunkFrameCount)" in app_js
assert (
'const REACTOR_PRESET_BASE_URL = "https://www.reactor.inc/lingbot-world-fast-v1";'
in app_js
)
assert "Dragon Dolly" in app_js
assert "no creature morphing" in app_js
assert "A static locked-off view of the back side of Plastic Beach" in app_js
assert "clouds slowly drifting behind the island" in app_js
assert "occasional shooting star" in app_js
assert "tiny distant pigeons" in app_js
assert "Ziggy Stardust" in app_js
assert "blue K. West sign" in app_js
assert "wet pavement reflecting a yellow streetlamp" in app_js
assert "ZiggyStardust.jpg" in app_js
assert "A slow aerial orbit around a pastel floating island hotel" not in app_js
assert app_js.index("Dragon Ride") < app_js.index("Dragon Dolly")
assert app_js.index("Ziggy Stardust") < app_js.index("Plastic Beach")
assert app_js.index("Dragon Dolly") < app_js.index("Kid A")
assert "dragon-ride.jpg" in app_js
assert "stageRenderFps" not in app_js
assert 'setStatus("Receiving"' not in app_js
assert "decodeChain = decodeChain" in app_js
assert "receiveChain" not in app_js
assert 'message.type === "chunk_stats"' in app_js
assert "chunkTotal > 0 ? numFrames / chunkTotal" in app_js
assert ".stage-stat" in styles_css