@hugpy/video-intelligence-ui 0.1.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/LICENSE +41 -0
- package/assets/sampels/images/bycycle/bd8603bad35d4f179f4d1134d8faf2ee.mp4 +0 -0
- package/assets/sampels/images/bycycle/frame_00000.png +0 -0
- package/assets/sampels/images/bycycle/frame_00001.png +0 -0
- package/assets/sampels/images/bycycle/frame_00002.png +0 -0
- package/assets/sampels/images/bycycle/frame_00003.png +0 -0
- package/assets/sampels/images/bycycle/frame_00004.png +0 -0
- package/assets/sampels/images/bycycle/frame_00005.png +0 -0
- package/assets/sampels/images/forrest/ff23090611b844a6b85c224f388bd542.mp4 +0 -0
- package/assets/sampels/images/forrest/frame_00000.png +0 -0
- package/assets/sampels/images/forrest/frame_00001.png +0 -0
- package/assets/sampels/images/forrest/frame_00002.png +0 -0
- package/assets/sampels/images/forrest/frame_00003.png +0 -0
- package/assets/sampels/images/forrest/frame_00004.png +0 -0
- package/assets/sampels/images/forrest/frame_00005.png +0 -0
- package/assets/sampels/images/shenzo/e1a4fcd623084ab4a986d11c8cd00db8.mp4 +0 -0
- package/assets/sampels/images/shenzo/frame_00000.png +0 -0
- package/assets/sampels/images/shenzo/frame_00001.png +0 -0
- package/assets/sampels/images/shenzo/frame_00002.png +0 -0
- package/assets/sampels/images/shenzo/frame_00003.png +0 -0
- package/assets/sampels/images/shenzo/frame_00004.png +0 -0
- package/assets/sampels/images/shenzo/frame_00005.png +0 -0
- package/assets/sampels/scenes/man.mp4 +0 -0
- package/assets/sampels/scenes/woman.mp4 +0 -0
- package/assets/sampels/studio_movies/caverns/_assembly/contrib_00.mp4 +0 -0
- package/assets/sampels/studio_movies/caverns/movie.json +67 -0
- package/assets/sampels/studio_movies/caverns/movie.mp4 +0 -0
- package/assets/sampels/studio_movies/caverns/segment_00/5eea7770a554fef0c3bdc38f8f38b925dcf75f13c015d423e8eadec47d41bf6b/clip.mp4 +0 -0
- package/assets/sampels/studio_movies/caverns/segment_00/5eea7770a554fef0c3bdc38f8f38b925dcf75f13c015d423e8eadec47d41bf6b/manifest.json +87 -0
- package/assets/sampels/studio_movies/caverns/segment_00/5eea7770a554fef0c3bdc38f8f38b925dcf75f13c015d423e8eadec47d41bf6b/provenance.json +7 -0
- package/assets/sampels/studio_movies/caverns/segment_01/branch.png +0 -0
- package/assets/sampels/studio_movies/keeper-matrix/_assembly/concat_list.txt +2 -0
- package/assets/sampels/studio_movies/keeper-matrix/_assembly/contrib_00.mp4 +0 -0
- package/assets/sampels/studio_movies/keeper-matrix/_assembly/contrib_01.mp4 +0 -0
- package/assets/sampels/studio_movies/keeper-matrix/movie.json +81 -0
- package/assets/sampels/studio_movies/keeper-matrix/movie.mp4 +0 -0
- package/assets/sampels/studio_movies/keeper-matrix/segment_00/64854c6a21d47ce10b23e884e12de448b25bfb0a375d33238dd0bec8056c076d/clip.mp4 +0 -0
- package/assets/sampels/studio_movies/keeper-matrix/segment_00/64854c6a21d47ce10b23e884e12de448b25bfb0a375d33238dd0bec8056c076d/manifest.json +87 -0
- package/assets/sampels/studio_movies/keeper-matrix/segment_00/64854c6a21d47ce10b23e884e12de448b25bfb0a375d33238dd0bec8056c076d/provenance.json +7 -0
- package/assets/sampels/studio_movies/keeper-matrix/segment_01/branch.png +0 -0
- package/assets/sampels/studio_movies/keeper-matrix/segment_01/cc926b44a42b90d7d291a4e50e3859458306c207a17c718086a3466e4049d6c6/clip.mp4 +0 -0
- package/assets/sampels/studio_movies/keeper-matrix/segment_01/cc926b44a42b90d7d291a4e50e3859458306c207a17c718086a3466e4049d6c6/manifest.json +87 -0
- package/assets/sampels/studio_movies/keeper-matrix/segment_01/cc926b44a42b90d7d291a4e50e3859458306c207a17c718086a3466e4049d6c6/provenance.json +7 -0
- package/assets/sampels/videos/a_red_paper_boat_drifting_on_a_calm_pond__gentle_ripples__mo_a62d6c28.mp4 +0 -0
- package/assets/sampels/videos/aurora.mp4 +0 -0
- package/assets/sampels/videos/dark_night.mp4 +0 -0
- package/assets/sampels/videos/fire_and_soul.mp4 +0 -0
- package/assets/sampels/videos/harsh_seas.mp4 +0 -0
- package/entry.tsx +74 -0
- package/index.html +53 -0
- package/package.json +42 -0
- package/src/assets/hugpy-mark.png +0 -0
- package/src/config.ts +676 -0
- package/src/demo/DemoBanner.tsx +54 -0
- package/src/demo/assets/demo-audio.mp3 +0 -0
- package/src/demo/assets/demo-clip.mp4 +0 -0
- package/src/demo/assets/demo-crop.mp4 +0 -0
- package/src/demo/assets/demo-poster.jpg +0 -0
- package/src/demo/assets/frame-1.jpg +0 -0
- package/src/demo/assets/frame-2.jpg +0 -0
- package/src/demo/assets/frame-3.jpg +0 -0
- package/src/demo/assets/frame-4.jpg +0 -0
- package/src/demo/assets/frame-5.jpg +0 -0
- package/src/demo/assets/frame-6.jpg +0 -0
- package/src/demo/assets/gen-1.jpg +0 -0
- package/src/demo/assets/gen-2.jpg +0 -0
- package/src/demo/assets.d.ts +14 -0
- package/src/demo/demo.css +64 -0
- package/src/demo/demoFetch.ts +305 -0
- package/src/demo/fixtures.ts +814 -0
- package/src/demo/index.ts +43 -0
- package/src/demo/mode.ts +81 -0
- package/src/demo/promptAssist.ts +181 -0
- package/src/demo/seed.ts +97 -0
- package/src/nav/BrandMark.css +66 -0
- package/src/nav/BrandMark.tsx +40 -0
- package/src/nav/Navbar.css +13 -0
- package/src/nav/Navbar.tsx +68 -0
- package/src/regions/SpatialRegionEditor.tsx +375 -0
- package/src/regions/TemporalRegionEditor.tsx +485 -0
- package/src/regions/types.ts +85 -0
- package/src/session.ts +85 -0
- package/src/share.ts +77 -0
- package/src/stations/ActivePanel.tsx +55 -0
- package/src/stations/ActiveProcessesStation.tsx +272 -0
- package/src/stations/ActiveProcessesStation.tsx.bak-sessions-20260812 +228 -0
- package/src/stations/AddFileBar.tsx +59 -0
- package/src/stations/AudioCropStation.tsx +157 -0
- package/src/stations/ComfyPanel.tsx +210 -0
- package/src/stations/ComfyStation.tsx +196 -0
- package/src/stations/CondensedKnobStrip.tsx +245 -0
- package/src/stations/ConsolePanel.tsx +82 -0
- package/src/stations/FrameExtractStation.tsx +148 -0
- package/src/stations/GenIdentityBar.tsx +412 -0
- package/src/stations/GenerateStation.tsx +5603 -0
- package/src/stations/GenerateStation.tsx.bak-tester-removal +5170 -0
- package/src/stations/GoalComposer.tsx +139 -0
- package/src/stations/ImageCropStation.tsx +153 -0
- package/src/stations/KnobInfo.tsx +64 -0
- package/src/stations/PlaceholderStation.tsx +26 -0
- package/src/stations/SectionTabs.tsx +276 -0
- package/src/stations/SharePanel.tsx +298 -0
- package/src/stations/SidebarRegistry.tsx +105 -0
- package/src/stations/StationShell.tsx +96 -0
- package/src/stations/StudioClipsStation.tsx +28 -0
- package/src/stations/WorkbenchDrawer.tsx +66 -0
- package/src/stations/WorkbenchSidebar.tsx +1224 -0
- package/src/stations/WorkbenchStation.tsx +91 -0
- package/src/stations/comfyEmbed.ts +79 -0
- package/src/stations/drawerShell.tsx +419 -0
- package/src/stations/ops/AudioCropCore.tsx +218 -0
- package/src/stations/ops/CharacterGroupsPanel.tsx +716 -0
- package/src/stations/ops/FrameExtractCore.tsx +695 -0
- package/src/stations/ops/ImageCropCore.tsx +164 -0
- package/src/stations/ops/VideoCropCore.tsx +193 -0
- package/src/stations/ops/useVideoCropJobs.ts +224 -0
- package/src/stations/pollCaps.ts +87 -0
- package/src/stations/registry.ts +191 -0
- package/src/stations/studio/AngleRingEditor.tsx +159 -0
- package/src/stations/studio/IdentitiesStation.tsx +2455 -0
- package/src/stations/studio/IdentityProfileControls.tsx +262 -0
- package/src/stations/studio/StudioActiveProcesses.tsx +224 -0
- package/src/stations/studio/StudioAssistLogPanel.tsx +220 -0
- package/src/stations/studio/StudioGenerateMode.tsx +29 -0
- package/src/stations/studio/StudioGenerateTab.tsx +2688 -0
- package/src/stations/studio/StudioItemCard.tsx +334 -0
- package/src/stations/studio/StudioLibraryTab.tsx +506 -0
- package/src/stations/studio/StudioMovieComposer.tsx +2875 -0
- package/src/stations/studio/StudioMovieComposer.tsx.bak-sessions-20260812 +2729 -0
- package/src/stations/studio/StudioMovieSessions.tsx +553 -0
- package/src/stations/studio/StudioMovieSessions.tsx.bak-sessions-20260812 +417 -0
- package/src/stations/studio/StudioPlane.tsx +302 -0
- package/src/stations/studio/StudioSplicedRow.tsx +476 -0
- package/src/stations/studio/StudioViewer.tsx +339 -0
- package/src/stations/studio/TurntableViewer.tsx +220 -0
- package/src/stations/studio/movieTimeline.ts +198 -0
- package/src/stations/studio/promptCard.tsx +498 -0
- package/src/stations/studio/studioItem.ts +87 -0
- package/src/stations/studio/studioShared.tsx +821 -0
- package/src/stations/studio/useIdentityProfiles.ts +575 -0
- package/src/stations/studio/useReconstruction.ts +283 -0
- package/src/stations/types.ts +43 -0
- package/src/stations/useAudioCropJobs.ts +202 -0
- package/src/stations/useAudioExtractJob.ts +144 -0
- package/src/stations/useCropJobs.ts +206 -0
- package/src/stations/useFrameJobs.ts +140 -0
- package/src/stations/useGenerateJob.ts +151 -0
- package/src/stations/useGenerateMovieJob.ts +177 -0
- package/src/stations/useGenerateRowJobs.ts +184 -0
- package/src/stations/useGenerateSceneJob.ts +156 -0
- package/src/stations/useGenerateSceneRowJobs.ts +282 -0
- package/src/style/app.css +4974 -0
- package/src/transport/client.ts +262 -0
- package/src/transport/telemetry.ts +97 -0
- package/src/video/DropReceptacle.tsx +122 -0
- package/src/video/PromptAssistButtons.tsx +118 -0
- package/src/video/composerBridge.ts +66 -0
- package/src/video/contract.ts +599 -0
- package/src/video/jobTracker.ts +702 -0
- package/src/video/mediaLibrary.ts +328 -0
- package/src/video/renderCompat.ts +512 -0
- package/src/video/sessionForm.ts +189 -0
- package/src/video/spreadAssist.ts +392 -0
- package/src/video/studioBridge.ts +82 -0
- package/src/video/useMediaJobs.ts +279 -0
- package/src/video/useModels.ts +413 -0
- package/src/video/useMoviePresets.ts +97 -0
- package/src/video/useMovieSessions.ts +292 -0
- package/src/video/usePresets.ts +167 -0
- package/src/video/useProjects.ts +87 -0
- package/src/video/usePromptAssist.ts +394 -0
- package/src/video/useRenderPresets.ts +221 -0
- package/src/video/useStudioAssistLog.ts +266 -0
- package/src/video/useStudioPresets.ts +128 -0
- package/src/video/useWorkerCalls.ts +165 -0
- package/tsconfig.json +17 -0
- package/vite.config.ts +33 -0
|
@@ -0,0 +1,814 @@
|
|
|
1
|
+
// Canned fixtures for the video-intelligence demo (?demo=1).
|
|
2
|
+
//
|
|
3
|
+
// Every MediaRef here points its `uri` at a Vite-BUNDLED asset URL rather than a
|
|
4
|
+
// server store key. That matters because <img>/<video> element loads do NOT go
|
|
5
|
+
// through window.fetch (the shim can't intercept them) — so in canned mode
|
|
6
|
+
// mediaBytesUrl() passes these absolute asset URLs straight through and the
|
|
7
|
+
// browser loads the bundled bytes directly. See config.ts.
|
|
8
|
+
import type { MediaRef } from "../video/contract";
|
|
9
|
+
|
|
10
|
+
// Real sample media dropped by the operator under assets/sampels/ (note: the dir
|
|
11
|
+
// is intentionally spelled "sampels" — do NOT rename it). Imported in place as
|
|
12
|
+
// Vite asset URLs (relative from src/demo/), which resolve to bundled hashed URLs.
|
|
13
|
+
import demoClipUrl from "../../assets/sampels/scenes/woman.mp4";
|
|
14
|
+
import demoCropUrl from "../../assets/sampels/scenes/man.mp4";
|
|
15
|
+
import demoAudioUrl from "./assets/demo-audio.mp3"; // no audio sample provided — kept
|
|
16
|
+
import frame1 from "../../assets/sampels/images/forrest/frame_00000.png";
|
|
17
|
+
import frame2 from "../../assets/sampels/images/forrest/frame_00001.png";
|
|
18
|
+
import frame3 from "../../assets/sampels/images/forrest/frame_00002.png";
|
|
19
|
+
import frame4 from "../../assets/sampels/images/forrest/frame_00003.png";
|
|
20
|
+
import frame5 from "../../assets/sampels/images/forrest/frame_00004.png";
|
|
21
|
+
import frame6 from "../../assets/sampels/images/forrest/frame_00005.png";
|
|
22
|
+
import gen1 from "../../assets/sampels/images/shenzo/frame_00000.png";
|
|
23
|
+
import gen2 from "../../assets/sampels/images/bycycle/frame_00000.png";
|
|
24
|
+
// The forrest scene's assembled clip (single .mp4 in that folder — hashed name).
|
|
25
|
+
import forrestSceneUrl from "../../assets/sampels/images/forrest/ff23090611b844a6b85c224f388bd542.mp4";
|
|
26
|
+
// The real 2-segment "keeper-matrix" sailboat movie (see movie.json for numbers).
|
|
27
|
+
import movieUrl from "../../assets/sampels/studio_movies/keeper-matrix/movie.mp4";
|
|
28
|
+
import movieSeg0Url from "../../assets/sampels/studio_movies/keeper-matrix/segment_00/64854c6a21d47ce10b23e884e12de448b25bfb0a375d33238dd0bec8056c076d/clip.mp4";
|
|
29
|
+
import movieSeg1Url from "../../assets/sampels/studio_movies/keeper-matrix/segment_01/cc926b44a42b90d7d291a4e50e3859458306c207a17c718086a3466e4049d6c6/clip.mp4";
|
|
30
|
+
import movieSeg1BranchUrl from "../../assets/sampels/studio_movies/keeper-matrix/segment_01/branch.png";
|
|
31
|
+
|
|
32
|
+
// ── the worked example: one ingested clip + derived assets ──────────────────
|
|
33
|
+
|
|
34
|
+
export const demoClip: MediaRef = {
|
|
35
|
+
asset_id: "demo-clip",
|
|
36
|
+
kind: "video",
|
|
37
|
+
uri: demoClipUrl, // scenes/woman.mp4
|
|
38
|
+
mime: "video/mp4",
|
|
39
|
+
width: 512,
|
|
40
|
+
height: 512,
|
|
41
|
+
duration_s: 4.0,
|
|
42
|
+
fps_native: 6,
|
|
43
|
+
sample_rate: null,
|
|
44
|
+
channels: null,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const demoCroppedClip: MediaRef = {
|
|
48
|
+
asset_id: "demo-clip-cropped",
|
|
49
|
+
kind: "video",
|
|
50
|
+
uri: demoCropUrl, // scenes/man.mp4
|
|
51
|
+
mime: "video/mp4",
|
|
52
|
+
width: 512,
|
|
53
|
+
height: 512,
|
|
54
|
+
duration_s: 3.333333,
|
|
55
|
+
fps_native: 6,
|
|
56
|
+
sample_rate: null,
|
|
57
|
+
channels: null,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const demoAudio: MediaRef = {
|
|
61
|
+
asset_id: "demo-audio",
|
|
62
|
+
kind: "audio",
|
|
63
|
+
uri: demoAudioUrl,
|
|
64
|
+
mime: "audio/mpeg",
|
|
65
|
+
width: null,
|
|
66
|
+
height: null,
|
|
67
|
+
duration_s: 3.0,
|
|
68
|
+
fps_native: null,
|
|
69
|
+
sample_rate: 44100,
|
|
70
|
+
channels: 1,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const demoPoster: MediaRef = {
|
|
74
|
+
asset_id: "demo-poster",
|
|
75
|
+
kind: "image",
|
|
76
|
+
uri: frame1, // images/forrest/frame_00000.png
|
|
77
|
+
mime: "image/png",
|
|
78
|
+
width: 512,
|
|
79
|
+
height: 512,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
function frameRef(url: string, i: number): MediaRef {
|
|
83
|
+
return {
|
|
84
|
+
asset_id: `demo-frame-${i}`,
|
|
85
|
+
kind: "image",
|
|
86
|
+
uri: url,
|
|
87
|
+
mime: "image/png",
|
|
88
|
+
width: 512,
|
|
89
|
+
height: 512,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export const demoFrames: MediaRef[] = [
|
|
94
|
+
frame1,
|
|
95
|
+
frame2,
|
|
96
|
+
frame3,
|
|
97
|
+
frame4,
|
|
98
|
+
frame5,
|
|
99
|
+
frame6,
|
|
100
|
+
].map(frameRef);
|
|
101
|
+
|
|
102
|
+
function genRef(url: string, i: number): MediaRef {
|
|
103
|
+
return {
|
|
104
|
+
asset_id: `demo-gen-${i}`,
|
|
105
|
+
kind: "image",
|
|
106
|
+
uri: url,
|
|
107
|
+
mime: "image/png",
|
|
108
|
+
width: 512,
|
|
109
|
+
height: 512,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Two real generated stills: shenzo/frame_00000 + bycycle/frame_00000.
|
|
114
|
+
export const demoGenerated: MediaRef[] = [gen1, gen2].map(genRef);
|
|
115
|
+
|
|
116
|
+
// The forrest scene's assembled clip — the LAST output of a generate_scene run
|
|
117
|
+
// (N frames then the clip). Real ffprobe: 512×512, 6fps, 6 frames, 1.0s.
|
|
118
|
+
export const demoSceneClip: MediaRef = {
|
|
119
|
+
asset_id: "demo-scene-clip",
|
|
120
|
+
kind: "video",
|
|
121
|
+
uri: forrestSceneUrl,
|
|
122
|
+
mime: "video/mp4",
|
|
123
|
+
width: 512,
|
|
124
|
+
height: 512,
|
|
125
|
+
duration_s: 1.0,
|
|
126
|
+
fps_native: 6,
|
|
127
|
+
sample_rate: null,
|
|
128
|
+
channels: null,
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// The assembled keeper-matrix movie — the terminal `movie:` MediaRef + the LAST
|
|
132
|
+
// output of a generate_movie run. From movie.json: 832×480, 16fps, 162 frames.
|
|
133
|
+
export const demoMovieClip: MediaRef = {
|
|
134
|
+
asset_id: "demo-movie",
|
|
135
|
+
kind: "video",
|
|
136
|
+
uri: movieUrl,
|
|
137
|
+
mime: "video/mp4",
|
|
138
|
+
width: 832,
|
|
139
|
+
height: 480,
|
|
140
|
+
duration_s: 10.125,
|
|
141
|
+
fps_native: 16,
|
|
142
|
+
sample_rate: null,
|
|
143
|
+
channels: null,
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// The two per-segment clips (used as segment "frames" in the movie scaffolding).
|
|
147
|
+
// Both from movie.json: 832×480, 16fps, 81 frames, 5.0625s.
|
|
148
|
+
function movieSegClipRef(url: string, i: number): MediaRef {
|
|
149
|
+
return {
|
|
150
|
+
asset_id: `demo-movie-seg-${i}`,
|
|
151
|
+
kind: "video",
|
|
152
|
+
uri: url,
|
|
153
|
+
mime: "video/mp4",
|
|
154
|
+
width: 832,
|
|
155
|
+
height: 480,
|
|
156
|
+
duration_s: 5.0625,
|
|
157
|
+
fps_native: 16,
|
|
158
|
+
sample_rate: null,
|
|
159
|
+
channels: null,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export const demoMovieSeg0Clip: MediaRef = movieSegClipRef(movieSeg0Url, 0);
|
|
164
|
+
export const demoMovieSeg1Clip: MediaRef = movieSegClipRef(movieSeg1Url, 1);
|
|
165
|
+
|
|
166
|
+
// The parent branch still for segment 1 (branch.png): 832×480.
|
|
167
|
+
export const demoMovieSeg1Branch: MediaRef = {
|
|
168
|
+
asset_id: "demo-movie-seg-1-branch",
|
|
169
|
+
kind: "image",
|
|
170
|
+
uri: movieSeg1BranchUrl,
|
|
171
|
+
mime: "image/png",
|
|
172
|
+
width: 832,
|
|
173
|
+
height: 480,
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// ── model registry / task defaults (Generate + Frames model dropdowns) ──────
|
|
177
|
+
|
|
178
|
+
export const V1_MODELS = {
|
|
179
|
+
object: "list",
|
|
180
|
+
data: [
|
|
181
|
+
{
|
|
182
|
+
id: "sd-turbo",
|
|
183
|
+
object: "model",
|
|
184
|
+
created: 0,
|
|
185
|
+
owned_by: "hugpy",
|
|
186
|
+
hub_id: "stabilityai/sd-turbo",
|
|
187
|
+
task: "text-to-image",
|
|
188
|
+
// Dual capability → prompt required, start image OPTIONAL (present ⇒ img2img).
|
|
189
|
+
tasks: ["text-to-image", "image-to-image"],
|
|
190
|
+
context_length: 77,
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
id: "sdxl-base",
|
|
194
|
+
object: "model",
|
|
195
|
+
created: 0,
|
|
196
|
+
owned_by: "hugpy",
|
|
197
|
+
hub_id: "stabilityai/stable-diffusion-xl-base-1.0",
|
|
198
|
+
task: "text-to-image",
|
|
199
|
+
// Pure text-to-image → prompt required, NO start-image slot (image "none").
|
|
200
|
+
tasks: ["text-to-image"],
|
|
201
|
+
context_length: 77,
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
id: "Qwen-Image-Edit-2509",
|
|
205
|
+
object: "model",
|
|
206
|
+
created: 0,
|
|
207
|
+
owned_by: "hugpy",
|
|
208
|
+
hub_id: "Qwen/Qwen-Image-Edit-2509",
|
|
209
|
+
task: "image-to-image",
|
|
210
|
+
// Edit-only → prompt AND start image REQUIRED (no text-only path).
|
|
211
|
+
tasks: ["image-to-image"],
|
|
212
|
+
context_length: 0,
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
id: "Qwen2.5-3B-Instruct-GGUF",
|
|
216
|
+
object: "model",
|
|
217
|
+
created: 0,
|
|
218
|
+
owned_by: "hugpy",
|
|
219
|
+
hub_id: "Qwen/Qwen2.5-3B-Instruct-GGUF",
|
|
220
|
+
task: "text-generation",
|
|
221
|
+
context_length: 32768,
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
id: "Qwen2.5-VL-3B-Instruct-GGUF",
|
|
225
|
+
object: "model",
|
|
226
|
+
created: 0,
|
|
227
|
+
owned_by: "hugpy",
|
|
228
|
+
hub_id: "Qwen/Qwen2.5-VL-3B-Instruct-GGUF",
|
|
229
|
+
// A vision-language model — the Movie director's judge (scores each segment).
|
|
230
|
+
task: "image-text-to-text",
|
|
231
|
+
tasks: ["image-text-to-text"],
|
|
232
|
+
context_length: 32768,
|
|
233
|
+
},
|
|
234
|
+
],
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
export const PROMPT_TASKS = {
|
|
238
|
+
tasks: ["text-to-image", "text-generation"],
|
|
239
|
+
defaults: { "text-to-image": "sd-turbo", "text-generation": "Qwen2.5-3B-Instruct-GGUF" },
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
// ── curated presets (Generate-station "ideal default loads" dropdown) ────────
|
|
243
|
+
// The GET /video/presets payload + a canned /video/presets/<id>/apply response, so
|
|
244
|
+
// ?demo=1 exercises the picker without a backend. Shapes match the API contract.
|
|
245
|
+
|
|
246
|
+
export const PRESETS = {
|
|
247
|
+
presets: [
|
|
248
|
+
{
|
|
249
|
+
id: "sd-turbo-square",
|
|
250
|
+
name: "SD-Turbo · fast square",
|
|
251
|
+
description:
|
|
252
|
+
"A single 512² still in 4 steps, guidance-free — the quickest path to an image.",
|
|
253
|
+
mode: "text-to-image",
|
|
254
|
+
model_key: "sd-turbo",
|
|
255
|
+
defaults: {
|
|
256
|
+
strength: 0.45,
|
|
257
|
+
steps: 4,
|
|
258
|
+
guidance: 0,
|
|
259
|
+
width: 512,
|
|
260
|
+
height: 512,
|
|
261
|
+
n_frames: 6,
|
|
262
|
+
fps: 6,
|
|
263
|
+
negative: "",
|
|
264
|
+
},
|
|
265
|
+
recommended: "gpu",
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
id: "sd-turbo-scene",
|
|
269
|
+
name: "SD-Turbo · 6-frame scene",
|
|
270
|
+
description:
|
|
271
|
+
"A short chained scene — 6 consecutive frames at 6fps, assembled into an mp4.",
|
|
272
|
+
mode: "edit-chain",
|
|
273
|
+
model_key: "sd-turbo",
|
|
274
|
+
defaults: {
|
|
275
|
+
strength: 0.45,
|
|
276
|
+
steps: 6,
|
|
277
|
+
guidance: 0,
|
|
278
|
+
width: 512,
|
|
279
|
+
height: 512,
|
|
280
|
+
n_frames: 6,
|
|
281
|
+
fps: 6,
|
|
282
|
+
negative: "",
|
|
283
|
+
},
|
|
284
|
+
recommended: "gpu",
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
id: "flux-klein-edit",
|
|
288
|
+
name: "flux-klein · image edit",
|
|
289
|
+
description:
|
|
290
|
+
"img2img edit chain — needs a start image; higher strength for bolder edits.",
|
|
291
|
+
mode: "img2img",
|
|
292
|
+
model_key: "sd-turbo",
|
|
293
|
+
defaults: {
|
|
294
|
+
strength: 0.6,
|
|
295
|
+
steps: 8,
|
|
296
|
+
guidance: 2.5,
|
|
297
|
+
width: 768,
|
|
298
|
+
height: 768,
|
|
299
|
+
n_frames: 4,
|
|
300
|
+
fps: 4,
|
|
301
|
+
negative: "blurry, low quality",
|
|
302
|
+
},
|
|
303
|
+
recommended: "gpu",
|
|
304
|
+
},
|
|
305
|
+
],
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
export function applyPreset(id: string):
|
|
309
|
+
| {
|
|
310
|
+
ok: true;
|
|
311
|
+
worker: { name: string; id: string };
|
|
312
|
+
model_key: string;
|
|
313
|
+
mode: string;
|
|
314
|
+
defaults: (typeof PRESETS.presets)[number]["defaults"];
|
|
315
|
+
warming: true;
|
|
316
|
+
}
|
|
317
|
+
| { ok: false; error: { code: string; message: string } } {
|
|
318
|
+
const preset = PRESETS.presets.find((p) => p.id === id);
|
|
319
|
+
if (!preset) {
|
|
320
|
+
return { ok: false, error: { code: "unknown_preset", message: `No preset "${id}".` } };
|
|
321
|
+
}
|
|
322
|
+
return {
|
|
323
|
+
ok: true,
|
|
324
|
+
worker: { name: "demo-gpu", id: "demo" },
|
|
325
|
+
model_key: preset.model_key,
|
|
326
|
+
mode: preset.mode,
|
|
327
|
+
defaults: preset.defaults,
|
|
328
|
+
warming: true,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// ── curated MOVIE TEMPLATES (Movie-tab dropdown) ─────────────────────────────
|
|
333
|
+
// The GET /movie/presets payload (a BARE ARRAY — the movie contract, distinct from
|
|
334
|
+
// the {presets:[…]} video envelope) + a canned /movie/presets/<id>/apply echo, so
|
|
335
|
+
// ?demo=1 exercises the template picker without a backend. Six ship. Each carries
|
|
336
|
+
// its WHOLE contiguous goal timeline; picking one drops it into the Movie editor.
|
|
337
|
+
export const MOVIE_PRESETS = [
|
|
338
|
+
{
|
|
339
|
+
id: "golden-hour",
|
|
340
|
+
name: "Golden Hour",
|
|
341
|
+
description:
|
|
342
|
+
"A valley from midday to blazing sunset — four contiguous goals, warm light climbing.",
|
|
343
|
+
model_key: "sd-turbo",
|
|
344
|
+
width: 512,
|
|
345
|
+
height: 512,
|
|
346
|
+
steps: 6,
|
|
347
|
+
guidance: 0,
|
|
348
|
+
fps: 8,
|
|
349
|
+
chain: true,
|
|
350
|
+
goals: [
|
|
351
|
+
{ start_frame: 0, end_frame: 8, prompt: "wide valley at midday, flat neutral light" },
|
|
352
|
+
{ start_frame: 8, end_frame: 16, prompt: "afternoon light warming, long soft shadows" },
|
|
353
|
+
{ start_frame: 16, end_frame: 24, prompt: "golden hour, sun low, amber rim light" },
|
|
354
|
+
{ start_frame: 24, end_frame: 32, prompt: "sunset, sky ablaze orange and pink" },
|
|
355
|
+
],
|
|
356
|
+
vision_enabled: false,
|
|
357
|
+
score_threshold: 70,
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
id: "four-seasons",
|
|
361
|
+
name: "Four Seasons",
|
|
362
|
+
description:
|
|
363
|
+
"One landscape cycled through spring, summer, autumn, winter — four goals, chained.",
|
|
364
|
+
model_key: "sd-turbo",
|
|
365
|
+
width: 512,
|
|
366
|
+
height: 512,
|
|
367
|
+
steps: 6,
|
|
368
|
+
guidance: 0,
|
|
369
|
+
fps: 6,
|
|
370
|
+
chain: true,
|
|
371
|
+
goals: [
|
|
372
|
+
{ start_frame: 0, end_frame: 6, prompt: "green meadow in spring, blossom on the trees" },
|
|
373
|
+
{ start_frame: 6, end_frame: 12, prompt: "same meadow in summer, lush and bright" },
|
|
374
|
+
{ start_frame: 12, end_frame: 18, prompt: "same meadow in autumn, amber and red leaves" },
|
|
375
|
+
{ start_frame: 18, end_frame: 24, prompt: "same meadow in winter, bare trees and snow" },
|
|
376
|
+
],
|
|
377
|
+
vision_enabled: false,
|
|
378
|
+
score_threshold: 70,
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
id: "rose-bloom",
|
|
382
|
+
name: "Rose Bloom",
|
|
383
|
+
description:
|
|
384
|
+
"A single rose opening in macro — tight bud to full bloom over four goals.",
|
|
385
|
+
model_key: "sd-turbo",
|
|
386
|
+
width: 512,
|
|
387
|
+
height: 512,
|
|
388
|
+
steps: 8,
|
|
389
|
+
guidance: 1.5,
|
|
390
|
+
fps: 10,
|
|
391
|
+
chain: true,
|
|
392
|
+
goals: [
|
|
393
|
+
{ start_frame: 0, end_frame: 8, prompt: "macro of a tight red rose bud, dewy" },
|
|
394
|
+
{ start_frame: 8, end_frame: 16, prompt: "the bud loosening, outer petals parting" },
|
|
395
|
+
{ start_frame: 16, end_frame: 24, prompt: "the rose half open, petals unfurling" },
|
|
396
|
+
{ start_frame: 24, end_frame: 32, prompt: "the rose in full bloom, petals wide" },
|
|
397
|
+
],
|
|
398
|
+
vision_enabled: true,
|
|
399
|
+
score_threshold: 75,
|
|
400
|
+
},
|
|
401
|
+
{
|
|
402
|
+
id: "storm-front",
|
|
403
|
+
name: "Storm Front",
|
|
404
|
+
description:
|
|
405
|
+
"A clear sky building into a lightning storm — four goals of gathering weather.",
|
|
406
|
+
model_key: "sd-turbo",
|
|
407
|
+
width: 512,
|
|
408
|
+
height: 512,
|
|
409
|
+
steps: 6,
|
|
410
|
+
guidance: 0,
|
|
411
|
+
fps: 8,
|
|
412
|
+
chain: true,
|
|
413
|
+
goals: [
|
|
414
|
+
{ start_frame: 0, end_frame: 8, prompt: "wide plain under a clear blue sky" },
|
|
415
|
+
{ start_frame: 8, end_frame: 16, prompt: "clouds building on the horizon, wind rising" },
|
|
416
|
+
{ start_frame: 16, end_frame: 24, prompt: "dark storm clouds overhead, rain sheeting down" },
|
|
417
|
+
{ start_frame: 24, end_frame: 32, prompt: "lightning splitting the sky over the plain" },
|
|
418
|
+
],
|
|
419
|
+
vision_enabled: false,
|
|
420
|
+
score_threshold: 70,
|
|
421
|
+
},
|
|
422
|
+
{
|
|
423
|
+
id: "anime-day",
|
|
424
|
+
name: "Anime Day Cycle",
|
|
425
|
+
description:
|
|
426
|
+
"An anime city through dawn, noon, dusk and night — four goals, stylized.",
|
|
427
|
+
model_key: "sd-turbo",
|
|
428
|
+
width: 512,
|
|
429
|
+
height: 512,
|
|
430
|
+
steps: 6,
|
|
431
|
+
guidance: 1,
|
|
432
|
+
fps: 6,
|
|
433
|
+
chain: true,
|
|
434
|
+
goals: [
|
|
435
|
+
{ start_frame: 0, end_frame: 6, prompt: "anime city skyline at dawn, soft pink light" },
|
|
436
|
+
{ start_frame: 6, end_frame: 12, prompt: "anime city skyline at noon, bright blue sky" },
|
|
437
|
+
{ start_frame: 12, end_frame: 18, prompt: "anime city skyline at dusk, orange glow" },
|
|
438
|
+
{ start_frame: 18, end_frame: 24, prompt: "anime city skyline at night, neon and stars" },
|
|
439
|
+
],
|
|
440
|
+
vision_enabled: false,
|
|
441
|
+
score_threshold: 70,
|
|
442
|
+
},
|
|
443
|
+
{
|
|
444
|
+
id: "cosmic-zoom",
|
|
445
|
+
name: "Cosmic Zoom",
|
|
446
|
+
description:
|
|
447
|
+
"A pull-back from a planet's surface out to a spiral galaxy — four goals.",
|
|
448
|
+
model_key: "sd-turbo",
|
|
449
|
+
width: 512,
|
|
450
|
+
height: 512,
|
|
451
|
+
steps: 6,
|
|
452
|
+
guidance: 0,
|
|
453
|
+
fps: 10,
|
|
454
|
+
chain: true,
|
|
455
|
+
goals: [
|
|
456
|
+
{ start_frame: 0, end_frame: 8, prompt: "rugged alien planet surface, red rock" },
|
|
457
|
+
{ start_frame: 8, end_frame: 16, prompt: "the planet seen from low orbit, curved horizon" },
|
|
458
|
+
{ start_frame: 16, end_frame: 24, prompt: "the solar system, the planet a small disc" },
|
|
459
|
+
{ start_frame: 24, end_frame: 32, prompt: "a vast spiral galaxy in deep space" },
|
|
460
|
+
],
|
|
461
|
+
vision_enabled: false,
|
|
462
|
+
score_threshold: 70,
|
|
463
|
+
},
|
|
464
|
+
] as const;
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Canned /movie/presets/<id>/apply — the movie contract echoes the SAME full
|
|
468
|
+
* template object the list returns (or null for an unknown id → the router 404s).
|
|
469
|
+
*/
|
|
470
|
+
export function applyMoviePreset(
|
|
471
|
+
id: string,
|
|
472
|
+
): (typeof MOVIE_PRESETS)[number] | null {
|
|
473
|
+
return MOVIE_PRESETS.find((p) => p.id === id) ?? null;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// ── job lifecycle: enqueue → staged polls → done with per-kind outputs ───────
|
|
477
|
+
|
|
478
|
+
export type JobKind =
|
|
479
|
+
| "crop"
|
|
480
|
+
| "frame_extract"
|
|
481
|
+
| "audio_extract"
|
|
482
|
+
| "generate_image"
|
|
483
|
+
| "generate_scene"
|
|
484
|
+
| "generate_movie";
|
|
485
|
+
|
|
486
|
+
interface CannedJob {
|
|
487
|
+
kind: JobKind;
|
|
488
|
+
enqueuedAt: number;
|
|
489
|
+
sourceKind: string | null; // MediaRef.kind of the request source, when sent
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const jobs = new Map<string, CannedJob>();
|
|
493
|
+
let seq = 0;
|
|
494
|
+
|
|
495
|
+
export function enqueueJob(kind: JobKind, sourceKind: string | null): string {
|
|
496
|
+
seq += 1;
|
|
497
|
+
const id = `demo-job-${kind}-${seq}`;
|
|
498
|
+
jobs.set(id, { kind, enqueuedAt: Date.now(), sourceKind });
|
|
499
|
+
return id;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function outputsFor(job: CannedJob): MediaRef[] {
|
|
503
|
+
switch (job.kind) {
|
|
504
|
+
case "crop":
|
|
505
|
+
// image crop → a still; video crop → the cropped clip
|
|
506
|
+
return job.sourceKind === "image" ? [demoFrames[0]] : [demoCroppedClip];
|
|
507
|
+
case "frame_extract":
|
|
508
|
+
return demoFrames;
|
|
509
|
+
case "audio_extract":
|
|
510
|
+
return [demoAudio];
|
|
511
|
+
case "generate_image":
|
|
512
|
+
return [demoGenerated[0]];
|
|
513
|
+
case "generate_scene":
|
|
514
|
+
// The 6 forrest frames + the assembled forrest scene clip LAST (scene contract).
|
|
515
|
+
return [...demoFrames, demoSceneClip];
|
|
516
|
+
case "generate_movie":
|
|
517
|
+
// Per-segment clips (as segment "frames") + the assembled movie.mp4 LAST
|
|
518
|
+
// (movie contract). The real keeper-matrix movie has 2 segments.
|
|
519
|
+
return [demoMovieSeg0Clip, demoMovieSeg1Clip, demoMovieClip];
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const cancelledJobs = new Set<string>();
|
|
524
|
+
|
|
525
|
+
export function cancelJob(id: string): { job_id: string; status: string | null; cancelled: boolean } {
|
|
526
|
+
if (!jobs.has(id)) return { job_id: id, status: null, cancelled: false };
|
|
527
|
+
cancelledJobs.add(id);
|
|
528
|
+
return { job_id: id, status: "cancelled", cancelled: true };
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// The image-only outputs (drops any trailing assembled clip) — the frames a live
|
|
532
|
+
// gallery fills in, and the `total` the running readout counts toward.
|
|
533
|
+
function imageOutputsFor(job: CannedJob): MediaRef[] {
|
|
534
|
+
return outputsFor(job).filter((m) => m.kind === "image");
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// The live-progress shape the pinned contract's `progress` block carries. Local to
|
|
538
|
+
// the demo — the real payload is validated by jobProgressSchema on the UI side.
|
|
539
|
+
interface DemoProgress {
|
|
540
|
+
done: number;
|
|
541
|
+
total: number;
|
|
542
|
+
stage: string;
|
|
543
|
+
label: string;
|
|
544
|
+
model: string;
|
|
545
|
+
frames: MediaRef[];
|
|
546
|
+
started_at: number;
|
|
547
|
+
eta_s: number | null;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
interface DemoProject {
|
|
551
|
+
name: string | null;
|
|
552
|
+
uuid: string;
|
|
553
|
+
dir: string;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const PROGRESS_STAGES = ["loading model", "denoising", "decoding", "encoding"];
|
|
557
|
+
|
|
558
|
+
// Age-derived progress: done climbs 0→total across the run, the completed frames
|
|
559
|
+
// accrue one-by-one, the stage walks the pipeline, and eta_s counts down — so
|
|
560
|
+
// ?demo=1 exercises the descriptive readout AND the live-fill gallery.
|
|
561
|
+
function progressFor(job: CannedJob, age: number, runFor: number): DemoProgress {
|
|
562
|
+
const imgs = imageOutputsFor(job);
|
|
563
|
+
const total = Math.max(1, imgs.length);
|
|
564
|
+
const frac = Math.max(0, Math.min(1, (age - 600) / (runFor - 600)));
|
|
565
|
+
const done = Math.min(total, Math.floor(frac * total));
|
|
566
|
+
const isScene = job.kind === "generate_scene";
|
|
567
|
+
return {
|
|
568
|
+
done,
|
|
569
|
+
total,
|
|
570
|
+
stage: PROGRESS_STAGES[
|
|
571
|
+
Math.min(PROGRESS_STAGES.length - 1, Math.floor(frac * PROGRESS_STAGES.length))
|
|
572
|
+
],
|
|
573
|
+
label:
|
|
574
|
+
done >= total
|
|
575
|
+
? "finalizing…"
|
|
576
|
+
: isScene
|
|
577
|
+
? `generating frame ${done + 1}`
|
|
578
|
+
: "generating image",
|
|
579
|
+
model: "sd-turbo",
|
|
580
|
+
frames: imgs.slice(0, done),
|
|
581
|
+
started_at: Math.floor(job.enqueuedAt / 1000),
|
|
582
|
+
eta_s: Math.max(0, Math.round((runFor - age) / 1000)),
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// The auto-archive project echoed on a terminal generation result (name set so the
|
|
587
|
+
// demo exercises the "Saved to <name> · <dir>" surface).
|
|
588
|
+
function projectFor(job: CannedJob): DemoProject {
|
|
589
|
+
const slug =
|
|
590
|
+
job.kind === "generate_movie"
|
|
591
|
+
? "demo-movie"
|
|
592
|
+
: job.kind === "generate_scene"
|
|
593
|
+
? "demo-scene"
|
|
594
|
+
: "demo-image";
|
|
595
|
+
return { name: "Demo project", uuid: `demo-proj-${slug}`, dir: `assets/${slug}` };
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// ── movie job: the NESTED progress blob + the terminal movie ledger ──────────
|
|
599
|
+
// The REAL 2-segment "keeper-matrix" sailboat movie (see movie.json). Each goal's
|
|
600
|
+
// `frames` are the real per-segment clip(s) so the live grids + per-segment strip
|
|
601
|
+
// paint actual media; seg0 0–81, seg1 81–162 (half-open, tiling [0,162)).
|
|
602
|
+
const DEMO_MOVIE_GOALS = [
|
|
603
|
+
{
|
|
604
|
+
goal: {
|
|
605
|
+
start_frame: 0,
|
|
606
|
+
end_frame: 81,
|
|
607
|
+
prompt: "a small wooden sailboat on a bright blue lake, gentle waves, sunny day",
|
|
608
|
+
},
|
|
609
|
+
frames: [demoMovieSeg0Clip],
|
|
610
|
+
},
|
|
611
|
+
{
|
|
612
|
+
goal: {
|
|
613
|
+
start_frame: 81,
|
|
614
|
+
end_frame: 162,
|
|
615
|
+
prompt: "the same lake at sunset, boat sailing away",
|
|
616
|
+
},
|
|
617
|
+
frames: [demoMovieSeg1Branch, demoMovieSeg1Clip],
|
|
618
|
+
},
|
|
619
|
+
];
|
|
620
|
+
|
|
621
|
+
interface DemoMovieSegment {
|
|
622
|
+
index: number;
|
|
623
|
+
goal: { start_frame: number; end_frame: number; prompt: string };
|
|
624
|
+
prompt: string;
|
|
625
|
+
attempt: number;
|
|
626
|
+
score: number | null;
|
|
627
|
+
status: string;
|
|
628
|
+
frames: MediaRef[];
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
interface DemoMovieProgress {
|
|
632
|
+
stage: string;
|
|
633
|
+
segment_done: number;
|
|
634
|
+
segment_total: number;
|
|
635
|
+
segments: DemoMovieSegment[];
|
|
636
|
+
current: DemoProgress | null;
|
|
637
|
+
started_at: number;
|
|
638
|
+
eta_s: number | null;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// Age-derived NESTED movie progress: segment_done climbs 0→total, each segment
|
|
642
|
+
// walks pending→generating→scoring→done with a score badge that fills in, and
|
|
643
|
+
// `current` mirrors the ACTIVE segment's per-frame render (fed to progressReadout).
|
|
644
|
+
function movieProgressFor(job: CannedJob, age: number, runFor: number): DemoMovieProgress {
|
|
645
|
+
const total = DEMO_MOVIE_GOALS.length;
|
|
646
|
+
const frac = Math.max(0, Math.min(1, (age - 600) / (runFor - 600)));
|
|
647
|
+
const segDone = Math.min(total, Math.floor(frac * total));
|
|
648
|
+
const startedAt = Math.floor(job.enqueuedAt / 1000);
|
|
649
|
+
const eta = Math.max(0, Math.round((runFor - age) / 1000));
|
|
650
|
+
const segments: DemoMovieSegment[] = DEMO_MOVIE_GOALS.map((gm, i) => {
|
|
651
|
+
if (i < segDone)
|
|
652
|
+
return {
|
|
653
|
+
index: i,
|
|
654
|
+
goal: gm.goal,
|
|
655
|
+
prompt: gm.goal.prompt,
|
|
656
|
+
attempt: i === 1 ? 2 : 1,
|
|
657
|
+
score: 74 + i * 6,
|
|
658
|
+
status: "done",
|
|
659
|
+
frames: gm.frames,
|
|
660
|
+
};
|
|
661
|
+
if (i === segDone) {
|
|
662
|
+
const scoring = ((frac * total) % 1) > 0.5;
|
|
663
|
+
return {
|
|
664
|
+
index: i,
|
|
665
|
+
goal: gm.goal,
|
|
666
|
+
prompt: gm.goal.prompt,
|
|
667
|
+
attempt: 1,
|
|
668
|
+
score: scoring ? 63 : null,
|
|
669
|
+
status: scoring ? "scoring" : "generating",
|
|
670
|
+
frames: gm.frames.slice(0, 1),
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
return {
|
|
674
|
+
index: i,
|
|
675
|
+
goal: gm.goal,
|
|
676
|
+
prompt: gm.goal.prompt,
|
|
677
|
+
attempt: 0,
|
|
678
|
+
score: null,
|
|
679
|
+
status: "pending",
|
|
680
|
+
frames: [],
|
|
681
|
+
};
|
|
682
|
+
});
|
|
683
|
+
const active = segDone < total ? DEMO_MOVIE_GOALS[segDone] : null;
|
|
684
|
+
const current: DemoProgress | null = active
|
|
685
|
+
? {
|
|
686
|
+
done: 1,
|
|
687
|
+
total: active.frames.length,
|
|
688
|
+
stage: "denoising",
|
|
689
|
+
label: `segment ${segDone + 1}`,
|
|
690
|
+
model: "sd-turbo",
|
|
691
|
+
frames: active.frames.slice(0, 1),
|
|
692
|
+
started_at: startedAt,
|
|
693
|
+
eta_s: eta,
|
|
694
|
+
}
|
|
695
|
+
: null;
|
|
696
|
+
return {
|
|
697
|
+
stage: segDone >= total ? "assembling" : "generating",
|
|
698
|
+
segment_done: segDone,
|
|
699
|
+
segment_total: total,
|
|
700
|
+
segments,
|
|
701
|
+
current,
|
|
702
|
+
started_at: startedAt,
|
|
703
|
+
eta_s: eta,
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// The terminal `movie` ledger echoed on a done generate_movie result (mirrors the
|
|
708
|
+
// contract: per-segment chosen take / attempts / scores / why + the assembled clip).
|
|
709
|
+
function movieResultFor(job: CannedJob): {
|
|
710
|
+
ok: true;
|
|
711
|
+
error: null;
|
|
712
|
+
outputs: MediaRef[];
|
|
713
|
+
project: DemoProject;
|
|
714
|
+
movie: Record<string, unknown>;
|
|
715
|
+
} {
|
|
716
|
+
const goals = DEMO_MOVIE_GOALS.map((g) => g.goal);
|
|
717
|
+
return {
|
|
718
|
+
// ok/error are REQUIRED by jobResultSchema — include them so the terminal movie
|
|
719
|
+
// result actually parses (and the finished preview renders) under ?demo=1.
|
|
720
|
+
ok: true,
|
|
721
|
+
error: null,
|
|
722
|
+
outputs: outputsFor(job),
|
|
723
|
+
project: projectFor(job),
|
|
724
|
+
movie: {
|
|
725
|
+
goals,
|
|
726
|
+
drift: { mean: 0.12, max: 0.28 },
|
|
727
|
+
vision_enabled: true,
|
|
728
|
+
score_threshold: 70,
|
|
729
|
+
n_frames_total: 162,
|
|
730
|
+
segments: goals.map((g, i) => ({
|
|
731
|
+
index: i,
|
|
732
|
+
goal: g,
|
|
733
|
+
prompt: g.prompt,
|
|
734
|
+
seed: 1000 + i,
|
|
735
|
+
attempts: i === 1 ? 2 : 1,
|
|
736
|
+
scores: i === 1 ? [62, 81] : [74 + i * 6],
|
|
737
|
+
chosen_take: i === 1 ? 1 : 0,
|
|
738
|
+
status: "done",
|
|
739
|
+
why:
|
|
740
|
+
i === 1
|
|
741
|
+
? "re-rolled: first take scored below threshold"
|
|
742
|
+
: "passed on the first take",
|
|
743
|
+
mp4: null,
|
|
744
|
+
})),
|
|
745
|
+
movie: demoMovieClip,
|
|
746
|
+
},
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// Stage the lifecycle on wall-clock so the console's poller sees a believable
|
|
751
|
+
// progression: unclaimed → running (with live progress) → done. generate_* takes a
|
|
752
|
+
// bit longer, and carries the progress block + a terminal project.
|
|
753
|
+
export function jobStatus(id: string): {
|
|
754
|
+
job_id: string;
|
|
755
|
+
status: string | null;
|
|
756
|
+
result: Record<string, unknown> | null;
|
|
757
|
+
progress?: unknown;
|
|
758
|
+
} {
|
|
759
|
+
const job = jobs.get(id);
|
|
760
|
+
if (!job) {
|
|
761
|
+
// Belt-and-braces: an id the shim never minted (e.g. a job-tracker record that
|
|
762
|
+
// somehow leaked in from a prior live visit) must NOT resolve to status:null —
|
|
763
|
+
// that drives the sidebar's "server no longer tracks this job — it may have
|
|
764
|
+
// expired" row, which has no place in a hermetic brochure. Answer any unknown
|
|
765
|
+
// id as a CLEAN completed image generation instead, so the row reads as finished
|
|
766
|
+
// work (its output is a bundled still) and never as an error. The primary guard
|
|
767
|
+
// is the demo-namespaced tracker storage key (jobTracker.ts) — this is the
|
|
768
|
+
// second line of defense so no leaked id can ever surface the expired path.
|
|
769
|
+
return {
|
|
770
|
+
job_id: id,
|
|
771
|
+
status: "done",
|
|
772
|
+
result: { outputs: [demoGenerated[0]] },
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
if (cancelledJobs.has(id)) {
|
|
776
|
+
return { job_id: id, status: "cancelled",
|
|
777
|
+
result: { error: { message: "cancelled" } } };
|
|
778
|
+
}
|
|
779
|
+
const age = Date.now() - job.enqueuedAt;
|
|
780
|
+
const isGen = job.kind.startsWith("generate");
|
|
781
|
+
const isMovie = job.kind === "generate_movie";
|
|
782
|
+
// A movie is many segments — give it a longer believable run than a scene/image.
|
|
783
|
+
const runFor = isMovie ? 5200 : isGen ? 3500 : 1800;
|
|
784
|
+
// Unclaimed window: the job is enqueued but no worker has picked it up yet. The
|
|
785
|
+
// REAL bus reports this as "queued" (a live, non-terminal status), NOT a null
|
|
786
|
+
// status — and the jobTracker reads a null poll as "the server forgot this job"
|
|
787
|
+
// and marks the record EXPIRED (the "server no longer tracks this job" row). So a
|
|
788
|
+
// freshly-tracked job whose first (immediate) poll lands here must see "queued",
|
|
789
|
+
// or it wrongly expires before it ever runs. (This also matches the contract the
|
|
790
|
+
// sidebar spinner expects for a just-enqueued job.)
|
|
791
|
+
if (age < 600) return { job_id: id, status: "queued", result: null };
|
|
792
|
+
if (age < runFor) {
|
|
793
|
+
return {
|
|
794
|
+
job_id: id,
|
|
795
|
+
status: "running",
|
|
796
|
+
result: null,
|
|
797
|
+
progress: isMovie
|
|
798
|
+
? movieProgressFor(job, age, runFor)
|
|
799
|
+
: isGen
|
|
800
|
+
? progressFor(job, age, runFor)
|
|
801
|
+
: null,
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
return {
|
|
805
|
+
job_id: id,
|
|
806
|
+
status: "done",
|
|
807
|
+
result: isMovie
|
|
808
|
+
? movieResultFor(job)
|
|
809
|
+
: {
|
|
810
|
+
outputs: outputsFor(job),
|
|
811
|
+
...(isGen ? { project: projectFor(job) } : {}),
|
|
812
|
+
},
|
|
813
|
+
};
|
|
814
|
+
}
|