@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/cli.js
CHANGED
|
@@ -213,8 +213,8 @@ var StudioClient = class {
|
|
|
213
213
|
}
|
|
214
214
|
/** S9: a rendered diagram of a saved workflow's document, so an agent can
|
|
215
215
|
* see the graph it just built. */
|
|
216
|
-
getWorkflowScreenshot(savedId2) {
|
|
217
|
-
return this.json("GET", `/api/v1/workflows/${encodeURIComponent(savedId2)}/screenshot`);
|
|
216
|
+
getWorkflowScreenshot(savedId2, opts = {}) {
|
|
217
|
+
return this.json("GET", `/api/v1/workflows/${encodeURIComponent(savedId2)}/screenshot${projectQuery(opts.projectId)}`);
|
|
218
218
|
}
|
|
219
219
|
// ── Recipes (S10): published workflow + closed post-run effect list ──
|
|
220
220
|
publishWorkflow(savedId2, body) {
|
|
@@ -239,9 +239,10 @@ var StudioClient = class {
|
|
|
239
239
|
describeNodeTypes() {
|
|
240
240
|
return this.json("GET", "/api/v1/node-types");
|
|
241
241
|
}
|
|
242
|
-
/** `savedId` carries a `saved:` prefix; the colon is URL-encoded.
|
|
243
|
-
|
|
244
|
-
|
|
242
|
+
/** `savedId` carries a `saved:` prefix; the colon is URL-encoded. `projectId`
|
|
243
|
+
* names the project the workflow lives in; absent, the Studio's default. */
|
|
244
|
+
getWorkflowDocument(savedId2, opts = {}) {
|
|
245
|
+
return this.json("GET", `/api/v1/workflows/${encodeURIComponent(savedId2)}${projectQuery(opts.projectId)}`);
|
|
245
246
|
}
|
|
246
247
|
/** Create a document by applying `ops` to an empty one (the same code path as apply). */
|
|
247
248
|
createWorkflow(body) {
|
|
@@ -412,6 +413,9 @@ function mimeForFilename(name) {
|
|
|
412
413
|
return "application/octet-stream";
|
|
413
414
|
}
|
|
414
415
|
}
|
|
416
|
+
function projectQuery(projectId) {
|
|
417
|
+
return projectId === void 0 ? "" : `?projectId=${encodeURIComponent(projectId)}`;
|
|
418
|
+
}
|
|
415
419
|
|
|
416
420
|
// src/tools.ts
|
|
417
421
|
import { z as z8 } from "zod";
|
|
@@ -444,6 +448,10 @@ var MODEL_TASKS = [
|
|
|
444
448
|
"speech-to-speech",
|
|
445
449
|
"image-to-text",
|
|
446
450
|
"image-to-json",
|
|
451
|
+
// Camera-motion record (2026-09-13): a video in, a prompt-ready description
|
|
452
|
+
// of its camera motion out, measured from the frames by Studio's own code
|
|
453
|
+
// (model `studio/motion-record`, no paid provider).
|
|
454
|
+
"video-to-text",
|
|
447
455
|
// A human decision gate over candidate images (P3 J1). Still a model task,
|
|
448
456
|
// not a new node kind: WorkflowNode already requires task + model, and the
|
|
449
457
|
// ranking call underneath is the same image-to-json vision model
|
|
@@ -488,6 +496,8 @@ var TASK_PORTS = {
|
|
|
488
496
|
// VISION + LANGUAGE.
|
|
489
497
|
"image-to-text": { inputs: ["image", "text"], output: "text" },
|
|
490
498
|
"image-to-json": { inputs: ["image", "text"], output: "json" },
|
|
499
|
+
// Camera-motion record: the wired clip is the whole input, no prompt.
|
|
500
|
+
"video-to-text": { inputs: ["video"], output: "text" },
|
|
491
501
|
// Judge (P3 J1): one candidate wire in (the several results of ONE
|
|
492
502
|
// upstream node-run, not fan-in), the approved candidate out. Both sides
|
|
493
503
|
// are typed 'image' so an approved candidate can feed straight into any
|
|
@@ -551,11 +561,21 @@ var RefBindingSchema = z2.object({
|
|
|
551
561
|
// the "no array-of-refs" gap without widening the binding grammar itself.
|
|
552
562
|
wrap: z2.literal("array").optional()
|
|
553
563
|
});
|
|
554
|
-
var
|
|
564
|
+
var ListItemBindingSchema = z2.discriminatedUnion("kind", [
|
|
555
565
|
LiteralBindingSchema,
|
|
556
566
|
ParamBindingSchema,
|
|
557
567
|
RefBindingSchema
|
|
558
568
|
]);
|
|
569
|
+
var ListBindingSchema = z2.object({
|
|
570
|
+
kind: z2.literal("list"),
|
|
571
|
+
items: z2.array(ListItemBindingSchema)
|
|
572
|
+
});
|
|
573
|
+
var BindingSchema = z2.discriminatedUnion("kind", [
|
|
574
|
+
LiteralBindingSchema,
|
|
575
|
+
ParamBindingSchema,
|
|
576
|
+
RefBindingSchema,
|
|
577
|
+
ListBindingSchema
|
|
578
|
+
]);
|
|
559
579
|
var WorkflowTaskSchema = z2.enum(MODEL_TASKS);
|
|
560
580
|
var RetryPolicySchema = z2.object({
|
|
561
581
|
maxAttempts: z2.number().int().positive(),
|
|
@@ -634,6 +654,7 @@ var AssetIdSchema = z3.string().max(128);
|
|
|
634
654
|
var PortTypeSchema = z3.enum(PORT_TYPES);
|
|
635
655
|
var InputTypeSchema = z3.enum(INPUT_PORT_TYPES);
|
|
636
656
|
var PositionSchema = z3.object({ x: z3.number().finite(), y: z3.number().finite() });
|
|
657
|
+
var CollapsedSchema = z3.boolean();
|
|
637
658
|
var TaskNodeSchema = z3.object({
|
|
638
659
|
id: IdSchema,
|
|
639
660
|
kind: z3.literal("task"),
|
|
@@ -654,13 +675,15 @@ var TaskNodeSchema = z3.object({
|
|
|
654
675
|
bakedInputs: z3.record(KeySchema, LiteralBindingSchema).default({}),
|
|
655
676
|
/** The generation id that feeds downstream. Absent = latest (D4). */
|
|
656
677
|
activeGeneration: IdSchema.optional(),
|
|
657
|
-
label: LabelSchema.optional()
|
|
678
|
+
label: LabelSchema.optional(),
|
|
679
|
+
collapsed: CollapsedSchema.optional()
|
|
658
680
|
});
|
|
659
681
|
var PromptSourceNodeSchema = z3.object({
|
|
660
682
|
id: IdSchema,
|
|
661
683
|
kind: z3.literal("prompt-source"),
|
|
662
684
|
text: PromptTextSchema.default(""),
|
|
663
|
-
label: LabelSchema.optional()
|
|
685
|
+
label: LabelSchema.optional(),
|
|
686
|
+
collapsed: CollapsedSchema.optional()
|
|
664
687
|
});
|
|
665
688
|
var ImageSourceNodeSchema = z3.object({
|
|
666
689
|
id: IdSchema,
|
|
@@ -671,7 +694,18 @@ var ImageSourceNodeSchema = z3.object({
|
|
|
671
694
|
/** The Storage Brain asset id, when known (E18: every image input resolves to
|
|
672
695
|
* an asset id). */
|
|
673
696
|
assetId: AssetIdSchema.optional(),
|
|
674
|
-
label: LabelSchema.optional()
|
|
697
|
+
label: LabelSchema.optional(),
|
|
698
|
+
collapsed: CollapsedSchema.optional()
|
|
699
|
+
});
|
|
700
|
+
var VideoSourceNodeSchema = z3.object({
|
|
701
|
+
id: IdSchema,
|
|
702
|
+
kind: z3.literal("video-source"),
|
|
703
|
+
videoUrl: UrlSchema.default(""),
|
|
704
|
+
/** The Storage Brain asset id, when known, so the clip can be re-signed and
|
|
705
|
+
* attributed the way an image source's asset is. */
|
|
706
|
+
assetId: AssetIdSchema.optional(),
|
|
707
|
+
label: LabelSchema.optional(),
|
|
708
|
+
collapsed: CollapsedSchema.optional()
|
|
675
709
|
});
|
|
676
710
|
var InputNodeSchema = z3.object({
|
|
677
711
|
id: IdSchema,
|
|
@@ -686,12 +720,14 @@ var InputNodeSchema = z3.object({
|
|
|
686
720
|
/** Used when a run omits the input. Never null: absent means no default. */
|
|
687
721
|
default: JsonNonNullValueSchema.optional(),
|
|
688
722
|
label: LabelSchema.optional(),
|
|
689
|
-
description: DescriptionSchema.optional()
|
|
723
|
+
description: DescriptionSchema.optional(),
|
|
724
|
+
collapsed: CollapsedSchema.optional()
|
|
690
725
|
});
|
|
691
726
|
var DocNodeSchema = z3.discriminatedUnion("kind", [
|
|
692
727
|
TaskNodeSchema,
|
|
693
728
|
PromptSourceNodeSchema,
|
|
694
729
|
ImageSourceNodeSchema,
|
|
730
|
+
VideoSourceNodeSchema,
|
|
695
731
|
InputNodeSchema
|
|
696
732
|
]);
|
|
697
733
|
var DocEdgeSchema = z3.object({
|
|
@@ -704,6 +740,18 @@ var DocGroupSchema = z3.object({
|
|
|
704
740
|
label: LabelSchema.optional(),
|
|
705
741
|
nodeIds: z3.array(IdSchema).min(1)
|
|
706
742
|
});
|
|
743
|
+
var EDGE_ROUTINGS = ["curved", "orthogonal"];
|
|
744
|
+
var EdgeRoutingSchema = z3.enum(EDGE_ROUTINGS);
|
|
745
|
+
var WireRouteSchema = z3.object({
|
|
746
|
+
from: PositionSchema,
|
|
747
|
+
to: PositionSchema,
|
|
748
|
+
points: z3.array(PositionSchema).max(64)
|
|
749
|
+
});
|
|
750
|
+
var DocumentViewSchema = z3.object({
|
|
751
|
+
edgeRouting: EdgeRoutingSchema.optional(),
|
|
752
|
+
/** Routed wires from the last Arrange, keyed by `edgeKey`. */
|
|
753
|
+
wires: z3.record(z3.string().max(512), WireRouteSchema).optional()
|
|
754
|
+
});
|
|
707
755
|
var WorkflowDocumentSchema = z3.object({
|
|
708
756
|
schema: z3.number().int().positive(),
|
|
709
757
|
id: IdSchema,
|
|
@@ -711,7 +759,10 @@ var WorkflowDocumentSchema = z3.object({
|
|
|
711
759
|
nodes: z3.array(DocNodeSchema).max(2e3),
|
|
712
760
|
edges: z3.array(DocEdgeSchema).max(8e3),
|
|
713
761
|
layout: z3.record(IdSchema, PositionSchema).default({}),
|
|
714
|
-
groups: z3.array(DocGroupSchema).max(500).default([])
|
|
762
|
+
groups: z3.array(DocGroupSchema).max(500).default([]),
|
|
763
|
+
/** Display preferences; absent on every document written before they
|
|
764
|
+
* existed (additive, so `DOCUMENT_SCHEMA_VERSION` is unchanged). */
|
|
765
|
+
view: DocumentViewSchema.optional()
|
|
715
766
|
});
|
|
716
767
|
function edgeKey(e) {
|
|
717
768
|
return JSON.stringify([e.source, e.target, e.port]);
|
|
@@ -963,7 +1014,7 @@ var MODEL_CATALOG = [
|
|
|
963
1014
|
label: "Kling 2.5 Turbo Pro (image-to-video)",
|
|
964
1015
|
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.",
|
|
965
1016
|
cost: { usd: 0.35 },
|
|
966
|
-
capabilities: { needsImageInput: true, maxInputImages: 1 },
|
|
1017
|
+
capabilities: { needsImageInput: true, maxInputImages: 1, acceptsEndFrame: true },
|
|
967
1018
|
isDefault: true
|
|
968
1019
|
},
|
|
969
1020
|
{
|
|
@@ -984,7 +1035,7 @@ var MODEL_CATALOG = [
|
|
|
984
1035
|
label: "Seedance 2.5 (image-to-video)",
|
|
985
1036
|
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.",
|
|
986
1037
|
cost: { usd: 1.89 },
|
|
987
|
-
capabilities: { needsImageInput: true, maxInputImages: 1 }
|
|
1038
|
+
capabilities: { needsImageInput: true, maxInputImages: 1, acceptsEndFrame: true }
|
|
988
1039
|
},
|
|
989
1040
|
{
|
|
990
1041
|
id: "fal/seedance-2-0-image-to-video",
|
|
@@ -994,7 +1045,7 @@ var MODEL_CATALOG = [
|
|
|
994
1045
|
label: "Seedance 2.0 (image-to-video)",
|
|
995
1046
|
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.",
|
|
996
1047
|
cost: { usd: 1.89 },
|
|
997
|
-
capabilities: { needsImageInput: true, maxInputImages: 1 }
|
|
1048
|
+
capabilities: { needsImageInput: true, maxInputImages: 1, acceptsEndFrame: true }
|
|
998
1049
|
},
|
|
999
1050
|
// ── FLUX.3 (Black Forest Labs) ───────────────────────────────────────────
|
|
1000
1051
|
// One model, three tiers of the same three input shapes:
|
|
@@ -1029,7 +1080,7 @@ var MODEL_CATALOG = [
|
|
|
1029
1080
|
label: "FLUX.3 draft (first + last frame)",
|
|
1030
1081
|
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.",
|
|
1031
1082
|
cost: { usd: 0.3 },
|
|
1032
|
-
capabilities: { needsImageInput: true, maxInputImages: 2, emitsDraftCache: true }
|
|
1083
|
+
capabilities: { needsImageInput: true, maxInputImages: 2, emitsDraftCache: true, acceptsEndFrame: true }
|
|
1033
1084
|
},
|
|
1034
1085
|
{
|
|
1035
1086
|
id: "fal/flux-3-keyframes-to-video-draft",
|
|
@@ -1059,7 +1110,7 @@ var MODEL_CATALOG = [
|
|
|
1059
1110
|
label: "FLUX.3 (first + last frame)",
|
|
1060
1111
|
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.",
|
|
1061
1112
|
cost: { usd: 0.85 },
|
|
1062
|
-
capabilities: { needsImageInput: true, maxInputImages: 2 }
|
|
1113
|
+
capabilities: { needsImageInput: true, maxInputImages: 2, acceptsEndFrame: true }
|
|
1063
1114
|
},
|
|
1064
1115
|
{
|
|
1065
1116
|
id: "fal/flux-3-keyframes-to-video",
|
|
@@ -1129,6 +1180,19 @@ var MODEL_CATALOG = [
|
|
|
1129
1180
|
capabilities: { needsImageInput: true, maxInputImages: 1 },
|
|
1130
1181
|
isDefault: true
|
|
1131
1182
|
},
|
|
1183
|
+
// Camera-motion record (2026-09-13): Studio's own analysis, no paid provider.
|
|
1184
|
+
// A video in, a prompt-ready description of its camera motion out
|
|
1185
|
+
// (job.output.text), with the measured record beside it (job.output.json).
|
|
1186
|
+
{
|
|
1187
|
+
id: "studio/motion-record",
|
|
1188
|
+
provider: "studio",
|
|
1189
|
+
modality: "text",
|
|
1190
|
+
task: "video-to-text",
|
|
1191
|
+
label: "Motion record (Studio)",
|
|
1192
|
+
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.",
|
|
1193
|
+
cost: { usd: 0 },
|
|
1194
|
+
isDefault: true
|
|
1195
|
+
},
|
|
1132
1196
|
{
|
|
1133
1197
|
id: "fal/moondream-next-vqa",
|
|
1134
1198
|
provider: "fal",
|
|
@@ -1320,6 +1384,9 @@ function portBinding(task, port) {
|
|
|
1320
1384
|
if (port === "mesh3d" && (task === "remesh" || task === "texture")) {
|
|
1321
1385
|
return { key: "parentAssetId", path: "assetId" };
|
|
1322
1386
|
}
|
|
1387
|
+
if (port === "video" && task === "video-to-text") {
|
|
1388
|
+
return { key: "inputVideoUrl", path: "url" };
|
|
1389
|
+
}
|
|
1323
1390
|
if (port === "video" && task === "video-to-video") {
|
|
1324
1391
|
return { key: "inputVideoUrl", path: "url" };
|
|
1325
1392
|
}
|
|
@@ -1350,6 +1417,30 @@ function canConnect(sourceOut, targetTask, targetPort) {
|
|
|
1350
1417
|
if (sourceOut !== targetPort) return false;
|
|
1351
1418
|
return portBinding(targetTask, targetPort) !== null;
|
|
1352
1419
|
}
|
|
1420
|
+
function portCardinality(task, port) {
|
|
1421
|
+
if (portBinding(task, port) === null) return "none";
|
|
1422
|
+
if (port === "image" && task === "image-edit") return "many";
|
|
1423
|
+
if (port === "image" && task === "image-to-video") return "two";
|
|
1424
|
+
return "one";
|
|
1425
|
+
}
|
|
1426
|
+
function cardinalityLimit(cardinality) {
|
|
1427
|
+
switch (cardinality) {
|
|
1428
|
+
case "none":
|
|
1429
|
+
return 0;
|
|
1430
|
+
case "one":
|
|
1431
|
+
return 1;
|
|
1432
|
+
case "two":
|
|
1433
|
+
return 2;
|
|
1434
|
+
case "many":
|
|
1435
|
+
return Number.POSITIVE_INFINITY;
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
function orderedPortKey(task, port, ordinal) {
|
|
1439
|
+
const canonical = portBinding(task, port);
|
|
1440
|
+
if (!canonical) return null;
|
|
1441
|
+
if (portCardinality(task, port) === "two" && ordinal === 1) return "endImageUrl";
|
|
1442
|
+
return canonical.key;
|
|
1443
|
+
}
|
|
1353
1444
|
function sourcePortOf(node) {
|
|
1354
1445
|
switch (node.kind) {
|
|
1355
1446
|
case "task":
|
|
@@ -1358,10 +1449,28 @@ function sourcePortOf(node) {
|
|
|
1358
1449
|
return "text";
|
|
1359
1450
|
case "image-source":
|
|
1360
1451
|
return "image";
|
|
1452
|
+
case "video-source":
|
|
1453
|
+
return "video";
|
|
1361
1454
|
case "input":
|
|
1362
1455
|
return node.type;
|
|
1363
1456
|
}
|
|
1364
1457
|
}
|
|
1458
|
+
function targetCardinality(node, port) {
|
|
1459
|
+
if (node.kind !== "task") return "none";
|
|
1460
|
+
return portCardinality(node.task, port);
|
|
1461
|
+
}
|
|
1462
|
+
function slotRefusal(node, port, wired) {
|
|
1463
|
+
const cardinality = targetCardinality(node, port);
|
|
1464
|
+
let limit = cardinalityLimit(cardinality);
|
|
1465
|
+
if (cardinality === "many" && node.kind === "task" && node.model) {
|
|
1466
|
+
const cap = tryGetModel(node.model)?.capabilities?.maxInputImages;
|
|
1467
|
+
if (typeof cap === "number" && cap > 0) limit = Math.min(limit, cap);
|
|
1468
|
+
}
|
|
1469
|
+
if (wired < limit) return null;
|
|
1470
|
+
if (cardinality === "one") return `node "${node.id}" accepts one ${port} wire, got more`;
|
|
1471
|
+
if (cardinality === "two") return `node "${node.id}" takes two ${port} wires (start frame, end frame), got more`;
|
|
1472
|
+
return `node "${node.id}" (${node.kind === "task" ? node.model : node.kind}) takes at most ${limit} ${port} wires`;
|
|
1473
|
+
}
|
|
1365
1474
|
function wireLegality(source, target, port) {
|
|
1366
1475
|
if (source.id === target.id) return { ok: false, code: "cycle", message: `node "${source.id}" cannot wire into itself` };
|
|
1367
1476
|
const produced = sourcePortOf(source);
|
|
@@ -1428,7 +1537,7 @@ function validateDocument(doc) {
|
|
|
1428
1537
|
inputNames.add(node.name);
|
|
1429
1538
|
}
|
|
1430
1539
|
const edgeKeys = /* @__PURE__ */ new Set();
|
|
1431
|
-
const slots = /* @__PURE__ */ new
|
|
1540
|
+
const slots = /* @__PURE__ */ new Map();
|
|
1432
1541
|
for (const e of doc.edges) {
|
|
1433
1542
|
const key = edgeKey(e);
|
|
1434
1543
|
if (edgeKeys.has(key)) return { ok: false, code: "conflict", message: `duplicate edge ${key}` };
|
|
@@ -1440,14 +1549,24 @@ function validateDocument(doc) {
|
|
|
1440
1549
|
const legal = wireLegality(source, target, e.port);
|
|
1441
1550
|
if (!legal.ok) return legal;
|
|
1442
1551
|
const slot = slotKey(e.target, e.port);
|
|
1443
|
-
|
|
1552
|
+
const wired = slots.get(slot) ?? 0;
|
|
1553
|
+
const refusal = slotRefusal(target, e.port, wired);
|
|
1554
|
+
if (refusal) return { ok: false, code: "cardinality", message: refusal };
|
|
1555
|
+
slots.set(slot, wired + 1);
|
|
1556
|
+
}
|
|
1557
|
+
for (const [slot, wired] of slots) {
|
|
1558
|
+
if (wired < 2) continue;
|
|
1559
|
+
const [targetId, port] = JSON.parse(slot);
|
|
1560
|
+
const target = byId.get(targetId);
|
|
1561
|
+
if (!target || target.kind !== "task" || port !== "image" || portCardinality(target.task, port) !== "two" || !target.model) continue;
|
|
1562
|
+
const model = tryGetModel(target.model);
|
|
1563
|
+
if (model && !model.capabilities?.acceptsEndFrame) {
|
|
1444
1564
|
return {
|
|
1445
1565
|
ok: false,
|
|
1446
|
-
code: "
|
|
1447
|
-
message: `
|
|
1566
|
+
code: "invalid_port",
|
|
1567
|
+
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.`
|
|
1448
1568
|
};
|
|
1449
1569
|
}
|
|
1450
|
-
slots.add(slot);
|
|
1451
1570
|
}
|
|
1452
1571
|
if (documentTopologicalOrder(doc) === null) {
|
|
1453
1572
|
return { ok: false, code: "cycle", message: `document "${doc.id}" has a cycle in its wires` };
|
|
@@ -1480,16 +1599,25 @@ var TaskNodePropsSchema = z4.object({
|
|
|
1480
1599
|
task: z4.enum(MODEL_TASKS),
|
|
1481
1600
|
model: ModelIdSchema,
|
|
1482
1601
|
prompt: PromptTextSchema,
|
|
1483
|
-
label: LabelSchema.nullable()
|
|
1602
|
+
label: LabelSchema.nullable(),
|
|
1603
|
+
collapsed: CollapsedSchema.nullable()
|
|
1484
1604
|
}).partial().strict();
|
|
1485
1605
|
var PromptSourcePropsSchema = z4.object({
|
|
1486
1606
|
text: PromptTextSchema,
|
|
1487
|
-
label: LabelSchema.nullable()
|
|
1607
|
+
label: LabelSchema.nullable(),
|
|
1608
|
+
collapsed: CollapsedSchema.nullable()
|
|
1488
1609
|
}).partial().strict();
|
|
1489
1610
|
var ImageSourcePropsSchema = z4.object({
|
|
1490
1611
|
imageUrl: UrlSchema,
|
|
1491
1612
|
assetId: AssetIdSchema.nullable(),
|
|
1492
|
-
label: LabelSchema.nullable()
|
|
1613
|
+
label: LabelSchema.nullable(),
|
|
1614
|
+
collapsed: CollapsedSchema.nullable()
|
|
1615
|
+
}).partial().strict();
|
|
1616
|
+
var VideoSourcePropsSchema = z4.object({
|
|
1617
|
+
videoUrl: UrlSchema,
|
|
1618
|
+
assetId: AssetIdSchema.nullable(),
|
|
1619
|
+
label: LabelSchema.nullable(),
|
|
1620
|
+
collapsed: CollapsedSchema.nullable()
|
|
1493
1621
|
}).partial().strict();
|
|
1494
1622
|
var InputNodePropsSchema = z4.object({
|
|
1495
1623
|
name: InputNameSchema,
|
|
@@ -1498,14 +1626,19 @@ var InputNodePropsSchema = z4.object({
|
|
|
1498
1626
|
cardinality: z4.enum(["one", "many"]).nullable(),
|
|
1499
1627
|
default: JsonNonNullValueSchema.nullable(),
|
|
1500
1628
|
label: LabelSchema.nullable(),
|
|
1501
|
-
description: DescriptionSchema.nullable()
|
|
1629
|
+
description: DescriptionSchema.nullable(),
|
|
1630
|
+
collapsed: CollapsedSchema.nullable()
|
|
1502
1631
|
}).partial().strict();
|
|
1503
1632
|
var GroupPropsSchema = z4.object({
|
|
1504
1633
|
label: LabelSchema.nullable(),
|
|
1505
1634
|
nodeIds: z4.array(IdSchema).min(1)
|
|
1506
1635
|
}).partial().strict();
|
|
1507
1636
|
var DocumentPropsSchema = z4.object({
|
|
1508
|
-
label: LabelSchema
|
|
1637
|
+
label: LabelSchema,
|
|
1638
|
+
/** Display preferences (`view.edgeRouting`); null clears the whole
|
|
1639
|
+
* object. The object is replaced, not merged: pass every field you want
|
|
1640
|
+
* kept. */
|
|
1641
|
+
view: DocumentViewSchema.strict().nullable()
|
|
1509
1642
|
}).partial().strict();
|
|
1510
1643
|
var AddNodeOpSchema = z4.object({
|
|
1511
1644
|
type: z4.literal("addNode"),
|
|
@@ -1523,6 +1656,7 @@ var SetNodePropsOpSchema = z4.discriminatedUnion("kind", [
|
|
|
1523
1656
|
z4.object({ type: z4.literal("setNodeProps"), kind: z4.literal("task"), id: IdSchema, props: TaskNodePropsSchema }),
|
|
1524
1657
|
z4.object({ type: z4.literal("setNodeProps"), kind: z4.literal("prompt-source"), id: IdSchema, props: PromptSourcePropsSchema }),
|
|
1525
1658
|
z4.object({ type: z4.literal("setNodeProps"), kind: z4.literal("image-source"), id: IdSchema, props: ImageSourcePropsSchema }),
|
|
1659
|
+
z4.object({ type: z4.literal("setNodeProps"), kind: z4.literal("video-source"), id: IdSchema, props: VideoSourcePropsSchema }),
|
|
1526
1660
|
z4.object({ type: z4.literal("setNodeProps"), kind: z4.literal("input"), id: IdSchema, props: InputNodePropsSchema })
|
|
1527
1661
|
]);
|
|
1528
1662
|
var SetNodeSettingOpSchema = z4.object({
|
|
@@ -1554,8 +1688,8 @@ var DisconnectOpSchema = z4.object({
|
|
|
1554
1688
|
});
|
|
1555
1689
|
var ExposeAsInputOpSchema = z4.object({
|
|
1556
1690
|
type: z4.literal("exposeAsInput"),
|
|
1557
|
-
/** A prompt-source or
|
|
1558
|
-
* (same id, same produced port type), so its wires survive. */
|
|
1691
|
+
/** A prompt-source, image-source or video-source node; it becomes an input
|
|
1692
|
+
* node IN PLACE (same id, same produced port type), so its wires survive. */
|
|
1559
1693
|
id: IdSchema,
|
|
1560
1694
|
name: InputNameSchema,
|
|
1561
1695
|
required: z4.boolean().optional(),
|
|
@@ -1568,8 +1702,9 @@ var ExposeAsInputOpSchema = z4.object({
|
|
|
1568
1702
|
});
|
|
1569
1703
|
var FreezeInputOpSchema = z4.object({
|
|
1570
1704
|
type: z4.literal("freezeInput"),
|
|
1571
|
-
/** A text or
|
|
1572
|
-
* PLACE carrying `value` (a chosen run's
|
|
1705
|
+
/** A text, image or video input node; it becomes a prompt-source /
|
|
1706
|
+
* image-source / video-source IN PLACE carrying `value` (a chosen run's
|
|
1707
|
+
* resolved value, or any literal). */
|
|
1573
1708
|
id: IdSchema,
|
|
1574
1709
|
value: PromptTextSchema,
|
|
1575
1710
|
assetId: AssetIdSchema.optional(),
|
|
@@ -1778,6 +1913,8 @@ var TASK_TO_KIND = {
|
|
|
1778
1913
|
// vision (text/json output, no binary asset)
|
|
1779
1914
|
"image-to-text": "analyze_image",
|
|
1780
1915
|
"image-to-json": "analyze_image",
|
|
1916
|
+
// camera-motion record (text + json output, no binary asset), Studio's own worker
|
|
1917
|
+
"video-to-text": "analyze_video",
|
|
1781
1918
|
// utility (new Image asset off the source)
|
|
1782
1919
|
upscale: "transform_image",
|
|
1783
1920
|
"background-remove": "transform_image",
|
|
@@ -1801,7 +1938,7 @@ function taskExecution(task) {
|
|
|
1801
1938
|
function paramTypeForInput(type) {
|
|
1802
1939
|
return type === "json" ? "json" : "string";
|
|
1803
1940
|
}
|
|
1804
|
-
function
|
|
1941
|
+
function isUsableSourceUrl(url) {
|
|
1805
1942
|
return url.length > 0 && !url.startsWith("data:");
|
|
1806
1943
|
}
|
|
1807
1944
|
function documentToDefinition(doc, opts = {}) {
|
|
@@ -1859,22 +1996,50 @@ function documentToDefinition(doc, opts = {}) {
|
|
|
1859
1996
|
issues.push({ code: "prompt_required", nodeId: node.id, message: `Node "${node.id}" (${task}) needs a prompt.` });
|
|
1860
1997
|
}
|
|
1861
1998
|
}
|
|
1999
|
+
const byPort = /* @__PURE__ */ new Map();
|
|
1862
2000
|
for (const e of wires) {
|
|
1863
2001
|
if (e.port === "text") continue;
|
|
1864
|
-
const
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
2002
|
+
const list = byPort.get(e.port);
|
|
2003
|
+
if (list) list.push(e);
|
|
2004
|
+
else byPort.set(e.port, [e]);
|
|
2005
|
+
}
|
|
2006
|
+
for (const [port, portWires] of byPort) {
|
|
2007
|
+
const b = portBinding(task, port);
|
|
2008
|
+
const cardinality = portCardinality(task, port);
|
|
2009
|
+
const items = [];
|
|
2010
|
+
let uploading = false;
|
|
2011
|
+
for (const [ordinal, e] of portWires.entries()) {
|
|
2012
|
+
const source = byId.get(e.source);
|
|
2013
|
+
const single = cardinality !== "many" || portWires.length === 1;
|
|
2014
|
+
const key = orderedPortKey(task, port, ordinal) ?? b.key;
|
|
2015
|
+
const wrap = single && b.wrap === "array";
|
|
2016
|
+
let item = null;
|
|
2017
|
+
if (source.kind === "image-source") {
|
|
2018
|
+
const url = source.imageUrl.trim();
|
|
2019
|
+
if (!isUsableSourceUrl(url)) {
|
|
2020
|
+
issues.push({ code: "source_uploading", nodeId: node.id, message: `Image source for "${node.id}" is still uploading; wait for it to finish.` });
|
|
2021
|
+
uploading = true;
|
|
2022
|
+
continue;
|
|
2023
|
+
}
|
|
2024
|
+
item = { kind: "literal", value: wrap ? [url] : url };
|
|
2025
|
+
} else if (source.kind === "video-source") {
|
|
2026
|
+
const clip = source.videoUrl.trim();
|
|
2027
|
+
if (!isUsableSourceUrl(clip)) {
|
|
2028
|
+
issues.push({ code: "source_uploading", nodeId: node.id, message: `Video source for "${node.id}" is still uploading; wait for it to finish.` });
|
|
2029
|
+
uploading = true;
|
|
2030
|
+
continue;
|
|
2031
|
+
}
|
|
2032
|
+
item = { kind: "literal", value: wrap ? [clip] : clip };
|
|
2033
|
+
} else if (source.kind === "input") {
|
|
2034
|
+
item = wrap && source.cardinality !== "many" ? { kind: "param", name: source.name, wrap: "array" } : { kind: "param", name: source.name };
|
|
2035
|
+
} else if (source.kind === "task") {
|
|
2036
|
+
item = wrap ? { kind: "ref", node: source.id, path: b.path, wrap: "array" } : { kind: "ref", node: source.id, path: b.path };
|
|
1871
2037
|
}
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
} else if (source.kind === "task") {
|
|
1876
|
-
inputs[b.key] = b.wrap === "array" ? { kind: "ref", node: source.id, path: b.path, wrap: "array" } : { kind: "ref", node: source.id, path: b.path };
|
|
2038
|
+
if (!item) continue;
|
|
2039
|
+
if (single) inputs[key] = item;
|
|
2040
|
+
else items.push(item);
|
|
1877
2041
|
}
|
|
2042
|
+
if (!uploading && items.length > 0) inputs[b.key] = { kind: "list", items };
|
|
1878
2043
|
}
|
|
1879
2044
|
for (const [key, binding] of Object.entries(node.bakedInputs)) if (!(key in inputs)) inputs[key] = binding;
|
|
1880
2045
|
if (Object.keys(inputs).length === 0 && !promptMissing) {
|
|
@@ -2076,7 +2241,7 @@ async function pollUntil(what, fetchState, isDone, opts = {}) {
|
|
|
2076
2241
|
}
|
|
2077
2242
|
}
|
|
2078
2243
|
var TERMINAL_JOB_STATUSES = /* @__PURE__ */ new Set(["succeeded", "failed", "canceled"]);
|
|
2079
|
-
var TERMINAL_RUN_STATUSES = /* @__PURE__ */ new Set(["succeeded", "failed", "partial"]);
|
|
2244
|
+
var TERMINAL_RUN_STATUSES = /* @__PURE__ */ new Set(["succeeded", "failed", "partial", "cancelled"]);
|
|
2080
2245
|
function isJobTerminal(res) {
|
|
2081
2246
|
return TERMINAL_JOB_STATUSES.has(res.job.status);
|
|
2082
2247
|
}
|
|
@@ -2109,8 +2274,24 @@ function runFailureSummary(res) {
|
|
|
2109
2274
|
var DEFAULT_SWAP_EDIT_MODEL = "fal/nano-banana-2-edit";
|
|
2110
2275
|
var DEFAULT_SWAP_VIDEO_MODEL = "fal/seedance-pro-image-to-video";
|
|
2111
2276
|
var KEYFRAME_SWAP_VIDEO_MODEL = "fal/kling-v2-5-image-to-video";
|
|
2112
|
-
var
|
|
2113
|
-
|
|
2277
|
+
var SWAP_SCOPES = ["identity", "full_body"];
|
|
2278
|
+
function composeSwapPrompt(options = {}) {
|
|
2279
|
+
const scope = options.scope ?? "identity";
|
|
2280
|
+
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";
|
|
2281
|
+
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.";
|
|
2282
|
+
const sentences = [
|
|
2283
|
+
`Replace the person in the first image with the person in the second image: ${taken}.`,
|
|
2284
|
+
kept,
|
|
2285
|
+
"Match the first image's lighting, focus and colour, and keep its framing and background."
|
|
2286
|
+
];
|
|
2287
|
+
for (const raw of options.preserve ?? []) {
|
|
2288
|
+
const constraint = raw.trim().replace(/[.\s]+$/, "");
|
|
2289
|
+
if (constraint) sentences.push(`${constraint.charAt(0).toUpperCase()}${constraint.slice(1)}.`);
|
|
2290
|
+
}
|
|
2291
|
+
return sentences.join(" ");
|
|
2292
|
+
}
|
|
2293
|
+
var DEFAULT_SWAP_PROMPT = composeSwapPrompt();
|
|
2294
|
+
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.";
|
|
2114
2295
|
var SWAP_EDIT_NODE_ID = "edit";
|
|
2115
2296
|
var SWAP_VIDEO_NODE_ID = "video";
|
|
2116
2297
|
function defaultVideoModelFor(input) {
|
|
@@ -2130,7 +2311,7 @@ function buildCharacterSwapDefinition(input) {
|
|
|
2130
2311
|
task: "image-edit",
|
|
2131
2312
|
model: input.editModel ?? DEFAULT_SWAP_EDIT_MODEL,
|
|
2132
2313
|
inputs: {
|
|
2133
|
-
prompt: { kind: "literal", value: input.swapPrompt ??
|
|
2314
|
+
prompt: { kind: "literal", value: input.swapPrompt ?? composeSwapPrompt({ scope: input.scope, preserve: input.preserve }) },
|
|
2134
2315
|
inputImages: { kind: "literal", value: [input.sceneFrameUrl, input.characterImageUrl] },
|
|
2135
2316
|
aspectRatio: { kind: "literal", value: input.aspectRatio ?? "16:9" }
|
|
2136
2317
|
}
|
|
@@ -2239,6 +2420,9 @@ function summarizeNode(node, wires) {
|
|
|
2239
2420
|
case "image-source":
|
|
2240
2421
|
base.imageUrl = clipText(node.imageUrl);
|
|
2241
2422
|
break;
|
|
2423
|
+
case "video-source":
|
|
2424
|
+
base.videoUrl = clipText(node.videoUrl);
|
|
2425
|
+
break;
|
|
2242
2426
|
case "input":
|
|
2243
2427
|
base.name = node.name;
|
|
2244
2428
|
base.type = node.type;
|
|
@@ -2409,11 +2593,12 @@ var studioWorkflowGet = defineTool({
|
|
|
2409
2593
|
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.",
|
|
2410
2594
|
inputSchema: {
|
|
2411
2595
|
savedId,
|
|
2596
|
+
projectId: z7.string().min(1).optional().describe("Project id from studio_list_projects the workflow lives in; omit for the default project."),
|
|
2412
2597
|
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.")
|
|
2413
2598
|
},
|
|
2414
2599
|
annotations: { readOnlyHint: true },
|
|
2415
2600
|
handler: async (args, ctx) => {
|
|
2416
|
-
const res = await ctx.client.getWorkflowDocument(args.savedId);
|
|
2601
|
+
const res = await ctx.client.getWorkflowDocument(args.savedId, args.projectId !== void 0 ? { projectId: args.projectId } : {});
|
|
2417
2602
|
if (args.full) return res;
|
|
2418
2603
|
if (res.readOnly === true || res.document === null) {
|
|
2419
2604
|
const definition = isRecord(res.definition) ? res.definition : null;
|
|
@@ -2486,6 +2671,7 @@ var studioWorkflowApply = defineTool({
|
|
|
2486
2671
|
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.',
|
|
2487
2672
|
inputSchema: {
|
|
2488
2673
|
savedId,
|
|
2674
|
+
projectId: z7.string().min(1).optional().describe("Project id from studio_list_projects the workflow lives in; omit for the default project."),
|
|
2489
2675
|
baseRevision: PatchSchema.shape.baseRevision.describe("The revision the ops were written against. The Studio refuses a stale one with stale_revision."),
|
|
2490
2676
|
ops: opsSchema,
|
|
2491
2677
|
idempotencyKey: PatchSchema.shape.idempotencyKey.describe(
|
|
@@ -2500,6 +2686,7 @@ var studioWorkflowApply = defineTool({
|
|
|
2500
2686
|
let res;
|
|
2501
2687
|
try {
|
|
2502
2688
|
res = await ctx.client.applyWorkflowPatch(args.savedId, {
|
|
2689
|
+
...args.projectId !== void 0 ? { projectId: args.projectId } : {},
|
|
2503
2690
|
baseRevision: args.baseRevision,
|
|
2504
2691
|
ops: args.ops,
|
|
2505
2692
|
idempotencyKey,
|
|
@@ -2534,16 +2721,18 @@ var studioWorkflowValidate = defineTool({
|
|
|
2534
2721
|
inputSchema: {
|
|
2535
2722
|
document: WorkflowDocumentSchema.optional().describe("The document to validate. Either this or savedId."),
|
|
2536
2723
|
savedId: savedId.optional().describe("Fetch this saved workflow first and validate against its current document. Either this or document, not both."),
|
|
2724
|
+
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."),
|
|
2537
2725
|
ops: PatchSchema.shape.ops.optional().describe("Ops to apply to the document before projecting it (the same vocabulary and limits as studio_workflow_apply)."),
|
|
2538
2726
|
params: paramsSchema.optional()
|
|
2539
2727
|
},
|
|
2540
2728
|
annotations: { readOnlyHint: true },
|
|
2541
2729
|
handler: async (args, ctx) => {
|
|
2542
2730
|
if (args.document && args.savedId) throw new ToolInputError("pass either `document` or `savedId`, not both");
|
|
2731
|
+
if (args.projectId !== void 0 && !args.savedId) throw new ToolInputError("`projectId` only applies when fetching by `savedId`");
|
|
2543
2732
|
let document = args.document;
|
|
2544
2733
|
if (!document) {
|
|
2545
2734
|
if (!args.savedId) throw new ToolInputError("provide a `document` or a `savedId` to validate");
|
|
2546
|
-
const saved = await ctx.client.getWorkflowDocument(args.savedId);
|
|
2735
|
+
const saved = await ctx.client.getWorkflowDocument(args.savedId, args.projectId !== void 0 ? { projectId: args.projectId } : {});
|
|
2547
2736
|
if (saved.readOnly === true || saved.document === null) {
|
|
2548
2737
|
throw new ToolInputError(
|
|
2549
2738
|
`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.`
|
|
@@ -2562,11 +2751,12 @@ var studioWorkflowScreenshot = defineTool({
|
|
|
2562
2751
|
name: "studio_workflow_screenshot",
|
|
2563
2752
|
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.",
|
|
2564
2753
|
inputSchema: {
|
|
2565
|
-
savedId
|
|
2754
|
+
savedId,
|
|
2755
|
+
projectId: z7.string().min(1).optional().describe("Project id from studio_list_projects the workflow lives in; omit for the default project.")
|
|
2566
2756
|
},
|
|
2567
2757
|
annotations: { readOnlyHint: true },
|
|
2568
2758
|
handler: async (args, ctx) => {
|
|
2569
|
-
return ctx.client.getWorkflowScreenshot(args.savedId);
|
|
2759
|
+
return ctx.client.getWorkflowScreenshot(args.savedId, args.projectId !== void 0 ? { projectId: args.projectId } : {});
|
|
2570
2760
|
}
|
|
2571
2761
|
});
|
|
2572
2762
|
var WORKFLOW_TOOLS = [
|
|
@@ -2599,6 +2789,8 @@ function projectJob(res) {
|
|
|
2599
2789
|
function projectNodeRun(nr) {
|
|
2600
2790
|
return {
|
|
2601
2791
|
nodeId: nr.nodeId,
|
|
2792
|
+
// The node-run (generation) id: what `upstreamGenerations` pins.
|
|
2793
|
+
id: nr.id ?? null,
|
|
2602
2794
|
status: nr.status,
|
|
2603
2795
|
url: nr.resultUrl ?? nr.results?.[0]?.url ?? null,
|
|
2604
2796
|
kind: nr.results?.[0]?.kind ?? null,
|
|
@@ -2852,17 +3044,20 @@ var studioUploadBrandReference = defineTool({
|
|
|
2852
3044
|
}
|
|
2853
3045
|
});
|
|
2854
3046
|
var workflowDefinitionSchema = WorkflowDefinitionSchema.describe(
|
|
2855
|
-
'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"}
|
|
3047
|
+
'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).'
|
|
2856
3048
|
);
|
|
2857
3049
|
var studioRunWorkflow = defineTool({
|
|
2858
3050
|
name: "studio_run_workflow",
|
|
2859
|
-
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.`,
|
|
3051
|
+
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.`,
|
|
2860
3052
|
inputSchema: {
|
|
2861
3053
|
projectId: z8.string().min(1).describe("Project id from studio_list_projects."),
|
|
2862
3054
|
definition: workflowDefinitionSchema.optional(),
|
|
2863
3055
|
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."),
|
|
2864
3056
|
params: z8.record(z8.string(), z8.unknown()).optional().describe("Values for the definition's params."),
|
|
2865
3057
|
nodeIds: z8.array(z8.string()).optional().describe("Run only this subset of nodes (requires an inline `definition` or a `savedId`)."),
|
|
3058
|
+
upstreamGenerations: z8.record(z8.string(), z8.string()).optional().describe(
|
|
3059
|
+
'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.'
|
|
3060
|
+
),
|
|
2866
3061
|
runs: z8.number().int().min(1).max(8).optional().describe("Repeat a node subset N times (1 to 8)."),
|
|
2867
3062
|
visibility: z8.enum(["listed", "unlisted"]).optional().describe("Whether the run shows in the Studio workflow list."),
|
|
2868
3063
|
budgetUsd: z8.number().nonnegative().optional().describe(
|
|
@@ -2886,6 +3081,7 @@ var studioRunWorkflow = defineTool({
|
|
|
2886
3081
|
...args.definition ? { definition: args.definition } : {},
|
|
2887
3082
|
...args.params ? { params: args.params } : {},
|
|
2888
3083
|
...args.nodeIds && args.nodeIds.length > 0 ? { nodeIds: args.nodeIds } : {},
|
|
3084
|
+
...args.upstreamGenerations ? { upstreamGenerations: args.upstreamGenerations } : {},
|
|
2889
3085
|
...args.runs !== void 0 ? { runs: args.runs } : {},
|
|
2890
3086
|
...args.savedId ? { savedId: args.savedId } : {},
|
|
2891
3087
|
...args.visibility ? { visibility: args.visibility } : {},
|
|
@@ -2899,7 +3095,7 @@ var studioRunWorkflow = defineTool({
|
|
|
2899
3095
|
});
|
|
2900
3096
|
var studioGetRun = defineTool({
|
|
2901
3097
|
name: "studio_get_run",
|
|
2902
|
-
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.",
|
|
3098
|
+
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.",
|
|
2903
3099
|
inputSchema: {
|
|
2904
3100
|
runId: z8.string().min(1),
|
|
2905
3101
|
wait: z8.boolean().default(false),
|
|
@@ -2946,14 +3142,16 @@ var studioGetRunOutput = defineTool({
|
|
|
2946
3142
|
});
|
|
2947
3143
|
var studioReplaceCharacterInVideo = defineTool({
|
|
2948
3144
|
name: "studio_replace_character_in_video",
|
|
2949
|
-
description:
|
|
3145
|
+
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.',
|
|
2950
3146
|
inputSchema: {
|
|
2951
3147
|
projectId: z8.string().min(1).describe("Project id from studio_list_projects."),
|
|
2952
3148
|
sceneFrameUrl: httpUrl.describe("Public or signed URL of ONE frame of the target shot (png/jpg)."),
|
|
2953
3149
|
characterSlug: slug.optional().describe("Minted character to insert; its full-body reference (fallback three-quarter, face) is resolved automatically."),
|
|
2954
3150
|
characterReferenceUrl: httpUrl.optional().describe("Explicit character reference image URL (use instead of characterSlug)."),
|
|
2955
3151
|
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."),
|
|
2956
|
-
|
|
3152
|
+
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."),
|
|
3153
|
+
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.'),
|
|
3154
|
+
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."),
|
|
2957
3155
|
motionPrompt: z8.string().optional().describe("Override the motion description for the video node."),
|
|
2958
3156
|
videoModel: z8.string().optional().describe(`Image-to-video model id. Default ${DEFAULT_SWAP_VIDEO_MODEL}, or ${KEYFRAME_SWAP_VIDEO_MODEL} when endFrameUrl is given.`),
|
|
2959
3157
|
editModel: z8.string().default(DEFAULT_SWAP_EDIT_MODEL).describe("Image-edit model id for the still swap."),
|
|
@@ -2984,6 +3182,8 @@ var studioReplaceCharacterInVideo = defineTool({
|
|
|
2984
3182
|
characterImageUrl,
|
|
2985
3183
|
...args.endFrameUrl ? { endFrameUrl: args.endFrameUrl } : {},
|
|
2986
3184
|
...args.swapPrompt ? { swapPrompt: args.swapPrompt } : {},
|
|
3185
|
+
...args.scope ? { scope: args.scope } : {},
|
|
3186
|
+
...args.preserve ? { preserve: args.preserve } : {},
|
|
2987
3187
|
...args.motionPrompt ? { motionPrompt: args.motionPrompt } : {},
|
|
2988
3188
|
editModel: args.editModel,
|
|
2989
3189
|
videoModel,
|
|
@@ -3146,7 +3346,7 @@ function registerTools(server, ctx) {
|
|
|
3146
3346
|
|
|
3147
3347
|
// src/server.ts
|
|
3148
3348
|
var SERVER_NAME = "lumitra-studio";
|
|
3149
|
-
var SERVER_VERSION = "0.
|
|
3349
|
+
var SERVER_VERSION = "0.2.0";
|
|
3150
3350
|
function createStudioMcpServer(opts = {}) {
|
|
3151
3351
|
const config = opts.config ?? resolveConfig(opts.env ?? process.env);
|
|
3152
3352
|
const client = new StudioClient(config, {
|
|
@@ -3160,7 +3360,8 @@ function createStudioMcpServer(opts = {}) {
|
|
|
3160
3360
|
"Lumitra Studio: brand-consistent image generation, character minting, workflow runs and the still-swap video recipe over the hosted Studio API.",
|
|
3161
3361
|
"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.",
|
|
3162
3362
|
"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.",
|
|
3163
|
-
"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."
|
|
3363
|
+
"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.",
|
|
3364
|
+
"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."
|
|
3164
3365
|
].join(" ")
|
|
3165
3366
|
}
|
|
3166
3367
|
);
|