@lumenkey/cli 0.3.1 → 0.4.0-rc.1
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/README.md +36 -2
- package/dist/cli/copied.js +86 -85
- package/dist/cli/index.js +56 -44
- package/dist/cli/render.js +3 -2
- package/dist/core/asset-stream.d.ts +16 -0
- package/dist/core/asset-stream.js +64 -0
- package/dist/core/atomic-config-file.js +8 -7
- package/dist/core/chinese-messages.d.ts +4 -0
- package/dist/core/chinese-messages.js +907 -0
- package/dist/core/chinese-script.d.ts +7 -0
- package/dist/core/chinese-script.js +28 -0
- package/dist/core/client.js +2 -1
- package/dist/core/config-dir.js +2 -1
- package/dist/core/config.js +7 -6
- package/dist/core/download-root.d.ts +5 -0
- package/dist/core/download-root.js +64 -0
- package/dist/core/download.js +15 -12
- package/dist/core/generation-input.js +3 -2
- package/dist/core/generation.d.ts +8 -1
- package/dist/core/generation.js +13 -10
- package/dist/core/job-display.js +7 -6
- package/dist/core/locale.js +163 -148
- package/dist/core/outcome.js +6 -4
- package/dist/core/output-path.d.ts +3 -1
- package/dist/core/output-path.js +5 -2
- package/dist/core/params.js +26 -25
- package/dist/core/profiles.js +5 -4
- package/dist/core/set-values.d.ts +0 -1
- package/dist/core/set-values.js +7 -7
- package/dist/core/submission-journal.js +31 -16
- package/dist/core/version.js +2 -3
- package/dist/mcp/server.js +30 -29
- package/dist/studio/asset-integrity-hash.d.ts +28 -0
- package/dist/studio/asset-integrity-hash.js +93 -0
- package/dist/studio/asset-integrity-queue.d.ts +24 -0
- package/dist/studio/asset-integrity-queue.js +94 -0
- package/dist/studio/asset-store.d.ts +52 -0
- package/dist/studio/asset-store.js +352 -0
- package/dist/studio/canvas/LICENSES.txt +1590 -0
- package/dist/studio/canvas/dependencies.json +862 -0
- package/dist/studio/canvas/director-source-manifest.json +47 -0
- package/dist/studio/canvas/editor.css +239 -0
- package/dist/studio/canvas/editor.js +4613 -0
- package/dist/studio/canvas/integration-source-manifest.json +384 -0
- package/dist/studio/canvas/source-manifest.json +240 -0
- package/dist/studio/canvas-store.d.ts +1254 -0
- package/dist/studio/canvas-store.js +205 -0
- package/dist/studio/capture-placement.d.ts +11 -0
- package/dist/studio/capture-placement.js +85 -0
- package/dist/studio/cloud-contract.d.ts +128 -0
- package/dist/studio/cloud-contract.js +229 -0
- package/dist/studio/cloud-service.d.ts +178 -0
- package/dist/studio/cloud-service.js +202 -0
- package/dist/studio/director-captures.d.ts +59 -0
- package/dist/studio/director-captures.js +141 -0
- package/dist/studio/director-contract.d.ts +1489 -0
- package/dist/studio/director-contract.js +288 -0
- package/dist/studio/director-png.d.ts +6 -0
- package/dist/studio/director-png.js +41 -0
- package/dist/studio/director-relations.d.ts +40 -0
- package/dist/studio/director-relations.js +103 -0
- package/dist/studio/document-authority.d.ts +7 -0
- package/dist/studio/document-authority.js +50 -0
- package/dist/studio/draft-store.d.ts +95 -0
- package/dist/studio/draft-store.js +100 -0
- package/dist/studio/errors.d.ts +85 -0
- package/dist/studio/errors.js +94 -0
- package/dist/studio/launch.d.ts +5 -0
- package/dist/studio/launch.js +31 -0
- package/dist/studio/media.d.ts +9 -0
- package/dist/studio/media.js +50 -0
- package/dist/studio/output-save-contract.d.ts +1321 -0
- package/dist/studio/output-save-contract.js +26 -0
- package/dist/studio/output-save-journal.d.ts +971 -0
- package/dist/studio/output-save-journal.js +287 -0
- package/dist/studio/output-save-recovery.d.ts +18 -0
- package/dist/studio/output-save-recovery.js +161 -0
- package/dist/studio/output-save-service.d.ts +436 -0
- package/dist/studio/output-save-service.js +251 -0
- package/dist/studio/output-save-worker.d.ts +40 -0
- package/dist/studio/output-save-worker.js +360 -0
- package/dist/studio/paths.d.ts +6 -0
- package/dist/studio/paths.js +68 -0
- package/dist/studio/run-journal.d.ts +993 -0
- package/dist/studio/run-journal.js +125 -0
- package/dist/studio/server.d.ts +17 -0
- package/dist/studio/server.js +440 -0
- package/dist/studio/session.d.ts +6 -0
- package/dist/studio/session.js +26 -0
- package/dist/studio/shell.d.ts +3 -0
- package/dist/studio/shell.js +23 -0
- package/dist/studio/style.d.ts +2 -0
- package/dist/studio/style.js +29 -0
- package/dist/studio/types.d.ts +26 -0
- package/dist/studio/types.js +2 -0
- package/dist/studio/web/app.d.ts +2 -0
- package/dist/studio/web/app.js +662 -0
- package/dist/studio/web/asset-controller.d.ts +37 -0
- package/dist/studio/web/asset-controller.js +191 -0
- package/dist/studio/web/asset-model.d.ts +29 -0
- package/dist/studio/web/asset-model.js +51 -0
- package/dist/studio/web/controller.d.ts +113 -0
- package/dist/studio/web/controller.js +401 -0
- package/dist/studio/web/messages.d.ts +2 -0
- package/dist/studio/web/messages.js +74 -0
- package/dist/studio/web/model.d.ts +75 -0
- package/dist/studio/web/model.js +182 -0
- package/dist/studio/web/save-controller.d.ts +85 -0
- package/dist/studio/web/save-controller.js +495 -0
- package/dist/studio/web/save-model.d.ts +63 -0
- package/dist/studio/web/save-model.js +64 -0
- package/dist/studio/web/save-queue.d.ts +7 -0
- package/dist/studio/web/save-queue.js +62 -0
- package/dist/studio/web/save-view.d.ts +9 -0
- package/dist/studio/web/save-view.js +19 -0
- package/dist/studio/writer-lock.d.ts +5 -0
- package/dist/studio/writer-lock.js +88 -0
- package/package.json +11 -5
- package/dist/cli/copied.d.ts.map +0 -1
- package/dist/cli/copied.js.map +0 -1
- package/dist/cli/index.d.ts.map +0 -1
- package/dist/cli/index.js.map +0 -1
- package/dist/cli/render.d.ts.map +0 -1
- package/dist/cli/render.js.map +0 -1
- package/dist/core/atomic-config-file.d.ts.map +0 -1
- package/dist/core/atomic-config-file.js.map +0 -1
- package/dist/core/chat.d.ts.map +0 -1
- package/dist/core/chat.js.map +0 -1
- package/dist/core/client.d.ts.map +0 -1
- package/dist/core/client.js.map +0 -1
- package/dist/core/config-dir.d.ts.map +0 -1
- package/dist/core/config-dir.js.map +0 -1
- package/dist/core/config-error.d.ts.map +0 -1
- package/dist/core/config-error.js.map +0 -1
- package/dist/core/config.d.ts.map +0 -1
- package/dist/core/config.js.map +0 -1
- package/dist/core/credit-cap.d.ts.map +0 -1
- package/dist/core/credit-cap.js.map +0 -1
- package/dist/core/download.d.ts.map +0 -1
- package/dist/core/download.js.map +0 -1
- package/dist/core/error-result.d.ts.map +0 -1
- package/dist/core/error-result.js.map +0 -1
- package/dist/core/explain.d.ts.map +0 -1
- package/dist/core/explain.js.map +0 -1
- package/dist/core/generation-input.d.ts.map +0 -1
- package/dist/core/generation-input.js.map +0 -1
- package/dist/core/generation-result.d.ts.map +0 -1
- package/dist/core/generation-result.js.map +0 -1
- package/dist/core/generation.d.ts.map +0 -1
- package/dist/core/generation.js.map +0 -1
- package/dist/core/job-display.d.ts.map +0 -1
- package/dist/core/job-display.js.map +0 -1
- package/dist/core/locale.d.ts.map +0 -1
- package/dist/core/locale.js.map +0 -1
- package/dist/core/origin.d.ts.map +0 -1
- package/dist/core/origin.js.map +0 -1
- package/dist/core/outcome.d.ts.map +0 -1
- package/dist/core/outcome.js.map +0 -1
- package/dist/core/output-path.d.ts.map +0 -1
- package/dist/core/output-path.js.map +0 -1
- package/dist/core/params.d.ts.map +0 -1
- package/dist/core/params.js.map +0 -1
- package/dist/core/poller.d.ts.map +0 -1
- package/dist/core/poller.js.map +0 -1
- package/dist/core/profiles.d.ts.map +0 -1
- package/dist/core/profiles.js.map +0 -1
- package/dist/core/set-values.d.ts.map +0 -1
- package/dist/core/set-values.js.map +0 -1
- package/dist/core/submission-journal.d.ts.map +0 -1
- package/dist/core/submission-journal.js.map +0 -1
- package/dist/core/types.d.ts.map +0 -1
- package/dist/core/types.js.map +0 -1
- package/dist/core/version.d.ts.map +0 -1
- package/dist/core/version.js.map +0 -1
- package/dist/core/wait.d.ts.map +0 -1
- package/dist/core/wait.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/mcp/render.d.ts.map +0 -1
- package/dist/mcp/render.js.map +0 -1
- package/dist/mcp/server.d.ts.map +0 -1
- package/dist/mcp/server.js.map +0 -1
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
// Browser-safe storage contract. Never import filesystem, network or source stores here.
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { StudioError } from './errors.js';
|
|
4
|
+
export const DIRECTOR_MAX_SCENE_BYTES = 128 * 1024;
|
|
5
|
+
export const DIRECTOR_ASPECTS = ['自适应', '1:1', '2:1', '3:4', '4:3', '16:9', '21:9', '9:16'];
|
|
6
|
+
export const DIRECTOR_BODY_TYPES = ['mannequin', 'female', 'broad', 'muscular', 'slim', 'teen', 'child', 'chibi'];
|
|
7
|
+
export const DIRECTOR_POSES = ['stand', 't-pose', 'walk', 'run', 'sit', 'crouch', 'kneel-one', 'kneel-two', 'hands-on-hips', 'lean', 'bow', 'think', 'fight', 'kick', 'throw', 'push', 'wave', 'reach', 'cross-arms', 'phone'];
|
|
8
|
+
export const DIRECTOR_PRIMITIVES = ['box', 'sphere', 'cylinder', 'cone', 'torus', 'pyramid', 'empty'];
|
|
9
|
+
export const directorUuid = z.string().regex(/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/);
|
|
10
|
+
const uuid = directorUuid;
|
|
11
|
+
const revision = z.number().int().nonnegative().safe();
|
|
12
|
+
const coordinate = z.number().finite().min(-10000).max(10000);
|
|
13
|
+
const position = z.tuple([coordinate, coordinate, coordinate]);
|
|
14
|
+
const angle = z.number().finite().min(-180).lt(180);
|
|
15
|
+
const scale = z.number().finite().min(0.05).max(100);
|
|
16
|
+
const name = z.string().trim().min(1).max(120).refine(v => !/[\u0000-\u001f\u007f]/.test(v));
|
|
17
|
+
const transform = z.object({ position, rotation: z.tuple([angle, angle, angle]), scale: z.tuple([scale, scale, scale]) }).strict();
|
|
18
|
+
const poseKey = /^(?:(?:body|torso|head)\.(?:pitch|yaw|roll)|(?:left|right)(?:Shoulder|Hip)\.(?:pitch|twist|spread)|(?:left|right)(?:Elbow|Knee)\.bend|(?:left|right)(?:Hand|Foot)\.(?:pitch|roll|twist)|body\.offsetY)$/;
|
|
19
|
+
const controls = z.record(z.number().finite().min(-180).max(180)).superRefine((value, ctx) => {
|
|
20
|
+
if (Object.keys(value).length > 64 || Object.keys(value).some(key => !poseKey.test(key)) || Math.abs(value['body.offsetY'] ?? 0) > 5)
|
|
21
|
+
ctx.addIssue({ code: 'custom', message: 'Invalid pose controls' });
|
|
22
|
+
});
|
|
23
|
+
const motionPathSchema = z.object({
|
|
24
|
+
closed: z.boolean(), smooth: z.boolean(), uniform: z.boolean(),
|
|
25
|
+
points: z.array(position).min(2).max(16),
|
|
26
|
+
}).strict();
|
|
27
|
+
const objectBase = { id: uuid, name, transform, color: z.string().regex(/^#[0-9a-fA-F]{6}$/), hidden: z.boolean(), locked: z.boolean(), path: motionPathSchema.optional() };
|
|
28
|
+
const objectSchema = z.discriminatedUnion('kind', [
|
|
29
|
+
z.object({ ...objectBase, kind: z.literal('mannequin'), bodyType: z.enum(DIRECTOR_BODY_TYPES), pose: z.enum(DIRECTOR_POSES), controls }).strict(),
|
|
30
|
+
z.object({ ...objectBase, kind: z.literal('geometry'), primitive: z.enum(DIRECTOR_PRIMITIVES) }).strict(),
|
|
31
|
+
]);
|
|
32
|
+
const viewFields = { position, target: position, fov: z.number().finite().min(10).max(120) };
|
|
33
|
+
function validView(value) {
|
|
34
|
+
const [dx, dy, dz] = value.target.map((n, i) => n - value.position[i]);
|
|
35
|
+
const length = Math.hypot(dx, dy, dz);
|
|
36
|
+
return length >= 0.0001 && Math.hypot(dx, dz) / length >= 0.0001;
|
|
37
|
+
}
|
|
38
|
+
const observerSchema = z.object(viewFields).strict().refine(validView);
|
|
39
|
+
// Absent follow means the stored position is the shot. Absent aim means the stored target.
|
|
40
|
+
// Geometry of a bound follow or rotation is checked when the shot is resolved, not here.
|
|
41
|
+
const followSchema = z.object({ objectId: uuid, view: z.enum(['third', 'first']), offset: position }).strict();
|
|
42
|
+
const aimSchema = z.discriminatedUnion('mode', [
|
|
43
|
+
z.object({ mode: z.literal('point') }).strict(),
|
|
44
|
+
z.object({ mode: z.literal('rotation'), rotation: z.tuple([angle, angle, angle]) }).strict(),
|
|
45
|
+
z.object({ mode: z.literal('object'), objectId: uuid }).strict(),
|
|
46
|
+
]);
|
|
47
|
+
export const DIRECTOR_CAMERA_MOVES = ['orbit', 'halfArc', 'pushIn', 'pullOut', 'pedestal', 'truck', 'helix', 'rollerCoaster'];
|
|
48
|
+
export const DIRECTOR_TRANSITIONS = ['dissolve', 'flashWhite', 'flashBlack', 'flashBlack2', 'flashWhite2'];
|
|
49
|
+
const sampleViewSchema = z.object({ position, target: position }).strict().refine(validView);
|
|
50
|
+
const timelineFocusSchema = z.discriminatedUnion('mode', [
|
|
51
|
+
z.object({ mode: z.literal('scene') }).strict(),
|
|
52
|
+
z.object({ mode: z.literal('object'), objectId: uuid }).strict(),
|
|
53
|
+
z.object({ mode: z.literal('point'), point: position }).strict(),
|
|
54
|
+
]);
|
|
55
|
+
const timelineTrackSchema = z.object({ id: uuid, kind: z.enum(['camera', 'object']), targetId: uuid }).strict();
|
|
56
|
+
const clipStart = z.number().finite().min(0).max(600);
|
|
57
|
+
const clipDuration = z.number().finite().gt(0).max(60);
|
|
58
|
+
const clipRate = z.number().finite().min(0.1).max(8).optional();
|
|
59
|
+
const clipSchema = z.discriminatedUnion('kind', [
|
|
60
|
+
z.object({ id: uuid, trackId: uuid, start: clipStart, duration: clipDuration, rate: clipRate, kind: z.literal('cameraMove'), move: z.enum(DIRECTOR_CAMERA_MOVES), fov: z.number().finite().min(10).max(120), samples: z.array(sampleViewSchema).min(2).max(24) }).strict(),
|
|
61
|
+
z.object({ id: uuid, trackId: uuid, start: clipStart, duration: clipDuration, rate: clipRate, kind: z.literal('cameraHold'), position, target: position, fov: z.number().finite().min(10).max(120) }).strict(),
|
|
62
|
+
z.object({ id: uuid, trackId: uuid, start: clipStart, duration: clipDuration, rate: clipRate, kind: z.literal('objectHold'), position, rotation: z.tuple([angle, angle, angle]), scale: z.tuple([scale, scale, scale]), pose: z.enum(DIRECTOR_POSES).optional() }).strict(),
|
|
63
|
+
z.object({ id: uuid, trackId: uuid, start: clipStart, duration: z.number().finite().gt(0).max(8), rate: clipRate, kind: z.literal('transition'), transition: z.enum(DIRECTOR_TRANSITIONS) }).strict(),
|
|
64
|
+
]);
|
|
65
|
+
const timelineSchema = z.object({
|
|
66
|
+
fps: z.union([z.literal(30), z.literal(60)]), loop: z.boolean(),
|
|
67
|
+
duration: z.number().finite().min(0.1).max(600), viewLock: z.boolean(),
|
|
68
|
+
focus: timelineFocusSchema.optional(), tracks: z.array(timelineTrackSchema).max(32), clips: z.array(clipSchema).max(64),
|
|
69
|
+
}).strict();
|
|
70
|
+
const environmentSchema = z.object({
|
|
71
|
+
sky: z.string().regex(/^#[0-9a-fA-F]{6}$/),
|
|
72
|
+
groundVisible: z.boolean(), groundOpacity: z.number().finite().min(0).max(1),
|
|
73
|
+
groundHeight: z.number().finite().min(-100).max(100),
|
|
74
|
+
gridSnap: z.boolean(), gaussianGroundSnap: z.boolean(), labels: z.boolean(),
|
|
75
|
+
panoramaYaw: angle.optional(), panoramaRadius: z.number().finite().min(1).max(500).optional(),
|
|
76
|
+
panoramaAssetId: uuid.optional(), gaussianAssetId: uuid.optional(),
|
|
77
|
+
gaussianQuality: z.enum(['fluent', 'standard', 'high']).optional(), gaussianGroundDetect: z.boolean().optional(),
|
|
78
|
+
}).strict();
|
|
79
|
+
const cameraSchema = z.object({
|
|
80
|
+
id: uuid, name, ...viewFields, aspect: z.enum(DIRECTOR_ASPECTS),
|
|
81
|
+
follow: followSchema.optional(), aim: aimSchema.optional(), path: motionPathSchema.optional(),
|
|
82
|
+
}).strict().refine(validView);
|
|
83
|
+
const sceneSchema = z.object({
|
|
84
|
+
version: z.literal(1), sceneId: uuid, sceneRevision: revision,
|
|
85
|
+
objects: z.array(objectSchema).max(80), cameras: z.array(cameraSchema).min(1).max(16),
|
|
86
|
+
activeCameraId: uuid, observerView: observerSchema, selectedObjectId: uuid.nullable(),
|
|
87
|
+
timeline: timelineSchema.optional(), environment: environmentSchema.optional(),
|
|
88
|
+
}).strict().superRefine((scene, ctx) => {
|
|
89
|
+
const objects = new Set(scene.objects.map(o => o.id)), cameras = new Set(scene.cameras.map(c => c.id));
|
|
90
|
+
const identityOk = objects.size === scene.objects.length && cameras.size === scene.cameras.length && cameras.has(scene.activeCameraId) && (scene.selectedObjectId === null || objects.has(scene.selectedObjectId));
|
|
91
|
+
const aimOk = scene.cameras.every(camera => (!camera.follow || objects.has(camera.follow.objectId)) && (camera.aim?.mode !== 'object' || objects.has(camera.aim.objectId)));
|
|
92
|
+
const timeline = scene.timeline;
|
|
93
|
+
const tracks = timeline?.tracks ?? [], clips = timeline?.clips ?? [];
|
|
94
|
+
const trackIds = new Set(tracks.map(track => track.id));
|
|
95
|
+
const clipIds = new Set(clips.map(clip => clip.id));
|
|
96
|
+
const timelineOk = !timeline || (trackIds.size === tracks.length && clipIds.size === clips.length
|
|
97
|
+
&& tracks.every(track => (track.kind === 'camera' ? cameras : objects).has(track.targetId))
|
|
98
|
+
&& clips.every(clip => trackIds.has(clip.trackId) && clipFitsTrack(clip, tracks.find(track => track.id === clip.trackId)) && (clip.kind !== 'cameraHold' || validView(clip)))
|
|
99
|
+
&& (timeline.focus?.mode !== 'object' || objects.has(timeline.focus.objectId)));
|
|
100
|
+
if (!identityOk || !aimOk || !timelineOk)
|
|
101
|
+
ctx.addIssue({ code: 'custom', message: 'Invalid scene identities' });
|
|
102
|
+
});
|
|
103
|
+
function clipFitsTrack(clip, track) {
|
|
104
|
+
if (clip.kind === 'transition' || clip.kind === 'cameraMove' || clip.kind === 'cameraHold')
|
|
105
|
+
return track.kind === 'camera';
|
|
106
|
+
return track.kind === 'object';
|
|
107
|
+
}
|
|
108
|
+
export function cameraDimensions(aspect) {
|
|
109
|
+
if (!DIRECTOR_ASPECTS.includes(aspect))
|
|
110
|
+
throw new StudioError('STUDIO_DIRECTOR_INVALID');
|
|
111
|
+
if (aspect === '自适应')
|
|
112
|
+
return { width: 1280, height: 720 };
|
|
113
|
+
const [w, h] = aspect.split(':').map(Number), ratio = w / h;
|
|
114
|
+
return ratio >= 1 ? { width: 1280, height: Math.round(1280 / ratio) } : { width: Math.round(1280 * ratio), height: 1280 };
|
|
115
|
+
}
|
|
116
|
+
// Check descriptors before serializing: accessors/prototype tricks must not run.
|
|
117
|
+
// Bound traversal independently from zod so cyclic/deep/untrusted values are safe.
|
|
118
|
+
export function isDirectorJson(value) {
|
|
119
|
+
const ancestors = new Set();
|
|
120
|
+
let count = 0;
|
|
121
|
+
function walk(v, depth) {
|
|
122
|
+
if (++count > 30000 || depth > 20)
|
|
123
|
+
return false;
|
|
124
|
+
if (v === null || typeof v === 'boolean' || typeof v === 'string')
|
|
125
|
+
return true;
|
|
126
|
+
if (typeof v === 'number')
|
|
127
|
+
return Number.isFinite(v);
|
|
128
|
+
if (typeof v !== 'object')
|
|
129
|
+
return false;
|
|
130
|
+
const proto = Object.getPrototypeOf(v);
|
|
131
|
+
if (Array.isArray(v) ? proto !== Array.prototype : proto !== Object.prototype && proto !== null)
|
|
132
|
+
return false;
|
|
133
|
+
if (ancestors.has(v))
|
|
134
|
+
return false;
|
|
135
|
+
ancestors.add(v);
|
|
136
|
+
for (const key of Reflect.ownKeys(v)) {
|
|
137
|
+
if (Array.isArray(v) && key === 'length')
|
|
138
|
+
continue;
|
|
139
|
+
if (typeof key !== 'string' || ['__proto__', 'constructor', 'prototype'].includes(key))
|
|
140
|
+
return false;
|
|
141
|
+
const descriptor = Object.getOwnPropertyDescriptor(v, key);
|
|
142
|
+
if (!descriptor.enumerable || !('value' in descriptor) || !walk(descriptor.value, depth + 1))
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
ancestors.delete(v);
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
return walk(value, 0);
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function parse(schema, value, maxBytes) {
|
|
156
|
+
if (!isDirectorJson(value))
|
|
157
|
+
throw new StudioError('STUDIO_DIRECTOR_INVALID');
|
|
158
|
+
if (new TextEncoder().encode(JSON.stringify(value)).length > maxBytes)
|
|
159
|
+
throw new StudioError('STUDIO_DIRECTOR_INVALID');
|
|
160
|
+
const parsed = schema.safeParse(value);
|
|
161
|
+
if (!parsed.success)
|
|
162
|
+
throw new StudioError('STUDIO_DIRECTOR_INVALID');
|
|
163
|
+
return parsed.data;
|
|
164
|
+
}
|
|
165
|
+
function dropAbsent(value, key) {
|
|
166
|
+
if (value[key] === undefined)
|
|
167
|
+
delete value[key];
|
|
168
|
+
}
|
|
169
|
+
export function parseDirectorScene(value) {
|
|
170
|
+
const scene = parse(sceneSchema, value, DIRECTOR_MAX_SCENE_BYTES);
|
|
171
|
+
// Optional keys must stay absent so older scenes stringify the same way.
|
|
172
|
+
for (const camera of scene.cameras) {
|
|
173
|
+
dropAbsent(camera, 'follow');
|
|
174
|
+
dropAbsent(camera, 'aim');
|
|
175
|
+
dropAbsent(camera, 'path');
|
|
176
|
+
}
|
|
177
|
+
for (const object of scene.objects)
|
|
178
|
+
dropAbsent(object, 'path');
|
|
179
|
+
dropAbsent(scene, 'timeline');
|
|
180
|
+
dropAbsent(scene, 'environment');
|
|
181
|
+
if (scene.timeline) {
|
|
182
|
+
dropAbsent(scene.timeline, 'focus');
|
|
183
|
+
for (const clip of scene.timeline.clips) {
|
|
184
|
+
dropAbsent(clip, 'rate');
|
|
185
|
+
if (clip.kind === 'objectHold')
|
|
186
|
+
dropAbsent(clip, 'pose');
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (scene.environment)
|
|
190
|
+
for (const key of ['panoramaYaw', 'panoramaRadius', 'panoramaAssetId', 'gaussianAssetId', 'gaussianQuality', 'gaussianGroundDetect'])
|
|
191
|
+
dropAbsent(scene.environment, key);
|
|
192
|
+
return scene;
|
|
193
|
+
}
|
|
194
|
+
export function cloneDirectorScene(value, newId = () => globalThis.crypto.randomUUID()) {
|
|
195
|
+
const scene = parseDirectorScene(value), reserved = new Set([
|
|
196
|
+
scene.sceneId, ...scene.objects.map(o => o.id), ...scene.cameras.map(c => c.id),
|
|
197
|
+
...(scene.timeline?.tracks.map(track => track.id) ?? []), ...(scene.timeline?.clips.map(clip => clip.id) ?? []),
|
|
198
|
+
...[scene.environment?.panoramaAssetId, scene.environment?.gaussianAssetId].filter((id) => !!id),
|
|
199
|
+
]);
|
|
200
|
+
const allocate = () => { const id = newId(); if (!uuid.safeParse(id).success || reserved.has(id))
|
|
201
|
+
throw new StudioError('STUDIO_DIRECTOR_INVALID'); reserved.add(id); return id; };
|
|
202
|
+
const nextScene = allocate(), objectMapping = new Map(), cameraMapping = new Map();
|
|
203
|
+
scene.objects.forEach(o => objectMapping.set(o.id, allocate()));
|
|
204
|
+
scene.cameras.forEach(c => cameraMapping.set(c.id, allocate()));
|
|
205
|
+
scene.sceneId = nextScene;
|
|
206
|
+
scene.sceneRevision = 0;
|
|
207
|
+
scene.objects.forEach(o => { o.id = objectMapping.get(o.id); });
|
|
208
|
+
scene.cameras.forEach(c => {
|
|
209
|
+
c.id = cameraMapping.get(c.id);
|
|
210
|
+
if (c.follow)
|
|
211
|
+
c.follow.objectId = objectMapping.get(c.follow.objectId);
|
|
212
|
+
if (c.aim?.mode === 'object')
|
|
213
|
+
c.aim.objectId = objectMapping.get(c.aim.objectId);
|
|
214
|
+
});
|
|
215
|
+
scene.activeCameraId = cameraMapping.get(scene.activeCameraId);
|
|
216
|
+
if (scene.selectedObjectId)
|
|
217
|
+
scene.selectedObjectId = objectMapping.get(scene.selectedObjectId);
|
|
218
|
+
if (scene.timeline) {
|
|
219
|
+
const trackMapping = new Map();
|
|
220
|
+
scene.timeline.tracks.forEach(track => trackMapping.set(track.id, allocate()));
|
|
221
|
+
scene.timeline.tracks.forEach(track => {
|
|
222
|
+
track.id = trackMapping.get(track.id);
|
|
223
|
+
const mapped = (track.kind === 'camera' ? cameraMapping : objectMapping).get(track.targetId);
|
|
224
|
+
if (!mapped)
|
|
225
|
+
throw new StudioError('STUDIO_DIRECTOR_INVALID');
|
|
226
|
+
track.targetId = mapped;
|
|
227
|
+
});
|
|
228
|
+
scene.timeline.clips.forEach(clip => {
|
|
229
|
+
clip.id = allocate();
|
|
230
|
+
const mapped = trackMapping.get(clip.trackId);
|
|
231
|
+
if (!mapped)
|
|
232
|
+
throw new StudioError('STUDIO_DIRECTOR_INVALID');
|
|
233
|
+
clip.trackId = mapped;
|
|
234
|
+
});
|
|
235
|
+
if (scene.timeline.focus?.mode === 'object') {
|
|
236
|
+
const mapped = objectMapping.get(scene.timeline.focus.objectId);
|
|
237
|
+
if (!mapped)
|
|
238
|
+
throw new StudioError('STUDIO_DIRECTOR_INVALID');
|
|
239
|
+
scene.timeline.focus.objectId = mapped;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return parseDirectorScene(scene);
|
|
243
|
+
}
|
|
244
|
+
export function cloneDirectorMetadata(metadata, newId) {
|
|
245
|
+
if (!isDirectorJson(metadata))
|
|
246
|
+
throw new StudioError('STUDIO_DIRECTOR_INVALID');
|
|
247
|
+
const next = structuredClone(metadata);
|
|
248
|
+
if (next.studioDirector !== undefined)
|
|
249
|
+
next.studioDirector = cloneDirectorScene(next.studioDirector, newId);
|
|
250
|
+
delete next.studioDirectorCaptures;
|
|
251
|
+
delete next.studioDirectorCaptureOrigin;
|
|
252
|
+
return next;
|
|
253
|
+
}
|
|
254
|
+
export function directorSceneContent(value) { const { sceneRevision: _, ...content } = value; return JSON.stringify(content); }
|
|
255
|
+
export function normalizeSceneEdit(previousValue, nextValue) {
|
|
256
|
+
const previous = parseDirectorScene(previousValue), next = parseDirectorScene(nextValue);
|
|
257
|
+
if (previous.sceneId !== next.sceneId) {
|
|
258
|
+
if (next.sceneRevision !== 0)
|
|
259
|
+
throw new StudioError('STUDIO_DIRECTOR_INVALID');
|
|
260
|
+
return next;
|
|
261
|
+
}
|
|
262
|
+
if (directorSceneContent(previous) === directorSceneContent(next)) {
|
|
263
|
+
next.sceneRevision = previous.sceneRevision;
|
|
264
|
+
return next;
|
|
265
|
+
}
|
|
266
|
+
if (previous.sceneRevision === Number.MAX_SAFE_INTEGER)
|
|
267
|
+
throw new StudioError('STUDIO_DIRECTOR_INVALID');
|
|
268
|
+
next.sceneRevision = previous.sceneRevision + 1;
|
|
269
|
+
return next;
|
|
270
|
+
}
|
|
271
|
+
export const directorCaptureSchema = z.object({ captureId: uuid, assetId: uuid, sceneId: uuid, sceneRevision: revision, cameraId: uuid,
|
|
272
|
+
createdAt: z.string().datetime(), width: z.number().int().positive().max(1280), height: z.number().int().positive().max(1280),
|
|
273
|
+
}).strict().refine(v => DIRECTOR_ASPECTS.some(a => { const d = cameraDimensions(a); return d.width === v.width && d.height === v.height; }));
|
|
274
|
+
export const directorProvenanceSchema = z.object({
|
|
275
|
+
captureId: uuid, requestHash: z.string().regex(/^[a-f0-9]{64}$/), canvasId: uuid,
|
|
276
|
+
expectedCanvasRevision: revision, nodeId: z.string().min(1).max(160).regex(/^[a-zA-Z0-9_-]+$/),
|
|
277
|
+
sceneId: uuid, sceneRevision: revision, cameraId: uuid,
|
|
278
|
+
width: z.number().int().positive().max(1280), height: z.number().int().positive().max(1280),
|
|
279
|
+
}).strict().refine(v => DIRECTOR_ASPECTS.some(a => { const d = cameraDimensions(a); return d.width === v.width && d.height === v.height; }));
|
|
280
|
+
const historySchema = z.array(directorCaptureSchema).max(64).refine(rows => new Set(rows.map(r => r.captureId)).size === rows.length && new Set(rows.map(r => r.assetId)).size === rows.length);
|
|
281
|
+
export function parseDirectorCaptureHistory(value, scene) {
|
|
282
|
+
const rows = parse(historySchema, value, 64 * 1024);
|
|
283
|
+
if (scene && rows.some(row => row.sceneId === scene.sceneId && row.sceneRevision > scene.sceneRevision))
|
|
284
|
+
throw new StudioError('STUDIO_DIRECTOR_INVALID');
|
|
285
|
+
// A historical camera may have been deleted; its PNG remains independently valid.
|
|
286
|
+
return rows;
|
|
287
|
+
}
|
|
288
|
+
//# sourceMappingURL=director-contract.js.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Node-only finite PNG structure check, NOT a CRC validator or compressed-data decoder.
|
|
2
|
+
import { StudioError } from './errors.js';
|
|
3
|
+
export const DIRECTOR_MAX_PNG_BYTES = 8 * 1024 * 1024;
|
|
4
|
+
const signature = Buffer.from('89504e470d0a1a0a', 'hex');
|
|
5
|
+
export function validateDirectorPng(bytes, expected) {
|
|
6
|
+
const invalid = () => { throw new StudioError('STUDIO_CAPTURE_PNG_INVALID', 422); };
|
|
7
|
+
if (!(bytes instanceof Uint8Array) || bytes.length > DIRECTOR_MAX_PNG_BYTES || bytes.length < 57 || !Number.isInteger(expected.width) || !Number.isInteger(expected.height) || expected.width < 1 || expected.height < 1 || expected.width > 1280 || expected.height > 1280)
|
|
8
|
+
invalid();
|
|
9
|
+
const data = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
10
|
+
if (!data.subarray(0, 8).equals(signature))
|
|
11
|
+
invalid();
|
|
12
|
+
let offset = 8, header = false, image = false, ended = false;
|
|
13
|
+
while (offset < data.length) {
|
|
14
|
+
if (data.length - offset < 12)
|
|
15
|
+
invalid();
|
|
16
|
+
const length = data.readUInt32BE(offset), end = offset + length + 12;
|
|
17
|
+
if (end > data.length)
|
|
18
|
+
invalid();
|
|
19
|
+
const kind = data.toString('ascii', offset + 4, offset + 8);
|
|
20
|
+
if (!/^[A-Za-z]{4}$/.test(kind) || (!header && kind !== 'IHDR'))
|
|
21
|
+
invalid();
|
|
22
|
+
if (kind === 'IHDR') {
|
|
23
|
+
if (header || offset !== 8 || length !== 13)
|
|
24
|
+
invalid();
|
|
25
|
+
if (data.readUInt32BE(offset + 8) !== expected.width || data.readUInt32BE(offset + 12) !== expected.height || data[offset + 16] !== 8 || ![2, 6].includes(data[offset + 17]) || data[offset + 18] !== 0 || data[offset + 19] !== 0 || data[offset + 20] !== 0)
|
|
26
|
+
invalid();
|
|
27
|
+
header = true;
|
|
28
|
+
}
|
|
29
|
+
else if (kind === 'IDAT')
|
|
30
|
+
image = true;
|
|
31
|
+
else if (kind === 'IEND') {
|
|
32
|
+
if (length !== 0 || end !== data.length || !image)
|
|
33
|
+
invalid();
|
|
34
|
+
ended = true;
|
|
35
|
+
}
|
|
36
|
+
offset = end;
|
|
37
|
+
}
|
|
38
|
+
if (!header || !image || !ended)
|
|
39
|
+
invalid();
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=director-png.js.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type DirectorCapture } from './director-contract.js';
|
|
2
|
+
import type { StudioCanvas, StudioCanvasSnapshot } from './canvas-store.js';
|
|
3
|
+
import type { StudioAsset } from './types.js';
|
|
4
|
+
export declare function captureHistoryEntry(asset: StudioAsset): DirectorCapture;
|
|
5
|
+
export declare function captureOrigin(asset: StudioAsset): {
|
|
6
|
+
canvasId: string;
|
|
7
|
+
nodeId: string;
|
|
8
|
+
createdAt: string;
|
|
9
|
+
sceneId: string;
|
|
10
|
+
sceneRevision: number;
|
|
11
|
+
captureId: string;
|
|
12
|
+
assetId: string;
|
|
13
|
+
cameraId: string;
|
|
14
|
+
width: number;
|
|
15
|
+
height: number;
|
|
16
|
+
};
|
|
17
|
+
export declare function referenceIds(metadata: Record<string, unknown>): string[];
|
|
18
|
+
export declare function assertDirectorCanvas(next: StudioCanvas, previous: StudioCanvas, all: StudioCanvas[], assets: StudioAsset[]): void;
|
|
19
|
+
export interface CaptureBinding {
|
|
20
|
+
reference: boolean;
|
|
21
|
+
assetNode: boolean;
|
|
22
|
+
connection: boolean;
|
|
23
|
+
}
|
|
24
|
+
export declare function captureBinding(canvas: StudioCanvas | undefined, asset: StudioAsset): {
|
|
25
|
+
binding: CaptureBinding;
|
|
26
|
+
history: boolean;
|
|
27
|
+
};
|
|
28
|
+
export declare function canBindOriginal(canvas: StudioCanvas | undefined, asset: StudioAsset): boolean;
|
|
29
|
+
export interface CaptureDelta {
|
|
30
|
+
nodeId: string;
|
|
31
|
+
history: DirectorCapture;
|
|
32
|
+
assetId: string;
|
|
33
|
+
assetNode: StudioCanvasSnapshot['nodes'][number];
|
|
34
|
+
connection: StudioCanvasSnapshot['connections'][number];
|
|
35
|
+
}
|
|
36
|
+
export declare function withCaptureRelation(canvas: StudioCanvas, asset: StudioAsset): {
|
|
37
|
+
snapshot: StudioCanvasSnapshot;
|
|
38
|
+
delta: CaptureDelta;
|
|
39
|
+
};
|
|
40
|
+
//# sourceMappingURL=director-relations.d.ts.map
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { directorSceneContent, directorUuid, parseDirectorScene, parseDirectorCaptureHistory } from './director-contract.js';
|
|
3
|
+
import { StudioError } from './errors.js';
|
|
4
|
+
import { CAPTURE_NODE_SIZE, capturePosition } from './capture-placement.js';
|
|
5
|
+
const referencesSchema = z.array(directorUuid).max(200).refine(v => new Set(v).size === v.length);
|
|
6
|
+
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
7
|
+
function exactRecord(a, b) {
|
|
8
|
+
return !!a && typeof a === 'object' && !Array.isArray(a) && Object.keys(a).length === Object.keys(b).length && Object.entries(b).every(([k, v]) => same(a[k], v));
|
|
9
|
+
}
|
|
10
|
+
export function captureHistoryEntry(asset) {
|
|
11
|
+
const p = asset.directorCapture;
|
|
12
|
+
return { captureId: p.captureId, assetId: asset.assetId, sceneId: p.sceneId, sceneRevision: p.sceneRevision, cameraId: p.cameraId, createdAt: asset.savedAt, width: p.width, height: p.height };
|
|
13
|
+
}
|
|
14
|
+
export function captureOrigin(asset) { const p = asset.directorCapture; return { ...captureHistoryEntry(asset), canvasId: p.canvasId, nodeId: p.nodeId }; }
|
|
15
|
+
export function referenceIds(metadata) {
|
|
16
|
+
const parsed = referencesSchema.safeParse(metadata.studioReferenceAssetIds ?? []);
|
|
17
|
+
if (!parsed.success)
|
|
18
|
+
throw new StudioError('STUDIO_DIRECTOR_INVALID');
|
|
19
|
+
return parsed.data;
|
|
20
|
+
}
|
|
21
|
+
export function assertDirectorCanvas(next, previous, all, assets) {
|
|
22
|
+
const assetMap = new Map(assets.map(a => [a.assetId, a])), captureMap = new Map(assets.filter(a => a.directorCapture).map(a => [a.directorCapture.captureId, a]));
|
|
23
|
+
const usedScenes = new Set();
|
|
24
|
+
for (const doc of all)
|
|
25
|
+
if (doc.id !== next.id)
|
|
26
|
+
for (const n of doc.snapshot.nodes)
|
|
27
|
+
if (n.metadata.studioDirector !== undefined)
|
|
28
|
+
usedScenes.add(parseDirectorScene(n.metadata.studioDirector).sceneId);
|
|
29
|
+
for (const n of next.snapshot.nodes) {
|
|
30
|
+
const m = n.metadata, old = previous.snapshot.nodes.find(o => o.id === n.id);
|
|
31
|
+
const scene = m.studioDirector === undefined ? undefined : parseDirectorScene(m.studioDirector);
|
|
32
|
+
if (scene) {
|
|
33
|
+
if (!['image', 'video'].includes(n.type) || usedScenes.has(scene.sceneId))
|
|
34
|
+
throw new StudioError('STUDIO_DIRECTOR_INVALID');
|
|
35
|
+
usedScenes.add(scene.sceneId);
|
|
36
|
+
const before = old?.metadata.studioDirector === undefined ? undefined : parseDirectorScene(old.metadata.studioDirector);
|
|
37
|
+
if (before?.sceneId === scene.sceneId && (scene.sceneRevision < before.sceneRevision || (directorSceneContent(scene) !== directorSceneContent(before) && scene.sceneRevision <= before.sceneRevision)))
|
|
38
|
+
throw new StudioError('STUDIO_DIRECTOR_INVALID');
|
|
39
|
+
// Reusing a removed scene identity on another node is not cloning.
|
|
40
|
+
if (previous.snapshot.nodes.some(p => p.id !== n.id && p.metadata.studioDirector !== undefined && parseDirectorScene(p.metadata.studioDirector).sceneId === scene.sceneId))
|
|
41
|
+
throw new StudioError('STUDIO_DIRECTOR_INVALID');
|
|
42
|
+
}
|
|
43
|
+
for (const id of referenceIds(m))
|
|
44
|
+
if (!assetMap.has(id))
|
|
45
|
+
throw new StudioError('STUDIO_ASSET_NOT_FOUND', 404);
|
|
46
|
+
if (m.studioDirectorCaptures !== undefined) {
|
|
47
|
+
if (!['image', 'video'].includes(n.type))
|
|
48
|
+
throw new StudioError('STUDIO_DIRECTOR_INVALID');
|
|
49
|
+
for (const record of parseDirectorCaptureHistory(m.studioDirectorCaptures, scene)) {
|
|
50
|
+
const asset = captureMap.get(record.captureId), p = asset?.directorCapture;
|
|
51
|
+
if (!asset || !p || p.canvasId !== next.id || p.nodeId !== n.id || !exactRecord(record, captureHistoryEntry(asset)))
|
|
52
|
+
throw new StudioError('STUDIO_DIRECTOR_INVALID');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (m.studioDirectorCaptureOrigin !== undefined) {
|
|
56
|
+
const asset = typeof m.studioAssetId === 'string' ? assetMap.get(m.studioAssetId) : undefined;
|
|
57
|
+
if (n.type !== 'asset' || !asset?.directorCapture || n.id !== `capture-${asset.directorCapture.captureId}` || asset.directorCapture.canvasId !== next.id || !exactRecord(m.studioDirectorCaptureOrigin, captureOrigin(asset)))
|
|
58
|
+
throw new StudioError('STUDIO_DIRECTOR_INVALID');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export function captureBinding(canvas, asset) {
|
|
63
|
+
const p = asset.directorCapture, snapshot = canvas?.snapshot;
|
|
64
|
+
const source = snapshot?.nodes.find(n => n.id === p.nodeId && ['image', 'video'].includes(n.type));
|
|
65
|
+
const target = snapshot?.nodes.find(n => n.id === `capture-${p.captureId}` && n.type === 'asset');
|
|
66
|
+
return { binding: {
|
|
67
|
+
reference: !!source && referenceIds(source.metadata).includes(asset.assetId),
|
|
68
|
+
assetNode: !!target && target.metadata.studioAssetId === asset.assetId && exactRecord(target.metadata.studioDirectorCaptureOrigin, captureOrigin(asset)),
|
|
69
|
+
connection: !!snapshot?.connections.some(e => e.id === `capture-edge-${p.captureId}` && e.fromNodeId === `capture-${p.captureId}` && e.toNodeId === p.nodeId),
|
|
70
|
+
}, history: !!source && parseDirectorCaptureHistory(source.metadata.studioDirectorCaptures ?? []).some(row => exactRecord(row, captureHistoryEntry(asset))) };
|
|
71
|
+
}
|
|
72
|
+
export function canBindOriginal(canvas, asset) {
|
|
73
|
+
const p = asset.directorCapture;
|
|
74
|
+
if (!canvas || canvas.revision !== p.expectedCanvasRevision)
|
|
75
|
+
return false;
|
|
76
|
+
const n = canvas.snapshot.nodes.find(n => n.id === p.nodeId && ['image', 'video'].includes(n.type));
|
|
77
|
+
if (!n || n.metadata.studioDirector === undefined)
|
|
78
|
+
return false;
|
|
79
|
+
const scene = parseDirectorScene(n.metadata.studioDirector);
|
|
80
|
+
return scene.sceneId === p.sceneId && scene.sceneRevision === p.sceneRevision && scene.cameras.some(c => c.id === p.cameraId);
|
|
81
|
+
}
|
|
82
|
+
export function withCaptureRelation(canvas, asset) {
|
|
83
|
+
const p = asset.directorCapture, snapshot = structuredClone(canvas.snapshot), source = snapshot.nodes.find(n => n.id === p.nodeId);
|
|
84
|
+
if (!source)
|
|
85
|
+
throw new StudioError('STUDIO_CAPTURE_BINDING_CONFLICT', 409);
|
|
86
|
+
const history = parseDirectorCaptureHistory(source.metadata.studioDirectorCaptures ?? []);
|
|
87
|
+
if (history.length >= 64 || snapshot.nodes.length >= 200 || snapshot.connections.length >= 400)
|
|
88
|
+
throw new StudioError('STUDIO_CAPTURE_CAPACITY_EXCEEDED', 409);
|
|
89
|
+
const id = `capture-${p.captureId}`, edgeId = `capture-edge-${p.captureId}`;
|
|
90
|
+
if (snapshot.nodes.some(n => n.id === id) || snapshot.connections.some(e => e.id === edgeId))
|
|
91
|
+
throw new StudioError('STUDIO_CAPTURE_BINDING_CONFLICT', 409);
|
|
92
|
+
const entry = captureHistoryEntry(asset);
|
|
93
|
+
const assetNode = { id, type: 'asset', title: '镜头截图', position: capturePosition(snapshot.nodes, source), ...CAPTURE_NODE_SIZE, status: 'idle', metadata: { studioAssetId: asset.assetId, mediaType: 'image', studioDirectorCaptureOrigin: captureOrigin(asset) } };
|
|
94
|
+
const connection = { id: edgeId, fromNodeId: id, toNodeId: source.id, fromHandle: 'right', toHandle: 'left', status: 'idle' };
|
|
95
|
+
source.metadata.studioDirectorCaptures = [...history, entry];
|
|
96
|
+
source.metadata.studioReferenceAssetIds = [...new Set([...referenceIds(source.metadata), asset.assetId])];
|
|
97
|
+
snapshot.nodes.push(assetNode);
|
|
98
|
+
snapshot.connections.push(connection);
|
|
99
|
+
if (new TextEncoder().encode(JSON.stringify(snapshot)).length > 512 * 1024)
|
|
100
|
+
throw new StudioError('STUDIO_CAPTURE_CAPACITY_EXCEEDED', 409);
|
|
101
|
+
return { snapshot, delta: { nodeId: source.id, history: entry, assetId: asset.assetId, assetNode, connection } };
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=director-relations.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type StudioErrorCode } from './errors.js';
|
|
2
|
+
import type { StudioRoot } from './types.js';
|
|
3
|
+
/** A durable initialization witness distinguishes first use from a missing authority after restart.
|
|
4
|
+
* Written BEFORE initial document creation, so an interrupted initialization is detectable.
|
|
5
|
+
* Existing valid documents are adopted. Deleting both the witness and document is outside recovery. */
|
|
6
|
+
export declare function prepareDocumentAuthority(root: StudioRoot, name: 'drafts' | 'runs' | 'canvases' | 'output-saves', hasDocument: boolean, code: StudioErrorCode): Promise<void>;
|
|
7
|
+
//# sourceMappingURL=document-authority.d.ts.map
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { open } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { assertRegularFile, assertStudioRoot } from './paths.js';
|
|
4
|
+
import { hasFsCode, StudioError } from './errors.js';
|
|
5
|
+
/** A durable initialization witness distinguishes first use from a missing authority after restart.
|
|
6
|
+
* Written BEFORE initial document creation, so an interrupted initialization is detectable.
|
|
7
|
+
* Existing valid documents are adopted. Deleting both the witness and document is outside recovery. */
|
|
8
|
+
export async function prepareDocumentAuthority(root, name, hasDocument, code) {
|
|
9
|
+
const marker = path.join(root.managedDir, `.${name}.initialized`), expected = `lumenkey-local-document-v1:${name}\n`;
|
|
10
|
+
await assertStudioRoot(root);
|
|
11
|
+
async function exists() {
|
|
12
|
+
try {
|
|
13
|
+
await assertRegularFile(marker);
|
|
14
|
+
const handle = await open(marker, 'r');
|
|
15
|
+
try {
|
|
16
|
+
if ((await handle.stat()).size > 100 || await handle.readFile('utf8') !== expected)
|
|
17
|
+
throw new StudioError(code, 409);
|
|
18
|
+
}
|
|
19
|
+
finally {
|
|
20
|
+
await handle.close();
|
|
21
|
+
}
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
if (hasFsCode(error, 'ENOENT'))
|
|
26
|
+
return false;
|
|
27
|
+
throw new StudioError(code, 409);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (await exists()) {
|
|
31
|
+
if (!hasDocument)
|
|
32
|
+
throw new StudioError(code, 409);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const handle = await open(marker, 'wx', 0o600);
|
|
37
|
+
try {
|
|
38
|
+
await handle.writeFile(expected);
|
|
39
|
+
await handle.sync();
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
await handle.close();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
if (!hasFsCode(error, 'EEXIST') || !hasDocument || !await exists())
|
|
47
|
+
throw new StudioError(code, 409);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=document-authority.js.map
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { StudioRoot } from './types.js';
|
|
3
|
+
declare const document: z.ZodObject<{
|
|
4
|
+
schemaVersion: z.ZodLiteral<1>;
|
|
5
|
+
revision: z.ZodNumber;
|
|
6
|
+
drafts: z.ZodObject<{
|
|
7
|
+
image: z.ZodOptional<z.ZodObject<{
|
|
8
|
+
modelId: z.ZodString;
|
|
9
|
+
fields: z.ZodPipeline<z.ZodEffects<z.ZodUnknown, unknown, unknown>, z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
10
|
+
savedAt: z.ZodString;
|
|
11
|
+
}, "strict", z.ZodTypeAny, {
|
|
12
|
+
modelId: string;
|
|
13
|
+
fields: Record<string, string>;
|
|
14
|
+
savedAt: string;
|
|
15
|
+
}, {
|
|
16
|
+
modelId: string;
|
|
17
|
+
savedAt: string;
|
|
18
|
+
fields?: unknown;
|
|
19
|
+
}>>;
|
|
20
|
+
video: z.ZodOptional<z.ZodObject<{
|
|
21
|
+
modelId: z.ZodString;
|
|
22
|
+
fields: z.ZodPipeline<z.ZodEffects<z.ZodUnknown, unknown, unknown>, z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
23
|
+
savedAt: z.ZodString;
|
|
24
|
+
}, "strict", z.ZodTypeAny, {
|
|
25
|
+
modelId: string;
|
|
26
|
+
fields: Record<string, string>;
|
|
27
|
+
savedAt: string;
|
|
28
|
+
}, {
|
|
29
|
+
modelId: string;
|
|
30
|
+
savedAt: string;
|
|
31
|
+
fields?: unknown;
|
|
32
|
+
}>>;
|
|
33
|
+
}, "strict", z.ZodTypeAny, {
|
|
34
|
+
image?: {
|
|
35
|
+
modelId: string;
|
|
36
|
+
fields: Record<string, string>;
|
|
37
|
+
savedAt: string;
|
|
38
|
+
} | undefined;
|
|
39
|
+
video?: {
|
|
40
|
+
modelId: string;
|
|
41
|
+
fields: Record<string, string>;
|
|
42
|
+
savedAt: string;
|
|
43
|
+
} | undefined;
|
|
44
|
+
}, {
|
|
45
|
+
image?: {
|
|
46
|
+
modelId: string;
|
|
47
|
+
savedAt: string;
|
|
48
|
+
fields?: unknown;
|
|
49
|
+
} | undefined;
|
|
50
|
+
video?: {
|
|
51
|
+
modelId: string;
|
|
52
|
+
savedAt: string;
|
|
53
|
+
fields?: unknown;
|
|
54
|
+
} | undefined;
|
|
55
|
+
}>;
|
|
56
|
+
}, "strict", z.ZodTypeAny, {
|
|
57
|
+
schemaVersion: 1;
|
|
58
|
+
revision: number;
|
|
59
|
+
drafts: {
|
|
60
|
+
image?: {
|
|
61
|
+
modelId: string;
|
|
62
|
+
fields: Record<string, string>;
|
|
63
|
+
savedAt: string;
|
|
64
|
+
} | undefined;
|
|
65
|
+
video?: {
|
|
66
|
+
modelId: string;
|
|
67
|
+
fields: Record<string, string>;
|
|
68
|
+
savedAt: string;
|
|
69
|
+
} | undefined;
|
|
70
|
+
};
|
|
71
|
+
}, {
|
|
72
|
+
schemaVersion: 1;
|
|
73
|
+
revision: number;
|
|
74
|
+
drafts: {
|
|
75
|
+
image?: {
|
|
76
|
+
modelId: string;
|
|
77
|
+
savedAt: string;
|
|
78
|
+
fields?: unknown;
|
|
79
|
+
} | undefined;
|
|
80
|
+
video?: {
|
|
81
|
+
modelId: string;
|
|
82
|
+
savedAt: string;
|
|
83
|
+
fields?: unknown;
|
|
84
|
+
} | undefined;
|
|
85
|
+
};
|
|
86
|
+
}>;
|
|
87
|
+
export type StudioDrafts = z.infer<typeof document>;
|
|
88
|
+
export interface StudioDraftStore {
|
|
89
|
+
read(): Promise<StudioDrafts>;
|
|
90
|
+
save(value: unknown): Promise<StudioDrafts>;
|
|
91
|
+
}
|
|
92
|
+
/** Uses the asset store's exclusive directory writer and lifecycle, not a second lock. */
|
|
93
|
+
export declare function createDraftStore(root: StudioRoot, serial: <T>(fn: () => Promise<T>) => Promise<T>): StudioDraftStore;
|
|
94
|
+
export {};
|
|
95
|
+
//# sourceMappingURL=draft-store.d.ts.map
|