@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,575 @@
|
|
|
1
|
+
// IDENTITY PROFILES (studio stage (a)) — the durable "the reference set IS the
|
|
2
|
+
// identity" library, as a hook shared by every surface that touches a profile
|
|
3
|
+
// (the Clip Generate tab, the Movie composer, and the dedicated Identities
|
|
4
|
+
// station). One concept, backed by the server store:
|
|
5
|
+
// • list — GET /video/identity-profiles → { profiles: [...] }
|
|
6
|
+
// • create — POST /video/identity-profiles { name, reference_images[1..4], notes? }
|
|
7
|
+
// • update — PATCH /video/identity-profiles/<slug> { name?, notes?, reference_images? }
|
|
8
|
+
// (partial — an omitted key is left untouched server-side); renaming is
|
|
9
|
+
// DISPLAY-ONLY, the slug never changes (see the backend docstring —
|
|
10
|
+
// templates/specs reference a profile by slug, so re-slugging on rename
|
|
11
|
+
// would silently strand them).
|
|
12
|
+
// • remove — DELETE /video/identity-profiles/<slug> (ARCHIVES, never erases)
|
|
13
|
+
// • resolve — ingest a profile's saved reference paths back into MediaRefs so the
|
|
14
|
+
// attach flow lands them in the SAME state a hand-picked upload does
|
|
15
|
+
// (as if hand-picked — the durable form of the identity, not a copy).
|
|
16
|
+
//
|
|
17
|
+
// Data-not-throws, like useProjects/useStudioClips (from which the fetch idiom is
|
|
18
|
+
// cloned): a failed list just yields []; create returns a discriminated result so a
|
|
19
|
+
// caller can tell a duplicate-name (409) from other failures without a try/catch.
|
|
20
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
21
|
+
import { z } from "zod";
|
|
22
|
+
import { request, okValue, errorOf, describeAppError } from "../../transport/client";
|
|
23
|
+
import {
|
|
24
|
+
hugpyConfig,
|
|
25
|
+
identityProfileUrl,
|
|
26
|
+
identityProfileSettingsUrl,
|
|
27
|
+
identityVersionUrl,
|
|
28
|
+
identityVersionActivateUrl,
|
|
29
|
+
} from "../../config";
|
|
30
|
+
import { mediaRefSchema } from "../../video/contract";
|
|
31
|
+
import type { MediaRef } from "../../video/contract";
|
|
32
|
+
|
|
33
|
+
// ── char360 S4 — VIDEO → CHARACTER VIEW-SET extraction ──────────────────────
|
|
34
|
+
// All-optional knobs for POST /video/identity-profiles/video-extract's
|
|
35
|
+
// `char360_params`; blank/omitted == the service default for every field (the
|
|
36
|
+
// Identities station's Advanced section only sends what the user actually set).
|
|
37
|
+
export const char360ParamsSchema = z
|
|
38
|
+
.object({
|
|
39
|
+
stride: z.number().optional(),
|
|
40
|
+
yolo_model: z.string().optional(),
|
|
41
|
+
min_h_frac: z.number().optional(),
|
|
42
|
+
cluster_dist: z.number().optional(),
|
|
43
|
+
min_faces: z.number().optional(),
|
|
44
|
+
})
|
|
45
|
+
.partial();
|
|
46
|
+
export type Char360Params = z.infer<typeof char360ParamsSchema>;
|
|
47
|
+
|
|
48
|
+
/** enqueue-only outcome of extractFromVideo — polling for the terminal
|
|
49
|
+
* result.ok/result.error lives in the caller (mirrors the /generate poll
|
|
50
|
+
* loop), same reason as CreateProfileResult: a flat shape, not a discriminated
|
|
51
|
+
* union, since the scoped tsconfig runs strictNullChecks OFF. */
|
|
52
|
+
export interface ExtractFromVideoResult {
|
|
53
|
+
ok: boolean;
|
|
54
|
+
jobId?: string;
|
|
55
|
+
message?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ── wire schema (tolerant/passthrough — assert only what the UI reads) ──────────
|
|
59
|
+
|
|
60
|
+
// One TURNAROUND RECONSTRUCTION ("character sheet") a profile has accrued — an
|
|
61
|
+
// additive field the reconstruction flow (POST .../reconstruction) writes: an id,
|
|
62
|
+
// when it was made, the ordered generated view paths (positionally matching the
|
|
63
|
+
// requested view names: front / ¾ / profile / back, each served via mediaBytesUrl),
|
|
64
|
+
// and the prompt/seed it was rolled with. Tolerant/passthrough — every field but
|
|
65
|
+
// recon_id/views is optional so a lean record still parses.
|
|
66
|
+
// src/stations/studio/useIdentityProfiles.ts
|
|
67
|
+
|
|
68
|
+
// ── wire schema (tolerant/passthrough — assert only what the UI reads) ──────────
|
|
69
|
+
|
|
70
|
+
export const reconstructionSchema = z
|
|
71
|
+
.object({
|
|
72
|
+
recon_id: z.string(),
|
|
73
|
+
created_at: z.number().nullable().optional(),
|
|
74
|
+
// Lenient: accept missing views and default to empty array
|
|
75
|
+
views: z.array(z.any()).nullable().optional().transform(v => v || []),
|
|
76
|
+
prompt: z.string().nullable().optional(),
|
|
77
|
+
seed: z.number().nullable().optional(),
|
|
78
|
+
// Lenient: accept any string for mode so legacy ones don't break
|
|
79
|
+
mode: z.string().nullable().optional(),
|
|
80
|
+
frame_count: z.number().nullable().optional(),
|
|
81
|
+
degrees_per_frame: z.number().nullable().optional(),
|
|
82
|
+
mesh: z.any().nullable().optional(),
|
|
83
|
+
})
|
|
84
|
+
.passthrough();
|
|
85
|
+
export type Reconstruction = z.infer<typeof reconstructionSchema>;
|
|
86
|
+
|
|
87
|
+
// ── IDENTITY VERSIONS slice (dev/IDENTITY-VERSIONS-SLICE.md) — additive, every
|
|
88
|
+
// field defaulted so an old (pre-versions) profile payload still parses clean. ──
|
|
89
|
+
|
|
90
|
+
// One named render-set an identity has accrued — the clay base (minted on first
|
|
91
|
+
// Generate, always kept) plus N derived "textured"/"styled" versions. `kind` is
|
|
92
|
+
// lenient: any string that isn't a recognized kind quietly falls back to "clay"
|
|
93
|
+
// rather than failing the whole profile parse.
|
|
94
|
+
export const identityVersionSchema = z
|
|
95
|
+
.object({
|
|
96
|
+
version_id: z.string(),
|
|
97
|
+
name: z.string().nullable().optional().transform(v => v || ""),
|
|
98
|
+
kind: z
|
|
99
|
+
.string()
|
|
100
|
+
.nullable()
|
|
101
|
+
.optional()
|
|
102
|
+
.transform((v): "clay" | "textured" | "styled" => (v === "textured" || v === "styled" ? v : "clay")),
|
|
103
|
+
recon_id: z.string().nullable().optional(),
|
|
104
|
+
created_at: z.number().nullable().optional(),
|
|
105
|
+
canonical: z.array(z.string()).nullable().optional().transform(v => v || []),
|
|
106
|
+
notes: z.string().nullable().optional(),
|
|
107
|
+
})
|
|
108
|
+
.passthrough();
|
|
109
|
+
export type IdentityVersion = z.infer<typeof identityVersionSchema>;
|
|
110
|
+
|
|
111
|
+
// Per-identity generation defaults (the Settings panel + the happy-path bare
|
|
112
|
+
// `/generate` click). Every field individually defaulted so `gen_settings.parse({})`
|
|
113
|
+
// (an entirely absent block, on an old payload) yields the full happy-path object.
|
|
114
|
+
export const genSettingsSchema = z
|
|
115
|
+
.object({
|
|
116
|
+
texture: z.boolean().nullable().optional().transform(v => v ?? true),
|
|
117
|
+
pose: z
|
|
118
|
+
.string()
|
|
119
|
+
.nullable()
|
|
120
|
+
.optional()
|
|
121
|
+
.transform((v): "none" | "t-pose" => (v === "t-pose" ? "t-pose" : "none")),
|
|
122
|
+
frame_count: z.number().nullable().optional().transform(v => v ?? 72),
|
|
123
|
+
fps: z.number().nullable().optional().transform(v => v ?? 24),
|
|
124
|
+
width: z.number().nullable().optional().transform(v => v ?? 768),
|
|
125
|
+
height: z.number().nullable().optional().transform(v => v ?? 768),
|
|
126
|
+
auto_promote: z.boolean().nullable().optional().transform(v => v ?? true),
|
|
127
|
+
front_ref: z.string().nullable().optional().transform(v => v ?? null),
|
|
128
|
+
remove_background: z.boolean().nullable().optional().transform(v => v ?? true),
|
|
129
|
+
// Per-identity VISION MODEL for the 3D-imaging front-select step. null (default)
|
|
130
|
+
// == the fleet-default VL model (the 3B) — zero regression. A non-null value is an
|
|
131
|
+
// image-text-to-text model key (validated server-side against the live registry).
|
|
132
|
+
vision_model: z.string().nullable().optional().transform(v => v ?? null),
|
|
133
|
+
// CLEANUP-PROMPT slice (C4 — Advanced-panel reachability): a positive-worded AVOID
|
|
134
|
+
// instruction woven into the T-pose front render (e.g. "no object on her back, no
|
|
135
|
+
// logos, clean bare back"). "" (default) == no steer, byte-identical to today.
|
|
136
|
+
cleanup_prompt: z.string().nullable().optional().transform(v => v ?? ""),
|
|
137
|
+
// A TRUE negative forwarded to the studio Wan-VACE render (reconstruction + the
|
|
138
|
+
// mesh route's id_lock front render). "" (default) == today's exact call.
|
|
139
|
+
negative_prompt: z.string().nullable().optional().transform(v => v ?? ""),
|
|
140
|
+
})
|
|
141
|
+
.passthrough();
|
|
142
|
+
export type GenSettings = z.infer<typeof genSettingsSchema>;
|
|
143
|
+
|
|
144
|
+
/** The happy-path defaults — a profile with no `gen_settings` block at all (every
|
|
145
|
+
* payload before this slice shipped) resolves to exactly this. */
|
|
146
|
+
export const DEFAULT_GEN_SETTINGS: GenSettings = genSettingsSchema.parse({});
|
|
147
|
+
|
|
148
|
+
export const identityProfileSchema = z
|
|
149
|
+
.object({
|
|
150
|
+
slug: z.string(),
|
|
151
|
+
// Lenient: if any core fields are missing/null, safely default them
|
|
152
|
+
name: z.string().nullable().optional().transform(v => v || "Unnamed"),
|
|
153
|
+
reference_images: z.array(z.string()).nullable().optional().transform(v => v || []),
|
|
154
|
+
created_at: z.number().nullable().optional(),
|
|
155
|
+
notes: z.string().nullable().optional(),
|
|
156
|
+
reconstructions: z.array(reconstructionSchema).nullable().optional().transform(v => v || []),
|
|
157
|
+
canonical: z.array(z.string()).nullable().optional().transform(v => v || []),
|
|
158
|
+
// ADDITIVE per-view angle provenance (dev/CHAR360-FEATURE-PLAN.md's parallel
|
|
159
|
+
// slice) — POSITIONALLY ALIGNED to `canonical` (canonical_angles[i] describes
|
|
160
|
+
// canonical[i]). OMITTED ENTIRELY on any profile from before the backend
|
|
161
|
+
// backfill (identity_profiles.py:1556) — optional with no default-to-[]
|
|
162
|
+
// transform, so "absent" stays distinguishable from "present but empty" for
|
|
163
|
+
// the UI's honest-fallback check (see GenIdentityBar.tsx's viewLabel). An
|
|
164
|
+
// individual element may be `null` (unknown for just that view) rather than a
|
|
165
|
+
// guessed 0.0. The backend already drops the key on a length mismatch with
|
|
166
|
+
// `canonical`, but the UI re-checks the lengths itself before trusting it
|
|
167
|
+
// (never assume the wire honored its own contract).
|
|
168
|
+
canonical_angles: z.array(z.number().nullable()).nullable().optional(),
|
|
169
|
+
// versions[] / active_version / gen_settings may be ABSENT entirely on any
|
|
170
|
+
// payload served before an API restart picks up the backend half of this
|
|
171
|
+
// slice — all three default so the station renders exactly as it does today.
|
|
172
|
+
versions: z.array(identityVersionSchema).nullable().optional().transform(v => v || []),
|
|
173
|
+
active_version: z.string().nullable().optional().transform(v => v ?? null),
|
|
174
|
+
gen_settings: genSettingsSchema
|
|
175
|
+
.nullable()
|
|
176
|
+
.optional()
|
|
177
|
+
.transform(v => v ?? genSettingsSchema.parse({})),
|
|
178
|
+
})
|
|
179
|
+
.passthrough();
|
|
180
|
+
export type IdentityProfile = z.infer<typeof identityProfileSchema>;
|
|
181
|
+
|
|
182
|
+
const listResponseSchema = z.object({ profiles: z.array(identityProfileSchema) }).passthrough();
|
|
183
|
+
const createResponseSchema = z.object({ profile: identityProfileSchema }).passthrough();
|
|
184
|
+
|
|
185
|
+
/** create() outcome — a duplicate name (409) is distinguishable from any other
|
|
186
|
+
* failure WITHOUT a thrown exception. A FLAT shape (not a discriminated union):
|
|
187
|
+
* the scoped tsconfig runs strictNullChecks OFF, under which the repo's Result
|
|
188
|
+
* narrowing is unreliable (hence its okValue/errorOf helpers) — so every field is
|
|
189
|
+
* directly accessible off `ok` instead of relying on control-flow narrowing. */
|
|
190
|
+
export interface CreateProfileResult {
|
|
191
|
+
ok: boolean;
|
|
192
|
+
profile?: IdentityProfile;
|
|
193
|
+
duplicate?: boolean;
|
|
194
|
+
message?: string;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** update() outcome — same flat (not discriminated-union) shape as
|
|
198
|
+
* CreateProfileResult, for the same strictNullChecks-off narrowing reason. A
|
|
199
|
+
* PATCH never 409s (renaming never re-slugs, so it can't collide with another
|
|
200
|
+
* profile's slug) — `message` alone carries any failure (404 unknown slug,
|
|
201
|
+
* 400 bad body, etc.). */
|
|
202
|
+
export interface UpdateProfileResult {
|
|
203
|
+
ok: boolean;
|
|
204
|
+
profile?: IdentityProfile;
|
|
205
|
+
message?: string;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Outcome shared by every version mutation (activate / rename / archive) — flat
|
|
209
|
+
* (not a discriminated union), same reason as CreateProfileResult/UpdateProfileResult.
|
|
210
|
+
* `profile` is populated whenever the caller could reconcile the identity's new
|
|
211
|
+
* state (either the response echoed it, or a same-slug re-GET did) — never
|
|
212
|
+
* required for `ok` to be true, since the wire contract doesn't fix a response
|
|
213
|
+
* envelope for these three (only the settings PATCH is documented as → {profile}). */
|
|
214
|
+
export interface VersionMutationResult {
|
|
215
|
+
ok: boolean;
|
|
216
|
+
profile?: IdentityProfile;
|
|
217
|
+
message?: string;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export interface IdentityProfilesState {
|
|
221
|
+
profiles: IdentityProfile[];
|
|
222
|
+
loading: boolean;
|
|
223
|
+
error: string | null;
|
|
224
|
+
/** Re-pull the list (foreground). */
|
|
225
|
+
reload: () => void;
|
|
226
|
+
/** Save a new profile from a name + already-attached reference URIs. */
|
|
227
|
+
create: (name: string, referenceImages: string[], notes?: string) => Promise<CreateProfileResult>;
|
|
228
|
+
/** Partially edit an existing profile — an omitted field is left untouched
|
|
229
|
+
* server-side. Renaming is display-only (the slug never changes). Passing an
|
|
230
|
+
* empty `referenceImages` array is rejected by the server (an identity keeps
|
|
231
|
+
* >=1 reference); omit the field (undefined) to leave the current set alone. */
|
|
232
|
+
update: (
|
|
233
|
+
slug: string,
|
|
234
|
+
fields: { name?: string; notes?: string; referenceImages?: string[] },
|
|
235
|
+
) => Promise<UpdateProfileResult>;
|
|
236
|
+
/** Archive a profile by slug (never-delete: the server moves it under _deleted). */
|
|
237
|
+
remove: (slug: string) => Promise<boolean>;
|
|
238
|
+
/** Ingest a profile's saved reference paths → MediaRefs (drops any that fail), so
|
|
239
|
+
* the caller can attach them into its OWN reference state — as if hand-picked. */
|
|
240
|
+
resolveRefs: (profile: IdentityProfile) => Promise<MediaRef[]>;
|
|
241
|
+
/** Ingest a profile's CANONICAL turntable views → MediaRefs, in order (drops any
|
|
242
|
+
* that fail to ingest, but never silently substitutes a different set). This is
|
|
243
|
+
* `profile.canonical` — the top-level field, which already reflects the profile's
|
|
244
|
+
* ACTIVE version (see the `canonical` field doc above) — NOT a walk of
|
|
245
|
+
* `versions[]`. Distinct from `resolveRefs` (which resolves the raw, messy
|
|
246
|
+
* `reference_images` source uploads) so Studio's `IdentityProfileControls` /
|
|
247
|
+
* `StudioMovieComposer` — both still on `resolveRefs` — are byte-for-byte
|
|
248
|
+
* unaffected by this addition. An EMPTY `profile.canonical` resolves to an EMPTY
|
|
249
|
+
* array (never falls back to `reference_images`) — the caller must treat that as
|
|
250
|
+
* an honest failure, not attach nothing silently. */
|
|
251
|
+
resolveCanonical: (profile: IdentityProfile) => Promise<MediaRef[]>;
|
|
252
|
+
/** Ingest ONE of a profile's CANONICAL views (by index into `profile.canonical`)
|
|
253
|
+
* → a MediaRef, or null when the index is out of range or the image won't ingest.
|
|
254
|
+
*
|
|
255
|
+
* Additive sibling to `resolveCanonical`, for the Generate station's
|
|
256
|
+
* one-carrying-view bind: the generate runner consumes only the FIRST image part
|
|
257
|
+
* (imagegen.py:117 `start_frame = image_paths[0]`), so a bind attaches exactly one
|
|
258
|
+
* view — resolving the whole set to keep a single ref would ingest N-1 images for
|
|
259
|
+
* nothing (visible cost once canonical grows to 8 azimuth views). Returns null
|
|
260
|
+
* rather than throwing: the caller turns it into a loud, human message and NEVER
|
|
261
|
+
* falls back to `reference_images`. `resolveRefs` / `resolveCanonical` are both
|
|
262
|
+
* untouched — Studio is byte-for-byte unaffected. */
|
|
263
|
+
resolveCanonicalView: (
|
|
264
|
+
profile: IdentityProfile,
|
|
265
|
+
index: number,
|
|
266
|
+
) => Promise<MediaRef | null>;
|
|
267
|
+
/** Partially save this identity's generation settings (texture/pose/turntable
|
|
268
|
+
* geometry/auto-promote/front-ref/background-removal) — an omitted key is left
|
|
269
|
+
* untouched server-side, same partial-PATCH idiom as `update`. Prefills the
|
|
270
|
+
* happy-path bare `/generate` click. */
|
|
271
|
+
saveSettings: (slug: string, fields: Partial<GenSettings>) => Promise<UpdateProfileResult>;
|
|
272
|
+
/** Make one version the identity's ACTIVE version — the resolver takes ITS
|
|
273
|
+
* canonical as the id_lock DNA for any future generation that doesn't name a
|
|
274
|
+
* specific `identity_version`. */
|
|
275
|
+
activateVersion: (slug: string, versionId: string) => Promise<VersionMutationResult>;
|
|
276
|
+
/** Rename one version (display-only, mirrors profile rename). */
|
|
277
|
+
renameVersion: (slug: string, versionId: string, name: string) => Promise<VersionMutationResult>;
|
|
278
|
+
/** Archive one version (never-delete: recoverable, not erased). The server
|
|
279
|
+
* refuses this with a 400 for the base (clay) version and for the currently
|
|
280
|
+
* active version — surfaced via `message`. */
|
|
281
|
+
archiveVersion: (slug: string, versionId: string) => Promise<VersionMutationResult>;
|
|
282
|
+
/** char360 S4 — enqueue a video → character-view-set extraction: POST
|
|
283
|
+
* /video/identity-profiles/video-extract { source, target, char360_params? }
|
|
284
|
+
* → { job_id }. `target` is "create" (mints new profile(s)) or an existing
|
|
285
|
+
* profile's slug (adds views to it). ENQUEUE ONLY — this just returns the
|
|
286
|
+
* job id; the caller polls `videoJobUrl(jobId)` (GET /video/jobs/<id>) until
|
|
287
|
+
* `result.ok` is true/false, same poll shape as the /generate flow, then
|
|
288
|
+
* calls `reload()` on success (there is no slug list in the payload). */
|
|
289
|
+
extractFromVideo: (
|
|
290
|
+
source: MediaRef,
|
|
291
|
+
target: "create" | string,
|
|
292
|
+
params?: Char360Params,
|
|
293
|
+
) => Promise<ExtractFromVideoResult>;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** Ingest ONE server path (already under the storage jail — a profile only ever
|
|
297
|
+
* stores jailed paths) into a resolved MediaRef, or null if it can't be resolved.
|
|
298
|
+
* Reuses POST /video/ingest, the exact first hop the upload flow already runs. */
|
|
299
|
+
async function ingestPath(path: string): Promise<MediaRef | null> {
|
|
300
|
+
const res = await request<unknown>(hugpyConfig.videoIngestUrl, {
|
|
301
|
+
method: "POST",
|
|
302
|
+
body: JSON.stringify({ path }),
|
|
303
|
+
headers: { "Content-Type": "application/json" },
|
|
304
|
+
meta: { specKey: "studio", operation: "identity.profile.ingest" },
|
|
305
|
+
});
|
|
306
|
+
if (!res.ok) return null;
|
|
307
|
+
const parsed = mediaRefSchema.safeParse(okValue(res));
|
|
308
|
+
return parsed.success ? parsed.data : null;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export function useIdentityProfiles(): IdentityProfilesState {
|
|
312
|
+
const [profiles, setProfiles] = useState<IdentityProfile[]>([]);
|
|
313
|
+
const [loading, setLoading] = useState(true);
|
|
314
|
+
const [error, setError] = useState<string | null>(null);
|
|
315
|
+
|
|
316
|
+
const mounted = useRef(true);
|
|
317
|
+
const inFlight = useRef(false);
|
|
318
|
+
|
|
319
|
+
const load = useCallback(async () => {
|
|
320
|
+
if (inFlight.current) return;
|
|
321
|
+
inFlight.current = true;
|
|
322
|
+
try {
|
|
323
|
+
const res = await request<unknown>(hugpyConfig.identityProfilesUrl, {
|
|
324
|
+
meta: { specKey: "studio", operation: "identity.profiles.list" },
|
|
325
|
+
});
|
|
326
|
+
if (!mounted.current) return;
|
|
327
|
+
if (!res.ok) {
|
|
328
|
+
setLoading(false);
|
|
329
|
+
setError(describeAppError(errorOf(res)));
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
const parsed = listResponseSchema.safeParse(okValue(res));
|
|
333
|
+
if (!parsed.success) {
|
|
334
|
+
console.error("Profile parsing error:", parsed.error); // Add this line!
|
|
335
|
+
setLoading(false);
|
|
336
|
+
setError("Malformed identity-profiles response.");
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
setProfiles(parsed.data.profiles);
|
|
340
|
+
setLoading(false);
|
|
341
|
+
setError(null);
|
|
342
|
+
} finally {
|
|
343
|
+
inFlight.current = false;
|
|
344
|
+
}
|
|
345
|
+
}, []);
|
|
346
|
+
|
|
347
|
+
useEffect(() => {
|
|
348
|
+
mounted.current = true;
|
|
349
|
+
void load();
|
|
350
|
+
return () => {
|
|
351
|
+
mounted.current = false;
|
|
352
|
+
};
|
|
353
|
+
}, [load]);
|
|
354
|
+
|
|
355
|
+
const create = useCallback<IdentityProfilesState["create"]>(
|
|
356
|
+
async (name, referenceImages, notes) => {
|
|
357
|
+
const res = await request<unknown>(hugpyConfig.identityProfilesUrl, {
|
|
358
|
+
method: "POST",
|
|
359
|
+
body: JSON.stringify({
|
|
360
|
+
name,
|
|
361
|
+
reference_images: referenceImages,
|
|
362
|
+
...(notes ? { notes } : {}),
|
|
363
|
+
}),
|
|
364
|
+
headers: { "Content-Type": "application/json" },
|
|
365
|
+
meta: { specKey: "studio", operation: "identity.profile.create" },
|
|
366
|
+
});
|
|
367
|
+
if (!res.ok) {
|
|
368
|
+
const err = errorOf(res);
|
|
369
|
+
const duplicate = err.kind === "server" && err.status === 409;
|
|
370
|
+
return { ok: false, duplicate, message: describeAppError(err) };
|
|
371
|
+
}
|
|
372
|
+
const parsed = createResponseSchema.safeParse(okValue(res));
|
|
373
|
+
if (!parsed.success) {
|
|
374
|
+
return { ok: false, duplicate: false, message: "Malformed create response." };
|
|
375
|
+
}
|
|
376
|
+
const profile = parsed.data.profile;
|
|
377
|
+
// Optimistic prepend (newest-first, matching the server sort) so the new
|
|
378
|
+
// profile is pickable immediately; a later reload reconciles.
|
|
379
|
+
if (mounted.current) setProfiles((prev) => [profile, ...prev.filter((p) => p.slug !== profile.slug)]);
|
|
380
|
+
return { ok: true, profile };
|
|
381
|
+
},
|
|
382
|
+
[],
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
const update = useCallback<IdentityProfilesState["update"]>(async (slug, fields) => {
|
|
386
|
+
const body: Record<string, unknown> = {};
|
|
387
|
+
if (fields.name !== undefined) body.name = fields.name;
|
|
388
|
+
if (fields.notes !== undefined) body.notes = fields.notes;
|
|
389
|
+
if (fields.referenceImages !== undefined) body.reference_images = fields.referenceImages;
|
|
390
|
+
const res = await request<unknown>(identityProfileUrl(slug), {
|
|
391
|
+
method: "PATCH",
|
|
392
|
+
body: JSON.stringify(body),
|
|
393
|
+
headers: { "Content-Type": "application/json" },
|
|
394
|
+
meta: { specKey: "studio", operation: "identity.profile.update" },
|
|
395
|
+
});
|
|
396
|
+
if (!res.ok) {
|
|
397
|
+
return { ok: false, message: describeAppError(errorOf(res)) };
|
|
398
|
+
}
|
|
399
|
+
// The PATCH response is the same {profile} envelope create() parses.
|
|
400
|
+
const parsed = createResponseSchema.safeParse(okValue(res));
|
|
401
|
+
if (!parsed.success) {
|
|
402
|
+
return { ok: false, message: "Malformed update response." };
|
|
403
|
+
}
|
|
404
|
+
const profile = parsed.data.profile;
|
|
405
|
+
// Replace in place (slug is stable across a PATCH, so this is a straight swap,
|
|
406
|
+
// never a re-sort); a later reload still reconciles.
|
|
407
|
+
if (mounted.current) {
|
|
408
|
+
setProfiles((prev) => prev.map((p) => (p.slug === profile.slug ? profile : p)));
|
|
409
|
+
}
|
|
410
|
+
return { ok: true, profile };
|
|
411
|
+
}, []);
|
|
412
|
+
|
|
413
|
+
const remove = useCallback(async (slug: string) => {
|
|
414
|
+
const res = await request<unknown>(identityProfileUrl(slug), {
|
|
415
|
+
method: "DELETE",
|
|
416
|
+
meta: { specKey: "studio", operation: "identity.profile.delete" },
|
|
417
|
+
});
|
|
418
|
+
if (!res.ok) return false;
|
|
419
|
+
if (mounted.current) setProfiles((prev) => prev.filter((p) => p.slug !== slug));
|
|
420
|
+
return true;
|
|
421
|
+
}, []);
|
|
422
|
+
|
|
423
|
+
const resolveRefs = useCallback(async (profile: IdentityProfile) => {
|
|
424
|
+
const refs = await Promise.all(profile.reference_images.map((p) => ingestPath(p)));
|
|
425
|
+
return refs.filter((r): r is MediaRef => r != null);
|
|
426
|
+
}, []);
|
|
427
|
+
|
|
428
|
+
// Additive sibling to resolveRefs — resolves the profile's CANONICAL turntable
|
|
429
|
+
// views (top-level `profile.canonical`, already active-version-aware) instead of
|
|
430
|
+
// the raw `reference_images` source uploads. Order is preserved (ref_00..ref_03,
|
|
431
|
+
// 90° apart) since it is semantic for turntable views — Promise.all over an
|
|
432
|
+
// already-ordered array preserves index order regardless of individual ingest
|
|
433
|
+
// timing. resolveRefs is left completely untouched so Studio's
|
|
434
|
+
// IdentityProfileControls / StudioMovieComposer keep today's exact behavior.
|
|
435
|
+
const resolveCanonical = useCallback(async (profile: IdentityProfile) => {
|
|
436
|
+
const refs = await Promise.all(profile.canonical.map((p) => ingestPath(p)));
|
|
437
|
+
return refs.filter((r): r is MediaRef => r != null);
|
|
438
|
+
}, []);
|
|
439
|
+
|
|
440
|
+
// Additive sibling to resolveCanonical — ingests exactly ONE canonical view (the
|
|
441
|
+
// Generate station's carrying view). Bounds-checked because the index comes from a
|
|
442
|
+
// UI row that may have been rendered against a since-refreshed profile list: an
|
|
443
|
+
// out-of-range index must resolve to null (→ a loud caller message), never to the
|
|
444
|
+
// wrong view. resolveRefs / resolveCanonical are untouched.
|
|
445
|
+
const resolveCanonicalView = useCallback(
|
|
446
|
+
async (profile: IdentityProfile, index: number) => {
|
|
447
|
+
const path = profile.canonical[index];
|
|
448
|
+
if (path == null) return null;
|
|
449
|
+
return await ingestPath(path);
|
|
450
|
+
},
|
|
451
|
+
[],
|
|
452
|
+
);
|
|
453
|
+
|
|
454
|
+
const saveSettings = useCallback<IdentityProfilesState["saveSettings"]>(async (slug, fields) => {
|
|
455
|
+
const res = await request<unknown>(identityProfileSettingsUrl(slug), {
|
|
456
|
+
method: "PATCH",
|
|
457
|
+
body: JSON.stringify(fields),
|
|
458
|
+
headers: { "Content-Type": "application/json" },
|
|
459
|
+
meta: { specKey: "studio", operation: "identity.profile.settings" },
|
|
460
|
+
});
|
|
461
|
+
if (!res.ok) {
|
|
462
|
+
return { ok: false, message: describeAppError(errorOf(res)) };
|
|
463
|
+
}
|
|
464
|
+
// Documented envelope for this one — → {profile}, the same shape update() parses.
|
|
465
|
+
const parsed = createResponseSchema.safeParse(okValue(res));
|
|
466
|
+
if (!parsed.success) {
|
|
467
|
+
return { ok: false, message: "Malformed settings response." };
|
|
468
|
+
}
|
|
469
|
+
const profile = parsed.data.profile;
|
|
470
|
+
if (mounted.current) {
|
|
471
|
+
setProfiles((prev) => prev.map((p) => (p.slug === profile.slug ? profile : p)));
|
|
472
|
+
}
|
|
473
|
+
return { ok: true, profile };
|
|
474
|
+
}, []);
|
|
475
|
+
|
|
476
|
+
/** Shared runner for the three version verbs below. The wire contract only fixes
|
|
477
|
+
* a response envelope for the settings PATCH — activate/rename/archive aren't
|
|
478
|
+
* documented, so this tries the fast path (the response itself echoes {profile})
|
|
479
|
+
* and otherwise falls back to a same-slug re-GET (`identityProfileUrl`, already
|
|
480
|
+
* documented as → {profile}) so local state reconciles regardless of what shape
|
|
481
|
+
* the backend ships once it's live. */
|
|
482
|
+
const runVersionMutation = useCallback(
|
|
483
|
+
async (slug: string, url: string, method: string, body?: unknown): Promise<VersionMutationResult> => {
|
|
484
|
+
const res = await request<unknown>(url, {
|
|
485
|
+
method,
|
|
486
|
+
...(body !== undefined
|
|
487
|
+
? { body: JSON.stringify(body), headers: { "Content-Type": "application/json" } }
|
|
488
|
+
: {}),
|
|
489
|
+
meta: { specKey: "studio", operation: "identity.version.mutate" },
|
|
490
|
+
});
|
|
491
|
+
if (!res.ok) {
|
|
492
|
+
return { ok: false, message: describeAppError(errorOf(res)) };
|
|
493
|
+
}
|
|
494
|
+
const direct = createResponseSchema.safeParse(okValue(res));
|
|
495
|
+
let profile: IdentityProfile | undefined = direct.success ? direct.data.profile : undefined;
|
|
496
|
+
if (!profile) {
|
|
497
|
+
const refetch = await request<unknown>(identityProfileUrl(slug), {
|
|
498
|
+
meta: { specKey: "studio", operation: "identity.profile.refetch" },
|
|
499
|
+
});
|
|
500
|
+
if (refetch.ok) {
|
|
501
|
+
const rp = createResponseSchema.safeParse(okValue(refetch));
|
|
502
|
+
if (rp.success) profile = rp.data.profile;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
if (profile && mounted.current) {
|
|
506
|
+
const resolved = profile;
|
|
507
|
+
setProfiles((prev) => prev.map((p) => (p.slug === resolved.slug ? resolved : p)));
|
|
508
|
+
}
|
|
509
|
+
return { ok: true, profile };
|
|
510
|
+
},
|
|
511
|
+
[],
|
|
512
|
+
);
|
|
513
|
+
|
|
514
|
+
const extractFromVideo = useCallback<IdentityProfilesState["extractFromVideo"]>(
|
|
515
|
+
async (source, target, params) => {
|
|
516
|
+
const parsedParams = params ? char360ParamsSchema.safeParse(params) : undefined;
|
|
517
|
+
const body: Record<string, unknown> = {
|
|
518
|
+
source,
|
|
519
|
+
target,
|
|
520
|
+
...(parsedParams && parsedParams.success && Object.keys(parsedParams.data).length > 0
|
|
521
|
+
? { char360_params: parsedParams.data }
|
|
522
|
+
: {}),
|
|
523
|
+
};
|
|
524
|
+
const res = await request<unknown>(hugpyConfig.videoExtractUrl, {
|
|
525
|
+
method: "POST",
|
|
526
|
+
body: JSON.stringify(body),
|
|
527
|
+
headers: { "Content-Type": "application/json" },
|
|
528
|
+
meta: { specKey: "studio", operation: "identity.video_extract.enqueue" },
|
|
529
|
+
});
|
|
530
|
+
if (!res.ok) {
|
|
531
|
+
return { ok: false, message: describeAppError(errorOf(res)) };
|
|
532
|
+
}
|
|
533
|
+
const enq = okValue(res) as { job_id?: string };
|
|
534
|
+
if (!enq.job_id) {
|
|
535
|
+
return { ok: false, message: "Malformed video-extract response." };
|
|
536
|
+
}
|
|
537
|
+
return { ok: true, jobId: enq.job_id };
|
|
538
|
+
},
|
|
539
|
+
[],
|
|
540
|
+
);
|
|
541
|
+
|
|
542
|
+
const activateVersion = useCallback<IdentityProfilesState["activateVersion"]>(
|
|
543
|
+
(slug, versionId) => runVersionMutation(slug, identityVersionActivateUrl(slug, versionId), "POST"),
|
|
544
|
+
[runVersionMutation],
|
|
545
|
+
);
|
|
546
|
+
|
|
547
|
+
const renameVersion = useCallback<IdentityProfilesState["renameVersion"]>(
|
|
548
|
+
(slug, versionId, name) =>
|
|
549
|
+
runVersionMutation(slug, identityVersionUrl(slug, versionId), "PATCH", { name }),
|
|
550
|
+
[runVersionMutation],
|
|
551
|
+
);
|
|
552
|
+
|
|
553
|
+
const archiveVersion = useCallback<IdentityProfilesState["archiveVersion"]>(
|
|
554
|
+
(slug, versionId) => runVersionMutation(slug, identityVersionUrl(slug, versionId), "DELETE"),
|
|
555
|
+
[runVersionMutation],
|
|
556
|
+
);
|
|
557
|
+
|
|
558
|
+
return {
|
|
559
|
+
profiles,
|
|
560
|
+
loading,
|
|
561
|
+
error,
|
|
562
|
+
reload: () => void load(),
|
|
563
|
+
create,
|
|
564
|
+
update,
|
|
565
|
+
remove,
|
|
566
|
+
resolveRefs,
|
|
567
|
+
resolveCanonical,
|
|
568
|
+
resolveCanonicalView,
|
|
569
|
+
saveSettings,
|
|
570
|
+
activateVersion,
|
|
571
|
+
renameVersion,
|
|
572
|
+
archiveVersion,
|
|
573
|
+
extractFromVideo,
|
|
574
|
+
};
|
|
575
|
+
}
|