@hugpy/video-intelligence-ui 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -3
- package/src/config.ts +36 -0
- package/src/demo/assets/generate/cinema-cavern/movie.mp4 +0 -0
- package/src/demo/assets/generate/cinema-cavern/seg_00.mp4 +0 -0
- package/src/demo/assets/generate/clips/cat-lounge.mp4 +0 -0
- package/src/demo/assets/generate/clips/drone-city.mp4 +0 -0
- package/src/demo/assets/generate/clips/paper-sails.mp4 +0 -0
- package/src/demo/assets/generate/movie-galaxy/movie.mp4 +0 -0
- package/src/demo/assets/generate/movie-galaxy/seg_00.mp4 +0 -0
- package/src/demo/assets/generate/movie-galaxy/seg_01.mp4 +0 -0
- package/src/demo/assets/generate/movie-galaxy/seg_02.mp4 +0 -0
- package/src/demo/assets/generate/movie-galaxy/seg_03.mp4 +0 -0
- package/src/demo/assets/generate/movie-medow/movie.mp4 +0 -0
- package/src/demo/assets/generate/movie-medow/seg_00.mp4 +0 -0
- package/src/demo/assets/generate/movie-medow/seg_01.mp4 +0 -0
- package/src/demo/assets/generate/scene-desert/frame_00000.png +0 -0
- package/src/demo/assets/generate/scene-desert/frame_00001.png +0 -0
- package/src/demo/assets/generate/scene-desert/frame_00002.png +0 -0
- package/src/demo/assets/generate/scene-desert/frame_00003.png +0 -0
- package/src/demo/assets/generate/scene-desert/frame_00004.png +0 -0
- package/src/demo/assets/generate/scene-desert/frame_00005.png +0 -0
- package/src/demo/assets/generate/scene-desert/scene.mp4 +0 -0
- package/src/demo/assets/generate/scene-vintage/frame_00000.png +0 -0
- package/src/demo/assets/generate/scene-vintage/frame_00001.png +0 -0
- package/src/demo/assets/generate/scene-vintage/frame_00002.png +0 -0
- package/src/demo/assets/generate/scene-vintage/frame_00003.png +0 -0
- package/src/demo/assets/generate/scene-vintage/frame_00004.png +0 -0
- package/src/demo/assets/generate/scene-vintage/frame_00005.png +0 -0
- package/src/demo/assets/generate/scene-vintage/scene.mp4 +0 -0
- package/src/demo/assets/generate2/s0.png +0 -0
- package/src/demo/assets/generate2/s1.png +0 -0
- package/src/demo/assets/generate2/s10.png +0 -0
- package/src/demo/assets/generate2/s11.png +0 -0
- package/src/demo/assets/generate2/s12.png +0 -0
- package/src/demo/assets/generate2/s13.png +0 -0
- package/src/demo/assets/generate2/s14.png +0 -0
- package/src/demo/assets/generate2/s15.png +0 -0
- package/src/demo/assets/generate2/s16.png +0 -0
- package/src/demo/assets/generate2/s17.png +0 -0
- package/src/demo/assets/generate2/s18.png +0 -0
- package/src/demo/assets/generate2/s19.png +0 -0
- package/src/demo/assets/generate2/s2.png +0 -0
- package/src/demo/assets/generate2/s20.png +0 -0
- package/src/demo/assets/generate2/s21.mp4 +0 -0
- package/src/demo/assets/generate2/s22.mp4 +0 -0
- package/src/demo/assets/generate2/s23.mp4 +0 -0
- package/src/demo/assets/generate2/s24.mp4 +0 -0
- package/src/demo/assets/generate2/s25.mp4 +0 -0
- package/src/demo/assets/generate2/s26.mp4 +0 -0
- package/src/demo/assets/generate2/s27.mp4 +0 -0
- package/src/demo/assets/generate2/s28.mp4 +0 -0
- package/src/demo/assets/generate2/s29.mp4 +0 -0
- package/src/demo/assets/generate2/s3.png +0 -0
- package/src/demo/assets/generate2/s30.mp4 +0 -0
- package/src/demo/assets/generate2/s4.png +0 -0
- package/src/demo/assets/generate2/s5.png +0 -0
- package/src/demo/assets/generate2/s6.png +0 -0
- package/src/demo/assets/generate2/s7.png +0 -0
- package/src/demo/assets/generate2/s8.png +0 -0
- package/src/demo/assets/generate2/s9.png +0 -0
- package/src/demo/demoFetch.ts +126 -3
- package/src/demo/fixtures.ts +72 -4
- package/src/demo/promptAssist.ts +25 -0
- package/src/demo/sampleData.ts +960 -0
- package/src/demo/sampleSets.ts +172 -0
- package/src/stations/ActiveProcessesStation.tsx +18 -3
- package/src/stations/GenerateStation.tsx +901 -366
- package/src/stations/GoalComposer.tsx +9 -1
- package/src/stations/ScriptFirstStation.tsx +1081 -0
- package/src/stations/ToksPanel.tsx +188 -0
- package/src/stations/WorkbenchDrawer.tsx +4 -3
- package/src/stations/WorkbenchSidebar.tsx +6 -3
- package/src/stations/oracle/DagPanel.tsx +391 -0
- package/src/stations/oracle/OracleStation.tsx +304 -0
- package/src/stations/oracle/SpatialTab.tsx +185 -0
- package/src/stations/oracle/StewardPanel.tsx +165 -0
- package/src/stations/oracle/oracle.css +467 -0
- package/src/stations/oracle/oracleShared.tsx +160 -0
- package/src/stations/oracle/oracleTypes.ts +304 -0
- package/src/stations/oracle/useOracle.ts +560 -0
- package/src/stations/registry.ts +37 -0
- package/src/stations/scriptFirst.css +350 -0
- package/src/stations/studio/IdentitiesStation.tsx +219 -292
- package/src/stations/studio/IdentityCreatePanel.tsx +390 -0
- package/src/stations/studio/StudioGenerateTab.tsx +10 -17
- package/src/stations/studio/StudioMovieComposer.tsx +296 -59
- package/src/stations/studio/StudioPlane.tsx +35 -3
- package/src/stations/studio/StudioViewer.tsx +116 -14
- package/src/stations/studio/coordinationBadge.tsx +206 -0
- package/src/stations/studio/identityUpload.ts +66 -0
- package/src/stations/studio/promptCard.tsx +362 -59
- package/src/stations/studio/useIdentityCreateJob.ts +184 -0
- package/src/stations/studio/useIdentityProfiles.ts +66 -0
- package/src/stations/useScriptFirstRun.ts +596 -0
- package/src/style/app.css +157 -0
- package/src/video/LengthRow.tsx +189 -0
- package/src/video/coordinationReport.ts +266 -0
- package/src/video/jobClock.ts +104 -0
- package/src/video/spreadAssist.ts +26 -0
- package/src/video/useMediaJobs.ts +26 -0
- package/src/video/useOperatorHistory.ts +5 -0
- package/src/video/useToks.ts +193 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hugpy/video-intelligence-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Video-intelligence console \u2014 sibling arm of media_intelligence_ui (Vite + Tailwind v4), served at /video. Four stations (image crop, audio crop, frames/models, generate) over the hugpy job bus. Dials the same-origin current-hugpy API (/api).",
|
|
@@ -29,8 +29,6 @@
|
|
|
29
29
|
"entry.tsx",
|
|
30
30
|
"index.html",
|
|
31
31
|
"assets",
|
|
32
|
-
"help",
|
|
33
|
-
"navbar",
|
|
34
32
|
"vite.config.ts",
|
|
35
33
|
"tsconfig.json",
|
|
36
34
|
"LICENSE"
|
package/src/config.ts
CHANGED
|
@@ -58,6 +58,23 @@ export const hugpyConfig = {
|
|
|
58
58
|
*/
|
|
59
59
|
videoExtractUrl:
|
|
60
60
|
read("VITE_HUGPY_VIDEO_EXTRACT_URL") ?? `${apiBase}/video/identity-profiles/video-extract`,
|
|
61
|
+
/**
|
|
62
|
+
* k94 — ONE PATH identity creation (Identities tab, "Create identity"):
|
|
63
|
+
* from-video: POST { source:<MediaRef, kind:"video">, name, mesh_params? }
|
|
64
|
+
* → 202 { job_id, name, slug, kind:"identity_from_video" }
|
|
65
|
+
* ONE chained char360 + Hunyuan3D job; on done ONE profile per
|
|
66
|
+
* detected character (slug, slug-2, …) with crops + canonical + GLB.
|
|
67
|
+
* from-images: POST { sources:[<MediaRef, kind:"image">…], name, mesh_params? }
|
|
68
|
+
* → 202 { job_id, recon_id, slug, profile, kind:"identity_mesh_build" }
|
|
69
|
+
* create profile + the one-click full build chained server-side.
|
|
70
|
+
* Both jobs are polled on `videoJobUrl` (GET /video/jobs/<id>): `progress`
|
|
71
|
+
* carries stage / progress / log_tail; `result.identities` (from-video) lists
|
|
72
|
+
* what was minted. Declared here (the one URL-literal module).
|
|
73
|
+
*/
|
|
74
|
+
identityFromVideoUrl:
|
|
75
|
+
read("VITE_HUGPY_IDENTITY_FROM_VIDEO_URL") ?? `${apiBase}/video/identity-profiles/from-video`,
|
|
76
|
+
identityFromImagesUrl:
|
|
77
|
+
read("VITE_HUGPY_IDENTITY_FROM_IMAGES_URL") ?? `${apiBase}/video/identity-profiles/from-images`,
|
|
61
78
|
/** Enqueue a crop job (spatial and/or temporal axis) on the job bus. */
|
|
62
79
|
cropEnqueueUrl: read("VITE_HUGPY_CROP_ENQUEUE_URL") ?? `${apiBase}/video/jobs/crop`,
|
|
63
80
|
/** Enqueue a frame-extract job (Frames & Models station) — one job → many frames. */
|
|
@@ -184,6 +201,18 @@ export const hugpyConfig = {
|
|
|
184
201
|
* Processes surface never derives the path inline.
|
|
185
202
|
*/
|
|
186
203
|
mediaJobsUrl: read("VITE_HUGPY_MEDIA_JOBS_URL") ?? `${apiBase}/video/jobs`,
|
|
204
|
+
/**
|
|
205
|
+
* LIVE tok/s telemetry for the generate workbench's Toks panel. Two endpoints,
|
|
206
|
+
* both same-origin `/api`, polled together every 2s by `useToks`:
|
|
207
|
+
* • `toksRecentUrl` GET → { entries:[{ ts, worker_id, worker_name, model_key,
|
|
208
|
+
* tok_s, ttft_s, completion_tokens, config_key, ok }] } newest-first — the
|
|
209
|
+
* live tail + the per-worker strip's source (avg computed from this window).
|
|
210
|
+
* • `toksReportUrl` GET → { groups:[{ worker_name, model_key, config_key, n,
|
|
211
|
+
* mean_tok_s, p50_tok_s, p95_tok_s }] } sorted best mean first — the
|
|
212
|
+
* best-config leaderboard. Declared here so the panel never derives the path.
|
|
213
|
+
*/
|
|
214
|
+
toksRecentUrl: read("VITE_HUGPY_TOKS_RECENT_URL") ?? `${apiBase}/llm/toks/recent`,
|
|
215
|
+
toksReportUrl: read("VITE_HUGPY_TOKS_REPORT_URL") ?? `${apiBase}/llm/toks/report`,
|
|
187
216
|
/**
|
|
188
217
|
* Curated STUDIO clip presets for the Studio Clips station — GET → { presets: [{
|
|
189
218
|
* id, name, description, capability:"i2v"|"t2v", width, height, fps, vram_budget_gb,
|
|
@@ -263,6 +292,13 @@ export const hugpyConfig = {
|
|
|
263
292
|
*/
|
|
264
293
|
promptAssistUrl:
|
|
265
294
|
read("VITE_HUGPY_PROMPT_ASSIST_URL") ?? `${apiBase}/video/prompt/assist`,
|
|
295
|
+
/**
|
|
296
|
+
* CINEMA PRODUCER (k120 slice 1): premise → full structured plan → per-segment
|
|
297
|
+
* {prompt, negative, seconds, joint}. The composer's 🎬 Produce action POSTs
|
|
298
|
+
* here and populates its segment rows from the reply.
|
|
299
|
+
*/
|
|
300
|
+
producerPlanUrl:
|
|
301
|
+
read("VITE_HUGPY_PRODUCER_PLAN_URL") ?? `${apiBase}/video/producer/plan`,
|
|
266
302
|
/**
|
|
267
303
|
* STUDIO-ASSIST LIVE LOG (operator directive, 2026-07-31) — a stored record per
|
|
268
304
|
* prompt-generate attempt: the UNTRUNCATED model reply, what was stripped
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/src/demo/demoFetch.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import { hugpyConfig } from "../config";
|
|
13
13
|
import * as fx from "./fixtures";
|
|
14
14
|
import type { JobKind } from "./fixtures";
|
|
15
|
-
import {
|
|
15
|
+
import { samplePromptFor, enhancePrompt } from "./promptAssist";
|
|
16
16
|
|
|
17
17
|
// ── request introspection (same helpers as the media arm) ───────────────────
|
|
18
18
|
function pathOf(input: RequestInfo | URL): string {
|
|
@@ -127,15 +127,24 @@ async function route(
|
|
|
127
127
|
const kind = JOB_ROUTES[path];
|
|
128
128
|
if (kind && method === "POST") {
|
|
129
129
|
let sourceKind: string | null = null;
|
|
130
|
+
let submitted = "";
|
|
130
131
|
try {
|
|
131
132
|
const body = JSON.parse((await readBody(input, init)) || "{}") as {
|
|
132
133
|
source?: { kind?: string };
|
|
134
|
+
parts?: Array<{ kind?: string; text?: string | null }>;
|
|
135
|
+
goals?: Array<{ prompt?: string | null }>;
|
|
136
|
+
prompt?: string;
|
|
133
137
|
};
|
|
134
138
|
sourceKind = body?.source?.kind ?? null;
|
|
139
|
+
submitted = [
|
|
140
|
+
...(body?.parts ?? []).map((p) => (p?.kind === "text" ? p?.text ?? "" : "")),
|
|
141
|
+
...(body?.goals ?? []).map((g) => g?.prompt ?? ""),
|
|
142
|
+
body?.prompt ?? "",
|
|
143
|
+
].join(" ");
|
|
135
144
|
} catch {
|
|
136
145
|
/* body optional */
|
|
137
146
|
}
|
|
138
|
-
return jsonResponse({ job_id: fx.enqueueJob(kind, sourceKind) });
|
|
147
|
+
return jsonResponse({ job_id: fx.enqueueJob(kind, sourceKind, submitted) });
|
|
139
148
|
}
|
|
140
149
|
|
|
141
150
|
// Job cancel (before poll — its path is a superset).
|
|
@@ -181,7 +190,7 @@ async function route(
|
|
|
181
190
|
} catch {
|
|
182
191
|
/* body optional — defaults stand */
|
|
183
192
|
}
|
|
184
|
-
const prompt = mode === "detail" ? enhancePrompt(draft) :
|
|
193
|
+
const prompt = mode === "detail" ? enhancePrompt(draft) : samplePromptFor(kind);
|
|
185
194
|
return jsonResponse({ prompt, model: "demo-showroom-llm", kind });
|
|
186
195
|
}
|
|
187
196
|
|
|
@@ -252,6 +261,39 @@ async function route(
|
|
|
252
261
|
return jsonResponse({ error: "operator authentication required" }, 401);
|
|
253
262
|
}
|
|
254
263
|
|
|
264
|
+
// STUDIO CLIP enqueue (Clip tab, POST /video/studio/i2v → {job_id}) and
|
|
265
|
+
// CINEMA enqueue (StudioMovieComposer, POST /video/studio/movie → {job_id}).
|
|
266
|
+
// Both poll the generic GET /video/jobs/<id> above, so the whole render
|
|
267
|
+
// lifecycle is canned: queued → running (staged progress) → done with
|
|
268
|
+
// bundled clips (cinema: segments + assembled movie LAST, the contract the
|
|
269
|
+
// composer plays back).
|
|
270
|
+
if ((path === "/video/studio/i2v" || path === "/video/studio/movie") && method === "POST") {
|
|
271
|
+
let submitted = "";
|
|
272
|
+
try {
|
|
273
|
+
const body = JSON.parse((await readBody(input, init)) || "{}") as {
|
|
274
|
+
prompt?: string;
|
|
275
|
+
goals?: Array<{ prompt?: string | null }>;
|
|
276
|
+
};
|
|
277
|
+
submitted = [body?.prompt ?? "", ...(body?.goals ?? []).map((g) => g?.prompt ?? "")].join(" ");
|
|
278
|
+
} catch { /* body optional */ }
|
|
279
|
+
const k = path.endsWith("i2v") ? "studio_i2v" : "generate_studio_movie";
|
|
280
|
+
return jsonResponse({ job_id: fx.enqueueJob(k as JobKind, null, submitted) });
|
|
281
|
+
}
|
|
282
|
+
// Cinema session pause/resume + clip archive/unarchive — acknowledge; the
|
|
283
|
+
// canned session store is empty so there is nothing durable to mutate.
|
|
284
|
+
const studioAct = path.match(
|
|
285
|
+
/^\/video\/studio\/(?:movie\/[^/]+\/(pause|resume)|clip\/[^/]+\/(archive|unarchive))$/,
|
|
286
|
+
);
|
|
287
|
+
if (studioAct && method === "POST") {
|
|
288
|
+
return jsonResponse({ ok: true });
|
|
289
|
+
}
|
|
290
|
+
// Studio tester sweep — operator-gated on the real backend; the brochure
|
|
291
|
+
// principal is not an operator, so the honest answer is 401 (same shape as
|
|
292
|
+
// the video-share keys above).
|
|
293
|
+
if (path === "/video/studio/tester" && method === "POST") {
|
|
294
|
+
return jsonResponse({ error: "operator authentication required" }, 401);
|
|
295
|
+
}
|
|
296
|
+
|
|
255
297
|
// Curated presets for the Generate-station dropdown + the per-id pre-warm.
|
|
256
298
|
if (path === "/video/presets" && method === "GET") {
|
|
257
299
|
return jsonResponse(fx.PRESETS);
|
|
@@ -275,15 +317,92 @@ async function route(
|
|
|
275
317
|
return jsonResponse(found ?? { error: { code: "unknown_preset" } }, found ? 200 : 404);
|
|
276
318
|
}
|
|
277
319
|
|
|
320
|
+
// Assist LOG surfaces (audit 2026-08-13b): models dropdown + log fetch.
|
|
321
|
+
// The SSE stream itself is handled by the FakeEventSource below (EventSource
|
|
322
|
+
// BYPASSES window.fetch — unshimmed it hit the demo host's nginx 404 and
|
|
323
|
+
// auto-reconnected forever: the reported "gets stuck" loop).
|
|
324
|
+
if (path === "/video/prompt/assist/models" && method === "GET") {
|
|
325
|
+
return jsonResponse({ models: [{ id: "demo-showroom-llm", label: "demo-showroom-llm" }] });
|
|
326
|
+
}
|
|
327
|
+
if (path === "/video/prompt/assist/log" && method === "GET") {
|
|
328
|
+
return jsonResponse({ lines: [] });
|
|
329
|
+
}
|
|
330
|
+
if (path === "/video/prompt/intent" && method === "POST") {
|
|
331
|
+
return jsonResponse({ ok: false }, 200);
|
|
332
|
+
}
|
|
333
|
+
if (path === "/video/render/presets" && method === "GET") {
|
|
334
|
+
return jsonResponse({ presets: [] });
|
|
335
|
+
}
|
|
336
|
+
const chatCancel = path.match(/^\/llm\/chat\/cancel\/[^/]+$/);
|
|
337
|
+
if (chatCancel && method === "POST") {
|
|
338
|
+
return jsonResponse({ ok: true });
|
|
339
|
+
}
|
|
340
|
+
// Identity flows are operator/member-gated on the real backend — honest 401
|
|
341
|
+
// (the SharePanel/keys pattern) keeps those affordances hidden in the demo.
|
|
342
|
+
if (path.startsWith("/video/identity-profiles/") && method === "POST") {
|
|
343
|
+
return jsonResponse({ error: "operator authentication required" }, 401);
|
|
344
|
+
}
|
|
345
|
+
// Single cinema-session probe (resume path) — the showroom has no durable
|
|
346
|
+
// movie dirs, so an unknown id is the honest answer.
|
|
347
|
+
const movieOne = path.match(/^\/video\/studio\/movie\/[^/]+$/);
|
|
348
|
+
if (movieOne && method === "GET") {
|
|
349
|
+
return jsonResponse({ ok: false, error: "unknown movie" }, 404);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// BACKSTOP (2026-08-13): anything addressed to the API base that no route
|
|
353
|
+
// above canned must FAIL FAST AND HONESTLY — never fall through to the real
|
|
354
|
+
// fetch. On the demo host nginx hard-404s /api, so a fall-through shows a
|
|
355
|
+
// spinner that dies slowly ("renders a while, then fails" — the exact
|
|
356
|
+
// reported symptom class). A crisp canned 501 lets every surface fail-soft
|
|
357
|
+
// immediately instead. Bundled assets / HMR never carry the API prefix, so
|
|
358
|
+
// the pass-through below still serves them.
|
|
359
|
+
if (pathname !== path || path.startsWith("/video/") || path.startsWith("/v1/")) {
|
|
360
|
+
return jsonResponse(
|
|
361
|
+
{ error: "showroom: this surface is not canned in the demo" },
|
|
362
|
+
501,
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
|
|
278
366
|
return null; // not a hugpy call — pass through (bundled assets, HMR, …)
|
|
279
367
|
}
|
|
280
368
|
|
|
281
369
|
// ── install / uninstall ──────────────────────────────────────────────────────
|
|
282
370
|
let _realFetch: typeof window.fetch | null = null;
|
|
371
|
+
let _realES: typeof window.EventSource | null = null;
|
|
372
|
+
|
|
373
|
+
// EventSource does NOT route through window.fetch, so the canned transport
|
|
374
|
+
// must shim it separately: a silent, open stream for the assist log (no
|
|
375
|
+
// events, no reconnect churn), pass-through for anything else.
|
|
376
|
+
class FakeEventSource {
|
|
377
|
+
url: string;
|
|
378
|
+
readyState = 0;
|
|
379
|
+
onopen: ((e: Event) => void) | null = null;
|
|
380
|
+
onmessage: ((e: MessageEvent) => void) | null = null;
|
|
381
|
+
onerror: ((e: Event) => void) | null = null;
|
|
382
|
+
constructor(url: string | URL) {
|
|
383
|
+
this.url = String(url);
|
|
384
|
+
setTimeout(() => {
|
|
385
|
+
this.readyState = 1;
|
|
386
|
+
this.onopen?.(new Event("open"));
|
|
387
|
+
}, 50);
|
|
388
|
+
}
|
|
389
|
+
addEventListener(type: string, fn: (e: Event) => void): void {
|
|
390
|
+
if (type === "open") this.onopen = fn as (e: Event) => void;
|
|
391
|
+
}
|
|
392
|
+
removeEventListener(): void {}
|
|
393
|
+
close(): void { this.readyState = 2; }
|
|
394
|
+
}
|
|
283
395
|
|
|
284
396
|
export function installDemoTransport(): void {
|
|
285
397
|
if (_realFetch) return; // already installed
|
|
286
398
|
_realFetch = window.fetch.bind(window);
|
|
399
|
+
_realES = window.EventSource;
|
|
400
|
+
window.EventSource = function (url: string | URL, init?: EventSourceInit) {
|
|
401
|
+
if (String(url).includes("/prompt/assist/log/stream")) {
|
|
402
|
+
return new FakeEventSource(url) as unknown as EventSource;
|
|
403
|
+
}
|
|
404
|
+
return new _realES!(url, init);
|
|
405
|
+
} as unknown as typeof window.EventSource;
|
|
287
406
|
window.fetch = async (
|
|
288
407
|
input: RequestInfo | URL,
|
|
289
408
|
init?: RequestInit,
|
|
@@ -302,4 +421,8 @@ export function uninstallDemoTransport(): void {
|
|
|
302
421
|
if (!_realFetch) return;
|
|
303
422
|
window.fetch = _realFetch;
|
|
304
423
|
_realFetch = null;
|
|
424
|
+
if (_realES) {
|
|
425
|
+
window.EventSource = _realES;
|
|
426
|
+
_realES = null;
|
|
427
|
+
}
|
|
305
428
|
}
|
package/src/demo/fixtures.ts
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
// mediaBytesUrl() passes these absolute asset URLs straight through and the
|
|
7
7
|
// browser loads the bundled bytes directly. See config.ts.
|
|
8
8
|
import type { MediaRef } from "../video/contract";
|
|
9
|
+
import { takeSet, sectionFor, type SampleSet } from "./sampleSets";
|
|
10
|
+
import { SAMPLES2, type Sample2 } from "./sampleData";
|
|
9
11
|
|
|
10
12
|
// Real sample media dropped by the operator under assets/sampels/ (note: the dir
|
|
11
13
|
// is intentionally spelled "sampels" — do NOT rename it). Imported in place as
|
|
@@ -481,25 +483,77 @@ export type JobKind =
|
|
|
481
483
|
| "audio_extract"
|
|
482
484
|
| "generate_image"
|
|
483
485
|
| "generate_scene"
|
|
484
|
-
| "generate_movie"
|
|
486
|
+
| "generate_movie"
|
|
487
|
+
| "studio_i2v"
|
|
488
|
+
| "generate_studio_movie";
|
|
485
489
|
|
|
486
490
|
interface CannedJob {
|
|
487
491
|
kind: JobKind;
|
|
488
492
|
enqueuedAt: number;
|
|
489
493
|
sourceKind: string | null; // MediaRef.kind of the request source, when sent
|
|
494
|
+
// Pre-rendered SAMPLE SET picked at enqueue (generate kinds only) — stored
|
|
495
|
+
// on the job so every poll answers with the SAME render, and so the ✨-armed
|
|
496
|
+
// prompt⇄media pairing survives the queued→running→done lifecycle.
|
|
497
|
+
sample?: SampleSet;
|
|
498
|
+
// 2026-08-13b: RECORDED-ENVELOPE sample (sampleData.ts) — matched against
|
|
499
|
+
// the SUBMITTED prompt at enqueue, so prompt⇄render consistency is
|
|
500
|
+
// structural, not cursor-based. When set, jobStatus replays this envelope
|
|
501
|
+
// verbatim on done (production's own wire shape — drift-proof).
|
|
502
|
+
replay?: Sample2;
|
|
490
503
|
}
|
|
491
504
|
|
|
492
505
|
const jobs = new Map<string, CannedJob>();
|
|
493
506
|
let seq = 0;
|
|
494
507
|
|
|
495
|
-
|
|
508
|
+
const norm = (t: string) => t.toLowerCase().replace(/[^a-z0-9 ]+/g, "").replace(/\s+/g, " ").trim();
|
|
509
|
+
|
|
510
|
+
const SECTION2: Record<string, string> = {
|
|
511
|
+
generate_image: "scene", generate_scene: "scene", generate_movie: "movie",
|
|
512
|
+
studio_i2v: "clip", generate_studio_movie: "cinema",
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
const rr2: Record<string, number> = {};
|
|
516
|
+
|
|
517
|
+
/** Pick the recorded sample whose prompt matches the submitted text; else
|
|
518
|
+
* round-robin within the job's section. Matching is normalized substring in
|
|
519
|
+
* BOTH directions, so an assist-provided prompt (verbatim) and a lightly
|
|
520
|
+
* edited one both land on their render. */
|
|
521
|
+
function pickReplay(kind: JobKind, submitted: string): Sample2 | undefined {
|
|
522
|
+
const section = SECTION2[kind];
|
|
523
|
+
if (!section) return undefined;
|
|
524
|
+
const pool = SAMPLES2.filter((x) => x.kind === section);
|
|
525
|
+
if (!pool.length) return undefined;
|
|
526
|
+
const t = norm(submitted);
|
|
527
|
+
if (t.length > 8) {
|
|
528
|
+
for (const x of pool) {
|
|
529
|
+
for (const pr of x.prompts) {
|
|
530
|
+
const n = norm(pr || "");
|
|
531
|
+
if (n && (n.includes(t) || t.includes(n))) return x;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
const i = (rr2[section] = ((rr2[section] ?? -1) + 1) % pool.length);
|
|
536
|
+
return pool[i];
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
export function enqueueJob(kind: JobKind, sourceKind: string | null,
|
|
540
|
+
submittedPrompt = ""): string {
|
|
496
541
|
seq += 1;
|
|
497
542
|
const id = `demo-job-${kind}-${seq}`;
|
|
498
|
-
|
|
543
|
+
const job: CannedJob = { kind, enqueuedAt: Date.now(), sourceKind };
|
|
544
|
+
// The demo never touches a GPU (operator ruling): generate kinds replay a
|
|
545
|
+
// RECORDED production envelope chosen by the submitted prompt, with the
|
|
546
|
+
// legacy curated sets as fallback for anything unmatched.
|
|
547
|
+
if (kind.startsWith("generate") || kind === "studio_i2v") {
|
|
548
|
+
job.replay = pickReplay(kind, submittedPrompt);
|
|
549
|
+
if (!job.replay) job.sample = takeSet(sectionFor(kind));
|
|
550
|
+
}
|
|
551
|
+
jobs.set(id, job);
|
|
499
552
|
return id;
|
|
500
553
|
}
|
|
501
554
|
|
|
502
555
|
function outputsFor(job: CannedJob): MediaRef[] {
|
|
556
|
+
if (job.sample) return job.sample.outputs;
|
|
503
557
|
switch (job.kind) {
|
|
504
558
|
case "crop":
|
|
505
559
|
// image crop → a still; video crop → the cropped clip
|
|
@@ -517,6 +571,13 @@ function outputsFor(job: CannedJob): MediaRef[] {
|
|
|
517
571
|
// Per-segment clips (as segment "frames") + the assembled movie.mp4 LAST
|
|
518
572
|
// (movie contract). The real keeper-matrix movie has 2 segments.
|
|
519
573
|
return [demoMovieSeg0Clip, demoMovieSeg1Clip, demoMovieClip];
|
|
574
|
+
case "studio_i2v":
|
|
575
|
+
// A studio CLIP render: one clip out (the same bundled scene clip).
|
|
576
|
+
return [demoSceneClip];
|
|
577
|
+
case "generate_studio_movie":
|
|
578
|
+
// CINEMA: segment clips + the assembled movie LAST — same contract as
|
|
579
|
+
// generate_movie, which is exactly how the composer reads it.
|
|
580
|
+
return [demoMovieSeg0Clip, demoMovieSeg1Clip, demoMovieClip];
|
|
520
581
|
}
|
|
521
582
|
}
|
|
522
583
|
|
|
@@ -778,7 +839,8 @@ export function jobStatus(id: string): {
|
|
|
778
839
|
}
|
|
779
840
|
const age = Date.now() - job.enqueuedAt;
|
|
780
841
|
const isGen = job.kind.startsWith("generate");
|
|
781
|
-
const isMovie =
|
|
842
|
+
const isMovie =
|
|
843
|
+
job.kind === "generate_movie" || job.kind === "generate_studio_movie";
|
|
782
844
|
// A movie is many segments — give it a longer believable run than a scene/image.
|
|
783
845
|
const runFor = isMovie ? 5200 : isGen ? 3500 : 1800;
|
|
784
846
|
// Unclaimed window: the job is enqueued but no worker has picked it up yet. The
|
|
@@ -801,6 +863,12 @@ export function jobStatus(id: string): {
|
|
|
801
863
|
: null,
|
|
802
864
|
};
|
|
803
865
|
}
|
|
866
|
+
if (job.replay) {
|
|
867
|
+
const result = JSON.parse(
|
|
868
|
+
JSON.stringify(job.replay.result).replace(/@@JOB@@/g, id),
|
|
869
|
+
) as Record<string, unknown>;
|
|
870
|
+
return { job_id: id, status: "done", result };
|
|
871
|
+
}
|
|
804
872
|
return {
|
|
805
873
|
job_id: id,
|
|
806
874
|
status: "done",
|
package/src/demo/promptAssist.ts
CHANGED
|
@@ -153,6 +153,31 @@ let _lastGenerated: string | null = null;
|
|
|
153
153
|
* same one twice in a row" a hard guarantee (bounded retry, then a forced tail
|
|
154
154
|
* descriptor so it always terminates even in the degenerate single-bank case).
|
|
155
155
|
*/
|
|
156
|
+
import { assistPrompt, sectionFor } from "./sampleSets";
|
|
157
|
+
import { SAMPLES2 } from "./sampleData";
|
|
158
|
+
|
|
159
|
+
const s2cursor: Record<string, number> = {};
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* 2026-08-13 (operator ruling): ✨ generate hands out the REAL prompts that
|
|
163
|
+
* produced the bundled sample renders for the requesting section, and ARMS
|
|
164
|
+
* that sample so the next generate renders exactly that media (sampleSets.ts).
|
|
165
|
+
* The fragment-bank composer below stays as the implementation detail of
|
|
166
|
+
* `enhancePrompt` only.
|
|
167
|
+
*/
|
|
168
|
+
export function samplePromptFor(kind: string): string {
|
|
169
|
+
// Prefer the RECORDED samples (sampleData.ts): the prompt handed out is one
|
|
170
|
+
// that provably produced a bundled render, and enqueue-side matching
|
|
171
|
+
// (fixtures.pickReplay) then replays exactly that envelope.
|
|
172
|
+
const section = sectionFor(kind === "image" ? "scene" : kind);
|
|
173
|
+
const pool = SAMPLES2.filter((x) => x.kind === section && x.prompts.length);
|
|
174
|
+
if (pool.length) {
|
|
175
|
+
const i = (s2cursor[section] = ((s2cursor[section] ?? -1) + 1) % pool.length);
|
|
176
|
+
return pool[i].prompts.join("\n\n");
|
|
177
|
+
}
|
|
178
|
+
return assistPrompt(sectionFor(kind));
|
|
179
|
+
}
|
|
180
|
+
|
|
156
181
|
export function nextGeneratedPrompt(): string {
|
|
157
182
|
let candidate = composePrompt();
|
|
158
183
|
for (let tries = 0; tries < 8 && candidate === _lastGenerated; tries++) {
|