@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.
Files changed (177) hide show
  1. package/LICENSE +41 -0
  2. package/assets/sampels/images/bycycle/bd8603bad35d4f179f4d1134d8faf2ee.mp4 +0 -0
  3. package/assets/sampels/images/bycycle/frame_00000.png +0 -0
  4. package/assets/sampels/images/bycycle/frame_00001.png +0 -0
  5. package/assets/sampels/images/bycycle/frame_00002.png +0 -0
  6. package/assets/sampels/images/bycycle/frame_00003.png +0 -0
  7. package/assets/sampels/images/bycycle/frame_00004.png +0 -0
  8. package/assets/sampels/images/bycycle/frame_00005.png +0 -0
  9. package/assets/sampels/images/forrest/ff23090611b844a6b85c224f388bd542.mp4 +0 -0
  10. package/assets/sampels/images/forrest/frame_00000.png +0 -0
  11. package/assets/sampels/images/forrest/frame_00001.png +0 -0
  12. package/assets/sampels/images/forrest/frame_00002.png +0 -0
  13. package/assets/sampels/images/forrest/frame_00003.png +0 -0
  14. package/assets/sampels/images/forrest/frame_00004.png +0 -0
  15. package/assets/sampels/images/forrest/frame_00005.png +0 -0
  16. package/assets/sampels/images/shenzo/e1a4fcd623084ab4a986d11c8cd00db8.mp4 +0 -0
  17. package/assets/sampels/images/shenzo/frame_00000.png +0 -0
  18. package/assets/sampels/images/shenzo/frame_00001.png +0 -0
  19. package/assets/sampels/images/shenzo/frame_00002.png +0 -0
  20. package/assets/sampels/images/shenzo/frame_00003.png +0 -0
  21. package/assets/sampels/images/shenzo/frame_00004.png +0 -0
  22. package/assets/sampels/images/shenzo/frame_00005.png +0 -0
  23. package/assets/sampels/scenes/man.mp4 +0 -0
  24. package/assets/sampels/scenes/woman.mp4 +0 -0
  25. package/assets/sampels/studio_movies/caverns/_assembly/contrib_00.mp4 +0 -0
  26. package/assets/sampels/studio_movies/caverns/movie.json +67 -0
  27. package/assets/sampels/studio_movies/caverns/movie.mp4 +0 -0
  28. package/assets/sampels/studio_movies/caverns/segment_00/5eea7770a554fef0c3bdc38f8f38b925dcf75f13c015d423e8eadec47d41bf6b/clip.mp4 +0 -0
  29. package/assets/sampels/studio_movies/caverns/segment_00/5eea7770a554fef0c3bdc38f8f38b925dcf75f13c015d423e8eadec47d41bf6b/manifest.json +87 -0
  30. package/assets/sampels/studio_movies/caverns/segment_00/5eea7770a554fef0c3bdc38f8f38b925dcf75f13c015d423e8eadec47d41bf6b/provenance.json +7 -0
  31. package/assets/sampels/studio_movies/caverns/segment_01/branch.png +0 -0
  32. package/assets/sampels/studio_movies/keeper-matrix/_assembly/concat_list.txt +2 -0
  33. package/assets/sampels/studio_movies/keeper-matrix/_assembly/contrib_00.mp4 +0 -0
  34. package/assets/sampels/studio_movies/keeper-matrix/_assembly/contrib_01.mp4 +0 -0
  35. package/assets/sampels/studio_movies/keeper-matrix/movie.json +81 -0
  36. package/assets/sampels/studio_movies/keeper-matrix/movie.mp4 +0 -0
  37. package/assets/sampels/studio_movies/keeper-matrix/segment_00/64854c6a21d47ce10b23e884e12de448b25bfb0a375d33238dd0bec8056c076d/clip.mp4 +0 -0
  38. package/assets/sampels/studio_movies/keeper-matrix/segment_00/64854c6a21d47ce10b23e884e12de448b25bfb0a375d33238dd0bec8056c076d/manifest.json +87 -0
  39. package/assets/sampels/studio_movies/keeper-matrix/segment_00/64854c6a21d47ce10b23e884e12de448b25bfb0a375d33238dd0bec8056c076d/provenance.json +7 -0
  40. package/assets/sampels/studio_movies/keeper-matrix/segment_01/branch.png +0 -0
  41. package/assets/sampels/studio_movies/keeper-matrix/segment_01/cc926b44a42b90d7d291a4e50e3859458306c207a17c718086a3466e4049d6c6/clip.mp4 +0 -0
  42. package/assets/sampels/studio_movies/keeper-matrix/segment_01/cc926b44a42b90d7d291a4e50e3859458306c207a17c718086a3466e4049d6c6/manifest.json +87 -0
  43. package/assets/sampels/studio_movies/keeper-matrix/segment_01/cc926b44a42b90d7d291a4e50e3859458306c207a17c718086a3466e4049d6c6/provenance.json +7 -0
  44. package/assets/sampels/videos/a_red_paper_boat_drifting_on_a_calm_pond__gentle_ripples__mo_a62d6c28.mp4 +0 -0
  45. package/assets/sampels/videos/aurora.mp4 +0 -0
  46. package/assets/sampels/videos/dark_night.mp4 +0 -0
  47. package/assets/sampels/videos/fire_and_soul.mp4 +0 -0
  48. package/assets/sampels/videos/harsh_seas.mp4 +0 -0
  49. package/entry.tsx +74 -0
  50. package/index.html +53 -0
  51. package/package.json +42 -0
  52. package/src/assets/hugpy-mark.png +0 -0
  53. package/src/config.ts +676 -0
  54. package/src/demo/DemoBanner.tsx +54 -0
  55. package/src/demo/assets/demo-audio.mp3 +0 -0
  56. package/src/demo/assets/demo-clip.mp4 +0 -0
  57. package/src/demo/assets/demo-crop.mp4 +0 -0
  58. package/src/demo/assets/demo-poster.jpg +0 -0
  59. package/src/demo/assets/frame-1.jpg +0 -0
  60. package/src/demo/assets/frame-2.jpg +0 -0
  61. package/src/demo/assets/frame-3.jpg +0 -0
  62. package/src/demo/assets/frame-4.jpg +0 -0
  63. package/src/demo/assets/frame-5.jpg +0 -0
  64. package/src/demo/assets/frame-6.jpg +0 -0
  65. package/src/demo/assets/gen-1.jpg +0 -0
  66. package/src/demo/assets/gen-2.jpg +0 -0
  67. package/src/demo/assets.d.ts +14 -0
  68. package/src/demo/demo.css +64 -0
  69. package/src/demo/demoFetch.ts +305 -0
  70. package/src/demo/fixtures.ts +814 -0
  71. package/src/demo/index.ts +43 -0
  72. package/src/demo/mode.ts +81 -0
  73. package/src/demo/promptAssist.ts +181 -0
  74. package/src/demo/seed.ts +97 -0
  75. package/src/nav/BrandMark.css +66 -0
  76. package/src/nav/BrandMark.tsx +40 -0
  77. package/src/nav/Navbar.css +13 -0
  78. package/src/nav/Navbar.tsx +68 -0
  79. package/src/regions/SpatialRegionEditor.tsx +375 -0
  80. package/src/regions/TemporalRegionEditor.tsx +485 -0
  81. package/src/regions/types.ts +85 -0
  82. package/src/session.ts +85 -0
  83. package/src/share.ts +77 -0
  84. package/src/stations/ActivePanel.tsx +55 -0
  85. package/src/stations/ActiveProcessesStation.tsx +272 -0
  86. package/src/stations/ActiveProcessesStation.tsx.bak-sessions-20260812 +228 -0
  87. package/src/stations/AddFileBar.tsx +59 -0
  88. package/src/stations/AudioCropStation.tsx +157 -0
  89. package/src/stations/ComfyPanel.tsx +210 -0
  90. package/src/stations/ComfyStation.tsx +196 -0
  91. package/src/stations/CondensedKnobStrip.tsx +245 -0
  92. package/src/stations/ConsolePanel.tsx +82 -0
  93. package/src/stations/FrameExtractStation.tsx +148 -0
  94. package/src/stations/GenIdentityBar.tsx +412 -0
  95. package/src/stations/GenerateStation.tsx +5603 -0
  96. package/src/stations/GenerateStation.tsx.bak-tester-removal +5170 -0
  97. package/src/stations/GoalComposer.tsx +139 -0
  98. package/src/stations/ImageCropStation.tsx +153 -0
  99. package/src/stations/KnobInfo.tsx +64 -0
  100. package/src/stations/PlaceholderStation.tsx +26 -0
  101. package/src/stations/SectionTabs.tsx +276 -0
  102. package/src/stations/SharePanel.tsx +298 -0
  103. package/src/stations/SidebarRegistry.tsx +105 -0
  104. package/src/stations/StationShell.tsx +96 -0
  105. package/src/stations/StudioClipsStation.tsx +28 -0
  106. package/src/stations/WorkbenchDrawer.tsx +66 -0
  107. package/src/stations/WorkbenchSidebar.tsx +1224 -0
  108. package/src/stations/WorkbenchStation.tsx +91 -0
  109. package/src/stations/comfyEmbed.ts +79 -0
  110. package/src/stations/drawerShell.tsx +419 -0
  111. package/src/stations/ops/AudioCropCore.tsx +218 -0
  112. package/src/stations/ops/CharacterGroupsPanel.tsx +716 -0
  113. package/src/stations/ops/FrameExtractCore.tsx +695 -0
  114. package/src/stations/ops/ImageCropCore.tsx +164 -0
  115. package/src/stations/ops/VideoCropCore.tsx +193 -0
  116. package/src/stations/ops/useVideoCropJobs.ts +224 -0
  117. package/src/stations/pollCaps.ts +87 -0
  118. package/src/stations/registry.ts +191 -0
  119. package/src/stations/studio/AngleRingEditor.tsx +159 -0
  120. package/src/stations/studio/IdentitiesStation.tsx +2455 -0
  121. package/src/stations/studio/IdentityProfileControls.tsx +262 -0
  122. package/src/stations/studio/StudioActiveProcesses.tsx +224 -0
  123. package/src/stations/studio/StudioAssistLogPanel.tsx +220 -0
  124. package/src/stations/studio/StudioGenerateMode.tsx +29 -0
  125. package/src/stations/studio/StudioGenerateTab.tsx +2688 -0
  126. package/src/stations/studio/StudioItemCard.tsx +334 -0
  127. package/src/stations/studio/StudioLibraryTab.tsx +506 -0
  128. package/src/stations/studio/StudioMovieComposer.tsx +2875 -0
  129. package/src/stations/studio/StudioMovieComposer.tsx.bak-sessions-20260812 +2729 -0
  130. package/src/stations/studio/StudioMovieSessions.tsx +553 -0
  131. package/src/stations/studio/StudioMovieSessions.tsx.bak-sessions-20260812 +417 -0
  132. package/src/stations/studio/StudioPlane.tsx +302 -0
  133. package/src/stations/studio/StudioSplicedRow.tsx +476 -0
  134. package/src/stations/studio/StudioViewer.tsx +339 -0
  135. package/src/stations/studio/TurntableViewer.tsx +220 -0
  136. package/src/stations/studio/movieTimeline.ts +198 -0
  137. package/src/stations/studio/promptCard.tsx +498 -0
  138. package/src/stations/studio/studioItem.ts +87 -0
  139. package/src/stations/studio/studioShared.tsx +821 -0
  140. package/src/stations/studio/useIdentityProfiles.ts +575 -0
  141. package/src/stations/studio/useReconstruction.ts +283 -0
  142. package/src/stations/types.ts +43 -0
  143. package/src/stations/useAudioCropJobs.ts +202 -0
  144. package/src/stations/useAudioExtractJob.ts +144 -0
  145. package/src/stations/useCropJobs.ts +206 -0
  146. package/src/stations/useFrameJobs.ts +140 -0
  147. package/src/stations/useGenerateJob.ts +151 -0
  148. package/src/stations/useGenerateMovieJob.ts +177 -0
  149. package/src/stations/useGenerateRowJobs.ts +184 -0
  150. package/src/stations/useGenerateSceneJob.ts +156 -0
  151. package/src/stations/useGenerateSceneRowJobs.ts +282 -0
  152. package/src/style/app.css +4974 -0
  153. package/src/transport/client.ts +262 -0
  154. package/src/transport/telemetry.ts +97 -0
  155. package/src/video/DropReceptacle.tsx +122 -0
  156. package/src/video/PromptAssistButtons.tsx +118 -0
  157. package/src/video/composerBridge.ts +66 -0
  158. package/src/video/contract.ts +599 -0
  159. package/src/video/jobTracker.ts +702 -0
  160. package/src/video/mediaLibrary.ts +328 -0
  161. package/src/video/renderCompat.ts +512 -0
  162. package/src/video/sessionForm.ts +189 -0
  163. package/src/video/spreadAssist.ts +392 -0
  164. package/src/video/studioBridge.ts +82 -0
  165. package/src/video/useMediaJobs.ts +279 -0
  166. package/src/video/useModels.ts +413 -0
  167. package/src/video/useMoviePresets.ts +97 -0
  168. package/src/video/useMovieSessions.ts +292 -0
  169. package/src/video/usePresets.ts +167 -0
  170. package/src/video/useProjects.ts +87 -0
  171. package/src/video/usePromptAssist.ts +394 -0
  172. package/src/video/useRenderPresets.ts +221 -0
  173. package/src/video/useStudioAssistLog.ts +266 -0
  174. package/src/video/useStudioPresets.ts +128 -0
  175. package/src/video/useWorkerCalls.ts +165 -0
  176. package/tsconfig.json +17 -0
  177. package/vite.config.ts +33 -0
package/src/config.ts ADDED
@@ -0,0 +1,676 @@
1
+ // Single source of truth for hugpy service URLs (video-intelligence arm).
2
+ //
3
+ // Cloned from media_intelligence_ui/src/config.ts. Demo layer: CANNED only
4
+ // (?demo=1 — see demo/mode.ts; deliberately no live flavor, since video jobs
5
+ // drive real worker GPUs). Same conventions: one VITE_* base var defaulting to
6
+ // the SAME-ORIGIN current-hugpy API (/api), derived URLs with per-URL
7
+ // overrides, and this being the ONE module permitted to hold URL literals.
8
+
9
+ import { isCanned } from "./demo/mode";
10
+ import { withShareParam } from "./share";
11
+
12
+ const DEFAULT_API_BASE = "/api";
13
+
14
+ // Read import.meta.env without depending on vite/client ambient types (the scoped
15
+ // strict tsconfig doesn't include them).
16
+ const env: Record<string, string | undefined> =
17
+ (import.meta as unknown as { env?: Record<string, string | undefined> }).env ??
18
+ {};
19
+
20
+ function read(key: string): string | undefined {
21
+ const v = env[key];
22
+ return v && v.trim() ? v.trim() : undefined;
23
+ }
24
+
25
+ function stripTrailingSlash(url: string): string {
26
+ return url.replace(/\/+$/, "");
27
+ }
28
+
29
+ function readBool(key: string, dflt: boolean): boolean {
30
+ const v = read(key);
31
+ if (v == null) return dflt;
32
+ return !/^(0|false|no|off)$/i.test(v);
33
+ }
34
+
35
+ const apiBase = stripTrailingSlash(read("VITE_HUGPY_API_BASE") ?? DEFAULT_API_BASE);
36
+
37
+ export const hugpyConfig = {
38
+ /** Base host for all hugpy API calls. */
39
+ apiBase,
40
+ /** Public hugpy site — CTA target. Override with VITE_HUGPY_SITE_URL. */
41
+ siteUrl: stripTrailingSlash(read("VITE_HUGPY_SITE_URL") ?? "https://hugpy.ai"),
42
+ /** Multipart file upload endpoint. */
43
+ uploadUrl: read("VITE_HUGPY_UPLOAD_URL") ?? `${apiBase}/uploads`,
44
+ /**
45
+ * Resolve an uploaded server path into a typed MediaRef (native dims, mime,
46
+ * kind). Phase 3 — the first hop after upload for every media station.
47
+ */
48
+ videoIngestUrl: read("VITE_HUGPY_VIDEO_INGEST_URL") ?? `${apiBase}/video/ingest`,
49
+ /**
50
+ * char360 S4 — VIDEO → CHARACTER VIEW-SET extraction (Identities station,
51
+ * "Extract from video"): POST { source:<MediaRef, kind:"video">,
52
+ * target:"create"|"<slug>", char360_params?:{stride?,yolo_model?,min_h_frac?,
53
+ * cluster_dist?,min_faces?} } → 200 { job_id, target }. Validation errors are a
54
+ * flat 400 {error}; an unknown add-target slug is 404 {error}. The job itself
55
+ * is polled on the generic `videoJobUrl`/`jobStatusUrl` (GET /video/jobs/<id>),
56
+ * NOT a bespoke status route. Declared here so the extract control is a
57
+ * pure-additive flip that never re-touches this URL-literal module.
58
+ */
59
+ videoExtractUrl:
60
+ read("VITE_HUGPY_VIDEO_EXTRACT_URL") ?? `${apiBase}/video/identity-profiles/video-extract`,
61
+ /** Enqueue a crop job (spatial and/or temporal axis) on the job bus. */
62
+ cropEnqueueUrl: read("VITE_HUGPY_CROP_ENQUEUE_URL") ?? `${apiBase}/video/jobs/crop`,
63
+ /** Enqueue a frame-extract job (Frames & Models station) — one job → many frames. */
64
+ frameExtractEnqueueUrl:
65
+ read("VITE_HUGPY_FRAME_EXTRACT_URL") ?? `${apiBase}/video/jobs/frame_extract`,
66
+ /**
67
+ * Enqueue an audio-extract job (Audio Crop station, Phase 5) — pull a video's
68
+ * audio track into ONE standalone audio MediaRef the temporal editor edits over.
69
+ * Mirrors frameExtractEnqueueUrl: declared here so the station build is a
70
+ * pure-additive registry flip that never re-touches this URL-literal module.
71
+ */
72
+ audioExtractEnqueueUrl:
73
+ read("VITE_HUGPY_AUDIO_EXTRACT_URL") ?? `${apiBase}/video/jobs/audio_extract`,
74
+ /**
75
+ * Enqueue a text/image → image generate job (Generate station, later phase).
76
+ * Declared here now so the Generate-station build is a pure-additive registry
77
+ * flip that never re-touches this URL-literal module.
78
+ */
79
+ generateEnqueueUrl:
80
+ read("VITE_HUGPY_GENERATE_URL") ?? `${apiBase}/video/jobs/generate_image`,
81
+ /**
82
+ * Enqueue a text/image → scene generate job (Generate station, Scene mode) — one
83
+ * ordered multimodal prompt → N consecutive image frames + (optionally) one
84
+ * assembled mp4 clip. Same job-bus + status seam as generateEnqueueUrl; declared
85
+ * here so Scene mode is a pure-additive flip that never re-touches this
86
+ * URL-literal module.
87
+ */
88
+ generateSceneEnqueueUrl:
89
+ read("VITE_HUGPY_GENERATE_SCENE_URL") ?? `${apiBase}/video/jobs/generate_scene`,
90
+ /**
91
+ * Enqueue a GOAL-TIMELINE → movie generate job (Generate station, Movie mode) — an
92
+ * ordered, contiguous list of goals tiling [0,total) → an N-segment movie (segment
93
+ * frames + one assembled mp4 LAST), with an opt-in vision director loop. Same
94
+ * job-bus + poll/cancel seam as generateSceneEnqueueUrl (the poll returns a NESTED
95
+ * movie progress); declared here so Movie mode is a pure-additive flip that never
96
+ * re-touches this URL-literal module.
97
+ */
98
+ generateMovieEnqueueUrl:
99
+ read("VITE_HUGPY_GENERATE_MOVIE_URL") ?? `${apiBase}/video/jobs/generate_movie`,
100
+ /**
101
+ * Curated "ideal default loads" for the Generate station — GET → { presets: [
102
+ * { id, name, description, mode, model_key, defaults{...}, recommended } ] }. Each
103
+ * preset prefills the generation knobs + model; the per-id apply POST
104
+ * (presetApplyUrl) pre-warms that model on a GPU worker. Declared here so the
105
+ * dropdown is a pure-additive flip that never re-touches this URL-literal module.
106
+ */
107
+ presetsUrl: read("VITE_HUGPY_PRESETS_URL") ?? `${apiBase}/video/presets`,
108
+ /**
109
+ * Curated MOVIE templates for the Generate-station Movie tab — GET → [{ id, name,
110
+ * description, model_key, width, height, steps, guidance, fps, chain,
111
+ * goals:[{start_frame,end_frame,prompt}], vision_enabled, score_threshold }]. Unlike
112
+ * `presetsUrl` (a {presets:[…]} envelope of knob-only "loads"), a movie preset is a
113
+ * BARE ARRAY and each item carries the WHOLE shot list — picking one drops a ready
114
+ * goal timeline + settings into the Movie editor. The per-id apply POST
115
+ * (moviePresetApplyUrl) echoes the same object. Declared here so the Movie-template
116
+ * dropdown is a pure-additive flip that never re-touches this URL-literal module.
117
+ */
118
+ moviePresetsUrl: read("VITE_HUGPY_MOVIE_PRESETS_URL") ?? `${apiBase}/movie/presets`,
119
+ /**
120
+ * Enqueue a STUDIO image-to-video clip (Studio Clips viewer, slice #3) — POST a
121
+ * {resolution:{width,height,fps}, seed, ...} body → {job_id}. The job runs through
122
+ * the cinema-studio spine (router → manifest → runner → content-addressed clip)
123
+ * and its mp4 is cataloged in the media store, playable via `studioClipUrl`.
124
+ */
125
+ studioI2VEnqueueUrl:
126
+ read("VITE_HUGPY_STUDIO_I2V_URL") ?? `${apiBase}/video/studio/i2v`,
127
+ /**
128
+ * Enqueue a STUDIO MOVIE (Studio Movie composer, slice B2/B3) — POST an ordered
129
+ * goal timeline {resolution:{width,height,fps}, seed, vram_budget_gb, negative_prompt,
130
+ * goals:[{prompt, seed?, branch_frame?}]} → {job_id}. Each goal renders one studio
131
+ * clip conjoined at splice points into one NLE row (segment clips + an assembled
132
+ * movie.mp4 LAST); polled on the generic GET /video/jobs/<id> (jobStatusUrl). Same
133
+ * idiom as studioI2VEnqueueUrl — declared here (the one URL-literal module) so the
134
+ * composer imports it instead of deriving the path inline.
135
+ */
136
+ studioMovieUrl:
137
+ read("VITE_HUGPY_STUDIO_MOVIE_URL") ?? `${apiBase}/video/studio/movie`,
138
+ /**
139
+ * STUDIO TESTER — POST {category, prompt, models?} to sweep one prompt across
140
+ * EVERY servable model of the category's type, recording a model-battery
141
+ * run-dir (one row per model). Operator-gated; runs as a background job and
142
+ * returns {job_id, battery_run_dir}. Per-model results stream to the studio-
143
+ * assist log. Same URL-literal idiom as studioI2VEnqueueUrl.
144
+ */
145
+ studioTesterUrl:
146
+ read("VITE_HUGPY_STUDIO_TESTER_URL") ?? `${apiBase}/video/studio/tester`,
147
+ /**
148
+ * DURABLE recent studio clips for the Studio Clips list — GET → { clips: [{ job_id,
149
+ * status, playable, created, updated, output:{asset_id,width,height,duration_s} }] }.
150
+ * Sourced from the media CATALOG (not the comms /llm/jobs view, which drops terminal
151
+ * rows after ~600s), so a clip produced an hour ago still lists. Each `playable` row's
152
+ * `job_id` feeds `studioClipUrl(job_id)` to stream the clip. Declared here so the
153
+ * Studio Clips station is a pure-additive registry flip that never re-touches this
154
+ * module.
155
+ */
156
+ studioClipsUrl:
157
+ read("VITE_HUGPY_STUDIO_CLIPS_URL") ?? `${apiBase}/video/studio/clips`,
158
+ /**
159
+ * CINEMA SESSIONS — every movie the studio-movies root holds, newest first: GET →
160
+ * { movies: [{ movie_id, job_id, title, project, status:"partial"|"paused"|"done"|
161
+ * "failed"|"running", segments_completed, segments_total, resumable, width, height,
162
+ * fps, id_lock, updated, movie, segments:[{ index, segment_id, prompt, status,
163
+ * clip_available, media, duration_s, frames, resumed, error:{code,message}|null }] }] }.
164
+ *
165
+ * A FILESYSTEM listing, not a bus query (k91): a movie is ONE job that can run for
166
+ * hours, and when that job ends — cancelled, reaped, or lost with the tab — its
167
+ * rendered segments stay on disk. This is the handle that outlives the job, which is
168
+ * why the id is the movie DIR leaf (stable across a resume, which mints a new job id)
169
+ * and never the job id. Each done segment carries a ready `/video/media?handle=` url,
170
+ * so a session's clips are watchable AS THEY LAND. Paired with
171
+ * `studioMoviePauseUrl` / `studioMovieResumeUrl` for the two actions on a session.
172
+ */
173
+ studioMoviesUrl:
174
+ read("VITE_HUGPY_STUDIO_MOVIES_URL") ?? `${apiBase}/video/studio/movies`,
175
+ /**
176
+ * Bus-wide MEDIA JOBS listing for the console-wide "Active Processes" view — GET
177
+ * (?all=1 appends recent terminal rows; ?limit=N, default 50) → { jobs: [{ job_id,
178
+ * name, status, created, updated, principal, progress:{…|phase:"awaiting_capacity",
179
+ * reason, held_since, overtaken}|null, placement?:{source:"reservation"|"template",
180
+ * host, worker_id, gpu, process, reserved_bytes} }] }. Unlike `llmJobsUrl` (the
181
+ * unified cross-transport feed, terminal rows dropped after ~600s) this is the
182
+ * media CATALOG projection and carries PLACEMENT — WHERE each in-flight render
183
+ * physically executes (ae · cuda:0 · P-studio). Declared here so the Active
184
+ * Processes surface never derives the path inline.
185
+ */
186
+ mediaJobsUrl: read("VITE_HUGPY_MEDIA_JOBS_URL") ?? `${apiBase}/video/jobs`,
187
+ /**
188
+ * Curated STUDIO clip presets for the Studio Clips station — GET → { presets: [{
189
+ * id, name, description, capability:"i2v"|"t2v", width, height, fps, vram_budget_gb,
190
+ * seed, prompt, negative, recommended }] } (a {presets:[…]} envelope, like
191
+ * `presetsUrl`/`moviePresetsUrl`). Unlike a video preset (which pins a model_key),
192
+ * a studio preset pins a CAPABILITY + geometry + a routing `vram_budget_gb` and lets
193
+ * the studio router pick the model — picking one prefills the generate affordance
194
+ * (and drives the enqueue's capability/budget/prompt). The per-id apply POST
195
+ * (studioPresetApplyUrl) echoes the same object as a POSTable /video/studio/i2v body.
196
+ * Declared here so the Studio-preset dropdown is a pure-additive flip that never
197
+ * re-touches this URL-literal module.
198
+ */
199
+ studioPresetsUrl:
200
+ read("VITE_HUGPY_STUDIO_PRESETS_URL") ?? `${apiBase}/video/studio/presets`,
201
+ /**
202
+ * RENDER PRESETS — what this fleet can ACTUALLY render today. GET → { presets: [{
203
+ * id, title, capability, capabilities[], model, precision, geometry, width, height,
204
+ * fps, default_frames, max_frames, inputs[], proven, evidence, composes[], joints[],
205
+ * vram_envelope_gb, vram_need_gib, fits_render_box }], unavailable: [{ capability,
206
+ * reason, refusal }], menu, frame_cadence, render_box, render_box_vram_gib }.
207
+ *
208
+ * The FOURTH and only MEASURED preset surface: the other three (`presetsUrl`,
209
+ * `moviePresetsUrl`, `studioPresetsUrl`) publish what we CURATED; this one publishes
210
+ * the eight ratified rows measured on the live fleet, plus — the half that makes a
211
+ * compatibility-aware picker possible — every capability NO preset covers, with the
212
+ * measured blocker. Read by useRenderPresets.ts, which every studio surface consults
213
+ * to decide what to OFFER. It is a courtesy layer: the server's own capability gate
214
+ * on POST /video/studio/i2v stays authoritative.
215
+ */
216
+ renderPresetsUrl:
217
+ read("VITE_HUGPY_RENDER_PRESETS_URL") ?? `${apiBase}/video/render/presets`,
218
+ /**
219
+ * Distinct known PROJECT names — GET → { projects: [name, …] } (read-only; derived
220
+ * from the media-bus job store). Feeds the Settings "Project" combobox (choose an
221
+ * existing name or free-type a new one) that threads `project` into the enqueue.
222
+ * Empty = auto-named (the default): the field is optional scaffolding.
223
+ */
224
+ projectsUrl: read("VITE_HUGPY_PROJECTS_URL") ?? `${apiBase}/video/projects`,
225
+ /**
226
+ * IDENTITY PROFILES (studio stage (a)) — GET → { profiles: [{ slug, name,
227
+ * reference_images[], created_at, notes }] }; POST { name, reference_images[1..4],
228
+ * notes? } → { profile } (409 on a duplicate name). An identity profile is the
229
+ * DURABLE form of "the reference set IS the identity": a named, curated reference
230
+ * set saved ONCE and associated anywhere (single clips, movies, stills) via the
231
+ * enqueue body's `identity_profile:<slug>`. Declared here so the identity picker /
232
+ * save affordance is a pure-additive flip that never re-touches this URL-literal module.
233
+ */
234
+ identityProfilesUrl:
235
+ read("VITE_HUGPY_IDENTITY_PROFILES_URL") ?? `${apiBase}/video/identity-profiles`,
236
+ /**
237
+ * CHARACTER-GROUPS COMMIT (dev/CHARACTER-GROUPS-PLAN.md slice S3) — POST the
238
+ * user-CURATED output of the char360 "review" extraction:
239
+ * { groups: [ { name?: string, reference_images: string[] } ] }
240
+ * where each `reference_images` entry is a media-handle (jailed absolute path,
241
+ * a `view.url` from the S1 review manifest) the user KEPT in that group after
242
+ * remove/move/merge edits, and only the groups the user marked "proceed" are
243
+ * sent. Response 200 { results: [ { name, ok, slug?, error? } ] } — one result
244
+ * per submitted group, positionally aligned to the request order, each minting
245
+ * (or failing to mint) one identity profile. Declared here so the
246
+ * character-groups panel is a pure-additive flip that never re-touches this
247
+ * URL-literal module. (S3 builds this route to THIS exact body shape.)
248
+ */
249
+ identityProfilesFromGroupsUrl:
250
+ read("VITE_HUGPY_IDENTITY_FROM_GROUPS_URL") ??
251
+ `${apiBase}/video/identity-profiles/from-groups`,
252
+ /**
253
+ * LLM PROMPT ASSIST for the Generate-station composer — POST { mode:"detail"|
254
+ * "generate", draft?, model?, context?:{kind?,hint?} } → 200 { prompt, model,
255
+ * kind }. `detail` enriches the caller's `draft` (required, non-empty); `generate`
256
+ * writes a full prompt from scratch (draft optional, used as a loose theme). Send
257
+ * NO `model` (the backend picks the best resolvable chat model) and pass
258
+ * `context.kind` = the active sub-mode (image|scene|movie) so video modes get
259
+ * motion/camera phrasing. Errors: 400 (validation/unknown model) / 502 (no worker /
260
+ * generation failed) carry a flat { error:"<string>" }. Declared here so the
261
+ * composer's assist buttons are a pure-additive flip that never re-touch this
262
+ * URL-literal module.
263
+ */
264
+ promptAssistUrl:
265
+ read("VITE_HUGPY_PROMPT_ASSIST_URL") ?? `${apiBase}/video/prompt/assist`,
266
+ /**
267
+ * STUDIO-ASSIST LIVE LOG (operator directive, 2026-07-31) — a stored record per
268
+ * prompt-generate attempt: the UNTRUNCATED model reply, what was stripped
269
+ * (text/reasoning), whether the answer came FROM the reasoning, and the outcome
270
+ * (served | empty | parse_error | worker_error | resolve_error). Lets the
271
+ * operator self-diagnose "the assistant returned only reasoning" / "did not
272
+ * return the spread JSON" without asking the keeper.
273
+ * GET {logUrl}?limit&since&after_id → { events:[…], count, cursor } (page-load
274
+ * backfill; `raw` bounded for the load only).
275
+ * GET {logStreamUrl} → SSE, replay-then-live, full `raw`.
276
+ * Same auth as every other /video call (the video gate) — no new credential.
277
+ */
278
+ promptAssistLogUrl:
279
+ read("VITE_HUGPY_PROMPT_ASSIST_LOG_URL")
280
+ ?? `${apiBase}/video/prompt/assist/log`,
281
+ promptAssistLogStreamUrl:
282
+ read("VITE_HUGPY_PROMPT_ASSIST_LOG_STREAM_URL")
283
+ ?? `${apiBase}/video/prompt/assist/log/stream`,
284
+ /**
285
+ * Which TEXT GENERATORS the fleet can actually run — GET →
286
+ * { models: [{ model, serving, framework, default }], default, excluded, ... }.
287
+ * Feeds the Enhance/Generate picker. Deliberately NOT the full model registry:
288
+ * the backend offers only rows a live worker holds, and drops catalog entries
289
+ * that are mis-tagged text-generation (a Flux image LoRA is in there), so the
290
+ * picker cannot offer a choice that 404s or renders nothing.
291
+ */
292
+ promptAssistModelsUrl:
293
+ read("VITE_HUGPY_PROMPT_ASSIST_MODELS_URL")
294
+ ?? `${apiBase}/video/prompt/assist/models`,
295
+ /**
296
+ * INTENT ROUTER (STUDIO-SPREAD-SPEC §1d) — POST { text, scope:"segment"|"movie" }
297
+ * → 200 { intent:"empty"|"direction"|"scene_prompt"|"ambiguous", operation,
298
+ * confidence, cached, degraded }. Decides whether what the user typed is a SCENE
299
+ * (→ enhance) or a DIRECTION (→ generate from it), so the composer can pre-arm the
300
+ * right button instead of making that an unassisted guess.
301
+ *
302
+ * ALWAYS 200 — that is the contract, not an accident: blank text short-circuits with
303
+ * no model call, and ANY router failure degrades to `ambiguous` + `degraded:true` so
304
+ * the UI shows BOTH actions. A classifier outage must never make a text box unusable,
305
+ * and a classification must never run a generation on its own. Call it on field BLUR
306
+ * or just before an action, never per keystroke (the backend caches by input hash,
307
+ * which is a latency nicety, not a licence to spam it).
308
+ */
309
+ promptIntentUrl:
310
+ read("VITE_HUGPY_PROMPT_INTENT_URL") ?? `${apiBase}/video/prompt/intent`,
311
+ /**
312
+ * Model registry — GET → { data: [{ id, task, tasks, hub_id, ... }] }. The Frames
313
+ * and Generate stations filter this to the GENERATION image tasks
314
+ * (text-to-image + image-to-image) off the FULL `tasks` capability list for the
315
+ * model dropdown (see useModels.ts); the registry is revalidated in-session so
316
+ * newly-adopted models appear without a reload.
317
+ */
318
+ modelsUrl: read("VITE_HUGPY_MODELS_URL") ?? `${apiBase}/v1/models`,
319
+ /** Task defaults — GET → { defaults: { "text-to-image": <id>, ... }, tasks:[...] }. */
320
+ promptTasksUrl: read("VITE_HUGPY_PROMPT_TASKS_URL") ?? `${apiBase}/prompt/tasks`,
321
+ /**
322
+ * The AUTHORITATIVE unified feed of EVERY in-flight worker call across ALL
323
+ * transports (web / v1 / discord / cli / media) — GET → { jobs: [{ id, kind,
324
+ * status, transport, model, model_name, worker, principal, elapsed, tokens,
325
+ * error, ... }] }. Unlike the per-session jobTracker (which only knows jobs THIS
326
+ * browser enqueued) this shows fleet-wide work — identity reconstructions
327
+ * (kind:"identity_reconstruction", transport:"media"), studio renders, and any
328
+ * call from another station/transport. The Active Processes panel polls it,
329
+ * keeps the in-flight rows (pending/processing/streaming, minus `download`
330
+ * provisioning), and merges them with the tracker rows. Declared here so the
331
+ * panel never derives the path inline.
332
+ */
333
+ llmJobsUrl: read("VITE_HUGPY_LLM_JOBS_URL") ?? `${apiBase}/llm/jobs`,
334
+ /**
335
+ * k9 VIDEO-SHARE links — GET → { keys:[{id,label,prefix,created_at,expires_at,
336
+ * expired,revoked,last_used}] } (OPERATOR-ONLY; a share/anon session gets 401,
337
+ * which the Share button uses as its auth probe). POST { label?, ttl_days? } →
338
+ * { key, url, id, ... } (the full key + link, shown ONCE). DELETE the per-id
339
+ * revoke via `keysVideoShareRevokeUrl`. Operator-gated + off the /video surface
340
+ * server-side, so a share principal can never reach it.
341
+ */
342
+ keysVideoShareUrl:
343
+ read("VITE_HUGPY_KEYS_VIDEO_SHARE_URL") ?? `${apiBase}/keys/video-share`,
344
+ /**
345
+ * Default endpoint for the ComfyUI viewer station — the URL its <iframe> embeds
346
+ * until the user overrides it (the override is kept per-tab in sessionStorage).
347
+ * MUST be an HTTPS endpoint: the video UI is served over HTTPS, so an http:// or
348
+ * localhost URL is silently blocked as mixed content. Set VITE_HUGPY_COMFY_URL to
349
+ * a documented placeholder (e.g. https://comfy.hugpy.ai); NEVER hardcode a secret.
350
+ * Empty by default — the station then shows its "enter an endpoint" empty state.
351
+ */
352
+ comfyUrl: read("VITE_HUGPY_COMFY_URL") ?? "",
353
+ /**
354
+ * Dedicated worker pool for this arm's jobs. Same semantics as the media arm:
355
+ * empty string = general resolution (un-pooled); set VITE_HUGPY_POOL to route
356
+ * to reserved workers once a pooled worker exists.
357
+ */
358
+ pool: env["VITE_HUGPY_POOL"] != null ? env["VITE_HUGPY_POOL"].trim() : "",
359
+ /**
360
+ * Send cookies on hugpy requests for session auth. DEFAULT OFF — see the
361
+ * media arm's config for the CORS rationale. Opt in with
362
+ * VITE_HUGPY_WITH_CREDENTIALS=true once hugpy's CORS is credential-ready.
363
+ */
364
+ withCredentials: readBool("VITE_HUGPY_WITH_CREDENTIALS", false),
365
+ /**
366
+ * Attach an X-Request-Id header for server-side correlation. DEFAULT OFF —
367
+ * a custom header forces a CORS preflight the server may not answer. Opt in
368
+ * with VITE_HUGPY_SEND_REQUEST_ID=true. The request id is still generated and
369
+ * used for client-side telemetry regardless.
370
+ */
371
+ sendRequestId: readBool("VITE_HUGPY_SEND_REQUEST_ID", false),
372
+ } as const;
373
+
374
+ // Per-id / per-handle endpoints. Kept as helpers (not static strings) because the
375
+ // job id and media handle are runtime values — but they still derive from apiBase
376
+ // here, in the ONE module permitted to hold URL literals. Both callers must reach
377
+ // these through config, never by concatenating paths themselves.
378
+
379
+ /** Poll a single crop/extract job by id: GET → {job_id, status, result}. */
380
+ export function jobStatusUrl(id: string): string {
381
+ return `${apiBase}/video/jobs/${encodeURIComponent(id)}`;
382
+ }
383
+
384
+ /** Revoke one video-share link by id: DELETE → {ok} (404 unknown id). Per-id like
385
+ * jobStatusUrl — derived from apiBase here, the one module permitted to hold URL
386
+ * literals. (List + create use `keysVideoShareUrl`.) */
387
+ export function keysVideoShareRevokeUrl(id: string): string {
388
+ return `${apiBase}/keys/video-share/${encodeURIComponent(id)}`;
389
+ }
390
+
391
+ /**
392
+ * The general VIDEO-JOBS status route, by job id: GET → { job_id, progress,
393
+ * result }. Byte-identical to `jobStatusUrl` (same route) but named generically
394
+ * for callers — like the char360 S4 "Extract from video" poll — that aren't
395
+ * crop-specific. `result` is null/absent while running (`progress` 0..1 with a
396
+ * stage/message); terminal is `result.ok===true` (success — no slug list, the
397
+ * created/updated profile is the durable record) or `result.ok===false` with
398
+ * `result.error.{code,message,retryable}` (honest error-as-data). Per-id like
399
+ * `jobStatusUrl` — derived from apiBase here, the one module permitted to hold
400
+ * URL literals.
401
+ */
402
+ export function videoJobUrl(jobId: string): string {
403
+ return `${apiBase}/video/jobs/${encodeURIComponent(jobId)}`;
404
+ }
405
+
406
+ /** Cooperative cancel: POST → {job_id, status, cancelled}. Queued jobs die
407
+ * outright; a running scene stops between frames. */
408
+ export function jobCancelUrl(id: string): string {
409
+ return `${apiBase}/video/jobs/${encodeURIComponent(id)}/cancel`;
410
+ }
411
+
412
+ /**
413
+ * Cancel a NON-media-transport worker call by id: POST → cooperative stop. The
414
+ * unified `/llm/jobs` feed carries calls from every transport; a `transport:"media"`
415
+ * job cancels through `jobCancelUrl` (the video job bus), but everything else
416
+ * (web / v1 / discord / cli chat + generation) cancels here. Per-id like
417
+ * `jobCancelUrl` — derived from apiBase, the one module permitted to hold URL literals.
418
+ */
419
+ export function llmChatCancelUrl(id: string): string {
420
+ return `${apiBase}/llm/chat/cancel/${encodeURIComponent(id)}`;
421
+ }
422
+
423
+ /**
424
+ * HARD-CANCEL a job by id on the unified feed: POST → {cancelled, status}. Unlike
425
+ * the transport-specific routes above, this is the ONE cancel the fleet-wide
426
+ * `/llm/jobs` Active-Processes rows hit regardless of which transport launched the
427
+ * job — the backend fans it out to the right bus. Per-id like `jobCancelUrl` —
428
+ * derived from apiBase, the one module permitted to hold URL literals.
429
+ */
430
+ export function llmJobCancelUrl(id: string): string {
431
+ return `${apiBase}/llm/jobs/${encodeURIComponent(id)}/cancel`;
432
+ }
433
+
434
+ /**
435
+ * Apply (pre-warm) a curated Generate-station preset by id: POST → on success
436
+ * {ok:true, worker:{name,id}, model_key, mode, defaults{...}, warming}; on failure
437
+ * a {ok:false, error:{code,message}} with 404 (unknown preset/model) or 409 (no GPU
438
+ * worker / won't fit). Per-id like jobStatusUrl — derived from apiBase here.
439
+ */
440
+ export function presetApplyUrl(id: string): string {
441
+ return `${apiBase}/video/presets/${encodeURIComponent(id)}/apply`;
442
+ }
443
+
444
+ /**
445
+ * Apply a curated Movie template by id: POST → the same full preset object the list
446
+ * endpoint returns (id/name/model_key/dims/knobs + the whole `goals` shot list +
447
+ * director defaults). Per-id like presetApplyUrl — derived from apiBase here. The
448
+ * Movie-template dropdown populates the editor from the ALREADY-fetched list object,
449
+ * so this seam exists for parity / a future server-side pre-warm.
450
+ */
451
+ export function moviePresetApplyUrl(id: string): string {
452
+ return `${apiBase}/movie/presets/${encodeURIComponent(id)}/apply`;
453
+ }
454
+
455
+ /**
456
+ * Raw media bytes for a MediaRef `uri` handle — usable directly as an <img> src.
457
+ * The handle is opaque and may contain reserved characters, so it is always
458
+ * URL-encoded into the query.
459
+ *
460
+ * Canned demo (?demo=1): fixture MediaRefs carry BUNDLED asset URLs as their
461
+ * uri — <img>/<video> element loads bypass window.fetch, so the demo shim can
462
+ * never answer them; the asset URL itself must be the src. Pass those through.
463
+ */
464
+ export function mediaBytesUrl(handle: string): string {
465
+ if (isCanned() && /^(\/|https?:|data:|blob:)/.test(handle) && !handle.startsWith("/mnt/")) {
466
+ return handle;
467
+ }
468
+ // k9: element-src loads (this is used as <img>/<video> src) can't carry the
469
+ // X-Video-Share header, so a share session rides the credential in the query.
470
+ return withShareParam(`${apiBase}/video/media?handle=${encodeURIComponent(handle)}`);
471
+ }
472
+
473
+ /**
474
+ * Stream a produced STUDIO clip by media-bus job id — usable directly as an HTML5
475
+ * `<video>` src (Studio Clips viewer, slice #3). The route resolves the job's
476
+ * content-addressed clip.mp4 server-side and streams it Range-aware (seek-able), so
477
+ * the browser never handles a filesystem path. Per-id like `jobStatusUrl` — derived
478
+ * from apiBase here, the one module permitted to hold URL literals.
479
+ */
480
+ export function studioClipUrl(jobId: string): string {
481
+ // k9: used directly as a <video> src (element load, no header) — a share
482
+ // session rides its credential in the query, exactly like mediaBytesUrl.
483
+ return withShareParam(`${apiBase}/video/studio/clip/${encodeURIComponent(jobId)}`);
484
+ }
485
+
486
+ /**
487
+ * The exact CREATION PARAMETERS of a studio render, for a list-row expander: GET →
488
+ * {job_id, status, spec, manifest|null, error|null}. `manifest` is the content-addressed
489
+ * render manifest for a done clip (the TRUE params); `error` is {code,message,retryable}
490
+ * for a failed/cancelled job. LAZY (fetched on expand, not on the list poll). Per-id like
491
+ * `studioClipUrl` — derived from apiBase here, the one module permitted to hold URL
492
+ * literals.
493
+ */
494
+ export function studioClipDetailUrl(jobId: string): string {
495
+ return `${apiBase}/video/studio/clip/${encodeURIComponent(jobId)}/detail`;
496
+ }
497
+
498
+ /**
499
+ * ARCHIVE a produced studio clip by job id: POST → {ok, job_id, archived:true,
500
+ * already, archived_at}. Fixes "removed clips just reappear" — GET
501
+ * `studioClipsUrl` is DB-driven (not a filesystem walk), so this is the ONE way
502
+ * to make a clip actually stop being listed; the clip's bytes are never touched
503
+ * (never-delete doctrine — see media_bus.archive's docstring). 404 for an unknown
504
+ * id; idempotent (a repeat archive answers `already:true`, never an error) — see
505
+ * `unarchiveUrl` for the reversible half. Per-id like `studioClipUrl` — derived
506
+ * from apiBase here, the one module permitted to hold URL literals.
507
+ */
508
+ export function studioClipArchiveUrl(jobId: string): string {
509
+ return `${apiBase}/video/studio/clip/${encodeURIComponent(jobId)}/archive`;
510
+ }
511
+
512
+ /**
513
+ * The honest counterpart to `studioClipArchiveUrl`: POST → {ok, job_id,
514
+ * archived:false, already}. Clears the archive mark so the clip rejoins the
515
+ * list; the bytes were never moved, so there is nothing to restore on disk.
516
+ * Per-id like `studioClipUrl` — derived from apiBase here, the one module
517
+ * permitted to hold URL literals.
518
+ */
519
+ export function studioClipUnarchiveUrl(jobId: string): string {
520
+ return `${apiBase}/video/studio/clip/${encodeURIComponent(jobId)}/unarchive`;
521
+ }
522
+
523
+ /**
524
+ * PAUSE a Cinema session by movie id (k91): POST → {ok, movie_id, job_id, status:
525
+ * "paused", cancelled, job_status, segments_completed, segments_total}. Pause is the
526
+ * cooperative cancel PLUS the manifest write that says "parked, not abandoned", so it
527
+ * is IDEMPOTENT: a session whose job already ended still parks (`cancelled:false`
528
+ * says there was nothing live to stop). 404 for an id that is not a session dir.
529
+ * Per-id like `studioClipUrl` — the movie id is a DIR LEAF, so it is URL-encoded here
530
+ * exactly like a job id, in the one module permitted to hold URL literals.
531
+ */
532
+ export function studioMoviePauseUrl(movieId: string): string {
533
+ return `${apiBase}/video/studio/movie/${encodeURIComponent(movieId)}/pause`;
534
+ }
535
+
536
+ /**
537
+ * RESUME a Cinema session by movie id (k91): POST → {ok, movie_id, job_id, …} once
538
+ * the persisted spec is back on the bus. There is no checkpoint format — the resumed
539
+ * run re-renders the SAME spec and content addressing walks the already-rendered
540
+ * prefix in seconds, so THE CLIPS ARE THE CHECKPOINT.
541
+ *
542
+ * Two honest refusals the caller must SHOW rather than swallow, both 409 {error}:
543
+ * a movie that predates spec.json (nothing to re-enqueue — its segments stay listed
544
+ * and playable), and one whose job is already in flight ("pause it first"). The
545
+ * transport surfaces either body's `error` verbatim through `describeAppError`.
546
+ * Per-id like `studioMoviePauseUrl`.
547
+ */
548
+ export function studioMovieResumeUrl(movieId: string): string {
549
+ return `${apiBase}/video/studio/movie/${encodeURIComponent(movieId)}/resume`;
550
+ }
551
+
552
+ /**
553
+ * Studio "Send to Editor" (k12): POST → {ok:true, filename, path, mode}. Hands a
554
+ * produced clip, in a Filmora-native MP4, into a stable inbox the operator's LAN
555
+ * Windows workstation (Filmora desktop) picks up. CONSOLE-OPERATOR ONLY — a
556
+ * video-share guest is refused 403 by the route even though it passes the /video
557
+ * gate (the button is hidden for non-operators; see StudioViewer's probe). No
558
+ * request body (the job id rides the URL, like archive/unarchive). `mode` is
559
+ * "copy" (lossless remux) or "transcode". Per-id like `studioClipUrl` — derived
560
+ * from apiBase here, the one module permitted to hold URL literals.
561
+ */
562
+ export function studioClipToEditorUrl(jobId: string): string {
563
+ return `${apiBase}/video/studio/clip/${encodeURIComponent(jobId)}/to-editor`;
564
+ }
565
+
566
+ /**
567
+ * Apply a curated Studio preset by id: POST → the pure-prefill envelope
568
+ * {ok:true, id, name, capability, request:{…a POSTable /video/studio/i2v body}} (a
569
+ * 404 {ok:false,error} for an unknown id). Per-id like `studioClipUrl` — derived
570
+ * from apiBase here, the one module permitted to hold URL literals. The Studio-preset
571
+ * dropdown prefills from the ALREADY-fetched list object, so this seam exists for
572
+ * parity / a future server-side pre-warm (mirrors moviePresetApplyUrl).
573
+ */
574
+ export function studioPresetApplyUrl(id: string): string {
575
+ return `${apiBase}/video/studio/presets/${encodeURIComponent(id)}/apply`;
576
+ }
577
+
578
+ /**
579
+ * One identity profile by slug — GET → { profile } (404 unknown); DELETE → { ok,
580
+ * archived, slug } (the store ARCHIVES under `_deleted`, never erases). Per-slug like
581
+ * `studioClipUrl` — derived from apiBase here, the one module permitted to hold URL
582
+ * literals. (List + create use `identityProfilesUrl`.)
583
+ */
584
+ export function identityProfileUrl(slug: string): string {
585
+ return `${apiBase}/video/identity-profiles/${encodeURIComponent(slug)}`;
586
+ }
587
+
588
+ /**
589
+ * Enqueue a TURNAROUND RECONSTRUCTION ("character sheet") for one identity profile
590
+ * — POST { prompt?, views?:string[] (default ["front","three_quarter","profile",
591
+ * "back"]), seed? } → { job_id | job_ids, recon_id } (the enqueue may fan out to ONE
592
+ * job or several — one per requested view; the caller flattens whichever it gets).
593
+ * Each returned job is polled on the existing GET /video/jobs/<id> (`jobStatusUrl`);
594
+ * on completion the profile's `reconstructions[]` carries the generated view paths
595
+ * (served via `mediaBytesUrl`). Per-slug like `identityProfileUrl` — derived from
596
+ * apiBase here, the one module permitted to hold URL literals.
597
+ */
598
+ export function identityReconstructionUrl(slug: string): string {
599
+ return `${apiBase}/video/identity-profiles/${encodeURIComponent(slug)}/reconstruction`;
600
+ }
601
+
602
+ /**
603
+ * PROMOTE reconstruction views to a profile's CANONICAL set — POST { recon_id,
604
+ * views:number[] (indices into that reconstruction's `views`) } → { profile } (the
605
+ * updated profile, now carrying the enlarged `canonical[]`). The durable "these are
606
+ * the approved reference angles" step after a turnaround is generated. Per-slug like
607
+ * `identityReconstructionUrl` — derived from apiBase here, the one module permitted
608
+ * to hold URL literals.
609
+ */
610
+ export function identityCanonicalUrl(slug: string): string {
611
+ return `${apiBase}/video/identity-profiles/${encodeURIComponent(slug)}/canonical`;
612
+ }
613
+
614
+ /**
615
+ * ONE-CLICK FULL IDENTITY GENERATION — POST { views?, texture?, turntable?,
616
+ * auto_promote? } (a bare {} is the intended happy-path call) → { job_id, recon_id }.
617
+ * Turns a saved identity PROFILE into a complete 3D identity in a single action: a
618
+ * Hunyuan3D mesh, a 360° turntable, and (by default) auto-promoted canonical angles —
619
+ * no prior reconstruction needed. Poll the minted recon on `identityMeshStatusUrl`.
620
+ * Per-slug like `identityReconstructionUrl` — derived from apiBase here, the one module
621
+ * permitted to hold URL literals.
622
+ */
623
+ export function identityGenerateUrl(slug: string): string {
624
+ return `${apiBase}/video/identity-profiles/${encodeURIComponent(slug)}/generate`;
625
+ }
626
+
627
+ /**
628
+ * Mesh-build STATUS for one reconstruction — GET → the mesh state block
629
+ * ({ status: "none"|"queued"|"running"|"done"|"error"|"cancelled", error?, glb_path?,
630
+ * video_path?, auto_promoted? }). Polled after `identityGenerateUrl` (or the 5g mesh
631
+ * build) until terminal. Per-id like `jobStatusUrl` — derived from apiBase here, the one
632
+ * module permitted to hold URL literals.
633
+ */
634
+ export function identityMeshStatusUrl(slug: string, reconId: string): string {
635
+ return `${apiBase}/video/identity-profiles/${encodeURIComponent(slug)}/reconstruction/${encodeURIComponent(reconId)}/mesh`;
636
+ }
637
+
638
+ /**
639
+ * IDENTITY VERSIONS slice (design: dev/IDENTITY-VERSIONS-SLICE.md) — per-identity
640
+ * generation settings: PATCH → a PARTIAL `gen_settings` body ({texture?, pose?,
641
+ * frame_count?, fps?, width?, height?, auto_promote?, front_ref?, remove_background?,
642
+ * vision_model?, cleanup_prompt?, negative_prompt?}) → { profile } (an omitted key is
643
+ * left untouched server-side, same partial-PATCH idiom as `identityProfileUrl`).
644
+ * Persists what the Settings panel edits and what a bare `/generate` click honors.
645
+ * cleanup_prompt/negative_prompt are the CLEANUP-PROMPT slice's Advanced-panel "Avoid /
646
+ * cleanup" + "Negative prompt" fields (C4). Per-slug like `identityProfileUrl` —
647
+ * derived from apiBase here, the one module permitted to hold URL literals.
648
+ */
649
+ export function identityProfileSettingsUrl(slug: string): string {
650
+ return `${apiBase}/video/identity-profiles/${encodeURIComponent(slug)}/settings`;
651
+ }
652
+
653
+ /**
654
+ * One VERSION of an identity (the clay base + N named render-sets it accrues) by
655
+ * id — PATCH { name?, notes? } (rename/annotate) and DELETE (archive; the base
656
+ * version and the currently-ACTIVE version are refused with a 400 — never-delete
657
+ * applies at the version layer too) both hang off this path. The response
658
+ * envelope for these two isn't fixed by the wire contract (unlike the settings
659
+ * PATCH above), so callers should treat it tolerantly. Per-slug+id like
660
+ * `identityMeshStatusUrl` — derived from apiBase here, the one module permitted
661
+ * to hold URL literals.
662
+ */
663
+ export function identityVersionUrl(slug: string, versionId: string): string {
664
+ return `${apiBase}/video/identity-profiles/${encodeURIComponent(slug)}/versions/${encodeURIComponent(versionId)}`;
665
+ }
666
+
667
+ /**
668
+ * Make one version the identity's ACTIVE version: POST (no body) → the resolver
669
+ * (`_reference_images_from_body`) then takes THIS version's canonical as the
670
+ * id_lock DNA for future generations that don't name a specific
671
+ * `identity_version`. Per-slug+id like `identityVersionUrl` — derived from
672
+ * apiBase here, the one module permitted to hold URL literals.
673
+ */
674
+ export function identityVersionActivateUrl(slug: string, versionId: string): string {
675
+ return `${identityVersionUrl(slug, versionId)}/activate`;
676
+ }