@slatesvideo/shared 0.7.1 → 0.7.2
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/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/manual/content.d.ts +1 -1
- package/dist/manual/content.js +1 -1
- package/dist/manual/index.d.ts +11 -2
- package/dist/manual/index.js +178 -14
- package/dist/operations/index.d.ts +286 -87
- package/dist/operations/index.js +715 -69
- package/dist/operations/surface.js +6 -0
- package/dist/prompts/agent-doctrine.js +1 -1
- package/dist/prompts/generation-policy.d.ts +1 -1
- package/dist/skills/content.js +2 -2
- package/package.json +1 -1
- package/skills/_partials/cinematic-card.md +1 -1
- package/skills/slates-prompting-inworld-tts.md +174 -174
- package/skills/slates-style-prompting.md +54 -54
package/dist/operations/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { MAX_IMAGE_VARIATIONS } from '../prompts/generation-policy.js';
|
|
2
|
-
import { retrieveGuide
|
|
2
|
+
import { retrieveGuide } from '../prompts/guide-retrieval.js';
|
|
3
3
|
import { MINIMAX_MAX_REFERENCE, minimaxMaxReferenceTokens, MODEL_CAPABILITIES, GPT_QUALITY_TIERS, DEFAULT_GPT_QUALITY, GPT_BACKGROUNDS } from '../prompts/model-capabilities.js';
|
|
4
4
|
// Operations layer — the ONE place every Slates agent tool is defined.
|
|
5
5
|
// Both the MCP server and the CLI register these as their tool / command
|
|
@@ -17,7 +17,7 @@ import { SlatesCloudClient } from '../clients/cloud.js';
|
|
|
17
17
|
import { SlatesDesktopClient } from '../clients/desktop.js';
|
|
18
18
|
import { BlenderBridgeClient, BLENDER_SETUP_HINT, RENDER_TIMEOUT_MS } from '../clients/blender.js';
|
|
19
19
|
import { SKILLS } from '../skills/content.js';
|
|
20
|
-
import { appManualSections } from '../manual/index.js';
|
|
20
|
+
import { appManualIndex, appManualSections } from '../manual/index.js';
|
|
21
21
|
// Reference-capacity prose is DERIVED, never hand-typed — root CLAUDE.md:
|
|
22
22
|
// "never hand-type a fact an LLM will read". These helpers read MODEL_FACTS.
|
|
23
23
|
import { multimodalRefSummary, multimodalRefModels, seedanceTaskIntentWords,
|
|
@@ -216,6 +216,14 @@ function creditsFromDollars(dollars) {
|
|
|
216
216
|
// sentence: it is repeated verbatim on seven ops, so every word costs seven
|
|
217
217
|
// times, and `slates_get_generation_status` explains the polling itself.
|
|
218
218
|
const BACKGROUND_DESCRIBE = 'Return generationId(s) now instead of blocking; poll slates_get_generation_status. Recommended for video.';
|
|
219
|
+
// `folderId` on the generate and upload ops (1.6.1, decision 17 in second-brain
|
|
220
|
+
// plans/2026-09-30-slates-manual-for-llms-1-6-1-decisions.md). The folder dot in Media means "new pictures go
|
|
221
|
+
// here", the user's own generations and drops follow it, and so does an agent's: the DESKTOP resolves an absent
|
|
222
|
+
// folderId to the folder chosen in the window, so every op and the Studio Agent get it at once.
|
|
223
|
+
const FOLDER_DESCRIBE = "Folder the result lands in (slates_list_folders). Omit it to follow the folder chosen in the user's Slates window, else the project root; null is the root, on purpose.";
|
|
224
|
+
const folderIdField = z.string().uuid().nullable().optional().describe(FOLDER_DESCRIBE);
|
|
225
|
+
/** The `folderId` part of a generate or upload body: present only when the caller named one (null is the root). */
|
|
226
|
+
const folderBody = (folderId) => (folderId === undefined ? {} : { folderId });
|
|
219
227
|
// ── Vision QC pointers (the "quality-check with vision" rule, made structural) ──
|
|
220
228
|
//
|
|
221
229
|
// QUALITY-CHECK is a POST-condition, so it cannot be gated the way a
|
|
@@ -356,14 +364,95 @@ export const getSelection = {
|
|
|
356
364
|
};
|
|
357
365
|
},
|
|
358
366
|
};
|
|
367
|
+
// 1.6.1: what is ticked. Mirrors slate's `SELECTION_TARGETS` and `SELECTION_MODES` in `@shared/types/view`
|
|
368
|
+
// (lockstep check 12), which this package cannot import.
|
|
369
|
+
const SELECTION_TARGETS = ['media', 'board'];
|
|
370
|
+
const SELECTION_MODES = ['replace', 'add', 'remove', 'clear', 'all'];
|
|
371
|
+
/**
|
|
372
|
+
* SET what is selected, as the user's clicks do: the cards ticked in Media, the Shots ticked on the Board.
|
|
373
|
+
* Pairs with `slates_get_selection`, which reads it: "tick the shots with no clip, then price them" is this op
|
|
374
|
+
* and then the quote. The desktop applies it through the stores the grids write and answers with what SETTLED.
|
|
375
|
+
*/
|
|
376
|
+
export const setSelection = {
|
|
377
|
+
id: 'slates_set_selection',
|
|
378
|
+
description: "Tick or untick cards in Media or Shots on the Board, as clicks, Shift-click, Escape and Ctrl/Cmd+A do: replace makes the ids the whole selection, add and remove change only them, clear empties it, all ticks everything the tab shows (Media: the grid as filtered, folded rounds left out; Board: every Shot on the open board). Media needs its grid showing and the Board needs the Board or Script tab; slates_set_view switches. Ids are asset ids or badge codes (Media) or Shot ids or SHOT-A codes (Board). Answers with the selection as it settled and names any id it could not tick. slates_get_selection reads it back; what is selected is what the user means by 'these'.",
|
|
379
|
+
input: z
|
|
380
|
+
.object({
|
|
381
|
+
surface: z.enum(SELECTION_TARGETS),
|
|
382
|
+
mode: z.enum(SELECTION_MODES),
|
|
383
|
+
ids: z.array(z.string().min(1)).max(500).optional().describe('Asset ids or codes (media) or Shot ids or codes (board). Not used by clear or all.'),
|
|
384
|
+
})
|
|
385
|
+
.strict(),
|
|
386
|
+
async run(input, ctx) {
|
|
387
|
+
await ctx.desktop().requireCapability('selection-set', 'setting what is selected');
|
|
388
|
+
if ((input.mode === 'add' || input.mode === 'remove') && !input.ids?.length)
|
|
389
|
+
throw new Error(`${input.mode} needs ids.`);
|
|
390
|
+
const r = await ctx.desktop().post('/agent/selection', input);
|
|
391
|
+
if (!r.selection)
|
|
392
|
+
return { text: `Nothing was changed (${r.reason ?? 'no answer'}).`, data: { selection: null } };
|
|
393
|
+
const where = r.selection.surface === 'media' ? 'Media' : 'the Board';
|
|
394
|
+
const named = (row) => (row.code ? (row.label ? `${row.code} — ${row.label}` : row.code) : row.id);
|
|
395
|
+
const notes = r.notes?.length ? ` Not applied: ${r.notes.join('; ')}.` : '';
|
|
396
|
+
return {
|
|
397
|
+
text: (r.selection.items.length ? `${r.selection.items.length} selected on ${where}: ${r.selection.items.map(named).join(', ')}.` : `Nothing is selected on ${where}.`) + notes,
|
|
398
|
+
data: { selection: r.selection, notes: r.notes ?? [] },
|
|
399
|
+
};
|
|
400
|
+
},
|
|
401
|
+
};
|
|
359
402
|
// The view's three lists, mirrored from the desktop's `@shared/types/view`
|
|
360
403
|
// (`LENSES`, `CUT_SIDES`, `DOCK_SECTIONS`), which this package cannot import.
|
|
361
404
|
// Lockstep check 12 fails when they differ.
|
|
362
405
|
const VIEW_LENSES = ['board', 'media', 'script'];
|
|
363
406
|
const VIEW_CUT_SIDES = ['bottom', 'left', 'right'];
|
|
364
407
|
const VIEW_DOCK_SECTIONS = ['storyboards', 'library', 'folders', 'pinned'];
|
|
408
|
+
// 1.6.1: what the user is looking at. Mirrors slate's `@shared/types/view` (lockstep check 12).
|
|
409
|
+
const VIEW_BOARD_LEVELS = ['film', 'scenes', 'shot'];
|
|
410
|
+
const VIEW_BOARD_FILTERS = ['all', 'with-clip', 'without-clip'];
|
|
411
|
+
const VIEW_MEDIA_TABS = ['all', 'images', 'videos', 'audio'];
|
|
412
|
+
const VIEW_SETTINGS_PANES = ['account', 'ai', 'storage', 'logs', 'general', 'keys'];
|
|
365
413
|
/** What each dock section is called on screen. */
|
|
366
414
|
const DOCK_SECTION_NAMES = { storyboards: 'Boards', library: 'Library', folders: 'Folders', pinned: 'Pinned' };
|
|
415
|
+
/** 1.6.1: what the user is looking at, in the words on screen. Empty parts are left out. */
|
|
416
|
+
const describeLooking = (v) => {
|
|
417
|
+
const parts = [];
|
|
418
|
+
if (!v.projectId)
|
|
419
|
+
parts.push('Home (the project list) is showing.');
|
|
420
|
+
if (v.board?.id) {
|
|
421
|
+
const level = v.board.level === 'film' ? 'Film' : v.board.level === 'shot' ? 'Shot' : 'Scenes';
|
|
422
|
+
const filter = v.board.filter === 'with-clip' ? ', Filter: With a linked clip' : v.board.filter === 'without-clip' ? ', Filter: Without a linked clip' : '';
|
|
423
|
+
parts.push(`The open board is ${v.board.id} (View: ${level}${filter}).`);
|
|
424
|
+
}
|
|
425
|
+
if (v.library?.categoryId)
|
|
426
|
+
parts.push(`A Library page is in Media's place (category ${v.library.categoryId}).`);
|
|
427
|
+
else if (v.media) {
|
|
428
|
+
const narrowing = [
|
|
429
|
+
v.media.unfiledOnly && 'No folder only',
|
|
430
|
+
v.media.search && `search "${v.media.search}"`,
|
|
431
|
+
v.media.favoritesOnly && 'favorites only',
|
|
432
|
+
v.media.linkedOnly && 'only with linked videos',
|
|
433
|
+
].filter(Boolean);
|
|
434
|
+
parts.push(`Media's tab is ${v.media.tab}${narrowing.length ? `, narrowed to ${narrowing.join(', ')}` : ''}.`);
|
|
435
|
+
}
|
|
436
|
+
if (v.media?.folderId)
|
|
437
|
+
parts.push(`Folder ${v.media.folderId} is chosen: new pictures land in it.`);
|
|
438
|
+
if (v.viewer?.assetId)
|
|
439
|
+
parts.push(`The picture viewer is open on ${v.viewer.assetId}.`);
|
|
440
|
+
if (v.compare?.open)
|
|
441
|
+
parts.push(`Compare is open on ${v.compare.assetIds.length} items.`);
|
|
442
|
+
if (v.animatic?.open)
|
|
443
|
+
parts.push('The animatic is up.');
|
|
444
|
+
if (v.settings?.open)
|
|
445
|
+
parts.push('Settings is open.');
|
|
446
|
+
if (v.composer && !v.composer.open)
|
|
447
|
+
parts.push("The prompt box is hidden (the ' key shows it).");
|
|
448
|
+
if (v.script?.panelShotId)
|
|
449
|
+
parts.push(`The Script page's shot panel is open on Shot ${v.script.panelShotId}.`);
|
|
450
|
+
if (v.layers?.length)
|
|
451
|
+
parts.push(`Open over the page: ${v.layers.join(', ')}.`);
|
|
452
|
+
if (v.notes?.length)
|
|
453
|
+
parts.push(`Not applied: ${v.notes.join('; ')}.`);
|
|
454
|
+
return parts.length ? ' ' + parts.join(' ') : '';
|
|
455
|
+
};
|
|
367
456
|
const describeView = (v) => {
|
|
368
457
|
const where = v.cut.full
|
|
369
458
|
? 'filling the workspace'
|
|
@@ -380,7 +469,8 @@ const describeView = (v) => {
|
|
|
380
469
|
return (`The ${v.lens} tab is showing. The timeline is ${where}. ` +
|
|
381
470
|
`The project navigator is ${v.leftDock.open ? `open (${v.leftDock.width}px)` : 'closed'}, and ${agent}.` +
|
|
382
471
|
(v.leftDock.folded?.length ? ` Folded in the navigator: ${v.leftDock.folded.map((f) => DOCK_SECTION_NAMES[f] ?? f).join(', ')}.` : '') +
|
|
383
|
-
(v.script ? ` The Script page shows ${v.script.details ? 'Words + shots (each shot\'s picture beside its words)' : 'Words (the words alone)'}.` : '')
|
|
472
|
+
(v.script ? ` The Script page shows ${v.script.details ? 'Words + shots (each shot\'s picture beside its words)' : 'Words (the words alone)'}.` : '') +
|
|
473
|
+
describeLooking(v));
|
|
384
474
|
};
|
|
385
475
|
/**
|
|
386
476
|
* How the Slates window is ARRANGED right now — which lens is showing, where
|
|
@@ -389,7 +479,7 @@ const describeView = (v) => {
|
|
|
389
479
|
*/
|
|
390
480
|
export const getView = {
|
|
391
481
|
id: 'slates_get_view',
|
|
392
|
-
description: "
|
|
482
|
+
description: "What the user is looking at in Slates: Home or a project, the tab (Media, Script or Board), the open board and its View and Filter, Media's tab, chosen folder and narrowing, a Library page, the picture viewer, Compare, the animatic, Settings, whether the prompt box is hidden, the timeline and the side panels, and any dialog or menu open over the page. Read it before telling the user where to click.",
|
|
393
483
|
input: z.object({}).strict(),
|
|
394
484
|
async run(_input, ctx) {
|
|
395
485
|
await ctx.desktop().requireCapability('view', 'the window layout');
|
|
@@ -417,9 +507,10 @@ export const getView = {
|
|
|
417
507
|
*/
|
|
418
508
|
export const setView = {
|
|
419
509
|
id: 'slates_set_view',
|
|
420
|
-
description: "
|
|
510
|
+
description: "Change what the user is looking at, as their own clicks would: open a project or go Home, switch tab, open a board, jump to a scene, set the Board's View and Filter, set Media's tab, folder and narrowing, jump to a card in Media, open a Library page, the picture viewer, Compare, the animatic or Settings on a pane, show or hide the prompt box, open or park the timeline or switch the cut on it, open a Shot's panel on Script. Only the fields you name change; the app clamps sizes, answers with what it settled on, and names any field it could not apply. A project change is applied alone (it restores that project's own view). Use it when the user asks you to show or open something, not to answer a question they can see for themselves.",
|
|
421
511
|
input: z
|
|
422
512
|
.object({
|
|
513
|
+
projectId: z.string().nullable().optional().describe('Open this project, or null for Home. Applied alone.'),
|
|
423
514
|
lens: z.enum(VIEW_LENSES).optional().describe('Which tab the centre shows.'),
|
|
424
515
|
cut: z
|
|
425
516
|
.object({
|
|
@@ -429,8 +520,9 @@ export const setView = {
|
|
|
429
520
|
.enum(VIEW_CUT_SIDES)
|
|
430
521
|
.optional()
|
|
431
522
|
.describe('Where the timeline is parked. A column suits a wide monitor; picking a side opens the timeline.'),
|
|
432
|
-
height: z.number().optional().describe(
|
|
433
|
-
width: z.number().optional().describe(
|
|
523
|
+
height: z.number().optional().describe("The bottom band's height in px."),
|
|
524
|
+
width: z.number().optional().describe("The side column's width in px."),
|
|
525
|
+
timelineId: z.string().optional().describe('The named cut to show on the timeline (slates_list_timelines).'),
|
|
434
526
|
})
|
|
435
527
|
.strict()
|
|
436
528
|
.optional(),
|
|
@@ -456,21 +548,67 @@ export const setView = {
|
|
|
456
548
|
.describe('The Studio Agent panel on the right. It cannot be opened while the agent is off in Settings.'),
|
|
457
549
|
script: z
|
|
458
550
|
.object({
|
|
459
|
-
details: z.boolean().optional().describe(
|
|
551
|
+
details: z.boolean().optional().describe("Words + shots (true): each shot's picture beside its words on the Script page. Words (false): the words alone."),
|
|
552
|
+
textScale: z.number().optional().describe("The page's text size; 1 is the default."),
|
|
553
|
+
panelShotId: z.string().nullable().optional().describe("Open this Shot's details panel on the Script page (it binds the Shot to the prompt box, as a click does); null closes it."),
|
|
460
554
|
})
|
|
461
555
|
.strict()
|
|
462
556
|
.optional()
|
|
463
557
|
.describe('The Script page.'),
|
|
558
|
+
board: z
|
|
559
|
+
.object({
|
|
560
|
+
id: z.string().optional().describe('Open this board (on Board and Script).'),
|
|
561
|
+
sceneId: z.string().optional().describe('Scroll the Board to this scene.'),
|
|
562
|
+
level: z.enum(VIEW_BOARD_LEVELS).optional().describe('View: Film (posters), Scenes (the working card) or Shot (each card open).'),
|
|
563
|
+
cardWidth: z.number().optional().describe('Card width in px, anywhere between the stops.'),
|
|
564
|
+
filter: z.enum(VIEW_BOARD_FILTERS).optional().describe('Filter: every shot, only shots with a linked clip, or only shots without one.'),
|
|
565
|
+
linkedClips: z.boolean().optional().describe('Show the row of linked clips under each shot.'),
|
|
566
|
+
scriptFollowsDrag: z.boolean().optional().describe("The board menu's Script follows a drag."),
|
|
567
|
+
collapsedSceneIds: z.array(z.string()).optional().describe('Every scene to fold; a scene not named unfolds.'),
|
|
568
|
+
})
|
|
569
|
+
.strict()
|
|
570
|
+
.optional(),
|
|
571
|
+
media: z
|
|
572
|
+
.object({
|
|
573
|
+
tab: z.enum(VIEW_MEDIA_TABS).optional(),
|
|
574
|
+
folderId: z.string().nullable().optional().describe('Choose this folder (new pictures land in it), or null for all media.'),
|
|
575
|
+
unfiledOnly: z.boolean().optional().describe('Show only media in no folder.'),
|
|
576
|
+
search: z.string().optional().describe("Media's search box (matches prompts)."),
|
|
577
|
+
favoritesOnly: z.boolean().optional(),
|
|
578
|
+
linkedOnly: z.boolean().optional().describe('Images tab: only pictures with a linked video.'),
|
|
579
|
+
rounds: z.boolean().optional().describe('Group by generation.'),
|
|
580
|
+
cardSize: z.number().optional().describe('Card size in px.'),
|
|
581
|
+
revealAssetId: z.string().optional().describe('Jump to this card in Media, undoing only what hides it (the app says what it changed).'),
|
|
582
|
+
})
|
|
583
|
+
.strict()
|
|
584
|
+
.optional(),
|
|
585
|
+
library: z.object({ categoryId: z.string().nullable() }).strict().optional().describe("Open a Library category's page in Media's place; null goes back to the grid."),
|
|
586
|
+
viewer: z.object({ assetId: z.string().nullable() }).strict().optional().describe('Open the picture viewer on a picture; null closes it.'),
|
|
587
|
+
compare: z
|
|
588
|
+
.object({ open: z.boolean().optional(), assetIds: z.array(z.string()).max(4).optional().describe('The compare set, two to four items.') })
|
|
589
|
+
.strict()
|
|
590
|
+
.optional(),
|
|
591
|
+
animatic: z.object({ open: z.boolean(), fromShotId: z.string().optional() }).strict().optional().describe('Play the board as a rough cut from a Shot (the Board or Script tab must be showing).'),
|
|
592
|
+
settings: z.object({ open: z.boolean(), pane: z.enum(VIEW_SETTINGS_PANES).optional() }).strict().optional().describe('Open Settings, on a pane (ai is AI tools, keys is API keys).'),
|
|
593
|
+
composer: z.object({ open: z.boolean() }).strict().optional().describe('Show or hide the prompt box.'),
|
|
464
594
|
})
|
|
465
595
|
.strict(),
|
|
466
596
|
async run(input, ctx) {
|
|
467
597
|
await ctx.desktop().requireCapability('view', 'the window layout');
|
|
468
598
|
if (Object.keys(input).length === 0) {
|
|
469
|
-
throw new Error('Name at least one
|
|
599
|
+
throw new Error('Name at least one field to change, e.g. lens, board, media, viewer or settings.');
|
|
470
600
|
}
|
|
601
|
+
// The 1.6.1 fields need a desktop that knows them; an older one would drop them without a word.
|
|
602
|
+
const LOOKING = ['projectId', 'board', 'media', 'library', 'viewer', 'compare', 'animatic', 'settings', 'composer'];
|
|
603
|
+
const widened = LOOKING.some((k) => input[k] !== undefined) ||
|
|
604
|
+
input.cut?.timelineId !== undefined ||
|
|
605
|
+
input.script?.textScale !== undefined ||
|
|
606
|
+
input.script?.panelShotId !== undefined;
|
|
607
|
+
if (widened)
|
|
608
|
+
await ctx.desktop().requireCapability('view-v2', 'showing and opening things in the window');
|
|
471
609
|
const r = await ctx.desktop().post('/agent/view', input);
|
|
472
610
|
if (!r.view) {
|
|
473
|
-
return { text: r.reason ? `Nothing was
|
|
611
|
+
return { text: r.reason ? `Nothing was changed (${r.reason}).` : 'Nothing was changed.', data: { view: null } };
|
|
474
612
|
}
|
|
475
613
|
// A desktop whose view predates folding takes the patch and ignores `folded`.
|
|
476
614
|
const noFolds = input.leftDock?.folded !== undefined && r.view.leftDock.folded === undefined
|
|
@@ -479,6 +617,288 @@ export const setView = {
|
|
|
479
617
|
return { text: describeView(r.view) + noFolds, data: { view: r.view } };
|
|
480
618
|
},
|
|
481
619
|
};
|
|
620
|
+
/**
|
|
621
|
+
* SHOW AND POINT (1.6.1). The manual's pictures and a ring around a live control, so an agent can
|
|
622
|
+
* answer "where is it?" with the thing itself instead of a paragraph. Neither changes the project or
|
|
623
|
+
* the view. The pictures come from the INSTALLED app (it ships them), so they show the user's version;
|
|
624
|
+
* the public URL is for a client that renders only links. Decisions 13-16 in second-brain
|
|
625
|
+
* plans/2026-09-30-slates-manual-for-llms-1-6-1-decisions.md.
|
|
626
|
+
*/
|
|
627
|
+
export const getManualPicture = {
|
|
628
|
+
id: 'slates_get_manual_picture',
|
|
629
|
+
description: 'A picture of a Slates screen from the app manual, with numbered callouts and their legend, taken from the version the user runs. Pass the picture id the manual names (app-manual sections show "Picture `id`"); no id lists them. Show one when the user cannot find something or asks what a screen looks like; never unasked. When the control is on screen, slates_highlight_control points at the real thing instead.',
|
|
630
|
+
input: z
|
|
631
|
+
.object({ id: z.string().min(1).optional().describe('Picture id from the manual, e.g. "window-overview". Omit to list.') })
|
|
632
|
+
.strict(),
|
|
633
|
+
async run(input, ctx) {
|
|
634
|
+
await ctx.desktop().requireCapability('manual-pictures', "the manual's pictures");
|
|
635
|
+
if (!input.id) {
|
|
636
|
+
const r = await ctx.desktop().get('/agent/manual/pictures');
|
|
637
|
+
return ok(r, r.pictures.length ? r.pictures.map((p) => `${p.id}: ${p.title}`).join('\n') : 'This Slates ships no manual pictures.');
|
|
638
|
+
}
|
|
639
|
+
const p = await ctx.desktop().get('/agent/manual/picture', { id: input.id });
|
|
640
|
+
const legend = p.callouts.length ? '\n' + p.callouts.map((c) => `${c.n}. ${c.name} (${c.target})`).join('\n') : '';
|
|
641
|
+
return {
|
|
642
|
+
text: `${p.title} (Slates ${p.appVersion}). Link: ${p.url}${legend}`,
|
|
643
|
+
images: [{ data: p.data, mimeType: p.mimeType }],
|
|
644
|
+
data: { id: p.id, title: p.title, url: p.url, appVersion: p.appVersion, callouts: p.callouts },
|
|
645
|
+
};
|
|
646
|
+
},
|
|
647
|
+
};
|
|
648
|
+
export const highlightControl = {
|
|
649
|
+
id: 'slates_highlight_control',
|
|
650
|
+
description: "Point at a control in the user's Slates window: a grey outline around it for a few seconds, with your caption beside it. It clicks nothing and never changes the view; the user's next click clears it. Target ids are listed at the end of each app-manual surface section (\"controls you can point at\"), or pass list:true to get every id with whether it is on screen now. shown:false means it is not on screen: tell the user the way there, or open that view with slates_set_view if they want you to.",
|
|
651
|
+
input: z
|
|
652
|
+
.object({
|
|
653
|
+
target: z.string().min(1).optional().describe('Control id, e.g. "shell.titlebar.search".'),
|
|
654
|
+
caption: z.string().max(140).optional().describe('A few words shown beside it, e.g. "Click here to open the timeline".'),
|
|
655
|
+
list: z.boolean().optional().describe('Return every target id, its on-screen name, where it is, and whether it is on screen now.'),
|
|
656
|
+
})
|
|
657
|
+
.strict(),
|
|
658
|
+
async run(input, ctx) {
|
|
659
|
+
await ctx.desktop().requireCapability('ui-pointer', 'pointing at controls');
|
|
660
|
+
if (input.list || !input.target) {
|
|
661
|
+
const r = await ctx.desktop().get('/agent/ui/targets');
|
|
662
|
+
const on = r.targets.filter((t) => t.onScreen);
|
|
663
|
+
return ok(r, `${r.targets.length} controls; on screen now: ${on.map((t) => `${t.id} (${t.name})`).join(', ') || 'none reported'}.`);
|
|
664
|
+
}
|
|
665
|
+
const r = await ctx.desktop().post('/agent/ui/highlight', {
|
|
666
|
+
target: input.target,
|
|
667
|
+
caption: input.caption,
|
|
668
|
+
});
|
|
669
|
+
return ok(r, r.shown
|
|
670
|
+
? `Pointing at ${r.name} (${r.where}) in the user's window.`
|
|
671
|
+
: `${r.name} is not on screen (${r.reason ?? 'not shown'}). It is at: ${r.where}. Tell the user the way there, or open that view with slates_set_view if they ask you to.`);
|
|
672
|
+
},
|
|
673
|
+
};
|
|
674
|
+
const COMPOSER_ROLES = Object.keys(ATTACHMENT_ROLE_DESCRIPTION);
|
|
675
|
+
function describeComposer(r) {
|
|
676
|
+
const lines = [];
|
|
677
|
+
const where = r.bound
|
|
678
|
+
? `editing Shot ${r.bound.code ?? r.bound.shotId}${r.bound.name ? ` "${r.bound.name}"` : ''} (every change is saved to it)`
|
|
679
|
+
: r.draftFrom
|
|
680
|
+
? `a draft from ${r.draftFrom.code ?? r.draftFrom.assetId}`
|
|
681
|
+
: 'an unsaved draft';
|
|
682
|
+
lines.push(`Prompt box (${r.open ? 'showing' : 'hidden'}), ${r.lane} lane, ${r.model.label}${r.destination === 'chatgpt' ? ' (ChatGPT)' : ''}; ${where}.`);
|
|
683
|
+
lines.push(`Words (${r.prompt.length}${r.promptMax ? `/${r.promptMax}` : ''}): ${r.prompt ? JSON.stringify(r.prompt) : 'none'}`);
|
|
684
|
+
if (r.params.length)
|
|
685
|
+
lines.push(`Settings: ${r.params.map((p) => `${p.label} ${p.value || '(none)'}`).join(' · ')}`);
|
|
686
|
+
if (r.tiles.length) {
|
|
687
|
+
const tiles = r.tiles.map((t) => `${t.badge} ${t.code ?? t.name}${t.role ? ` (${t.role})` : t.token ? ` (${t.token})` : ''}${t.voice ? ' voice' : ''}${t.sent ? '' : ' NOT SENT'}`);
|
|
688
|
+
lines.push(`References: ${tiles.join(', ')}`);
|
|
689
|
+
}
|
|
690
|
+
const ti = r.toolInputs;
|
|
691
|
+
if (ti && r.tool === 'lip-sync')
|
|
692
|
+
lines.push(`Lip Sync source: ${ti.sourceAssetId ?? 'none'}${ti.sourceType ? ` (${ti.sourceType})` : ''}; Speech Text: ${ti.speechText ? JSON.stringify(ti.speechText) : 'none'}`);
|
|
693
|
+
if (ti && r.tool === 'motion-transfer')
|
|
694
|
+
lines.push(`Motion: ${ti.drivingVideoAssetId ?? 'none'}; Character: ${ti.characterImageAssetId ?? 'none'}`);
|
|
695
|
+
if (r.composedPrompt !== r.prompt)
|
|
696
|
+
lines.push(`Sent as: ${JSON.stringify(r.composedPrompt)}`);
|
|
697
|
+
if (r.unresolved.length)
|
|
698
|
+
lines.push(`Match nothing (sent as plain words): ${r.unresolved.join(', ')}`);
|
|
699
|
+
if (r.dangling.length)
|
|
700
|
+
lines.push(`Point past what is sent: ${r.dangling.join(', ')}`);
|
|
701
|
+
if (r.notice)
|
|
702
|
+
lines.push(`Notice line: ${r.notice.text}`);
|
|
703
|
+
lines.push(`Button: ${r.generate}.${r.running ? ` ${r.running} generating.` : ''}`);
|
|
704
|
+
return lines.join('\n');
|
|
705
|
+
}
|
|
706
|
+
export const getComposer = {
|
|
707
|
+
id: 'slates_get_composer',
|
|
708
|
+
description: "What the prompt box holds right now, as the user sees it: the lane and model, whether it is editing a Shot or a draft, the words, every setting with the values it offers, each reference tile with its number, role and whether the model's limit leaves it out, the exact text the model receives (See what gets sent), mentions that match nothing, the notice line, and the Generate button's price. Read it before telling the user what a press will do, or before staging with slates_set_composer.",
|
|
709
|
+
input: z.object({}).strict(),
|
|
710
|
+
async run(_input, ctx) {
|
|
711
|
+
await ctx.desktop().requireCapability('composer', 'reading the prompt box');
|
|
712
|
+
const r = await ctx.desktop().get('/agent/composer');
|
|
713
|
+
if (!r.report)
|
|
714
|
+
return { text: r.reason ?? 'The prompt box did not answer.', data: { report: null } };
|
|
715
|
+
return { text: describeComposer(r.report), data: r.report };
|
|
716
|
+
},
|
|
717
|
+
};
|
|
718
|
+
export const setComposer = {
|
|
719
|
+
id: 'slates_set_composer',
|
|
720
|
+
description: "Stage a request in the user's prompt box, as their own clicks would, and leave it for them to press Generate: edit a Shot in it or stop, clear or restore the draft, reuse a result's prompt, edit a clip, pick the lane and model, write the words, attach pictures, clips or audio with roles, re-file or remove them, set settings and the voice, fill Lip Sync or Motion Control's inputs. It never generates and never spends; the answer is the box as it settled (the same report as slates_get_composer) plus a line for anything it could not apply and why. Use it when the user wants a setup ready to look at; to generate yourself, quote with slates_estimate_generation_cost and use the generate ops.",
|
|
721
|
+
input: z
|
|
722
|
+
.object({
|
|
723
|
+
projectId: z.string().uuid().optional().describe('The project you expect the box to be in; refused if another is open.'),
|
|
724
|
+
bindShotId: z
|
|
725
|
+
.string()
|
|
726
|
+
.min(1)
|
|
727
|
+
.nullable()
|
|
728
|
+
.optional()
|
|
729
|
+
.describe('Edit this Shot in the box (id or code; every change is saved to it); null stops editing and brings the draft back.'),
|
|
730
|
+
clear: z.boolean().optional().describe('Empty the unsaved draft; restoreDraft undoes it. Not while a Shot is bound.'),
|
|
731
|
+
restoreDraft: z.boolean().optional().describe('Put back the draft that Clear, Reuse prompt or Edit with AI replaced (they swap).'),
|
|
732
|
+
restoreSetup: z.boolean().optional().describe('On a bound Shot, swap back the setup Continue replaced (writes the Shot; again swaps back).'),
|
|
733
|
+
fromAssetId: z.string().min(1).optional().describe("Reuse prompt: this result's recipe replaces the box (id or code)."),
|
|
734
|
+
editSourceAssetId: z.string().min(1).optional().describe('Edit with AI: this clip becomes the canvas and the box starts clean.'),
|
|
735
|
+
lane: z.enum(['image', 'video', 'audio']).optional().describe('The Image / Video / Audio tab; it lands on the last model used there.'),
|
|
736
|
+
model: z
|
|
737
|
+
.string()
|
|
738
|
+
.min(1)
|
|
739
|
+
.optional()
|
|
740
|
+
.describe('A model id (slates_list_available_models), `<id>::face` for a face route, `chatgpt`, `lip-sync` or `motion-transfer`.'),
|
|
741
|
+
prompt: z.string().optional().describe('Replaces the words. @name and #name attach what they name, as typing does.'),
|
|
742
|
+
addMentions: z
|
|
743
|
+
.array(z.string().regex(/^[@#]\S+$/))
|
|
744
|
+
.optional()
|
|
745
|
+
.describe('@name / #name added at the end of the words (slates_list_library gives each item its mention).'),
|
|
746
|
+
attach: z
|
|
747
|
+
.array(z.object({ assetId: z.string().min(1), role: z.enum(COMPOSER_ROLES).optional() }).strict())
|
|
748
|
+
.optional()
|
|
749
|
+
.describe('Pictures, clips or audio from the project (id or code). A picture with no role goes in as a dropped one does; a clip is a video-reference, audio an audio-reference.'),
|
|
750
|
+
detach: z
|
|
751
|
+
.array(z.string().min(1))
|
|
752
|
+
.optional()
|
|
753
|
+
.describe("Take these out of the box: asset ids or codes, or a character's id to drop the voice her @mention brought."),
|
|
754
|
+
setRole: z
|
|
755
|
+
.array(z.object({ assetId: z.string().min(1), role: z.enum(COMPOSER_ROLES) }).strict())
|
|
756
|
+
.optional()
|
|
757
|
+
.describe("Re-file pictures already in the box, as a tile's role menu does."),
|
|
758
|
+
params: z
|
|
759
|
+
.record(z.union([z.string(), z.number(), z.boolean()]))
|
|
760
|
+
.optional()
|
|
761
|
+
.describe('Settings by id, each to a value it offers (slates_get_composer lists both), applied after the model.'),
|
|
762
|
+
voice: z
|
|
763
|
+
.union([
|
|
764
|
+
z.object({ presetId: z.string().min(1) }).strict(),
|
|
765
|
+
z.object({ clipAssetId: z.string().min(1) }).strict(),
|
|
766
|
+
z.object({ description: z.string().min(1) }).strict(),
|
|
767
|
+
])
|
|
768
|
+
.optional()
|
|
769
|
+
.describe('The voice on a voice model: a preset (slates_list_voices), an audio clip in the project, or described in words.'),
|
|
770
|
+
tool: z
|
|
771
|
+
.object({
|
|
772
|
+
sourceAssetId: z.string().min(1).nullable().optional().describe('Lip Sync: the picture or clip with the face; null clears it.'),
|
|
773
|
+
speechText: z.string().max(120).optional().describe('Lip Sync, Text to speech: the words spoken (Speech Text).'),
|
|
774
|
+
drivingVideoAssetId: z.string().min(1).nullable().optional().describe('Motion Control: the clip whose motion is copied (Motion).'),
|
|
775
|
+
characterImageAssetId: z.string().min(1).nullable().optional().describe('Motion Control: the picture to animate (Character).'),
|
|
776
|
+
})
|
|
777
|
+
.strict()
|
|
778
|
+
.optional()
|
|
779
|
+
.describe('Lip Sync and Motion Control inputs (pick model lip-sync or motion-transfer first). An uploaded audio file is only the user.'),
|
|
780
|
+
})
|
|
781
|
+
.strict(),
|
|
782
|
+
async run(input, ctx) {
|
|
783
|
+
await ctx.desktop().requireCapability('composer', 'staging the prompt box');
|
|
784
|
+
if (Object.keys(input).filter((k) => k !== 'projectId').length === 0) {
|
|
785
|
+
throw new Error('Name at least one change, e.g. model, prompt, attach or params. To read the box, use slates_get_composer.');
|
|
786
|
+
}
|
|
787
|
+
const r = await ctx.desktop().post('/agent/composer', input);
|
|
788
|
+
if (!r.report)
|
|
789
|
+
return { text: `Nothing was staged (${r.reason ?? 'no answer'}).`, data: { report: null } };
|
|
790
|
+
const skipped = r.skipped?.length ? `\nNot applied:\n- ${r.skipped.join('\n- ')}` : '';
|
|
791
|
+
return { text: `${describeComposer(r.report)}${skipped}`, data: { report: r.report, skipped: r.skipped ?? [] } };
|
|
792
|
+
},
|
|
793
|
+
};
|
|
794
|
+
/**
|
|
795
|
+
* AGENT PARITY (1.6.1): what a user could do in the window and an agent could not. Each calls a desktop
|
|
796
|
+
* route that runs the window's own function (`slate/src/main/agent/routes-parity.ts`). Plan step 5b,
|
|
797
|
+
* second-brain plans/2026-09-30-slates-manual-for-llms-1-6-1.md.
|
|
798
|
+
*/
|
|
799
|
+
export const reorderFolders = {
|
|
800
|
+
id: 'slates_reorder_folders',
|
|
801
|
+
description: "Reorder the Folders section of the left dock, as dragging a row does. Pass folder ids in the new order; ones you leave out keep their order after them.",
|
|
802
|
+
input: z.object({ projectId: z.string().uuid(), folderIds: z.array(z.string().min(1)).min(1) }).strict(),
|
|
803
|
+
async run(input, ctx) {
|
|
804
|
+
await ctx.desktop().requireCapability('reorder', 'reordering folders and pins');
|
|
805
|
+
return ok(await ctx.desktop().post('/agent/folders/reorder', input));
|
|
806
|
+
},
|
|
807
|
+
};
|
|
808
|
+
export const reorderPins = {
|
|
809
|
+
id: 'slates_reorder_pins',
|
|
810
|
+
description: "Reorder the Pinned section of the left dock, as dragging a pin does. Pass the pinned images (UUIDs or badge codes) in the new order; ones you leave out keep their order after them.",
|
|
811
|
+
input: z.object({ projectId: z.string().uuid(), assetIds: z.array(z.string().min(1)).min(1) }).strict(),
|
|
812
|
+
async run(input, ctx) {
|
|
813
|
+
await ctx.desktop().requireCapability('reorder', 'reordering folders and pins');
|
|
814
|
+
const resolved = await resolveAssetRefs(ctx, input.projectId, input.assetIds);
|
|
815
|
+
return ok(await ctx.desktop().post('/agent/pins/reorder', { projectId: input.projectId, assetIds: input.assetIds.map((r) => resolved.get(r).id) }));
|
|
816
|
+
},
|
|
817
|
+
};
|
|
818
|
+
export const getUsage = {
|
|
819
|
+
id: 'slates_get_usage',
|
|
820
|
+
description: 'What Settings → Account → Usage shows: estimated spend across every project for this month (default), the last 7 days or all time, the image and video counts, video seconds and the models spent on most.',
|
|
821
|
+
input: z.object({ period: z.enum(['week', 'month', 'all']).optional() }).strict(),
|
|
822
|
+
async run(input, ctx) {
|
|
823
|
+
await ctx.desktop().requireCapability('usage', 'usage');
|
|
824
|
+
return ok(await ctx.desktop().get('/agent/usage', { period: input.period ?? 'month' }));
|
|
825
|
+
},
|
|
826
|
+
};
|
|
827
|
+
export const getAppSettings = {
|
|
828
|
+
id: 'slates_get_app_settings',
|
|
829
|
+
description: "The app's own settings an agent may read: the app version, the projects folder, which tab a never-opened project lands on (newProjectLens), whether ChatGPT images is on, and (from Slates 1.6.1) who does the Studio Agent's thinking (studioAgentHost: slates, codex or claude) with each host's model and thinking level ('' is its default). Never keys or sign-in.",
|
|
830
|
+
input: z.object({}).strict(),
|
|
831
|
+
async run(_input, ctx) {
|
|
832
|
+
await ctx.desktop().requireCapability('app-settings', 'app settings');
|
|
833
|
+
return ok(await ctx.desktop().get('/agent/app-settings'));
|
|
834
|
+
},
|
|
835
|
+
};
|
|
836
|
+
const HOST_KEYS = ['studioAgentHost', 'studioAgentCodexModel', 'studioAgentCodexEffort', 'studioAgentClaudeModel', 'studioAgentClaudeEffort'];
|
|
837
|
+
export const setAppSettings = {
|
|
838
|
+
id: 'slates_set_app_settings',
|
|
839
|
+
description: "Change an app setting the user asked you to: newProjectLens (the tab a never-opened project lands on: media, script or board), chatGptImagesEnabled (Settings → AI tools → ChatGPT images), or who does the Studio Agent's thinking: studioAgentHost (slates, codex for the user's own Codex on their ChatGPT plan, claude for their own Claude Code on their Claude plan) and each host's model and thinking level, ids from the host's own list or '' for its default. The projects folder, keys and sign-in are the user's to change.",
|
|
840
|
+
input: z
|
|
841
|
+
.object({
|
|
842
|
+
newProjectLens: z.enum(['media', 'script', 'board']).optional(),
|
|
843
|
+
chatGptImagesEnabled: z.boolean().optional(),
|
|
844
|
+
studioAgentHost: z.enum(['slates', 'codex', 'claude']).optional(),
|
|
845
|
+
studioAgentCodexModel: z.string().max(120).optional(),
|
|
846
|
+
studioAgentCodexEffort: z.string().max(120).optional(),
|
|
847
|
+
studioAgentClaudeModel: z.string().max(120).optional(),
|
|
848
|
+
studioAgentClaudeEffort: z.string().max(120).optional(),
|
|
849
|
+
})
|
|
850
|
+
.strict(),
|
|
851
|
+
async run(input, ctx) {
|
|
852
|
+
await ctx.desktop().requireCapability('app-settings', 'app settings');
|
|
853
|
+
// An older desktop has the route but not these keys: ask it by capability, so it says "update Slates".
|
|
854
|
+
if (HOST_KEYS.some((key) => input[key] !== undefined)) {
|
|
855
|
+
await ctx.desktop().requireCapability('agent-host', "choosing who does the Studio Agent's thinking");
|
|
856
|
+
}
|
|
857
|
+
return ok(await ctx.desktop().post('/agent/app-settings', input));
|
|
858
|
+
},
|
|
859
|
+
};
|
|
860
|
+
export const getAsset = {
|
|
861
|
+
id: 'slates_get_asset',
|
|
862
|
+
description: 'One asset in full (UUID or badge code): its recorded prompt, model, settings and inputs, its source picture and linked clips, and how many clips and board shots use it. slates_list_assets rows are compact; read this before reusing or explaining one card.',
|
|
863
|
+
input: z.object({ projectId: z.string().uuid(), assetId: z.string().min(1) }).strict(),
|
|
864
|
+
async run(input, ctx) {
|
|
865
|
+
await ctx.desktop().requireCapability('asset-detail', 'reading one asset in full');
|
|
866
|
+
const id = (await resolveAssetRefs(ctx, input.projectId, [input.assetId])).get(input.assetId).id;
|
|
867
|
+
return ok(await ctx.desktop().get('/agent/assets/get', { id }));
|
|
868
|
+
},
|
|
869
|
+
};
|
|
870
|
+
export const linkAssetSource = {
|
|
871
|
+
id: 'slates_link_asset_source',
|
|
872
|
+
description: "Link a clip to the picture it came from, as the clip card's Link video to image does (it adds a link; a clip can have several), or pass imageAssetId null to unlink it from every picture. UUIDs or badge codes.",
|
|
873
|
+
input: z
|
|
874
|
+
.object({ projectId: z.string().uuid(), assetId: z.string().min(1).describe('The clip.'), imageAssetId: z.string().min(1).nullable().describe('The picture, or null to unlink.') })
|
|
875
|
+
.strict(),
|
|
876
|
+
async run(input, ctx) {
|
|
877
|
+
await ctx.desktop().requireCapability('asset-link', 'linking a clip to its picture');
|
|
878
|
+
const refs = [input.assetId, ...(input.imageAssetId ? [input.imageAssetId] : [])];
|
|
879
|
+
const resolved = await resolveAssetRefs(ctx, input.projectId, refs);
|
|
880
|
+
return ok(await ctx.desktop().post('/agent/assets/link-source', {
|
|
881
|
+
assetId: resolved.get(input.assetId).id,
|
|
882
|
+
imageAssetId: input.imageAssetId ? resolved.get(input.imageAssetId).id : null,
|
|
883
|
+
}));
|
|
884
|
+
},
|
|
885
|
+
};
|
|
886
|
+
export const extractVideoFrame = {
|
|
887
|
+
id: 'slates_extract_video_frame',
|
|
888
|
+
description: "Save one frame of a clip as a new picture in Media, as the clip card's camera does: at 'first', 'last' (default 'first') or a second. Free. The usual continuity move: extract a clip's last frame and use it as the next clip's first frame.",
|
|
889
|
+
input: z
|
|
890
|
+
.object({
|
|
891
|
+
projectId: z.string().uuid(),
|
|
892
|
+
assetId: z.string().min(1).describe('The clip, UUID or badge code.'),
|
|
893
|
+
at: z.union([z.enum(['first', 'last']), z.number().min(0)]).optional(),
|
|
894
|
+
})
|
|
895
|
+
.strict(),
|
|
896
|
+
async run(input, ctx) {
|
|
897
|
+
await ctx.desktop().requireCapability('frame-extract', 'extracting a frame');
|
|
898
|
+
const id = (await resolveAssetRefs(ctx, input.projectId, [input.assetId])).get(input.assetId).id;
|
|
899
|
+
return ok(await ctx.desktop().post('/agent/assets/extract-frame', { assetId: id, at: input.at ?? 'first' }));
|
|
900
|
+
},
|
|
901
|
+
};
|
|
482
902
|
export const getMe = {
|
|
483
903
|
id: 'slates_get_me',
|
|
484
904
|
description: 'Identity, license tier, and credit balance for the connected Slates account.',
|
|
@@ -1149,10 +1569,13 @@ export const generateChatGptImage = {
|
|
|
1149
1569
|
description: 'Generate an image through the local Codex host using the connected ChatGPT account, then save it in Slates with its exact submitted prompt, reference lineage and measured dimensions. Uses ChatGPT account limits, never Slates credits or a paid API fallback. Check slates_get_chatgpt_status first. Supply a new UUID requestId once per intended generation; reuse it for retries to avoid duplicate generation/import. No explicit image model, quality or size controls are exposed. Prompt may request visual properties without guaranteeing them. Background returns a generationId for slates_get_generation_status.',
|
|
1150
1570
|
input: z.object({ projectId: z.string().uuid(), requestId: z.string().uuid(), prompt: z.string().min(1),
|
|
1151
1571
|
aspectRatio: z.enum(CHATGPT_FRAMING_RATIOS).optional().describe('Optional framing request appended verbally to the prompt, not an exact output-size guarantee.'),
|
|
1152
|
-
referenceAssetIds: z.array(z.string().min(1)).optional(), background: z.boolean().optional() }),
|
|
1572
|
+
referenceAssetIds: z.array(z.string().min(1)).optional(), background: z.boolean().optional(), folderId: folderIdField }),
|
|
1153
1573
|
async run(input, ctx) {
|
|
1154
1574
|
const desktop = ctx.desktop();
|
|
1155
1575
|
await desktop.requireCapability('chatgpt-image-generation', 'ChatGPT image generation');
|
|
1576
|
+
// A desktop before the folder rule ignores folderId and files at the root.
|
|
1577
|
+
if (input.folderId !== undefined)
|
|
1578
|
+
await desktop.requireCapability('media-folder', 'choosing the folder a result lands in');
|
|
1156
1579
|
const refs = await resolveAssetRefs(ctx, input.projectId, input.referenceAssetIds ?? []);
|
|
1157
1580
|
const result = await desktop.post('/agent/generation/chatgpt-image', {
|
|
1158
1581
|
...input, referenceAssetIds: (input.referenceAssetIds ?? []).map(id => refs.get(id).id),
|
|
@@ -1203,17 +1626,22 @@ export const uploadReferenceImage = {
|
|
|
1203
1626
|
.enum(['image', 'video', 'audio'])
|
|
1204
1627
|
.optional()
|
|
1205
1628
|
.describe('Asset kind for a filePath import — "image" (default), "video", or "audio". A dataUrl is always an image.'),
|
|
1629
|
+
folderId: folderIdField,
|
|
1206
1630
|
})
|
|
1207
1631
|
.refine((d) => !!d.filePath !== !!d.dataUrl, {
|
|
1208
1632
|
message: 'Pass exactly one of filePath or dataUrl',
|
|
1209
1633
|
}),
|
|
1210
1634
|
async run(input, ctx) {
|
|
1211
1635
|
const desktop = ctx.desktop();
|
|
1636
|
+
// A desktop before the folder rule ignores folderId and files at the root.
|
|
1637
|
+
if (input.folderId !== undefined)
|
|
1638
|
+
await desktop.requireCapability('media-folder', 'choosing the folder a result lands in');
|
|
1212
1639
|
if (input.filePath) {
|
|
1213
1640
|
const r = await desktop.post('/agent/assets/upload', {
|
|
1214
1641
|
projectId: input.projectId,
|
|
1215
1642
|
filePath: input.filePath,
|
|
1216
1643
|
type: input.type ?? 'image',
|
|
1644
|
+
...folderBody(input.folderId),
|
|
1217
1645
|
});
|
|
1218
1646
|
return ok(r);
|
|
1219
1647
|
}
|
|
@@ -1223,6 +1651,7 @@ export const uploadReferenceImage = {
|
|
|
1223
1651
|
const r = await desktop.post('/agent/assets/upload-base64', {
|
|
1224
1652
|
projectId: input.projectId,
|
|
1225
1653
|
dataUrl: input.dataUrl,
|
|
1654
|
+
...folderBody(input.folderId),
|
|
1226
1655
|
});
|
|
1227
1656
|
return ok(r);
|
|
1228
1657
|
},
|
|
@@ -1787,8 +2216,12 @@ export const generateImage = {
|
|
|
1787
2216
|
referenceAssetIds: z.array(z.string()).max(16).optional().describe("Project assets as references — UUIDs or badge codes (\"IMG-A8\"), resolved at call time. Requires projectId. Caps: GPT Image 16, nano-banana-2 14, FLUX/Seedream lower. Label every reference role in the prompt."),
|
|
1788
2217
|
background: z.boolean().optional().describe(BACKGROUND_DESCRIBE),
|
|
1789
2218
|
confirm: z.boolean().optional().describe('Set true to bypass the confirm gate.'),
|
|
2219
|
+
folderId: z.string().uuid().nullable().optional().describe(`${FOLDER_DESCRIBE} Needs projectId.`),
|
|
1790
2220
|
}),
|
|
1791
2221
|
async run(input, ctx) {
|
|
2222
|
+
// A desktop before the folder rule ignores folderId and files at the root.
|
|
2223
|
+
if (input.folderId !== undefined)
|
|
2224
|
+
await ctx.desktop().requireCapability('media-folder', 'choosing the folder a result lands in');
|
|
1792
2225
|
// Clarification gate: aspectRatio + resolution must be deliberate.
|
|
1793
2226
|
// Mirrors the cost confirm gate — defaults silently wasted credits
|
|
1794
2227
|
// (1:1 when user wanted 16:9, 4k when 1k would have done). The LLM
|
|
@@ -1970,6 +2403,7 @@ export const generateImage = {
|
|
|
1970
2403
|
count: input.count ?? 1,
|
|
1971
2404
|
...(isGptImageModel(imageModel) ? { gptQuality: input.quality ?? DEFAULT_GPT_QUALITY, gptBackground: input.backgroundMode } : {}),
|
|
1972
2405
|
...(referenceAssetIds.length > 0 ? { referenceAssetIds } : {}),
|
|
2406
|
+
...folderBody(input.folderId),
|
|
1973
2407
|
background: input.background,
|
|
1974
2408
|
});
|
|
1975
2409
|
// Partial multi-image failure: the desktop attaches an error message
|
|
@@ -2282,6 +2716,105 @@ export const editImage = {
|
|
|
2282
2716
|
};
|
|
2283
2717
|
},
|
|
2284
2718
|
};
|
|
2719
|
+
/**
|
|
2720
|
+
* The picture viewer's EXTRACT (its Cells tab): a 2x2 or 3x3 grid picture, cells cropped out and re-rendered
|
|
2721
|
+
* at full resolution, each as its own new picture. BILLABLE, and priced by the DESKTOP: its quote is the
|
|
2722
|
+
* `imageCost` the viewer's Extract button prints, for this seat, rung and cell count, so the price an agent
|
|
2723
|
+
* is shown cannot differ from the button's. The confirm gate is every billable op's: above CONFIRM_CREDITS
|
|
2724
|
+
* with no `confirm`, the op answers with the price and the exact prompt, and spends nothing.
|
|
2725
|
+
*/
|
|
2726
|
+
/** The seats the viewer's Cells box offers. Mirrors slate's `GRID_EXTRACT_MODELS` (`shared/prompts/storyboard-grids.ts`), which this package cannot import. */
|
|
2727
|
+
const GRID_EXTRACT_SEATS = ['nano-banana-2', 'flux-2-max', 'seedream-5-lite'];
|
|
2728
|
+
export const extractGridCells = {
|
|
2729
|
+
id: 'slates_extract_grid_cells',
|
|
2730
|
+
billable: true,
|
|
2731
|
+
description: "Pull cells out of a 2x2 or 3x3 grid picture, as the picture viewer's Cells tab does: each named cell is cropped and re-rendered at full resolution as its own NEW picture (the grid is untouched). Name cells as they read on screen, row number then column letter: 1A top-left, 2C second row third column. Default seat " +
|
|
2732
|
+
toolModelFor('grid-extract') +
|
|
2733
|
+
" (the grid-extract tool seat, what the viewer opens on); resolution and aspect ratio default as the viewer does (the seat's own rung, the ratio nearest the grid's shape). prompt adds words to every cell; on the default seat @name and #look in it attach their pictures, and referenceAssetIds adds pictures, as in the viewer. " +
|
|
2734
|
+
CONFIRM_GATE_SENTENCE +
|
|
2735
|
+
" The confirm answer carries the price the viewer's Extract button prints and the exact prompt that will be sent.",
|
|
2736
|
+
input: z.object({
|
|
2737
|
+
projectId: z.string().uuid(),
|
|
2738
|
+
assetId: z.string().min(1).describe('The grid picture — UUID or badge code. Must be a 2x2 or 3x3 grid.'),
|
|
2739
|
+
cells: z.array(z.string().regex(/^[1-3][A-Ca-c]$/, 'a cell is its row number then column letter, e.g. 1A')).min(1).max(9).describe('Cells to extract, e.g. ["1A","2C"].'),
|
|
2740
|
+
model: zEnum(GRID_EXTRACT_SEATS).optional(),
|
|
2741
|
+
resolution: z.enum(['1k', '2k', '3k', '4k']).optional().describe("Default and ceiling are the seat's own, as in the viewer."),
|
|
2742
|
+
aspectRatio: z.string().optional().describe("Output aspect ratio, one the seat offers. Default: the ratio nearest the grid's own shape."),
|
|
2743
|
+
prompt: z.string().max(2500).optional().describe('Words for every cell (what the shot is). On the default seat @name and #look attach their pictures; the crop is image 1.'),
|
|
2744
|
+
referenceAssetIds: z.array(z.string()).max(13).optional().describe('Pictures added beside the crop (default seat only), UUIDs or badge codes.'),
|
|
2745
|
+
confirm: z.boolean().optional().describe('Set true to bypass the confirm gate after explicit user OK.'),
|
|
2746
|
+
background: z.boolean().optional().describe(BACKGROUND_DESCRIBE),
|
|
2747
|
+
}),
|
|
2748
|
+
async run(input, ctx) {
|
|
2749
|
+
const desktop = ctx.desktop();
|
|
2750
|
+
await desktop.requireCapability('grid-extract', 'extracting grid cells');
|
|
2751
|
+
if (input.background)
|
|
2752
|
+
await desktop.requireCapability('background-generation', 'background generation');
|
|
2753
|
+
const refs = await resolveAssetRefs(ctx, input.projectId, [input.assetId, ...(input.referenceAssetIds ?? [])]);
|
|
2754
|
+
const grid = refs.get(input.assetId);
|
|
2755
|
+
const request = {
|
|
2756
|
+
projectId: input.projectId,
|
|
2757
|
+
assetId: grid.id,
|
|
2758
|
+
cells: input.cells.map((c) => c.toUpperCase()),
|
|
2759
|
+
model: input.model,
|
|
2760
|
+
resolution: input.resolution,
|
|
2761
|
+
aspectRatio: input.aspectRatio,
|
|
2762
|
+
prompt: input.prompt,
|
|
2763
|
+
referenceAssetIds: input.referenceAssetIds?.map((r) => refs.get(r).id),
|
|
2764
|
+
};
|
|
2765
|
+
// The desktop prices it: the viewer's own `imageCost`, for this seat, rung and cell count. The desktop also
|
|
2766
|
+
// refuses what the viewer would (not a grid, a cell the grid lacks, a seat it does not offer) before any spend.
|
|
2767
|
+
const quote = await desktop.get('/agent/generation/extract-quote', { request: JSON.stringify(request) });
|
|
2768
|
+
const gridName = grid.code ?? grid.id;
|
|
2769
|
+
if (quote.credits > CONFIRM_CREDITS && !input.confirm) {
|
|
2770
|
+
return ok({
|
|
2771
|
+
requires_confirm: true,
|
|
2772
|
+
estimated_credits: quote.credits,
|
|
2773
|
+
estimated_cents: quote.credits,
|
|
2774
|
+
cells: quote.cells,
|
|
2775
|
+
model: quote.model,
|
|
2776
|
+
resolution: quote.resolution,
|
|
2777
|
+
aspectRatio: quote.aspectRatio,
|
|
2778
|
+
credits_per_cell: quote.creditsPerCell,
|
|
2779
|
+
prompt_sent: quote.prompt,
|
|
2780
|
+
...(quote.notes.length ? { notes: quote.notes } : {}),
|
|
2781
|
+
message: `Cost: ${fmtCredits(quote.credits)} to extract ${quote.cells.length} cell${quote.cells.length === 1 ? '' : 's'} (${quote.cells.join(', ')}) of ${gridName} with ${quote.modelLabel} at ${quote.resolution}, ${fmtCredits(quote.creditsPerCell)} each. ` +
|
|
2782
|
+
(quote.notes.length ? `${quote.notes.join('. ')}. ` : '') +
|
|
2783
|
+
`Re-call with confirm=true after the user explicitly OKs the spend. When discussing with the user, refer to the grid by its code (matches the gallery badge).`,
|
|
2784
|
+
});
|
|
2785
|
+
}
|
|
2786
|
+
const result = await desktop.post('/agent/generation/extract-cells', { ...request, background: input.background });
|
|
2787
|
+
if (result.background) {
|
|
2788
|
+
return backgroundSubmitted(`extraction of ${quote.cells.join(', ')} from ${gridName}`, result.generationIds ?? [], {
|
|
2789
|
+
projectId: input.projectId,
|
|
2790
|
+
gridAssetId: grid.id,
|
|
2791
|
+
cells: quote.cells,
|
|
2792
|
+
model: quote.model,
|
|
2793
|
+
cost_credits: quote.credits,
|
|
2794
|
+
});
|
|
2795
|
+
}
|
|
2796
|
+
const assets = result.assets ?? [];
|
|
2797
|
+
if (assets.length === 0)
|
|
2798
|
+
throw new Error(result.error ?? 'Extraction failed');
|
|
2799
|
+
const codes = assets.map((a) => a.code ?? a.id).join(', ');
|
|
2800
|
+
const partial = result.success === false ? ` Only ${assets.length} of ${quote.cells.length} cells came back; the rest failed (slates_list_generations shows why).` : '';
|
|
2801
|
+
return {
|
|
2802
|
+
text: `Extracted ${assets.length} cell${assets.length === 1 ? '' : 's'} (${quote.cells.join(', ')}) of ${gridName} as ${codes} via ${quote.modelLabel} at ${quote.resolution}, ${fmtCredits(quote.credits)}.${partial} ` +
|
|
2803
|
+
BACKGROUND_REVIEW_POINTER,
|
|
2804
|
+
data: {
|
|
2805
|
+
projectId: input.projectId,
|
|
2806
|
+
gridAssetId: grid.id,
|
|
2807
|
+
cells: quote.cells,
|
|
2808
|
+
model: quote.model,
|
|
2809
|
+
resolution: quote.resolution,
|
|
2810
|
+
aspectRatio: quote.aspectRatio,
|
|
2811
|
+
cost_credits: quote.credits,
|
|
2812
|
+
assets,
|
|
2813
|
+
generationIds: result.generationIds,
|
|
2814
|
+
},
|
|
2815
|
+
};
|
|
2816
|
+
},
|
|
2817
|
+
};
|
|
2285
2818
|
// ── Generate video ──────────────────────────────────────────────
|
|
2286
2819
|
//
|
|
2287
2820
|
// VIDEO_MODELS and the capability-derived param vocabulary are declared near the
|
|
@@ -2751,8 +3284,12 @@ export const generateVideo = {
|
|
|
2751
3284
|
negativePrompt: z.string().optional(),
|
|
2752
3285
|
background: z.boolean().optional().describe(BACKGROUND_DESCRIBE),
|
|
2753
3286
|
confirm: z.boolean().optional().describe('Set true after explicit user OK to bypass the confirm gate (which fires for almost every video gen since they\'re expensive).'),
|
|
3287
|
+
folderId: folderIdField,
|
|
2754
3288
|
}),
|
|
2755
3289
|
async run(input, ctx) {
|
|
3290
|
+
// A desktop before the folder rule ignores folderId and files at the root.
|
|
3291
|
+
if (input.folderId !== undefined)
|
|
3292
|
+
await ctx.desktop().requireCapability('media-folder', 'choosing the folder a result lands in');
|
|
2756
3293
|
// Resolve the model FIRST — forgiving normalization (cost keys, alias
|
|
2757
3294
|
// spellings) with a teaching error, so a wrong id never costs the agent
|
|
2758
3295
|
// a retry spiral. Anything the key encoded (duration/res/audio) fills
|
|
@@ -3206,6 +3743,7 @@ export const generateVideo = {
|
|
|
3206
3743
|
seedanceRealFace: input.seedanceRealFace,
|
|
3207
3744
|
realFaceConsent: input.realFaceConsent,
|
|
3208
3745
|
negativePrompt: input.negativePrompt,
|
|
3746
|
+
...folderBody(input.folderId),
|
|
3209
3747
|
background: input.background,
|
|
3210
3748
|
});
|
|
3211
3749
|
if (!result.success) {
|
|
@@ -3365,8 +3903,12 @@ export const generateAudio = {
|
|
|
3365
3903
|
.describe('seed-audio only — ONE image asset to score what is in frame. MUTUALLY EXCLUSIVE with audioReferenceAssetIds.'),
|
|
3366
3904
|
background: z.boolean().optional().describe(BACKGROUND_DESCRIBE),
|
|
3367
3905
|
confirm: z.boolean().optional().describe('Set true to bypass the confirm gate after explicit user OK.'),
|
|
3906
|
+
folderId: folderIdField,
|
|
3368
3907
|
}),
|
|
3369
3908
|
run: async (input, ctx) => {
|
|
3909
|
+
// A desktop before the folder rule ignores folderId and files at the root.
|
|
3910
|
+
if (input.folderId !== undefined)
|
|
3911
|
+
await ctx.desktop().requireCapability('media-folder', 'choosing the folder a result lands in');
|
|
3370
3912
|
// ── Per-surface clarification + constraint gates ──
|
|
3371
3913
|
//
|
|
3372
3914
|
// `null` for the TTS seat is load-bearing rather than a placeholder: that
|
|
@@ -3500,6 +4042,7 @@ export const generateAudio = {
|
|
|
3500
4042
|
promptInfluence: input.promptInfluence,
|
|
3501
4043
|
audioReferenceAssetIds: (input.audioReferenceAssetIds ?? []).map((r) => rid(r)),
|
|
3502
4044
|
imageReferenceAssetId: rid(input.imageReferenceAssetId),
|
|
4045
|
+
...folderBody(input.folderId),
|
|
3503
4046
|
background: input.background,
|
|
3504
4047
|
});
|
|
3505
4048
|
if (!result.success)
|
|
@@ -3813,9 +4356,13 @@ export const editVideo = {
|
|
|
3813
4356
|
seedanceFace: z.boolean().optional().describe('seedance-2.5-edit ONLY: set true when a CHARACTER FACE is visible in the clip. Faceless edits run on BytePlus; faces are blocked there and must route to the relaxed provider, which costs ~35% more. A face edit submitted without this flag is rejected by the provider, not silently downgraded.'),
|
|
3814
4357
|
background: z.boolean().optional().describe(BACKGROUND_DESCRIBE),
|
|
3815
4358
|
confirm: z.boolean().optional().describe('Set true to bypass the cost confirm gate after the user OKs the spend.'),
|
|
4359
|
+
folderId: folderIdField,
|
|
3816
4360
|
}),
|
|
3817
4361
|
async run(input, ctx) {
|
|
3818
4362
|
const desktop = ctx.desktop();
|
|
4363
|
+
// A desktop before the folder rule ignores folderId and files at the root.
|
|
4364
|
+
if (input.folderId !== undefined)
|
|
4365
|
+
await desktop.requireCapability('media-folder', 'choosing the folder a result lands in');
|
|
3819
4366
|
await desktop.requireCapability('edit-video', 'video editing (Kling O3 edit)');
|
|
3820
4367
|
if (input.background) {
|
|
3821
4368
|
await desktop.requireCapability('background-generation', 'background generation');
|
|
@@ -3918,6 +4465,7 @@ export const editVideo = {
|
|
|
3918
4465
|
keepAudio: input.keepAudio !== false,
|
|
3919
4466
|
videoResolution: input.videoResolution,
|
|
3920
4467
|
seedanceFace: input.seedanceFace,
|
|
4468
|
+
...folderBody(input.folderId),
|
|
3921
4469
|
background: input.background,
|
|
3922
4470
|
});
|
|
3923
4471
|
if (!result.success)
|
|
@@ -3955,7 +4503,7 @@ export const editVideo = {
|
|
|
3955
4503
|
// ── Trim a video to an exact window (fit-to-model primitive) ────
|
|
3956
4504
|
export const trimVideo = {
|
|
3957
4505
|
id: 'slates_trim_video',
|
|
3958
|
-
description: 'Trim a video asset to an exact [inSec, outSec] window and save the result as a NEW clip linked to the original (the original is untouched). This is the fit-to-model primitive: an 11s clip will not run on omni-flash-edit (3–10s) or Kling edit (3–15s), and a Seedance video reference must be 2–15s — trim it first, then edit/relocate the trimmed clip. EXACT re-encode (not a keyframe-snapped cut) so the result honors hard duration caps to the frame; any phone rotation flag is baked into the pixels in the same pass. The new clip lands with correct duration/width/height immediately. inSec defaults to 0.',
|
|
4506
|
+
description: 'Trim a video asset to an exact [inSec, outSec] window and save the result as a NEW clip linked to the original (the original is untouched). This is the fit-to-model primitive: an 11s clip will not run on omni-flash-edit (3–10s) or Kling edit (3–15s), and a Seedance video reference must be 2–15s — trim it first, then edit/relocate the trimmed clip. EXACT re-encode (not a keyframe-snapped cut) so the result honors hard duration caps to the frame; any phone rotation flag is baked into the pixels in the same pass. The new clip lands with correct duration/width/height immediately. inSec defaults to 0. Pass pieces to cut the window into SEVERAL clips in one call, as the Trim & split dialog does (split points, or Auto-split by longest piece, with seconds shared between neighbours); every new clip comes back.',
|
|
3959
4507
|
input: z.object({
|
|
3960
4508
|
projectId: z.string().uuid().describe('Project the clip lives in.'),
|
|
3961
4509
|
assetId: z
|
|
@@ -3963,6 +4511,15 @@ export const trimVideo = {
|
|
|
3963
4511
|
.describe('The VIDEO asset to trim — UUID or badge code ("VID-V3", bare "V3"); resolves against the project at call time.'),
|
|
3964
4512
|
inSec: z.number().min(0).optional().describe('Trim start in seconds (default 0).'),
|
|
3965
4513
|
outSec: z.number().positive().describe('Trim end in seconds. Must be greater than inSec.'),
|
|
4514
|
+
pieces: z
|
|
4515
|
+
.object({
|
|
4516
|
+
splitsAtSec: z.array(z.number().positive()).max(60).optional().describe('Cut the window at these seconds of the clip (Split here): each at least 0.15 s inside inSec and outSec and 0.2 s from the next.'),
|
|
4517
|
+
longestSec: z.number().min(1).max(60).optional().describe('Auto-split: even pieces, none longer than this (Longest piece). Use instead of splitsAtSec.'),
|
|
4518
|
+
shareSec: z.number().min(0).max(5).optional().describe('Pieces share: each piece also holds this many seconds of the next (default 0), so an edit can carry on into the next clip.'),
|
|
4519
|
+
})
|
|
4520
|
+
.strict()
|
|
4521
|
+
.optional()
|
|
4522
|
+
.describe('Cut the window into several clips instead of one. Needs splitsAtSec or longestSec.'),
|
|
3966
4523
|
}),
|
|
3967
4524
|
async run(input, ctx) {
|
|
3968
4525
|
const desktop = ctx.desktop();
|
|
@@ -3972,6 +4529,24 @@ export const trimVideo = {
|
|
|
3972
4529
|
if (input.outSec - inSec < 0.05) {
|
|
3973
4530
|
throw new Error('outSec must be at least ~0.1s after inSec.');
|
|
3974
4531
|
}
|
|
4532
|
+
if (input.pieces) {
|
|
4533
|
+
// 1.6.0 would cut the one window and ignore pieces without a word.
|
|
4534
|
+
await desktop.requireCapability('trim-pieces', 'cutting a clip into pieces');
|
|
4535
|
+
if (input.pieces.splitsAtSec === undefined && input.pieces.longestSec === undefined) {
|
|
4536
|
+
throw new Error('pieces needs splitsAtSec (where to cut) or longestSec (Auto-split by longest piece).');
|
|
4537
|
+
}
|
|
4538
|
+
const cut = await desktop.post('/agent/assets/trim-video', {
|
|
4539
|
+
assetId,
|
|
4540
|
+
inSec,
|
|
4541
|
+
outSec: input.outSec,
|
|
4542
|
+
pieces: input.pieces,
|
|
4543
|
+
});
|
|
4544
|
+
const named = cut.assets.map((a, i) => `${a.code ?? a.id} (${cut.pieces[i].start.toFixed(1)}–${cut.pieces[i].end.toFixed(1)}s)`);
|
|
4545
|
+
return {
|
|
4546
|
+
text: `Cut ${cut.assets.length} clip${cut.assets.length === 1 ? '' : 's'} from the ${inSec.toFixed(1)}–${input.outSec.toFixed(1)}s window: ${named.join(', ')}. Edit or generate from them by id/code.`,
|
|
4547
|
+
data: { assets: cut.assets, pieces: cut.pieces },
|
|
4548
|
+
};
|
|
4549
|
+
}
|
|
3975
4550
|
const r = await desktop.post('/agent/assets/trim-video', {
|
|
3976
4551
|
assetId,
|
|
3977
4552
|
inSec,
|
|
@@ -4792,7 +5367,7 @@ export const updateFrame = {
|
|
|
4792
5367
|
// of what the Shot in this slot already encodes — the image's role and the
|
|
4793
5368
|
// beat's words — and they were backfilled into Shots on 2026-08-31. Use
|
|
4794
5369
|
// slates_update_shot for either.
|
|
4795
|
-
'Update a slot: its shot label, notes, bound asset (assetId=null unbinds), scene or position. A scene or position change carries the words in this slot with it on the Script page. The BEAT — its line, references, model, prompt and framing — lives on the Shot in this slot; use slates_update_shot for that.',
|
|
5370
|
+
'Update a slot: its shot label, notes, bound asset (assetId=null unbinds), scene or position. A scene or position change carries the words in this slot with it on the Script page. preferredClipId picks the clip the timeline takes for this slot (a video asset id or VID code; null goes back to the slot\'s most recent linked clip), as the take menu\'s "Use in the timeline" does. The BEAT — its line, references, model, prompt and framing — lives on the Shot in this slot; use slates_update_shot for that.',
|
|
4796
5371
|
input: z.object({
|
|
4797
5372
|
frameId: z.string().uuid(),
|
|
4798
5373
|
shotLabel: z.string().optional(),
|
|
@@ -4800,8 +5375,12 @@ export const updateFrame = {
|
|
|
4800
5375
|
assetId: z.string().uuid().nullable().optional(),
|
|
4801
5376
|
sceneId: z.string().uuid().optional(),
|
|
4802
5377
|
position: z.number().int().min(0).optional(),
|
|
5378
|
+
preferredClipId: z.string().min(1).nullable().optional().describe('The clip the timeline takes for this slot: a video asset id or badge code in the project; null clears it.'),
|
|
4803
5379
|
}),
|
|
4804
5380
|
async run(input, ctx) {
|
|
5381
|
+
// 1.6.0 takes the call and drops preferredClipId without a word.
|
|
5382
|
+
if (input.preferredClipId !== undefined)
|
|
5383
|
+
await ctx.desktop().requireCapability('frame-preferred-clip', 'choosing the clip a slot uses');
|
|
4805
5384
|
return ok(await ctx.desktop().post('/agent/frames/update', {
|
|
4806
5385
|
id: input.frameId,
|
|
4807
5386
|
data: {
|
|
@@ -4810,6 +5389,7 @@ export const updateFrame = {
|
|
|
4810
5389
|
assetId: input.assetId,
|
|
4811
5390
|
sceneId: input.sceneId,
|
|
4812
5391
|
position: input.position,
|
|
5392
|
+
preferredClipId: input.preferredClipId,
|
|
4813
5393
|
},
|
|
4814
5394
|
}));
|
|
4815
5395
|
},
|
|
@@ -4829,7 +5409,7 @@ export const updateFrame = {
|
|
|
4829
5409
|
*/
|
|
4830
5410
|
export const batchUpdateFrames = {
|
|
4831
5411
|
id: 'slates_batch_update_frames',
|
|
4832
|
-
description: 'Update MANY slots in one call — shot labels, notes, asset binding, scene/position. Prefer this over repeated slates_update_frame when re-arranging a scene: it is one round-trip and one UI refresh, and every id is validated before anything is written, so the batch never lands half-applied. A scene or position change carries the words in each slot with it on the Script page. To write the BEATS themselves, use slates_create_shot / slates_update_shot.',
|
|
5412
|
+
description: 'Update MANY slots in one call — shot labels, notes, asset binding, scene/position, and preferredClipId (the clip the timeline takes for the slot: a video asset id or VID code, null to clear). Prefer this over repeated slates_update_frame when re-arranging a scene: it is one round-trip and one UI refresh, and every id is validated before anything is written, so the batch never lands half-applied. A scene or position change carries the words in each slot with it on the Script page. To write the BEATS themselves, use slates_create_shot / slates_update_shot.',
|
|
4833
5413
|
input: z.object({
|
|
4834
5414
|
updates: z
|
|
4835
5415
|
.array(z.object({
|
|
@@ -4839,10 +5419,14 @@ export const batchUpdateFrames = {
|
|
|
4839
5419
|
assetId: z.string().uuid().nullable().optional(),
|
|
4840
5420
|
sceneId: z.string().uuid().optional(),
|
|
4841
5421
|
position: z.number().int().min(0).optional(),
|
|
5422
|
+
preferredClipId: z.string().min(1).nullable().optional().describe('A video asset id or badge code in the project; null clears it.'),
|
|
4842
5423
|
}))
|
|
4843
5424
|
.min(1),
|
|
4844
5425
|
}),
|
|
4845
5426
|
async run(input, ctx) {
|
|
5427
|
+
// 1.6.0 takes the call and drops preferredClipId without a word.
|
|
5428
|
+
if (input.updates.some((u) => u.preferredClipId !== undefined))
|
|
5429
|
+
await ctx.desktop().requireCapability('frame-preferred-clip', 'choosing the clip a slot uses');
|
|
4846
5430
|
return ok(await ctx.desktop().post('/agent/frames/batch-update', {
|
|
4847
5431
|
updates: input.updates.map((u) => ({
|
|
4848
5432
|
id: u.frameId,
|
|
@@ -4852,6 +5436,7 @@ export const batchUpdateFrames = {
|
|
|
4852
5436
|
assetId: u.assetId,
|
|
4853
5437
|
sceneId: u.sceneId,
|
|
4854
5438
|
position: u.position,
|
|
5439
|
+
preferredClipId: u.preferredClipId,
|
|
4855
5440
|
},
|
|
4856
5441
|
})),
|
|
4857
5442
|
}));
|
|
@@ -5143,6 +5728,60 @@ async function buildShotSpecInput(ctx, projectId, input) {
|
|
|
5143
5728
|
refEcho: describeResolvedRefs(refInputs, resolvedRefs),
|
|
5144
5729
|
};
|
|
5145
5730
|
}
|
|
5731
|
+
/**
|
|
5732
|
+
* The spec fields the caller NAMED, read out of the COMPLETE spec `buildShotSpecInput` built from the whole
|
|
5733
|
+
* input. Only those go to a route that merges a partial spec over a stored one (`slates_update_shot`, and
|
|
5734
|
+
* `slates_create_shot` from a result), so anything the caller omitted is left exactly as it was.
|
|
5735
|
+
*/
|
|
5736
|
+
function namedSpecPatch(input, spec) {
|
|
5737
|
+
const patch = {};
|
|
5738
|
+
if (input.prompt !== undefined)
|
|
5739
|
+
patch.prompt = input.prompt;
|
|
5740
|
+
if (input.model !== undefined) {
|
|
5741
|
+
patch.model = input.model;
|
|
5742
|
+
// 🚨 `authoredFor` is NOT re-stamped on a model swap. The whole point of
|
|
5743
|
+
// recording it is that the prompt stays written for the model it was
|
|
5744
|
+
// written for — the grammars genuinely differ — so the card can say so.
|
|
5745
|
+
}
|
|
5746
|
+
if (input.params !== undefined)
|
|
5747
|
+
patch.params = spec.params;
|
|
5748
|
+
// 🚨 ONLY THE ROLES THE CALLER NAMED. `buildShotSpecInput` always returns a
|
|
5749
|
+
// COMPLETE refs record, and the route merges one level deep — so sending all
|
|
5750
|
+
// five would clear every role the caller never mentioned. Clearing a role is
|
|
5751
|
+
// explicit: send `[]`.
|
|
5752
|
+
if (input.refs !== undefined) {
|
|
5753
|
+
const built = spec.refs;
|
|
5754
|
+
const named = {};
|
|
5755
|
+
for (const role of ORDERED_ATTACHMENT_ROLES) {
|
|
5756
|
+
if (input.refs[role] !== undefined)
|
|
5757
|
+
named[role] = built[role];
|
|
5758
|
+
}
|
|
5759
|
+
if (Object.keys(named).length > 0)
|
|
5760
|
+
patch.refs = named;
|
|
5761
|
+
}
|
|
5762
|
+
if (input.firstFrameAssetId !== undefined)
|
|
5763
|
+
patch.firstFrameAssetId = spec.firstFrameAssetId;
|
|
5764
|
+
if (input.lastFrameAssetId !== undefined)
|
|
5765
|
+
patch.lastFrameAssetId = spec.lastFrameAssetId;
|
|
5766
|
+
if (input.audioRefSpokenText !== undefined)
|
|
5767
|
+
patch.audioRefSpokenText = spec.audioRefSpokenText;
|
|
5768
|
+
Object.assign(patch, shotScriptPatch(input));
|
|
5769
|
+
// Same rule for the three mention lists — naming one must not clear the
|
|
5770
|
+
// other two.
|
|
5771
|
+
{
|
|
5772
|
+
const built = spec.mentions;
|
|
5773
|
+
const named = {};
|
|
5774
|
+
if (input.characterIds !== undefined)
|
|
5775
|
+
named.characterIds = built.characterIds;
|
|
5776
|
+
if (input.environmentIds !== undefined)
|
|
5777
|
+
named.environmentIds = built.environmentIds;
|
|
5778
|
+
if (input.styleIds !== undefined)
|
|
5779
|
+
named.styleIds = built.styleIds;
|
|
5780
|
+
if (Object.keys(named).length > 0)
|
|
5781
|
+
patch.mentions = named;
|
|
5782
|
+
}
|
|
5783
|
+
return patch;
|
|
5784
|
+
}
|
|
5146
5785
|
/**
|
|
5147
5786
|
* The capability gate, applied to a SAVED recipe.
|
|
5148
5787
|
*
|
|
@@ -5179,7 +5818,8 @@ export const createShot = {
|
|
|
5179
5818
|
input: z.object({
|
|
5180
5819
|
projectId: z.string().uuid(),
|
|
5181
5820
|
name: z.string().max(120).optional().describe('What to call it. Shown on the card; the prompt supplies one if you omit it.'),
|
|
5182
|
-
prompt: z.string().min(1).max(4000).describe('The RAW prompt, @mentions intact. Never write "image 1" yourself — the composer numbers references, and a hand-written number is wrong the moment one moves.'),
|
|
5821
|
+
prompt: z.string().min(1).max(4000).optional().describe('The RAW prompt, @mentions intact. Never write "image 1" yourself — the composer numbers references, and a hand-written number is wrong the moment one moves. Required unless fromAssetId supplies the recipe.'),
|
|
5822
|
+
fromAssetId: z.string().optional().describe("Save as shot: start from this asset's RECORDED recipe (UUID or badge code) — its prompt, model, settings and references — as Media's Save as shot does. That result becomes the Shot's first take unless another Shot already holds it. Anything else you pass overrides the recipe."),
|
|
5183
5823
|
model: z.string().optional().describe('Model id — the same ids slates_generate_image / slates_generate_video / slates_generate_audio take, and their descriptions carry the routing. Optional: a Shot can be planned before the model is decided.'),
|
|
5184
5824
|
params: shotParamsSchema,
|
|
5185
5825
|
refs: shotRefsSchema,
|
|
@@ -5196,6 +5836,8 @@ export const createShot = {
|
|
|
5196
5836
|
...shotScriptSchema,
|
|
5197
5837
|
}),
|
|
5198
5838
|
async run(input, ctx) {
|
|
5839
|
+
if (!input.prompt && !input.fromAssetId)
|
|
5840
|
+
throw new Error('Pass a prompt, or fromAssetId to save a result\'s recorded recipe as the Shot.');
|
|
5199
5841
|
const capErr = assertShotCapabilities(input.model, input.params);
|
|
5200
5842
|
if (capErr)
|
|
5201
5843
|
return capErr;
|
|
@@ -5204,6 +5846,9 @@ export const createShot = {
|
|
|
5204
5846
|
return alignErr;
|
|
5205
5847
|
const desktop = ctx.desktop();
|
|
5206
5848
|
await desktop.requireCapability('shots', 'saved Shots');
|
|
5849
|
+
// 1.6.0 ignores fromAssetId, and would then refuse the missing prompt.
|
|
5850
|
+
if (input.fromAssetId)
|
|
5851
|
+
await desktop.requireCapability('shot-from-asset', 'saving a result as a Shot');
|
|
5207
5852
|
// 1.5.8 ignores `position` and files the Shot last.
|
|
5208
5853
|
if (input.position !== undefined)
|
|
5209
5854
|
await desktop.requireCapability('shot-position', 'placing a new Shot at a slot');
|
|
@@ -5211,15 +5856,21 @@ export const createShot = {
|
|
|
5211
5856
|
const r = await desktop.post('/agent/shots', {
|
|
5212
5857
|
projectId: input.projectId,
|
|
5213
5858
|
name: input.name,
|
|
5214
|
-
|
|
5859
|
+
// From a result, only what the caller NAMED goes over its recorded recipe: the route merges it.
|
|
5860
|
+
spec: input.fromAssetId ? namedSpecPatch(input, spec) : { ...spec, ...shotScriptPatch(input) },
|
|
5861
|
+
fromAssetId: input.fromAssetId,
|
|
5215
5862
|
frameId: input.frameId ?? null,
|
|
5216
5863
|
sceneId: input.sceneId ?? null,
|
|
5217
5864
|
storyboardId: input.storyboardId ?? null,
|
|
5218
5865
|
position: input.position,
|
|
5219
5866
|
});
|
|
5867
|
+
const fromNote = input.fromAssetId
|
|
5868
|
+
? ` Saved from ${input.fromAssetId}: its recorded recipe is the Shot's, and that result is its first take (unless another Shot already holds it).` +
|
|
5869
|
+
(r.unsavedPaths?.length ? ` ${r.unsavedPaths.length} recorded attachment(s) are not project assets and are not on the Shot.` : '')
|
|
5870
|
+
: '';
|
|
5220
5871
|
// The CODE is the address the user sees on the row — say it back so the
|
|
5221
5872
|
// next call, and the next sentence to the user, can point at it.
|
|
5222
|
-
return ok(r.shot, `${r.shot?.code || 'Shot'} — "${r.shot?.name || 'Untitled'}". ${refEcho}`.trim());
|
|
5873
|
+
return ok(r.shot, `${r.shot?.code || 'Shot'} — "${r.shot?.name || 'Untitled'}". ${refEcho}${fromNote}`.trim());
|
|
5223
5874
|
},
|
|
5224
5875
|
};
|
|
5225
5876
|
export const updateShot = {
|
|
@@ -5257,52 +5908,7 @@ export const updateShot = {
|
|
|
5257
5908
|
const { spec } = await buildShotSpecInput(ctx, input.projectId, input);
|
|
5258
5909
|
// Only send the halves the caller actually named; the route merges a PARTIAL
|
|
5259
5910
|
// spec over the stored one, so an omitted field is never silently cleared.
|
|
5260
|
-
const patch =
|
|
5261
|
-
if (input.prompt !== undefined)
|
|
5262
|
-
patch.prompt = input.prompt;
|
|
5263
|
-
if (input.model !== undefined) {
|
|
5264
|
-
patch.model = input.model;
|
|
5265
|
-
// 🚨 `authoredFor` is NOT re-stamped on a model swap. The whole point of
|
|
5266
|
-
// recording it is that the prompt stays written for the model it was
|
|
5267
|
-
// written for — the grammars genuinely differ — so the card can say so.
|
|
5268
|
-
}
|
|
5269
|
-
if (input.params !== undefined)
|
|
5270
|
-
patch.params = spec.params;
|
|
5271
|
-
// 🚨 ONLY THE ROLES THE CALLER NAMED. `buildShotSpecInput` always returns a
|
|
5272
|
-
// COMPLETE refs record, and the route merges one level deep — so sending all
|
|
5273
|
-
// five would clear every role the caller never mentioned. Clearing a role is
|
|
5274
|
-
// explicit: send `[]`.
|
|
5275
|
-
if (input.refs !== undefined) {
|
|
5276
|
-
const built = spec.refs;
|
|
5277
|
-
const named = {};
|
|
5278
|
-
for (const role of ORDERED_ATTACHMENT_ROLES) {
|
|
5279
|
-
if (input.refs[role] !== undefined)
|
|
5280
|
-
named[role] = built[role];
|
|
5281
|
-
}
|
|
5282
|
-
if (Object.keys(named).length > 0)
|
|
5283
|
-
patch.refs = named;
|
|
5284
|
-
}
|
|
5285
|
-
if (input.firstFrameAssetId !== undefined)
|
|
5286
|
-
patch.firstFrameAssetId = spec.firstFrameAssetId;
|
|
5287
|
-
if (input.lastFrameAssetId !== undefined)
|
|
5288
|
-
patch.lastFrameAssetId = spec.lastFrameAssetId;
|
|
5289
|
-
if (input.audioRefSpokenText !== undefined)
|
|
5290
|
-
patch.audioRefSpokenText = spec.audioRefSpokenText;
|
|
5291
|
-
Object.assign(patch, shotScriptPatch(input));
|
|
5292
|
-
// Same rule for the three mention lists — naming one must not clear the
|
|
5293
|
-
// other two.
|
|
5294
|
-
{
|
|
5295
|
-
const built = spec.mentions;
|
|
5296
|
-
const named = {};
|
|
5297
|
-
if (input.characterIds !== undefined)
|
|
5298
|
-
named.characterIds = built.characterIds;
|
|
5299
|
-
if (input.environmentIds !== undefined)
|
|
5300
|
-
named.environmentIds = built.environmentIds;
|
|
5301
|
-
if (input.styleIds !== undefined)
|
|
5302
|
-
named.styleIds = built.styleIds;
|
|
5303
|
-
if (Object.keys(named).length > 0)
|
|
5304
|
-
patch.mentions = named;
|
|
5305
|
-
}
|
|
5911
|
+
const patch = namedSpecPatch(input, spec);
|
|
5306
5912
|
const r = await desktop.post('/agent/shots/update', {
|
|
5307
5913
|
id: input.shotId,
|
|
5308
5914
|
data: {
|
|
@@ -5407,7 +6013,7 @@ function describeVarietyReport(v) {
|
|
|
5407
6013
|
}
|
|
5408
6014
|
export const listShots = {
|
|
5409
6015
|
id: 'slates_list_shots',
|
|
5410
|
-
description: "Read the shot list — every Shot as a compact row IN BOARD ORDER (scene, then position), with the piece's cut count, runtime, saved-recipe price and its variety distribution. Read this before firing a set: if one shot size is the plurality or three cuts in a row share a camera move, the batch is wrong before a credit is spent.",
|
|
6016
|
+
description: "Read the shot list — every Shot as a compact row IN BOARD ORDER (scene, then position), with its place on the board (2C: scene number and slot letter, as the window shows it and the user says it), the piece's cut count, runtime, saved-recipe price and its variety distribution. Read this before firing a set: if one shot size is the plurality or three cuts in a row share a camera move, the batch is wrong before a credit is spent.",
|
|
5411
6017
|
input: z.object({
|
|
5412
6018
|
projectId: z.string().uuid(),
|
|
5413
6019
|
storyboardId: z.string().uuid().optional().describe('Only Shots attached to a frame in this board.'),
|
|
@@ -5448,6 +6054,7 @@ export const listShots = {
|
|
|
5448
6054
|
name: s.name,
|
|
5449
6055
|
scene: s.sceneName,
|
|
5450
6056
|
position: s.position,
|
|
6057
|
+
...(s.place !== undefined ? { place: s.place } : {}),
|
|
5451
6058
|
model: s.model,
|
|
5452
6059
|
references: s.referenceCount,
|
|
5453
6060
|
cuts: s.cuts,
|
|
@@ -5472,7 +6079,7 @@ ${describeVarietyReport(r.variety)}` : ''));
|
|
|
5472
6079
|
};
|
|
5473
6080
|
export const getShot = {
|
|
5474
6081
|
id: 'slates_get_shot',
|
|
5475
|
-
description: 'Read one Shot in full — the COMPOSED prompt the request will actually carry, its numbered references, anything it points at that no longer exists,
|
|
6082
|
+
description: 'Read one Shot in full — the COMPOSED prompt the request will actually carry, its numbered references, anything it points at that no longer exists, its exact credit quote, and its place on the board (2C, as the window shows it). Audit your own work here before firing.',
|
|
5476
6083
|
input: z.object({
|
|
5477
6084
|
shotId: z.string().describe('The Shot id, or its SHOT-A code as shown on the row.'),
|
|
5478
6085
|
}),
|
|
@@ -5484,7 +6091,7 @@ export const getShot = {
|
|
|
5484
6091
|
const r = await desktop.get('/agent/shots/get', { id: input.shotId });
|
|
5485
6092
|
const quote = await desktop.get('/agent/shots/quote', { input: JSON.stringify({ projectId: r.shot.projectId, shotIds: [r.shot.id] }) });
|
|
5486
6093
|
const q = quote.items[0];
|
|
5487
|
-
return ok({ ...r.shot, credits: q.credits, quoteFingerprint: quote.fingerprint }, `"${r.shot.name || 'Untitled'}" — ${r.shot.model ?? 'no model set'}, ` +
|
|
6094
|
+
return ok({ ...r.shot, credits: q.credits, quoteFingerprint: quote.fingerprint }, `${r.shot.place ? `${r.shot.place} ` : ''}"${r.shot.name || 'Untitled'}" — ${r.shot.model ?? 'no model set'}, ` +
|
|
5488
6095
|
(q.credits != null && !r.shot.blocked
|
|
5489
6096
|
? `${fmtCredits(q.credits ?? 0)}.`
|
|
5490
6097
|
: `CANNOT FIRE YET: ${r.shot.blocked ?? 'not priceable — set a model and a duration.'}`) +
|
|
@@ -5492,6 +6099,31 @@ export const getShot = {
|
|
|
5492
6099
|
`\nCOMPOSED PROMPT (what the model is told): ${r.shot.composedPrompt}`);
|
|
5493
6100
|
},
|
|
5494
6101
|
};
|
|
6102
|
+
/**
|
|
6103
|
+
* The Board's "Use pictures as first frames": each video Shot with a picture and no first frame starts its
|
|
6104
|
+
* video from that picture. The desktop runs the command's own pick and write (`firstFrameCandidate`,
|
|
6105
|
+
* `withFirstFrame`), so the recipe changes exactly as a click changes it. Free: nothing is generated.
|
|
6106
|
+
*/
|
|
6107
|
+
export const usePicturesAsFirstFrames = {
|
|
6108
|
+
id: 'slates_use_pictures_as_first_frames',
|
|
6109
|
+
description: "The Board's 'Use pictures as first frames': each video Shot that has a picture and no first frame starts its video from that picture (its tile picture, else its newest picture take; a picture that was its only plain Reference moves into the frame slot). Nothing is generated and nothing is charged. Pass shotIds (ids or SHOT-A codes) to act on those Shots; omit them to act on every Shot of the board open in the window, as the command does (open one with slates_set_view first). Returns, per Shot, the picture it set or why it had none.",
|
|
6110
|
+
input: z
|
|
6111
|
+
.object({
|
|
6112
|
+
projectId: z.string().uuid(),
|
|
6113
|
+
shotIds: z.array(z.string().min(1)).min(1).max(200).optional().describe('Only these Shots. Omit for every Shot of the board open in the window.'),
|
|
6114
|
+
})
|
|
6115
|
+
.strict(),
|
|
6116
|
+
async run(input, ctx) {
|
|
6117
|
+
await ctx.desktop().requireCapability('first-frames', 'using pictures as first frames');
|
|
6118
|
+
const r = await ctx.desktop().post('/agent/shots/first-frames', input);
|
|
6119
|
+
const name = (row) => row.place ?? row.code ?? row.shotId;
|
|
6120
|
+
const lines = r.results.map((row) => `${name(row)}: ${row.set ? 'first frame set' : (row.reason ?? 'nothing to set')}`);
|
|
6121
|
+
return {
|
|
6122
|
+
text: `${r.set} of ${r.results.length} Shot${r.results.length === 1 ? '' : 's'} now start from their picture.${lines.length ? `\n${lines.join('\n')}` : ''}`,
|
|
6123
|
+
data: r,
|
|
6124
|
+
};
|
|
6125
|
+
},
|
|
6126
|
+
};
|
|
5495
6127
|
/**
|
|
5496
6128
|
* SPLIT and MERGE — the chop decision, and the only two cross-row operations on
|
|
5497
6129
|
* this surface.
|
|
@@ -6124,7 +6756,7 @@ function describeGuideTopics() {
|
|
|
6124
6756
|
}
|
|
6125
6757
|
export const getPromptingGuide = {
|
|
6126
6758
|
id: 'slates_get_prompting_guide',
|
|
6127
|
-
description: 'For app help and exact UI instructions use topic "app-manual" with
|
|
6759
|
+
description: 'For app help and exact UI instructions use topic "app-manual" with the words of the question (query "export an mp4", "where is the timeline"): it returns the ONE best section of the canonical product manual and the headings of related ones to ask for next. No query returns the map of surfaces; a surface asked for by name (query "THE TIMELINE (CUT) AND EXPORT") returns its opening and its section names. depth "full" is the whole manual and large; avoid it. ' +
|
|
6128
6760
|
// 🚨 NO "ALWAYS READ THIS FIRST" SENTENCE. It stood here for months and was
|
|
6129
6761
|
// MEASURED at 13% compliance before and after the enforcement work — pointer
|
|
6130
6762
|
// prose is the shape that does not move the agent. What replaced it is
|
|
@@ -6144,8 +6776,7 @@ export const getPromptingGuide = {
|
|
|
6144
6776
|
if (input.topic.trim().toLowerCase() === 'app-manual') {
|
|
6145
6777
|
const content = input.query || input.depth === 'full'
|
|
6146
6778
|
? appManualSections(input.query)
|
|
6147
|
-
:
|
|
6148
|
-
guideSections(appManualSections()).map((s) => `- ${s.title}`).join('\n');
|
|
6779
|
+
: appManualIndex();
|
|
6149
6780
|
return { text: content, data: { topic: 'app-manual', bytes: Buffer.byteLength(content, 'utf8'), guide: content } };
|
|
6150
6781
|
}
|
|
6151
6782
|
const resolved = resolveGuideTopic(input.topic);
|
|
@@ -6350,8 +6981,21 @@ export const blenderRenderBlocking = {
|
|
|
6350
6981
|
export const ALL_OPERATIONS = [
|
|
6351
6982
|
getWorkspaceState,
|
|
6352
6983
|
getSelection,
|
|
6984
|
+
setSelection,
|
|
6353
6985
|
getView,
|
|
6354
6986
|
setView,
|
|
6987
|
+
getManualPicture,
|
|
6988
|
+
highlightControl,
|
|
6989
|
+
getComposer,
|
|
6990
|
+
setComposer,
|
|
6991
|
+
reorderFolders,
|
|
6992
|
+
reorderPins,
|
|
6993
|
+
getUsage,
|
|
6994
|
+
getAppSettings,
|
|
6995
|
+
setAppSettings,
|
|
6996
|
+
getAsset,
|
|
6997
|
+
linkAssetSource,
|
|
6998
|
+
extractVideoFrame,
|
|
6355
6999
|
getMe,
|
|
6356
7000
|
getCreditBalance,
|
|
6357
7001
|
listAvailableModels,
|
|
@@ -6400,6 +7044,7 @@ export const ALL_OPERATIONS = [
|
|
|
6400
7044
|
editVideo,
|
|
6401
7045
|
trimVideo,
|
|
6402
7046
|
editImage,
|
|
7047
|
+
extractGridCells,
|
|
6403
7048
|
getGenerationStatus,
|
|
6404
7049
|
listGenerations,
|
|
6405
7050
|
listTimelines,
|
|
@@ -6485,6 +7130,7 @@ export const ALL_OPERATIONS = [
|
|
|
6485
7130
|
pasteScript,
|
|
6486
7131
|
listShots,
|
|
6487
7132
|
getShot,
|
|
7133
|
+
usePicturesAsFirstFrames,
|
|
6488
7134
|
generateFromShots,
|
|
6489
7135
|
quoteBoard,
|
|
6490
7136
|
getBoardProgress,
|