@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,394 @@
|
|
|
1
|
+
// Shared LLM PROMPT-ASSIST hook — the Enhance / Generate transport + busy/error
|
|
2
|
+
// model, extracted from GenerateStation so the Generate, Studio Clip, and Studio
|
|
3
|
+
// Movie composers all POST the SAME promptAssistUrl with ONE in-flight call at a
|
|
4
|
+
// time and identical error semantics.
|
|
5
|
+
//
|
|
6
|
+
// Presentational-by-callback, like GoalComposer: this hook owns NO editor state.
|
|
7
|
+
// Each host passes the current draft when it fires a call and an `apply` callback
|
|
8
|
+
// that drops the returned prompt wherever that host keeps its text (replace the
|
|
9
|
+
// primary text part / set the prompt string / patch a goal row). A failed call
|
|
10
|
+
// NEVER reaches `apply`, so a bad response can't clobber the existing prompt — it
|
|
11
|
+
// surfaces on `assistError` instead. The paired <PromptAssistButtons> renders the
|
|
12
|
+
// button cluster; keeping the two beside each other keeps adoption a small,
|
|
13
|
+
// non-destabilizing flip per composer.
|
|
14
|
+
import { useCallback, useEffect, useState } from "react";
|
|
15
|
+
import { request, okValue, errorOf, describeAppError } from "../transport/client";
|
|
16
|
+
import { hugpyConfig } from "../config";
|
|
17
|
+
import {
|
|
18
|
+
readIntentResult,
|
|
19
|
+
readSpreadResult,
|
|
20
|
+
type AssistContextWire,
|
|
21
|
+
type IntentResult,
|
|
22
|
+
type SpreadRequestWire,
|
|
23
|
+
type SpreadResult,
|
|
24
|
+
} from "./spreadAssist";
|
|
25
|
+
|
|
26
|
+
/** The two assist actions: "detail" = Enhance (enrich a draft), "generate" = write fresh. */
|
|
27
|
+
export type AssistMode = "detail" | "generate";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Every mode that can hold the ONE in-flight assist slot. The two whole-request
|
|
31
|
+
* modes added by STUDIO-SPREAD-SPEC ("spread" = one call rewriting N selected rows,
|
|
32
|
+
* "negative" = an exclusion list rather than prose) are NOT `AssistMode`, because
|
|
33
|
+
* they take no `draft` and do not return a prompt for a single field — but they DO
|
|
34
|
+
* share the concurrency gate and the error model, so they share the busy value.
|
|
35
|
+
* Widening only the BUSY type keeps `runAssist`'s signature honest.
|
|
36
|
+
*/
|
|
37
|
+
export type AssistBusyMode = AssistMode | "spread" | "negative";
|
|
38
|
+
|
|
39
|
+
/** The active sub-mode, sent as `context.kind` so video modes get motion/camera phrasing. */
|
|
40
|
+
export type AssistKind = "image" | "scene" | "movie";
|
|
41
|
+
|
|
42
|
+
/** The `specKey` a host's telemetry rides under (matches the host's other calls). */
|
|
43
|
+
type SpecKey = "generate" | "studio";
|
|
44
|
+
|
|
45
|
+
export interface UsePromptAssistOptions {
|
|
46
|
+
/** Sub-mode sent as `context.kind` (image | scene | movie). */
|
|
47
|
+
kind: AssistKind;
|
|
48
|
+
/** Telemetry spec key for the request meta (the host's own key). */
|
|
49
|
+
specKey: SpecKey;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** One selectable text generator, as offered by the backend's discovery route. */
|
|
53
|
+
export interface AssistModel {
|
|
54
|
+
/** Catalog key sent back as `body.model`. */
|
|
55
|
+
model: string;
|
|
56
|
+
/** Seated on a worker right now (answers in ~1s) vs present but cold (first call loads it). */
|
|
57
|
+
serving: boolean;
|
|
58
|
+
framework?: string | null;
|
|
59
|
+
/** True for the fleet default — used when the user has never chosen. */
|
|
60
|
+
default?: boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The per-call typed extras (SPEC §1c). Purely ADDITIVE: a host that passes nothing
|
|
65
|
+
* sends the byte-identical body it sent before, which is what keeps GenerateStation
|
|
66
|
+
* and StudioGenerateTab compiling and behaving unchanged.
|
|
67
|
+
*/
|
|
68
|
+
export interface AssistExtras {
|
|
69
|
+
/**
|
|
70
|
+
* Typed row/identity state merged into `context` beside `kind`. This is the
|
|
71
|
+
* carrier for structured state — `hint` stays free-form user text (§1c ruling).
|
|
72
|
+
*/
|
|
73
|
+
context?: Omit<AssistContextWire, "kind">;
|
|
74
|
+
/**
|
|
75
|
+
* Per-CALL model override. When set, this generator writes THIS call's prompt
|
|
76
|
+
* regardless of the hook-wide `assistModel` — the carrier for per-component
|
|
77
|
+
* assist-model selection (each prompt component can pick its own). Omitted by
|
|
78
|
+
* hosts that pass nothing, which then use the hook `assistModel` / fleet default.
|
|
79
|
+
*/
|
|
80
|
+
model?: string | null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface PromptAssistApi {
|
|
84
|
+
/** The ACTIVE mode while a call is in flight, or null when idle. Blocks concurrency + drives labels. */
|
|
85
|
+
assistBusy: AssistBusyMode | null;
|
|
86
|
+
/** The dismissible inline notice; a failed call sets it and leaves the prompt intact. */
|
|
87
|
+
assistError: string | null;
|
|
88
|
+
/** Clear the inline error notice. */
|
|
89
|
+
dismissError: () => void;
|
|
90
|
+
/**
|
|
91
|
+
* Fire an assist call. `draft` is the host's current prompt text (Enhance requires it;
|
|
92
|
+
* Generate sends it as a loose theme). On success the returned prompt is handed to
|
|
93
|
+
* `apply`; on ANY failure `apply` is never called and `assistError` is set instead.
|
|
94
|
+
*/
|
|
95
|
+
runAssist: (
|
|
96
|
+
mode: AssistMode,
|
|
97
|
+
draft: string,
|
|
98
|
+
apply: (prompt: string) => void,
|
|
99
|
+
extra?: AssistExtras,
|
|
100
|
+
) => Promise<void>;
|
|
101
|
+
/**
|
|
102
|
+
* Generate a NEGATIVE prompt (mode "negative", §1b) — an artifact/quality exclusion
|
|
103
|
+
* list, not prose. `subject` is what is being negated (the row's own prompt); a
|
|
104
|
+
* failure never reaches `apply`, exactly like runAssist.
|
|
105
|
+
*/
|
|
106
|
+
runNegative: (
|
|
107
|
+
opts: { subject?: string; draft?: string; extra?: AssistExtras },
|
|
108
|
+
apply: (negative: string) => void,
|
|
109
|
+
) => Promise<void>;
|
|
110
|
+
/**
|
|
111
|
+
* Run ONE coherent spread across the selected rows (mode "spread", §1a). Resolves
|
|
112
|
+
* to the parsed result, or null when the call failed (the reason is on
|
|
113
|
+
* `assistError`). NEVER N sequential per-row calls — that is the incoherence this
|
|
114
|
+
* whole feature exists to delete.
|
|
115
|
+
*/
|
|
116
|
+
runSpread: (body: SpreadRequestWire) => Promise<SpreadResult | null>;
|
|
117
|
+
/**
|
|
118
|
+
* Classify a field's text (§1d). Deliberately OUTSIDE the busy gate: this is called
|
|
119
|
+
* on blur, must not be blocked by an in-flight generation, and must never surface as
|
|
120
|
+
* an error — any failure reads as a degraded `ambiguous`, i.e. "show both actions".
|
|
121
|
+
*/
|
|
122
|
+
classifyIntent: (text: string, scope?: "segment" | "movie") => Promise<IntentResult>;
|
|
123
|
+
/** The text generators this fleet can actually run (empty until discovery returns). */
|
|
124
|
+
assistModels: AssistModel[];
|
|
125
|
+
/** The selected generator, or null to let the backend use the fleet default. */
|
|
126
|
+
assistModel: string | null;
|
|
127
|
+
/** Choose a generator; persisted so the choice survives a reload. */
|
|
128
|
+
setAssistModel: (model: string | null) => void;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// The user's pick is a PREFERENCE, not app state: persist it in localStorage so it
|
|
132
|
+
// survives a reload, and share it across all three composers (Generate, Studio Clip,
|
|
133
|
+
// Studio Movie) — they each mount their own hook, so a module-level key is what makes
|
|
134
|
+
// the choice feel like one setting rather than three.
|
|
135
|
+
const ASSIST_MODEL_KEY = "hugpy.promptAssist.model";
|
|
136
|
+
|
|
137
|
+
function readStoredModel(): string | null {
|
|
138
|
+
try {
|
|
139
|
+
return window.localStorage.getItem(ASSIST_MODEL_KEY) || null;
|
|
140
|
+
} catch {
|
|
141
|
+
return null; // private mode / storage disabled — not fatal
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Defensively pull `.prompt` (a non-empty string) out of the prompt-assist 200 body
|
|
146
|
+
// ({ prompt, model, kind }). Returns null for any malformed/empty shape so a bad
|
|
147
|
+
// response surfaces as a notice instead of clobbering the prompt with junk. (Lifted
|
|
148
|
+
// verbatim from GenerateStation's local helper so every host parses identically.)
|
|
149
|
+
function readAssistPrompt(value: unknown): string | null {
|
|
150
|
+
if (value != null && typeof value === "object" && "prompt" in value) {
|
|
151
|
+
const p = (value as { prompt?: unknown }).prompt;
|
|
152
|
+
if (typeof p === "string" && p.trim() !== "") return p;
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function usePromptAssist({ kind, specKey }: UsePromptAssistOptions): PromptAssistApi {
|
|
158
|
+
// One in-flight call at a time: `assistBusy` holds the ACTIVE mode or null when idle —
|
|
159
|
+
// it both blocks concurrent calls and drives which button shows its loading label.
|
|
160
|
+
const [assistBusy, setAssistBusy] = useState<AssistBusyMode | null>(null);
|
|
161
|
+
const [assistError, setAssistError] = useState<string | null>(null);
|
|
162
|
+
const [assistModels, setAssistModels] = useState<AssistModel[]>([]);
|
|
163
|
+
const [assistModel, setAssistModelState] = useState<string | null>(readStoredModel);
|
|
164
|
+
|
|
165
|
+
// Discover the offerable generators once per mount. A failure is SILENT on purpose:
|
|
166
|
+
// the picker simply stays empty and assist keeps working on the fleet default, so a
|
|
167
|
+
// discovery hiccup can never block Enhance/Generate.
|
|
168
|
+
useEffect(() => {
|
|
169
|
+
let alive = true;
|
|
170
|
+
(async () => {
|
|
171
|
+
const res = await request<unknown>(hugpyConfig.promptAssistModelsUrl, {
|
|
172
|
+
method: "GET",
|
|
173
|
+
timeoutMs: 15_000,
|
|
174
|
+
meta: { specKey, operation: "prompt.assist.models" },
|
|
175
|
+
});
|
|
176
|
+
if (!alive || !res.ok) return;
|
|
177
|
+
const value = okValue(res);
|
|
178
|
+
const rows =
|
|
179
|
+
value != null && typeof value === "object" && Array.isArray((value as { models?: unknown }).models)
|
|
180
|
+
? ((value as { models: AssistModel[] }).models)
|
|
181
|
+
: [];
|
|
182
|
+
setAssistModels(rows);
|
|
183
|
+
// Drop a stored pick the fleet no longer offers (model retired, worker gone) —
|
|
184
|
+
// otherwise the UI shows a selection that would fail on use.
|
|
185
|
+
setAssistModelState((current) =>
|
|
186
|
+
current && !rows.some((r) => r.model === current) ? null : current,
|
|
187
|
+
);
|
|
188
|
+
})();
|
|
189
|
+
return () => {
|
|
190
|
+
alive = false;
|
|
191
|
+
};
|
|
192
|
+
}, [specKey]);
|
|
193
|
+
|
|
194
|
+
const setAssistModel = useCallback((model: string | null) => {
|
|
195
|
+
setAssistModelState(model);
|
|
196
|
+
try {
|
|
197
|
+
if (model) window.localStorage.setItem(ASSIST_MODEL_KEY, model);
|
|
198
|
+
else window.localStorage.removeItem(ASSIST_MODEL_KEY);
|
|
199
|
+
} catch {
|
|
200
|
+
/* storage disabled — the choice still applies for this session */
|
|
201
|
+
}
|
|
202
|
+
}, []);
|
|
203
|
+
|
|
204
|
+
const dismissError = useCallback(() => setAssistError(null), []);
|
|
205
|
+
|
|
206
|
+
const runAssist = useCallback(
|
|
207
|
+
async (
|
|
208
|
+
mode: AssistMode,
|
|
209
|
+
draft: string,
|
|
210
|
+
apply: (prompt: string) => void,
|
|
211
|
+
extra?: AssistExtras,
|
|
212
|
+
): Promise<void> => {
|
|
213
|
+
if (assistBusy) return; // block concurrent calls
|
|
214
|
+
const trimmed = draft.trim();
|
|
215
|
+
if (mode === "detail" && trimmed === "") return; // Enhance needs a draft
|
|
216
|
+
setAssistError(null);
|
|
217
|
+
setAssistBusy(mode);
|
|
218
|
+
|
|
219
|
+
// `model` is sent ONLY when the user has picked one; omitting it lets the backend
|
|
220
|
+
// apply the fleet default (flux2-klein-9b-uncensored-text-encoder, served from
|
|
221
|
+
// computron). The old comment here claimed "the backend picks the best resolvable
|
|
222
|
+
// chat model" — it did not, it used one hardcoded key; the picker plus a resolved
|
|
223
|
+
// default is what finally made that sentence true. The draft is omitted when blank
|
|
224
|
+
// (Generate from a truly empty box) and context.kind carries the active sub-mode so
|
|
225
|
+
// video modes get motion/camera phrasing.
|
|
226
|
+
//
|
|
227
|
+
// Thinking is suppressed server-side for EVERY generator (the assist route appends
|
|
228
|
+
// a no-think directive and strips any <think>...</think> that survives), so picking
|
|
229
|
+
// a reasoning model here cannot dump a monologue into the prompt box.
|
|
230
|
+
//
|
|
231
|
+
// TYPED CONTEXT (§1c): `extra.context` carries the STRUCTURED row state — this
|
|
232
|
+
// segment, its neighbours, the locked identity — beside `kind`. The backend
|
|
233
|
+
// renders joint modes into plain sentences and attaches the identity's
|
|
234
|
+
// do-not-invent list, which is what stopped the generator making up wardrobes
|
|
235
|
+
// for a bare character name. Omitted entirely by hosts that pass nothing, so
|
|
236
|
+
// their request body is unchanged.
|
|
237
|
+
const body: {
|
|
238
|
+
mode: AssistMode;
|
|
239
|
+
draft?: string;
|
|
240
|
+
model?: string;
|
|
241
|
+
context: AssistContextWire;
|
|
242
|
+
} = {
|
|
243
|
+
mode,
|
|
244
|
+
...(trimmed !== "" ? { draft: trimmed } : {}),
|
|
245
|
+
...((extra?.model ?? assistModel) ? { model: extra?.model ?? assistModel ?? undefined } : {}),
|
|
246
|
+
context: { kind, ...(extra?.context ?? {}) },
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const res = await request<unknown>(hugpyConfig.promptAssistUrl, {
|
|
250
|
+
method: "POST",
|
|
251
|
+
headers: { "Content-Type": "application/json" },
|
|
252
|
+
body: JSON.stringify(body),
|
|
253
|
+
timeoutMs: 90_000, // a few seconds typically; generous for an on-demand worker
|
|
254
|
+
meta: { specKey, operation: `prompt.assist.${mode}` },
|
|
255
|
+
});
|
|
256
|
+
if (!res.ok) {
|
|
257
|
+
setAssistError(describeAppError(errorOf(res)));
|
|
258
|
+
setAssistBusy(null);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
const prompt = readAssistPrompt(okValue(res));
|
|
262
|
+
if (prompt == null) {
|
|
263
|
+
setAssistError("The assistant returned an empty prompt — try again.");
|
|
264
|
+
setAssistBusy(null);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
apply(prompt);
|
|
268
|
+
setAssistBusy(null);
|
|
269
|
+
},
|
|
270
|
+
[assistBusy, kind, specKey, assistModel],
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
// ── mode "negative" (§1b) ────────────────────────────────────────────────
|
|
274
|
+
// A negative prompt is an ARTIFACT/QUALITY EXCLUSION LIST, not prose — reusing the
|
|
275
|
+
// scene system prompt returns a poem, which is why this is its own mode rather than
|
|
276
|
+
// a `detail` call with different words. `prompt` and `negative` carry the same text
|
|
277
|
+
// on the wire (no existing key moved); we read `prompt` via the shared reader.
|
|
278
|
+
const runNegative = useCallback(
|
|
279
|
+
async (
|
|
280
|
+
opts: { subject?: string; draft?: string; extra?: AssistExtras },
|
|
281
|
+
apply: (negative: string) => void,
|
|
282
|
+
): Promise<void> => {
|
|
283
|
+
if (assistBusy) return;
|
|
284
|
+
setAssistError(null);
|
|
285
|
+
setAssistBusy("negative");
|
|
286
|
+
const body: Record<string, unknown> = {
|
|
287
|
+
mode: "negative",
|
|
288
|
+
context: { kind, ...(opts.extra?.context ?? {}) },
|
|
289
|
+
};
|
|
290
|
+
if (opts.subject && opts.subject.trim()) body.subject = opts.subject.trim();
|
|
291
|
+
if (opts.draft && opts.draft.trim()) body.draft = opts.draft.trim();
|
|
292
|
+
const negModel = opts.extra?.model ?? assistModel;
|
|
293
|
+
if (negModel) body.model = negModel;
|
|
294
|
+
|
|
295
|
+
const res = await request<unknown>(hugpyConfig.promptAssistUrl, {
|
|
296
|
+
method: "POST",
|
|
297
|
+
headers: { "Content-Type": "application/json" },
|
|
298
|
+
body: JSON.stringify(body),
|
|
299
|
+
timeoutMs: 90_000,
|
|
300
|
+
meta: { specKey, operation: "prompt.assist.negative" },
|
|
301
|
+
});
|
|
302
|
+
if (!res.ok) {
|
|
303
|
+
setAssistError(describeAppError(errorOf(res)));
|
|
304
|
+
setAssistBusy(null);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
const text = readAssistPrompt(okValue(res));
|
|
308
|
+
if (text == null) {
|
|
309
|
+
setAssistError("The assistant returned an empty negative — try again.");
|
|
310
|
+
setAssistBusy(null);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
apply(text);
|
|
314
|
+
setAssistBusy(null);
|
|
315
|
+
},
|
|
316
|
+
[assistBusy, kind, specKey, assistModel],
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
// ── mode "spread" (§1a) ──────────────────────────────────────────────────
|
|
320
|
+
// ONE call for the whole selection. Returns the parsed result to the caller instead
|
|
321
|
+
// of applying anything itself: only the host knows which editor rows the returned
|
|
322
|
+
// segment_ids map to, and the "apply ONLY to rows that came back" rule is the whole
|
|
323
|
+
// safety property. A 502 carries the raw reply — surfaced honestly by the shared
|
|
324
|
+
// error mapper, never replaced with a fabricated segment.
|
|
325
|
+
const runSpread = useCallback(
|
|
326
|
+
async (body: SpreadRequestWire): Promise<SpreadResult | null> => {
|
|
327
|
+
if (assistBusy) return null;
|
|
328
|
+
setAssistError(null);
|
|
329
|
+
setAssistBusy("spread");
|
|
330
|
+
const payload = { ...body, ...(assistModel ? { model: assistModel } : {}) };
|
|
331
|
+
const res = await request<unknown>(hugpyConfig.promptAssistUrl, {
|
|
332
|
+
method: "POST",
|
|
333
|
+
headers: { "Content-Type": "application/json" },
|
|
334
|
+
body: JSON.stringify(payload),
|
|
335
|
+
// A spread writes N paragraphs in one generation, so it is the longest assist
|
|
336
|
+
// call there is — and on a cold generator it pays a model load first.
|
|
337
|
+
timeoutMs: 180_000,
|
|
338
|
+
meta: { specKey, operation: "prompt.assist.spread" },
|
|
339
|
+
});
|
|
340
|
+
if (!res.ok) {
|
|
341
|
+
setAssistError(describeAppError(errorOf(res)));
|
|
342
|
+
setAssistBusy(null);
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
const parsed = readSpreadResult(okValue(res));
|
|
346
|
+
setAssistBusy(null);
|
|
347
|
+
if (parsed == null) {
|
|
348
|
+
setAssistError("The assistant returned a malformed spread — nothing was changed.");
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
if (parsed.segments.length === 0) {
|
|
352
|
+
setAssistError("The assistant wrote no usable segments — nothing was changed.");
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
return parsed;
|
|
356
|
+
},
|
|
357
|
+
[assistBusy, specKey, assistModel],
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
// ── the intent router (§1d) ──────────────────────────────────────────────
|
|
361
|
+
// Outside the busy gate on purpose (see the API doc above) and outside the error
|
|
362
|
+
// model too: this route ALWAYS returns 200, and a transport failure reads as a
|
|
363
|
+
// degraded "ambiguous" — the UI then offers both actions, which is the correct
|
|
364
|
+
// answer to "we don't know", not an error the user has to dismiss.
|
|
365
|
+
const classifyIntent = useCallback(
|
|
366
|
+
async (text: string, scope: "segment" | "movie" = "segment"): Promise<IntentResult> => {
|
|
367
|
+
const res = await request<unknown>(hugpyConfig.promptIntentUrl, {
|
|
368
|
+
method: "POST",
|
|
369
|
+
headers: { "Content-Type": "application/json" },
|
|
370
|
+
body: JSON.stringify({ text, scope }),
|
|
371
|
+
timeoutMs: 30_000,
|
|
372
|
+
meta: { specKey, operation: "prompt.intent" },
|
|
373
|
+
});
|
|
374
|
+
if (!res.ok) {
|
|
375
|
+
return { intent: "ambiguous", operation: null, confidence: 0, cached: false, degraded: true };
|
|
376
|
+
}
|
|
377
|
+
return readIntentResult(okValue(res));
|
|
378
|
+
},
|
|
379
|
+
[specKey],
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
return {
|
|
383
|
+
assistBusy,
|
|
384
|
+
assistError,
|
|
385
|
+
dismissError,
|
|
386
|
+
runAssist,
|
|
387
|
+
runNegative,
|
|
388
|
+
runSpread,
|
|
389
|
+
classifyIntent,
|
|
390
|
+
assistModels,
|
|
391
|
+
assistModel,
|
|
392
|
+
setAssistModel,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// GET /video/render/presets — WHAT THIS FLEET CAN ACTUALLY RENDER TODAY, read by the
|
|
2
|
+
// console instead of guessed at.
|
|
3
|
+
//
|
|
4
|
+
// This is the fourth and only MEASURED preset surface (video_intel/studio/presets.py:
|
|
5
|
+
// the eight ratified `RenderPreset` rows). The other three answer "what did we
|
|
6
|
+
// curate?"; this one answers "what will come back as pixels?" — and until 2026-07-29
|
|
7
|
+
// NOTHING in this SPA read it. The studio instead carried hand-maintained mirrors of
|
|
8
|
+
// backend facts (a static model-id list, a hand-curated capability union, a
|
|
9
|
+
// `STUDIO_REAL_FLOOR_GB = 6` that had drifted below every real row in the table), so a
|
|
10
|
+
// user could compose a request the server was always going to refuse and only find out
|
|
11
|
+
// at render time.
|
|
12
|
+
//
|
|
13
|
+
// WHAT THE PAYLOAD CARRIES, and why each half matters to a compatibility-aware UI:
|
|
14
|
+
// • `presets[]` — per row: capability, model, precision, geometry (width/height/fps),
|
|
15
|
+
// frame budget, `inputs[]` (what the CALLER must supply), `proven`
|
|
16
|
+
// (this exact path has produced pixels on this fleet), and the
|
|
17
|
+
// placement numbers (`vram_envelope_gb` = the minimum budget that can
|
|
18
|
+
// bind this model; `vram_need_gib` / `fits_render_box` = provenance).
|
|
19
|
+
// • `unavailable[]` — every capability the enum declares that NO preset covers, with
|
|
20
|
+
// the measured blocker and the full refusal text, straight from the
|
|
21
|
+
// registry's own `capability_verdict`. A picker can therefore show
|
|
22
|
+
// the dead options DISABLED with the real reason instead of hiding
|
|
23
|
+
// the fleet's shape from the user.
|
|
24
|
+
//
|
|
25
|
+
// COURTESY LAYER, NEVER A GATE. The server-side refusal (POST /video/studio/i2v's
|
|
26
|
+
// capability gate) stays authoritative — everything derived from this hook only decides
|
|
27
|
+
// what to OFFER, never what to allow. If this fetch fails the surfaces fall back to
|
|
28
|
+
// their prior hand-maintained lists and the server still refuses what it always did.
|
|
29
|
+
//
|
|
30
|
+
// SESSION-CACHED. The table is frozen data derived at request time from a registry that
|
|
31
|
+
// only changes on deploy, so one fetch per page load is enough; the cache is a module
|
|
32
|
+
// singleton shared by every mount (the Clip surface and the Movie composer both read
|
|
33
|
+
// it) rather than a fetch per component.
|
|
34
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
35
|
+
import { z } from "zod";
|
|
36
|
+
import { request, okValue, errorOf, describeAppError } from "../transport/client";
|
|
37
|
+
import { hugpyConfig } from "../config";
|
|
38
|
+
|
|
39
|
+
// Tolerant/passthrough, like every other wire schema in this tree: assert only what we
|
|
40
|
+
// read, so a payload that grows a field never breaks the parse.
|
|
41
|
+
export const renderPresetSchema = z
|
|
42
|
+
.object({
|
|
43
|
+
id: z.string(),
|
|
44
|
+
title: z.string(),
|
|
45
|
+
description: z.string().nullable().optional(),
|
|
46
|
+
/** The PRIMARY capability — what a caller asks for to reach this preset. */
|
|
47
|
+
capability: z.string(),
|
|
48
|
+
/** The full set (every ratified row serves exactly one today). */
|
|
49
|
+
capabilities: z.array(z.string()).nullable().optional(),
|
|
50
|
+
model: z.string(),
|
|
51
|
+
framework: z.string().nullable().optional(),
|
|
52
|
+
task: z.string().nullable().optional(),
|
|
53
|
+
precision: z.string().nullable().optional(),
|
|
54
|
+
/** Human geometry string; "source" on the two ffmpeg enhance rows. */
|
|
55
|
+
geometry: z.string().nullable().optional(),
|
|
56
|
+
// null on the enhance rows — they inherit the source clip's geometry.
|
|
57
|
+
width: z.number().nullable().optional(),
|
|
58
|
+
height: z.number().nullable().optional(),
|
|
59
|
+
fps: z.number().nullable().optional(),
|
|
60
|
+
default_frames: z.number().nullable().optional(),
|
|
61
|
+
max_frames: z.number().nullable().optional(),
|
|
62
|
+
/** What the CALLER must supply — the compatibility spine of this whole module. */
|
|
63
|
+
inputs: z.array(z.string()).nullable().optional(),
|
|
64
|
+
/** Checked claim: this exact path has produced pixels on this fleet. */
|
|
65
|
+
proven: z.boolean().nullable().optional(),
|
|
66
|
+
evidence: z.string().nullable().optional(),
|
|
67
|
+
/** MOVIE ONLY: the preset ids a multi-segment render composes + its joint modes. */
|
|
68
|
+
composes: z.array(z.string()).nullable().optional(),
|
|
69
|
+
joints: z.array(z.string()).nullable().optional(),
|
|
70
|
+
// Placement (added backend-side 2026-07-29). vram_envelope_gb is the MINIMUM
|
|
71
|
+
// vram_budget_gb that can bind this row's model; the other two are provenance.
|
|
72
|
+
vram_envelope_gb: z.number().nullable().optional(),
|
|
73
|
+
vram_need_gib: z.number().nullable().optional(),
|
|
74
|
+
fits_render_box: z.boolean().nullable().optional(),
|
|
75
|
+
})
|
|
76
|
+
.passthrough();
|
|
77
|
+
export type RenderPreset = z.infer<typeof renderPresetSchema>;
|
|
78
|
+
|
|
79
|
+
export const unavailableCapabilitySchema = z
|
|
80
|
+
.object({
|
|
81
|
+
capability: z.string(),
|
|
82
|
+
/** The measured blocker, one clause — good tooltip material. */
|
|
83
|
+
reason: z.string().nullable().optional(),
|
|
84
|
+
/** The full user-facing refusal, including the menu of what IS available. */
|
|
85
|
+
refusal: z.string().nullable().optional(),
|
|
86
|
+
})
|
|
87
|
+
.passthrough();
|
|
88
|
+
export type UnavailableCapability = z.infer<typeof unavailableCapabilitySchema>;
|
|
89
|
+
|
|
90
|
+
const renderPresetsResponseSchema = z
|
|
91
|
+
.object({
|
|
92
|
+
presets: z.array(renderPresetSchema),
|
|
93
|
+
unavailable: z.array(unavailableCapabilitySchema).nullable().optional(),
|
|
94
|
+
menu: z.string().nullable().optional(),
|
|
95
|
+
/** Wan requires num_frames == 4k+1; the runner snaps down to the nearest. */
|
|
96
|
+
frame_cadence: z.number().nullable().optional(),
|
|
97
|
+
render_box: z.string().nullable().optional(),
|
|
98
|
+
render_box_vram_gib: z.number().nullable().optional(),
|
|
99
|
+
})
|
|
100
|
+
.passthrough();
|
|
101
|
+
export type RenderPresetsPayload = z.infer<typeof renderPresetsResponseSchema>;
|
|
102
|
+
|
|
103
|
+
export interface RenderPresetsState {
|
|
104
|
+
presets: RenderPreset[];
|
|
105
|
+
unavailable: UnavailableCapability[];
|
|
106
|
+
menu: string;
|
|
107
|
+
frameCadence: number | null;
|
|
108
|
+
renderBox: string;
|
|
109
|
+
renderBoxVramGib: number | null;
|
|
110
|
+
loading: boolean;
|
|
111
|
+
error: string | null;
|
|
112
|
+
/** True once a fetch has SUCCEEDED — the "is it safe to filter by this?" flag.
|
|
113
|
+
* Every consumer must fall back to its unfiltered list while this is false, so a
|
|
114
|
+
* slow or failed discovery call never presents an empty picker. */
|
|
115
|
+
loaded: boolean;
|
|
116
|
+
refresh: () => void;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ── module-singleton session cache ──────────────────────────────────────────
|
|
120
|
+
// One fetch per page load, shared by every mount. `inFlight` coalesces concurrent
|
|
121
|
+
// first-mounts (the Clip surface and the Movie composer mount together) into one call.
|
|
122
|
+
let cached: RenderPresetsPayload | null = null;
|
|
123
|
+
let inFlight: Promise<void> | null = null;
|
|
124
|
+
const subscribers = new Set<() => void>();
|
|
125
|
+
|
|
126
|
+
function notify(): void {
|
|
127
|
+
for (const fn of subscribers) fn();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function fetchOnce(force: boolean): Promise<string | null> {
|
|
131
|
+
if (cached && !force) return null;
|
|
132
|
+
if (inFlight && !force) {
|
|
133
|
+
await inFlight;
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
let err: string | null = null;
|
|
137
|
+
const p = (async () => {
|
|
138
|
+
const res = await request<unknown>(hugpyConfig.renderPresetsUrl, {
|
|
139
|
+
meta: { specKey: "studio", operation: "render.presets.list" },
|
|
140
|
+
});
|
|
141
|
+
if (!res.ok) {
|
|
142
|
+
err = describeAppError(errorOf(res));
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const parsed = renderPresetsResponseSchema.safeParse(okValue(res));
|
|
146
|
+
if (!parsed.success) {
|
|
147
|
+
err = "Malformed render-presets response.";
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
cached = parsed.data;
|
|
151
|
+
})();
|
|
152
|
+
inFlight = p;
|
|
153
|
+
try {
|
|
154
|
+
await p;
|
|
155
|
+
} finally {
|
|
156
|
+
inFlight = null;
|
|
157
|
+
notify();
|
|
158
|
+
}
|
|
159
|
+
return err;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** TEST/DEV seam: drop the session cache so the next mount re-fetches. */
|
|
163
|
+
export function resetRenderPresetsCache(): void {
|
|
164
|
+
cached = null;
|
|
165
|
+
inFlight = null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* The fleet's measured render capability, as data. Never throws; a failed fetch
|
|
170
|
+
* reports `loaded:false` + an error string and every consumer keeps its fallback.
|
|
171
|
+
*
|
|
172
|
+
* `active` gates the network call the same way useStudioPresets/useMoviePresets do,
|
|
173
|
+
* so a surface that is not mounted costs nothing.
|
|
174
|
+
*/
|
|
175
|
+
export function useRenderPresets(active = true): RenderPresetsState {
|
|
176
|
+
const [, bump] = useState(0);
|
|
177
|
+
const [error, setError] = useState<string | null>(null);
|
|
178
|
+
const [loading, setLoading] = useState(false);
|
|
179
|
+
const mounted = useRef(true);
|
|
180
|
+
|
|
181
|
+
useEffect(() => {
|
|
182
|
+
mounted.current = true;
|
|
183
|
+
const rerender = () => {
|
|
184
|
+
if (mounted.current) bump((n) => n + 1);
|
|
185
|
+
};
|
|
186
|
+
subscribers.add(rerender);
|
|
187
|
+
return () => {
|
|
188
|
+
mounted.current = false;
|
|
189
|
+
subscribers.delete(rerender);
|
|
190
|
+
};
|
|
191
|
+
}, []);
|
|
192
|
+
|
|
193
|
+
const run = useCallback(async (force: boolean) => {
|
|
194
|
+
if (cached && !force) return;
|
|
195
|
+
setLoading(true);
|
|
196
|
+
const err = await fetchOnce(force);
|
|
197
|
+
if (!mounted.current) return;
|
|
198
|
+
setLoading(false);
|
|
199
|
+
setError(err);
|
|
200
|
+
}, []);
|
|
201
|
+
|
|
202
|
+
useEffect(() => {
|
|
203
|
+
if (!active) return;
|
|
204
|
+
void run(false);
|
|
205
|
+
}, [active, run]);
|
|
206
|
+
|
|
207
|
+
const refresh = useCallback(() => void run(true), [run]);
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
presets: cached?.presets ?? [],
|
|
211
|
+
unavailable: cached?.unavailable ?? [],
|
|
212
|
+
menu: cached?.menu ?? "",
|
|
213
|
+
frameCadence: cached?.frame_cadence ?? null,
|
|
214
|
+
renderBox: cached?.render_box ?? "",
|
|
215
|
+
renderBoxVramGib: cached?.render_box_vram_gib ?? null,
|
|
216
|
+
loading,
|
|
217
|
+
error,
|
|
218
|
+
loaded: cached != null,
|
|
219
|
+
refresh,
|
|
220
|
+
};
|
|
221
|
+
}
|