@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,599 @@
|
|
|
1
|
+
// The frozen hugpy video-intelligence backend contract, in one typed module.
|
|
2
|
+
// Mirrors hugpy_video_intelligence_map.md — a backend is being built to this in
|
|
3
|
+
// parallel, so these shapes are the source of truth the UI serialises to and
|
|
4
|
+
// parses from. zod schemas validate the two responses the UI actually reads
|
|
5
|
+
// (ingest → MediaRef, job poll → JobRecord) so contract drift surfaces as a clean
|
|
6
|
+
// message instead of an undefined-property crash under strictNullChecks-off.
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import type { SpatialRegion, TemporalRegion } from "../regions/types";
|
|
9
|
+
|
|
10
|
+
// A nullable numeric MediaRef field. The contract says "numbers may be null"; we
|
|
11
|
+
// also tolerate the key being absent, and preserve extra unknown keys via
|
|
12
|
+
// passthrough so a MediaRef round-trips untouched back into a crop request.
|
|
13
|
+
const nullableNumber = z.number().nullable().optional();
|
|
14
|
+
|
|
15
|
+
export const mediaRefSchema = z
|
|
16
|
+
.object({
|
|
17
|
+
asset_id: z.string(),
|
|
18
|
+
kind: z.string(),
|
|
19
|
+
uri: z.string(),
|
|
20
|
+
mime: z.string(),
|
|
21
|
+
/** For an image, the native pixel dims — the space CropSpec regions live in. */
|
|
22
|
+
width: nullableNumber,
|
|
23
|
+
height: nullableNumber,
|
|
24
|
+
duration_s: nullableNumber,
|
|
25
|
+
fps_native: nullableNumber,
|
|
26
|
+
sample_rate: nullableNumber,
|
|
27
|
+
channels: nullableNumber,
|
|
28
|
+
})
|
|
29
|
+
.passthrough();
|
|
30
|
+
|
|
31
|
+
/** A resolved media asset. Native `width`/`height` anchor the spatial pixel space. */
|
|
32
|
+
export type MediaRef = z.infer<typeof mediaRefSchema>;
|
|
33
|
+
|
|
34
|
+
/** POST /uploads → the server file handle used to ingest. */
|
|
35
|
+
export const uploadResultSchema = z
|
|
36
|
+
.object({
|
|
37
|
+
path: z.string(),
|
|
38
|
+
name: z.string(),
|
|
39
|
+
size: z.number(),
|
|
40
|
+
})
|
|
41
|
+
.passthrough();
|
|
42
|
+
export type UploadResult = z.infer<typeof uploadResultSchema>;
|
|
43
|
+
|
|
44
|
+
/** POST /video/jobs/crop request body. Exactly one axis is set per station today. */
|
|
45
|
+
export interface CropJobRequest {
|
|
46
|
+
source: MediaRef;
|
|
47
|
+
spatial: SpatialRegion | null;
|
|
48
|
+
temporal: TemporalRegion | null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** POST /video/jobs/crop → the enqueued job's id. */
|
|
52
|
+
export const enqueueResultSchema = z
|
|
53
|
+
.object({ job_id: z.string() })
|
|
54
|
+
.passthrough();
|
|
55
|
+
export type EnqueueResult = z.infer<typeof enqueueResultSchema>;
|
|
56
|
+
|
|
57
|
+
/** The frame image encodings the frame-extract job accepts. */
|
|
58
|
+
export const FRAME_FORMATS = ["jpg", "png", "webp"] as const;
|
|
59
|
+
export type FrameFormat = (typeof FRAME_FORMATS)[number];
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* POST /video/jobs/frame_extract request body. ONE job produces MANY frame
|
|
63
|
+
* MediaRefs (polled back via jobRecordSchema → result.outputs, already an array).
|
|
64
|
+
* `window` null = the whole video; `max_frames` null = uncapped. `quality`
|
|
65
|
+
* semantics differ by fmt (jpg/webp 1..100, png 0..9) — the station surfaces the
|
|
66
|
+
* valid range per selected fmt rather than silently clamping.
|
|
67
|
+
*/
|
|
68
|
+
export interface FrameExtractRequest {
|
|
69
|
+
source: MediaRef;
|
|
70
|
+
fps: number;
|
|
71
|
+
quality: number;
|
|
72
|
+
fmt: FrameFormat;
|
|
73
|
+
window: TemporalRegion | null;
|
|
74
|
+
max_frames: number | null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The audio encodings the audio-extract job can emit. Default (station) is wav. */
|
|
78
|
+
export const AUDIO_FORMATS = ["wav", "mp3", "m4a"] as const;
|
|
79
|
+
export type AudioFormat = (typeof AUDIO_FORMATS)[number];
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* POST /video/jobs/audio_extract request body. Pulls the audio track out of a
|
|
83
|
+
* video `source` into ONE standalone audio MediaRef (polled back via
|
|
84
|
+
* jobRecordSchema → result.outputs[0]). `fmt` is the output container/codec; the
|
|
85
|
+
* station defaults it to "wav" (lossless and trivially decodable for the
|
|
86
|
+
* waveform). Mirrors FrameExtractRequest's explicit shape — no silent defaults on
|
|
87
|
+
* the wire. The temporal crop that follows reuses the EXISTING crop endpoint
|
|
88
|
+
* (CropJobRequest with spatial: null + a temporal window).
|
|
89
|
+
*/
|
|
90
|
+
export interface AudioExtractRequest {
|
|
91
|
+
source: MediaRef;
|
|
92
|
+
fmt: AudioFormat;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* POST /video/jobs/generate_image — one ordered multimodal prompt part.
|
|
97
|
+
* The prompt is an ORDERED list: text segments, images from the library (picked
|
|
98
|
+
* frames or prior generations), and optional raw video parts the backend chain
|
|
99
|
+
* resolves into frames (uniform-N sampling). Order is preserved by insertion.
|
|
100
|
+
*/
|
|
101
|
+
export type GenPromptPart =
|
|
102
|
+
| { kind: "text"; text: string }
|
|
103
|
+
| { kind: "image"; media: MediaRef }
|
|
104
|
+
| { kind: "video"; media: MediaRef };
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* POST /video/jobs/generate_image request body. `parts` is the ordered prompt;
|
|
108
|
+
* `model_id` comes from the shared image-model dropdown (task text-to-image).
|
|
109
|
+
* All knobs are explicit numbers the station renders and the user can edit;
|
|
110
|
+
* `seed` null = random, `negative` null = none. The station guards that at least
|
|
111
|
+
* one non-empty text part exists before enabling Run (a prompt with no text part
|
|
112
|
+
* fails backend-side with "no_prompt"). Polled back via jobRecordSchema →
|
|
113
|
+
* result.outputs[0] = the generated image MediaRef.
|
|
114
|
+
*/
|
|
115
|
+
export interface GenerateImageRequest {
|
|
116
|
+
parts: GenPromptPart[];
|
|
117
|
+
model_id: string;
|
|
118
|
+
width: number;
|
|
119
|
+
height: number;
|
|
120
|
+
steps: number;
|
|
121
|
+
guidance: number;
|
|
122
|
+
seed: number | null;
|
|
123
|
+
negative: string | null;
|
|
124
|
+
/**
|
|
125
|
+
* Optional auto-archive project NAME. When set, the backend files the output
|
|
126
|
+
* under assets/<project> and echoes the resolved project on result.project.
|
|
127
|
+
* Blank/omitted = an auto-named folder.
|
|
128
|
+
*/
|
|
129
|
+
project?: string;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* ONE row of the image TESTER (Generate station, Image mode).
|
|
134
|
+
*
|
|
135
|
+
* The point of the tester is COMPARISON: settings belong to a prompt, not to the
|
|
136
|
+
* station, so you can sit two prompts side by side with different steps/guidance/
|
|
137
|
+
* seed/model and see the difference. Each row therefore carries its OWN complete
|
|
138
|
+
* knob set — a row is a whole `GenerateImageRequest` waiting to happen, and "Run
|
|
139
|
+
* all" fires one job per row.
|
|
140
|
+
*
|
|
141
|
+
* Knobs are held as STRINGS because these are live text inputs (blank seed =
|
|
142
|
+
* random, blank negative = none); they are parsed into a `GenerateImageRequest`
|
|
143
|
+
* only at submit, exactly as the base composer already does. A row that fails to
|
|
144
|
+
* parse is skipped rather than submitted with silently coerced numbers.
|
|
145
|
+
*
|
|
146
|
+
* `media` is deliberately NOT per-row: rows inherit the base composer's image /
|
|
147
|
+
* video / identity parts, so a tester run holds the reference material fixed and
|
|
148
|
+
* varies only what you are testing. Give a row its own text and its own knobs;
|
|
149
|
+
* everything else stays comparable by construction.
|
|
150
|
+
*/
|
|
151
|
+
export interface ImageTestRow {
|
|
152
|
+
/** Stable client-side id (React key + jobs-by-row map key). Never sent. */
|
|
153
|
+
id: string;
|
|
154
|
+
/** This row's prompt text. Combined with the base composer's media parts. */
|
|
155
|
+
text: string;
|
|
156
|
+
model_id: string;
|
|
157
|
+
width: string;
|
|
158
|
+
height: string;
|
|
159
|
+
steps: string;
|
|
160
|
+
guidance: string;
|
|
161
|
+
/** Blank = random. */
|
|
162
|
+
seed: string;
|
|
163
|
+
/** Blank = none. */
|
|
164
|
+
negative: string;
|
|
165
|
+
/**
|
|
166
|
+
* PER-SETTING CARRY TICKS (operator ask 2026-08-04, k63) — which of this row's
|
|
167
|
+
* settings are carried into the next component seeded FROM this one ("+ Add
|
|
168
|
+
* row" / "+ Row per model" / "+ Row per preset"). An unticked setting is NOT
|
|
169
|
+
* copied: the new row falls back to the mode default instead.
|
|
170
|
+
*
|
|
171
|
+
* CLIENT-SIDE ONLY. Like `id`, this is editor bookkeeping — it is never
|
|
172
|
+
* serialized into a GenerateImageRequest and never reaches the backend (see
|
|
173
|
+
* runnableTestRows in GenerateStation.tsx, which reads only the knob fields).
|
|
174
|
+
*/
|
|
175
|
+
carry: CarryTicks;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* The settings a carry tick can cover. `size` is width+height as ONE tick — they
|
|
180
|
+
* are edited as one "size W × H" control, so they carry together or not at all.
|
|
181
|
+
*/
|
|
182
|
+
export type CarrySettingKey =
|
|
183
|
+
| "model"
|
|
184
|
+
| "size"
|
|
185
|
+
| "steps"
|
|
186
|
+
| "guidance"
|
|
187
|
+
| "seed"
|
|
188
|
+
| "negative";
|
|
189
|
+
|
|
190
|
+
/** Tick state for every carryable setting. Client-side only — never sent. */
|
|
191
|
+
export type CarryTicks = Record<CarrySettingKey, boolean>;
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* POST /video/jobs/generate_scene request body (Generate station, Scene mode).
|
|
195
|
+
* ONE ordered multimodal prompt → N consecutive image frames forming a scene +
|
|
196
|
+
* (when `assemble`) one assembled mp4 clip. Shares the prompt/model/knob shape of
|
|
197
|
+
* GenerateImageRequest and adds the scene-only knobs: `n_frames` (REQUIRED,
|
|
198
|
+
* explicit; backend caps at 24 → HTTP 400 "frame_cap_exceeded"), `fps` (REQUIRED,
|
|
199
|
+
* explicit), `assemble` (build the mp4 from the frames), and `motion` (an
|
|
200
|
+
* optional per-frame progression template that may contain {i}/{n}). Polled back
|
|
201
|
+
* via jobRecordSchema → result.outputs = N image refs, then (if assemble) ONE
|
|
202
|
+
* video ref LAST — the SAME status seam generate_image uses.
|
|
203
|
+
*
|
|
204
|
+
* Image-to-image conditioning: the FIRST image part in `parts` is the scene's
|
|
205
|
+
* start frame. `strength` is the img2img denoising strength (0..1, null = backend
|
|
206
|
+
* default of 0.45); lower keeps more of the start frame. `chain` (default true)
|
|
207
|
+
* conditions each frame on the previous one (sequential img2img). Until the fleet
|
|
208
|
+
* ships an img2img-capable worker a chained request may fail retryably with
|
|
209
|
+
* "image-to-image not available on the fleet" — surfaced via the normal job-error
|
|
210
|
+
* path.
|
|
211
|
+
*/
|
|
212
|
+
export interface GenerateSceneRequest {
|
|
213
|
+
parts: GenPromptPart[];
|
|
214
|
+
model_id: string;
|
|
215
|
+
width: number;
|
|
216
|
+
height: number;
|
|
217
|
+
steps: number;
|
|
218
|
+
guidance: number;
|
|
219
|
+
n_frames: number;
|
|
220
|
+
fps: number;
|
|
221
|
+
assemble: boolean;
|
|
222
|
+
strength: number | null;
|
|
223
|
+
chain: boolean;
|
|
224
|
+
seed: number | null;
|
|
225
|
+
motion: string | null;
|
|
226
|
+
negative: string | null;
|
|
227
|
+
/**
|
|
228
|
+
* Optional auto-archive project NAME. When set, the backend files the scene's
|
|
229
|
+
* frames + clip under assets/<project> and echoes it on result.project.
|
|
230
|
+
* Blank/omitted = an auto-named folder.
|
|
231
|
+
*/
|
|
232
|
+
project?: string;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* ONE goal row of a Movie's GOAL TIMELINE (Generate station, Movie mode). The
|
|
237
|
+
* timeline is an ORDERED, CONTIGUOUS list of goals that tile the whole clip:
|
|
238
|
+
* `start_frame`..`end_frame` is HALF-OPEN ([start,end)), goal[0] starts at 0, and
|
|
239
|
+
* each goal begins exactly where the previous one ends (no gaps/overlaps) so the
|
|
240
|
+
* union is [0,total). `prompt` describes what that interval should depict; `ref`
|
|
241
|
+
* is an optional per-goal reference/start image the backend conditions on.
|
|
242
|
+
*/
|
|
243
|
+
export const movieGoalSchema = z
|
|
244
|
+
.object({
|
|
245
|
+
start_frame: z.number(),
|
|
246
|
+
end_frame: z.number(),
|
|
247
|
+
prompt: z.string(),
|
|
248
|
+
ref: mediaRefSchema.optional(),
|
|
249
|
+
// Per-goal PROMPT-COMPONENT overrides (k92): each goal IS a prompt component
|
|
250
|
+
// and may carry its OWN generation knobs, mirroring Scene's per-part settings.
|
|
251
|
+
// ALL optional — an omitted field means the backend INHERITS the movie-level
|
|
252
|
+
// (GenerateMovieRequest) value, so a movie with no overrides is byte-for-byte
|
|
253
|
+
// the old payload. `motion` is goal-only (the movie request has no shared one).
|
|
254
|
+
model_id: z.string().optional(),
|
|
255
|
+
width: z.number().optional(),
|
|
256
|
+
height: z.number().optional(),
|
|
257
|
+
steps: z.number().optional(),
|
|
258
|
+
guidance: z.number().optional(),
|
|
259
|
+
seed: z.number().nullable().optional(),
|
|
260
|
+
negative: z.string().nullable().optional(),
|
|
261
|
+
strength: z.number().nullable().optional(),
|
|
262
|
+
chain: z.boolean().optional(),
|
|
263
|
+
motion: z.string().nullable().optional(),
|
|
264
|
+
})
|
|
265
|
+
.passthrough();
|
|
266
|
+
export type MovieGoal = z.infer<typeof movieGoalSchema>;
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* POST /video/jobs/generate_movie request body (Generate station, Movie mode). A
|
|
270
|
+
* GOAL TIMELINE (`goals`, contiguous, tiling [0,total)) drives an N-segment movie:
|
|
271
|
+
* each goal becomes one segment the backend renders into consecutive frames, then
|
|
272
|
+
* (when `assemble`) an mp4 clip is built LAST. The scene-template knobs
|
|
273
|
+
* (`model_id`/dims/`steps`/`guidance`/`fps`/`assemble` + optional
|
|
274
|
+
* `seed`/`negative`/`strength`/`chain`/`project`) are REUSED verbatim from Scene
|
|
275
|
+
* mode. The director knobs are OPT-IN: with `vision_enabled` the backend scores
|
|
276
|
+
* each segment with a judge VLM (`judge_model_id`) and re-rolls a segment until it
|
|
277
|
+
* clears `score_threshold` (0-100) or hits `max_attempts_per_segment` /
|
|
278
|
+
* `time_budget_s`. Polled back via jobRecordSchema → a NESTED movie progress
|
|
279
|
+
* (movieProgressSchema) while running, then result.outputs = segment frames then
|
|
280
|
+
* the movie.mp4 LAST (+ result.movie metadata).
|
|
281
|
+
*/
|
|
282
|
+
export interface GenerateMovieRequest {
|
|
283
|
+
goals: MovieGoal[];
|
|
284
|
+
model_id: string;
|
|
285
|
+
width: number;
|
|
286
|
+
height: number;
|
|
287
|
+
steps: number;
|
|
288
|
+
guidance: number;
|
|
289
|
+
fps: number;
|
|
290
|
+
assemble: boolean;
|
|
291
|
+
/** Scene-template optionals (reused from GenerateSceneRequest). */
|
|
292
|
+
seed?: number | null;
|
|
293
|
+
negative?: string | null;
|
|
294
|
+
strength?: number | null;
|
|
295
|
+
chain?: boolean;
|
|
296
|
+
project?: string;
|
|
297
|
+
/** Director knobs — opt-in vision scoring / re-roll loop. */
|
|
298
|
+
vision_enabled?: boolean;
|
|
299
|
+
score_threshold?: number;
|
|
300
|
+
max_attempts_per_segment?: number;
|
|
301
|
+
judge_model_id?: string;
|
|
302
|
+
time_budget_s?: number;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* ONE goal of a curated MOVIE TEMPLATE's shot list — a lighter cousin of
|
|
307
|
+
* movieGoalSchema (no per-goal `ref`; a template ships prompt-only goals). The
|
|
308
|
+
* timeline is still the contiguous [start,end) tiling the Movie editor loads.
|
|
309
|
+
*/
|
|
310
|
+
export const moviePresetGoalSchema = z
|
|
311
|
+
.object({
|
|
312
|
+
start_frame: z.number(),
|
|
313
|
+
end_frame: z.number(),
|
|
314
|
+
prompt: z.string(),
|
|
315
|
+
})
|
|
316
|
+
.passthrough();
|
|
317
|
+
export type MoviePresetGoal = z.infer<typeof moviePresetGoalSchema>;
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* A curated MOVIE TEMPLATE (Generate station, Movie tab). GET /movie/presets → a
|
|
321
|
+
* BARE ARRAY of these (NOT the {presets:[…]} envelope video presets use), and
|
|
322
|
+
* POST /movie/presets/<id>/apply echoes the SAME object. Unlike a video preset
|
|
323
|
+
* (knob-only "ideal default load"), a movie preset carries the WHOLE shot list —
|
|
324
|
+
* picking one drops a ready `goals` timeline + the shared knobs (model/dims/steps/
|
|
325
|
+
* guidance/fps/chain) + opt-in director defaults into the editor, ready to run or
|
|
326
|
+
* tweak. Tolerant/passthrough: numeric knobs are nullable+optional (a template may
|
|
327
|
+
* omit any — the editor keeps its current value), only id/name/model_key/goals are
|
|
328
|
+
* asserted.
|
|
329
|
+
*/
|
|
330
|
+
export const moviePresetSchema = z
|
|
331
|
+
.object({
|
|
332
|
+
id: z.string(),
|
|
333
|
+
name: z.string(),
|
|
334
|
+
description: z.string().nullable().optional(),
|
|
335
|
+
model_key: z.string(),
|
|
336
|
+
width: z.number().nullable().optional(),
|
|
337
|
+
height: z.number().nullable().optional(),
|
|
338
|
+
steps: z.number().nullable().optional(),
|
|
339
|
+
guidance: z.number().nullable().optional(),
|
|
340
|
+
fps: z.number().nullable().optional(),
|
|
341
|
+
chain: z.boolean().nullable().optional(),
|
|
342
|
+
strength: z.number().nullable().optional(),
|
|
343
|
+
negative: z.string().nullable().optional(),
|
|
344
|
+
goals: z.array(moviePresetGoalSchema),
|
|
345
|
+
vision_enabled: z.boolean().nullable().optional(),
|
|
346
|
+
score_threshold: z.number().nullable().optional(),
|
|
347
|
+
})
|
|
348
|
+
.passthrough();
|
|
349
|
+
export type MoviePreset = z.infer<typeof moviePresetSchema>;
|
|
350
|
+
|
|
351
|
+
export const JOB_STATUSES = [
|
|
352
|
+
"queued",
|
|
353
|
+
"claimed",
|
|
354
|
+
"running",
|
|
355
|
+
"cancelling", // cancel requested; a running scene honors it between frames
|
|
356
|
+
"cancelled",
|
|
357
|
+
"done",
|
|
358
|
+
"failed",
|
|
359
|
+
] as const;
|
|
360
|
+
export type JobStatus = (typeof JOB_STATUSES)[number] | null;
|
|
361
|
+
|
|
362
|
+
export const jobErrorSchema = z
|
|
363
|
+
.object({
|
|
364
|
+
code: z.string(),
|
|
365
|
+
message: z.string(),
|
|
366
|
+
retryable: z.boolean(),
|
|
367
|
+
})
|
|
368
|
+
.passthrough();
|
|
369
|
+
export type JobError = z.infer<typeof jobErrorSchema>;
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* The auto-archive project a terminal generation was saved into. Present on a
|
|
373
|
+
* `done` result for generate_image/generate_scene: `name` is the user-supplied
|
|
374
|
+
* label (null when the backend auto-named), `dir` is the on-disk folder the
|
|
375
|
+
* frames + clip were written to (the "Saved to …" surface), `uuid` a stable id.
|
|
376
|
+
*/
|
|
377
|
+
export const jobProjectSchema = z
|
|
378
|
+
.object({
|
|
379
|
+
name: z.string().nullable(),
|
|
380
|
+
uuid: z.string(),
|
|
381
|
+
dir: z.string(),
|
|
382
|
+
})
|
|
383
|
+
.passthrough();
|
|
384
|
+
export type JobProject = z.infer<typeof jobProjectSchema>;
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* The terminal `movie` metadata block a generate_movie run echoes on its result —
|
|
388
|
+
* the per-segment ledger (chosen take, attempts, scores, why) + the assembled
|
|
389
|
+
* clip + overall stats. Tolerant/passthrough: the UI renders the movie mostly off
|
|
390
|
+
* result.outputs (frames + movie.mp4 last) + result.project, so every field here
|
|
391
|
+
* is optional — this schema exists so the block round-trips + is inspectable.
|
|
392
|
+
*/
|
|
393
|
+
export const movieResultSchema = z
|
|
394
|
+
.object({
|
|
395
|
+
goals: z.array(movieGoalSchema).optional(),
|
|
396
|
+
drift: z.unknown().optional(),
|
|
397
|
+
vision_enabled: z.boolean().optional(),
|
|
398
|
+
score_threshold: z.number().nullable().optional(),
|
|
399
|
+
n_frames_total: z.number().optional(),
|
|
400
|
+
segments: z
|
|
401
|
+
.array(
|
|
402
|
+
z
|
|
403
|
+
.object({
|
|
404
|
+
index: z.number(),
|
|
405
|
+
goal: z.unknown().optional(),
|
|
406
|
+
prompt: z.string().optional(),
|
|
407
|
+
seed: z.number().nullable().optional(),
|
|
408
|
+
attempts: z.number().optional(),
|
|
409
|
+
// scores carries null for UNSCORED takes (vision off) — must allow null
|
|
410
|
+
scores: z.array(z.number().nullable()).optional(),
|
|
411
|
+
chosen_take: z.number().nullable().optional(),
|
|
412
|
+
status: z.string().optional(),
|
|
413
|
+
why: z.string().nullable().optional(),
|
|
414
|
+
// backend writes mp4 as a relative FILENAME string (e.g. "segment_00/
|
|
415
|
+
// video.mp4"), not a MediaRef — accept either, tolerantly.
|
|
416
|
+
mp4: z.union([z.string(), mediaRefSchema]).nullable().optional(),
|
|
417
|
+
})
|
|
418
|
+
.passthrough(),
|
|
419
|
+
)
|
|
420
|
+
.optional(),
|
|
421
|
+
// manifest movie is the FILENAME string "movie.mp4" (the playable clip rides
|
|
422
|
+
// in result.outputs as a MediaRef) — accept string or MediaRef.
|
|
423
|
+
movie: z.union([z.string(), mediaRefSchema]).nullable().optional(),
|
|
424
|
+
})
|
|
425
|
+
.passthrough();
|
|
426
|
+
export type MovieResult = z.infer<typeof movieResultSchema>;
|
|
427
|
+
|
|
428
|
+
export const jobResultSchema = z
|
|
429
|
+
.object({
|
|
430
|
+
ok: z.boolean(),
|
|
431
|
+
outputs: z.array(mediaRefSchema),
|
|
432
|
+
error: jobErrorSchema.nullable(),
|
|
433
|
+
/** Auto-archive destination (generation kinds); absent/null otherwise. */
|
|
434
|
+
project: jobProjectSchema.nullable().optional(),
|
|
435
|
+
/** Movie-mode ledger (generate_movie only); absent/null otherwise. */
|
|
436
|
+
movie: movieResultSchema.nullable().optional(),
|
|
437
|
+
})
|
|
438
|
+
.passthrough();
|
|
439
|
+
export type JobResult = z.infer<typeof jobResultSchema>;
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Live per-run progress a job reports WHILE running (null/absent at terminal).
|
|
443
|
+
* `done`/`total` drive the "image {done}/{total}" readout; `stage`+`label`
|
|
444
|
+
* describe the current step; `frames` are completed outputs so far (SAME MediaRef
|
|
445
|
+
* shape as result.outputs — rendered with the existing frame renderer so a scene
|
|
446
|
+
* gallery fills in live); `started_at` (epoch seconds) anchors the elapsed clock;
|
|
447
|
+
* `eta_s` is the backend's remaining-time estimate when it has one.
|
|
448
|
+
*/
|
|
449
|
+
export const jobProgressSchema = z
|
|
450
|
+
.object({
|
|
451
|
+
done: z.number(),
|
|
452
|
+
total: z.number(),
|
|
453
|
+
stage: z.string(),
|
|
454
|
+
label: z.string().optional(),
|
|
455
|
+
model: z.string().optional(),
|
|
456
|
+
frames: z.array(mediaRefSchema).optional(),
|
|
457
|
+
started_at: z.number().optional(),
|
|
458
|
+
eta_s: z.number().nullable().optional(),
|
|
459
|
+
})
|
|
460
|
+
.passthrough();
|
|
461
|
+
export type JobProgress = z.infer<typeof jobProgressSchema>;
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* One segment's live state inside a movie's NESTED progress. `index` is the goal's
|
|
465
|
+
* position; `goal` echoes the source goal (tolerated as unknown — the UI derives a
|
|
466
|
+
* label from `prompt`/`index`); `attempt` is the 1-based re-roll count; `score` is
|
|
467
|
+
* the latest judge score (0-100, null before scoring / when vision is off);
|
|
468
|
+
* `status` walks pending→generating→scoring→done (skipped/resumed for the vision
|
|
469
|
+
* loop); `frames` are the take's frames so far (SAME MediaRef shape as outputs).
|
|
470
|
+
*/
|
|
471
|
+
export const movieSegmentProgressSchema = z
|
|
472
|
+
.object({
|
|
473
|
+
index: z.number(),
|
|
474
|
+
goal: z.unknown().optional(),
|
|
475
|
+
prompt: z.string().optional(),
|
|
476
|
+
attempt: z.number().optional(),
|
|
477
|
+
score: z.number().nullable().optional(),
|
|
478
|
+
status: z.string().optional(),
|
|
479
|
+
frames: z.array(mediaRefSchema).optional(),
|
|
480
|
+
})
|
|
481
|
+
.passthrough();
|
|
482
|
+
export type MovieSegmentProgress = z.infer<typeof movieSegmentProgressSchema>;
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* The NESTED progress a generate_movie job reports WHILE running — the key shape
|
|
486
|
+
* difference from a flat scene job. `segment_done`/`segment_total` drive the
|
|
487
|
+
* overall "segment d/t" line; `segments[]` is the per-segment strip; `current` is
|
|
488
|
+
* a flat JobProgress-shaped view of the ACTIVE segment's frame render (fed VERBATIM
|
|
489
|
+
* to the shipped progressReadout + live-frame grid). `stage` names the movie-level
|
|
490
|
+
* step (loading/generating/scoring/assembling/archiving).
|
|
491
|
+
*/
|
|
492
|
+
export const movieProgressSchema = z
|
|
493
|
+
.object({
|
|
494
|
+
stage: z.string(),
|
|
495
|
+
segment_done: z.number(),
|
|
496
|
+
segment_total: z.number(),
|
|
497
|
+
segments: z.array(movieSegmentProgressSchema),
|
|
498
|
+
current: jobProgressSchema.nullable().optional(),
|
|
499
|
+
started_at: z.number().optional(),
|
|
500
|
+
eta_s: z.number().nullable().optional(),
|
|
501
|
+
})
|
|
502
|
+
.passthrough();
|
|
503
|
+
export type MovieProgress = z.infer<typeof movieProgressSchema>;
|
|
504
|
+
|
|
505
|
+
/** Either progress shape a poll may carry (flat scene/image OR nested movie). */
|
|
506
|
+
export type AnyProgress = JobProgress | MovieProgress;
|
|
507
|
+
|
|
508
|
+
/** GET /video/jobs/<id> → the current status + terminal result of one job. */
|
|
509
|
+
export const jobRecordSchema = z
|
|
510
|
+
.object({
|
|
511
|
+
job_id: z.string(),
|
|
512
|
+
status: z.enum(JOB_STATUSES).nullable(),
|
|
513
|
+
result: jobResultSchema.nullable(),
|
|
514
|
+
/**
|
|
515
|
+
* Live progress while running; null/absent at terminal. A UNION of the flat
|
|
516
|
+
* JobProgress (image/scene) and the nested MovieProgress (movie) — discriminated
|
|
517
|
+
* by the presence of `segments`. A movie blob carries no top-level done/total so
|
|
518
|
+
* jobProgressSchema rejects it, and a flat blob carries no segment_* so
|
|
519
|
+
* movieProgressSchema rejects it — the union is unambiguous either order. Both
|
|
520
|
+
* members passthrough, and `.nullable().optional()` keeps applyPoll's
|
|
521
|
+
* kind-agnostic `progress: jr.progress ?? null` line working unchanged.
|
|
522
|
+
*/
|
|
523
|
+
progress: z
|
|
524
|
+
.union([jobProgressSchema, movieProgressSchema])
|
|
525
|
+
.nullable()
|
|
526
|
+
.optional(),
|
|
527
|
+
})
|
|
528
|
+
.passthrough();
|
|
529
|
+
export type JobRecord = z.infer<typeof jobRecordSchema>;
|
|
530
|
+
|
|
531
|
+
/** Narrow a poll's progress union to the nested movie shape (presence of segments). */
|
|
532
|
+
export function isMovieProgress(p: AnyProgress): p is MovieProgress {
|
|
533
|
+
return (
|
|
534
|
+
typeof p === "object" &&
|
|
535
|
+
p != null &&
|
|
536
|
+
"segments" in p &&
|
|
537
|
+
Array.isArray((p as { segments?: unknown }).segments)
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/** Terminal states — polling stops here. */
|
|
542
|
+
export function isTerminal(status: JobStatus): boolean {
|
|
543
|
+
return status === "done" || status === "failed" || status === "cancelled";
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Flatten an ordered multimodal prompt's TEXT parts into one human-readable
|
|
548
|
+
* summary string (media parts are dropped). Used to annotate a generation's
|
|
549
|
+
* library outputs with the prompt that produced them (session-library provenance)
|
|
550
|
+
* and to re-stage that prompt into the composer for Replicate / Continue. Purely
|
|
551
|
+
* client-side derivation — the wire contract is untouched.
|
|
552
|
+
*/
|
|
553
|
+
export function promptSummary(parts: GenPromptPart[]): string {
|
|
554
|
+
return parts
|
|
555
|
+
.filter(
|
|
556
|
+
(p): p is Extract<GenPromptPart, { kind: "text" }> => p.kind === "text",
|
|
557
|
+
)
|
|
558
|
+
.map((p) => p.text.trim())
|
|
559
|
+
.filter((t) => t !== "")
|
|
560
|
+
.join(" ")
|
|
561
|
+
.trim();
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
export const MAX_SOURCE_IMAGES = 12;
|
|
565
|
+
export const MAX_CANONICAL_IMAGES = 4;
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* A discrete angular view in an angle-ring reconstruction.
|
|
569
|
+
*/
|
|
570
|
+
export const identityAngleViewSchema = z.object({
|
|
571
|
+
view_id: z.string(),
|
|
572
|
+
azimuth_deg: z.number(),
|
|
573
|
+
elevation_deg: z.number().optional(),
|
|
574
|
+
image_uri: z.string().nullable().optional(),
|
|
575
|
+
status: z.enum(["pending", "generating", "generated", "approved", "rejected", "failed"]).optional(),
|
|
576
|
+
version: z.number().optional(),
|
|
577
|
+
seed: z.number().nullable().optional(),
|
|
578
|
+
error: z.string().nullable().optional(),
|
|
579
|
+
}).passthrough();
|
|
580
|
+
|
|
581
|
+
export type IdentityAngleView = z.infer<typeof identityAngleViewSchema>;
|
|
582
|
+
|
|
583
|
+
export const identityReconstructionMeshSchema = z.object({
|
|
584
|
+
status: z.enum(["none", "queued", "running", "completed", "failed"]),
|
|
585
|
+
glb_uri: z.string().optional(),
|
|
586
|
+
error: z.string().optional(),
|
|
587
|
+
}).passthrough();
|
|
588
|
+
|
|
589
|
+
export const identityReconstructionSchema = z.object({
|
|
590
|
+
recon_id: z.string(),
|
|
591
|
+
mode: z.enum(["sheet", "turntable", "angle-ring"]).optional(),
|
|
592
|
+
views: z.array(z.any()).optional(), // Lenient: accepts both legacy strings & new view objects
|
|
593
|
+
mesh: identityReconstructionMeshSchema.optional(),
|
|
594
|
+
created_at: z.number().optional(),
|
|
595
|
+
degrees_per_frame: z.number().optional(),
|
|
596
|
+
frame_count: z.number().optional(),
|
|
597
|
+
}).passthrough();
|
|
598
|
+
|
|
599
|
+
export type IdentityReconstruction = z.infer<typeof identityReconstructionSchema>;
|