@marlinjai/studio-mcp 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/cli.js +259 -58
- package/dist/index.d.ts +380 -9
- package/dist/index.js +261 -58
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -162,8 +162,8 @@ var StudioClient = class {
|
|
|
162
162
|
}
|
|
163
163
|
/** S9: a rendered diagram of a saved workflow's document, so an agent can
|
|
164
164
|
* see the graph it just built. */
|
|
165
|
-
getWorkflowScreenshot(savedId2) {
|
|
166
|
-
return this.json("GET", `/api/v1/workflows/${encodeURIComponent(savedId2)}/screenshot`);
|
|
165
|
+
getWorkflowScreenshot(savedId2, opts = {}) {
|
|
166
|
+
return this.json("GET", `/api/v1/workflows/${encodeURIComponent(savedId2)}/screenshot${projectQuery(opts.projectId)}`);
|
|
167
167
|
}
|
|
168
168
|
// ── Recipes (S10): published workflow + closed post-run effect list ──
|
|
169
169
|
publishWorkflow(savedId2, body) {
|
|
@@ -188,9 +188,10 @@ var StudioClient = class {
|
|
|
188
188
|
describeNodeTypes() {
|
|
189
189
|
return this.json("GET", "/api/v1/node-types");
|
|
190
190
|
}
|
|
191
|
-
/** `savedId` carries a `saved:` prefix; the colon is URL-encoded.
|
|
192
|
-
|
|
193
|
-
|
|
191
|
+
/** `savedId` carries a `saved:` prefix; the colon is URL-encoded. `projectId`
|
|
192
|
+
* names the project the workflow lives in; absent, the Studio's default. */
|
|
193
|
+
getWorkflowDocument(savedId2, opts = {}) {
|
|
194
|
+
return this.json("GET", `/api/v1/workflows/${encodeURIComponent(savedId2)}${projectQuery(opts.projectId)}`);
|
|
194
195
|
}
|
|
195
196
|
/** Create a document by applying `ops` to an empty one (the same code path as apply). */
|
|
196
197
|
createWorkflow(body) {
|
|
@@ -361,6 +362,9 @@ function mimeForFilename(name) {
|
|
|
361
362
|
return "application/octet-stream";
|
|
362
363
|
}
|
|
363
364
|
}
|
|
365
|
+
function projectQuery(projectId) {
|
|
366
|
+
return projectId === void 0 ? "" : `?projectId=${encodeURIComponent(projectId)}`;
|
|
367
|
+
}
|
|
364
368
|
|
|
365
369
|
// src/config.ts
|
|
366
370
|
var DEFAULT_BASE_URL = "https://studio.lumitra.co";
|
|
@@ -436,6 +440,10 @@ var MODEL_TASKS = [
|
|
|
436
440
|
"speech-to-speech",
|
|
437
441
|
"image-to-text",
|
|
438
442
|
"image-to-json",
|
|
443
|
+
// Camera-motion record (2026-09-13): a video in, a prompt-ready description
|
|
444
|
+
// of its camera motion out, measured from the frames by Studio's own code
|
|
445
|
+
// (model `studio/motion-record`, no paid provider).
|
|
446
|
+
"video-to-text",
|
|
439
447
|
// A human decision gate over candidate images (P3 J1). Still a model task,
|
|
440
448
|
// not a new node kind: WorkflowNode already requires task + model, and the
|
|
441
449
|
// ranking call underneath is the same image-to-json vision model
|
|
@@ -480,6 +488,8 @@ var TASK_PORTS = {
|
|
|
480
488
|
// VISION + LANGUAGE.
|
|
481
489
|
"image-to-text": { inputs: ["image", "text"], output: "text" },
|
|
482
490
|
"image-to-json": { inputs: ["image", "text"], output: "json" },
|
|
491
|
+
// Camera-motion record: the wired clip is the whole input, no prompt.
|
|
492
|
+
"video-to-text": { inputs: ["video"], output: "text" },
|
|
483
493
|
// Judge (P3 J1): one candidate wire in (the several results of ONE
|
|
484
494
|
// upstream node-run, not fan-in), the approved candidate out. Both sides
|
|
485
495
|
// are typed 'image' so an approved candidate can feed straight into any
|
|
@@ -543,11 +553,21 @@ var RefBindingSchema = z2.object({
|
|
|
543
553
|
// the "no array-of-refs" gap without widening the binding grammar itself.
|
|
544
554
|
wrap: z2.literal("array").optional()
|
|
545
555
|
});
|
|
546
|
-
var
|
|
556
|
+
var ListItemBindingSchema = z2.discriminatedUnion("kind", [
|
|
547
557
|
LiteralBindingSchema,
|
|
548
558
|
ParamBindingSchema,
|
|
549
559
|
RefBindingSchema
|
|
550
560
|
]);
|
|
561
|
+
var ListBindingSchema = z2.object({
|
|
562
|
+
kind: z2.literal("list"),
|
|
563
|
+
items: z2.array(ListItemBindingSchema)
|
|
564
|
+
});
|
|
565
|
+
var BindingSchema = z2.discriminatedUnion("kind", [
|
|
566
|
+
LiteralBindingSchema,
|
|
567
|
+
ParamBindingSchema,
|
|
568
|
+
RefBindingSchema,
|
|
569
|
+
ListBindingSchema
|
|
570
|
+
]);
|
|
551
571
|
var WorkflowTaskSchema = z2.enum(MODEL_TASKS);
|
|
552
572
|
var RetryPolicySchema = z2.object({
|
|
553
573
|
maxAttempts: z2.number().int().positive(),
|
|
@@ -626,6 +646,7 @@ var AssetIdSchema = z3.string().max(128);
|
|
|
626
646
|
var PortTypeSchema = z3.enum(PORT_TYPES);
|
|
627
647
|
var InputTypeSchema = z3.enum(INPUT_PORT_TYPES);
|
|
628
648
|
var PositionSchema = z3.object({ x: z3.number().finite(), y: z3.number().finite() });
|
|
649
|
+
var CollapsedSchema = z3.boolean();
|
|
629
650
|
var TaskNodeSchema = z3.object({
|
|
630
651
|
id: IdSchema,
|
|
631
652
|
kind: z3.literal("task"),
|
|
@@ -646,13 +667,15 @@ var TaskNodeSchema = z3.object({
|
|
|
646
667
|
bakedInputs: z3.record(KeySchema, LiteralBindingSchema).default({}),
|
|
647
668
|
/** The generation id that feeds downstream. Absent = latest (D4). */
|
|
648
669
|
activeGeneration: IdSchema.optional(),
|
|
649
|
-
label: LabelSchema.optional()
|
|
670
|
+
label: LabelSchema.optional(),
|
|
671
|
+
collapsed: CollapsedSchema.optional()
|
|
650
672
|
});
|
|
651
673
|
var PromptSourceNodeSchema = z3.object({
|
|
652
674
|
id: IdSchema,
|
|
653
675
|
kind: z3.literal("prompt-source"),
|
|
654
676
|
text: PromptTextSchema.default(""),
|
|
655
|
-
label: LabelSchema.optional()
|
|
677
|
+
label: LabelSchema.optional(),
|
|
678
|
+
collapsed: CollapsedSchema.optional()
|
|
656
679
|
});
|
|
657
680
|
var ImageSourceNodeSchema = z3.object({
|
|
658
681
|
id: IdSchema,
|
|
@@ -663,7 +686,18 @@ var ImageSourceNodeSchema = z3.object({
|
|
|
663
686
|
/** The Storage Brain asset id, when known (E18: every image input resolves to
|
|
664
687
|
* an asset id). */
|
|
665
688
|
assetId: AssetIdSchema.optional(),
|
|
666
|
-
label: LabelSchema.optional()
|
|
689
|
+
label: LabelSchema.optional(),
|
|
690
|
+
collapsed: CollapsedSchema.optional()
|
|
691
|
+
});
|
|
692
|
+
var VideoSourceNodeSchema = z3.object({
|
|
693
|
+
id: IdSchema,
|
|
694
|
+
kind: z3.literal("video-source"),
|
|
695
|
+
videoUrl: UrlSchema.default(""),
|
|
696
|
+
/** The Storage Brain asset id, when known, so the clip can be re-signed and
|
|
697
|
+
* attributed the way an image source's asset is. */
|
|
698
|
+
assetId: AssetIdSchema.optional(),
|
|
699
|
+
label: LabelSchema.optional(),
|
|
700
|
+
collapsed: CollapsedSchema.optional()
|
|
667
701
|
});
|
|
668
702
|
var InputNodeSchema = z3.object({
|
|
669
703
|
id: IdSchema,
|
|
@@ -678,12 +712,14 @@ var InputNodeSchema = z3.object({
|
|
|
678
712
|
/** Used when a run omits the input. Never null: absent means no default. */
|
|
679
713
|
default: JsonNonNullValueSchema.optional(),
|
|
680
714
|
label: LabelSchema.optional(),
|
|
681
|
-
description: DescriptionSchema.optional()
|
|
715
|
+
description: DescriptionSchema.optional(),
|
|
716
|
+
collapsed: CollapsedSchema.optional()
|
|
682
717
|
});
|
|
683
718
|
var DocNodeSchema = z3.discriminatedUnion("kind", [
|
|
684
719
|
TaskNodeSchema,
|
|
685
720
|
PromptSourceNodeSchema,
|
|
686
721
|
ImageSourceNodeSchema,
|
|
722
|
+
VideoSourceNodeSchema,
|
|
687
723
|
InputNodeSchema
|
|
688
724
|
]);
|
|
689
725
|
var DocEdgeSchema = z3.object({
|
|
@@ -696,6 +732,18 @@ var DocGroupSchema = z3.object({
|
|
|
696
732
|
label: LabelSchema.optional(),
|
|
697
733
|
nodeIds: z3.array(IdSchema).min(1)
|
|
698
734
|
});
|
|
735
|
+
var EDGE_ROUTINGS = ["curved", "orthogonal"];
|
|
736
|
+
var EdgeRoutingSchema = z3.enum(EDGE_ROUTINGS);
|
|
737
|
+
var WireRouteSchema = z3.object({
|
|
738
|
+
from: PositionSchema,
|
|
739
|
+
to: PositionSchema,
|
|
740
|
+
points: z3.array(PositionSchema).max(64)
|
|
741
|
+
});
|
|
742
|
+
var DocumentViewSchema = z3.object({
|
|
743
|
+
edgeRouting: EdgeRoutingSchema.optional(),
|
|
744
|
+
/** Routed wires from the last Arrange, keyed by `edgeKey`. */
|
|
745
|
+
wires: z3.record(z3.string().max(512), WireRouteSchema).optional()
|
|
746
|
+
});
|
|
699
747
|
var WorkflowDocumentSchema = z3.object({
|
|
700
748
|
schema: z3.number().int().positive(),
|
|
701
749
|
id: IdSchema,
|
|
@@ -703,7 +751,10 @@ var WorkflowDocumentSchema = z3.object({
|
|
|
703
751
|
nodes: z3.array(DocNodeSchema).max(2e3),
|
|
704
752
|
edges: z3.array(DocEdgeSchema).max(8e3),
|
|
705
753
|
layout: z3.record(IdSchema, PositionSchema).default({}),
|
|
706
|
-
groups: z3.array(DocGroupSchema).max(500).default([])
|
|
754
|
+
groups: z3.array(DocGroupSchema).max(500).default([]),
|
|
755
|
+
/** Display preferences; absent on every document written before they
|
|
756
|
+
* existed (additive, so `DOCUMENT_SCHEMA_VERSION` is unchanged). */
|
|
757
|
+
view: DocumentViewSchema.optional()
|
|
707
758
|
});
|
|
708
759
|
function edgeKey(e) {
|
|
709
760
|
return JSON.stringify([e.source, e.target, e.port]);
|
|
@@ -955,7 +1006,7 @@ var MODEL_CATALOG = [
|
|
|
955
1006
|
label: "Kling 2.5 Turbo Pro (image-to-video)",
|
|
956
1007
|
description: "Kuaishou Kling 2.5 Turbo Pro via fal. Cinematic image-to-video; $0.35 base (5s) plus $0.07 per extra second. Default image-to-video.",
|
|
957
1008
|
cost: { usd: 0.35 },
|
|
958
|
-
capabilities: { needsImageInput: true, maxInputImages: 1 },
|
|
1009
|
+
capabilities: { needsImageInput: true, maxInputImages: 1, acceptsEndFrame: true },
|
|
959
1010
|
isDefault: true
|
|
960
1011
|
},
|
|
961
1012
|
{
|
|
@@ -976,7 +1027,7 @@ var MODEL_CATALOG = [
|
|
|
976
1027
|
label: "Seedance 2.5 (image-to-video)",
|
|
977
1028
|
description: "ByteDance Seedance 2.5 via fal. Accepts a start frame AND an end frame (end_image_url), selectable duration 4-30s, up to 1080p, with synchronised audio (generate_audio, on by default). Token-billed: (w*h*s*24)/1024 at $0.0214 per 1000 tokens, so roughly $1.89 for 4s at 720p and $4.16 at 1080p. CAUTION: ByteDance partner moderation on 2.5 rejected even fal's own robot test image with content_policy_violation (observed 2026-08-21), so it may refuse input that 2.0 accepts. Try 2.0 first for photographic input.",
|
|
978
1029
|
cost: { usd: 1.89 },
|
|
979
|
-
capabilities: { needsImageInput: true, maxInputImages: 1 }
|
|
1030
|
+
capabilities: { needsImageInput: true, maxInputImages: 1, acceptsEndFrame: true }
|
|
980
1031
|
},
|
|
981
1032
|
{
|
|
982
1033
|
id: "fal/seedance-2-0-image-to-video",
|
|
@@ -986,7 +1037,7 @@ var MODEL_CATALOG = [
|
|
|
986
1037
|
label: "Seedance 2.0 (image-to-video)",
|
|
987
1038
|
description: "ByteDance Seedance 2.0 via fal. Start frame plus optional end frame (end_image_url), duration 4-15s, up to 4k, and it accepts seven explicit aspect ratios where 2.5 is fixed to auto. Not simply superseded by 2.5: 2.0 generated from photographic input that 2.5 refused on content policy (observed 2026-08-21), and it is the only one offering 4k. Token-billed identically to 2.5.",
|
|
988
1039
|
cost: { usd: 1.89 },
|
|
989
|
-
capabilities: { needsImageInput: true, maxInputImages: 1 }
|
|
1040
|
+
capabilities: { needsImageInput: true, maxInputImages: 1, acceptsEndFrame: true }
|
|
990
1041
|
},
|
|
991
1042
|
// ── FLUX.3 (Black Forest Labs) ───────────────────────────────────────────
|
|
992
1043
|
// One model, three tiers of the same three input shapes:
|
|
@@ -1021,7 +1072,7 @@ var MODEL_CATALOG = [
|
|
|
1021
1072
|
label: "FLUX.3 draft (first + last frame)",
|
|
1022
1073
|
description: "Black Forest Labs FLUX.3 draft tier via fal. Start AND end keyframe, duration 5-20s, fixed 720p, audio ON by default. $0.06 per second of draft, roughly a third of Kling 2.5 for the same two-keyframe shot. Note it names the start frame start_image_url, not image_url. Minimum 5s, so a 4s cut means rendering 5s and trimming. PREVIEW QUALITY: enhance before shipping.",
|
|
1023
1074
|
cost: { usd: 0.3 },
|
|
1024
|
-
capabilities: { needsImageInput: true, maxInputImages: 2, emitsDraftCache: true }
|
|
1075
|
+
capabilities: { needsImageInput: true, maxInputImages: 2, emitsDraftCache: true, acceptsEndFrame: true }
|
|
1025
1076
|
},
|
|
1026
1077
|
{
|
|
1027
1078
|
id: "fal/flux-3-keyframes-to-video-draft",
|
|
@@ -1051,7 +1102,7 @@ var MODEL_CATALOG = [
|
|
|
1051
1102
|
label: "FLUX.3 (first + last frame)",
|
|
1052
1103
|
description: "Black Forest Labs FLUX.3 direct tier via fal. Start AND end keyframe (start_image_url / end_image_url), duration 5-20s, resolution 720p (default) or 1080p, audio ON by default. $0.17 per second at 720p, $0.29 at 1080p; the catalog usd anchors 5s at 720p. Minimum 5s, so a 4s cut means rendering 5s and trimming. Finished quality, returns the seed.",
|
|
1053
1104
|
cost: { usd: 0.85 },
|
|
1054
|
-
capabilities: { needsImageInput: true, maxInputImages: 2 }
|
|
1105
|
+
capabilities: { needsImageInput: true, maxInputImages: 2, acceptsEndFrame: true }
|
|
1055
1106
|
},
|
|
1056
1107
|
{
|
|
1057
1108
|
id: "fal/flux-3-keyframes-to-video",
|
|
@@ -1121,6 +1172,19 @@ var MODEL_CATALOG = [
|
|
|
1121
1172
|
capabilities: { needsImageInput: true, maxInputImages: 1 },
|
|
1122
1173
|
isDefault: true
|
|
1123
1174
|
},
|
|
1175
|
+
// Camera-motion record (2026-09-13): Studio's own analysis, no paid provider.
|
|
1176
|
+
// A video in, a prompt-ready description of its camera motion out
|
|
1177
|
+
// (job.output.text), with the measured record beside it (job.output.json).
|
|
1178
|
+
{
|
|
1179
|
+
id: "studio/motion-record",
|
|
1180
|
+
provider: "studio",
|
|
1181
|
+
modality: "text",
|
|
1182
|
+
task: "video-to-text",
|
|
1183
|
+
label: "Motion record (Studio)",
|
|
1184
|
+
description: "Measures a clip's camera motion from its frames (phase correlation every 0.1 s: pan, tilt, dolly, steadiness, subject motion) and writes a deterministic description to job.output.text plus the full record to job.output.json. Runs in Studio, costs nothing. Default video-to-text.",
|
|
1185
|
+
cost: { usd: 0 },
|
|
1186
|
+
isDefault: true
|
|
1187
|
+
},
|
|
1124
1188
|
{
|
|
1125
1189
|
id: "fal/moondream-next-vqa",
|
|
1126
1190
|
provider: "fal",
|
|
@@ -1312,6 +1376,9 @@ function portBinding(task, port) {
|
|
|
1312
1376
|
if (port === "mesh3d" && (task === "remesh" || task === "texture")) {
|
|
1313
1377
|
return { key: "parentAssetId", path: "assetId" };
|
|
1314
1378
|
}
|
|
1379
|
+
if (port === "video" && task === "video-to-text") {
|
|
1380
|
+
return { key: "inputVideoUrl", path: "url" };
|
|
1381
|
+
}
|
|
1315
1382
|
if (port === "video" && task === "video-to-video") {
|
|
1316
1383
|
return { key: "inputVideoUrl", path: "url" };
|
|
1317
1384
|
}
|
|
@@ -1342,6 +1409,30 @@ function canConnect(sourceOut, targetTask, targetPort) {
|
|
|
1342
1409
|
if (sourceOut !== targetPort) return false;
|
|
1343
1410
|
return portBinding(targetTask, targetPort) !== null;
|
|
1344
1411
|
}
|
|
1412
|
+
function portCardinality(task, port) {
|
|
1413
|
+
if (portBinding(task, port) === null) return "none";
|
|
1414
|
+
if (port === "image" && task === "image-edit") return "many";
|
|
1415
|
+
if (port === "image" && task === "image-to-video") return "two";
|
|
1416
|
+
return "one";
|
|
1417
|
+
}
|
|
1418
|
+
function cardinalityLimit(cardinality) {
|
|
1419
|
+
switch (cardinality) {
|
|
1420
|
+
case "none":
|
|
1421
|
+
return 0;
|
|
1422
|
+
case "one":
|
|
1423
|
+
return 1;
|
|
1424
|
+
case "two":
|
|
1425
|
+
return 2;
|
|
1426
|
+
case "many":
|
|
1427
|
+
return Number.POSITIVE_INFINITY;
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
function orderedPortKey(task, port, ordinal) {
|
|
1431
|
+
const canonical = portBinding(task, port);
|
|
1432
|
+
if (!canonical) return null;
|
|
1433
|
+
if (portCardinality(task, port) === "two" && ordinal === 1) return "endImageUrl";
|
|
1434
|
+
return canonical.key;
|
|
1435
|
+
}
|
|
1345
1436
|
function sourcePortOf(node) {
|
|
1346
1437
|
switch (node.kind) {
|
|
1347
1438
|
case "task":
|
|
@@ -1350,10 +1441,28 @@ function sourcePortOf(node) {
|
|
|
1350
1441
|
return "text";
|
|
1351
1442
|
case "image-source":
|
|
1352
1443
|
return "image";
|
|
1444
|
+
case "video-source":
|
|
1445
|
+
return "video";
|
|
1353
1446
|
case "input":
|
|
1354
1447
|
return node.type;
|
|
1355
1448
|
}
|
|
1356
1449
|
}
|
|
1450
|
+
function targetCardinality(node, port) {
|
|
1451
|
+
if (node.kind !== "task") return "none";
|
|
1452
|
+
return portCardinality(node.task, port);
|
|
1453
|
+
}
|
|
1454
|
+
function slotRefusal(node, port, wired) {
|
|
1455
|
+
const cardinality = targetCardinality(node, port);
|
|
1456
|
+
let limit = cardinalityLimit(cardinality);
|
|
1457
|
+
if (cardinality === "many" && node.kind === "task" && node.model) {
|
|
1458
|
+
const cap = tryGetModel(node.model)?.capabilities?.maxInputImages;
|
|
1459
|
+
if (typeof cap === "number" && cap > 0) limit = Math.min(limit, cap);
|
|
1460
|
+
}
|
|
1461
|
+
if (wired < limit) return null;
|
|
1462
|
+
if (cardinality === "one") return `node "${node.id}" accepts one ${port} wire, got more`;
|
|
1463
|
+
if (cardinality === "two") return `node "${node.id}" takes two ${port} wires (start frame, end frame), got more`;
|
|
1464
|
+
return `node "${node.id}" (${node.kind === "task" ? node.model : node.kind}) takes at most ${limit} ${port} wires`;
|
|
1465
|
+
}
|
|
1357
1466
|
function wireLegality(source, target, port) {
|
|
1358
1467
|
if (source.id === target.id) return { ok: false, code: "cycle", message: `node "${source.id}" cannot wire into itself` };
|
|
1359
1468
|
const produced = sourcePortOf(source);
|
|
@@ -1420,7 +1529,7 @@ function validateDocument(doc) {
|
|
|
1420
1529
|
inputNames.add(node.name);
|
|
1421
1530
|
}
|
|
1422
1531
|
const edgeKeys = /* @__PURE__ */ new Set();
|
|
1423
|
-
const slots = /* @__PURE__ */ new
|
|
1532
|
+
const slots = /* @__PURE__ */ new Map();
|
|
1424
1533
|
for (const e of doc.edges) {
|
|
1425
1534
|
const key = edgeKey(e);
|
|
1426
1535
|
if (edgeKeys.has(key)) return { ok: false, code: "conflict", message: `duplicate edge ${key}` };
|
|
@@ -1432,14 +1541,24 @@ function validateDocument(doc) {
|
|
|
1432
1541
|
const legal = wireLegality(source, target, e.port);
|
|
1433
1542
|
if (!legal.ok) return legal;
|
|
1434
1543
|
const slot = slotKey(e.target, e.port);
|
|
1435
|
-
|
|
1544
|
+
const wired = slots.get(slot) ?? 0;
|
|
1545
|
+
const refusal = slotRefusal(target, e.port, wired);
|
|
1546
|
+
if (refusal) return { ok: false, code: "cardinality", message: refusal };
|
|
1547
|
+
slots.set(slot, wired + 1);
|
|
1548
|
+
}
|
|
1549
|
+
for (const [slot, wired] of slots) {
|
|
1550
|
+
if (wired < 2) continue;
|
|
1551
|
+
const [targetId, port] = JSON.parse(slot);
|
|
1552
|
+
const target = byId.get(targetId);
|
|
1553
|
+
if (!target || target.kind !== "task" || port !== "image" || portCardinality(target.task, port) !== "two" || !target.model) continue;
|
|
1554
|
+
const model = tryGetModel(target.model);
|
|
1555
|
+
if (model && !model.capabilities?.acceptsEndFrame) {
|
|
1436
1556
|
return {
|
|
1437
1557
|
ok: false,
|
|
1438
|
-
code: "
|
|
1439
|
-
message: `
|
|
1558
|
+
code: "invalid_port",
|
|
1559
|
+
message: `"${target.id}" (${target.model}) takes no end frame, so its second image wire has nowhere to bind. Pick a model with start and end frame support (Kling 2.5, Seedance 2.x, FLUX.3 first-last) or disconnect it.`
|
|
1440
1560
|
};
|
|
1441
1561
|
}
|
|
1442
|
-
slots.add(slot);
|
|
1443
1562
|
}
|
|
1444
1563
|
if (documentTopologicalOrder(doc) === null) {
|
|
1445
1564
|
return { ok: false, code: "cycle", message: `document "${doc.id}" has a cycle in its wires` };
|
|
@@ -1472,16 +1591,25 @@ var TaskNodePropsSchema = z4.object({
|
|
|
1472
1591
|
task: z4.enum(MODEL_TASKS),
|
|
1473
1592
|
model: ModelIdSchema,
|
|
1474
1593
|
prompt: PromptTextSchema,
|
|
1475
|
-
label: LabelSchema.nullable()
|
|
1594
|
+
label: LabelSchema.nullable(),
|
|
1595
|
+
collapsed: CollapsedSchema.nullable()
|
|
1476
1596
|
}).partial().strict();
|
|
1477
1597
|
var PromptSourcePropsSchema = z4.object({
|
|
1478
1598
|
text: PromptTextSchema,
|
|
1479
|
-
label: LabelSchema.nullable()
|
|
1599
|
+
label: LabelSchema.nullable(),
|
|
1600
|
+
collapsed: CollapsedSchema.nullable()
|
|
1480
1601
|
}).partial().strict();
|
|
1481
1602
|
var ImageSourcePropsSchema = z4.object({
|
|
1482
1603
|
imageUrl: UrlSchema,
|
|
1483
1604
|
assetId: AssetIdSchema.nullable(),
|
|
1484
|
-
label: LabelSchema.nullable()
|
|
1605
|
+
label: LabelSchema.nullable(),
|
|
1606
|
+
collapsed: CollapsedSchema.nullable()
|
|
1607
|
+
}).partial().strict();
|
|
1608
|
+
var VideoSourcePropsSchema = z4.object({
|
|
1609
|
+
videoUrl: UrlSchema,
|
|
1610
|
+
assetId: AssetIdSchema.nullable(),
|
|
1611
|
+
label: LabelSchema.nullable(),
|
|
1612
|
+
collapsed: CollapsedSchema.nullable()
|
|
1485
1613
|
}).partial().strict();
|
|
1486
1614
|
var InputNodePropsSchema = z4.object({
|
|
1487
1615
|
name: InputNameSchema,
|
|
@@ -1490,14 +1618,19 @@ var InputNodePropsSchema = z4.object({
|
|
|
1490
1618
|
cardinality: z4.enum(["one", "many"]).nullable(),
|
|
1491
1619
|
default: JsonNonNullValueSchema.nullable(),
|
|
1492
1620
|
label: LabelSchema.nullable(),
|
|
1493
|
-
description: DescriptionSchema.nullable()
|
|
1621
|
+
description: DescriptionSchema.nullable(),
|
|
1622
|
+
collapsed: CollapsedSchema.nullable()
|
|
1494
1623
|
}).partial().strict();
|
|
1495
1624
|
var GroupPropsSchema = z4.object({
|
|
1496
1625
|
label: LabelSchema.nullable(),
|
|
1497
1626
|
nodeIds: z4.array(IdSchema).min(1)
|
|
1498
1627
|
}).partial().strict();
|
|
1499
1628
|
var DocumentPropsSchema = z4.object({
|
|
1500
|
-
label: LabelSchema
|
|
1629
|
+
label: LabelSchema,
|
|
1630
|
+
/** Display preferences (`view.edgeRouting`); null clears the whole
|
|
1631
|
+
* object. The object is replaced, not merged: pass every field you want
|
|
1632
|
+
* kept. */
|
|
1633
|
+
view: DocumentViewSchema.strict().nullable()
|
|
1501
1634
|
}).partial().strict();
|
|
1502
1635
|
var AddNodeOpSchema = z4.object({
|
|
1503
1636
|
type: z4.literal("addNode"),
|
|
@@ -1515,6 +1648,7 @@ var SetNodePropsOpSchema = z4.discriminatedUnion("kind", [
|
|
|
1515
1648
|
z4.object({ type: z4.literal("setNodeProps"), kind: z4.literal("task"), id: IdSchema, props: TaskNodePropsSchema }),
|
|
1516
1649
|
z4.object({ type: z4.literal("setNodeProps"), kind: z4.literal("prompt-source"), id: IdSchema, props: PromptSourcePropsSchema }),
|
|
1517
1650
|
z4.object({ type: z4.literal("setNodeProps"), kind: z4.literal("image-source"), id: IdSchema, props: ImageSourcePropsSchema }),
|
|
1651
|
+
z4.object({ type: z4.literal("setNodeProps"), kind: z4.literal("video-source"), id: IdSchema, props: VideoSourcePropsSchema }),
|
|
1518
1652
|
z4.object({ type: z4.literal("setNodeProps"), kind: z4.literal("input"), id: IdSchema, props: InputNodePropsSchema })
|
|
1519
1653
|
]);
|
|
1520
1654
|
var SetNodeSettingOpSchema = z4.object({
|
|
@@ -1546,8 +1680,8 @@ var DisconnectOpSchema = z4.object({
|
|
|
1546
1680
|
});
|
|
1547
1681
|
var ExposeAsInputOpSchema = z4.object({
|
|
1548
1682
|
type: z4.literal("exposeAsInput"),
|
|
1549
|
-
/** A prompt-source or
|
|
1550
|
-
* (same id, same produced port type), so its wires survive. */
|
|
1683
|
+
/** A prompt-source, image-source or video-source node; it becomes an input
|
|
1684
|
+
* node IN PLACE (same id, same produced port type), so its wires survive. */
|
|
1551
1685
|
id: IdSchema,
|
|
1552
1686
|
name: InputNameSchema,
|
|
1553
1687
|
required: z4.boolean().optional(),
|
|
@@ -1560,8 +1694,9 @@ var ExposeAsInputOpSchema = z4.object({
|
|
|
1560
1694
|
});
|
|
1561
1695
|
var FreezeInputOpSchema = z4.object({
|
|
1562
1696
|
type: z4.literal("freezeInput"),
|
|
1563
|
-
/** A text or
|
|
1564
|
-
* PLACE carrying `value` (a chosen run's
|
|
1697
|
+
/** A text, image or video input node; it becomes a prompt-source /
|
|
1698
|
+
* image-source / video-source IN PLACE carrying `value` (a chosen run's
|
|
1699
|
+
* resolved value, or any literal). */
|
|
1565
1700
|
id: IdSchema,
|
|
1566
1701
|
value: PromptTextSchema,
|
|
1567
1702
|
assetId: AssetIdSchema.optional(),
|
|
@@ -1770,6 +1905,8 @@ var TASK_TO_KIND = {
|
|
|
1770
1905
|
// vision (text/json output, no binary asset)
|
|
1771
1906
|
"image-to-text": "analyze_image",
|
|
1772
1907
|
"image-to-json": "analyze_image",
|
|
1908
|
+
// camera-motion record (text + json output, no binary asset), Studio's own worker
|
|
1909
|
+
"video-to-text": "analyze_video",
|
|
1773
1910
|
// utility (new Image asset off the source)
|
|
1774
1911
|
upscale: "transform_image",
|
|
1775
1912
|
"background-remove": "transform_image",
|
|
@@ -1793,7 +1930,7 @@ function taskExecution(task) {
|
|
|
1793
1930
|
function paramTypeForInput(type) {
|
|
1794
1931
|
return type === "json" ? "json" : "string";
|
|
1795
1932
|
}
|
|
1796
|
-
function
|
|
1933
|
+
function isUsableSourceUrl(url) {
|
|
1797
1934
|
return url.length > 0 && !url.startsWith("data:");
|
|
1798
1935
|
}
|
|
1799
1936
|
function documentToDefinition(doc, opts = {}) {
|
|
@@ -1851,22 +1988,50 @@ function documentToDefinition(doc, opts = {}) {
|
|
|
1851
1988
|
issues.push({ code: "prompt_required", nodeId: node.id, message: `Node "${node.id}" (${task}) needs a prompt.` });
|
|
1852
1989
|
}
|
|
1853
1990
|
}
|
|
1991
|
+
const byPort = /* @__PURE__ */ new Map();
|
|
1854
1992
|
for (const e of wires) {
|
|
1855
1993
|
if (e.port === "text") continue;
|
|
1856
|
-
const
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1994
|
+
const list = byPort.get(e.port);
|
|
1995
|
+
if (list) list.push(e);
|
|
1996
|
+
else byPort.set(e.port, [e]);
|
|
1997
|
+
}
|
|
1998
|
+
for (const [port, portWires] of byPort) {
|
|
1999
|
+
const b = portBinding(task, port);
|
|
2000
|
+
const cardinality = portCardinality(task, port);
|
|
2001
|
+
const items = [];
|
|
2002
|
+
let uploading = false;
|
|
2003
|
+
for (const [ordinal, e] of portWires.entries()) {
|
|
2004
|
+
const source = byId.get(e.source);
|
|
2005
|
+
const single = cardinality !== "many" || portWires.length === 1;
|
|
2006
|
+
const key = orderedPortKey(task, port, ordinal) ?? b.key;
|
|
2007
|
+
const wrap = single && b.wrap === "array";
|
|
2008
|
+
let item = null;
|
|
2009
|
+
if (source.kind === "image-source") {
|
|
2010
|
+
const url = source.imageUrl.trim();
|
|
2011
|
+
if (!isUsableSourceUrl(url)) {
|
|
2012
|
+
issues.push({ code: "source_uploading", nodeId: node.id, message: `Image source for "${node.id}" is still uploading; wait for it to finish.` });
|
|
2013
|
+
uploading = true;
|
|
2014
|
+
continue;
|
|
2015
|
+
}
|
|
2016
|
+
item = { kind: "literal", value: wrap ? [url] : url };
|
|
2017
|
+
} else if (source.kind === "video-source") {
|
|
2018
|
+
const clip = source.videoUrl.trim();
|
|
2019
|
+
if (!isUsableSourceUrl(clip)) {
|
|
2020
|
+
issues.push({ code: "source_uploading", nodeId: node.id, message: `Video source for "${node.id}" is still uploading; wait for it to finish.` });
|
|
2021
|
+
uploading = true;
|
|
2022
|
+
continue;
|
|
2023
|
+
}
|
|
2024
|
+
item = { kind: "literal", value: wrap ? [clip] : clip };
|
|
2025
|
+
} else if (source.kind === "input") {
|
|
2026
|
+
item = wrap && source.cardinality !== "many" ? { kind: "param", name: source.name, wrap: "array" } : { kind: "param", name: source.name };
|
|
2027
|
+
} else if (source.kind === "task") {
|
|
2028
|
+
item = wrap ? { kind: "ref", node: source.id, path: b.path, wrap: "array" } : { kind: "ref", node: source.id, path: b.path };
|
|
1863
2029
|
}
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
} else if (source.kind === "task") {
|
|
1868
|
-
inputs[b.key] = b.wrap === "array" ? { kind: "ref", node: source.id, path: b.path, wrap: "array" } : { kind: "ref", node: source.id, path: b.path };
|
|
2030
|
+
if (!item) continue;
|
|
2031
|
+
if (single) inputs[key] = item;
|
|
2032
|
+
else items.push(item);
|
|
1869
2033
|
}
|
|
2034
|
+
if (!uploading && items.length > 0) inputs[b.key] = { kind: "list", items };
|
|
1870
2035
|
}
|
|
1871
2036
|
for (const [key, binding] of Object.entries(node.bakedInputs)) if (!(key in inputs)) inputs[key] = binding;
|
|
1872
2037
|
if (Object.keys(inputs).length === 0 && !promptMissing) {
|
|
@@ -2071,7 +2236,7 @@ async function pollUntil(what, fetchState, isDone, opts = {}) {
|
|
|
2071
2236
|
}
|
|
2072
2237
|
}
|
|
2073
2238
|
var TERMINAL_JOB_STATUSES = /* @__PURE__ */ new Set(["succeeded", "failed", "canceled"]);
|
|
2074
|
-
var TERMINAL_RUN_STATUSES = /* @__PURE__ */ new Set(["succeeded", "failed", "partial"]);
|
|
2239
|
+
var TERMINAL_RUN_STATUSES = /* @__PURE__ */ new Set(["succeeded", "failed", "partial", "cancelled"]);
|
|
2075
2240
|
function isJobTerminal(res) {
|
|
2076
2241
|
return TERMINAL_JOB_STATUSES.has(res.job.status);
|
|
2077
2242
|
}
|
|
@@ -2104,8 +2269,24 @@ function runFailureSummary(res) {
|
|
|
2104
2269
|
var DEFAULT_SWAP_EDIT_MODEL = "fal/nano-banana-2-edit";
|
|
2105
2270
|
var DEFAULT_SWAP_VIDEO_MODEL = "fal/seedance-pro-image-to-video";
|
|
2106
2271
|
var KEYFRAME_SWAP_VIDEO_MODEL = "fal/kling-v2-5-image-to-video";
|
|
2107
|
-
var
|
|
2108
|
-
|
|
2272
|
+
var SWAP_SCOPES = ["identity", "full_body"];
|
|
2273
|
+
function composeSwapPrompt(options = {}) {
|
|
2274
|
+
const scope = options.scope ?? "identity";
|
|
2275
|
+
const taken = scope === "full_body" ? "take the face, head, hair and body proportions from the second image" : "take the face, head and hair from the second image";
|
|
2276
|
+
const kept = scope === "full_body" ? "Keep the first image's clothing, pose, expression, gaze and hand placement." : "Keep the first image's body, clothing, pose, expression, gaze and hand placement.";
|
|
2277
|
+
const sentences = [
|
|
2278
|
+
`Replace the person in the first image with the person in the second image: ${taken}.`,
|
|
2279
|
+
kept,
|
|
2280
|
+
"Match the first image's lighting, focus and colour, and keep its framing and background."
|
|
2281
|
+
];
|
|
2282
|
+
for (const raw of options.preserve ?? []) {
|
|
2283
|
+
const constraint = raw.trim().replace(/[.\s]+$/, "");
|
|
2284
|
+
if (constraint) sentences.push(`${constraint.charAt(0).toUpperCase()}${constraint.slice(1)}.`);
|
|
2285
|
+
}
|
|
2286
|
+
return sentences.join(" ");
|
|
2287
|
+
}
|
|
2288
|
+
var DEFAULT_SWAP_PROMPT = composeSwapPrompt();
|
|
2289
|
+
var DEFAULT_MOTION_PROMPT = "Continue the scene as photographed: keep the camera position and framing of the still, move the person naturally and continuously, and keep the lighting consistent. Add no camera movement.";
|
|
2109
2290
|
var SWAP_EDIT_NODE_ID = "edit";
|
|
2110
2291
|
var SWAP_VIDEO_NODE_ID = "video";
|
|
2111
2292
|
function defaultVideoModelFor(input) {
|
|
@@ -2125,7 +2306,7 @@ function buildCharacterSwapDefinition(input) {
|
|
|
2125
2306
|
task: "image-edit",
|
|
2126
2307
|
model: input.editModel ?? DEFAULT_SWAP_EDIT_MODEL,
|
|
2127
2308
|
inputs: {
|
|
2128
|
-
prompt: { kind: "literal", value: input.swapPrompt ??
|
|
2309
|
+
prompt: { kind: "literal", value: input.swapPrompt ?? composeSwapPrompt({ scope: input.scope, preserve: input.preserve }) },
|
|
2129
2310
|
inputImages: { kind: "literal", value: [input.sceneFrameUrl, input.characterImageUrl] },
|
|
2130
2311
|
aspectRatio: { kind: "literal", value: input.aspectRatio ?? "16:9" }
|
|
2131
2312
|
}
|
|
@@ -2234,6 +2415,9 @@ function summarizeNode(node, wires) {
|
|
|
2234
2415
|
case "image-source":
|
|
2235
2416
|
base.imageUrl = clipText(node.imageUrl);
|
|
2236
2417
|
break;
|
|
2418
|
+
case "video-source":
|
|
2419
|
+
base.videoUrl = clipText(node.videoUrl);
|
|
2420
|
+
break;
|
|
2237
2421
|
case "input":
|
|
2238
2422
|
base.name = node.name;
|
|
2239
2423
|
base.type = node.type;
|
|
@@ -2404,11 +2588,12 @@ var studioWorkflowGet = defineTool({
|
|
|
2404
2588
|
description: "Read a saved workflow. By default returns a summary: { savedId, revision, label, readOnly, nodes: [{ id, kind, label?, task?, model?, prompt? (first 80 chars, plus promptLength), imageUrl?, name?, type?, required?, wires: { in, out } }], edgeCount, groupCount, projection: { ok, issues } } where revision is the baseRevision for studio_workflow_apply and projection says whether the workflow can run and why not. A workflow the canvas cannot represent comes back as { readOnly: true, definitionNodeCount, projection, hint } with no nodes: it is readable and runnable (studio_run_workflow with its savedId) but not patchable. needsLossyAcknowledgement: true means its first patch must carry acknowledgeLossy: true. full: true returns the whole response instead; use it only when you need every field.",
|
|
2405
2589
|
inputSchema: {
|
|
2406
2590
|
savedId,
|
|
2591
|
+
projectId: z7.string().min(1).optional().describe("Project id from studio_list_projects the workflow lives in; omit for the default project."),
|
|
2407
2592
|
full: z7.boolean().default(false).describe("Return the whole response (the full document, or the stored definition of a read-only workflow) instead of the summary.")
|
|
2408
2593
|
},
|
|
2409
2594
|
annotations: { readOnlyHint: true },
|
|
2410
2595
|
handler: async (args, ctx) => {
|
|
2411
|
-
const res = await ctx.client.getWorkflowDocument(args.savedId);
|
|
2596
|
+
const res = await ctx.client.getWorkflowDocument(args.savedId, args.projectId !== void 0 ? { projectId: args.projectId } : {});
|
|
2412
2597
|
if (args.full) return res;
|
|
2413
2598
|
if (res.readOnly === true || res.document === null) {
|
|
2414
2599
|
const definition = isRecord(res.definition) ? res.definition : null;
|
|
@@ -2481,6 +2666,7 @@ var studioWorkflowApply = defineTool({
|
|
|
2481
2666
|
description: 'Change a saved workflow atomically: the ops are applied all-or-nothing on top of baseRevision (the revision from studio_workflow_get, studio_workflow_create or your last apply). Success returns { ok: true, savedId, revision, appliedCount, inverseCount, affectedNodes (summaries of the nodes the patch touched, removed ones as { id, removed: true }), projection: { ok, issues }, idempotencyKey, replayed }. Every refusal comes back as { ok: false, code, hint } you can act on rather than an error: "stale_revision" (re-read, rebase your ops, apply again on the revision returned), "invalid_patch" (error.opIndex names the offending op), "forbidden" (opIndex plus the missingScope your credential needs), "not_editable" (this workflow is readable and runnable but cannot be patched), "lossy_import" (read notes, then resend with acknowledgeLossy: true and a NEW idempotencyKey), "idempotency_mismatch" (that key belongs to a different patch: use a fresh one), and "network_error" / "server_error" which you retry with the SAME idempotencyKey so the patch can never apply twice.',
|
|
2482
2667
|
inputSchema: {
|
|
2483
2668
|
savedId,
|
|
2669
|
+
projectId: z7.string().min(1).optional().describe("Project id from studio_list_projects the workflow lives in; omit for the default project."),
|
|
2484
2670
|
baseRevision: PatchSchema.shape.baseRevision.describe("The revision the ops were written against. The Studio refuses a stale one with stale_revision."),
|
|
2485
2671
|
ops: opsSchema,
|
|
2486
2672
|
idempotencyKey: PatchSchema.shape.idempotencyKey.describe(
|
|
@@ -2495,6 +2681,7 @@ var studioWorkflowApply = defineTool({
|
|
|
2495
2681
|
let res;
|
|
2496
2682
|
try {
|
|
2497
2683
|
res = await ctx.client.applyWorkflowPatch(args.savedId, {
|
|
2684
|
+
...args.projectId !== void 0 ? { projectId: args.projectId } : {},
|
|
2498
2685
|
baseRevision: args.baseRevision,
|
|
2499
2686
|
ops: args.ops,
|
|
2500
2687
|
idempotencyKey,
|
|
@@ -2529,16 +2716,18 @@ var studioWorkflowValidate = defineTool({
|
|
|
2529
2716
|
inputSchema: {
|
|
2530
2717
|
document: WorkflowDocumentSchema.optional().describe("The document to validate. Either this or savedId."),
|
|
2531
2718
|
savedId: savedId.optional().describe("Fetch this saved workflow first and validate against its current document. Either this or document, not both."),
|
|
2719
|
+
projectId: z7.string().min(1).optional().describe("Project id from studio_list_projects the saved workflow lives in (only with savedId); omit for the default project."),
|
|
2532
2720
|
ops: PatchSchema.shape.ops.optional().describe("Ops to apply to the document before projecting it (the same vocabulary and limits as studio_workflow_apply)."),
|
|
2533
2721
|
params: paramsSchema.optional()
|
|
2534
2722
|
},
|
|
2535
2723
|
annotations: { readOnlyHint: true },
|
|
2536
2724
|
handler: async (args, ctx) => {
|
|
2537
2725
|
if (args.document && args.savedId) throw new ToolInputError("pass either `document` or `savedId`, not both");
|
|
2726
|
+
if (args.projectId !== void 0 && !args.savedId) throw new ToolInputError("`projectId` only applies when fetching by `savedId`");
|
|
2538
2727
|
let document = args.document;
|
|
2539
2728
|
if (!document) {
|
|
2540
2729
|
if (!args.savedId) throw new ToolInputError("provide a `document` or a `savedId` to validate");
|
|
2541
|
-
const saved = await ctx.client.getWorkflowDocument(args.savedId);
|
|
2730
|
+
const saved = await ctx.client.getWorkflowDocument(args.savedId, args.projectId !== void 0 ? { projectId: args.projectId } : {});
|
|
2542
2731
|
if (saved.readOnly === true || saved.document === null) {
|
|
2543
2732
|
throw new ToolInputError(
|
|
2544
2733
|
`workflow ${args.savedId} has no document to validate: the canvas cannot represent its stored definition (studio_workflow_get shows readOnly: true and why). It is still runnable with studio_run_workflow.`
|
|
@@ -2557,11 +2746,12 @@ var studioWorkflowScreenshot = defineTool({
|
|
|
2557
2746
|
name: "studio_workflow_screenshot",
|
|
2558
2747
|
description: "Render a saved workflow's document as a diagram (labelled boxes and wires, positioned by the document's own layout) so you can see the graph you just built, the Figma get_screenshot equivalent. NOT a pixel-exact preview of the real canvas: a plain diagnostic diagram for checking topology (is everything wired the way I meant, did that addNode land where I expected). Returns { url, width, height }; the url is a signed Storage Brain link, valid for 24h. Free, read-only.",
|
|
2559
2748
|
inputSchema: {
|
|
2560
|
-
savedId
|
|
2749
|
+
savedId,
|
|
2750
|
+
projectId: z7.string().min(1).optional().describe("Project id from studio_list_projects the workflow lives in; omit for the default project.")
|
|
2561
2751
|
},
|
|
2562
2752
|
annotations: { readOnlyHint: true },
|
|
2563
2753
|
handler: async (args, ctx) => {
|
|
2564
|
-
return ctx.client.getWorkflowScreenshot(args.savedId);
|
|
2754
|
+
return ctx.client.getWorkflowScreenshot(args.savedId, args.projectId !== void 0 ? { projectId: args.projectId } : {});
|
|
2565
2755
|
}
|
|
2566
2756
|
});
|
|
2567
2757
|
var WORKFLOW_TOOLS = [
|
|
@@ -2594,6 +2784,8 @@ function projectJob(res) {
|
|
|
2594
2784
|
function projectNodeRun(nr) {
|
|
2595
2785
|
return {
|
|
2596
2786
|
nodeId: nr.nodeId,
|
|
2787
|
+
// The node-run (generation) id: what `upstreamGenerations` pins.
|
|
2788
|
+
id: nr.id ?? null,
|
|
2597
2789
|
status: nr.status,
|
|
2598
2790
|
url: nr.resultUrl ?? nr.results?.[0]?.url ?? null,
|
|
2599
2791
|
kind: nr.results?.[0]?.kind ?? null,
|
|
@@ -2847,17 +3039,20 @@ var studioUploadBrandReference = defineTool({
|
|
|
2847
3039
|
}
|
|
2848
3040
|
});
|
|
2849
3041
|
var workflowDefinitionSchema = WorkflowDefinitionSchema.describe(
|
|
2850
|
-
'Inline workflow DAG: { id, label?, params?: [{ name, type: string|number|boolean|json, required?, label?, description?, inputType?, cardinality?: one|many, default? }], nodes: [{ id, task (one of the model tasks listed), model (catalog id, e.g. fal/nano-banana-2-edit), inputs, retry? }] }. Node inputs are bindings: {kind:"literal",value}, {kind:"param",name,wrap?:"array"}
|
|
3042
|
+
'Inline workflow DAG: { id, label?, params?: [{ name, type: string|number|boolean|json, required?, label?, description?, inputType?, cardinality?: one|many, default? }], nodes: [{ id, task (one of the model tasks listed), model (catalog id, e.g. fal/nano-banana-2-edit), inputs, retry? }] }. Node inputs are bindings: {kind:"literal",value}, {kind:"param",name,wrap?:"array"}, {kind:"ref",node,path:"url",wrap?:"array"} or an ordered {kind:"list",items:[...]} of those. Image-edit nodes take inputImages (an array: one wrapped ref/param, or a list in reference order, scene frame first); image-to-video nodes take inputImageUrl (start frame) and optionally endImageUrl (end frame, on models that accept one).'
|
|
2851
3043
|
);
|
|
2852
3044
|
var studioRunWorkflow = defineTool({
|
|
2853
3045
|
name: "studio_run_workflow",
|
|
2854
|
-
description: `Run a generation workflow DAG. Pass an inline definition (nodes of provider calls wired by ref bindings; this is what the hosted Studio accepts) or the savedId of a workflow saved in the Studio (the Studio runs that workflow's current document, exactly what the canvas would run). Params fill {kind:"param"} bindings. Returns the runId plus per-node status, URLs and cost; with wait=true (default) it blocks until the run is terminal (succeeded, failed, partial). Each node becomes a paid job.`,
|
|
3046
|
+
description: `Run a generation workflow DAG. Pass an inline definition (nodes of provider calls wired by ref bindings; this is what the hosted Studio accepts) or the savedId of a workflow saved in the Studio (the Studio runs that workflow's current document, exactly what the canvas would run). Params fill {kind:"param"} bindings. Returns the runId plus per-node status, URLs and cost; with wait=true (default) it blocks until the run is terminal (succeeded, failed, partial, cancelled). Each node becomes a paid job. Use an inline definition ONLY for the first batch of a new deliverable (one workflow, a node per item, a stable label); a follow-up on an existing item belongs in that saved workflow as a new node (studio_workflow_apply: addNode plus connect from the node it refines) run here with savedId and nodeIds, because every listed inline run is saved as its own workflow.`,
|
|
2855
3047
|
inputSchema: {
|
|
2856
3048
|
projectId: z8.string().min(1).describe("Project id from studio_list_projects."),
|
|
2857
3049
|
definition: workflowDefinitionSchema.optional(),
|
|
2858
3050
|
savedId: z8.string().optional().describe("Id of a saved workflow (from studio_workflow_create, studio_workflow_get or a listed run) to run instead of an inline definition. An unknown id is a 404 naming it; a workflow that is not runnable yet comes back as not_runnable with per-node issues."),
|
|
2859
3051
|
params: z8.record(z8.string(), z8.unknown()).optional().describe("Values for the definition's params."),
|
|
2860
3052
|
nodeIds: z8.array(z8.string()).optional().describe("Run only this subset of nodes (requires an inline `definition` or a `savedId`)."),
|
|
3053
|
+
upstreamGenerations: z8.record(z8.string(), z8.string()).optional().describe(
|
|
3054
|
+
'Pin WHICH generation of an upstream node feeds this subset run: { "<upstream node id>": "<node-run id>" } (the per-node `id` that studio_get_run returns). A verified pin is reused even when the planner has no matching cache key for it (an inline first batch, or a node edited and reverted), so a follow-up node wired from an earlier generation runs against exactly that image. An unknown id is refused naming the node, never silently replaced by the latest.'
|
|
3055
|
+
),
|
|
2861
3056
|
runs: z8.number().int().min(1).max(8).optional().describe("Repeat a node subset N times (1 to 8)."),
|
|
2862
3057
|
visibility: z8.enum(["listed", "unlisted"]).optional().describe("Whether the run shows in the Studio workflow list."),
|
|
2863
3058
|
budgetUsd: z8.number().nonnegative().optional().describe(
|
|
@@ -2881,6 +3076,7 @@ var studioRunWorkflow = defineTool({
|
|
|
2881
3076
|
...args.definition ? { definition: args.definition } : {},
|
|
2882
3077
|
...args.params ? { params: args.params } : {},
|
|
2883
3078
|
...args.nodeIds && args.nodeIds.length > 0 ? { nodeIds: args.nodeIds } : {},
|
|
3079
|
+
...args.upstreamGenerations ? { upstreamGenerations: args.upstreamGenerations } : {},
|
|
2884
3080
|
...args.runs !== void 0 ? { runs: args.runs } : {},
|
|
2885
3081
|
...args.savedId ? { savedId: args.savedId } : {},
|
|
2886
3082
|
...args.visibility ? { visibility: args.visibility } : {},
|
|
@@ -2894,7 +3090,7 @@ var studioRunWorkflow = defineTool({
|
|
|
2894
3090
|
});
|
|
2895
3091
|
var studioGetRun = defineTool({
|
|
2896
3092
|
name: "studio_get_run",
|
|
2897
|
-
description: "Fetch a workflow run by runId: overall status (pending, running, succeeded, failed, partial, awaiting_decision), total cost, and per-node status, result URL, cost and error message. Set wait=true to block until the run settles. A run at awaiting_decision is parked on a person at a judge node: waiting returns immediately with a `waiting` block naming the node and its candidates, because polling can never clear it; ask the user to choose and submit their answer with studio_decide_judge.",
|
|
3093
|
+
description: "Fetch a workflow run by runId: overall status (pending, running, succeeded, failed, partial, awaiting_decision, cancelled; cancelled means somebody stopped it), total cost, and per-node status, result URL, cost and error message. Set wait=true to block until the run settles. A run at awaiting_decision is parked on a person at a judge node: waiting returns immediately with a `waiting` block naming the node and its candidates, because polling can never clear it; ask the user to choose and submit their answer with studio_decide_judge.",
|
|
2898
3094
|
inputSchema: {
|
|
2899
3095
|
runId: z8.string().min(1),
|
|
2900
3096
|
wait: z8.boolean().default(false),
|
|
@@ -2941,14 +3137,16 @@ var studioGetRunOutput = defineTool({
|
|
|
2941
3137
|
});
|
|
2942
3138
|
var studioReplaceCharacterInVideo = defineTool({
|
|
2943
3139
|
name: "studio_replace_character_in_video",
|
|
2944
|
-
description:
|
|
3140
|
+
description: 'Put a minted character into a video shot through the still-swap route: (1) an image-edit node (fal/nano-banana-2-edit) replaces the person in a scene FRAME with the character; (2) an image-to-video node animates that still (default fal/seedance-pro-image-to-video; with endFrameUrl it switches to fal/kling-v2-5-image-to-video and binds the end keyframe so the clip lands on it). Be honest with the user about what this route is: the video step REGENERATES the motion from the stills, so the original performance, timing and background are recreated rather than preserved; when the original motion must survive exactly, say so before spending. Declare scope: identity (default) takes face, head and hair and keeps the footage body; full_body also takes the character body proportions, which changes the silhouette. Pass preserve with the two or three shot-specific constraints most likely to fail, from looking at the frame (e.g. "keep the open door in front of his body", "match the face soft focus"), or a complete swapPrompt of your own. Never add a realism suffix like "photorealistic" or a promise that every pixel stays identical; the model cannot keep either and the suffix sharpens deliberately soft subjects. Give the character as characterSlug (its full-body reference is used, falling back to three-quarter then face) or as an explicit characterReferenceUrl. Requires a frame URL: extract a representative frame from the video yourself first (this tool does not decode video). Waits for both nodes and returns stillUrl, videoUrl, runId and costUsd (about $0.70 per take). Inspect the still before paying for more takes, keep generated cuts under 5 seconds, and budget one take plus two retries per shot. Do not use video-to-video swap models on dark or handheld footage.',
|
|
2945
3141
|
inputSchema: {
|
|
2946
3142
|
projectId: z8.string().min(1).describe("Project id from studio_list_projects."),
|
|
2947
3143
|
sceneFrameUrl: httpUrl.describe("Public or signed URL of ONE frame of the target shot (png/jpg)."),
|
|
2948
3144
|
characterSlug: slug.optional().describe("Minted character to insert; its full-body reference (fallback three-quarter, face) is resolved automatically."),
|
|
2949
3145
|
characterReferenceUrl: httpUrl.optional().describe("Explicit character reference image URL (use instead of characterSlug)."),
|
|
2950
3146
|
endFrameUrl: httpUrl.optional().describe("Optional second approved keyframe for the END of the clip. Switches the video model to Kling (start + end frame) unless videoModel is set explicitly."),
|
|
2951
|
-
|
|
3147
|
+
scope: z8.enum(SWAP_SCOPES).optional().describe("How much of the person to replace. identity (default): face, head and hair from the character, the footage keeps its body. full_body: also the character body proportions; the silhouette changes, so the background behind the old body and every contact point must be re-derived."),
|
|
3148
|
+
preserve: z8.array(z8.string().min(1).max(200)).max(6).optional().describe('Shot-specific constraints most likely to fail, as concrete relationships seen in the frame, one per entry (e.g. "keep the open door in front of his body"). Appended to the composed prompt.'),
|
|
3149
|
+
swapPrompt: z8.string().optional().describe("A complete still-swap edit prompt of your own; replaces the composed one, so scope and preserve are then ignored."),
|
|
2952
3150
|
motionPrompt: z8.string().optional().describe("Override the motion description for the video node."),
|
|
2953
3151
|
videoModel: z8.string().optional().describe(`Image-to-video model id. Default ${DEFAULT_SWAP_VIDEO_MODEL}, or ${KEYFRAME_SWAP_VIDEO_MODEL} when endFrameUrl is given.`),
|
|
2954
3152
|
editModel: z8.string().default(DEFAULT_SWAP_EDIT_MODEL).describe("Image-edit model id for the still swap."),
|
|
@@ -2979,6 +3177,8 @@ var studioReplaceCharacterInVideo = defineTool({
|
|
|
2979
3177
|
characterImageUrl,
|
|
2980
3178
|
...args.endFrameUrl ? { endFrameUrl: args.endFrameUrl } : {},
|
|
2981
3179
|
...args.swapPrompt ? { swapPrompt: args.swapPrompt } : {},
|
|
3180
|
+
...args.scope ? { scope: args.scope } : {},
|
|
3181
|
+
...args.preserve ? { preserve: args.preserve } : {},
|
|
2982
3182
|
...args.motionPrompt ? { motionPrompt: args.motionPrompt } : {},
|
|
2983
3183
|
editModel: args.editModel,
|
|
2984
3184
|
videoModel,
|
|
@@ -3141,7 +3341,7 @@ function registerTools(server, ctx) {
|
|
|
3141
3341
|
|
|
3142
3342
|
// src/server.ts
|
|
3143
3343
|
var SERVER_NAME = "lumitra-studio";
|
|
3144
|
-
var SERVER_VERSION = "0.
|
|
3344
|
+
var SERVER_VERSION = "0.2.0";
|
|
3145
3345
|
function createStudioMcpServer(opts = {}) {
|
|
3146
3346
|
const config = opts.config ?? resolveConfig(opts.env ?? process.env);
|
|
3147
3347
|
const client = new StudioClient(config, {
|
|
@@ -3155,7 +3355,8 @@ function createStudioMcpServer(opts = {}) {
|
|
|
3155
3355
|
"Lumitra Studio: brand-consistent image generation, character minting, workflow runs and the still-swap video recipe over the hosted Studio API.",
|
|
3156
3356
|
"Start with studio_list_projects to get a projectId. Every generation costs real money (roughly $0.04 per image, $0.30 per character mint, $0.70 per video take); check studio_get_spend when in doubt.",
|
|
3157
3357
|
"Long jobs: tools wait by default (up to timeoutSeconds, 600 by default) and return ids you can poll with studio_get_job / studio_get_run if they time out.",
|
|
3158
|
-
"Authoring a workflow is free and revisioned: studio_workflow_describe for the vocabulary, studio_workflow_create to build one from ops, studio_workflow_apply to change it on top of a baseRevision (a stale_revision result means re-read, rebase, retry), studio_workflow_validate to dry-run before spending, then studio_run_workflow with its savedId."
|
|
3358
|
+
"Authoring a workflow is free and revisioned: studio_workflow_describe for the vocabulary, studio_workflow_create to build one from ops, studio_workflow_apply to change it on top of a baseRevision (a stale_revision result means re-read, rebase, retry), studio_workflow_validate to dry-run before spending, then studio_run_workflow with its savedId.",
|
|
3359
|
+
"Workflow hygiene: one deliverable is ONE saved workflow with a node per item, and a refinement of an item (edit, upscale, variation) is a NEW NODE appended to that workflow with studio_workflow_apply (addNode plus a connect from the node it refines, so the earlier generation is its input), then studio_run_workflow with savedId and nodeIds limited to the new node. Never send a fresh inline definition for a follow-up: every listed inline run is saved as its own workflow, which scatters one deliverable across duplicates."
|
|
3159
3360
|
].join(" ")
|
|
3160
3361
|
}
|
|
3161
3362
|
);
|
|
@@ -3312,6 +3513,7 @@ export {
|
|
|
3312
3513
|
SERVER_NAME,
|
|
3313
3514
|
SERVER_VERSION,
|
|
3314
3515
|
SUMMARY_TEXT_CHARS,
|
|
3516
|
+
SWAP_SCOPES,
|
|
3315
3517
|
StudioApiError,
|
|
3316
3518
|
StudioClient,
|
|
3317
3519
|
TOOLS,
|
|
@@ -3323,6 +3525,7 @@ export {
|
|
|
3323
3525
|
buildCharacterSwapDefinition,
|
|
3324
3526
|
categoryForNode,
|
|
3325
3527
|
characterSheetDefinition,
|
|
3528
|
+
composeSwapPrompt,
|
|
3326
3529
|
createHttpHandler,
|
|
3327
3530
|
createStudioMcpServer,
|
|
3328
3531
|
invokeTool,
|