@vivix-labs/project-studio 0.0.0-stage → 1.0.0-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/dist/Inter-LICENSE.txt +93 -0
  2. package/dist/UI-LICENSE.txt +21 -0
  3. package/dist/assets/35a50-G5EJUNOC.svg +6 -0
  4. package/dist/assets/369ca-BISNKZ3M.svg +6 -0
  5. package/dist/assets/441bc-BCMNQ72W.svg +6 -0
  6. package/dist/assets/483f0-T6ZAGTDG.svg +6 -0
  7. package/dist/assets/6911b-ESLYKSC6.svg +6 -0
  8. package/dist/assets/Inter-LICENSE.txt +93 -0
  9. package/dist/assets/UI-system-LICENSE.txt +21 -0
  10. package/dist/assets/add-JY533O73.svg +5 -0
  11. package/dist/assets/area-P2WOMBT5.svg +9 -0
  12. package/dist/assets/b70bb-BTTOKPOH.svg +6 -0
  13. package/dist/assets/b9919-3S3O6YOT.svg +6 -0
  14. package/dist/assets/calendar-JI6USFRJ.svg +1 -0
  15. package/dist/assets/check-JSRRIXN5.svg +5 -0
  16. package/dist/assets/check-on-content-FQUVG3IZ.svg +5 -0
  17. package/dist/assets/chevron-down-K7YKS2EY.svg +5 -0
  18. package/dist/assets/chevron-left-JQCLK2WZ.svg +3 -0
  19. package/dist/assets/chevron-right-TNE35DBJ.svg +3 -0
  20. package/dist/assets/chevron-up-VBHYAJVB.svg +5 -0
  21. package/dist/assets/clear-XNEZJKFC.svg +5 -0
  22. package/dist/assets/close-GO5F7YFS.svg +10 -0
  23. package/dist/assets/dccd4-CZJXWICW.svg +6 -0
  24. package/dist/assets/dd163-3S3O6YOT.svg +6 -0
  25. package/dist/assets/draw-FOFX5TOM.svg +6 -0
  26. package/dist/assets/e8aed-CZJXWICW.svg +6 -0
  27. package/dist/assets/ea990-CZJXWICW.svg +6 -0
  28. package/dist/assets/expand-IAV75AFA.svg +6 -0
  29. package/dist/assets/fullscreen-L2JCXIN2.svg +6 -0
  30. package/dist/assets/grid-TLG5JYWB.svg +8 -0
  31. package/dist/assets/import-AFBUV2VV.svg +4 -0
  32. package/dist/assets/inter-latin-400-normal-55WT6UWF.woff2 +0 -0
  33. package/dist/assets/inter-latin-500-normal-W62DVTXI.woff2 +0 -0
  34. package/dist/assets/inter-latin-600-normal-R6Y2SZCR.woff2 +0 -0
  35. package/dist/assets/list-5VUAOZUH.svg +9 -0
  36. package/dist/assets/magic-OMME7FOV.svg +9 -0
  37. package/dist/assets/more-AEJ5MHWV.svg +5 -0
  38. package/dist/assets/mute-SXOSCR2U.svg +6 -0
  39. package/dist/assets/pause-2E4UY4FW.svg +6 -0
  40. package/dist/assets/play-WIIT3XQK.svg +5 -0
  41. package/dist/assets/reset-JESMTEMV.svg +6 -0
  42. package/dist/assets/search-JUOSGQRM.svg +6 -0
  43. package/dist/assets/select-VU7WR7B4.svg +7 -0
  44. package/dist/assets/sound-file-YYY6VNYN.svg +3 -0
  45. package/dist/assets/volume-2TYGJL6F.svg +6 -0
  46. package/dist/client.bundle.js +1373 -0
  47. package/dist/client.css +1900 -0
  48. package/dist/client.d.ts +19 -0
  49. package/dist/client.js +3 -0
  50. package/dist/events.d.ts +102 -0
  51. package/dist/events.js +244 -0
  52. package/dist/server.d.ts +16 -0
  53. package/dist/server.js +3502 -0
  54. package/dist/shared-BUdviVVD.d.ts +263 -0
  55. package/dist/shared.d.ts +510 -0
  56. package/dist/shared.js +228 -0
  57. package/package.json +50 -3
  58. package/schemas/billing-notice.json +16 -0
  59. package/schemas/task-notice.json +57 -0
  60. package/studio-plugin.json +40 -0
  61. package/README.md +0 -3
package/dist/server.js ADDED
@@ -0,0 +1,3502 @@
1
+ // packages/project-studio/src/events.ts
2
+ import { z as z2 } from "vivix-studio/sdk/shared";
3
+
4
+ // packages/vivix-api/src/billing.ts
5
+ import { z } from "vivix-studio/sdk/shared";
6
+ var taskBilling = z.object({
7
+ status: z.enum(["pending", "settled", "released", "unavailable"]),
8
+ credits: z.string().regex(/^\d+(?:\.\d+)?$/).nullable()
9
+ }).strict().refine(
10
+ (value) => value.status === "settled" ? value.credits !== null : value.status === "released" ? value.credits === "0" : value.credits === null
11
+ );
12
+ function readTaskBilling(response) {
13
+ const parsed = taskBilling.safeParse(response?.billing);
14
+ return parsed.success ? parsed.data : { status: "unavailable", credits: null };
15
+ }
16
+
17
+ // packages/project-studio/src/events.ts
18
+ var taskEventNames = [
19
+ "project-studio/task.completed",
20
+ "project-studio/task.failed",
21
+ "project-studio/task.cancelled",
22
+ "project-studio/billing.updated"
23
+ ];
24
+ var result = z2.object({
25
+ remoteTaskId: z2.string(),
26
+ outputIndex: z2.number().int().nonnegative(),
27
+ assetId: z2.string().optional()
28
+ }).strict();
29
+ var taskNotice = z2.object({
30
+ taskId: z2.string(),
31
+ attemptId: z2.string(),
32
+ revision: z2.number().int().positive(),
33
+ requestedCount: z2.number().int().positive().nullable(),
34
+ resultCount: z2.number().int().nonnegative().nullable(),
35
+ partialResults: z2.boolean().nullable(),
36
+ results: z2.array(result)
37
+ }).strict();
38
+ var billingNotice = z2.object({
39
+ taskId: z2.string(),
40
+ attemptId: z2.string(),
41
+ billingRecordId: z2.string(),
42
+ revision: z2.number().int().positive()
43
+ }).strict();
44
+ var eventDefinitions = taskEventNames.map((name) => ({
45
+ name,
46
+ version: 1,
47
+ mode: "notification",
48
+ schema: name.endsWith("billing.updated") ? "schemas/billing-notice.json" : "schemas/task-notice.json",
49
+ scope: "project"
50
+ }));
51
+ var stateKey = "project-studio.state";
52
+ function saved(record) {
53
+ const value = record.metadata?.[stateKey];
54
+ return value?.format === 1 && value.remote && Number.isSafeInteger(value.remote.revision) ? value : void 0;
55
+ }
56
+ function readableResults(record) {
57
+ if (typeof record.data.remoteTaskId !== "string" || !record.data.remoteTaskId) return [];
58
+ return (record.data.remoteOutputs || []).flatMap((value, outputIndex) => {
59
+ if (!value || typeof value !== "object") return [];
60
+ const output = value;
61
+ return ["asset_id", "file_id", "id", "url", "download_url", "content", "text"].some(
62
+ (k) => typeof output[k] === "string" && Boolean(output[k])
63
+ ) ? [{ remoteTaskId: record.data.remoteTaskId, outputIndex }] : [];
64
+ });
65
+ }
66
+ function outcome(record) {
67
+ const d = record.data;
68
+ if (d.remoteStatus === "succeeded" && readableResults(record).length) return "succeeded";
69
+ if (d.remoteStatus === "failed") return "failed";
70
+ if (["cancelled", "canceled"].includes(d.remoteStatus) || d.state === "canceled" && !d.submittedAt && !d.remoteTaskId && d.attemptCount === 0)
71
+ return "cancelled";
72
+ return "unknown";
73
+ }
74
+ function taskState(record) {
75
+ const old = saved(record), status = outcome(record);
76
+ const requestedCount = Number.isInteger(record.data.inputSnapshot?.outputCount) ? record.data.inputSnapshot.outputCount : null;
77
+ const resultSignature = JSON.stringify(readableResults(record));
78
+ const storedCount = Array.isArray(record.data.remoteOutputs) ? record.data.remoteOutputs.length : null;
79
+ const unchanged = old?.remote.status === status && old.remote.requestedCount === requestedCount && old.remote.resultSignature === resultSignature && (old.remote.resultCount === null || old.remote.resultCount === storedCount);
80
+ const resultCount = status === "unknown" ? null : unchanged ? old.remote.resultCount : record.data.remoteOutputs?.length ? record.data.remoteOutputs.length : null;
81
+ return {
82
+ format: 1,
83
+ remote: {
84
+ status,
85
+ revision: unchanged ? old.remote.revision : record.revision,
86
+ requestedCount,
87
+ resultCount,
88
+ partialResults: requestedCount !== null && resultCount !== null ? resultCount < requestedCount : null,
89
+ resultSignature
90
+ },
91
+ billing: currentBilling(record, old?.billing)
92
+ };
93
+ }
94
+ function stateMetadata(before, data, observation) {
95
+ const previous = taskState(before), next = taskState({ ...before, data, metadata: {} });
96
+ if (observation && next.remote.status !== "unknown")
97
+ next.remote.resultCount = observation.resultCount;
98
+ else if (previous.remote.status === next.remote.status)
99
+ next.remote.resultCount = previous.remote.resultCount;
100
+ next.remote.partialResults = next.remote.requestedCount !== null && next.remote.resultCount !== null ? next.remote.resultCount < next.remote.requestedCount : null;
101
+ const signature = ({ revision: _, ...r }) => JSON.stringify(r);
102
+ next.remote.revision = signature(next.remote) === signature(previous.remote) ? previous.remote.revision : before.revision + 1;
103
+ const view = billingViewFromI4(
104
+ before,
105
+ data.cost ?? { status: "unavailable", credits: null },
106
+ before.revision + 1
107
+ );
108
+ const semanticDigest = billingDigest(view);
109
+ next.billing = {
110
+ billingRecordId: view.billingRecordId,
111
+ semanticDigest,
112
+ revision: previous.billing?.semanticDigest === semanticDigest ? previous.billing.revision : before.revision + 1
113
+ };
114
+ return { ...before.metadata, [stateKey]: next };
115
+ }
116
+ function taskProjection(record) {
117
+ const s = taskState(record), d = record.data;
118
+ const localPersistence = d.state === "succeeded" ? "saved" : d.state === "partial" ? "partial" : d.state === "save_failed" ? "failed" : d.state === "saving" && d.outputRefs?.length ? "partial" : "pending";
119
+ return {
120
+ ...record,
121
+ projection: {
122
+ attemptId: record.objectId,
123
+ remoteOutcome: s.remote,
124
+ localPersistence,
125
+ billing: taskBillingView(record)
126
+ }
127
+ };
128
+ }
129
+ function billingViewFromI4(record, cost, revision2) {
130
+ const { status, credits } = readTaskBilling({
131
+ billing: { status: cost.status, credits: cost.credits }
132
+ });
133
+ return {
134
+ billingRecordId: JSON.stringify([
135
+ record.storeId,
136
+ record.scope?.id,
137
+ record.objectId,
138
+ "task.cost"
139
+ ]),
140
+ taskId: record.objectId,
141
+ attemptId: record.objectId,
142
+ revision: revision2,
143
+ status,
144
+ amount: status === "settled" || status === "released" ? { value: credits.replace(/^0+(?=\d)/, ""), unit: "credits" } : null
145
+ };
146
+ }
147
+ function normalizeDecimal(value) {
148
+ const [whole, fraction = ""] = value.split(".");
149
+ const trimmed = fraction.replace(/0+$/, "");
150
+ return whole.replace(/^0+(?=\d)/, "") + (trimmed ? "." + trimmed : "");
151
+ }
152
+ function billingDigest(view) {
153
+ return JSON.stringify([
154
+ view.status,
155
+ view.amount && { ...view.amount, value: normalizeDecimal(view.amount.value) }
156
+ ]);
157
+ }
158
+ function currentBilling(record, old) {
159
+ const view = billingViewFromI4(
160
+ record,
161
+ record.data.cost ?? { status: "unavailable", credits: null },
162
+ record.revision
163
+ );
164
+ const semanticDigest = billingDigest(view);
165
+ return {
166
+ billingRecordId: view.billingRecordId,
167
+ semanticDigest,
168
+ revision: old?.semanticDigest === semanticDigest ? old.revision : record.revision
169
+ };
170
+ }
171
+ function taskBillingView(record) {
172
+ const state = taskState(record);
173
+ return billingViewFromI4(
174
+ record,
175
+ record.data.cost ?? { status: "unavailable", credits: null },
176
+ state.billing?.revision ?? record.revision
177
+ );
178
+ }
179
+ function notifyCommitted(events, before, after, diagnostic = (code) => console.error(code)) {
180
+ void (async () => {
181
+ const old = structuredClone(before), committed = structuredClone(after);
182
+ const a = taskState(old), b = taskState(committed), scope = events();
183
+ if (b.remote.status !== "unknown" && (a.remote.status !== b.remote.status || a.remote.revision !== b.remote.revision)) {
184
+ const name = b.remote.status === "succeeded" ? taskEventNames[0] : b.remote.status === "failed" ? taskEventNames[1] : taskEventNames[2];
185
+ const results = readableResults(committed);
186
+ await scope.emit(name, {
187
+ version: 1,
188
+ occurredAt: committed.updatedAt,
189
+ correlation: { taskId: committed.objectId, attemptId: committed.objectId },
190
+ entity: { type: "task", id: committed.objectId, revision: b.remote.revision },
191
+ payload: {
192
+ taskId: committed.objectId,
193
+ attemptId: committed.objectId,
194
+ revision: b.remote.revision,
195
+ requestedCount: b.remote.requestedCount,
196
+ resultCount: b.remote.resultCount,
197
+ partialResults: b.remote.partialResults,
198
+ results
199
+ }
200
+ });
201
+ }
202
+ if (b.billing && b.billing.revision !== a.billing?.revision)
203
+ await scope.emit(taskEventNames[3], {
204
+ version: 1,
205
+ occurredAt: committed.updatedAt,
206
+ correlation: { taskId: committed.objectId, attemptId: committed.objectId },
207
+ entity: { type: "billing", id: b.billing.billingRecordId, revision: b.billing.revision },
208
+ payload: {
209
+ taskId: committed.objectId,
210
+ attemptId: committed.objectId,
211
+ billingRecordId: b.billing.billingRecordId,
212
+ revision: b.billing.revision
213
+ }
214
+ });
215
+ })().catch(() => {
216
+ try {
217
+ diagnostic("EVENT_NOTIFICATION_FAILED");
218
+ } catch {
219
+ }
220
+ });
221
+ }
222
+
223
+ // packages/project-studio/src/ads.ts
224
+ import { createHash as createHash2 } from "crypto";
225
+
226
+ // packages/project-studio/src/ads-shared.ts
227
+ import { z as z3 } from "vivix-studio/sdk/shared";
228
+ var fixedRef = z3.object({ storeId: z3.uuid(), objectId: z3.uuid(), versionId: z3.uuid() }).strict();
229
+ var copy = z3.string().max(8e3).default("");
230
+ var clipScript = z3.object({
231
+ title: z3.string().trim().min(1).max(200),
232
+ visual: copy,
233
+ voiceover: copy,
234
+ onScreenText: copy,
235
+ cta: copy,
236
+ plannedDurationMs: z3.number().int().positive().nullable().default(null)
237
+ }).strict();
238
+ var adBrief = z3.object({
239
+ product: copy,
240
+ category: copy,
241
+ audience: copy,
242
+ sellingPoints: copy,
243
+ channel: copy,
244
+ aspectRatio: z3.string().max(40).nullable().default(null),
245
+ targetDurationMs: z3.number().int().positive().nullable().default(null),
246
+ body: copy,
247
+ productRefs: z3.array(fixedRef).max(100).default([]),
248
+ modelRefs: z3.array(fixedRef).max(100).default([])
249
+ }).strict();
250
+ var adStoryboard = z3.object({
251
+ brief: adBrief,
252
+ clips: z3.array(
253
+ z3.object({
254
+ assetId: z3.uuid(),
255
+ script: clipScript,
256
+ edits: z3.array(z3.object({ sourceVersionId: z3.uuid(), editRef: fixedRef }).strict()).max(200)
257
+ }).strict()
258
+ ).max(200)
259
+ }).strict().superRefine((value, context) => {
260
+ if (new Set(value.clips.map((c) => c.assetId)).size !== value.clips.length)
261
+ context.addIssue({ code: "custom", message: "A clip can appear only once in a storyboard." });
262
+ for (const clip of value.clips)
263
+ if (new Set(clip.edits.map((e) => e.sourceVersionId)).size !== clip.edits.length)
264
+ context.addIssue({
265
+ code: "custom",
266
+ message: "Each candidate has one current edit binding."
267
+ });
268
+ });
269
+ var adTypeId = "studio.adStoryboard";
270
+
271
+ // packages/project-studio/src/cost.ts
272
+ function taskCostLabel(task) {
273
+ const cost = task.cost;
274
+ if (cost?.status === "settled" && cost.credits != null) return `${cost.credits} credits`;
275
+ if (cost?.status === "pending") return "Pending settlement";
276
+ return "-";
277
+ }
278
+
279
+ // packages/project-studio/src/shared.ts
280
+ import { z as z4, defineOperation } from "vivix-studio/sdk/shared";
281
+ var studioConfig = z4.object({
282
+ role: z4.enum(["project", "coordinator"]).default("project"),
283
+ dataInstance: z4.string().min(1).default("user:data"),
284
+ apiInstance: z4.string().min(1).default("api"),
285
+ libraryInstance: z4.string().min(1).nullable().default(null)
286
+ }).strict();
287
+ var versionRef = z4.object({ storeId: z4.uuid(), objectId: z4.uuid(), versionId: z4.uuid() }).strict();
288
+ var tags = z4.array(z4.string().trim().min(1).max(64)).max(50).transform((x) => [...new Set(x)]).default([]);
289
+ var kind = z4.enum(["text", "image", "video", "audio", "file"]);
290
+ var projectData = z4.object({
291
+ name: z4.string().trim().min(1).max(120),
292
+ description: z4.string().max(4e3).default(""),
293
+ status: z4.enum(["draft", "active", "archived"]).default("draft"),
294
+ aspectRatio: z4.string().nullable().default(null),
295
+ coverRef: versionRef.nullable().default(null),
296
+ briefRef: z4.object({ storeId: z4.uuid(), objectId: z4.uuid() }).strict().nullable().default(null),
297
+ tags
298
+ }).strict();
299
+ var assetData = z4.object({
300
+ projectId: z4.uuid(),
301
+ name: z4.string().trim().min(1).max(200),
302
+ kind,
303
+ purpose: z4.enum(["reference", "script", "shot", "music", "voice", "subtitle", "final", "other"]).default("other"),
304
+ origin: z4.enum(["upload", "library", "generated", "authored"]),
305
+ orderKey: z4.number().int().nullable().default(null),
306
+ tags,
307
+ lifecycle: z4.enum(["draft", "ready", "archived"]).default("draft"),
308
+ sourceRef: versionRef.nullable().default(null),
309
+ adoptedVersionId: z4.uuid().nullable().default(null)
310
+ }).strict();
311
+ var taskStates = [
312
+ "queued",
313
+ "submitting",
314
+ "submission_unknown",
315
+ "running",
316
+ "cancel_requested",
317
+ "remote_succeeded",
318
+ "saving",
319
+ "save_failed",
320
+ "partial",
321
+ "succeeded",
322
+ "failed",
323
+ "canceled"
324
+ ];
325
+ var anchor = z4.discriminatedUnion("kind", [
326
+ z4.object({ kind: z4.literal("whole") }).strict(),
327
+ z4.object({
328
+ kind: z4.literal("timeRange"),
329
+ startMs: z4.number().int().nonnegative(),
330
+ endMs: z4.number().int().nonnegative()
331
+ }).strict()
332
+ ]);
333
+ var generationInput = z4.object({
334
+ targetAssetId: z4.uuid().optional(),
335
+ taskName: z4.string().trim().min(1).max(200).optional(),
336
+ context: z4.record(z4.string().min(1).max(64), z4.string().min(1).max(200)).optional(),
337
+ expectedRevision: z4.number().int().positive().optional(),
338
+ newAsset: z4.object({
339
+ name: z4.string().trim().min(1).max(200),
340
+ kind: z4.enum(["image", "video", "audio"])
341
+ }).strict().optional(),
342
+ modelId: z4.string().min(1),
343
+ capabilityId: z4.string().min(1),
344
+ prompt: z4.string().max(32e3),
345
+ parameters: z4.record(z4.string(), z4.unknown()).default({}),
346
+ outputCount: z4.number().int().min(1).max(100).default(1),
347
+ inputs: z4.array(
348
+ z4.object({
349
+ ref: versionRef,
350
+ role: z4.string().min(1),
351
+ order: z4.number().int().nonnegative(),
352
+ weight: z4.number().finite().nullable().default(null)
353
+ }).strict()
354
+ ).max(100).default([]),
355
+ baseVersionId: z4.uuid().optional(),
356
+ changeRequest: z4.string().trim().min(1).max(4e3).optional(),
357
+ preserveRequest: z4.string().max(4e3).default(""),
358
+ anchor: anchor.default({ kind: "whole" }),
359
+ retryOfTaskId: z4.uuid().optional(),
360
+ acknowledgeUnknown: z4.boolean().optional()
361
+ }).strict();
362
+ var projectSettingsInput = z4.object({
363
+ projectId: z4.uuid(),
364
+ expectedRevision: z4.number().int().positive(),
365
+ changes: projectData.pick({ name: true, description: true, tags: true, aspectRatio: true, coverRef: true }).extend({
366
+ description: projectData.shape.description.unwrap(),
367
+ tags: projectData.shape.tags.unwrap(),
368
+ aspectRatio: projectData.shape.aspectRatio.unwrap(),
369
+ coverRef: projectData.shape.coverRef.unwrap()
370
+ }).partial().strict()
371
+ }).strict();
372
+ var generationSubmitOperation = defineOperation({
373
+ id: "generation.submit",
374
+ title: "Generate",
375
+ description: "Submit one media generation without approval, retaining its paid request identity.",
376
+ input: generationInput,
377
+ output: z4.any(),
378
+ effects: ["write", "paid"],
379
+ confirm: "none"
380
+ });
381
+ var favoriteData = z4.object({ ref: versionRef }).strict();
382
+ var sqlScalar = z4.union([z4.string(), z4.number().finite(), z4.null()]);
383
+ var globalQueryInput = z4.object({
384
+ sql: z4.string().trim().min(1),
385
+ params: z4.union([z4.array(sqlScalar), z4.record(z4.string(), sqlScalar)]).optional(),
386
+ timeoutMs: z4.number().int().positive().optional(),
387
+ maxRows: z4.number().int().positive().optional(),
388
+ limit: z4.number().int().min(1).max(200).optional(),
389
+ offset: z4.number().int().nonnegative().optional(),
390
+ source: z4.enum(["projects", "assets", "tasks"]),
391
+ catalog: z4.array(z4.object({ id: z4.string().min(1), name: z4.string(), available: z4.boolean() }).strict()).optional()
392
+ }).strict();
393
+
394
+ // packages/project-studio/src/assets.ts
395
+ import { createHash } from "crypto";
396
+ import { PluginError } from "vivix-studio/sdk/shared";
397
+ function reject(code, message, revision2) {
398
+ throw new PluginError(
399
+ code,
400
+ message,
401
+ "Refresh the current project state and retry the original operation.",
402
+ { currentRevision: revision2 }
403
+ );
404
+ }
405
+ var Assets = class {
406
+ constructor(data, workspace, library) {
407
+ this.data = data;
408
+ this.workspace = workspace;
409
+ this.library = library;
410
+ }
411
+ data;
412
+ workspace;
413
+ library;
414
+ assertWritable() {
415
+ const p = this.data.get(this.data.scope.id);
416
+ if (p.typeId !== "studio.project" || p.data.status === "archived")
417
+ reject("PROJECT_ARCHIVED", "Restore the project before creating or changing content.");
418
+ }
419
+ get(id2, deleted = false) {
420
+ const a = this.data.get(id2, deleted);
421
+ if (a.typeId !== "studio.asset") reject("TYPE_MISMATCH", "Expected a project asset.");
422
+ return a;
423
+ }
424
+ async describe(id2) {
425
+ const object = this.get(id2, true), versions = this.data.versions(id2), latest = versions.at(-1);
426
+ const adopted = versions.find((v) => v.versionId === object.data.adoptedVersionId);
427
+ return {
428
+ ...object,
429
+ versions,
430
+ latestVersionId: latest?.versionId || null,
431
+ adoptedVersionId: adopted?.versionId || null,
432
+ newCandidateCount: versions.filter((v) => !adopted || v.versionNumber > adopted.versionNumber).length,
433
+ reviewState: !latest ? "none" : adopted ? "adopted" : "pending",
434
+ availability: latest ? await this.data.availability({
435
+ storeId: this.data.storeId,
436
+ objectId: id2,
437
+ versionId: latest.versionId
438
+ }) : "no_content"
439
+ };
440
+ }
441
+ async import(input, call, existing = false, intent = input, operationName = existing ? "assets.importVersion" : "assets.import") {
442
+ this.assertWritable();
443
+ const replay = this.data.reserve(operationName, intent, call);
444
+ if (replay) return replay.result;
445
+ const op = stableId(this.data.storeId, call, operationName);
446
+ const objectId = existing ? input.assetId : stableId(op, call, "object"), versionId = stableId(op, call, "version");
447
+ const prior = existing ? this.get(objectId) : void 0;
448
+ const pub = await this.data.publish(
449
+ input.stagingFileIds,
450
+ objectId,
451
+ versionId,
452
+ op,
453
+ call.actor.id,
454
+ input.fileRoles
455
+ );
456
+ if (!["text", "image", "video", "audio"].includes(pub.kind))
457
+ reject("UNSUPPORTED_MEDIA", "Import text, image, video or audio content.");
458
+ if (prior && pub.kind !== prior.data.kind)
459
+ reject("TYPE_MISMATCH", "New content must have the same actual kind as the asset.");
460
+ if (input.kind && input.kind !== pub.kind)
461
+ reject("TYPE_MISMATCH", "Actual content kind does not match the requested kind.");
462
+ const result2 = this.data.operation(operationName, intent, call, (t) => {
463
+ return this.commitPreparedImport(t, input, pub, objectId, versionId, existing);
464
+ });
465
+ await this.data.finishFiles(op);
466
+ return result2;
467
+ }
468
+ commitPreparedImport(t, input, pub, objectId, versionId, existing = false) {
469
+ const object = existing ? t.get(objectId) : t.create(
470
+ "studio.asset",
471
+ assetData.parse({
472
+ projectId: this.data.scope.id,
473
+ name: input.name,
474
+ kind: pub.kind,
475
+ origin: input.origin || "upload",
476
+ sourceRef: input.sourceRef || null,
477
+ tags: input.tags
478
+ }),
479
+ input.metadata || {},
480
+ objectId
481
+ );
482
+ const version = t.commitVersion(objectId, existing ? input.expectedRevision : object.revision, {
483
+ versionId,
484
+ parentVersionId: input.parentVersionId,
485
+ sourceRef: input.sourceRef,
486
+ origin: input.origin === "authored" ? "authored" : input.sourceRef ? "copied" : "upload",
487
+ payload: { ...input.sourcePayload, ...payload(pub, input.sourcePayload?.generation) },
488
+ metadata: input.versionMetadata,
489
+ files: pub.files
490
+ });
491
+ selectFirstAdCandidate(this.data, t, objectId, version.versionId);
492
+ return { asset: t.get(objectId), version };
493
+ }
494
+ async text(input, call) {
495
+ this.assertWritable();
496
+ const name = input.assetId ? "assets.saveText" : "assets.createText", replay = this.data.reserve(name, input, call);
497
+ if (replay) return replay.result;
498
+ const staged = await this.data.stageText(input.text, call.actor.id);
499
+ return this.import(
500
+ { ...input, origin: "authored", kind: "text", stagingFileIds: [staged.stagingFileId] },
501
+ call,
502
+ Boolean(input.assetId),
503
+ input,
504
+ name
505
+ );
506
+ }
507
+ update(input, call) {
508
+ this.assertWritable();
509
+ return this.data.operation("assets.update", input, call, (t) => {
510
+ const a = this.get(input.assetId);
511
+ return t.update(a.objectId, input.expectedRevision, { ...a.data, ...input.changes });
512
+ });
513
+ }
514
+ async adopt(input, call, clear = false) {
515
+ this.assertWritable();
516
+ const a = this.get(input.assetId);
517
+ if (!clear) {
518
+ const ref = { storeId: this.data.storeId, objectId: a.objectId, versionId: input.versionId };
519
+ this.data.version(ref);
520
+ await this.data.location(ref);
521
+ }
522
+ return this.data.operation(
523
+ clear ? "choices.clear" : "choices.adopt",
524
+ input,
525
+ call,
526
+ (t) => t.update(a.objectId, input.expectedRevision, {
527
+ ...t.get(a.objectId).data,
528
+ adoptedVersionId: clear ? null : input.versionId
529
+ })
530
+ );
531
+ }
532
+ remove(input, call) {
533
+ this.assertWritable();
534
+ const result2 = this.data.operation("assets.delete", input, call, (t) => {
535
+ const a = this.get(input.assetId);
536
+ return t.update(
537
+ a.objectId,
538
+ input.expectedRevision,
539
+ a.data,
540
+ void 0,
541
+ (/* @__PURE__ */ new Date()).toISOString()
542
+ );
543
+ });
544
+ this.removeUsages(input.assetId, call);
545
+ return result2;
546
+ }
547
+ removeUsages(assetId, call) {
548
+ this.library?.removeUsage(
549
+ { projectId: this.data.scope.id, targetAssetId: assetId },
550
+ this.libraryCall(call, "remove-" + assetId)
551
+ );
552
+ }
553
+ libraryCall(call, suffix) {
554
+ const originalRequestKey = call.originalRequestKey || call.requestKey;
555
+ return {
556
+ scope: this.data.scope,
557
+ actor: call.actor,
558
+ requestKey: originalRequestKey ? "studio-lib-" + createHash("sha256").update(JSON.stringify([this.data.scope, call.actor.id, originalRequestKey, suffix])).digest("hex") : void 0,
559
+ originalRequestKey,
560
+ sourceOperation: suffix,
561
+ confirmation: call.confirmation
562
+ };
563
+ }
564
+ async copy(source, ref, input, call, existing = false) {
565
+ this.assertWritable();
566
+ const operationName = existing ? "assets.updateFromMaterial" : "assets.importMaterial", intent = { ref, ...input };
567
+ const replay = this.data.reserve(operationName, intent, call);
568
+ let result2 = replay?.result;
569
+ if (!result2) {
570
+ if (typeof source === "function") source = await source();
571
+ const snapshot = "material" in source ? source : null;
572
+ const original = snapshot ? snapshot.material : source.get(ref.objectId);
573
+ const v = snapshot ? snapshot.version : source.version(ref);
574
+ const files = snapshot ? snapshot.files : source.files(ref);
575
+ const ids = [], roles = [];
576
+ for (const f of files) {
577
+ let stream;
578
+ if (snapshot) stream = snapshot.files.find((file) => file.fileId === f.fileId).source;
579
+ else {
580
+ const loc = await source.location(ref, f.fileId);
581
+ const { createReadStream } = await import("fs");
582
+ stream = createReadStream(loc.absolutePath);
583
+ }
584
+ roles.push(f.role);
585
+ ids.push((await this.data.stage(stream, f.originalName, call.actor.id)).stagingFileId);
586
+ }
587
+ result2 = await this.import(
588
+ {
589
+ ...input,
590
+ name: input.name || original.data.name,
591
+ kind: v.payload.kind,
592
+ sourceRef: ref,
593
+ sourcePayload: v.payload,
594
+ metadata: original.metadata,
595
+ versionMetadata: v.metadata,
596
+ parentVersionId: existing ? input.parentVersionId || this.data.versions(input.assetId).at(-1)?.versionId : void 0,
597
+ origin: "library",
598
+ stagingFileIds: ids,
599
+ fileRoles: roles
600
+ },
601
+ call,
602
+ existing,
603
+ intent,
604
+ operationName
605
+ );
606
+ }
607
+ if (this.library)
608
+ this.library.reportUsage(
609
+ {
610
+ materialRef: ref,
611
+ projectId: this.data.scope.id,
612
+ targetAssetId: result2.asset.objectId,
613
+ targetVersionId: result2.version.versionId,
614
+ name: result2.asset.data.name
615
+ },
616
+ this.libraryCall(call, "usage-" + result2.version.versionId)
617
+ );
618
+ return result2;
619
+ }
620
+ async collect(input, call) {
621
+ this.assertWritable();
622
+ if (!this.library)
623
+ reject("LIBRARY_UNAVAILABLE", "Configure a material library before publishing.");
624
+ const ref = input.ref;
625
+ const object = this.get(ref.objectId), version = this.data.version(ref), project = this.data.get(this.data.scope.id);
626
+ const { createReadStream } = await import("fs");
627
+ const files = [];
628
+ for (const f of this.data.files(ref)) {
629
+ const loc = await this.data.location(ref, f.fileId);
630
+ files.push({ ...f, source: createReadStream(loc.absolutePath) });
631
+ }
632
+ return this.library.publish(
633
+ {
634
+ projectId: this.data.scope.id,
635
+ ref,
636
+ sourceLabel: {
637
+ projectName: project.data.name,
638
+ objectName: object.data.name,
639
+ versionNumber: version.versionNumber
640
+ },
641
+ name: input.name,
642
+ kind: version.payload.kind,
643
+ payload: version.payload,
644
+ metadata: version.metadata,
645
+ files
646
+ },
647
+ this.libraryCall(call, "publish")
648
+ );
649
+ }
650
+ };
651
+ function stableId(seed, call, name) {
652
+ if (!call.requestKey) reject("INVALID_REQUEST_KEY", "A stable request key is required.");
653
+ if (!call.scope) reject("SCOPE_MISMATCH", "A bound business scope is required.");
654
+ const h = createHash("sha256").update(
655
+ JSON.stringify([seed, call.scope.kind, call.scope.id, call.actor.id, call.requestKey, name])
656
+ ).digest("hex");
657
+ return `${h.slice(0, 8)}-${h.slice(8, 12)}-4${h.slice(13, 16)}-a${h.slice(17, 20)}-${h.slice(20, 32)}`;
658
+ }
659
+ function payload(pub, generation = null) {
660
+ return {
661
+ kind: pub.kind,
662
+ primaryFileId: pub.files[0].fileId,
663
+ attachments: pub.files.slice(1).map((f) => ({ fileId: f.fileId, role: f.role })),
664
+ ...pub.details,
665
+ generation
666
+ };
667
+ }
668
+
669
+ // packages/project-studio/src/ads.ts
670
+ function storyboardId(projectId) {
671
+ const hash2 = createHash2("sha256").update("studio.adStoryboard:" + projectId).digest("hex");
672
+ return `${hash2.slice(0, 8)}-${hash2.slice(8, 12)}-4${hash2.slice(13, 16)}-a${hash2.slice(17, 20)}-${hash2.slice(20, 32)}`;
673
+ }
674
+ var adDefinition = {
675
+ typeId: adTypeId,
676
+ schemaVersion: 1,
677
+ scopeKinds: ["project"],
678
+ validate: (input) => adStoryboard.parse(input)
679
+ };
680
+ function activeAd(data) {
681
+ try {
682
+ const record = data.get(storyboardId(data.scope.id));
683
+ if (record.typeId !== adTypeId) reject("TYPE_MISMATCH", "Expected an advertising storyboard.");
684
+ return record;
685
+ } catch (error) {
686
+ if (error.code === "OBJECT_NOT_FOUND") return null;
687
+ throw error;
688
+ }
689
+ }
690
+ function selectFirstAdCandidate(data, tx, assetId, versionId) {
691
+ const ad = activeAd(data);
692
+ if (!ad || !ad.data.clips.some((clip) => clip.assetId === assetId)) return;
693
+ const asset = tx.get(assetId);
694
+ if (asset.data.kind !== "video" || asset.data.adoptedVersionId || data.versions(assetId).length !== 1)
695
+ return;
696
+ tx.update(assetId, asset.revision, { ...asset.data, adoptedVersionId: versionId });
697
+ }
698
+ function hasReferenceContract(tx) {
699
+ return "removeRelations" in tx && typeof tx.removeRelations === "function";
700
+ }
701
+ var Ads = class {
702
+ constructor(data) {
703
+ this.data = data;
704
+ }
705
+ data;
706
+ assertWritable() {
707
+ const project = this.data.get(this.data.scope.id);
708
+ if (project.typeId !== "studio.project" || project.data.status === "archived")
709
+ reject("PROJECT_ARCHIVED", "Restore the project before changing its storyboard.");
710
+ }
711
+ get() {
712
+ return activeAd(this.data);
713
+ }
714
+ required() {
715
+ const ad = this.get();
716
+ if (!ad) reject("STORYBOARD_NOT_FOUND", "Initialize this advertising project first.");
717
+ return ad;
718
+ }
719
+ initialize(call) {
720
+ this.assertWritable();
721
+ return this.data.operation("ads.initialize", {}, call, (tx) => {
722
+ const existing = this.get();
723
+ if (existing) return existing;
724
+ const project = tx.get(this.data.scope.id);
725
+ return tx.create(
726
+ adTypeId,
727
+ {
728
+ brief: adBrief.parse({ aspectRatio: project.data.aspectRatio }),
729
+ clips: []
730
+ },
731
+ {},
732
+ storyboardId(this.data.scope.id)
733
+ );
734
+ });
735
+ }
736
+ change(name, input, call, update) {
737
+ this.assertWritable();
738
+ return this.data.operation(name, input, call, (tx) => {
739
+ const ad = this.required();
740
+ if (ad.revision !== input.expectedRevision)
741
+ reject(
742
+ "REVISION_CONFLICT",
743
+ "The storyboard changed. Keep your draft and reload.",
744
+ ad.revision
745
+ );
746
+ const next = update(structuredClone(ad.data), tx);
747
+ this.syncReferences(tx, ad.objectId, ad.data, next);
748
+ return tx.update(ad.objectId, ad.revision, next);
749
+ });
750
+ }
751
+ syncReferences(tx, id2, before, after) {
752
+ const refs = (ad) => [
753
+ ...ad.brief.productRefs,
754
+ ...ad.brief.modelRefs,
755
+ ...ad.clips.flatMap((clip) => clip.edits.map((edit) => edit.editRef))
756
+ ];
757
+ const next = refs(after);
758
+ if (!refs(before).length && !next.length) return;
759
+ if (!hasReferenceContract(tx))
760
+ reject(
761
+ "REFERENCE_PROTECTION_UNAVAILABLE",
762
+ "Update the storage reference contract before saving fixed references. Your draft and saved edit are retained."
763
+ );
764
+ tx.removeRelations(id2, "reference");
765
+ const seen = /* @__PURE__ */ new Set();
766
+ for (const ref of next) {
767
+ const key = JSON.stringify(ref);
768
+ if (seen.has(key)) continue;
769
+ this.data.get(ref.objectId);
770
+ this.data.version(ref);
771
+ tx.relation(id2, null, ref, "reference");
772
+ seen.add(key);
773
+ }
774
+ }
775
+ clip(ad, assetId) {
776
+ const clip = ad.clips.find((c) => c.assetId === assetId);
777
+ if (!clip) reject("CLIP_NOT_FOUND", "This clip is not part of the current storyboard.");
778
+ const asset = this.data.get(assetId);
779
+ if (asset.typeId !== "studio.asset" || asset.data.kind !== "video")
780
+ reject("TYPE_MISMATCH", "A clip requires a project video asset.");
781
+ return clip;
782
+ }
783
+ addClip(input, call) {
784
+ const assetId = stableId(this.data.storeId, call, "ads.clip");
785
+ return this.change("ads.addClip", input, call, (ad, tx) => {
786
+ tx.create(
787
+ "studio.asset",
788
+ assetData.parse({
789
+ projectId: this.data.scope.id,
790
+ name: input.title,
791
+ kind: "video",
792
+ purpose: "shot",
793
+ origin: "generated"
794
+ }),
795
+ {},
796
+ assetId
797
+ );
798
+ ad.clips.push({ assetId, script: clipScript.parse({ title: input.title }), edits: [] });
799
+ return ad;
800
+ });
801
+ }
802
+ updateBrief(input, call) {
803
+ for (const ref of [...input.brief.productRefs, ...input.brief.modelRefs]) {
804
+ this.data.version(ref);
805
+ const record = this.data.get(ref.objectId);
806
+ if (record.typeId !== "studio.asset" || record.data.kind !== "image")
807
+ reject("TYPE_MISMATCH", "Product and model references must be project images.");
808
+ }
809
+ return this.change("ads.updateBrief", input, call, (ad) => ({ ...ad, brief: input.brief }));
810
+ }
811
+ updateClip(input, call) {
812
+ return this.change("ads.updateClip", input, call, (ad) => {
813
+ this.clip(ad, input.assetId).script = input.script;
814
+ return ad;
815
+ });
816
+ }
817
+ reorder(input, call) {
818
+ return this.change("ads.reorder", input, call, (ad) => {
819
+ if (input.clipIds.length !== ad.clips.length || new Set(input.clipIds).size !== ad.clips.length || input.clipIds.some((id2) => !ad.clips.some((c) => c.assetId === id2)))
820
+ reject("INVALID_CLIP_ORDER", "Include every current clip exactly once.");
821
+ ad.clips = input.clipIds.map((id2) => ad.clips.find((c) => c.assetId === id2));
822
+ return ad;
823
+ });
824
+ }
825
+ removeClip(input, call) {
826
+ return this.change("ads.removeClip", input, call, (ad) => {
827
+ this.clip(ad, input.assetId);
828
+ ad.clips = ad.clips.filter((c) => c.assetId !== input.assetId);
829
+ return ad;
830
+ });
831
+ }
832
+ assertMedia(media) {
833
+ if (media.capabilityVersion !== "media-tools/1" || media.storeId !== this.data.storeId || media.projectId !== this.data.scope.id)
834
+ reject("SCOPE_MISMATCH", "Configure media tools for this same project and store.");
835
+ }
836
+ async bindEdit(input, call, media) {
837
+ this.assertWritable();
838
+ const replay = this.data.receipt("ads.bindEdit", input, call);
839
+ if (replay) return replay.result;
840
+ if (!media)
841
+ reject(
842
+ "MEDIA_TOOLS_UNAVAILABLE",
843
+ "Install and configure the media tools before applying an edit."
844
+ );
845
+ this.assertMedia(media);
846
+ this.data.get(input.editRef.objectId);
847
+ this.data.version(input.editRef);
848
+ const resolved = await media.resolve({ kind: "edit", editRef: input.editRef });
849
+ const source = resolved.file.source;
850
+ if (source.storeId !== this.data.storeId || source.objectId !== input.assetId || source.versionId !== input.sourceVersionId || !this.data.files(source).some((f) => f.fileId === resolved.file.fileId))
851
+ reject(
852
+ "CANDIDATE_CHANGED",
853
+ "The saved edit does not belong to this clip candidate. Your saved edit is retained."
854
+ );
855
+ return this.change("ads.bindEdit", input, call, (ad, tx) => {
856
+ const clip = this.clip(ad, input.assetId), asset = tx.get(input.assetId);
857
+ if (asset.revision !== input.expectedAssetRevision || asset.data.adoptedVersionId !== input.sourceVersionId)
858
+ reject(
859
+ "CANDIDATE_CHANGED",
860
+ "The selected candidate changed. Your draft and saved edit are retained."
861
+ );
862
+ clip.edits = clip.edits.filter((e) => e.sourceVersionId !== input.sourceVersionId);
863
+ clip.edits.push({ sourceVersionId: input.sourceVersionId, editRef: input.editRef });
864
+ return ad;
865
+ });
866
+ }
867
+ async resolveSequence(media) {
868
+ const ad = this.required();
869
+ const snapshots = ad.data.clips.map((clip) => {
870
+ try {
871
+ const asset = this.data.get(clip.assetId), versions = this.data.versions(clip.assetId);
872
+ return { clip: structuredClone(clip), asset, versions };
873
+ } catch (error) {
874
+ return { clip, error };
875
+ }
876
+ });
877
+ const entries = await Promise.all(
878
+ snapshots.map(async (snapshot) => {
879
+ const { clip, asset, versions } = snapshot;
880
+ const base2 = {
881
+ assetId: clip.assetId,
882
+ title: clip.script.title,
883
+ assetRevision: asset?.revision || 0,
884
+ selectedVersionId: asset?.data.adoptedVersionId || null,
885
+ candidates: (versions || []).map((v) => ({
886
+ versionId: v.versionId,
887
+ versionNumber: v.versionNumber,
888
+ durationMs: v.payload.durationMs ?? null,
889
+ generation: v.payload.generation || null
890
+ }))
891
+ };
892
+ try {
893
+ if (snapshot.error) throw snapshot.error;
894
+ if (asset.typeId !== "studio.asset" || asset.data.kind !== "video")
895
+ reject("TYPE_MISMATCH", "Clip asset is not a video.");
896
+ const versionId = asset.data.adoptedVersionId;
897
+ if (!versionId)
898
+ return {
899
+ ...base2,
900
+ state: "empty",
901
+ message: "This clip has no selected video yet."
902
+ };
903
+ const source = { storeId: this.data.storeId, objectId: asset.objectId, versionId };
904
+ const version = this.data.version(source);
905
+ const fileId = version.payload.primaryFileId;
906
+ const file = { source, fileId };
907
+ const saved2 = clip.edits.find(
908
+ (e) => e.sourceVersionId === versionId
909
+ );
910
+ if (saved2) {
911
+ if (!media)
912
+ reject(
913
+ "MEDIA_TOOLS_UNAVAILABLE",
914
+ "The media tools are needed to preview this saved edit."
915
+ );
916
+ this.assertMedia(media);
917
+ this.data.get(saved2.editRef.objectId);
918
+ this.data.version(saved2.editRef);
919
+ const resolved = await media.resolve({ kind: "edit", editRef: saved2.editRef });
920
+ if (resolved.file.source.storeId !== file.source.storeId || resolved.file.source.objectId !== file.source.objectId || resolved.file.source.versionId !== file.source.versionId || resolved.file.fileId !== file.fileId)
921
+ reject("INVALID_EDIT_SOURCE", "The edit references another candidate.");
922
+ const durationMs2 = resolved.recipe.trim.outMs - resolved.recipe.trim.inMs;
923
+ if (!Number.isSafeInteger(durationMs2) || durationMs2 <= 0 || resolved.effectiveDurationMs !== durationMs2 || !resolved.previewUrl)
924
+ reject("INVALID_EDIT_RECIPE", "The saved edit has inconsistent playable duration.");
925
+ return {
926
+ ...base2,
927
+ ...resolved,
928
+ state: "ready",
929
+ input: { kind: "edit", editRef: saved2.editRef },
930
+ durationMs: durationMs2
931
+ };
932
+ }
933
+ const location = await this.data.location(source, fileId);
934
+ const durationMs = version.payload.durationMs;
935
+ if (!Number.isFinite(durationMs) || durationMs <= 0 || !location.previewUrl)
936
+ reject("MEDIA_UNAVAILABLE", "The selected video has no playable duration or URL.");
937
+ return {
938
+ ...base2,
939
+ state: "ready",
940
+ input: { kind: "source", file },
941
+ file,
942
+ previewUrl: location.previewUrl,
943
+ durationMs,
944
+ recipe: {
945
+ trim: { inMs: 0, outMs: durationMs },
946
+ crop: { x: 0, y: 0, width: 1, height: 1 }
947
+ }
948
+ };
949
+ } catch (error) {
950
+ return { ...base2, state: "unavailable", message: error.message };
951
+ }
952
+ })
953
+ );
954
+ return {
955
+ storyboardId: ad.objectId,
956
+ storyboardRevision: ad.revision,
957
+ aspectRatio: ad.data.brief.aspectRatio,
958
+ entries,
959
+ actualDurationMs: entries.reduce(
960
+ (sum, entry) => sum + (entry.state === "ready" ? entry.durationMs : 0),
961
+ 0
962
+ ),
963
+ targetDurationMs: ad.data.brief.targetDurationMs
964
+ };
965
+ }
966
+ };
967
+
968
+ // packages/project-studio/src/server.ts
969
+ import { z as z8, defineOperation as defineOperation2 } from "vivix-studio/sdk/shared";
970
+
971
+ // packages/project-studio/src/global.ts
972
+ import { performance } from "perf_hooks";
973
+
974
+ // packages/project-studio/src/projects.ts
975
+ var Projects = class {
976
+ constructor(data) {
977
+ this.data = data;
978
+ }
979
+ data;
980
+ get(projectId = this.data.scope.id) {
981
+ if (projectId !== this.data.scope.id)
982
+ reject("SCOPE_MISMATCH", "Project settings belong to the active project.");
983
+ const project = this.data.get(projectId);
984
+ if (project.typeId !== "studio.project") reject("TYPE_MISMATCH", "Expected project settings.");
985
+ return project;
986
+ }
987
+ update(input, call) {
988
+ const old = this.get(input.projectId);
989
+ if (old.data.status === "archived" && input.changes.status !== "active")
990
+ reject("PROJECT_ARCHIVED", "Restore the project before changing its settings.");
991
+ return this.data.operation("projects.update", input, call, (tx) => {
992
+ const { coverRef } = input.changes;
993
+ if (coverRef !== void 0) {
994
+ if (coverRef) {
995
+ const asset = tx.get(coverRef.objectId);
996
+ if (asset.typeId !== "studio.asset" || asset.data.kind !== "image")
997
+ reject("TYPE_MISMATCH", "Choose a saved image from this project for the cover.");
998
+ }
999
+ tx.removeRelations(old.objectId, "reference");
1000
+ if (coverRef) tx.relation(old.objectId, null, coverRef, "reference");
1001
+ }
1002
+ return tx.update(old.objectId, input.expectedRevision, { ...old.data, ...input.changes });
1003
+ });
1004
+ }
1005
+ };
1006
+
1007
+ // packages/project-studio/src/global.ts
1008
+ var refKey = (ref) => `${ref.storeId}/${ref.objectId}/${ref.versionId}`;
1009
+ var assetColumns = [
1010
+ "rowId",
1011
+ "projectId",
1012
+ "assetId",
1013
+ "ref",
1014
+ "name",
1015
+ "kind",
1016
+ "purpose",
1017
+ "metadata",
1018
+ "generation",
1019
+ "prompt",
1020
+ "originalPrompt",
1021
+ "refs",
1022
+ "modelId",
1023
+ "parameters",
1024
+ "snapshotAvailable",
1025
+ "taskId",
1026
+ "outputIndex",
1027
+ "previewUrl",
1028
+ "mimeType",
1029
+ "createdAt",
1030
+ "updatedAt",
1031
+ "adoptedVersionId",
1032
+ "versionId",
1033
+ "versionNumber",
1034
+ "latestVersionId",
1035
+ "favorite",
1036
+ "favoriteRefs"
1037
+ ];
1038
+ var projectColumns = [
1039
+ "rowId",
1040
+ "projectId",
1041
+ "name",
1042
+ "hostName",
1043
+ "available",
1044
+ "assetCount",
1045
+ "metadata",
1046
+ "createdAt",
1047
+ "updatedAt",
1048
+ "status",
1049
+ "description",
1050
+ "tags",
1051
+ "aspectRatio",
1052
+ "coverRef",
1053
+ "previewRef"
1054
+ ];
1055
+ var taskColumns = [
1056
+ "revision",
1057
+ "attemptId",
1058
+ "remoteOutcome",
1059
+ "localPersistence",
1060
+ "rowId",
1061
+ "projectId",
1062
+ "taskId",
1063
+ "name",
1064
+ "state",
1065
+ "outputRefs",
1066
+ "createdAt",
1067
+ "updatedAt",
1068
+ "targetAssetId",
1069
+ "kind",
1070
+ "prompt",
1071
+ "originalPrompt",
1072
+ "refs",
1073
+ "modelId",
1074
+ "parameters",
1075
+ "progress",
1076
+ "error",
1077
+ "expectedOutputs",
1078
+ "receivedOutputs",
1079
+ "cost",
1080
+ "costStatus",
1081
+ "chargedCredits",
1082
+ "costLabel",
1083
+ "finishedAt"
1084
+ ];
1085
+ var jsonColumns = /* @__PURE__ */ new Set([
1086
+ "remoteOutcome",
1087
+ "ref",
1088
+ "metadata",
1089
+ "generation",
1090
+ "refs",
1091
+ "parameters",
1092
+ "favoriteRefs",
1093
+ "outputRefs",
1094
+ "error",
1095
+ "cost",
1096
+ "tags",
1097
+ "coverRef",
1098
+ "previewRef"
1099
+ ]);
1100
+ function sqlRow(row) {
1101
+ return Object.fromEntries(
1102
+ Object.entries(row).map(([key, value]) => [
1103
+ key,
1104
+ typeof value === "boolean" ? Number(value) : value !== null && typeof value === "object" ? JSON.stringify(value) : value
1105
+ ])
1106
+ );
1107
+ }
1108
+ function resultRow(row) {
1109
+ return Object.fromEntries(
1110
+ Object.entries(row).map(([key, value]) => {
1111
+ if (jsonColumns.has(key) && typeof value === "string") {
1112
+ try {
1113
+ return [key, JSON.parse(value)];
1114
+ } catch {
1115
+ }
1116
+ }
1117
+ return [key, value];
1118
+ })
1119
+ );
1120
+ }
1121
+ function querySources(sql) {
1122
+ const tokens = (sql.match(
1123
+ /--[^\r\n]*|\/\*[\s\S]*?\*\/|'(?:''|[^'])*'|"(?:""|[^"])*"|`(?:``|[^`])*`|\[[^\]]*\]|[A-Za-z_][A-Za-z0-9_$]*|[(),.*]/g
1124
+ ) || []).filter((token) => !token.startsWith("--") && !token.startsWith("/*"));
1125
+ const name = (token = "") => {
1126
+ const quote = token[0];
1127
+ return (['"', "'", "`"].includes(quote) ? token.slice(1, -1).replaceAll(quote + quote, quote) : quote === "[" ? token.slice(1, -1) : token).toLowerCase();
1128
+ };
1129
+ const known = /* @__PURE__ */ new Set([
1130
+ "studio_projects",
1131
+ "studio_assets",
1132
+ "studio_asset_versions",
1133
+ "studio_tasks"
1134
+ ]);
1135
+ const sources = /* @__PURE__ */ new Set();
1136
+ const aliases = /* @__PURE__ */ new Map();
1137
+ let uncertain = false;
1138
+ const relation = (position) => {
1139
+ if (tokens[position] === "(") uncertain = true;
1140
+ while (tokens[position] === "(") position++;
1141
+ if (tokens[position + 1] === ".") position += 2;
1142
+ const table = name(tokens[position]);
1143
+ if (known.has(table)) sources.add(table);
1144
+ else uncertain = true;
1145
+ const bind = (qualifier) => {
1146
+ const previous = aliases.get(qualifier);
1147
+ aliases.set(qualifier, previous === void 0 || previous === table ? table : null);
1148
+ };
1149
+ bind(table);
1150
+ const alias = tokens[position + (tokens[position + 1]?.toLowerCase() === "as" ? 2 : 1)];
1151
+ if (alias && (/^[A-Za-z_]/.test(alias) || ['"', "'", "`", "["].includes(alias[0])))
1152
+ bind(name(alias));
1153
+ };
1154
+ const from = /* @__PURE__ */ new Set();
1155
+ const endsFrom = /* @__PURE__ */ new Set(["select", "where", "having", "limit", "union", "except", "intersect"]);
1156
+ let depth = 0;
1157
+ for (const [index, token] of tokens.entries()) {
1158
+ const keyword = token.toLowerCase();
1159
+ const previous = tokens[index - 1]?.toLowerCase();
1160
+ const next = tokens[index + 1]?.toLowerCase();
1161
+ if (token === "(") {
1162
+ if (from.has(depth) && ["from", "join", ",", "("].includes(previous)) from.add(depth + 1);
1163
+ depth++;
1164
+ } else if (token === ")") {
1165
+ from.delete(depth--);
1166
+ } else if (keyword === "from" || keyword === "join") {
1167
+ if (keyword === "from" && previous === "distinct") continue;
1168
+ from.add(depth);
1169
+ relation(index + 1);
1170
+ } else if (token === "," && from.has(depth)) {
1171
+ relation(index + 1);
1172
+ } else if (previous !== "as" && (endsFrom.has(keyword) || ["group", "order"].includes(keyword) && next === "by" || keyword === "window" && tokens[index + 2]?.toLowerCase() === "as" && tokens[index + 3] === "(")) {
1173
+ from.delete(depth);
1174
+ }
1175
+ }
1176
+ if (uncertain)
1177
+ for (const [index, token] of tokens.entries()) {
1178
+ const table = name(token);
1179
+ if (!token.startsWith("'") && tokens[index - 1]?.toLowerCase() !== "as" && known.has(table))
1180
+ sources.add(table);
1181
+ }
1182
+ const projectProjection = (column) => tokens.some((token, index) => {
1183
+ if ((!token.startsWith("'") || tokens[index - 1] === ".") && name(token) === column)
1184
+ return true;
1185
+ if (token !== "*" || tokens[index - 1] === "(") return false;
1186
+ if (tokens[index - 1] !== ".") return true;
1187
+ const table = aliases.get(name(tokens[index - 2]));
1188
+ return uncertain || !table || !known.has(table) || table === "studio_projects";
1189
+ });
1190
+ return {
1191
+ sources,
1192
+ projectCounts: projectProjection("assetcount"),
1193
+ projectPreviews: projectProjection("previewref")
1194
+ };
1195
+ }
1196
+ function listAll(data, typeId, check) {
1197
+ const records = [];
1198
+ for (let offset = 0; ; ) {
1199
+ check?.();
1200
+ const page = data.list({ typeId, sort: "createdAt", direction: "asc", limit: 200, offset });
1201
+ check?.();
1202
+ records.push(...page.items);
1203
+ if (page.nextOffset === null) return records;
1204
+ offset = page.nextOffset;
1205
+ }
1206
+ }
1207
+ function projectsAll(workspace, check) {
1208
+ const records = [];
1209
+ for (let offset = 0; ; ) {
1210
+ check?.();
1211
+ const page = workspace.projectRecords({
1212
+ sort: "createdAt",
1213
+ direction: "asc",
1214
+ limit: 200,
1215
+ offset
1216
+ });
1217
+ check?.();
1218
+ records.push(...page);
1219
+ if (page.length < 200) return records;
1220
+ offset += page.length;
1221
+ }
1222
+ }
1223
+ var GlobalStudio = class {
1224
+ constructor(data) {
1225
+ this.data = data;
1226
+ }
1227
+ data;
1228
+ projectNames() {
1229
+ return projectsAll(this.data).flatMap(
1230
+ ({ objectId, data }) => typeof data.name === "string" && data.name ? [{ id: objectId, name: data.name }] : []
1231
+ );
1232
+ }
1233
+ projectSummaries() {
1234
+ return projectsAll(this.data).flatMap(
1235
+ ({ objectId, data, revision: revision2 }) => typeof data.name === "string" && data.name ? [
1236
+ {
1237
+ id: objectId,
1238
+ name: data.name,
1239
+ coverRef: data.coverRef ?? null,
1240
+ tags: data.tags ?? [],
1241
+ revision: revision2
1242
+ }
1243
+ ] : []
1244
+ );
1245
+ }
1246
+ renameProject(input, call) {
1247
+ const data = this.data.project(input.projectId);
1248
+ return new Projects(data).update(
1249
+ {
1250
+ projectId: input.projectId,
1251
+ expectedRevision: input.expectedRevision,
1252
+ changes: { name: input.name }
1253
+ },
1254
+ { ...call, scope: data.scope }
1255
+ );
1256
+ }
1257
+ setProjectCover({
1258
+ projectId,
1259
+ expectedRevision,
1260
+ coverRef
1261
+ }, call) {
1262
+ const data = this.data.project(projectId);
1263
+ return new Projects(data).update(
1264
+ { projectId, expectedRevision, changes: { coverRef } },
1265
+ { ...call, scope: data.scope }
1266
+ );
1267
+ }
1268
+ source(ref) {
1269
+ const data = this.data.source(ref);
1270
+ const object = data.get(ref.objectId, true);
1271
+ if (object.typeId !== "studio.asset" || object.scope.kind !== "project")
1272
+ reject("TYPE_MISMATCH", "Expected a saved project asset version.");
1273
+ const version = data.version(ref);
1274
+ return { data, object, version };
1275
+ }
1276
+ async describe(data, object, version, latestVersionId, favorites, check) {
1277
+ const ref = { storeId: data.storeId, objectId: object.objectId, versionId: version.versionId };
1278
+ check?.();
1279
+ const files = data.files(ref);
1280
+ if (!files.length) return null;
1281
+ let location = null;
1282
+ try {
1283
+ for (const file of files) {
1284
+ check?.();
1285
+ const checked = await data.location(ref, file.fileId);
1286
+ check?.();
1287
+ if (file.role === "primary" && file.ordinal === 0) location = checked;
1288
+ }
1289
+ } catch (error) {
1290
+ if (["FILE_MISSING", "FILE_CORRUPT", "ENOENT"].includes(error.code)) return null;
1291
+ throw error;
1292
+ }
1293
+ if (!location) return null;
1294
+ const generation = version.payload.generation || null;
1295
+ return {
1296
+ rowId: refKey(ref),
1297
+ projectId: object.scope.id,
1298
+ assetId: object.objectId,
1299
+ ref: { ...ref },
1300
+ name: object.data.name,
1301
+ kind: object.data.kind,
1302
+ purpose: object.data.purpose,
1303
+ metadata: structuredClone(object.metadata),
1304
+ generation: structuredClone(generation),
1305
+ prompt: generation?.prompt ?? null,
1306
+ originalPrompt: generation?.originalPrompt ?? null,
1307
+ refs: generation?.refs ?? null,
1308
+ modelId: generation?.modelId ?? null,
1309
+ parameters: generation?.parameters ?? null,
1310
+ snapshotAvailable: generation !== null && ["prompt", "originalPrompt", "refs", "modelId", "parameters"].every(
1311
+ (key) => Object.hasOwn(generation, key)
1312
+ ),
1313
+ taskId: generation?.taskId ?? version.taskId,
1314
+ outputIndex: generation?.outputIndex ?? null,
1315
+ previewUrl: location.previewUrl,
1316
+ mimeType: location.mimeType,
1317
+ createdAt: version.createdAt,
1318
+ updatedAt: object.updatedAt,
1319
+ adoptedVersionId: object.data.adoptedVersionId,
1320
+ versionId: version.versionId,
1321
+ versionNumber: version.versionNumber,
1322
+ latestVersionId,
1323
+ favorite: favorites.some((favorite) => refKey(favorite) === refKey(ref)),
1324
+ favoriteRefs: structuredClone(favorites)
1325
+ };
1326
+ }
1327
+ favoriteRefs(data, objectId) {
1328
+ return listAll(data, "studio.favorite").map((record) => record.data.ref).filter((ref) => ref.objectId === objectId);
1329
+ }
1330
+ async asset(value) {
1331
+ const ref = versionRef.parse(value);
1332
+ const { data, object, version } = this.source(ref);
1333
+ const versions = data.versions(object.objectId);
1334
+ const item = await this.describe(
1335
+ data,
1336
+ object,
1337
+ version,
1338
+ versions.at(-1)?.versionId || null,
1339
+ this.favoriteRefs(data, object.objectId)
1340
+ );
1341
+ if (!item) reject("CONTENT_UNAVAILABLE", "The selected saved version is unavailable.");
1342
+ return { item, versions, files: data.files(ref) };
1343
+ }
1344
+ async favorite(value, call) {
1345
+ const input = { ref: versionRef.parse(value.ref), enabled: value.enabled };
1346
+ const receiptCall = { ...call, scope: this.data.scope };
1347
+ const replay = this.data.receipt("global.favorite", input, receiptCall);
1348
+ if (replay) return replay.result;
1349
+ const { data, object } = this.source(input.ref);
1350
+ const boundCall = { ...call, scope: data.scope };
1351
+ const previousReceipt = data.receipt("global.favorite", input, boundCall);
1352
+ if (previousReceipt)
1353
+ return this.data.operation(
1354
+ "global.favorite",
1355
+ input,
1356
+ receiptCall,
1357
+ () => previousReceipt.result
1358
+ );
1359
+ if (input.enabled && (object.deletedAt || await data.availability(input.ref) !== "ready"))
1360
+ reject("CONTENT_UNAVAILABLE", "Favorite an available saved asset version.");
1361
+ const id2 = stableId(
1362
+ data.storeId,
1363
+ {
1364
+ scope: data.scope,
1365
+ actor: call.actor,
1366
+ requestKey: refKey(input.ref)
1367
+ },
1368
+ "studio.favorite"
1369
+ );
1370
+ return this.data.operation(
1371
+ "global.favorite",
1372
+ input,
1373
+ receiptCall,
1374
+ () => data.operation("global.favorite", input, boundCall, (tx) => {
1375
+ let previous = null;
1376
+ try {
1377
+ previous = tx.get(id2, true);
1378
+ } catch (error) {
1379
+ if (error.code !== "OBJECT_NOT_FOUND") throw error;
1380
+ }
1381
+ let favorite = previous;
1382
+ if (input.enabled) {
1383
+ if (!previous) {
1384
+ favorite = tx.create("studio.favorite", favoriteData.parse({ ref: input.ref }), {}, id2);
1385
+ tx.relation(id2, null, input.ref, "reference");
1386
+ } else if (previous.deletedAt) {
1387
+ favorite = tx.update(id2, previous.revision, previous.data, void 0, null);
1388
+ }
1389
+ } else if (previous && !previous.deletedAt) {
1390
+ favorite = tx.update(
1391
+ id2,
1392
+ previous.revision,
1393
+ previous.data,
1394
+ void 0,
1395
+ (/* @__PURE__ */ new Date()).toISOString()
1396
+ );
1397
+ }
1398
+ return { favorite, ref: input.ref, enabled: input.enabled };
1399
+ })
1400
+ );
1401
+ }
1402
+ async importAsset(input, call) {
1403
+ const receiptCall = { ...call, scope: this.data.scope };
1404
+ const replay = this.data.receipt("global.import", input, receiptCall);
1405
+ if (replay) return replay.result;
1406
+ const data = this.data.project(input.projectId);
1407
+ const result2 = await new Assets(data, this.data).import(
1408
+ { name: input.name, stagingFileIds: input.stagingFileIds },
1409
+ { ...call, scope: data.scope }
1410
+ );
1411
+ return this.data.operation("global.import", input, receiptCall, () => result2);
1412
+ }
1413
+ async exportEntries(values) {
1414
+ const refs = new Map(
1415
+ values.map((value) => {
1416
+ const ref = versionRef.parse(value);
1417
+ return [refKey(ref), ref];
1418
+ })
1419
+ );
1420
+ const entries = [];
1421
+ for (const ref of refs.values()) {
1422
+ const { data } = this.source(ref);
1423
+ const files = data.files(ref);
1424
+ if (!files.length) reject("CONTENT_UNAVAILABLE", "The selected version has no saved files.");
1425
+ for (const file of files) {
1426
+ const fixedRef2 = Object.freeze({ ...ref });
1427
+ entries.push({
1428
+ ref: fixedRef2,
1429
+ fileId: file.fileId,
1430
+ name: file.originalName,
1431
+ sizeBytes: file.sizeBytes,
1432
+ open: (signal) => data.openFile(fixedRef2, file.fileId, signal)
1433
+ });
1434
+ }
1435
+ }
1436
+ return entries;
1437
+ }
1438
+ async tables(catalog, selection, check) {
1439
+ const { sources } = selection;
1440
+ const needProjects = sources.has("studio_projects");
1441
+ const needAssets = sources.has("studio_assets");
1442
+ const needVersions = sources.has("studio_asset_versions");
1443
+ const needTasks = sources.has("studio_tasks");
1444
+ const needCounts = needProjects && selection.projectCounts;
1445
+ const needPreviews = needProjects && selection.projectPreviews;
1446
+ const projects = sources.size ? projectsAll(this.data, check) : [];
1447
+ const host = new Map(catalog.map((entry) => [entry.id, entry]));
1448
+ const assets = [], versions = [], tasks = [];
1449
+ const projectRows = /* @__PURE__ */ new Map();
1450
+ for (const project of projects) {
1451
+ check();
1452
+ const data = this.data.project(project.objectId);
1453
+ const favorites = needAssets || needVersions ? listAll(data, "studio.favorite", check).map((record) => record.data.ref) : [];
1454
+ const projectAssets = needAssets || needVersions || needCounts || needPreviews || needTasks ? listAll(data, "studio.asset", check) : [];
1455
+ let assetCount = 0;
1456
+ let firstMediaRef = null;
1457
+ const described = /* @__PURE__ */ new Map();
1458
+ if (needAssets || needVersions || needCounts || needPreviews)
1459
+ for (const object of projectAssets) {
1460
+ check();
1461
+ const saved2 = data.versions(object.objectId);
1462
+ const latest = saved2.at(-1);
1463
+ const pinned = favorites.filter((ref) => ref.objectId === object.objectId);
1464
+ for (const version of needVersions ? saved2 : latest ? [latest] : []) {
1465
+ check();
1466
+ const row = await this.describe(
1467
+ data,
1468
+ object,
1469
+ version,
1470
+ latest?.versionId || null,
1471
+ pinned,
1472
+ check
1473
+ );
1474
+ described.set(version.versionId, row);
1475
+ check();
1476
+ if (row) {
1477
+ if (!firstMediaRef && (row.kind === "image" || row.kind === "video"))
1478
+ firstMediaRef = row.ref;
1479
+ if (needVersions) versions.push(row);
1480
+ if (version.versionId === latest?.versionId) {
1481
+ assetCount++;
1482
+ if (needAssets) assets.push(row);
1483
+ }
1484
+ }
1485
+ }
1486
+ }
1487
+ if (needTasks)
1488
+ for (const task of listAll(data, "studio.task", check)) {
1489
+ check();
1490
+ const input = task.data.inputSnapshot;
1491
+ tasks.push({
1492
+ rowId: task.objectId,
1493
+ projectId: project.objectId,
1494
+ taskId: task.objectId,
1495
+ revision: task.revision,
1496
+ attemptId: task.objectId,
1497
+ remoteOutcome: taskProjection(task).projection.remoteOutcome,
1498
+ localPersistence: taskProjection(task).projection.localPersistence,
1499
+ name: task.data.name || projectAssets.find((asset) => asset.objectId === task.data.targetAssetId)?.data.name || "Generation",
1500
+ state: task.data.state,
1501
+ outputRefs: task.data.outputRefs,
1502
+ createdAt: task.createdAt,
1503
+ updatedAt: task.updatedAt,
1504
+ targetAssetId: task.data.targetAssetId,
1505
+ kind: task.data.kind,
1506
+ prompt: input?.prompt ?? null,
1507
+ originalPrompt: input?.originalPrompt ?? null,
1508
+ refs: input?.inputs ?? null,
1509
+ modelId: input?.modelId ?? null,
1510
+ parameters: input?.parameters ?? null,
1511
+ progress: task.data.progress,
1512
+ error: task.data.error,
1513
+ expectedOutputs: task.data.expectedOutputs,
1514
+ receivedOutputs: task.data.receivedOutputs,
1515
+ cost: task.data.cost ?? null,
1516
+ costStatus: task.data.cost?.status ?? "unknown",
1517
+ chargedCredits: task.data.cost?.credits ?? null,
1518
+ costLabel: taskCostLabel(task.data),
1519
+ finishedAt: task.data.finishedAt
1520
+ });
1521
+ }
1522
+ if (needProjects) {
1523
+ let previewRef = needPreviews ? project.data.coverRef : null;
1524
+ if (needPreviews && !previewRef) {
1525
+ const clips = new Ads(data).get()?.data.clips ?? [];
1526
+ for (const clip of clips) {
1527
+ check();
1528
+ const asset = projectAssets.find((item) => item.objectId === clip.assetId);
1529
+ const selected = asset?.data.adoptedVersionId;
1530
+ if (!asset || asset.data.kind !== "video" || !selected) continue;
1531
+ const saved2 = data.versions(asset.objectId);
1532
+ const version = saved2.find((item) => item.versionId === selected);
1533
+ if (!version) continue;
1534
+ const row = described.has(version.versionId) ? described.get(version.versionId) : await this.describe(
1535
+ data,
1536
+ asset,
1537
+ version,
1538
+ saved2.at(-1)?.versionId ?? null,
1539
+ [],
1540
+ check
1541
+ );
1542
+ if (row) {
1543
+ previewRef = row.ref;
1544
+ break;
1545
+ }
1546
+ }
1547
+ previewRef ??= firstMediaRef;
1548
+ }
1549
+ const registered = host.get(project.objectId);
1550
+ projectRows.set(project.objectId, {
1551
+ rowId: project.objectId,
1552
+ projectId: project.objectId,
1553
+ name: project.data.name,
1554
+ hostName: registered?.name ?? null,
1555
+ available: registered?.available ?? false,
1556
+ assetCount,
1557
+ metadata: project.metadata,
1558
+ createdAt: project.createdAt,
1559
+ updatedAt: project.updatedAt,
1560
+ status: project.data.status,
1561
+ description: project.data.description,
1562
+ tags: project.data.tags,
1563
+ aspectRatio: project.data.aspectRatio,
1564
+ coverRef: project.data.coverRef,
1565
+ previewRef: previewRef ? { ...previewRef } : null
1566
+ });
1567
+ }
1568
+ }
1569
+ if (needProjects)
1570
+ for (const entry of host.values()) {
1571
+ check();
1572
+ if (projectRows.has(entry.id)) continue;
1573
+ projectRows.set(entry.id, {
1574
+ rowId: entry.id,
1575
+ projectId: entry.id,
1576
+ name: entry.name,
1577
+ hostName: entry.name,
1578
+ available: entry.available,
1579
+ assetCount: 0,
1580
+ metadata: {},
1581
+ createdAt: null,
1582
+ updatedAt: null,
1583
+ status: null,
1584
+ description: "",
1585
+ tags: [],
1586
+ aspectRatio: null,
1587
+ coverRef: null,
1588
+ previewRef: null
1589
+ });
1590
+ }
1591
+ check();
1592
+ return {
1593
+ studio_projects: { columns: projectColumns, rows: [...projectRows.values()].map(sqlRow) },
1594
+ studio_assets: { columns: assetColumns, rows: assets.map(sqlRow) },
1595
+ studio_asset_versions: { columns: assetColumns, rows: versions.map(sqlRow) },
1596
+ studio_tasks: { columns: taskColumns, rows: tasks.map(sqlRow) }
1597
+ };
1598
+ }
1599
+ async query(value) {
1600
+ const started = performance.now();
1601
+ const input = globalQueryInput.parse(value);
1602
+ const maxRows = input.maxRows ?? 1e3;
1603
+ const timeoutMs = input.timeoutMs ?? 2e3;
1604
+ const limit = input.limit ?? Math.min(50, maxRows);
1605
+ const offset = input.offset ?? 0;
1606
+ if (maxRows > 1e4 || timeoutMs > 1e4 || limit > maxRows)
1607
+ reject("INVALID_SQL_INPUT", "Invalid SQL execution budget or pagination.");
1608
+ const deadline = started + timeoutMs;
1609
+ const check = () => {
1610
+ if (performance.now() >= deadline)
1611
+ reject("SQL_TIMEOUT", "The global query exceeded its deadline.");
1612
+ };
1613
+ check();
1614
+ const selection = querySources(input.sql);
1615
+ check();
1616
+ const tables = await this.tables(input.catalog || [], selection, check);
1617
+ check();
1618
+ const remaining = Math.floor(deadline - performance.now());
1619
+ if (remaining < 1) reject("SQL_TIMEOUT", "The global query exceeded its deadline.");
1620
+ const items = await this.data.queryProjection(
1621
+ {
1622
+ sql: input.sql,
1623
+ params: input.params,
1624
+ timeoutMs: remaining,
1625
+ maxRows: Math.max(maxRows, limit + 1),
1626
+ limit: limit + 1,
1627
+ offset
1628
+ },
1629
+ tables
1630
+ );
1631
+ check();
1632
+ const result2 = {
1633
+ items: items.slice(0, limit).map(resultRow),
1634
+ nextOffset: items.length > limit ? offset + limit : null
1635
+ };
1636
+ check();
1637
+ return result2;
1638
+ }
1639
+ };
1640
+
1641
+ // packages/project-studio/src/media-uses.ts
1642
+ import { createHash as createHash3 } from "crypto";
1643
+ import { z as z6, PluginError as PluginError3 } from "vivix-studio/sdk/shared";
1644
+
1645
+ // packages/media-tools/src/shared.ts
1646
+ import { z as z5, PluginError as PluginError2 } from "vivix-studio/sdk/shared";
1647
+ var Config = z5.object({
1648
+ dataInstance: z5.string().min(1).default("user:data"),
1649
+ studioInstance: z5.string().min(1).default("studio"),
1650
+ executionNamespace: z5.string().regex(/^[a-zA-Z0-9_-]{1,64}$/).default("media"),
1651
+ ffmpegPath: z5.string().min(1).default("ffmpeg"),
1652
+ ffprobePath: z5.string().min(1).default("ffprobe")
1653
+ }).strict();
1654
+ var versionRef2 = z5.object({ storeId: z5.uuid(), objectId: z5.uuid(), versionId: z5.uuid() }).strict();
1655
+ var videoFileRef = z5.object({ source: versionRef2, fileId: z5.uuid() }).strict();
1656
+ var sourceInput = z5.object({ kind: z5.literal("source"), file: videoFileRef }).strict();
1657
+ var editInput = z5.object({ kind: z5.literal("edit"), editRef: versionRef2 }).strict();
1658
+ var videoInput = z5.discriminatedUnion("kind", [sourceInput, editInput]);
1659
+ var sequenceVideoInput = z5.discriminatedUnion("kind", [
1660
+ sourceInput.extend({ trimEndMs: z5.number().int().positive().optional() }),
1661
+ editInput
1662
+ ]);
1663
+ var recipe = z5.object({
1664
+ trim: z5.object({ inMs: z5.number().int().nonnegative(), outMs: z5.number().int().positive() }).strict(),
1665
+ crop: z5.object({
1666
+ x: z5.number().finite().nonnegative(),
1667
+ y: z5.number().finite().nonnegative(),
1668
+ width: z5.number().finite().positive().max(1),
1669
+ height: z5.number().finite().positive().max(1)
1670
+ }).strict()
1671
+ }).strict().superRefine((v, c) => {
1672
+ if (v.trim.inMs >= v.trim.outMs)
1673
+ c.addIssue({ code: "custom", message: "The selected interval must have positive duration." });
1674
+ if (v.crop.x + v.crop.width > 1 + 1e-9 || v.crop.y + v.crop.height > 1 + 1e-9)
1675
+ c.addIssue({ code: "custom", message: "The crop must remain inside the source picture." });
1676
+ });
1677
+ var pixelRect = z5.object({
1678
+ x: z5.number().int().nonnegative(),
1679
+ y: z5.number().int().nonnegative(),
1680
+ width: z5.number().int().positive(),
1681
+ height: z5.number().int().positive()
1682
+ }).strict();
1683
+ var geometry = z5.object({
1684
+ displayWidth: z5.number().int().positive(),
1685
+ displayHeight: z5.number().int().positive(),
1686
+ pixelRect
1687
+ }).strict();
1688
+ var metadata = z5.object({
1689
+ durationMs: z5.number().int().positive(),
1690
+ width: z5.number().int().positive(),
1691
+ height: z5.number().int().positive(),
1692
+ displayWidth: z5.number().int().positive(),
1693
+ displayHeight: z5.number().int().positive(),
1694
+ rotation: z5.union([z5.literal(0), z5.literal(90), z5.literal(180), z5.literal(270)]),
1695
+ frameRate: z5.number().positive().max(60),
1696
+ startTime: z5.number().finite(),
1697
+ hasAudio: z5.boolean(),
1698
+ audioSampleRate: z5.number().int().nullable(),
1699
+ audioChannels: z5.number().int().nullable(),
1700
+ audioStartTime: z5.number().finite().nullable(),
1701
+ sizeBytes: z5.number().int().nonnegative(),
1702
+ sha256: z5.string().regex(/^[a-f0-9]{64}$/),
1703
+ colorInterpretation: z5.enum(["declared-bt709", "assumed-bt709"]).optional()
1704
+ }).strict();
1705
+ var editDescriptor = z5.object({ format: z5.literal(1), file: videoFileRef, recipe, geometry, metadata }).strict();
1706
+ var saveEdit = z5.object({
1707
+ file: videoFileRef,
1708
+ recipe,
1709
+ editId: z5.uuid().optional(),
1710
+ expectedRevision: z5.number().int().positive().optional()
1711
+ }).strict().superRefine((v, c) => {
1712
+ if (Boolean(v.editId) !== Boolean(v.expectedRevision))
1713
+ c.addIssue({ code: "custom", message: "An edit update requires its revision." });
1714
+ });
1715
+ var outputName = z5.string().trim().min(1).max(200);
1716
+ var processVideo = z5.object({ input: videoInput, outputName, retryOfJobId: z5.uuid().optional() }).strict();
1717
+ var canvas = z5.object({ width: z5.number().int().min(2).max(3840), height: z5.number().int().min(2).max(3840) }).strict().refine(
1718
+ (v) => v.width % 2 === 0 && v.height % 2 === 0 && Math.min(v.width, v.height) <= 2160,
1719
+ "Canvas dimensions must be even and within 3840 by 2160."
1720
+ );
1721
+ var processSequence = z5.object({
1722
+ inputs: z5.array(sequenceVideoInput).min(1).max(64),
1723
+ outputName,
1724
+ canvas,
1725
+ retryOfJobId: z5.uuid().optional()
1726
+ }).strict();
1727
+ var mediaRequest = z5.discriminatedUnion("operation", [
1728
+ z5.object({ operation: z5.literal("video.process"), input: processVideo }).strict(),
1729
+ z5.object({ operation: z5.literal("sequence.process"), input: processSequence }).strict()
1730
+ ]);
1731
+ var jobsQuery = z5.object({
1732
+ limit: z5.number().int().min(1).max(100).default(50),
1733
+ offset: z5.number().int().nonnegative().default(0),
1734
+ request: z5.object({
1735
+ operation: z5.enum(["video.process", "sequence.process"]),
1736
+ requestKey: z5.string().min(1).max(128),
1737
+ intent: z5.union([processVideo, processSequence])
1738
+ }).strict().optional()
1739
+ }).strict();
1740
+ function mediaError(code, message) {
1741
+ throw new PluginError2(
1742
+ code,
1743
+ message,
1744
+ "Review the fixed source, edit and current project, then retry explicitly."
1745
+ );
1746
+ }
1747
+ function normalizeRecipe(raw, displayWidth, displayHeight, durationMs) {
1748
+ const value = recipe.parse(raw);
1749
+ if (![displayWidth, displayHeight, durationMs].every(Number.isSafeInteger) || displayWidth < 2 || displayHeight < 2 || durationMs < 1 || displayWidth % 2 !== 0 || displayHeight % 2 !== 0)
1750
+ mediaError("UNSUPPORTED_GEOMETRY", "The source needs positive even display dimensions.");
1751
+ if (value.trim.outMs > durationMs)
1752
+ mediaError("INVALID_RECIPE", "The trim exceeds the fixed source duration.");
1753
+ const edge = (fraction, size) => Math.round(fraction * size / 2) * 2;
1754
+ const x = edge(value.crop.x, displayWidth);
1755
+ const y = edge(value.crop.y, displayHeight);
1756
+ const right = Math.min(displayWidth, edge(value.crop.x + value.crop.width, displayWidth));
1757
+ const bottom = Math.min(displayHeight, edge(value.crop.y + value.crop.height, displayHeight));
1758
+ if (right - x < 2 || bottom - y < 2)
1759
+ mediaError("INVALID_RECIPE", "The crop must contain at least two pixels in each dimension.");
1760
+ const pixelRect2 = { x, y, width: right - x, height: bottom - y };
1761
+ return {
1762
+ recipe: {
1763
+ trim: { ...value.trim },
1764
+ crop: {
1765
+ x: x / displayWidth,
1766
+ y: y / displayHeight,
1767
+ width: pixelRect2.width / displayWidth,
1768
+ height: pixelRect2.height / displayHeight
1769
+ }
1770
+ },
1771
+ geometry: { displayWidth, displayHeight, pixelRect: pixelRect2 }
1772
+ };
1773
+ }
1774
+ function sameFile(a, b) {
1775
+ return a.fileId === b.fileId && a.source.storeId === b.source.storeId && a.source.objectId === b.source.objectId && a.source.versionId === b.source.versionId;
1776
+ }
1777
+ function readSavedEdit(data, raw) {
1778
+ const ref = versionRef2.parse(raw);
1779
+ if (data.scope.kind !== "project" || ref.storeId !== data.storeId)
1780
+ mediaError("SCOPE_MISMATCH", "The edit is not in this project store.");
1781
+ const object = data.get(ref.objectId, true);
1782
+ if (object.typeId !== "media.edit" || object.schemaVersion !== 1 || object.scope.kind !== data.scope.kind || object.scope.id !== data.scope.id)
1783
+ mediaError("EDIT_FORMAT_UNSUPPORTED", "A supported fixed media edit is required.");
1784
+ const version = data.version(ref);
1785
+ if (version.schemaVersion !== 1 || version.payload?.kind !== "media.edit")
1786
+ mediaError("EDIT_FORMAT_UNSUPPORTED", "This edit descriptor format is unsupported.");
1787
+ const parsed = editDescriptor.safeParse(version.payload.descriptor);
1788
+ if (!parsed.success)
1789
+ mediaError("EDIT_FORMAT_UNSUPPORTED", "The fixed edit descriptor is invalid.");
1790
+ const value = parsed.data;
1791
+ if (!sameFile(object.data.file, value.file))
1792
+ mediaError("IMMUTABLE_FIELD", "The fixed edit version changed its original source.");
1793
+ const canonical2 = normalizeRecipe(
1794
+ value.recipe,
1795
+ value.metadata.displayWidth,
1796
+ value.metadata.displayHeight,
1797
+ value.metadata.durationMs
1798
+ );
1799
+ if (JSON.stringify(canonical2.recipe) !== JSON.stringify(value.recipe) || JSON.stringify(canonical2.geometry) !== JSON.stringify(value.geometry))
1800
+ mediaError("EDIT_FORMAT_UNSUPPORTED", "The stored edit geometry is not canonical.");
1801
+ if (value.file.source.storeId !== data.storeId)
1802
+ mediaError("SCOPE_MISMATCH", "The edit source belongs to another store.");
1803
+ const original = data.get(value.file.source.objectId, true);
1804
+ if (original.typeId !== "studio.asset" || original.data.kind !== "video")
1805
+ mediaError("SOURCE_NOT_VIDEO", "The edit source is not a project video.");
1806
+ const sourceVersion = data.version(value.file.source), files = data.files(value.file.source);
1807
+ if (sourceVersion.payload.kind !== "video" || !files.some(
1808
+ (f) => f.fileId === value.file.fileId && f.mimeType === "video/mp4" && f.sha256 === value.metadata.sha256
1809
+ ))
1810
+ mediaError(
1811
+ "INVALID_REFERENCE",
1812
+ "The saved video file is not part of its fixed source version."
1813
+ );
1814
+ const ownFiles = data.files(ref);
1815
+ if (!ownFiles.some(
1816
+ (f) => f.fileId === version.payload.primaryFileId && f.role === "primary" && f.mimeType.startsWith("text/plain")
1817
+ ))
1818
+ mediaError("EDIT_FORMAT_UNSUPPORTED", "The edit is missing its authored descriptor file.");
1819
+ if (value.recipe.trim.outMs > value.metadata.durationMs)
1820
+ mediaError("INVALID_RECIPE", "The saved trim exceeds the fixed source.");
1821
+ return {
1822
+ editRef: ref,
1823
+ revision: object.revision,
1824
+ file: value.file,
1825
+ recipe: value.recipe,
1826
+ geometry: value.geometry,
1827
+ effectiveDurationMs: value.recipe.trim.outMs - value.recipe.trim.inMs
1828
+ };
1829
+ }
1830
+
1831
+ // packages/project-studio/src/media-uses.ts
1832
+ var getMediaUse = z6.object({ usageId: z6.string().min(1).max(200), file: videoFileRef }).strict();
1833
+ var bindMediaUse = getMediaUse.extend({
1834
+ editRef: versionRef2,
1835
+ expectedRevision: z6.number().int().nonnegative()
1836
+ }).strict();
1837
+ var useData = getMediaUse.extend({ editRef: versionRef2 }).strict();
1838
+ var definition = {
1839
+ typeId: "studio.video-use",
1840
+ schemaVersion: 1,
1841
+ scopeKinds: ["project"],
1842
+ validate: (value) => useData.parse(value),
1843
+ validateUpdate: (old, next) => {
1844
+ if (old.usageId !== next.usageId || !sameFile(old.file, next.file))
1845
+ reject2("CANDIDATE_CHANGED", "This usage is fixed to another source video.");
1846
+ }
1847
+ };
1848
+ function reject2(code, message) {
1849
+ throw new PluginError3(code, message, "Reload the current usage and preserve the editing draft.");
1850
+ }
1851
+ function idFor(data, usageId) {
1852
+ const hex = createHash3("sha256").update(JSON.stringify(["studio.video-use/1", data.storeId, data.scope, usageId])).digest("hex");
1853
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-4${hex.slice(13, 16)}-8${hex.slice(17, 20)}-${hex.slice(20, 32)}`;
1854
+ }
1855
+ var MediaUses = class {
1856
+ constructor(data, register, assertWritable) {
1857
+ this.data = data;
1858
+ this.assertWritable = assertWritable;
1859
+ register(definition);
1860
+ }
1861
+ data;
1862
+ assertWritable;
1863
+ source(file, includeDeleted = false) {
1864
+ if (this.data.scope.kind !== "project" || file.source.storeId !== this.data.storeId)
1865
+ reject2("SCOPE_MISMATCH", "This video does not belong to the current project.");
1866
+ const record = this.data.get(file.source.objectId, includeDeleted);
1867
+ if (record.typeId !== "studio.asset" || record.data.kind !== "video" || !this.data.files(file.source).some((value) => value.fileId === file.fileId))
1868
+ reject2("INVALID_REFERENCE", "Choose a readable fixed project video.");
1869
+ }
1870
+ current(usageId, file) {
1871
+ try {
1872
+ const record = this.data.get(idFor(this.data, usageId));
1873
+ if (record.typeId !== "studio.video-use")
1874
+ reject2("TYPE_MISMATCH", "This usage identity is unavailable.");
1875
+ const value = useData.parse(record.data);
1876
+ if (!sameFile(value.file, file))
1877
+ reject2("CANDIDATE_CHANGED", "The selected candidate changed.");
1878
+ return { ...value, revision: record.revision };
1879
+ } catch (error) {
1880
+ if (!(error && typeof error === "object" && "code" in error && error.code === "OBJECT_NOT_FOUND"))
1881
+ throw error;
1882
+ return { usageId, revision: 0, file, editRef: null };
1883
+ }
1884
+ }
1885
+ get(raw) {
1886
+ const input = getMediaUse.parse(raw), current = this.current(input.usageId, input.file);
1887
+ this.source(input.file, current.revision > 0);
1888
+ return current;
1889
+ }
1890
+ async bind(raw, call) {
1891
+ const input = bindMediaUse.parse(raw);
1892
+ const replay = this.data.receipt("mediaUses.bind", input, call);
1893
+ if (replay) return replay.result;
1894
+ this.assertWritable();
1895
+ this.source(input.file);
1896
+ this.data.get(input.editRef.objectId);
1897
+ const saved2 = readSavedEdit(this.data, input.editRef);
1898
+ if (!sameFile(saved2.file, input.file))
1899
+ reject2("CANDIDATE_CHANGED", "This edit belongs to another candidate.");
1900
+ await this.data.location(input.editRef);
1901
+ await this.data.location(saved2.file.source, saved2.file.fileId);
1902
+ return this.data.operation("mediaUses.bind", input, call, (transaction) => {
1903
+ this.assertWritable();
1904
+ this.source(input.file);
1905
+ transaction.get(input.editRef.objectId);
1906
+ const fixed = readSavedEdit(this.data, input.editRef);
1907
+ if (!sameFile(fixed.file, input.file))
1908
+ reject2("CANDIDATE_CHANGED", "This edit belongs to another candidate.");
1909
+ const current = this.current(input.usageId, input.file);
1910
+ if (current.revision !== input.expectedRevision)
1911
+ throw new PluginError3(
1912
+ "REVISION_CONFLICT",
1913
+ "This usage changed while you were editing.",
1914
+ "Reload the current usage and keep the editing draft.",
1915
+ { currentRevision: current.revision }
1916
+ );
1917
+ const value = { usageId: input.usageId, file: input.file, editRef: input.editRef };
1918
+ const objectId = idFor(this.data, input.usageId);
1919
+ const record = current.revision === 0 ? transaction.create("studio.video-use", value, {}, objectId) : transaction.update(objectId, current.revision, value);
1920
+ transaction.relation(objectId, null, input.file.source, "reference");
1921
+ transaction.relation(objectId, null, input.editRef, "reference");
1922
+ return { ...value, revision: record.revision };
1923
+ });
1924
+ }
1925
+ };
1926
+
1927
+ // packages/project-studio/src/derived-videos.ts
1928
+ var DerivedVideos = class {
1929
+ constructor(assets, provider) {
1930
+ this.assets = assets;
1931
+ this.provider = provider;
1932
+ this.storeId = assets.data.storeId;
1933
+ this.projectId = assets.data.scope.id;
1934
+ }
1935
+ assets;
1936
+ provider;
1937
+ capabilityVersion = "derived-video/1";
1938
+ storeId;
1939
+ projectId;
1940
+ assertSameWorkspace(provider, projectId) {
1941
+ if (provider !== this.provider || projectId !== this.projectId)
1942
+ reject(
1943
+ "WORKSPACE_MISMATCH",
1944
+ "Media and assets must share the same storage provider and project."
1945
+ );
1946
+ }
1947
+ async importDerivedVideo(input, call, complete) {
1948
+ const data = this.assets.data;
1949
+ if (call.requestKey !== input.publicationKey || !input.inputs.length)
1950
+ reject("INVALID_PUBLICATION", "A fixed publication identity and sources are required.");
1951
+ const name = "assets.importDerivedVideo";
1952
+ const replay = data.receipt(name, input, call);
1953
+ if (replay) return replay.result;
1954
+ if (complete.constructor.name === "AsyncFunction")
1955
+ reject("ASYNC_TRANSACTION", "Media completion must be synchronous.");
1956
+ this.assets.assertWritable();
1957
+ const operationId = stableId(data.storeId, call, name), objectId = stableId(operationId, call, "object"), versionId = stableId(operationId, call, "version");
1958
+ let result2;
1959
+ try {
1960
+ const published = await data.publish(
1961
+ [input.stagingFileId],
1962
+ objectId,
1963
+ versionId,
1964
+ operationId,
1965
+ call.actor.id
1966
+ );
1967
+ if (published.kind !== "video")
1968
+ reject("TYPE_MISMATCH", "A derived output must be a validated video.");
1969
+ result2 = data.operation(name, input, call, (t) => {
1970
+ this.assets.assertWritable();
1971
+ const guard = () => {
1972
+ const job2 = t.get(input.completion.jobId);
1973
+ if (job2.typeId !== "media.job" || job2.data.state !== "saving" || job2.revision !== input.completion.expectedRevision || job2.data.executionToken !== input.completion.executionToken || job2.data.cancelRequested)
1974
+ reject("STALE_EXECUTION", "The media job is canceled, replaced or no longer saving.");
1975
+ const request = job2.data.snapshot.request;
1976
+ const expected = job2.data.snapshot.descriptors.map((descriptor, index) => {
1977
+ const selected = request.operation === "video.process" ? request.input.input : request.input.inputs[index];
1978
+ return {
1979
+ file: descriptor.file,
1980
+ editRef: selected.kind === "edit" ? selected.editRef : null,
1981
+ recipe: descriptor.recipe
1982
+ };
1983
+ });
1984
+ if (canonical(expected) !== canonical(input.inputs) || request.input.outputName !== input.outputName || input.provenance?.kind !== job2.data.kind)
1985
+ reject(
1986
+ "PUBLICATION_INPUT_CONFLICT",
1987
+ "The output must preserve the accepted ordered sources, recipe and name."
1988
+ );
1989
+ return job2;
1990
+ };
1991
+ guard();
1992
+ for (const source of input.inputs) {
1993
+ const asset = t.get(source.file.source.objectId, true);
1994
+ if (asset.typeId !== "studio.asset" || asset.data.kind !== "video" || !data.files(source.file.source).some((f) => f.fileId === source.file.fileId))
1995
+ reject("INVALID_REFERENCE", "A fixed source is no longer available.");
1996
+ if (source.editRef) data.version(source.editRef);
1997
+ }
1998
+ const sourceRef = input.provenance?.kind !== "sequence" && input.inputs.length === 1 ? input.inputs[0].file.source : null;
1999
+ const saved2 = this.assets.commitPreparedImport(
2000
+ t,
2001
+ {
2002
+ name: input.outputName,
2003
+ origin: "authored",
2004
+ sourceRef,
2005
+ metadata: { media: input.provenance },
2006
+ versionMetadata: { media: input.provenance }
2007
+ },
2008
+ published,
2009
+ objectId,
2010
+ versionId
2011
+ );
2012
+ const file = {
2013
+ source: { storeId: data.storeId, objectId, versionId: saved2.version.versionId },
2014
+ fileId: published.files[0].fileId
2015
+ };
2016
+ for (const source of input.inputs) {
2017
+ t.relation(objectId, versionId, source.file.source, "reference");
2018
+ if (!sourceRef) t.relation(objectId, versionId, source.file.source, "source");
2019
+ if (source.editRef) t.relation(objectId, versionId, source.editRef, "reference");
2020
+ }
2021
+ const completed = complete(file);
2022
+ if (completed && typeof completed.then === "function")
2023
+ reject("ASYNC_TRANSACTION", "Media completion must be synchronous.");
2024
+ const job = t.get(input.completion.jobId);
2025
+ if (job.data.state !== "succeeded" || job.data.progress !== 1 || job.data.cancelRequested || job.data.executionToken !== input.completion.executionToken || job.data.result?.fileId !== file.fileId || job.data.result?.source?.versionId !== file.source.versionId || job.data.result?.source?.objectId !== file.source.objectId || job.data.result?.source?.storeId !== file.source.storeId)
2026
+ reject("INCOMPLETE_PUBLICATION", "Media completion did not commit the same result.");
2027
+ return file;
2028
+ });
2029
+ } catch (error) {
2030
+ try {
2031
+ await data.discardPublication(operationId);
2032
+ } catch {
2033
+ if (error && typeof error === "object") Object.assign(error, { cleanupPending: true });
2034
+ }
2035
+ throw error;
2036
+ }
2037
+ try {
2038
+ await data.finishFiles(operationId);
2039
+ } catch {
2040
+ }
2041
+ return result2;
2042
+ }
2043
+ };
2044
+ function canonical(value) {
2045
+ const sorted = (v) => Array.isArray(v) ? v.map(sorted) : v && typeof v === "object" ? Object.fromEntries(
2046
+ Object.keys(v).sort().map((k) => [k, sorted(v[k])])
2047
+ ) : v;
2048
+ return JSON.stringify(sorted(value));
2049
+ }
2050
+
2051
+ // packages/project-studio/src/generation.ts
2052
+ import { z as z7 } from "vivix-studio/sdk/shared";
2053
+ import { createHash as createHash4 } from "crypto";
2054
+ var taskData = z7.object({
2055
+ taskId: z7.uuid(),
2056
+ name: z7.string().trim().min(1).max(200).optional(),
2057
+ projectId: z7.uuid(),
2058
+ targetAssetId: z7.uuid(),
2059
+ kind: z7.enum(["generate", "iterate"]),
2060
+ state: z7.enum(taskStates),
2061
+ retryOfTaskId: z7.uuid().nullable(),
2062
+ provider: z7.string(),
2063
+ apiInstanceId: z7.string(),
2064
+ remoteTaskId: z7.string().nullable(),
2065
+ remoteRequestKey: z7.string(),
2066
+ remoteStatus: z7.string().nullable(),
2067
+ outputRefs: z7.array(versionRef),
2068
+ progress: z7.number().min(0).max(1).nullable(),
2069
+ expectedOutputs: z7.number().int().positive(),
2070
+ receivedOutputs: z7.number().int().nonnegative(),
2071
+ error: z7.any().nullable(),
2072
+ submittedAt: z7.string().nullable(),
2073
+ finishedAt: z7.string().nullable(),
2074
+ lastPolledAt: z7.string().nullable(),
2075
+ nextPollAt: z7.string().nullable(),
2076
+ attemptCount: z7.number().int().nonnegative(),
2077
+ configDigest: z7.string(),
2078
+ inputSnapshot: z7.any(),
2079
+ remoteOutputs: z7.array(z7.any()),
2080
+ execution: z7.any().nullable().default(null),
2081
+ cost: taskBilling.safeExtend({
2082
+ checkedAt: z7.string().nullable(),
2083
+ nextCheckAt: z7.string().nullable(),
2084
+ error: z7.object({ code: z7.string(), message: z7.string() }).nullable()
2085
+ }).nullable().default(null)
2086
+ }).strict();
2087
+ var Generation = class {
2088
+ constructor(assets, api, apiInstanceId, config) {
2089
+ this.assets = assets;
2090
+ this.api = api;
2091
+ this.apiInstanceId = apiInstanceId;
2092
+ this.config = config;
2093
+ }
2094
+ assets;
2095
+ api;
2096
+ apiInstanceId;
2097
+ config;
2098
+ async submit(value, call) {
2099
+ call = {
2100
+ actor: call.actor,
2101
+ requestKey: call.requestKey,
2102
+ confirmation: call.confirmation,
2103
+ scope: this.assets.data.scope
2104
+ };
2105
+ const input = generationInput.parse(value), data = this.assets.data;
2106
+ this.assets.assertWritable();
2107
+ const replay = data.receipt("generation.submit", input, call);
2108
+ if (replay) return replay.result;
2109
+ if (Boolean(input.targetAssetId) === Boolean(input.newAsset))
2110
+ reject("INVALID_TARGET", "Choose an existing target asset or a new asset.");
2111
+ const target = input.targetAssetId ? this.assets.get(input.targetAssetId) : null;
2112
+ if (target && target.revision !== input.expectedRevision)
2113
+ reject("REVISION_CONFLICT", "Target asset changed.", target.revision);
2114
+ const kind2 = target?.data.kind || input.newAsset.kind;
2115
+ if (!["image", "video", "audio"].includes(kind2))
2116
+ reject("INVALID_TARGET", "Generation requires an image, video or audio target.");
2117
+ const refs = [];
2118
+ for (const i of [...input.inputs].sort((a, b) => a.order - b.order)) {
2119
+ const object = this.assets.get(i.ref.objectId);
2120
+ const version = data.version(i.ref);
2121
+ const location = await data.location(i.ref);
2122
+ refs.push({
2123
+ ...i,
2124
+ kind: version.payload.kind,
2125
+ mimeType: location.mimeType,
2126
+ absolutePath: location.absolutePath
2127
+ });
2128
+ }
2129
+ if (new Set(input.inputs.map((x) => x.order)).size !== input.inputs.length)
2130
+ reject("INVALID_REFERENCE", "Reference order must be unique.");
2131
+ let base2 = null;
2132
+ if (input.baseVersionId) {
2133
+ if (!target || !input.changeRequest)
2134
+ reject("INVALID_ITERATION", "A fixed target version and a change request are required.");
2135
+ base2 = data.version({
2136
+ storeId: data.storeId,
2137
+ objectId: target.objectId,
2138
+ versionId: input.baseVersionId
2139
+ });
2140
+ await data.location({
2141
+ storeId: data.storeId,
2142
+ objectId: target.objectId,
2143
+ versionId: base2.versionId
2144
+ });
2145
+ if (input.anchor.kind === "timeRange" && (!["video", "audio"].includes(base2.payload.kind) || !Number.isInteger(base2.payload.durationMs) || input.anchor.endMs < input.anchor.startMs || input.anchor.endMs > base2.payload.durationMs))
2146
+ reject("INVALID_ANCHOR", "Time range exceeds the fixed version duration.");
2147
+ } else if (input.changeRequest || input.anchor.kind !== "whole")
2148
+ reject("INVALID_ITERATION", "Iteration context requires a base version.");
2149
+ if (input.retryOfTaskId) {
2150
+ const old = data.get(input.retryOfTaskId);
2151
+ if (old.typeId !== "studio.task" || ![
2152
+ "succeeded",
2153
+ "failed",
2154
+ "canceled",
2155
+ "submission_unknown",
2156
+ "partial",
2157
+ "save_failed"
2158
+ ].includes(old.data.state))
2159
+ reject("INVALID_RETRY", "This task is not eligible for an explicit new generation.");
2160
+ if (old.data.state === "submission_unknown" && !input.acknowledgeUnknown)
2161
+ reject(
2162
+ "UNKNOWN_ACKNOWLEDGEMENT_REQUIRED",
2163
+ "The original submission may be charged. Explicitly acknowledge a new request."
2164
+ );
2165
+ }
2166
+ const finalPrompt = base2 ? `${input.prompt}
2167
+
2168
+ Change request: ${input.changeRequest}
2169
+ Preserve: ${input.preserveRequest}` : input.prompt;
2170
+ const prepared = await this.api.prepare({ ...input, prompt: finalPrompt }, kind2, refs);
2171
+ const taskId = stableId(data.storeId, call, "generation.task"), targetAssetId = target?.objectId || stableId(data.storeId, call, "generation.asset");
2172
+ const snapshot = structuredClone({
2173
+ scope: data.scope,
2174
+ originalRequestKey: call.requestKey,
2175
+ actor: call.actor,
2176
+ approval: call.confirmation,
2177
+ prompt: finalPrompt,
2178
+ originalPrompt: input.prompt,
2179
+ modelId: input.modelId,
2180
+ capabilityId: input.capabilityId,
2181
+ outputKind: kind2,
2182
+ ...input.context && { context: input.context },
2183
+ capabilityVersion: prepared.capabilityVersion,
2184
+ capabilityHash: prepared.capabilityHash,
2185
+ parameters: prepared.parameters,
2186
+ inputs: input.inputs,
2187
+ baseVersionId: base2?.versionId || null,
2188
+ changeRequest: input.changeRequest || null,
2189
+ preserveRequest: base2 ? input.preserveRequest : null,
2190
+ anchor: base2 ? input.anchor : null,
2191
+ outputCount: input.outputCount,
2192
+ configSnapshot: {
2193
+ studio: this.config,
2194
+ api: this.api.config,
2195
+ identity: prepared.identity,
2196
+ adapterVersion: "1"
2197
+ }
2198
+ });
2199
+ return data.operation("generation.submit", input, call, (t) => {
2200
+ if (!target)
2201
+ t.create(
2202
+ "studio.asset",
2203
+ assetData.parse({
2204
+ projectId: data.scope.id,
2205
+ name: input.newAsset.name,
2206
+ kind: kind2,
2207
+ origin: "generated"
2208
+ }),
2209
+ {},
2210
+ targetAssetId
2211
+ );
2212
+ else if (t.get(targetAssetId).revision !== input.expectedRevision)
2213
+ reject(
2214
+ "REVISION_CONFLICT",
2215
+ "Target changed before local acceptance.",
2216
+ t.get(targetAssetId).revision
2217
+ );
2218
+ const task = t.create(
2219
+ "studio.task",
2220
+ taskData.parse({
2221
+ taskId,
2222
+ name: input.taskName || target?.data.name || input.newAsset.name,
2223
+ projectId: data.scope.id,
2224
+ targetAssetId,
2225
+ kind: base2 ? "iterate" : "generate",
2226
+ state: "queued",
2227
+ retryOfTaskId: input.retryOfTaskId || null,
2228
+ provider: "official",
2229
+ apiInstanceId: this.apiInstanceId,
2230
+ remoteTaskId: null,
2231
+ remoteRequestKey: "studio-task-" + taskId,
2232
+ remoteStatus: null,
2233
+ outputRefs: [],
2234
+ progress: null,
2235
+ expectedOutputs: input.outputCount,
2236
+ receivedOutputs: 0,
2237
+ error: null,
2238
+ submittedAt: null,
2239
+ finishedAt: null,
2240
+ lastPolledAt: null,
2241
+ nextPollAt: null,
2242
+ attemptCount: 0,
2243
+ configDigest: hash(snapshot.configSnapshot),
2244
+ inputSnapshot: snapshot,
2245
+ execution: {
2246
+ preparedRequest: prepared.wireRequest,
2247
+ catalog: prepared.catalog,
2248
+ wireRequest: null,
2249
+ receipt: null
2250
+ },
2251
+ remoteOutputs: []
2252
+ }),
2253
+ {},
2254
+ taskId
2255
+ );
2256
+ const protectedInputs = [
2257
+ ...input.inputs.map((i) => i.ref),
2258
+ ...base2 ? [{ storeId: data.storeId, objectId: target.objectId, versionId: base2.versionId }] : []
2259
+ ];
2260
+ for (const ref of new Map(protectedInputs.map((ref2) => [ref2.versionId, ref2])).values())
2261
+ t.relation(taskId, null, ref, "input");
2262
+ return { task, asset: t.get(targetAssetId), acceptedLocally: true };
2263
+ });
2264
+ }
2265
+ };
2266
+ var hash = (value) => createHash4("sha256").update(JSON.stringify(value)).digest("hex");
2267
+
2268
+ // packages/project-studio/src/task-runner.ts
2269
+ import { randomUUID } from "crypto";
2270
+
2271
+ // packages/project-studio/src/cost-recovery.ts
2272
+ var costRecoveryKey = "project-studio.costRecovery";
2273
+ var costAttemptOffsets = [0, 2e3, 4e3, 1e4, 15e3];
2274
+ var costWindowMs = 18e3;
2275
+ var legacyOffsets = [0, 2e3, 4e3];
2276
+ function costSchedule(format) {
2277
+ return format === 1 ? legacyOffsets : costAttemptOffsets;
2278
+ }
2279
+ function confirmedCost(cost) {
2280
+ const value = readTaskBilling({
2281
+ billing: cost && { status: cost.status, credits: cost.credits }
2282
+ });
2283
+ return value.status === "settled" || value.status === "released";
2284
+ }
2285
+ function costBudget(record) {
2286
+ const raw = record.metadata?.[costRecoveryKey];
2287
+ if (!raw || raw.format !== 1 && raw.format !== 2 || !Number.isInteger(raw.attemptCount) || raw.attemptCount < 1 || raw.attemptCount > costSchedule(raw.format).length || typeof raw.startedAt !== "string" || typeof raw.deadlineAt !== "string")
2288
+ return null;
2289
+ const start = Date.parse(raw.startedAt), end = Date.parse(raw.deadlineAt);
2290
+ if (!Number.isFinite(start) || end !== start + (raw.format === 1 ? 6e3 : costWindowMs) || raw.nextAttemptAt !== null && (typeof raw.nextAttemptAt !== "string" || !Number.isFinite(Date.parse(raw.nextAttemptAt)) || Date.parse(raw.nextAttemptAt) < start + (costSchedule(raw.format)[raw.attemptCount] ?? Infinity) || Date.parse(raw.nextAttemptAt) >= end) || raw.inFlight !== null && (!raw.inFlight || raw.inFlight.ordinal !== raw.attemptCount || typeof raw.inFlight.claimId !== "string" || !raw.inFlight.claimId))
2291
+ return null;
2292
+ return raw;
2293
+ }
2294
+
2295
+ // packages/project-studio/src/task-runner.ts
2296
+ var advancing = /* @__PURE__ */ new Set();
2297
+ var activeCostQueries = /* @__PURE__ */ new Map();
2298
+ var terminal = ["succeeded", "failed", "canceled"];
2299
+ var maxUploadAttempts = 3;
2300
+ var beforeSubmission = (task) => task.attemptCount === 0 && !task.remoteTaskId && !task.execution?.receipt && !task.execution?.wireRequest;
2301
+ var TaskRunner = class {
2302
+ constructor(data, api, notify = () => {
2303
+ }) {
2304
+ this.data = data;
2305
+ this.api = api;
2306
+ this.notify = notify;
2307
+ }
2308
+ data;
2309
+ api;
2310
+ notify;
2311
+ costQueries = /* @__PURE__ */ new Map();
2312
+ dispose() {
2313
+ for (const controller of this.costQueries.values()) controller.abort();
2314
+ }
2315
+ call(key) {
2316
+ return { scope: this.data.scope, actor: { kind: "local-user", id: "local" }, requestKey: key };
2317
+ }
2318
+ task(id2) {
2319
+ const record = this.data.get(id2);
2320
+ if (record.typeId !== "studio.task")
2321
+ reject("TYPE_MISMATCH", "Expected a persisted project task.");
2322
+ if (record.data.projectId !== this.data.scope.id || record.data.inputSnapshot?.scope?.id !== this.data.scope.id)
2323
+ reject("SCOPE_MISMATCH", "The task belongs to another project.");
2324
+ return record;
2325
+ }
2326
+ update(id2, changes, observation, old = this.task(id2)) {
2327
+ if (changes.error) changes = { ...changes, error: errorValue(changes.error) };
2328
+ const committed = this.data.operation(
2329
+ "tasks.transition",
2330
+ { id: id2, revision: old.revision, changes },
2331
+ this.call(`transition-${id2}-${old.revision}`),
2332
+ (t) => {
2333
+ const data = { ...old.data, ...changes };
2334
+ const metadata2 = stateMetadata(old, data, observation);
2335
+ const currentBudget = costBudget(old);
2336
+ if (currentBudget)
2337
+ metadata2[costRecoveryKey] = confirmedCost(data.cost) ? {
2338
+ ...currentBudget,
2339
+ inFlight: null,
2340
+ nextAttemptAt: null,
2341
+ stoppedReason: "confirmed"
2342
+ } : currentBudget;
2343
+ return t.update(id2, old.revision, data, metadata2);
2344
+ }
2345
+ );
2346
+ if (confirmedCost(committed.data.cost)) {
2347
+ for (const [key, controller] of activeCostQueries)
2348
+ if (key.startsWith(this.data.storeId + "/" + id2 + ":")) controller.abort();
2349
+ }
2350
+ try {
2351
+ this.notify(structuredClone(old), structuredClone(committed));
2352
+ } catch {
2353
+ }
2354
+ return committed;
2355
+ }
2356
+ costChanges(remote, previous) {
2357
+ const current = readTaskBilling(remote);
2358
+ if (confirmedCost(previous) && !confirmedCost(current))
2359
+ return {
2360
+ cost: {
2361
+ ...previous,
2362
+ error: {
2363
+ code: "BILLING_UNAVAILABLE",
2364
+ message: "The latest cost is unavailable. The last confirmed charge is retained."
2365
+ }
2366
+ }
2367
+ };
2368
+ return {
2369
+ cost: { ...current, checkedAt: (/* @__PURE__ */ new Date()).toISOString(), nextCheckAt: null, error: null }
2370
+ };
2371
+ }
2372
+ // Queue visits can refresh old pending charges after their automatic checks have ended.
2373
+ queueCostReads = /* @__PURE__ */ new Map();
2374
+ async refreshQueueCost(id2) {
2375
+ const record = this.task(id2);
2376
+ const budget = costBudget(record);
2377
+ if (!terminal.includes(record.data.state) || record.data.cost?.status !== "pending" || !budget || !budget.stoppedReason && Date.now() < Date.parse(budget.deadlineAt))
2378
+ return false;
2379
+ const existing = this.queueCostReads.get(id2);
2380
+ if (existing) return confirmedCost((await existing).data.cost);
2381
+ const work = this.syncCost(id2, true);
2382
+ this.queueCostReads.set(id2, work);
2383
+ try {
2384
+ return confirmedCost((await work).data.cost);
2385
+ } finally {
2386
+ this.queueCostReads.delete(id2);
2387
+ }
2388
+ }
2389
+ claimCostAttempt(id2) {
2390
+ const initial = this.task(id2), existing = costBudget(initial), now = Date.now();
2391
+ if (confirmedCost(initial.data.cost) || initial.metadata?.[costRecoveryKey] !== void 0 && !existing || existing?.stoppedReason)
2392
+ return null;
2393
+ if (existing && now >= Date.parse(existing.startedAt) && now < Date.parse(existing.deadlineAt) && (existing.inFlight || existing.attemptCount >= costSchedule(existing.format).length || !existing.nextAttemptAt || now < Date.parse(existing.nextAttemptAt)))
2394
+ return null;
2395
+ const claimId = randomUUID();
2396
+ let claimed = false;
2397
+ const record = this.data.operation(
2398
+ "tasks.costClaim",
2399
+ { id: id2, claimId },
2400
+ this.call(`cost-claim-${claimId}`),
2401
+ (tx) => {
2402
+ const current = tx.get(id2), now2 = Date.now();
2403
+ if (confirmedCost(current.data.cost)) return current;
2404
+ let budget = costBudget(current);
2405
+ if (current.metadata?.[costRecoveryKey] !== void 0 && !budget) return current;
2406
+ if (budget) {
2407
+ if (budget.stoppedReason) return current;
2408
+ if (now2 < Date.parse(budget.startedAt) || now2 >= Date.parse(budget.deadlineAt))
2409
+ return tx.update(id2, current.revision, current.data, {
2410
+ ...current.metadata,
2411
+ [costRecoveryKey]: {
2412
+ ...budget,
2413
+ inFlight: null,
2414
+ nextAttemptAt: null,
2415
+ stoppedReason: "deadline"
2416
+ }
2417
+ });
2418
+ if (budget.inFlight || budget.attemptCount >= costSchedule(budget.format).length || !budget.nextAttemptAt || now2 < Date.parse(budget.nextAttemptAt))
2419
+ return current;
2420
+ const ordinal = budget.attemptCount + 1;
2421
+ budget = {
2422
+ ...budget,
2423
+ attemptCount: ordinal,
2424
+ inFlight: { ordinal, claimId },
2425
+ nextAttemptAt: null
2426
+ };
2427
+ } else
2428
+ budget = {
2429
+ format: 2,
2430
+ startedAt: new Date(now2).toISOString(),
2431
+ deadlineAt: new Date(now2 + costWindowMs).toISOString(),
2432
+ attemptCount: 1,
2433
+ nextAttemptAt: null,
2434
+ inFlight: { ordinal: 1, claimId }
2435
+ };
2436
+ const result2 = tx.update(id2, current.revision, current.data, {
2437
+ ...current.metadata,
2438
+ [costRecoveryKey]: budget
2439
+ });
2440
+ claimed = true;
2441
+ return result2;
2442
+ }
2443
+ );
2444
+ return claimed ? { record, budget: costBudget(record) } : null;
2445
+ }
2446
+ async syncCost(id2, force = false) {
2447
+ const original = this.task(id2);
2448
+ const task = original.data;
2449
+ if (!task.remoteTaskId && !task.execution?.receipt && !task.attemptCount) return original;
2450
+ if (force) {
2451
+ try {
2452
+ const remote2 = await this.api.query(task);
2453
+ return this.update(id2, this.costChanges(remote2, this.task(id2).data.cost));
2454
+ } catch (error2) {
2455
+ const current = this.task(id2);
2456
+ return this.update(id2, {
2457
+ cost: {
2458
+ status: current.data.cost?.status || "unavailable",
2459
+ credits: current.data.cost?.credits ?? null,
2460
+ checkedAt: current.data.cost?.checkedAt || null,
2461
+ nextCheckAt: null,
2462
+ error: {
2463
+ code: error2?.code || "BILLING_QUERY_FAILED",
2464
+ message: "Cost could not be refreshed. Try again later."
2465
+ }
2466
+ }
2467
+ });
2468
+ }
2469
+ }
2470
+ let claim;
2471
+ try {
2472
+ claim = this.claimCostAttempt(id2);
2473
+ } catch (error2) {
2474
+ if (!["SQLITE_BUSY", "SQLITE_BUSY_SNAPSHOT", "REVISION_CONFLICT"].includes(error2?.code))
2475
+ throw error2;
2476
+ return this.task(id2);
2477
+ }
2478
+ if (!claim) return this.task(id2);
2479
+ const { record, budget } = claim, claimId = budget.inFlight.claimId;
2480
+ const controller = new AbortController(), queryKey = this.data.storeId + "/" + id2 + ":" + claimId;
2481
+ const remaining = Date.parse(budget.deadlineAt) - Date.now();
2482
+ if (remaining <= 0) controller.abort();
2483
+ const timer = setTimeout(() => controller.abort(), Math.max(0, remaining));
2484
+ this.costQueries.set(queryKey, controller);
2485
+ activeCostQueries.set(queryKey, controller);
2486
+ let remote, error, aborted;
2487
+ try {
2488
+ controller.signal.throwIfAborted();
2489
+ remote = await Promise.race([
2490
+ this.api.query(record.data, { signal: controller.signal }),
2491
+ new Promise((_, reject3) => {
2492
+ aborted = () => reject3(controller.signal.reason);
2493
+ controller.signal.addEventListener("abort", aborted, { once: true });
2494
+ if (controller.signal.aborted) aborted();
2495
+ })
2496
+ ]);
2497
+ } catch (cause) {
2498
+ error = cause;
2499
+ } finally {
2500
+ clearTimeout(timer);
2501
+ if (aborted) controller.signal.removeEventListener("abort", aborted);
2502
+ this.costQueries.delete(queryKey);
2503
+ activeCostQueries.delete(queryKey);
2504
+ }
2505
+ let before;
2506
+ const committed = this.data.operation(
2507
+ "tasks.costResult",
2508
+ { id: id2, claimId },
2509
+ this.call(`cost-result-${claimId}`),
2510
+ (tx) => {
2511
+ const current = tx.get(id2), active = costBudget(current), now = Date.now();
2512
+ if (!active || active.inFlight?.claimId !== claimId || active.stoppedReason || confirmedCost(current.data.cost))
2513
+ return current;
2514
+ if (controller.signal.aborted && now >= Date.parse(active.startedAt) && now < Date.parse(active.deadlineAt))
2515
+ return current;
2516
+ before = current;
2517
+ let next = { ...active, inFlight: null, nextAttemptAt: null };
2518
+ let data = current.data;
2519
+ if (controller.signal.aborted || now < Date.parse(active.startedAt) || now >= Date.parse(active.deadlineAt))
2520
+ next.stoppedReason = "deadline";
2521
+ else {
2522
+ data = error ? {
2523
+ ...data,
2524
+ cost: {
2525
+ status: data.cost?.status || "unavailable",
2526
+ credits: data.cost?.credits ?? null,
2527
+ checkedAt: data.cost?.checkedAt || null,
2528
+ nextCheckAt: null,
2529
+ error: {
2530
+ code: error.code || "BILLING_QUERY_FAILED",
2531
+ message: "Cost could not be refreshed. Try again later."
2532
+ }
2533
+ }
2534
+ } : { ...data, ...this.costChanges(remote, data.cost) };
2535
+ if (confirmedCost(data.cost)) next.stoppedReason = "confirmed";
2536
+ else if (active.attemptCount === costSchedule(active.format).length)
2537
+ next.stoppedReason = "exhausted";
2538
+ else {
2539
+ const retryAfter = Math.max(
2540
+ 0,
2541
+ Number(error?.retryAfterMs || remote?.retry_after_ms) || 0
2542
+ );
2543
+ const nextAt = Math.max(
2544
+ Date.parse(active.startedAt) + costSchedule(active.format)[active.attemptCount],
2545
+ now + retryAfter
2546
+ );
2547
+ if (nextAt >= Date.parse(active.deadlineAt)) next.stoppedReason = "retry-after";
2548
+ else next.nextAttemptAt = new Date(nextAt).toISOString();
2549
+ }
2550
+ }
2551
+ return tx.update(id2, current.revision, data, {
2552
+ ...stateMetadata(current, data),
2553
+ [costRecoveryKey]: next
2554
+ });
2555
+ }
2556
+ );
2557
+ if (before) {
2558
+ try {
2559
+ this.notify(structuredClone(before), structuredClone(committed));
2560
+ } catch {
2561
+ }
2562
+ }
2563
+ return committed;
2564
+ }
2565
+ updateSubmission(task, changes, beforePost = false) {
2566
+ const record = this.task(task.taskId);
2567
+ if (record.data.state !== "submitting" || record.data.execution?.uploadAttempt !== task.execution?.uploadAttempt || beforePost && !beforeSubmission(record.data))
2568
+ reject("TASK_ALREADY_CLAIMED", "The submission claim is no longer current.");
2569
+ return this.update(task.taskId, changes, void 0, record);
2570
+ }
2571
+ recoverUpload(record, error) {
2572
+ const task = record.data;
2573
+ const retry = error.details?.retryable === true && task.execution.uploadAttempt < maxUploadAttempts;
2574
+ const delay = Math.max(
2575
+ task.inputSnapshot.configSnapshot.api.pollIntervalMs * 2 ** (task.execution.uploadAttempt - 1),
2576
+ error.retryAfterMs || 0
2577
+ );
2578
+ return this.update(
2579
+ task.taskId,
2580
+ {
2581
+ state: retry ? "queued" : "failed",
2582
+ error,
2583
+ nextPollAt: retry ? new Date(Date.now() + delay).toISOString() : null,
2584
+ finishedAt: retry ? null : (/* @__PURE__ */ new Date()).toISOString()
2585
+ },
2586
+ void 0,
2587
+ record
2588
+ );
2589
+ }
2590
+ async advance(id2, restarting = false) {
2591
+ const key = `${this.data.storeId}/${id2}`;
2592
+ if (advancing.has(key)) return;
2593
+ advancing.add(key);
2594
+ try {
2595
+ let record = this.task(id2), task = record.data;
2596
+ if (terminal.includes(task.state) || ["save_failed", "partial"].includes(task.state)) {
2597
+ await this.syncCost(id2);
2598
+ return;
2599
+ }
2600
+ if (task.inputSnapshot?.configSnapshot?.adapterVersion !== "1") {
2601
+ this.update(id2, {
2602
+ error: {
2603
+ code: "ADAPTER_INCOMPATIBLE",
2604
+ message: "The original task requires a compatible receipt adapter.",
2605
+ blocked: true
2606
+ }
2607
+ });
2608
+ return;
2609
+ }
2610
+ if (restarting && task.state === "submitting") {
2611
+ if (beforeSubmission(task) && Number.isSafeInteger(task.execution?.uploadAttempt) && task.execution.uploadAttempt > 0) {
2612
+ record = this.recoverUpload(record, {
2613
+ code: "REFERENCE_UPLOAD_INTERRUPTED",
2614
+ message: "Reference preparation was interrupted before generation submission.",
2615
+ details: { phase: "reference_upload", retryable: true }
2616
+ });
2617
+ } else
2618
+ record = this.update(
2619
+ id2,
2620
+ {
2621
+ state: "submission_unknown",
2622
+ error: {
2623
+ code: "INTERRUPTED_SUBMISSION",
2624
+ message: "Submission may have been accepted and charged. Only the original key will be queried."
2625
+ }
2626
+ },
2627
+ void 0,
2628
+ record
2629
+ );
2630
+ task = record.data;
2631
+ }
2632
+ if (task.state === "submitting") return;
2633
+ if (task.nextPollAt && Date.parse(task.nextPollAt) > Date.now()) return;
2634
+ if (task.state === "queued") {
2635
+ record = this.data.operation(
2636
+ "tasks.claim",
2637
+ { id: id2, revision: record.revision },
2638
+ this.call(`claim-${id2}-${record.revision}`),
2639
+ (t) => {
2640
+ const current = t.get(id2);
2641
+ if (current.revision !== record.revision || current.data.state !== "queued")
2642
+ reject("TASK_ALREADY_CLAIMED", "The task is already being advanced.");
2643
+ return t.update(id2, current.revision, {
2644
+ ...current.data,
2645
+ state: "submitting",
2646
+ submittedAt: current.data.submittedAt || (/* @__PURE__ */ new Date()).toISOString(),
2647
+ nextPollAt: null,
2648
+ execution: {
2649
+ ...current.data.execution,
2650
+ uploadAttempt: (current.data.execution?.uploadAttempt || 0) + 1
2651
+ },
2652
+ error: null
2653
+ });
2654
+ }
2655
+ );
2656
+ task = record.data;
2657
+ let receipt;
2658
+ try {
2659
+ const inputs = [];
2660
+ for (const i of task.inputSnapshot.inputs) {
2661
+ const v = this.data.version(i.ref), location = await this.data.location(i.ref);
2662
+ inputs.push({ ...i, ...location, kind: v.payload.kind });
2663
+ }
2664
+ receipt = await this.api.submit(task, inputs, (pending) => {
2665
+ if (!pending)
2666
+ reject(
2667
+ "RECEIPT_PERSISTENCE_REQUIRED",
2668
+ "Submission requires the original durable task receipt."
2669
+ );
2670
+ this.updateSubmission(
2671
+ task,
2672
+ {
2673
+ attemptCount: 1,
2674
+ execution: {
2675
+ ...task.execution,
2676
+ wireRequest: pending.items[0].request,
2677
+ receipt: pending
2678
+ }
2679
+ },
2680
+ true
2681
+ );
2682
+ });
2683
+ } catch (e) {
2684
+ if (["TASK_ALREADY_CLAIMED", "REVISION_CONFLICT"].includes(e?.code)) throw e;
2685
+ const current = this.task(id2);
2686
+ if (e?.code === "REFERENCE_UPLOAD_FAILED" && e?.details?.phase === "reference_upload" && beforeSubmission(current.data)) {
2687
+ if (current.data.state !== "submitting" || current.data.execution?.uploadAttempt !== task.execution.uploadAttempt)
2688
+ reject("TASK_ALREADY_CLAIMED", "The upload claim is no longer current.");
2689
+ this.recoverUpload(current, e);
2690
+ } else this.updateSubmission(task, { state: "submission_unknown", error: errorValue(e) });
2691
+ return;
2692
+ }
2693
+ const item = receipt.items?.[0];
2694
+ record = this.updateSubmission(task, {
2695
+ state: item?.status === "accepted" ? "running" : item?.status === "rejected" ? "failed" : "submission_unknown",
2696
+ remoteTaskId: item?.task_id || null,
2697
+ error: item?.error || null,
2698
+ execution: {
2699
+ ...task.execution,
2700
+ wireRequest: item?.request || task.execution?.preparedRequest,
2701
+ receipt
2702
+ },
2703
+ ...item?.status === "rejected" ? { finishedAt: (/* @__PURE__ */ new Date()).toISOString() } : {}
2704
+ });
2705
+ task = record.data;
2706
+ }
2707
+ if (["submission_unknown", "running", "cancel_requested"].includes(task.state)) {
2708
+ let remote;
2709
+ try {
2710
+ remote = await this.api.query(task);
2711
+ } catch (e) {
2712
+ this.update(id2, {
2713
+ error: errorValue(e),
2714
+ lastPolledAt: (/* @__PURE__ */ new Date()).toISOString(),
2715
+ nextPollAt: new Date(
2716
+ Date.now() + Math.max(
2717
+ task.inputSnapshot.configSnapshot.api.pollIntervalMs,
2718
+ e.retryAfterMs || 0
2719
+ )
2720
+ ).toISOString()
2721
+ });
2722
+ return;
2723
+ }
2724
+ const state = remote.status === "succeeded" ? "remote_succeeded" : remote.status === "failed" ? "failed" : ["cancelled", "canceled"].includes(remote.status) ? "canceled" : task.state === "submission_unknown" && !remote.task_id ? "submission_unknown" : task.state === "cancel_requested" ? "cancel_requested" : "running";
2725
+ record = this.update(
2726
+ id2,
2727
+ {
2728
+ state,
2729
+ remoteTaskId: remote.task_id || task.remoteTaskId,
2730
+ ...this.costChanges(remote, this.task(id2).data.cost),
2731
+ remoteStatus: remote.status || null,
2732
+ progress: typeof remote.progress === "number" ? remote.progress : null,
2733
+ lastPolledAt: (/* @__PURE__ */ new Date()).toISOString(),
2734
+ nextPollAt: ["running", "submission_unknown", "cancel_requested"].includes(state) ? new Date(
2735
+ Date.now() + Math.max(
2736
+ task.inputSnapshot.configSnapshot.api.pollIntervalMs,
2737
+ remote.retry_after_ms || 0
2738
+ )
2739
+ ).toISOString() : null,
2740
+ remoteOutputs: remote.outputs || remote.assets || task.remoteOutputs,
2741
+ error: remote.error || null,
2742
+ ...terminal.includes(state) ? { finishedAt: (/* @__PURE__ */ new Date()).toISOString() } : {}
2743
+ },
2744
+ {
2745
+ status: remote.status,
2746
+ resultCount: Array.isArray(remote.outputs || remote.assets) ? (remote.outputs || remote.assets).length : null
2747
+ }
2748
+ );
2749
+ task = record.data;
2750
+ }
2751
+ if (["remote_succeeded", "saving"].includes(task.state)) await this.save(id2);
2752
+ } catch (e) {
2753
+ if (["TASK_ALREADY_CLAIMED", "REVISION_CONFLICT"].includes(e.code)) return;
2754
+ const current = this.task(id2);
2755
+ if (!terminal.includes(current.data.state))
2756
+ this.update(id2, {
2757
+ error: errorValue(e),
2758
+ ...current.data.state === "queued" ? { state: "failed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() } : current.data.state === "submitting" ? { state: "submission_unknown" } : {}
2759
+ });
2760
+ } finally {
2761
+ advancing.delete(key);
2762
+ }
2763
+ }
2764
+ async save(id2) {
2765
+ let task = this.task(id2).data;
2766
+ this.update(id2, { state: "saving", nextPollAt: null });
2767
+ const errors = [];
2768
+ for (const [index, output] of task.remoteOutputs.entries()) {
2769
+ if (this.data.list({
2770
+ typeId: "studio.taskOutput",
2771
+ taskId: id2,
2772
+ outputIndex: index,
2773
+ includeDeleted: true,
2774
+ limit: 1
2775
+ }).items.length)
2776
+ continue;
2777
+ try {
2778
+ const call = this.call(`task-output-${id2}-${index}`), op = stableId(this.data.storeId, call, "output"), versionId = stableId(op, call, "version");
2779
+ const staged = await this.api.download(
2780
+ output,
2781
+ (source, name) => this.data.stage(source, name, call.actor.id),
2782
+ task
2783
+ );
2784
+ const asset = this.data.get(task.targetAssetId);
2785
+ if (staged.kind !== asset.data.kind)
2786
+ reject("TYPE_MISMATCH", "Remote result has an unexpected actual media kind.");
2787
+ const pub = await this.data.publish(
2788
+ [staged.stagingFileId],
2789
+ asset.objectId,
2790
+ versionId,
2791
+ op,
2792
+ call.actor.id
2793
+ );
2794
+ this.data.operation(
2795
+ "tasks.saveOutput",
2796
+ {
2797
+ taskId: id2,
2798
+ outputIndex: index,
2799
+ remoteOutputId: output.asset_id || output.file_id || output.id
2800
+ },
2801
+ call,
2802
+ (t) => {
2803
+ const existing = this.data.list({
2804
+ typeId: "studio.taskOutput",
2805
+ taskId: id2,
2806
+ outputIndex: index,
2807
+ includeDeleted: true,
2808
+ limit: 1
2809
+ }).items[0];
2810
+ if (existing) return existing;
2811
+ const current = t.get(id2), target = t.get(task.targetAssetId);
2812
+ const version = t.commitVersion(target.objectId, target.revision, {
2813
+ versionId,
2814
+ parentVersionId: task.inputSnapshot.baseVersionId,
2815
+ origin: "generated",
2816
+ taskId: id2,
2817
+ payload: payload(pub, {
2818
+ taskId: id2,
2819
+ outputIndex: index,
2820
+ prompt: task.inputSnapshot.prompt,
2821
+ originalPrompt: task.inputSnapshot.originalPrompt,
2822
+ refs: structuredClone(task.inputSnapshot.inputs),
2823
+ modelId: task.inputSnapshot.modelId,
2824
+ parameters: structuredClone(task.inputSnapshot.parameters)
2825
+ }),
2826
+ files: pub.files
2827
+ });
2828
+ selectFirstAdCandidate(this.data, t, target.objectId, version.versionId);
2829
+ const ref = {
2830
+ storeId: this.data.storeId,
2831
+ objectId: target.objectId,
2832
+ versionId: version.versionId
2833
+ };
2834
+ const mapping = t.create("studio.taskOutput", {
2835
+ taskId: id2,
2836
+ outputIndex: index,
2837
+ versionRef: ref,
2838
+ remoteOutputId: output.asset_id || output.file_id || output.id
2839
+ });
2840
+ t.update(id2, current.revision, {
2841
+ ...current.data,
2842
+ outputRefs: [...current.data.outputRefs, ref],
2843
+ receivedOutputs: current.data.receivedOutputs + 1
2844
+ });
2845
+ return mapping;
2846
+ }
2847
+ );
2848
+ await this.data.finishFiles(op);
2849
+ task = this.task(id2).data;
2850
+ } catch (e) {
2851
+ errors.push({ outputIndex: index, ...errorValue(e) });
2852
+ }
2853
+ }
2854
+ task = this.task(id2).data;
2855
+ const complete = task.receivedOutputs === task.expectedOutputs;
2856
+ this.update(id2, {
2857
+ state: complete ? "succeeded" : task.receivedOutputs ? "partial" : "save_failed",
2858
+ error: errors.length ? {
2859
+ code: "SAVE_INCOMPLETE",
2860
+ message: "Some original outputs could not be saved.",
2861
+ retryAction: "retryDownload",
2862
+ details: { outputs: errors }
2863
+ } : complete ? null : {
2864
+ code: "OUTPUTS_MISSING",
2865
+ message: "The original remote task did not return all expected outputs."
2866
+ },
2867
+ finishedAt: complete ? (/* @__PURE__ */ new Date()).toISOString() : null
2868
+ });
2869
+ }
2870
+ async queryOriginal(id2) {
2871
+ const task = this.task(id2).data;
2872
+ if (terminal.includes(task.state)) return this.syncCost(id2, true);
2873
+ if (["partial", "save_failed", "remote_succeeded"].includes(task.state)) {
2874
+ const remote = await this.api.query(task);
2875
+ return this.update(
2876
+ id2,
2877
+ {
2878
+ remoteOutputs: remote.outputs || remote.assets || task.remoteOutputs,
2879
+ ...this.costChanges(remote, this.task(id2).data.cost),
2880
+ remoteStatus: remote.status || task.remoteStatus,
2881
+ lastPolledAt: (/* @__PURE__ */ new Date()).toISOString()
2882
+ },
2883
+ {
2884
+ status: remote.status,
2885
+ resultCount: Array.isArray(remote.outputs || remote.assets) ? (remote.outputs || remote.assets).length : null
2886
+ }
2887
+ );
2888
+ }
2889
+ if (task.state === "queued") return this.task(id2);
2890
+ await this.advance(id2);
2891
+ return this.task(id2);
2892
+ }
2893
+ async retryDownload(id2) {
2894
+ const task = this.task(id2).data;
2895
+ if (!["partial", "save_failed", "remote_succeeded"].includes(task.state))
2896
+ reject("INVALID_STATE", "This task has no failed local save to retry.");
2897
+ const remote = await this.api.query(task);
2898
+ this.update(
2899
+ id2,
2900
+ {
2901
+ remoteOutputs: remote.outputs || remote.assets || task.remoteOutputs,
2902
+ ...this.costChanges(remote, this.task(id2).data.cost),
2903
+ remoteStatus: remote.status || task.remoteStatus,
2904
+ state: "saving",
2905
+ nextPollAt: null
2906
+ },
2907
+ {
2908
+ status: remote.status,
2909
+ resultCount: Array.isArray(remote.outputs || remote.assets) ? (remote.outputs || remote.assets).length : null
2910
+ }
2911
+ );
2912
+ await this.advance(id2);
2913
+ return this.task(id2);
2914
+ }
2915
+ async cancel(id2) {
2916
+ const task = this.task(id2).data;
2917
+ if (task.state === "queued")
2918
+ return this.update(id2, { state: "canceled", finishedAt: (/* @__PURE__ */ new Date()).toISOString() });
2919
+ if (!["running", "cancel_requested"].includes(task.state))
2920
+ reject("CANCEL_UNAVAILABLE", "This task cannot currently be canceled.");
2921
+ this.update(id2, { state: "cancel_requested" });
2922
+ const remote = await this.api.cancel(task);
2923
+ if (["cancelled", "canceled"].includes(remote.status))
2924
+ this.update(id2, {
2925
+ state: "canceled",
2926
+ finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
2927
+ ...this.costChanges(remote, this.task(id2).data.cost),
2928
+ remoteStatus: remote.status
2929
+ });
2930
+ return this.task(id2);
2931
+ }
2932
+ };
2933
+ function errorValue(e) {
2934
+ return {
2935
+ code: e?.code || "TASK_ERROR",
2936
+ message: typeof e === "string" ? e : e?.message || "Task failed.",
2937
+ retryAction: e?.retryAction || null,
2938
+ details: e?.details || (e?.blocked ? { blocked: true } : e?.currentRevision ? { currentRevision: e.currentRevision } : null)
2939
+ };
2940
+ }
2941
+
2942
+ // packages/project-studio/src/server.ts
2943
+ var Config2 = studioConfig;
2944
+ var dependencies = (c) => [
2945
+ c.dataInstance,
2946
+ c.apiInstance,
2947
+ ...c.libraryInstance ? [c.libraryInstance] : []
2948
+ ];
2949
+ var definitions = [
2950
+ ["studio.project", (x) => projectData.parse(x)],
2951
+ ["studio.asset", (x) => assetData.parse(x)],
2952
+ ["studio.task", (x) => taskData.parse(x)],
2953
+ ["studio.favorite", (x) => favoriteData.parse(x)],
2954
+ [
2955
+ "studio.taskOutput",
2956
+ (x) => z8.object({
2957
+ taskId: z8.uuid(),
2958
+ outputIndex: z8.number().int().nonnegative(),
2959
+ versionRef,
2960
+ remoteOutputId: z8.string().nullable().optional()
2961
+ }).strict().parse(x)
2962
+ ]
2963
+ ];
2964
+ var typeDefinitions = definitions.map(([typeId, validate]) => ({
2965
+ typeId,
2966
+ schemaVersion: 1,
2967
+ scopeKinds: ["project"],
2968
+ validate,
2969
+ validateUpdate: (a, b) => {
2970
+ if (typeId === "studio.favorite" && ["storeId", "objectId", "versionId"].some((key) => a.ref[key] !== b.ref[key]))
2971
+ reject("IMMUTABLE_FIELD", "A favorite keeps its selected asset version.");
2972
+ if (typeId === "studio.asset" && (a.kind !== b.kind || a.projectId !== b.projectId || a.origin !== b.origin || ["storeId", "objectId", "versionId"].some((k) => a.sourceRef?.[k] !== b.sourceRef?.[k])))
2973
+ reject("IMMUTABLE_FIELD", "Asset kind, origin, project and source are immutable.");
2974
+ if (typeId === "studio.task" && (hash(a.inputSnapshot) !== hash(b.inputSnapshot) || a.taskId !== b.taskId || a.projectId !== b.projectId || a.remoteRequestKey !== b.remoteRequestKey || a.configDigest !== b.configDigest || a.apiInstanceId !== b.apiInstanceId || hash(a.execution?.preparedRequest) !== hash(b.execution?.preparedRequest) || hash(a.execution?.catalog) !== hash(b.execution?.catalog)))
2975
+ reject(
2976
+ "IMMUTABLE_FIELD",
2977
+ "Task identity, execution request and configuration snapshots cannot be changed."
2978
+ );
2979
+ }
2980
+ }));
2981
+ var id = z8.uuid();
2982
+ var revision = z8.number().int().positive();
2983
+ var empty = z8.object({}).strict();
2984
+ var base = z8.object({ assetId: id, expectedRevision: revision }).strict();
2985
+ var locationInput = z8.union([
2986
+ z8.object({ source: versionRef, fileId: id.optional() }).strict(),
2987
+ z8.object({ ref: versionRef, fileId: id.optional() }).strict()
2988
+ ]);
2989
+ var operationDescriptions = {
2990
+ "documents.list": "List persisted extension documents in the bound scope. Does not create content.",
2991
+ "documents.create": "Create a persisted extension document with the original write request key.",
2992
+ "documents.update": "Save document content only at its observed revision; preserve the draft on conflict.",
2993
+ "documents.delete": "Delete the specified extension document at its observed revision.",
2994
+ "files.resolveLocation": "Resolve an exact immutable version and optional file ID to a readable local location and preview URL. Does not render or export.",
2995
+ "files.openLocation": "Open the containing directory of an exact readable immutable version. Does not modify its bytes.",
2996
+ "metadata.patch": "Update only the named object metadata using its observed revision and a stable write request key.",
2997
+ "metadata.keys": "List metadata keys available in this binding for building actual queries and views.",
2998
+ "generation.submit": "Submit one approved generation with a durable original request key; acceptance is not completion or successful file saving.",
2999
+ "projects.get": "Read the current project record and its revision before changing settings.",
3000
+ "projects.update": "Update the specified project settings at the observed revision, preserving unrelated project content.",
3001
+ "projects.archive": "Archive the project record without deleting its saved asset versions.",
3002
+ "projects.restore": "Restore the archived project record without regenerating content.",
3003
+ "assets.list": "List project assets with current selection, availability, and version summary. Candidate versions remain separate.",
3004
+ "assets.get": "Read one project asset, immutable versions, selected version, and current revision.",
3005
+ "assets.import": "Create a new project asset from validated staged media IDs. Does not treat a host path as a staged upload.",
3006
+ "assets.importVersion": "Append imported content as an immutable candidate version of the existing asset at its observed revision.",
3007
+ "assets.createText": "Create a project text asset and its initial saved version.",
3008
+ "assets.saveText": "Save a new immutable text version from the specified parent and observed asset revision.",
3009
+ "assets.update": "Change asset name, tags, or settings at the observed revision without rewriting saved media.",
3010
+ "assets.delete": "Remove the asset from active use while retaining history and protected references according to the existing lifecycle.",
3011
+ "choices.adopt": "Explicitly select an existing immutable candidate version for this asset at the observed revision.",
3012
+ "choices.clear": "Clear the current candidate choice at the observed revision while keeping saved versions.",
3013
+ "assets.importMaterial": "Copy the chosen fixed library material version into this project and retain provenance.",
3014
+ "assets.collectMaterial": "Collect the specified saved project version into the library without regenerating or changing original bytes.",
3015
+ "tasks.list": "List persisted project tasks and their original request identities. Reading never submits another generation.",
3016
+ "tasks.get": "Read one persisted task, its remote receipt, save state, errors, and actual output references.",
3017
+ "tasks.queryOriginal": "Query the original Provider receipt for a persisted task with an unknown submission outcome. Never resubmit it.",
3018
+ "tasks.retryDownload": "Save the original completed Provider outputs again after a local saving failure. Does not regenerate or create a new paid intent.",
3019
+ "tasks.cancel": "Request cancellation of the existing task and return its actual outcome; closing a page is not cancellation.",
3020
+ "assets.cleanupImpact": "Read references that protect an asset or its versions before requesting purge.",
3021
+ "assets.purge": "Permanently purge eligible asset data only after impact checks and explicit confirmation.",
3022
+ "assets.batchLocations": "Resolve the requested exact version references for download without switching to newer candidates.",
3023
+ "mediaUses.get": "Read the edit bound to this exact fixed video usage. Does not select or encode content.",
3024
+ "mediaUses.bind": "Bind an immutable edit to the observed fixed usage and revision while preserving protected references. Does not encode a file.",
3025
+ "ads.get": "Read the persisted advertising brief, storyboard order, clip identities, and revision.",
3026
+ "ads.initialize": "Create the initial advertising storyboard once without generating video.",
3027
+ "ads.addClip": "Add an empty storyboard clip and stable asset identity at the observed revision.",
3028
+ "ads.updateBrief": "Save brief context and fixed references at the observed storyboard revision; this does not generate content.",
3029
+ "ads.updateClip": "Save the specified clip script at the observed storyboard revision without synthesizing narration or captions.",
3030
+ "ads.reorder": "Save the explicit storyboard order at the observed revision while retaining clip choices.",
3031
+ "ads.removeClip": "Remove the clip from the storyboard at the observed revision while preserving its asset and versions.",
3032
+ "global.query": "Query the bound workspace business projections with read-only SQL, filtering and ordering before pagination. Does not copy assets into a library.",
3033
+ "global.projectNames": "Read the saved display names for the Studio Home project catalog.",
3034
+ "global.projectSummaries": "Read saved project display names and cover references for Studio Home.",
3035
+ "global.renameProject": "Rename a project at its observed settings revision.",
3036
+ "global.setProjectCover": "Set or clear an exact saved image as the project cover at the observed project revision.",
3037
+ "global.asset": "Read an exact saved asset version through the coordinator while retaining its original project identity.",
3038
+ "global.favorite": "Persist or remove a favorite pinned to the specified immutable VersionRef. Does not change the adopted candidate.",
3039
+ "tasks.global": "List persisted tasks across the coordinator binding with pagination, including actual execution state.",
3040
+ "tasks.rename": "Change the persisted display name of the specified task without changing its execution."
3041
+ };
3042
+ async function activate(ctx) {
3043
+ const scope = ctx.scope || { kind: "project", id: ctx.project.id };
3044
+ if (ctx.config.role === "coordinator" !== (scope.kind === "user"))
3045
+ reject(
3046
+ "SCOPE_MISMATCH",
3047
+ "The coordinator belongs to user scope and project studio belongs to project scope."
3048
+ );
3049
+ const all = ctx.plugins.get(ctx.config.dataInstance).bind({
3050
+ scope,
3051
+ ownerPackage: "@vivix/project-studio",
3052
+ // Compatible project-studio releases share the same registered storage schemas.
3053
+ packageVersion: "0.1.22-ecommerce.20261110.1",
3054
+ actorId: "local",
3055
+ access: ["data.workspace"]
3056
+ });
3057
+ for (const definition2 of [...typeDefinitions, adDefinition]) all.register(definition2);
3058
+ const api = ctx.plugins.get(ctx.config.apiInstance);
3059
+ const bindCall = (c) => ({
3060
+ actor: c.actor,
3061
+ requestKey: c.requestKey,
3062
+ confirmation: c.confirmation,
3063
+ scope
3064
+ });
3065
+ const handle = (name, input, fn, effects = ["read"], confirm = "none") => ctx.handle(
3066
+ defineOperation2({
3067
+ id: name,
3068
+ title: name,
3069
+ description: operationDescriptions[name] || "Operate on the specified scoped record using the declared schema and observed revision.",
3070
+ input,
3071
+ output: z8.any(),
3072
+ effects,
3073
+ confirm
3074
+ }),
3075
+ (i, c) => fn(i, bindCall(c))
3076
+ );
3077
+ let stopped = false, running = false;
3078
+ let costCursor = "";
3079
+ const runners = /* @__PURE__ */ new Map();
3080
+ const runnerFor = (projectId) => {
3081
+ let runner2 = runners.get(projectId);
3082
+ if (!runner2) {
3083
+ runner2 = new TaskRunner(
3084
+ all.project(projectId),
3085
+ api,
3086
+ (before, after) => notifyCommitted(
3087
+ () => ctx.events.forScope({ kind: "project", id: projectId }),
3088
+ before,
3089
+ after
3090
+ )
3091
+ );
3092
+ runners.set(projectId, runner2);
3093
+ }
3094
+ return runner2;
3095
+ };
3096
+ const tick = async (restarting = false) => {
3097
+ if (stopped || running) return;
3098
+ running = true;
3099
+ try {
3100
+ for (let afterId = ""; !stopped; ) {
3101
+ const page = all.tasks({ limit: 200, unfinished: true, afterId });
3102
+ for (const record of page) {
3103
+ if (stopped) break;
3104
+ if (ctx.config.role === "project" && record.scope.id !== scope.id) continue;
3105
+ try {
3106
+ await runnerFor(record.scope.id).advance(record.objectId, restarting);
3107
+ } catch (error) {
3108
+ if (["REVISION_CONFLICT", "TASK_ALREADY_CLAIMED"].includes(error?.code))
3109
+ continue;
3110
+ const runner2 = runnerFor(record.scope.id);
3111
+ runner2.update(record.objectId, {
3112
+ error: { ...errorValue(error), details: { blocked: true } }
3113
+ });
3114
+ }
3115
+ }
3116
+ if (page.length < 200) break;
3117
+ afterId = page.at(-1).objectId;
3118
+ }
3119
+ const costs = all.tasks({ limit: 5, needsCostSync: true, afterId: costCursor });
3120
+ for (const record of costs) {
3121
+ if (stopped) break;
3122
+ costCursor = record.objectId;
3123
+ if (ctx.config.role === "project" && record.scope.id !== scope.id) continue;
3124
+ await runnerFor(record.scope.id).syncCost(record.objectId);
3125
+ }
3126
+ if (costs.length < 5) costCursor = "";
3127
+ } finally {
3128
+ running = false;
3129
+ }
3130
+ };
3131
+ const start = async () => {
3132
+ await tick(true);
3133
+ const interval = setInterval(() => {
3134
+ void tick().catch(() => {
3135
+ });
3136
+ }, 1e3);
3137
+ interval.unref();
3138
+ ctx.onDispose(async () => {
3139
+ stopped = true;
3140
+ clearInterval(interval);
3141
+ for (const runner2 of runners.values()) runner2.dispose();
3142
+ while (running) await new Promise((resolve) => setTimeout(resolve, 25));
3143
+ });
3144
+ };
3145
+ if (ctx.config.role === "coordinator") {
3146
+ const global = new GlobalStudio(all);
3147
+ handle(
3148
+ "global.import",
3149
+ z8.object({
3150
+ projectId: z8.uuid(),
3151
+ name: z8.string().trim().min(1).max(200),
3152
+ stagingFileIds: z8.array(z8.uuid()).min(1).max(20)
3153
+ }).strict(),
3154
+ (input, call) => global.importAsset(input, call),
3155
+ ["write"]
3156
+ );
3157
+ handle("global.query", globalQueryInput, (input) => global.query(input));
3158
+ handle(
3159
+ "global.refreshTaskCost",
3160
+ z8.object({ projectId: id, taskId: id }).strict(),
3161
+ async ({ projectId, taskId }) => ({
3162
+ confirmed: await runnerFor(projectId).refreshQueueCost(taskId)
3163
+ }),
3164
+ ["write"]
3165
+ );
3166
+ handle("global.projectNames", z8.object({}).strict(), () => global.projectNames());
3167
+ handle("global.projectSummaries", z8.object({}).strict(), () => global.projectSummaries());
3168
+ handle(
3169
+ "global.renameProject",
3170
+ projectSettingsInput.pick({ projectId: true, expectedRevision: true }).extend({ name: projectSettingsInput.shape.changes.shape.name.unwrap() }).strict(),
3171
+ (input, call) => global.renameProject(input, call),
3172
+ ["write"]
3173
+ );
3174
+ handle(
3175
+ "global.setProjectCover",
3176
+ projectSettingsInput.pick({ projectId: true, expectedRevision: true }).extend({ coverRef: versionRef.nullable() }).strict(),
3177
+ (input, call) => global.setProjectCover(input, call),
3178
+ ["write"]
3179
+ );
3180
+ handle(
3181
+ "global.asset",
3182
+ z8.object({ ref: versionRef }).strict(),
3183
+ (input) => global.asset(input.ref)
3184
+ );
3185
+ handle(
3186
+ "global.favorite",
3187
+ z8.object({ ref: versionRef, enabled: z8.boolean() }).strict(),
3188
+ (input, call) => global.favorite(input, call),
3189
+ ["write"]
3190
+ );
3191
+ handle(
3192
+ "tasks.global",
3193
+ z8.object({
3194
+ limit: z8.number().int().min(1).max(200).default(50),
3195
+ offset: z8.number().int().nonnegative().default(0)
3196
+ }).strict(),
3197
+ (i) => {
3198
+ const items = all.tasks({ limit: i.limit + 1, offset: i.offset });
3199
+ return {
3200
+ items: items.slice(0, i.limit).map(taskProjection),
3201
+ nextOffset: items.length > i.limit ? i.offset + i.limit : null
3202
+ };
3203
+ }
3204
+ );
3205
+ ctx.provide({ role: "coordinator", data: all, global });
3206
+ await start();
3207
+ return;
3208
+ }
3209
+ const initialProject = projectData.safeParse({ name: ctx.project.name });
3210
+ all.ensureProject(
3211
+ scope.id,
3212
+ initialProject.success ? initialProject.data : projectData.parse({ name: scope.id }),
3213
+ {
3214
+ scope,
3215
+ actor: { kind: "local-user", id: "local" },
3216
+ requestKey: `studio-project-${scope.id}`
3217
+ }
3218
+ );
3219
+ const data = all.project(scope.id);
3220
+ const library = ctx.config.libraryInstance ? ctx.plugins.get(ctx.config.libraryInstance) : void 0;
3221
+ const assets = new Assets(data, all, library), projects = new Projects(data);
3222
+ const mediaUses = new MediaUses(
3223
+ data,
3224
+ (definition2) => all.register(definition2),
3225
+ () => assets.assertWritable()
3226
+ );
3227
+ handle("mediaUses.get", getMediaUse, (input) => mediaUses.get(input));
3228
+ handle("mediaUses.bind", bindMediaUse, (input, call) => mediaUses.bind(input, call), ["write"]);
3229
+ const generation = new Generation(assets, api, ctx.config.apiInstance, ctx.config), runner = runnerFor(scope.id);
3230
+ const ads = new Ads(data);
3231
+ const adRevision = z8.object({ expectedRevision: revision }).strict();
3232
+ handle("ads.get", empty, () => ads.get());
3233
+ handle("ads.initialize", empty, (_i, c) => ads.initialize(c), ["write"]);
3234
+ handle(
3235
+ "ads.addClip",
3236
+ adRevision.extend({ title: z8.string().trim().min(1).max(200) }).strict(),
3237
+ (i, c) => ads.addClip(i, c),
3238
+ ["write"]
3239
+ );
3240
+ handle(
3241
+ "ads.updateBrief",
3242
+ adRevision.extend({ brief: adBrief }).strict(),
3243
+ (i, c) => ads.updateBrief(i, c),
3244
+ ["write"]
3245
+ );
3246
+ handle(
3247
+ "ads.updateClip",
3248
+ adRevision.extend({ assetId: id, script: clipScript }).strict(),
3249
+ (i, c) => ads.updateClip(i, c),
3250
+ ["write"]
3251
+ );
3252
+ handle(
3253
+ "ads.reorder",
3254
+ adRevision.extend({ clipIds: z8.array(id).max(200) }).strict(),
3255
+ (i, c) => ads.reorder(i, c),
3256
+ ["write"]
3257
+ );
3258
+ handle(
3259
+ "ads.removeClip",
3260
+ adRevision.extend({ assetId: id }).strict(),
3261
+ (i, c) => ads.removeClip(i, c),
3262
+ ["write"]
3263
+ );
3264
+ handle(
3265
+ "projects.get",
3266
+ z8.object({ projectId: id.optional() }).strict(),
3267
+ (i) => projects.get(i.projectId)
3268
+ );
3269
+ handle("projects.update", projectSettingsInput, (i, c) => projects.update(i, c), ["write"]);
3270
+ for (const [name, status] of [
3271
+ ["projects.archive", "archived"],
3272
+ ["projects.restore", "active"]
3273
+ ])
3274
+ handle(
3275
+ name,
3276
+ z8.object({ projectId: id, expectedRevision: revision }).strict(),
3277
+ (i, c) => projects.update({ ...i, changes: { status } }, c),
3278
+ ["write"]
3279
+ );
3280
+ handle(
3281
+ "assets.list",
3282
+ z8.object({
3283
+ search: z8.string().optional(),
3284
+ kind: z8.string().optional(),
3285
+ tag: z8.string().optional(),
3286
+ metadataEquals: z8.record(z8.string(), z8.any()).optional(),
3287
+ metadataContains: z8.record(z8.string(), z8.any()).optional(),
3288
+ sort: z8.enum(["name", "createdAt", "updatedAt"]).optional(),
3289
+ direction: z8.enum(["asc", "desc"]).optional(),
3290
+ limit: z8.number().int().min(1).max(200).optional(),
3291
+ offset: z8.number().int().nonnegative().optional()
3292
+ }).strict(),
3293
+ async (i) => {
3294
+ const rows = data.list({ typeId: "studio.asset", ...i });
3295
+ return {
3296
+ ...rows,
3297
+ items: await Promise.all(rows.items.map((x) => assets.describe(x.objectId)))
3298
+ };
3299
+ }
3300
+ );
3301
+ handle("assets.get", z8.object({ assetId: id }).strict(), (i) => assets.describe(i.assetId));
3302
+ handle(
3303
+ "assets.import",
3304
+ z8.object({
3305
+ name: z8.string().min(1).max(200),
3306
+ stagingFileIds: z8.array(id).min(1).max(20),
3307
+ kind: z8.enum(["text", "image", "video", "audio", "file"]).optional(),
3308
+ tags: assetData.shape.tags.optional()
3309
+ }).strict(),
3310
+ (i, c) => assets.import(i, c),
3311
+ ["write"]
3312
+ );
3313
+ handle(
3314
+ "assets.importVersion",
3315
+ base.extend({ parentVersionId: id.optional(), stagingFileIds: z8.array(id).min(1).max(20) }).strict(),
3316
+ (i, c) => assets.import(i, c, true),
3317
+ ["write"]
3318
+ );
3319
+ handle(
3320
+ "assets.createText",
3321
+ z8.object({ name: z8.string().min(1).max(200), text: z8.string().max(8e5) }).strict(),
3322
+ (i, c) => assets.text(i, c),
3323
+ ["write"]
3324
+ );
3325
+ handle(
3326
+ "assets.saveText",
3327
+ base.extend({ parentVersionId: id.optional(), text: z8.string().max(8e5) }).strict(),
3328
+ (i, c) => assets.text(i, c),
3329
+ ["write"]
3330
+ );
3331
+ handle(
3332
+ "assets.update",
3333
+ base.extend({
3334
+ changes: assetData.pick({ name: true, tags: true, purpose: true, orderKey: true, lifecycle: true }).extend({
3335
+ tags: assetData.shape.tags.unwrap(),
3336
+ purpose: assetData.shape.purpose.unwrap(),
3337
+ orderKey: assetData.shape.orderKey.unwrap(),
3338
+ lifecycle: assetData.shape.lifecycle.unwrap()
3339
+ }).partial().strict()
3340
+ }).strict(),
3341
+ (i, c) => assets.update(i, c),
3342
+ ["write"]
3343
+ );
3344
+ handle("assets.delete", base, (i, c) => assets.remove(i, c), ["write"]);
3345
+ handle("choices.adopt", base.extend({ versionId: id }).strict(), (i, c) => assets.adopt(i, c), [
3346
+ "write"
3347
+ ]);
3348
+ handle("choices.clear", base, (i, c) => assets.adopt(i, c, true), ["write"]);
3349
+ handle(
3350
+ "assets.importMaterial",
3351
+ z8.object({
3352
+ ref: versionRef,
3353
+ targetProjectId: id.optional(),
3354
+ assetId: id.optional(),
3355
+ expectedRevision: revision.optional(),
3356
+ parentVersionId: id.optional()
3357
+ }).strict(),
3358
+ async (i, c) => {
3359
+ if (i.targetProjectId && i.targetProjectId !== scope.id)
3360
+ reject("SCOPE_MISMATCH", "Target project does not match the active project.");
3361
+ if (!library) reject("LIBRARY_UNAVAILABLE", "Configure a material library before importing.");
3362
+ return assets.copy(() => library.snapshot(i.ref), i.ref, i, c, Boolean(i.assetId));
3363
+ },
3364
+ ["write"]
3365
+ );
3366
+ handle(
3367
+ "assets.collectMaterial",
3368
+ z8.object({ ref: versionRef, name: z8.string().min(1).max(200).optional() }).strict(),
3369
+ (i, c) => assets.collect(i, c),
3370
+ ["write"]
3371
+ );
3372
+ handle("files.resolveLocation", locationInput, (i) => data.location(i.source || i.ref, i.fileId));
3373
+ handle("files.openLocation", locationInput, async (i) => {
3374
+ const loc = await data.location(i.source || i.ref, i.fileId), { execFile } = await import("child_process"), { promisify } = await import("util");
3375
+ if (process.platform === "darwin") await promisify(execFile)("open", ["-R", loc.absolutePath]);
3376
+ else reject("OPEN_LOCATION_UNAVAILABLE", "Open-location is not verified on this platform.");
3377
+ return loc;
3378
+ });
3379
+ handle(
3380
+ "metadata.patch",
3381
+ z8.object({
3382
+ objectId: id,
3383
+ expectedRevision: revision,
3384
+ set: z8.record(z8.string(), z8.any()),
3385
+ remove: z8.array(z8.string())
3386
+ }).strict(),
3387
+ (i, c) => {
3388
+ assets.assertWritable();
3389
+ return data.patchMetadata(i.objectId, i.expectedRevision, i.set, i.remove, c);
3390
+ },
3391
+ ["write"]
3392
+ );
3393
+ handle(
3394
+ "metadata.keys",
3395
+ z8.object({
3396
+ typeId: z8.enum(["studio.asset", "extensions.document"]),
3397
+ limit: z8.number().int().min(1).max(200).optional()
3398
+ }).strict(),
3399
+ (i) => data.metadataKeys(i.typeId, i.limit)
3400
+ );
3401
+ handle("documents.list", empty, () => data.list({ typeId: "extensions.document" }));
3402
+ for (const action of ["create", "update", "delete"])
3403
+ handle(
3404
+ "documents." + action,
3405
+ action === "create" ? z8.object({ name: z8.string(), payload: z8.json() }).strict() : action === "update" ? z8.object({
3406
+ objectId: id,
3407
+ expectedRevision: revision,
3408
+ name: z8.string(),
3409
+ payload: z8.json()
3410
+ }).strict() : z8.object({ objectId: id, expectedRevision: revision }).strict(),
3411
+ (i, c) => {
3412
+ assets.assertWritable();
3413
+ return data.documents(action, i, c);
3414
+ },
3415
+ ["write"]
3416
+ );
3417
+ ctx.handle(generationSubmitOperation, (i, c) => generation.submit(i, bindCall(c)));
3418
+ handle(
3419
+ "tasks.list",
3420
+ z8.object({
3421
+ targetAssetId: id.optional(),
3422
+ limit: z8.number().int().min(1).max(200).optional(),
3423
+ offset: z8.number().int().nonnegative().optional()
3424
+ }).strict(),
3425
+ (i) => {
3426
+ const page = data.list({ typeId: "studio.task", ...i });
3427
+ return { ...page, items: page.items.map(taskProjection) };
3428
+ }
3429
+ );
3430
+ handle("tasks.get", z8.object({ taskId: id }).strict(), (i) => {
3431
+ const t = data.get(i.taskId);
3432
+ if (t.typeId !== "studio.task") reject("TYPE_MISMATCH", "Expected a task.");
3433
+ return taskProjection(t);
3434
+ });
3435
+ handle(
3436
+ "tasks.rename",
3437
+ z8.object({ taskId: id, expectedRevision: revision, name: z8.string().trim().min(1).max(200) }).strict(),
3438
+ (input, call) => data.operation("tasks.rename", input, call, (tx) => {
3439
+ const task = runner.task(input.taskId);
3440
+ return tx.update(task.objectId, input.expectedRevision, { ...task.data, name: input.name });
3441
+ }),
3442
+ ["write"]
3443
+ );
3444
+ handle(
3445
+ "tasks.queryOriginal",
3446
+ z8.object({ taskId: id }).strict(),
3447
+ (i) => runner.queryOriginal(i.taskId)
3448
+ );
3449
+ handle(
3450
+ "tasks.retryDownload",
3451
+ z8.object({ taskId: id }).strict(),
3452
+ (i) => runner.retryDownload(i.taskId),
3453
+ ["write"]
3454
+ );
3455
+ handle("tasks.cancel", z8.object({ taskId: id }).strict(), (i) => runner.cancel(i.taskId), [
3456
+ "write"
3457
+ ]);
3458
+ handle("assets.cleanupImpact", z8.object({ assetId: id }).strict(), (i) => data.impact(i.assetId));
3459
+ handle(
3460
+ "assets.purge",
3461
+ base,
3462
+ (i, c) => {
3463
+ assets.assertWritable();
3464
+ const result2 = data.purge(i.assetId, i.expectedRevision, c);
3465
+ assets.removeUsages(i.assetId, c);
3466
+ return result2;
3467
+ },
3468
+ ["write"],
3469
+ "user"
3470
+ );
3471
+ handle(
3472
+ "assets.batchLocations",
3473
+ z8.object({ refs: z8.array(versionRef).max(100) }).strict(),
3474
+ (i) => Promise.all(
3475
+ i.refs.map(async (ref) => {
3476
+ try {
3477
+ return { ref, ok: true, result: await data.location(ref) };
3478
+ } catch (e) {
3479
+ return { ref, ok: false, error: errorValue(e) };
3480
+ }
3481
+ })
3482
+ )
3483
+ );
3484
+ ctx.provide({
3485
+ role: "project",
3486
+ assets,
3487
+ generation,
3488
+ runner,
3489
+ data,
3490
+ ads,
3491
+ derivedVideos: new DerivedVideos(
3492
+ assets,
3493
+ ctx.plugins.get(ctx.config.dataInstance)
3494
+ )
3495
+ });
3496
+ await start();
3497
+ }
3498
+ export {
3499
+ Config2 as Config,
3500
+ activate as default,
3501
+ dependencies
3502
+ };