@writepanda/mcp 1.180.0 → 1.191.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/bin/appConnection.mjs +201 -0
- package/bin/server.mjs +741 -109
- package/package.json +1 -1
package/bin/server.mjs
CHANGED
|
@@ -29,7 +29,6 @@
|
|
|
29
29
|
|
|
30
30
|
import { spawn } from "node:child_process";
|
|
31
31
|
import { existsSync, readFileSync } from "node:fs";
|
|
32
|
-
import fs from "node:fs/promises";
|
|
33
32
|
import http from "node:http";
|
|
34
33
|
import os from "node:os";
|
|
35
34
|
import path from "node:path";
|
|
@@ -37,78 +36,26 @@ import process from "node:process";
|
|
|
37
36
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
38
37
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
39
38
|
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
39
|
+
import {
|
|
40
|
+
appLaunchEnv,
|
|
41
|
+
cleanFileValue,
|
|
42
|
+
configDir,
|
|
43
|
+
connectInApp,
|
|
44
|
+
fileCredentials,
|
|
45
|
+
IN_APP_UNAVAILABLE,
|
|
46
|
+
isInAppCaller,
|
|
47
|
+
probeHealth,
|
|
48
|
+
waitForHealth,
|
|
49
|
+
} from "./appConnection.mjs";
|
|
40
50
|
|
|
41
51
|
// ── Credentials + transport ───────────────────────────────────────────
|
|
52
|
+
//
|
|
53
|
+
// Finding and reaching the app lives in appConnection.mjs (unit-tested).
|
|
42
54
|
|
|
43
|
-
|
|
44
|
-
if (process.env.PANDASTUDIO_CONFIG_DIR) return process.env.PANDASTUDIO_CONFIG_DIR;
|
|
45
|
-
if (process.platform === "win32") {
|
|
46
|
-
const appData = process.env.APPDATA ?? path.join(os.homedir(), "AppData", "Roaming");
|
|
47
|
-
return path.join(appData, "pandastudio");
|
|
48
|
-
}
|
|
49
|
-
return path.join(os.homedir(), ".config", "pandastudio");
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const TOKEN_FILE = path.join(configDir(), "token");
|
|
53
|
-
const PORT_FILE = path.join(configDir(), "port");
|
|
55
|
+
const IN_APP = isInAppCaller();
|
|
54
56
|
|
|
55
57
|
async function readCredentials() {
|
|
56
|
-
|
|
57
|
-
const token = (await fs.readFile(TOKEN_FILE, "utf-8")).trim();
|
|
58
|
-
if (!Number.isInteger(port) || port <= 0) throw new Error(`invalid port in ${PORT_FILE}`);
|
|
59
|
-
if (!token) throw new Error(`empty token in ${TOKEN_FILE}`);
|
|
60
|
-
return { port, token };
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
async function probeHealth(port, timeoutMs = 1500) {
|
|
64
|
-
const ctrl = new AbortController();
|
|
65
|
-
const t = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
66
|
-
try {
|
|
67
|
-
const res = await fetch(`http://127.0.0.1:${port}/v1/health`, { signal: ctrl.signal });
|
|
68
|
-
clearTimeout(t);
|
|
69
|
-
return res.ok;
|
|
70
|
-
} catch {
|
|
71
|
-
clearTimeout(t);
|
|
72
|
-
return false;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* One health probe: "up" (answered), "busy" (the socket is open but no answer
|
|
78
|
-
* within `timeoutMs`: the app's main process is stalled, e.g. creating an
|
|
79
|
-
* editor window on a loaded machine) or "down" (connection refused / reset:
|
|
80
|
-
* not running).
|
|
81
|
-
*/
|
|
82
|
-
async function probeHealthState(port, timeoutMs) {
|
|
83
|
-
const ctrl = new AbortController();
|
|
84
|
-
const t = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
85
|
-
try {
|
|
86
|
-
const res = await fetch(`http://127.0.0.1:${port}/v1/health`, { signal: ctrl.signal });
|
|
87
|
-
return res.ok ? "up" : "down";
|
|
88
|
-
} catch {
|
|
89
|
-
return ctrl.signal.aborted ? "busy" : "down";
|
|
90
|
-
} finally {
|
|
91
|
-
clearTimeout(t);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Is the app reachable? A busy app is waited for (up to `busyWaitMs`) rather
|
|
97
|
-
* than reported as not running: a single 1.5 s probe used to fail whenever the
|
|
98
|
-
* main process stalled for longer (window creation takes ~0.4 s idle and
|
|
99
|
-
* several seconds under load), so agents saw "not reachable" at random.
|
|
100
|
-
*/
|
|
101
|
-
async function appReachable(port, { probeMs = 1500, busyWaitMs = 30_000, onBusy } = {}) {
|
|
102
|
-
const deadline = Date.now() + busyWaitMs;
|
|
103
|
-
let warned = false;
|
|
104
|
-
for (;;) {
|
|
105
|
-
const s = await probeHealthState(port, probeMs);
|
|
106
|
-
if (s === "up") return true;
|
|
107
|
-
if (s === "down" || Date.now() >= deadline) return false;
|
|
108
|
-
if (!warned && onBusy) onBusy();
|
|
109
|
-
warned = true;
|
|
110
|
-
probeMs = Math.min(probeMs * 2, 5000);
|
|
111
|
-
}
|
|
58
|
+
return fileCredentials(configDir());
|
|
112
59
|
}
|
|
113
60
|
|
|
114
61
|
function findInstalledApp() {
|
|
@@ -116,7 +63,7 @@ function findInstalledApp() {
|
|
|
116
63
|
// The app records its own location (it can live anywhere on Windows,
|
|
117
64
|
// and in ~/Applications on a Mac).
|
|
118
65
|
try {
|
|
119
|
-
const recorded = readFileSync(path.join(configDir(), "app-path"), "utf-8")
|
|
66
|
+
const recorded = cleanFileValue(readFileSync(path.join(configDir(), "app-path"), "utf-8"));
|
|
120
67
|
if (recorded && existsSync(recorded)) return recorded;
|
|
121
68
|
} catch {
|
|
122
69
|
/* older app, or never launched */
|
|
@@ -159,14 +106,16 @@ async function autoLaunchAndWait(timeoutSec = 60) {
|
|
|
159
106
|
const c = await readCredentials();
|
|
160
107
|
// Busy (stalled main process) is not "not running": wait for it
|
|
161
108
|
// instead of launching a second copy.
|
|
162
|
-
if (await
|
|
109
|
+
if ((await waitForHealth(c.port)).state === "up") return c;
|
|
163
110
|
} catch {
|
|
164
111
|
/* not running */
|
|
165
112
|
}
|
|
166
113
|
|
|
167
114
|
const bin = findInstalledApp();
|
|
168
115
|
try {
|
|
169
|
-
|
|
116
|
+
// appLaunchEnv drops ELECTRON_RUN_AS_NODE: inherited, it made the
|
|
117
|
+
// "launch" start a Node REPL that exits instead of PandaStudio.
|
|
118
|
+
const child = spawn(bin, [], { detached: true, stdio: "ignore", env: appLaunchEnv() });
|
|
170
119
|
child.unref();
|
|
171
120
|
} catch (err) {
|
|
172
121
|
throw new Error(`failed to launch PandaStudio (${bin}): ${err?.message ?? err}`);
|
|
@@ -176,7 +125,7 @@ async function autoLaunchAndWait(timeoutSec = 60) {
|
|
|
176
125
|
while (Date.now() < deadline) {
|
|
177
126
|
try {
|
|
178
127
|
const c = await readCredentials();
|
|
179
|
-
if (await probeHealth(c.port, 2000)) return c;
|
|
128
|
+
if ((await probeHealth(c.port, 2000)).state === "up") return c;
|
|
180
129
|
} catch {
|
|
181
130
|
/* still booting */
|
|
182
131
|
}
|
|
@@ -211,18 +160,50 @@ function httpRequestLocal(port, pathSuffix, { method = "GET", headers = {}, body
|
|
|
211
160
|
});
|
|
212
161
|
}
|
|
213
162
|
|
|
214
|
-
async function
|
|
215
|
-
|
|
216
|
-
const res = await httpRequestLocal(creds.port, "/v1/call", {
|
|
163
|
+
async function postCall(creds, command, args) {
|
|
164
|
+
return httpRequestLocal(creds.port, "/v1/call", {
|
|
217
165
|
method: "POST",
|
|
218
166
|
headers: {
|
|
219
167
|
"Content-Type": "application/json",
|
|
220
168
|
Authorization: `Bearer ${creds.token}`,
|
|
221
169
|
// Set by the app when it runs this server for its own chat panel.
|
|
222
|
-
...(
|
|
170
|
+
...(IN_APP ? { "X-PandaStudio-Caller": "in-app" } : {}),
|
|
223
171
|
},
|
|
224
172
|
body: JSON.stringify({ command, args }),
|
|
225
173
|
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function callPandastudio(command, args = {}) {
|
|
177
|
+
// In-app: the app is running (it spawned us). Never launch it, and fail in
|
|
178
|
+
// seconds with the real reason instead of outliving the client's timeout.
|
|
179
|
+
let creds = IN_APP ? await connectInApp() : await autoLaunchAndWait();
|
|
180
|
+
let res;
|
|
181
|
+
try {
|
|
182
|
+
res = await postCall(creds, command, args);
|
|
183
|
+
} catch (err) {
|
|
184
|
+
if (!IN_APP) throw err;
|
|
185
|
+
throw new Error(
|
|
186
|
+
`${IN_APP_UNAVAILABLE}: the call to 127.0.0.1:${creds.port} failed (${err?.code ?? err?.message ?? err}).`,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
if (IN_APP && res.status === 401) {
|
|
190
|
+
// The app restarted its automation server (new token) after it
|
|
191
|
+
// spawned us. The files hold the current one.
|
|
192
|
+
try {
|
|
193
|
+
const fresh = await fileCredentials(configDir());
|
|
194
|
+
if (fresh.token !== creds.token || fresh.port !== creds.port) {
|
|
195
|
+
creds = fresh;
|
|
196
|
+
res = await postCall(creds, command, args);
|
|
197
|
+
}
|
|
198
|
+
} catch {
|
|
199
|
+
/* reported below */
|
|
200
|
+
}
|
|
201
|
+
if (res.status === 401) {
|
|
202
|
+
throw new Error(
|
|
203
|
+
`${IN_APP_UNAVAILABLE}: PandaStudio rejected the tools' access token (its automation server restarted).`,
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
226
207
|
const text = res.text;
|
|
227
208
|
let body;
|
|
228
209
|
try {
|
|
@@ -565,12 +546,75 @@ const ASSETS_PROP = {
|
|
|
565
546
|
};
|
|
566
547
|
const ANCHOR_PROP = {
|
|
567
548
|
type: "number",
|
|
568
|
-
description: "SOURCE ms
|
|
549
|
+
description: "SOURCE ms of a transcript word; survives cuts.",
|
|
569
550
|
};
|
|
570
551
|
const ANCHOR_END_PROP = { type: "number", description: "Anchor end, SOURCE ms." };
|
|
571
552
|
const KEYFRAME_TARGET_DOC =
|
|
572
553
|
"main = motion region (its target decides fields); overlay: x y scale rotation opacity backdropBlur keyStrength; annotation: x y scale rotation opacity; adjustment: effects, amount, lookIntensity; focus: x y width height roundness feathering blurAmount pixelSize maskOpacity; background-effect: amount strength; mask (regionId = overlay id): x y width height roundness feather expand.";
|
|
573
554
|
|
|
555
|
+
const MOTION_ELEMENT_TYPES = [
|
|
556
|
+
"keyword",
|
|
557
|
+
"chip",
|
|
558
|
+
"stamp",
|
|
559
|
+
"count",
|
|
560
|
+
"steps",
|
|
561
|
+
"slam",
|
|
562
|
+
"behind",
|
|
563
|
+
"lowerThird",
|
|
564
|
+
"highlight",
|
|
565
|
+
"endCard",
|
|
566
|
+
"progress",
|
|
567
|
+
"iconPop",
|
|
568
|
+
"frame",
|
|
569
|
+
"sticker",
|
|
570
|
+
];
|
|
571
|
+
const MOTION_ELEMENT_PROPS = {
|
|
572
|
+
type: { type: "string", enum: MOTION_ELEMENT_TYPES },
|
|
573
|
+
content: {
|
|
574
|
+
type: ["object", "string", "array"],
|
|
575
|
+
description:
|
|
576
|
+
'keyword/chip/stamp/slam/behind/highlight { text } (or a bare string); count { value, to?, prefix?, suffix?, label?, decimals? } (value "20-30" = range); steps { items[], cues? (ms offsets from start) }; lowerThird { title, subtitle? }; endCard { lines: [a, b] }; progress { label?, fromMs?, toMs? }; iconPop { emoji } | { icon }; frame { look: viewfinder | selection | corners, label? }; sticker { image: transparent PNG path, label? }. Latin script only.',
|
|
577
|
+
},
|
|
578
|
+
atMs: { type: "number", description: "Start, edited ms. Or wordId." },
|
|
579
|
+
wordId: {
|
|
580
|
+
type: "string",
|
|
581
|
+
description: "Start on this transcript word; follows it through cuts.",
|
|
582
|
+
},
|
|
583
|
+
offsetMs: { type: "number", description: "Shift from atMs/wordId (may be negative)." },
|
|
584
|
+
durationMs: { type: "number", description: "Default endWordId, else the type's length." },
|
|
585
|
+
endWordId: { type: "string", description: "Run to the end of this word." },
|
|
586
|
+
cueWordIds: {
|
|
587
|
+
type: "array",
|
|
588
|
+
items: { type: "string" },
|
|
589
|
+
description: "steps: one word id per item.",
|
|
590
|
+
},
|
|
591
|
+
style: {
|
|
592
|
+
type: "object",
|
|
593
|
+
properties: {
|
|
594
|
+
family: { type: "string", enum: ["bold", "editorial", "clean", "playful", "paper"] },
|
|
595
|
+
accent: { type: "string" },
|
|
596
|
+
highlight: { type: "string" },
|
|
597
|
+
text: { type: "string" },
|
|
598
|
+
font: { type: "string" },
|
|
599
|
+
size: { type: "string", enum: ["s", "m", "l", "xl"] },
|
|
600
|
+
outline: { type: "boolean" },
|
|
601
|
+
},
|
|
602
|
+
description: "Default: the brand kit.",
|
|
603
|
+
},
|
|
604
|
+
zone: {
|
|
605
|
+
type: ["string", "object"],
|
|
606
|
+
description:
|
|
607
|
+
"auto (clear of the face) | top | upper | center | lower | bottom | { x, y, w? } (% of frame; x,y = centre).",
|
|
608
|
+
},
|
|
609
|
+
layer: { type: "string", enum: ["front", "behind"] },
|
|
610
|
+
sound: {
|
|
611
|
+
type: "string",
|
|
612
|
+
enum: ["auto", "none", "pop", "whoosh", "hit", "tick", "bell"],
|
|
613
|
+
description: "Role for project_compose_soundtrack.",
|
|
614
|
+
},
|
|
615
|
+
zIndex: { type: "number" },
|
|
616
|
+
anchor: { type: "string", enum: ["auto", "free"] },
|
|
617
|
+
};
|
|
574
618
|
const TOOLS = [
|
|
575
619
|
// ── system + discovery ──────────────────────────────────────────
|
|
576
620
|
{
|
|
@@ -660,7 +704,7 @@ const TOOLS = [
|
|
|
660
704
|
{
|
|
661
705
|
name: "system_set_transcription_language",
|
|
662
706
|
description:
|
|
663
|
-
"Set the workspace transcription language. Non-auto uses Whisper, a ~1.1 GB download: tell the user first (system.is-whisper-model-downloaded via pandastudio_call).",
|
|
707
|
+
"Set the workspace transcription language. English = auto (english/en are saved as auto). Non-auto uses Whisper, a ~1.1 GB download: tell the user first (system.is-whisper-model-downloaded via pandastudio_call).",
|
|
664
708
|
inputSchema: {
|
|
665
709
|
type: "object",
|
|
666
710
|
properties: {
|
|
@@ -668,6 +712,8 @@ const TOOLS = [
|
|
|
668
712
|
type: "string",
|
|
669
713
|
enum: [
|
|
670
714
|
"auto",
|
|
715
|
+
"english",
|
|
716
|
+
"en",
|
|
671
717
|
"chinese",
|
|
672
718
|
"japanese",
|
|
673
719
|
"korean",
|
|
@@ -1347,6 +1393,11 @@ const TOOLS = [
|
|
|
1347
1393
|
slots: { type: "object", description: "Templates: changed slots only." },
|
|
1348
1394
|
background: { type: "string", enum: ["solid", "transparent", "glass"] },
|
|
1349
1395
|
html: { type: "string", description: "HTML graphics: full replacement." },
|
|
1396
|
+
durationMs: { type: "number", description: "text-behind length." },
|
|
1397
|
+
templateId: {
|
|
1398
|
+
type: "string",
|
|
1399
|
+
description: "Switch template (e.g. a retired one's replacement); slots map across.",
|
|
1400
|
+
},
|
|
1350
1401
|
expectedRevision: { type: "number" },
|
|
1351
1402
|
},
|
|
1352
1403
|
required: ["overlayId"],
|
|
@@ -1526,16 +1577,33 @@ const TOOLS = [
|
|
|
1526
1577
|
},
|
|
1527
1578
|
{
|
|
1528
1579
|
name: "project_add_transition",
|
|
1529
|
-
description:
|
|
1580
|
+
description:
|
|
1581
|
+
"Place a transition centred on a cut (a clip boundary or a jump cut). kind overlay (fade-black, flash, glitch, torn-paper…) draws a WebM over the cut; kind native (zoom-blur, whip-left, whip-right, whip-up, spin) moves the footage itself (captions/graphics stay put), 350 ms default. Snaps atMs to a cut within snapMs (default 500) and places the transition's default sound.",
|
|
1530
1582
|
inputSchema: {
|
|
1531
1583
|
type: "object",
|
|
1532
1584
|
properties: {
|
|
1533
1585
|
id: { type: "string" },
|
|
1534
1586
|
path: { type: "string" },
|
|
1535
|
-
transitionId: {
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1587
|
+
transitionId: {
|
|
1588
|
+
type: "string",
|
|
1589
|
+
description: "asset_list_transitions id (overlay or native kind).",
|
|
1590
|
+
},
|
|
1591
|
+
file: { type: "string", description: "Custom overlay WebM, instead of transitionId." },
|
|
1592
|
+
atMs: { type: "number", description: "The cut (edited ms)." },
|
|
1593
|
+
durationMs: {
|
|
1594
|
+
type: "number",
|
|
1595
|
+
description: "Default: the catalog's (1000 overlays, 900 torn-paper, 350 native).",
|
|
1596
|
+
},
|
|
1597
|
+
snapMs: {
|
|
1598
|
+
type: "number",
|
|
1599
|
+
description: "Snap to the nearest cut within this many ms. Default 500; 0 = exact.",
|
|
1600
|
+
},
|
|
1601
|
+
sound: {
|
|
1602
|
+
type: "string",
|
|
1603
|
+
description:
|
|
1604
|
+
"Bundled sound id / path. Default: the transition's defaultSoundId. none = silent.",
|
|
1605
|
+
},
|
|
1606
|
+
soundVolume: { type: "number", description: "0-1." },
|
|
1539
1607
|
expectedRevision: { type: "number" },
|
|
1540
1608
|
},
|
|
1541
1609
|
required: ["atMs"],
|
|
@@ -1545,7 +1613,7 @@ const TOOLS = [
|
|
|
1545
1613
|
{
|
|
1546
1614
|
name: "project_add_lower_third",
|
|
1547
1615
|
description:
|
|
1548
|
-
"Render
|
|
1616
|
+
"Render a nameplate and place it at atMs in one call, laid out for the project's aspect. ASYNC: { jobId }; job_wait gives result.overlayId once placed.",
|
|
1549
1617
|
inputSchema: {
|
|
1550
1618
|
type: "object",
|
|
1551
1619
|
properties: {
|
|
@@ -1553,12 +1621,16 @@ const TOOLS = [
|
|
|
1553
1621
|
path: { type: "string" },
|
|
1554
1622
|
name: { type: "string" },
|
|
1555
1623
|
title: { type: "string", description: "Second line (role / handle)." },
|
|
1556
|
-
templateId: {
|
|
1624
|
+
templateId: {
|
|
1625
|
+
type: "string",
|
|
1626
|
+
description:
|
|
1627
|
+
"lt-vox-marker (default, bold marker) | lt-glass-card | lt-minimal-line | lt-bold-bar | lt-logo-name (slots.logo) | lt-duo (slots.name2/title2).",
|
|
1628
|
+
},
|
|
1557
1629
|
atMs: { type: "number" },
|
|
1558
1630
|
aspectRatio: {
|
|
1559
1631
|
type: "string",
|
|
1560
1632
|
enum: ["16:9", "9:16", "1:1"],
|
|
1561
|
-
description: "
|
|
1633
|
+
description: "Default: the project's aspect.",
|
|
1562
1634
|
},
|
|
1563
1635
|
slots: { type: "object", description: "Extra slot values (motion_list)." },
|
|
1564
1636
|
soundUrl: { type: "string", description: 'Default bundled:sound/mouse-click; "none".' },
|
|
@@ -1575,6 +1647,29 @@ const TOOLS = [
|
|
|
1575
1647
|
},
|
|
1576
1648
|
command: "project.add-lower-third",
|
|
1577
1649
|
},
|
|
1650
|
+
{
|
|
1651
|
+
name: "project_add_title_behind",
|
|
1652
|
+
description:
|
|
1653
|
+
"1-3 huge words at head height BEHIND the presenter. ASYNC: job_wait gives regionId, face.",
|
|
1654
|
+
inputSchema: {
|
|
1655
|
+
type: "object",
|
|
1656
|
+
properties: {
|
|
1657
|
+
id: { type: "string" },
|
|
1658
|
+
path: { type: "string" },
|
|
1659
|
+
text: { type: "string" },
|
|
1660
|
+
atMs: { type: "number" },
|
|
1661
|
+
style: { type: "string", enum: ["3d", "bold", "serif"] },
|
|
1662
|
+
durationMs: { type: "number", description: "Default 3000." },
|
|
1663
|
+
accentColor: { type: "string" },
|
|
1664
|
+
position: { type: "string", enum: ["auto", "top", "center"] },
|
|
1665
|
+
animation: { type: "string", enum: ["in-out", "in", "none"] },
|
|
1666
|
+
behind: { type: "boolean", description: "false: in front (no face)." },
|
|
1667
|
+
anchorSourceMs: ANCHOR_PROP,
|
|
1668
|
+
},
|
|
1669
|
+
required: ["text", "atMs"],
|
|
1670
|
+
},
|
|
1671
|
+
command: "project.add-title-behind",
|
|
1672
|
+
},
|
|
1578
1673
|
|
|
1579
1674
|
// ── compose: visual regions ─────────────────────────────────────
|
|
1580
1675
|
{
|
|
@@ -2062,7 +2157,7 @@ const TOOLS = [
|
|
|
2062
2157
|
{
|
|
2063
2158
|
name: "project_render_frame",
|
|
2064
2159
|
description:
|
|
2065
|
-
"Render the composited frame at an edited time to a PNG (matches export). Returns { path, timeMs, maskRect (video rect in 0-1 image fractions, for mapping boxes to spotlight coords), warnings? } (tell the user warnings).",
|
|
2160
|
+
"Render the composited frame at an edited time to a PNG (matches export). Returns { path, timeMs (snapped to the export's frame grid, fps), maskRect (video rect in 0-1 image fractions, for mapping boxes to spotlight coords), warnings? } (tell the user warnings).",
|
|
2066
2161
|
inputSchema: {
|
|
2067
2162
|
type: "object",
|
|
2068
2163
|
properties: {
|
|
@@ -2464,13 +2559,14 @@ const TOOLS = [
|
|
|
2464
2559
|
// ── compose: edit existing regions ─────────────────────────────
|
|
2465
2560
|
{
|
|
2466
2561
|
name: "project_remove_region",
|
|
2467
|
-
description:
|
|
2562
|
+
description:
|
|
2563
|
+
"Delete a region by id (editor.*Regions, motion-element = editor.motionElements, or audioOverlays); linked peers go too.",
|
|
2468
2564
|
inputSchema: {
|
|
2469
2565
|
type: "object",
|
|
2470
2566
|
properties: {
|
|
2471
2567
|
id: { type: "string" },
|
|
2472
2568
|
path: { type: "string" },
|
|
2473
|
-
regionType: { type: "string", enum: REGION_TYPES },
|
|
2569
|
+
regionType: { type: "string", enum: [...REGION_TYPES, "motion-element"] },
|
|
2474
2570
|
regionId: { type: "string" },
|
|
2475
2571
|
expectedRevision: { type: "number" },
|
|
2476
2572
|
},
|
|
@@ -2500,6 +2596,7 @@ const TOOLS = [
|
|
|
2500
2596
|
"spotlight",
|
|
2501
2597
|
"motion",
|
|
2502
2598
|
"adjustment",
|
|
2599
|
+
"motion-element",
|
|
2503
2600
|
"audio-overlay",
|
|
2504
2601
|
],
|
|
2505
2602
|
},
|
|
@@ -2649,12 +2746,14 @@ const TOOLS = [
|
|
|
2649
2746
|
},
|
|
2650
2747
|
{
|
|
2651
2748
|
name: "project_set_crop",
|
|
2652
|
-
description:
|
|
2749
|
+
description:
|
|
2750
|
+
"Crop the main video's source frame (0-1 fractions), e.g. to cut baked-in black bars. Without clipId it applies to the project and every clip (a clip's own crop, like auto-reframe's, wins otherwise). Default 0,0,1,1 (no crop).",
|
|
2653
2751
|
inputSchema: {
|
|
2654
2752
|
type: "object",
|
|
2655
2753
|
properties: {
|
|
2656
2754
|
id: { type: "string" },
|
|
2657
2755
|
path: { type: "string" },
|
|
2756
|
+
clipId: { type: "string", description: "Crop only this clip." },
|
|
2658
2757
|
x: { type: "number" },
|
|
2659
2758
|
y: { type: "number" },
|
|
2660
2759
|
width: { type: "number" },
|
|
@@ -2792,6 +2891,10 @@ const TOOLS = [
|
|
|
2792
2891
|
type: ["boolean", "string"],
|
|
2793
2892
|
description: "streaming / true (-14 LUFS, default), podcast (-16), off / false.",
|
|
2794
2893
|
},
|
|
2894
|
+
frameRate: {
|
|
2895
|
+
type: ["number", "string"],
|
|
2896
|
+
description: "auto (default) | 30 | 60 | source (fastest source).",
|
|
2897
|
+
},
|
|
2795
2898
|
expectedRevision: { type: "number" },
|
|
2796
2899
|
},
|
|
2797
2900
|
},
|
|
@@ -2893,6 +2996,47 @@ const TOOLS = [
|
|
|
2893
2996
|
},
|
|
2894
2997
|
command: "project.add-audio",
|
|
2895
2998
|
},
|
|
2999
|
+
{
|
|
3000
|
+
name: "project_add_sound_cues",
|
|
3001
|
+
description:
|
|
3002
|
+
"Place many timed SFX in one write (one undo). Each cue = an audio overlay. Same group again REPLACES its cues ([] clears). Returns overlayIds + warnings.",
|
|
3003
|
+
inputSchema: {
|
|
3004
|
+
type: "object",
|
|
3005
|
+
properties: {
|
|
3006
|
+
id: { type: "string" },
|
|
3007
|
+
path: { type: "string" },
|
|
3008
|
+
cues: {
|
|
3009
|
+
type: "array",
|
|
3010
|
+
maxItems: 500,
|
|
3011
|
+
items: {
|
|
3012
|
+
type: "object",
|
|
3013
|
+
properties: {
|
|
3014
|
+
sound: {
|
|
3015
|
+
type: "string",
|
|
3016
|
+
description: "Bundled id or absolute path.",
|
|
3017
|
+
},
|
|
3018
|
+
atMs: { type: "number", description: "Edited-timeline ms." },
|
|
3019
|
+
volume: { type: "number", description: "0-2." },
|
|
3020
|
+
durationMs: { type: "number" },
|
|
3021
|
+
sourceStartMs: { type: "number" },
|
|
3022
|
+
fadeInMs: { type: "number" },
|
|
3023
|
+
fadeOutMs: { type: "number" },
|
|
3024
|
+
anchorSourceMs: {
|
|
3025
|
+
type: "number",
|
|
3026
|
+
description: "SOURCE ms word anchor; omit for graphics.",
|
|
3027
|
+
},
|
|
3028
|
+
},
|
|
3029
|
+
required: ["sound", "atMs"],
|
|
3030
|
+
},
|
|
3031
|
+
},
|
|
3032
|
+
group: { type: "string", description: 'e.g. "sfx".' },
|
|
3033
|
+
volume: { type: "number", description: "Cue default, 0.8." },
|
|
3034
|
+
expectedRevision: { type: "number" },
|
|
3035
|
+
},
|
|
3036
|
+
required: ["cues"],
|
|
3037
|
+
},
|
|
3038
|
+
command: "project.add-sound-cues",
|
|
3039
|
+
},
|
|
2896
3040
|
{
|
|
2897
3041
|
name: "project_remove_audio",
|
|
2898
3042
|
description:
|
|
@@ -3169,13 +3313,14 @@ const TOOLS = [
|
|
|
3169
3313
|
{
|
|
3170
3314
|
name: "transcript_transcribe",
|
|
3171
3315
|
description:
|
|
3172
|
-
"Transcribe clips with word timestamps
|
|
3316
|
+
"Transcribe clips with word timestamps (default: only clips without words). To REDO a transcript (wrong language, bad run) fix the language first, then force: true (all clips) or clipId. Word fixes are re-applied; tell the user about result droppedWordEdits[] (fixes that may need redoing). ASYNC: { jobId }, job_wait.",
|
|
3173
3317
|
inputSchema: {
|
|
3174
3318
|
type: "object",
|
|
3175
3319
|
properties: {
|
|
3176
3320
|
id: { type: "string" },
|
|
3177
3321
|
path: { type: "string" },
|
|
3178
|
-
clipId: { type: "string" },
|
|
3322
|
+
clipId: { type: "string", description: "Only this clip, even if already transcribed." },
|
|
3323
|
+
force: { type: "boolean", description: "Re-transcribe clips that already have words." },
|
|
3179
3324
|
},
|
|
3180
3325
|
},
|
|
3181
3326
|
command: "transcript.transcribe",
|
|
@@ -3364,7 +3509,8 @@ const TOOLS = [
|
|
|
3364
3509
|
},
|
|
3365
3510
|
{
|
|
3366
3511
|
name: "caption_set_template",
|
|
3367
|
-
description:
|
|
3512
|
+
description:
|
|
3513
|
+
"Pick a caption template (default glowStack); clears style overrides. Emphasis templates (hormoziEmphasis, tiltedBox, serifItalic, condensedCaps, scriptKeyword) style the IMPORTANT word of each phrase, not the spoken one; emphasis is detected on apply when missing (result.emphasis).",
|
|
3368
3514
|
inputSchema: {
|
|
3369
3515
|
type: "object",
|
|
3370
3516
|
properties: {
|
|
@@ -3389,6 +3535,11 @@ const TOOLS = [
|
|
|
3389
3535
|
"matrixDecode",
|
|
3390
3536
|
"glitchRgb",
|
|
3391
3537
|
"blendDifference",
|
|
3538
|
+
"hormoziEmphasis",
|
|
3539
|
+
"tiltedBox",
|
|
3540
|
+
"serifItalic",
|
|
3541
|
+
"condensedCaps",
|
|
3542
|
+
"scriptKeyword",
|
|
3392
3543
|
],
|
|
3393
3544
|
},
|
|
3394
3545
|
expectedRevision: { type: "number" },
|
|
@@ -3400,7 +3551,8 @@ const TOOLS = [
|
|
|
3400
3551
|
|
|
3401
3552
|
{
|
|
3402
3553
|
name: "caption_set_style",
|
|
3403
|
-
description:
|
|
3554
|
+
description:
|
|
3555
|
+
"Override caption style fields (template kept); pass only what changes. highlightMode emphasis/both detects emphasis words when missing (result.emphasis).",
|
|
3404
3556
|
inputSchema: {
|
|
3405
3557
|
type: "object",
|
|
3406
3558
|
properties: {
|
|
@@ -3417,11 +3569,45 @@ const TOOLS = [
|
|
|
3417
3569
|
strokeWidth: { type: "number" },
|
|
3418
3570
|
fontSize: { type: "string", description: "e.g. '2.6rem' (1-5rem)." },
|
|
3419
3571
|
uppercase: { type: "boolean" },
|
|
3572
|
+
highlightMode: {
|
|
3573
|
+
type: "string",
|
|
3574
|
+
enum: ["spoken", "emphasis", "both"],
|
|
3575
|
+
description: "spoken = word being said; emphasis = key words all group long.",
|
|
3576
|
+
},
|
|
3577
|
+
emphasisColor: { type: "string" },
|
|
3578
|
+
emphasisBackgroundColor: { type: "string", description: "'transparent' = no box." },
|
|
3579
|
+
emphasisScale: { type: "number", description: "0.5-2." },
|
|
3580
|
+
emphasisItalic: { type: "boolean" },
|
|
3581
|
+
emphasisFontFamily: {
|
|
3582
|
+
type: "string",
|
|
3583
|
+
description: "Engine font, e.g. Great Vibes, Playfair Display, Bebas Neue, Anton.",
|
|
3584
|
+
},
|
|
3585
|
+
boxRotation: { type: "number", description: "Box tilt degrees 0-15." },
|
|
3420
3586
|
expectedRevision: { type: "number" },
|
|
3421
3587
|
},
|
|
3422
3588
|
},
|
|
3423
3589
|
command: "caption.set-style",
|
|
3424
3590
|
},
|
|
3591
|
+
{
|
|
3592
|
+
name: "caption_mark_emphasis",
|
|
3593
|
+
description:
|
|
3594
|
+
"Set the key words emphasis caption styles mark. No wordIds: detect from the speech map (strong moments + numbers; hand marks kept unless reset). wordIds: mark (emphasis true) or unmark (false). Returns { emphasizedWords, words }.",
|
|
3595
|
+
inputSchema: {
|
|
3596
|
+
type: "object",
|
|
3597
|
+
properties: {
|
|
3598
|
+
id: { type: "string" },
|
|
3599
|
+
path: { type: "string" },
|
|
3600
|
+
wordIds: { type: "array", items: { type: "string" }, description: "From transcript_get." },
|
|
3601
|
+
emphasis: { type: "boolean", description: "With wordIds; default true." },
|
|
3602
|
+
detect: { type: "boolean" },
|
|
3603
|
+
reset: { type: "boolean", description: "Detection replaces hand marks too." },
|
|
3604
|
+
minGapMs: { type: "number", description: "Default 1200." },
|
|
3605
|
+
everyMs: { type: "number", description: "Default 2500; lower = more words." },
|
|
3606
|
+
expectedRevision: { type: "number" },
|
|
3607
|
+
},
|
|
3608
|
+
},
|
|
3609
|
+
command: "caption.mark-emphasis",
|
|
3610
|
+
},
|
|
3425
3611
|
{
|
|
3426
3612
|
name: "caption_move",
|
|
3427
3613
|
description:
|
|
@@ -3455,14 +3641,20 @@ const TOOLS = [
|
|
|
3455
3641
|
{
|
|
3456
3642
|
name: "media_generate_image",
|
|
3457
3643
|
description:
|
|
3458
|
-
"Generate one image
|
|
3644
|
+
"Generate one image on the user's own image connector (auto: Replicate, else Higgsfield; their credits). Returns { imagePath, provider, model, transparent }. transparent=true: trimmed PNG sticker. No connector: code NO_IMAGE_CONNECTOR, so ask the user for their own images. Each call is a paid generation.",
|
|
3459
3645
|
inputSchema: {
|
|
3460
3646
|
type: "object",
|
|
3461
3647
|
properties: {
|
|
3462
3648
|
prompt: { type: "string", description: "Keep text out of the image." },
|
|
3463
|
-
aspectRatio: {
|
|
3649
|
+
aspectRatio: {
|
|
3650
|
+
type: "string",
|
|
3651
|
+
enum: ["1:1", "3:2", "2:3", "4:3", "3:4", "16:9", "9:16"],
|
|
3652
|
+
description: "Default 3:2. 16:9 / 9:16 for full-frame video stills.",
|
|
3653
|
+
},
|
|
3464
3654
|
quality: { type: "string", enum: ["low", "medium", "high"] },
|
|
3465
3655
|
referenceImagePath: { type: "string", description: "Style reference (path or URL)." },
|
|
3656
|
+
transparent: { type: "boolean", description: "Background removed (sticker PNG)." },
|
|
3657
|
+
provider: { type: "string", enum: ["auto", "replicate", "higgsfield"] },
|
|
3466
3658
|
outputName: { type: "string" },
|
|
3467
3659
|
},
|
|
3468
3660
|
required: ["prompt"],
|
|
@@ -3613,6 +3805,350 @@ const TOOLS = [
|
|
|
3613
3805
|
},
|
|
3614
3806
|
command: "media.generate-presenter",
|
|
3615
3807
|
},
|
|
3808
|
+
{
|
|
3809
|
+
name: "project_inspect_footage",
|
|
3810
|
+
description:
|
|
3811
|
+
"Import checks before styling camera footage: flat/log (needs a grade), a flat green/blue backdrop (key it), rotation (vertical phone/camera clips). Returns per clip { flat, backdrop, rotation, contrast, blackLevel, saturation, suggestions:[{ why, verb, args }] }; run the suggested verbs.",
|
|
3812
|
+
inputSchema: {
|
|
3813
|
+
type: "object",
|
|
3814
|
+
properties: { id: { type: "string" }, path: { type: "string" }, clipId: { type: "string" } },
|
|
3815
|
+
},
|
|
3816
|
+
command: "project.inspect-footage",
|
|
3817
|
+
},
|
|
3818
|
+
{
|
|
3819
|
+
name: "project_speech_map",
|
|
3820
|
+
description:
|
|
3821
|
+
"The words the speaker STRESSES and where they land on the edited timeline (any language, incl. code-switched speech): measured from the audio (louder, sharp attack, pause before, drawn out) plus numbers and English key terms. Returns { durationMs, moments:[{ atMs, text, phrase, strength 1-3, emphasis, reasons, suggest }] }. Use as cues: zooms on strength 2-3, keyword graphics on the phrase, hits on strength 3. Needs a transcript.",
|
|
3822
|
+
inputSchema: {
|
|
3823
|
+
type: "object",
|
|
3824
|
+
properties: {
|
|
3825
|
+
id: { type: "string" },
|
|
3826
|
+
path: { type: "string" },
|
|
3827
|
+
minGapMs: { type: "number", description: "Default 2500." },
|
|
3828
|
+
everyMs: {
|
|
3829
|
+
type: "number",
|
|
3830
|
+
description:
|
|
3831
|
+
"~one moment per this long. Default 5000; Shorts 3000-4000; long-form 8000-15000.",
|
|
3832
|
+
},
|
|
3833
|
+
words: { type: "boolean", description: "Also return every word's emphasis." },
|
|
3834
|
+
},
|
|
3835
|
+
},
|
|
3836
|
+
command: "project.speech-map",
|
|
3837
|
+
},
|
|
3838
|
+
{
|
|
3839
|
+
name: "project_compose_soundtrack",
|
|
3840
|
+
description:
|
|
3841
|
+
"Score this edit: music + sound design composed FROM THE PROJECT'S OWN TIMELINE (zooms, graphics, pops, cuts, speech, stressed words) and placed under the voice with ducking; takes over zoom/overlay default sounds; replaces the previous score on re-run. Call LAST, after cuts/zooms/graphics. dryRun returns the score for media_compose_soundtrack. Returns { audioPath, scorePath, events, heroMoments, acts, soundsTakenOver, replaced }.",
|
|
3842
|
+
inputSchema: {
|
|
3843
|
+
type: "object",
|
|
3844
|
+
properties: {
|
|
3845
|
+
id: { type: "string" },
|
|
3846
|
+
path: { type: "string" },
|
|
3847
|
+
style: { type: "string", enum: ["talking-head", "short", "promo", "calm"] },
|
|
3848
|
+
mood: { type: "string", enum: ["minor", "major"] },
|
|
3849
|
+
bpm: { type: "number" },
|
|
3850
|
+
sfx: { type: "boolean", description: "Default true." },
|
|
3851
|
+
useSpeech: {
|
|
3852
|
+
type: "boolean",
|
|
3853
|
+
description: "Hero hits on the most stressed words. Default true.",
|
|
3854
|
+
},
|
|
3855
|
+
cues: {
|
|
3856
|
+
type: "array",
|
|
3857
|
+
items: {
|
|
3858
|
+
type: "object",
|
|
3859
|
+
properties: {
|
|
3860
|
+
atMs: { type: "number" },
|
|
3861
|
+
strength: { type: "number" },
|
|
3862
|
+
label: { type: "string" },
|
|
3863
|
+
},
|
|
3864
|
+
required: ["atMs"],
|
|
3865
|
+
},
|
|
3866
|
+
description: "Extra moments, e.g. a graphic's own cue times.",
|
|
3867
|
+
},
|
|
3868
|
+
musicVolume: { type: "number", description: "0-2. Default by style." },
|
|
3869
|
+
takeOverSounds: { type: "boolean", description: "Default true." },
|
|
3870
|
+
dryRun: { type: "boolean" },
|
|
3871
|
+
seed: { type: "number" },
|
|
3872
|
+
},
|
|
3873
|
+
},
|
|
3874
|
+
command: "project.compose-soundtrack",
|
|
3875
|
+
},
|
|
3876
|
+
// ── native motion elements ──────────────────────────────────────
|
|
3877
|
+
{
|
|
3878
|
+
name: "project_add_motion_element",
|
|
3879
|
+
description:
|
|
3880
|
+
"Native motion element: an editable, word-timed graphic the engine draws (preview = export, no HTML render): keyword headline, chip, stamp, count (number/range rolls up), steps, slam (+ camera kick), behind (word behind the speaker), lowerThird, highlight, endCard, progress, iconPop, frame (viewfinder / selection box / corners around the speaker). Anchor with wordId (follows the word through cuts) or atMs. Brand kit by default. Returns { elementId, element }.",
|
|
3881
|
+
inputSchema: {
|
|
3882
|
+
type: "object",
|
|
3883
|
+
properties: {
|
|
3884
|
+
id: { type: "string" },
|
|
3885
|
+
path: { type: "string" },
|
|
3886
|
+
...MOTION_ELEMENT_PROPS,
|
|
3887
|
+
expectedRevision: { type: "number" },
|
|
3888
|
+
},
|
|
3889
|
+
required: ["type", "content"],
|
|
3890
|
+
},
|
|
3891
|
+
command: "project.add-motion-element",
|
|
3892
|
+
},
|
|
3893
|
+
{
|
|
3894
|
+
name: "project_add_motion_elements",
|
|
3895
|
+
description:
|
|
3896
|
+
"Add many native motion elements in one edit (one revision). All or nothing: an invalid one rejects the batch with 'element <i>: <reason>'. Returns { elementIds, elements }.",
|
|
3897
|
+
inputSchema: {
|
|
3898
|
+
type: "object",
|
|
3899
|
+
properties: {
|
|
3900
|
+
id: { type: "string" },
|
|
3901
|
+
path: { type: "string" },
|
|
3902
|
+
elements: {
|
|
3903
|
+
type: "array",
|
|
3904
|
+
items: {
|
|
3905
|
+
type: "object",
|
|
3906
|
+
properties: MOTION_ELEMENT_PROPS,
|
|
3907
|
+
required: ["type", "content"],
|
|
3908
|
+
},
|
|
3909
|
+
},
|
|
3910
|
+
expectedRevision: { type: "number" },
|
|
3911
|
+
},
|
|
3912
|
+
required: ["elements"],
|
|
3913
|
+
},
|
|
3914
|
+
command: "project.add-motion-elements",
|
|
3915
|
+
},
|
|
3916
|
+
{
|
|
3917
|
+
name: "project_update_motion_element",
|
|
3918
|
+
description:
|
|
3919
|
+
"Change a native motion element; pass only what changes. style merges (field null = default); zone/layer/sound/zIndex null = default; atMs/wordId re-anchors and keeps the length. Editing a style-edit element makes it yours (re-runs keep it).",
|
|
3920
|
+
inputSchema: {
|
|
3921
|
+
type: "object",
|
|
3922
|
+
properties: {
|
|
3923
|
+
id: { type: "string" },
|
|
3924
|
+
path: { type: "string" },
|
|
3925
|
+
elementId: { type: "string" },
|
|
3926
|
+
...MOTION_ELEMENT_PROPS,
|
|
3927
|
+
expectedRevision: { type: "number" },
|
|
3928
|
+
},
|
|
3929
|
+
required: ["elementId"],
|
|
3930
|
+
},
|
|
3931
|
+
command: "project.update-motion-element",
|
|
3932
|
+
},
|
|
3933
|
+
{
|
|
3934
|
+
name: "project_remove_motion_element",
|
|
3935
|
+
description: "Remove a native motion element by id.",
|
|
3936
|
+
inputSchema: {
|
|
3937
|
+
type: "object",
|
|
3938
|
+
properties: {
|
|
3939
|
+
id: { type: "string" },
|
|
3940
|
+
path: { type: "string" },
|
|
3941
|
+
elementId: { type: "string" },
|
|
3942
|
+
expectedRevision: { type: "number" },
|
|
3943
|
+
},
|
|
3944
|
+
required: ["elementId"],
|
|
3945
|
+
},
|
|
3946
|
+
command: "project.remove-motion-element",
|
|
3947
|
+
},
|
|
3948
|
+
{
|
|
3949
|
+
name: "project_list_motion_elements",
|
|
3950
|
+
description:
|
|
3951
|
+
"List placed native motion elements (id, type, span, content, style, zone, layer, sound, wordId, origin) with one-line summaries. catalog=true adds every type's content shape and defaults.",
|
|
3952
|
+
inputSchema: {
|
|
3953
|
+
type: "object",
|
|
3954
|
+
properties: {
|
|
3955
|
+
id: { type: "string" },
|
|
3956
|
+
path: { type: "string" },
|
|
3957
|
+
type: { type: "string", enum: MOTION_ELEMENT_TYPES },
|
|
3958
|
+
catalog: { type: "boolean" },
|
|
3959
|
+
},
|
|
3960
|
+
},
|
|
3961
|
+
command: "project.list-motion-elements",
|
|
3962
|
+
},
|
|
3963
|
+
{
|
|
3964
|
+
name: "project_style_edit",
|
|
3965
|
+
description:
|
|
3966
|
+
"Style a talking-head / Short in one call: inspect-footage (applyFixes applies grade/key), speech-map, native motion elements on the stressed moments (numbers → count, strength-3 English statement → slam, other heroes → keyword/stamp, code-switch key terms → chip, first moment → hook, last 4 s → end card, long-form topic turns → lower third) + silent zooms (1.5x/1.25x, 1.8x hero), then compose-soundtrack. paper-cut = the Vox paper-cut look (serif ink on torn paper strips, newsprint backdrop, speaker cut out with a white keyline in the lower half, paper foley, no zooms; look=false keeps your background/framing). Latin text only: for non-Latin speech pass texts { wordId: 'English keyword' } (else zoom only). Re-runs replace only what style-edit placed. Run after cuts. dryRun returns the plan.",
|
|
3967
|
+
inputSchema: {
|
|
3968
|
+
type: "object",
|
|
3969
|
+
properties: {
|
|
3970
|
+
id: { type: "string" },
|
|
3971
|
+
path: { type: "string" },
|
|
3972
|
+
style: {
|
|
3973
|
+
type: "string",
|
|
3974
|
+
enum: ["bold-short", "editorial-long", "clean-tutorial", "paper-cut"],
|
|
3975
|
+
},
|
|
3976
|
+
format: {
|
|
3977
|
+
type: "string",
|
|
3978
|
+
enum: ["short", "long"],
|
|
3979
|
+
description: "Density; default from aspect (9:16 = short).",
|
|
3980
|
+
},
|
|
3981
|
+
applyFixes: { type: "boolean", description: "Apply inspect-footage fixes. Default false." },
|
|
3982
|
+
backdrop: {
|
|
3983
|
+
type: "string",
|
|
3984
|
+
description:
|
|
3985
|
+
"Background behind a keyed-out speaker (applyFixes) and the paper-cut backdrop: wallpaper id, colour or CSS gradient. Default: the style's studio gradient when no background is set; paper-cut uses /wallpapers/paper-cream.jpg.",
|
|
3986
|
+
},
|
|
3987
|
+
look: {
|
|
3988
|
+
type: "boolean",
|
|
3989
|
+
description:
|
|
3990
|
+
"paper-cut only: also set the look (paper backdrop, speaker cutout + white keyline, lower-half framing). Default true.",
|
|
3991
|
+
},
|
|
3992
|
+
texts: {
|
|
3993
|
+
type: "object",
|
|
3994
|
+
additionalProperties: { type: "string" },
|
|
3995
|
+
description: "{ [wordId]: 'English keyword' } overrides.",
|
|
3996
|
+
},
|
|
3997
|
+
endCard: {
|
|
3998
|
+
type: "array",
|
|
3999
|
+
items: { type: "string" },
|
|
4000
|
+
description: "[line1, line2]. Default: hook + hero line.",
|
|
4001
|
+
},
|
|
4002
|
+
accent: { type: "string", description: "Accent colour for every element; default brand." },
|
|
4003
|
+
highlight: { type: "string", description: "Emphasis colour; default brand." },
|
|
4004
|
+
zooms: { type: "boolean", description: "Default true." },
|
|
4005
|
+
score: { type: "boolean", description: "Run compose-soundtrack. Default true." },
|
|
4006
|
+
mood: { type: "string", enum: ["minor", "major"] },
|
|
4007
|
+
dryRun: { type: "boolean" },
|
|
4008
|
+
expectedRevision: { type: "number" },
|
|
4009
|
+
},
|
|
4010
|
+
},
|
|
4011
|
+
command: "project.style-edit",
|
|
4012
|
+
},
|
|
4013
|
+
{
|
|
4014
|
+
name: "media_compose_soundtrack",
|
|
4015
|
+
description:
|
|
4016
|
+
"Compose + render an original soundtrack (music, beats, SFX) from a score on the SAME timeline as the edit, so hits land on their moments. Synthesized locally: no key, no licensing, ~1 s per 15 s. Plan cues from the edit first, set bpm so cuts land on beats, then place sounds on cues. Returns { audioPath, scorePath, durationMs, lufs, truePeakDb, events, cues, warnings }; place with project_add_audio. Format + recipes: skill reference/soundtrack.md.",
|
|
4017
|
+
inputSchema: {
|
|
4018
|
+
type: "object",
|
|
4019
|
+
properties: {
|
|
4020
|
+
score: {
|
|
4021
|
+
type: "object",
|
|
4022
|
+
description: "The score. Pass score or scorePath.",
|
|
4023
|
+
properties: {
|
|
4024
|
+
duration: { type: "number", description: "Seconds (max 600)." },
|
|
4025
|
+
seed: { type: "number" },
|
|
4026
|
+
cues: {
|
|
4027
|
+
type: "object",
|
|
4028
|
+
description: 'Named moments: { "slam": 3.0, "logo": "slam+9.95" }.',
|
|
4029
|
+
additionalProperties: { type: ["number", "string"] },
|
|
4030
|
+
},
|
|
4031
|
+
events: {
|
|
4032
|
+
type: "array",
|
|
4033
|
+
items: {
|
|
4034
|
+
type: "object",
|
|
4035
|
+
properties: {
|
|
4036
|
+
at: { type: ["number", "string"], description: 'Seconds, a cue, or "cue+0.2".' },
|
|
4037
|
+
sound: {
|
|
4038
|
+
type: "string",
|
|
4039
|
+
enum: [
|
|
4040
|
+
"kick",
|
|
4041
|
+
"snare",
|
|
4042
|
+
"clap",
|
|
4043
|
+
"hat",
|
|
4044
|
+
"openhat",
|
|
4045
|
+
"tick",
|
|
4046
|
+
"click",
|
|
4047
|
+
"blip",
|
|
4048
|
+
"pop",
|
|
4049
|
+
"typing",
|
|
4050
|
+
"whoosh",
|
|
4051
|
+
"riser",
|
|
4052
|
+
"downlifter",
|
|
4053
|
+
"impact",
|
|
4054
|
+
"boom",
|
|
4055
|
+
"subdrop",
|
|
4056
|
+
"shimmer",
|
|
4057
|
+
"bass",
|
|
4058
|
+
"pluck",
|
|
4059
|
+
"pad",
|
|
4060
|
+
"lead",
|
|
4061
|
+
"bell",
|
|
4062
|
+
"keys",
|
|
4063
|
+
],
|
|
4064
|
+
},
|
|
4065
|
+
gain: { type: "number" },
|
|
4066
|
+
pan: { type: "number", description: "-1 left .. 1 right" },
|
|
4067
|
+
dur: { type: "number" },
|
|
4068
|
+
note: { type: ["string", "number"], description: '"A3" or MIDI' },
|
|
4069
|
+
notes: { type: "array", items: { type: ["string", "number"] } },
|
|
4070
|
+
chord: { type: "string", description: '"Am", "Fmaj7"' },
|
|
4071
|
+
from: { type: "number", description: "Sweep start Hz" },
|
|
4072
|
+
to: { type: "number", description: "Sweep end Hz" },
|
|
4073
|
+
bright: { type: "number", description: "0 dark .. 1 bright" },
|
|
4074
|
+
reverb: { type: "number", description: "0..1 send" },
|
|
4075
|
+
repeat: {
|
|
4076
|
+
type: "object",
|
|
4077
|
+
properties: {
|
|
4078
|
+
count: { type: "number" },
|
|
4079
|
+
every: { type: "number" },
|
|
4080
|
+
gainStep: { type: "number" },
|
|
4081
|
+
alternatePan: { type: "boolean" },
|
|
4082
|
+
jitter: { type: "number" },
|
|
4083
|
+
},
|
|
4084
|
+
},
|
|
4085
|
+
},
|
|
4086
|
+
required: ["at", "sound"],
|
|
4087
|
+
},
|
|
4088
|
+
},
|
|
4089
|
+
sections: {
|
|
4090
|
+
type: "array",
|
|
4091
|
+
items: {
|
|
4092
|
+
type: "object",
|
|
4093
|
+
properties: {
|
|
4094
|
+
from: { type: ["number", "string"] },
|
|
4095
|
+
to: { type: ["number", "string"] },
|
|
4096
|
+
bpm: { type: "number" },
|
|
4097
|
+
chords: { type: "array", items: { type: "string" } },
|
|
4098
|
+
chordBeats: { type: "number", description: "Beats per chord, default 4." },
|
|
4099
|
+
drums: {
|
|
4100
|
+
type: "string",
|
|
4101
|
+
enum: ["four-on-floor", "backbeat", "half-time", "breakbeat", "none"],
|
|
4102
|
+
},
|
|
4103
|
+
hats: { type: "string", enum: ["16ths", "8ths", "offbeat", "none"] },
|
|
4104
|
+
bass: { type: "string", enum: ["pulse", "octave", "root", "sustain", "none"] },
|
|
4105
|
+
arp: { type: "string", enum: ["up", "down", "updown", "none"] },
|
|
4106
|
+
arpRate: { type: "string", enum: ["4ths", "8ths", "16ths"] },
|
|
4107
|
+
pad: { type: ["boolean", "number"] },
|
|
4108
|
+
whooshOnChange: { type: "boolean" },
|
|
4109
|
+
melody: {
|
|
4110
|
+
type: "object",
|
|
4111
|
+
properties: {
|
|
4112
|
+
notes: { type: "array", items: { type: ["string", "number", "null"] } },
|
|
4113
|
+
rate: { type: "string", enum: ["4ths", "8ths", "16ths"] },
|
|
4114
|
+
sound: { type: "string", enum: ["pluck", "lead", "bell", "keys"] },
|
|
4115
|
+
gain: { type: "number" },
|
|
4116
|
+
length: { type: "number" },
|
|
4117
|
+
reverb: { type: "number" },
|
|
4118
|
+
},
|
|
4119
|
+
},
|
|
4120
|
+
gain: { type: "number" },
|
|
4121
|
+
mix: {
|
|
4122
|
+
type: "object",
|
|
4123
|
+
description: "Per-part levels 0-2: drums, hats, bass, arp, pad, melody.",
|
|
4124
|
+
},
|
|
4125
|
+
},
|
|
4126
|
+
required: ["from", "to", "bpm"],
|
|
4127
|
+
},
|
|
4128
|
+
},
|
|
4129
|
+
master: {
|
|
4130
|
+
type: "object",
|
|
4131
|
+
properties: {
|
|
4132
|
+
lufs: { type: "number", description: "Default -14." },
|
|
4133
|
+
ceiling: { type: "number", description: "True-peak dBFS, default -1." },
|
|
4134
|
+
fadeIn: { type: "number" },
|
|
4135
|
+
fadeOut: { type: "number" },
|
|
4136
|
+
drive: { type: "number", description: "1 clean .. 4 heavy, default 1.3." },
|
|
4137
|
+
room: { type: "number", description: "Reverb size 0..1." },
|
|
4138
|
+
},
|
|
4139
|
+
},
|
|
4140
|
+
},
|
|
4141
|
+
required: ["duration"],
|
|
4142
|
+
},
|
|
4143
|
+
scorePath: {
|
|
4144
|
+
type: "string",
|
|
4145
|
+
description: "Absolute path to a score JSON (e.g. a previous scorePath).",
|
|
4146
|
+
},
|
|
4147
|
+
name: { type: "string", description: "File name stem." },
|
|
4148
|
+
},
|
|
4149
|
+
},
|
|
4150
|
+
command: "media.compose-soundtrack",
|
|
4151
|
+
},
|
|
3616
4152
|
{
|
|
3617
4153
|
name: "media_generate_music",
|
|
3618
4154
|
description:
|
|
@@ -3654,6 +4190,28 @@ const TOOLS = [
|
|
|
3654
4190
|
},
|
|
3655
4191
|
command: "media.import",
|
|
3656
4192
|
},
|
|
4193
|
+
{
|
|
4194
|
+
name: "media_download_url",
|
|
4195
|
+
description:
|
|
4196
|
+
"Download a video or its audio from a page link (YouTube, Vimeo...). Only content the user has rights to. ASYNC: job_wait gives { path, title, durationMs }.",
|
|
4197
|
+
inputSchema: {
|
|
4198
|
+
type: "object",
|
|
4199
|
+
properties: {
|
|
4200
|
+
url: { type: "string" },
|
|
4201
|
+
format: { type: "string", enum: ["video", "audio"] },
|
|
4202
|
+
maxHeight: { type: "number", description: "Default 1080." },
|
|
4203
|
+
startMs: { type: "number" },
|
|
4204
|
+
endMs: { type: "number" },
|
|
4205
|
+
maxMinutes: { type: "number", description: "Default 120." },
|
|
4206
|
+
name: { type: "string" },
|
|
4207
|
+
addToProject: { type: "string", description: "Project id." },
|
|
4208
|
+
as: { type: "string", enum: ["clip", "overlay", "audio"] },
|
|
4209
|
+
atMs: { type: "number" },
|
|
4210
|
+
},
|
|
4211
|
+
required: ["url"],
|
|
4212
|
+
},
|
|
4213
|
+
command: "media.download-url",
|
|
4214
|
+
},
|
|
3657
4215
|
{
|
|
3658
4216
|
name: "media_generate_sound_effect",
|
|
3659
4217
|
description:
|
|
@@ -3743,7 +4301,7 @@ const TOOLS = [
|
|
|
3743
4301
|
recipe: {
|
|
3744
4302
|
type: "object",
|
|
3745
4303
|
description:
|
|
3746
|
-
"{ id?, title, description, category, footage?, aspectRatio?, fields: [{ key, label, type: text|longtext|select|color, default?, options?, required?, help? }], prompt, style?: { aspectRatio?, colorCorrection?, lut?: { preset, intensity? }, background?: { mode, image? }, cameraLayout?, captions?: { enabled, templateId? }, wallpaper? }, checklist: string[] }",
|
|
4304
|
+
"{ id?, title, description, category, footage?, aspectRatio?, fields: [{ key, label, type: text|longtext|select|color|images, default?, options?, required?, help?, minCount?, maxCount?, optional? }], prompt, style?: { aspectRatio?, colorCorrection?, lut?: { preset, intensity? }, background?: { mode, image? }, cameraLayout?, captions?: { enabled, templateId? }, wallpaper? }, checklist: string[] }",
|
|
3747
4305
|
},
|
|
3748
4306
|
},
|
|
3749
4307
|
required: ["recipe"],
|
|
@@ -3802,8 +4360,22 @@ const TOOLS = [
|
|
|
3802
4360
|
{
|
|
3803
4361
|
name: "motion_list",
|
|
3804
4362
|
description:
|
|
3805
|
-
"List motion-graphic templates (slots; render with motion_generate) and slot-less registryBlocks (edit htmlPath, render with motion_render_html).",
|
|
3806
|
-
inputSchema: {
|
|
4363
|
+
"List motion-graphic templates (slots, family, tags; render with motion_generate) and slot-less registryBlocks (edit htmlPath, render with motion_render_html). Retired templates are hidden unless includeRetired.",
|
|
4364
|
+
inputSchema: {
|
|
4365
|
+
type: "object",
|
|
4366
|
+
properties: {
|
|
4367
|
+
family: {
|
|
4368
|
+
type: "string",
|
|
4369
|
+
description:
|
|
4370
|
+
"titles|text-behind|captions|callouts|lists-steps|stats-data|comparisons|product|panels|social-proof|social|intro-outro|end-cards|lower-thirds (comma-separated ok).",
|
|
4371
|
+
},
|
|
4372
|
+
tags: { type: "string", description: "Any of these tags, comma-separated." },
|
|
4373
|
+
query: { type: "string", description: "Search, e.g. 'subscribe', 'bar chart'." },
|
|
4374
|
+
aspect: { type: "string", enum: ["16:9", "9:16", "1:1"] },
|
|
4375
|
+
includeRetired: { type: "boolean" },
|
|
4376
|
+
includeBlocks: { type: "boolean", description: "Default true." },
|
|
4377
|
+
},
|
|
4378
|
+
},
|
|
3807
4379
|
command: "motion.list",
|
|
3808
4380
|
},
|
|
3809
4381
|
{
|
|
@@ -3935,7 +4507,11 @@ const TOOLS = [
|
|
|
3935
4507
|
aspectRatio: { type: "string", enum: ["16:9", "9:16", "1:1"] },
|
|
3936
4508
|
width: { type: "number" },
|
|
3937
4509
|
height: { type: "number" },
|
|
3938
|
-
durationMs: {
|
|
4510
|
+
durationMs: {
|
|
4511
|
+
type: "number",
|
|
4512
|
+
description:
|
|
4513
|
+
"Omit when the root declares data-duration (that is the length rendered; a mismatch warns). Else default 2500. Max 600000 (10 min).",
|
|
4514
|
+
},
|
|
3939
4515
|
frameRate: { type: "number" },
|
|
3940
4516
|
outputName: { type: "string" },
|
|
3941
4517
|
audioPath: { type: "string", description: "Muxed in (not when transparent)." },
|
|
@@ -3949,16 +4525,21 @@ const TOOLS = [
|
|
|
3949
4525
|
{
|
|
3950
4526
|
name: "motion_screenshot",
|
|
3951
4527
|
description:
|
|
3952
|
-
"Capture one PNG frame of an HTML composition, as motion_render_html would. Returns { outputPath, previewPath } (inspect previewPath).",
|
|
4528
|
+
"Capture one PNG frame of an HTML composition (or a template with slots), as motion_render_html / motion_generate would. Returns { outputPath, previewPath } (inspect previewPath).",
|
|
3953
4529
|
inputSchema: {
|
|
3954
4530
|
type: "object",
|
|
3955
4531
|
properties: {
|
|
3956
|
-
html: { type: "string", description: "Or htmlPath." },
|
|
4532
|
+
html: { type: "string", description: "Or htmlPath, or templateId." },
|
|
3957
4533
|
htmlPath: { type: "string" },
|
|
4534
|
+
templateId: { type: "string", description: "Template id; fill with slots." },
|
|
4535
|
+
slots: { type: "object" },
|
|
3958
4536
|
aspectRatio: { type: "string", enum: ["16:9", "9:16", "1:1"] },
|
|
3959
4537
|
width: { type: "number" },
|
|
3960
4538
|
height: { type: "number" },
|
|
3961
|
-
atMs: {
|
|
4539
|
+
atMs: {
|
|
4540
|
+
type: "number",
|
|
4541
|
+
description: "Clamped to the composition's data-duration; result atMs = time captured.",
|
|
4542
|
+
},
|
|
3962
4543
|
transparent: { type: "boolean" },
|
|
3963
4544
|
assets: ASSETS_PROP,
|
|
3964
4545
|
outputName: { type: "string" },
|
|
@@ -4005,8 +4586,23 @@ const TOOLS = [
|
|
|
4005
4586
|
},
|
|
4006
4587
|
{
|
|
4007
4588
|
name: "asset_list_sounds",
|
|
4008
|
-
description:
|
|
4009
|
-
|
|
4589
|
+
description:
|
|
4590
|
+
"List bundled SFX (~190: id, category, tags, durationMs, variantGroup, path). Filter; summary=true gives categories + groups only.",
|
|
4591
|
+
inputSchema: {
|
|
4592
|
+
type: "object",
|
|
4593
|
+
properties: {
|
|
4594
|
+
category: {
|
|
4595
|
+
type: "string",
|
|
4596
|
+
description:
|
|
4597
|
+
"ui|notification|motion|digital|impact|typing|outcome|ambience, comma-separated.",
|
|
4598
|
+
},
|
|
4599
|
+
tag: { type: "string", description: 'e.g. "click,soft" (all match).' },
|
|
4600
|
+
mood: { type: "string" },
|
|
4601
|
+
group: { type: "string", description: "Variant group." },
|
|
4602
|
+
query: { type: "string" },
|
|
4603
|
+
summary: { type: "boolean" },
|
|
4604
|
+
},
|
|
4605
|
+
},
|
|
4010
4606
|
command: "asset.list-sounds",
|
|
4011
4607
|
},
|
|
4012
4608
|
{
|
|
@@ -4018,7 +4614,7 @@ const TOOLS = [
|
|
|
4018
4614
|
{
|
|
4019
4615
|
name: "asset_list_transitions",
|
|
4020
4616
|
description:
|
|
4021
|
-
"List bundled transitions (id, title, category, durationSeconds) for project_add_transition.",
|
|
4617
|
+
"List bundled transitions (id, title, kind overlay|native, effect, category, durationSeconds, defaultSoundId) for project_add_transition.",
|
|
4022
4618
|
inputSchema: { type: "object", properties: {} },
|
|
4023
4619
|
command: "asset.list-transitions",
|
|
4024
4620
|
},
|
|
@@ -4105,6 +4701,10 @@ const TOOLS = [
|
|
|
4105
4701
|
type: ["boolean", "string"],
|
|
4106
4702
|
description: "streaming / podcast / off; omit = project setting.",
|
|
4107
4703
|
},
|
|
4704
|
+
frameRate: {
|
|
4705
|
+
type: ["number", "string"],
|
|
4706
|
+
description: "30 | 60 | source | auto; omit = project setting.",
|
|
4707
|
+
},
|
|
4108
4708
|
},
|
|
4109
4709
|
},
|
|
4110
4710
|
command: "export.start",
|
|
@@ -4175,7 +4775,7 @@ const TOOLS = [
|
|
|
4175
4775
|
{
|
|
4176
4776
|
name: "export_generate_thumbnail",
|
|
4177
4777
|
description:
|
|
4178
|
-
"Generate a YouTube thumbnail for an export (Replicate, spends credits). Pass subject + hook, or omit to derive from the transcript. Returns { imagePath }.",
|
|
4778
|
+
"Generate a YouTube thumbnail for an export (user's image connector: Replicate, else Higgsfield; spends their credits). Pass subject + hook, or omit to derive from the transcript. Returns { imagePath }.",
|
|
4179
4779
|
inputSchema: {
|
|
4180
4780
|
type: "object",
|
|
4181
4781
|
properties: {
|
|
@@ -4478,7 +5078,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
4478
5078
|
};
|
|
4479
5079
|
});
|
|
4480
5080
|
|
|
4481
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
5081
|
+
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
4482
5082
|
const tool = TOOLS.find((t) => t.name === request.params.name);
|
|
4483
5083
|
if (!tool) {
|
|
4484
5084
|
return {
|
|
@@ -4509,7 +5109,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4509
5109
|
}
|
|
4510
5110
|
|
|
4511
5111
|
try {
|
|
4512
|
-
const result = await
|
|
5112
|
+
const result = await withProgressHeartbeat(request, extra, () =>
|
|
5113
|
+
callPandastudio(command, dispatchArgs),
|
|
5114
|
+
);
|
|
4513
5115
|
// Format the response for the MCP client. Default is a text
|
|
4514
5116
|
// block carrying the JSON result. For `motion.screenshot` and
|
|
4515
5117
|
// `motion.verify-frames` we ALSO inline the downscaled preview
|
|
@@ -4559,6 +5161,36 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4559
5161
|
}
|
|
4560
5162
|
});
|
|
4561
5163
|
|
|
5164
|
+
/**
|
|
5165
|
+
* Keep a long call alive on the client side. MCP clients time a request out
|
|
5166
|
+
* after 60 s unless progress arrives (opencode, the in-app agent's runtime,
|
|
5167
|
+
* sends every tool call with a progress token and resets its timer on each
|
|
5168
|
+
* notification). job.wait, renders and presenter takes run for minutes, so
|
|
5169
|
+
* while the app works we report progress every 15 s. Clients that sent no
|
|
5170
|
+
* progress token get nothing.
|
|
5171
|
+
*/
|
|
5172
|
+
const HEARTBEAT_MS = Number(process.env.PANDASTUDIO_MCP_PROGRESS_MS) || 15_000;
|
|
5173
|
+
|
|
5174
|
+
async function withProgressHeartbeat(request, extra, run) {
|
|
5175
|
+
const progressToken = request.params?._meta?.progressToken;
|
|
5176
|
+
if (progressToken === undefined || typeof extra?.sendNotification !== "function") return run();
|
|
5177
|
+
let beats = 0;
|
|
5178
|
+
const timer = setInterval(() => {
|
|
5179
|
+
beats += 1;
|
|
5180
|
+
extra
|
|
5181
|
+
.sendNotification({
|
|
5182
|
+
method: "notifications/progress",
|
|
5183
|
+
params: { progressToken, progress: beats, message: "PandaStudio is working" },
|
|
5184
|
+
})
|
|
5185
|
+
.catch(() => undefined);
|
|
5186
|
+
}, HEARTBEAT_MS);
|
|
5187
|
+
try {
|
|
5188
|
+
return await run();
|
|
5189
|
+
} finally {
|
|
5190
|
+
clearInterval(timer);
|
|
5191
|
+
}
|
|
5192
|
+
}
|
|
5193
|
+
|
|
4562
5194
|
/** Decide which (if any) preview PNGs to inline as image content
|
|
4563
5195
|
* blocks for a given tool result. Only motion.screenshot (one
|
|
4564
5196
|
* preview) and motion.verify-frames (one per frame) qualify today.
|