@slatesvideo/shared 0.6.0 → 0.6.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/clients/blender.d.ts +50 -0
- package/dist/clients/blender.js +195 -0
- package/dist/operations/index.d.ts +24 -0
- package/dist/operations/index.js +246 -3
- package/dist/prompts/model-facts.js +1 -1
- package/dist/prompts/prompting-tips.js +1 -1
- package/dist/prompts/reference-composer.d.ts +57 -0
- package/dist/prompts/reference-composer.js +70 -1
- package/dist/skills/content.js +7 -2
- package/exports/slates-prompt-builder/generated/slates-prompt-builder-manifest.json +1 -1
- package/package.json +1 -1
- package/skills/slates-blocking-to-prompt.md +250 -0
- package/skills/slates-camera-language.md +196 -0
- package/skills/slates-dialogue-blocking.md +134 -0
- package/skills/slates-model-selection.md +1 -1
- package/skills/slates-previs-blocking.md +153 -0
- package/skills/slates-prompting-minimax-h3.md +2 -2
- package/skills/slates-restyle-from-blocking.md +121 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export declare const RENDER_TIMEOUT_MS: number;
|
|
2
|
+
/**
|
|
3
|
+
* Where the add-on comes from, and how to switch it on. ONE string.
|
|
4
|
+
*
|
|
5
|
+
* 🚨 IT WAS TWO, AND THAT IS ALWAYS A DRIFT. The op layer had its own
|
|
6
|
+
* differently-worded copy naming the same URL, so a moved download page or a
|
|
7
|
+
* renamed panel button would have had to be found in two files by someone who
|
|
8
|
+
* remembered both existed. Same fact, same sentence, one home.
|
|
9
|
+
*
|
|
10
|
+
* `slates.video/blender` is a real page as of 2026-08-28 (`slates-web`
|
|
11
|
+
* `src/app/blender/page.tsx`), and it is the ONLY thing that tells a stuck user
|
|
12
|
+
* where the add-on comes from. It is a hard dependency of this string, not a
|
|
13
|
+
* nice-to-have: keep the path or 301 it.
|
|
14
|
+
*/
|
|
15
|
+
export declare const BLENDER_SETUP_HINT: string;
|
|
16
|
+
export declare class BlenderNotRunningError extends Error {
|
|
17
|
+
constructor();
|
|
18
|
+
}
|
|
19
|
+
/** Raised when Blender executed the code and Python threw. Carries the traceback. */
|
|
20
|
+
export declare class BlenderExecError extends Error {
|
|
21
|
+
readonly stdout: string;
|
|
22
|
+
readonly stderr: string;
|
|
23
|
+
constructor(message: string, stdout?: string, stderr?: string);
|
|
24
|
+
}
|
|
25
|
+
export declare class BlenderBridgeClient {
|
|
26
|
+
/**
|
|
27
|
+
* Execute Python in Blender and return whatever the code assigned to
|
|
28
|
+
* `result`. `strict_json` is false so a stray Blender object comes back as
|
|
29
|
+
* its repr instead of failing the whole call — the agent can then correct
|
|
30
|
+
* itself, which it cannot do if the error is about serialization.
|
|
31
|
+
*/
|
|
32
|
+
execute(code: string, opts?: {
|
|
33
|
+
timeoutMs?: number;
|
|
34
|
+
}): Promise<{
|
|
35
|
+
result: unknown;
|
|
36
|
+
stdout: string;
|
|
37
|
+
stderr: string;
|
|
38
|
+
}>;
|
|
39
|
+
/**
|
|
40
|
+
* Execute code with the add-on package bound to `_slates`, so ops can call
|
|
41
|
+
* the shipped helpers (`_slates.previs`, `_slates.scene`, `_slates.docs`)
|
|
42
|
+
* instead of re-implementing them as inline Python string blobs.
|
|
43
|
+
*/
|
|
44
|
+
call(body: string, opts?: {
|
|
45
|
+
timeoutMs?: number;
|
|
46
|
+
}): Promise<unknown>;
|
|
47
|
+
/** True when a Blender with the add-on is reachable. Never throws. */
|
|
48
|
+
isReachable(): Promise<boolean>;
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=blender.d.ts.map
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import net from 'node:net';
|
|
2
|
+
// Thin client for the Slates Blender add-on's localhost execution bridge.
|
|
3
|
+
//
|
|
4
|
+
// Wire protocol (matches the add-on's `bridge/server.py`, which is Blender
|
|
5
|
+
// Lab's `blender_mcp` protocol):
|
|
6
|
+
//
|
|
7
|
+
// -> {"type":"execute","code":"...","strict_json":false}\0
|
|
8
|
+
// <- {"status":"ok","result":{...},"stdout":"...","stderr":"..."}\0
|
|
9
|
+
// <- {"status":"error","message":"<traceback>"}\0
|
|
10
|
+
//
|
|
11
|
+
// Requests and responses are NUL-delimited JSON over a plain TCP socket. The
|
|
12
|
+
// add-on executes on Blender's main thread via a timer, so a request is
|
|
13
|
+
// serviced within a tick rather than immediately — the socket stays open until
|
|
14
|
+
// the reply lands, and long jobs (renders) hold it open for as long as they
|
|
15
|
+
// take. That is why the timeout here is generous and configurable per call
|
|
16
|
+
// rather than a single global value.
|
|
17
|
+
const HOST = '127.0.0.1';
|
|
18
|
+
// The add-on binds the first free port in this range, so a second Blender (or
|
|
19
|
+
// a stale process holding 9876) shifts it forward instead of failing. We probe
|
|
20
|
+
// the same range in the same order.
|
|
21
|
+
const BASE_PORT = 9876;
|
|
22
|
+
const PORT_FALLBACKS = 3;
|
|
23
|
+
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
24
|
+
// Renders are the one call that legitimately runs for minutes.
|
|
25
|
+
export const RENDER_TIMEOUT_MS = 15 * 60_000;
|
|
26
|
+
const CONNECT_TIMEOUT_MS = 1_500;
|
|
27
|
+
/**
|
|
28
|
+
* Where the add-on comes from, and how to switch it on. ONE string.
|
|
29
|
+
*
|
|
30
|
+
* 🚨 IT WAS TWO, AND THAT IS ALWAYS A DRIFT. The op layer had its own
|
|
31
|
+
* differently-worded copy naming the same URL, so a moved download page or a
|
|
32
|
+
* renamed panel button would have had to be found in two files by someone who
|
|
33
|
+
* remembered both existed. Same fact, same sentence, one home.
|
|
34
|
+
*
|
|
35
|
+
* `slates.video/blender` is a real page as of 2026-08-28 (`slates-web`
|
|
36
|
+
* `src/app/blender/page.tsx`), and it is the ONLY thing that tells a stuck user
|
|
37
|
+
* where the add-on comes from. It is a hard dependency of this string, not a
|
|
38
|
+
* nice-to-have: keep the path or 301 it.
|
|
39
|
+
*/
|
|
40
|
+
export const BLENDER_SETUP_HINT = 'Install the Slates add-on from https://slates.video/blender, then in the 3D ' +
|
|
41
|
+
'viewport sidebar (press N) open the Slates tab and click Start Bridge.';
|
|
42
|
+
const NOT_RUNNING_MESSAGE = 'No Blender with the Slates add-on is listening on ' +
|
|
43
|
+
`${HOST}:${BASE_PORT}-${BASE_PORT + PORT_FALLBACKS}. ` +
|
|
44
|
+
BLENDER_SETUP_HINT;
|
|
45
|
+
export class BlenderNotRunningError extends Error {
|
|
46
|
+
constructor() {
|
|
47
|
+
super(NOT_RUNNING_MESSAGE);
|
|
48
|
+
this.name = 'BlenderNotRunningError';
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** Raised when Blender executed the code and Python threw. Carries the traceback. */
|
|
52
|
+
export class BlenderExecError extends Error {
|
|
53
|
+
stdout;
|
|
54
|
+
stderr;
|
|
55
|
+
constructor(message, stdout = '', stderr = '') {
|
|
56
|
+
super(message);
|
|
57
|
+
this.name = 'BlenderExecError';
|
|
58
|
+
this.stdout = stdout;
|
|
59
|
+
this.stderr = stderr;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Resolve the installed add-on package regardless of what Blender named it.
|
|
64
|
+
*
|
|
65
|
+
* Extensions are imported as `bl_ext.<repo>.slates_blender`, and the repo
|
|
66
|
+
* segment depends on where the user installed from — so the name cannot be
|
|
67
|
+
* hardcoded. Scanning `sys.modules` for the root package is the only stable
|
|
68
|
+
* handle. Submodules (`...slates_blender.bridge`) do not match the suffix, so
|
|
69
|
+
* the generator yields exactly the root.
|
|
70
|
+
*/
|
|
71
|
+
const PRELUDE = `
|
|
72
|
+
import sys as _sys
|
|
73
|
+
from importlib import import_module as _import_module
|
|
74
|
+
_slates = next(
|
|
75
|
+
(m for n, m in list(_sys.modules.items())
|
|
76
|
+
if n.endswith('slates_blender') and m is not None),
|
|
77
|
+
None,
|
|
78
|
+
)
|
|
79
|
+
if _slates is None:
|
|
80
|
+
raise RuntimeError(
|
|
81
|
+
'The Slates Blender add-on is not loaded in this Blender. '
|
|
82
|
+
'Enable it in Edit > Preferences > Add-ons.'
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
def _mod(_name):
|
|
86
|
+
"""Import a submodule of the add-on by short name.
|
|
87
|
+
|
|
88
|
+
The root package only imports \`bridge\` at load time — everything else is
|
|
89
|
+
lazy so enabling the add-on stays cheap — so \`_slates.previs\` is not
|
|
90
|
+
reliably an attribute. Go through importlib rather than assuming it is.
|
|
91
|
+
"""
|
|
92
|
+
return _import_module(_slates.__name__ + '.' + _name)
|
|
93
|
+
`.trim();
|
|
94
|
+
function connect(port) {
|
|
95
|
+
return new Promise((resolve, reject) => {
|
|
96
|
+
const socket = new net.Socket();
|
|
97
|
+
const onError = (err) => {
|
|
98
|
+
socket.destroy();
|
|
99
|
+
reject(err);
|
|
100
|
+
};
|
|
101
|
+
socket.setTimeout(CONNECT_TIMEOUT_MS, () => onError(new Error('connect timeout')));
|
|
102
|
+
socket.once('error', onError);
|
|
103
|
+
socket.connect(port, HOST, () => {
|
|
104
|
+
socket.setTimeout(0);
|
|
105
|
+
socket.removeListener('error', onError);
|
|
106
|
+
resolve(socket);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
async function connectAny() {
|
|
111
|
+
for (let port = BASE_PORT; port <= BASE_PORT + PORT_FALLBACKS; port++) {
|
|
112
|
+
try {
|
|
113
|
+
return await connect(port);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// Try the next port in the range.
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
throw new BlenderNotRunningError();
|
|
120
|
+
}
|
|
121
|
+
function request(socket, payload, timeoutMs) {
|
|
122
|
+
return new Promise((resolve, reject) => {
|
|
123
|
+
let buffer = '';
|
|
124
|
+
let settled = false;
|
|
125
|
+
const finish = (fn) => {
|
|
126
|
+
if (settled)
|
|
127
|
+
return;
|
|
128
|
+
settled = true;
|
|
129
|
+
clearTimeout(timer);
|
|
130
|
+
socket.destroy();
|
|
131
|
+
fn();
|
|
132
|
+
};
|
|
133
|
+
const timer = setTimeout(() => finish(() => reject(new Error(`Blender did not respond within ${Math.round(timeoutMs / 1000)}s. ` +
|
|
134
|
+
'It may be busy with a modal operator — check the Blender window.'))), timeoutMs);
|
|
135
|
+
socket.setEncoding('utf8');
|
|
136
|
+
socket.on('data', (chunk) => {
|
|
137
|
+
buffer += chunk;
|
|
138
|
+
const end = buffer.indexOf('\0');
|
|
139
|
+
if (end === -1)
|
|
140
|
+
return;
|
|
141
|
+
const raw = buffer.slice(0, end);
|
|
142
|
+
finish(() => {
|
|
143
|
+
try {
|
|
144
|
+
resolve(JSON.parse(raw));
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
reject(new Error(`Malformed reply from Blender: ${err.message}`));
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
socket.on('error', (err) => finish(() => reject(err)));
|
|
152
|
+
socket.on('close', () => finish(() => reject(new Error('Blender closed the connection before replying.'))));
|
|
153
|
+
socket.write(payload + '\0');
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
export class BlenderBridgeClient {
|
|
157
|
+
/**
|
|
158
|
+
* Execute Python in Blender and return whatever the code assigned to
|
|
159
|
+
* `result`. `strict_json` is false so a stray Blender object comes back as
|
|
160
|
+
* its repr instead of failing the whole call — the agent can then correct
|
|
161
|
+
* itself, which it cannot do if the error is about serialization.
|
|
162
|
+
*/
|
|
163
|
+
async execute(code, opts) {
|
|
164
|
+
const socket = await connectAny();
|
|
165
|
+
const payload = JSON.stringify({ type: 'execute', code, strict_json: false });
|
|
166
|
+
const res = await request(socket, payload, opts?.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
167
|
+
const stdout = res.stdout ?? '';
|
|
168
|
+
const stderr = res.stderr ?? '';
|
|
169
|
+
if (res.status !== 'ok') {
|
|
170
|
+
throw new BlenderExecError(res.message ?? 'Blender reported an error', stdout, stderr);
|
|
171
|
+
}
|
|
172
|
+
return { result: res.result, stdout, stderr };
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Execute code with the add-on package bound to `_slates`, so ops can call
|
|
176
|
+
* the shipped helpers (`_slates.previs`, `_slates.scene`, `_slates.docs`)
|
|
177
|
+
* instead of re-implementing them as inline Python string blobs.
|
|
178
|
+
*/
|
|
179
|
+
async call(body, opts) {
|
|
180
|
+
const { result } = await this.execute(`${PRELUDE}\n${body}`, opts);
|
|
181
|
+
return result;
|
|
182
|
+
}
|
|
183
|
+
/** True when a Blender with the add-on is reachable. Never throws. */
|
|
184
|
+
async isReachable() {
|
|
185
|
+
try {
|
|
186
|
+
const socket = await connectAny();
|
|
187
|
+
socket.destroy();
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
//# sourceMappingURL=blender.js.map
|
|
@@ -277,6 +277,7 @@ export declare const generateVideo: Operation<{
|
|
|
277
277
|
videoReferenceAssetIds?: string[];
|
|
278
278
|
videoReferenceSecondsEach?: number[];
|
|
279
279
|
audioReferenceAssetIds?: string[];
|
|
280
|
+
audioReferenceSpokenText?: string[];
|
|
280
281
|
sound?: boolean;
|
|
281
282
|
audioLanguage?: 'EN' | 'ZH' | 'JA' | 'KO' | 'ES';
|
|
282
283
|
generateMusic?: boolean;
|
|
@@ -538,6 +539,29 @@ export declare const deleteFrame: Operation<{
|
|
|
538
539
|
export declare const getPromptingGuide: Operation<{
|
|
539
540
|
topic: string;
|
|
540
541
|
}>;
|
|
542
|
+
export declare const blenderStatus: Operation<Record<string, never>>;
|
|
543
|
+
export declare const blenderExecute: Operation<{
|
|
544
|
+
code: string;
|
|
545
|
+
timeoutSeconds?: number;
|
|
546
|
+
}>;
|
|
547
|
+
export declare const blenderScene: Operation<Record<string, never>>;
|
|
548
|
+
export declare const blenderDocs: Operation<{
|
|
549
|
+
identifier: string;
|
|
550
|
+
}>;
|
|
551
|
+
export declare const blenderSearchDocs: Operation<{
|
|
552
|
+
query: string;
|
|
553
|
+
scope?: 'api' | 'manual';
|
|
554
|
+
maxResults?: number;
|
|
555
|
+
}>;
|
|
556
|
+
export declare const blenderRenderBlocking: Operation<{
|
|
557
|
+
projectId?: string;
|
|
558
|
+
resolutionX?: number;
|
|
559
|
+
resolutionY?: number;
|
|
560
|
+
fps?: number;
|
|
561
|
+
frameStart?: number;
|
|
562
|
+
frameEnd?: number;
|
|
563
|
+
basename?: string;
|
|
564
|
+
}>;
|
|
541
565
|
export declare const ALL_OPERATIONS: ReadonlyArray<Operation<unknown>>;
|
|
542
566
|
export {};
|
|
543
567
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/operations/index.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { z } from 'zod';
|
|
13
13
|
import { SlatesCloudClient } from '../clients/cloud.js';
|
|
14
14
|
import { SlatesDesktopClient } from '../clients/desktop.js';
|
|
15
|
+
import { BlenderBridgeClient, BLENDER_SETUP_HINT, RENDER_TIMEOUT_MS } from '../clients/blender.js';
|
|
15
16
|
import { SKILLS } from '../skills/content.js';
|
|
16
17
|
// Reference-capacity prose is DERIVED, never hand-typed — root CLAUDE.md:
|
|
17
18
|
// "never hand-type a fact an LLM will read". These helpers read MODEL_FACTS.
|
|
@@ -294,7 +295,7 @@ export const estimateGenerationCost = {
|
|
|
294
295
|
sound: z.boolean().optional().describe('Veo only — audio flag changes the cost key.'),
|
|
295
296
|
seedanceFace: z.boolean().optional().describe('Seedance AI-face route (pricier key).'),
|
|
296
297
|
seedanceRealFace: z.boolean().optional().describe('Seedance consented real-face route (premium key).'),
|
|
297
|
-
referenceImages: z.number().int().min(0).optional().describe(`minimax-h3 only — how many reference IMAGES the generation will carry. The first ${MINIMAX_FREE_REF_IMAGES} are free and each one after that is a paid dimension of the cost key, so a quote that omits this UNDER-REPORTS a reference-heavy job. Ignored by every other model
|
|
298
|
+
referenceImages: z.number().int().min(0).optional().describe(`minimax-h3 only — how many reference IMAGES the generation will carry. The first ${MINIMAX_FREE_REF_IMAGES} are free and each one after that is a paid dimension of the cost key, so a quote that omits this UNDER-REPORTS a reference-heavy job. Ignored by every other model — including minimax-h3-max, which has no reference endpoint (its start/end frames are free and are not reference images).`),
|
|
298
299
|
}),
|
|
299
300
|
async run(input, ctx) {
|
|
300
301
|
const registry = await ctx.cloud().get('/api/agent/models');
|
|
@@ -1813,6 +1814,33 @@ function resolveVideoModel(raw) {
|
|
|
1813
1814
|
}
|
|
1814
1815
|
return null;
|
|
1815
1816
|
}
|
|
1817
|
+
/**
|
|
1818
|
+
* Pair each reference-audio asset id with the words spoken in it.
|
|
1819
|
+
*
|
|
1820
|
+
* 🚨 THE MODEL RE-TRANSCRIBES A SUPPLIED TAKE. Seedance does not consume
|
|
1821
|
+
* reference audio verbatim — it re-synthesises something close to it, and a
|
|
1822
|
+
* 2026-08-28 field test heard "an app called Slates" come back as "a map called
|
|
1823
|
+
* Slates". The clip carries the voice, the accent and the timing; only text
|
|
1824
|
+
* carries the words. This is the text, and without it every generation with a
|
|
1825
|
+
* voice take has its line guessed.
|
|
1826
|
+
*
|
|
1827
|
+
* Positional in, KEYED out: the desktop route merges its deprecated singular
|
|
1828
|
+
* `audioReferenceAssetId` onto the END of the plural list, so an index would
|
|
1829
|
+
* address different clips depending on which shape the caller used. Blank
|
|
1830
|
+
* entries are dropped rather than sent as empty strings — a clip with no
|
|
1831
|
+
* speech has no line, which is not the same as a line that is empty.
|
|
1832
|
+
*/
|
|
1833
|
+
function spokenTextByAssetId(assetIds, spoken) {
|
|
1834
|
+
if (!assetIds?.length || !spoken?.length)
|
|
1835
|
+
return undefined;
|
|
1836
|
+
const out = {};
|
|
1837
|
+
assetIds.forEach((id, i) => {
|
|
1838
|
+
const text = spoken[i]?.trim();
|
|
1839
|
+
if (id && text)
|
|
1840
|
+
out[id] = text;
|
|
1841
|
+
});
|
|
1842
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
1843
|
+
}
|
|
1816
1844
|
// Maps a video model id to its bundled prompting skill (frontmatter `name:`),
|
|
1817
1845
|
// so guidance text points at a skill that actually exists. Deriving the name
|
|
1818
1846
|
// via model.split('-')[0] produced 'slates-prompting-kling' / '...-veo', which
|
|
@@ -1848,7 +1876,7 @@ export const generateVideo = {
|
|
|
1848
1876
|
// "seedance-2.5 480p/720p" were both stated here AND there, and the two
|
|
1849
1877
|
// copies disagreed — and the second of those went stale on 2026-08-24 when
|
|
1850
1878
|
// 2.5 gained 1080p, which is exactly the failure mode generating it fixes.
|
|
1851
|
-
model: z.string().describe(`One of: ${VIDEO_MODELS.join(' | ')}. Pass the BASE id — duration and videoResolution are separate params (registry cost keys like "kling-v3-standard-8s" auto-resolve). Route per the slates-model-selection skill: Kling std = general-purpose DEFAULT, Seedance 2 = premium physics/effects/hero tier, seedance-2.5 = a SECOND SEAT beside it (longer takes, far more references, audio-only refs — but no 4K, and dearer than seedance-2 at every shared resolution, so stay on seedance-2 unless length or reference count is the point), Veo = native-synced-audio niche only, never the default, omni-flash = cheap tier with audio included (t2v, single-start-frame i2v, or reference images; no last frame, no video/audio refs), minimax-h3 = the AUTHORED-AUDIO seat (dialogue, scene sound and score directed as three separate layers in one pass, plus declared reference relationships; reference images past the fifth are a PAID key dimension — pass referenceImages when quoting), minimax-h3-max = the same model post-trained by fal for SPEED, capped at 768p,
|
|
1879
|
+
model: z.string().describe(`One of: ${VIDEO_MODELS.join(' | ')}. Pass the BASE id — duration and videoResolution are separate params (registry cost keys like "kling-v3-standard-8s" auto-resolve). Route per the slates-model-selection skill: Kling std = general-purpose DEFAULT, Seedance 2 = premium physics/effects/hero tier, seedance-2.5 = a SECOND SEAT beside it (longer takes, far more references, audio-only refs — but no 4K, and dearer than seedance-2 at every shared resolution, so stay on seedance-2 unless length or reference count is the point), Veo = native-synced-audio niche only, never the default, omni-flash = cheap tier with audio included (t2v, single-start-frame i2v, or reference images; no last frame, no video/audio refs), minimax-h3 = the AUTHORED-AUDIO seat (dialogue, scene sound and score directed as three separate layers in one pass, plus declared reference relationships; reference images past the fifth are a PAID key dimension — pass referenceImages when quoting), minimax-h3-max = the same model post-trained by fal for SPEED, capped at 768p, and DEARER than minimax-h3 at the tier they share — a deliberate pick, never a default and never the cheap H3; it still takes firstFrameAssetId/lastFrameAssetId, but has no reference endpoint, so the reference set is minimax-h3 only. All are VIDEO-only. Each model's legal aspect ratios, durations and resolutions are in those params' own descriptions — read them there, not from memory. For per-call cost, call slates_estimate_generation_cost.`),
|
|
1852
1880
|
projectId: z.string().uuid().optional().describe('Save into this Slates project. Strongly recommended — the desktop UI shows a progress card live and the asset appears when complete.'),
|
|
1853
1881
|
// 🚨 THESE THREE DESCRIPTIONS ARE GENERATED FROM `MODEL_CAPABILITIES`.
|
|
1854
1882
|
// Never hand-write a ratio, resolution or duration into them again — every
|
|
@@ -1877,6 +1905,7 @@ export const generateVideo = {
|
|
|
1877
1905
|
videoReferenceAssetIds: z.array(z.string()).optional().describe(`Reference VIDEOS (UUIDs or badge codes) read alongside the images and audio in the same generation — own-footage restyle, MOTION TRANSFER ("the character from image 1 performs the motion from video 1"), or dialogue conditioning. Cited in the prompt as "video 1", "video 2"… in the order given. ${multimodalRefModels().join(' / ')} only; ignored elsewhere. ${multimodalRefSummary('seedance-2')} ${multimodalRefSummary('seedance-2.5')} Billing switches to combined input+output seconds (the vref key) — pass videoReferenceSecondsEach so the quote is right. If any clip contains a human/AI character, pair with seedanceFace=true (the default Seedance route blocks people). Over the cap is REFUSED, never trimmed: a dropped clip would already have been priced in.`),
|
|
1878
1906
|
videoReferenceSecondsEach: z.array(z.number()).optional().describe('REQUIRED with videoReferenceAssetIds, same order and length: each reference clip\'s duration in seconds (from the asset listing). Feeds the vref cost key — the bill is Σceil(each) + output seconds. The server re-derives this by probing every uploaded clip, so an understated value just gets corrected upward.'),
|
|
1879
1907
|
audioReferenceAssetIds: z.array(z.string()).optional().describe(`Reference AUDIO clips (UUIDs or badge codes) read alongside the images and video — e.g. lip-sync a character to a line ("the character in image 1 speaks the dialogue from audio 1"). Cited as "audio 1", "audio 2"… in the order given. No billing surcharge (Seedance audio is included). ${multimodalRefSummary('seedance-2')} ${multimodalRefSummary('seedance-2.5')}`),
|
|
1908
|
+
audioReferenceSpokenText: z.array(z.string()).optional().describe('STRONGLY RECOMMENDED whenever a reference clip contains SPEECH. Same order and length as audioReferenceAssetIds; use "" for a clip with no words (music, ambience, room tone). The model RE-TRANSCRIBES a supplied take rather than using it verbatim — a field test heard "an app called Slates" come back as "a map called Slates" — so the audio decides the VOICE, the ACCENT and the TIMING while only text decides the WORDS. Give the exact line here and it is quoted into the prompt beside the citation. Omit it and the words are a guess. Pairs with the plural audioReferenceAssetIds; the deprecated singular audioReferenceAssetId carries no text.'),
|
|
1880
1909
|
sound: z.boolean().optional().describe('Kling Omni / Veo / Seedance: enable audio generation. Default true.'),
|
|
1881
1910
|
audioLanguage: z.enum(['EN', 'ZH', 'JA', 'KO', 'ES']).optional().describe('Kling Omni only — language for dialogue.'),
|
|
1882
1911
|
generateMusic: z.boolean().optional().describe('Kling Omni only — auto-generate background music.'),
|
|
@@ -2033,6 +2062,20 @@ export const generateVideo = {
|
|
|
2033
2062
|
message: `${input.model} takes at most ${maxTotal} reference files across all modalities (you passed ${refImages + refMedia}).`,
|
|
2034
2063
|
});
|
|
2035
2064
|
}
|
|
2065
|
+
// A misaligned spoken-text array would attach one clip's line to another
|
|
2066
|
+
// and send the model the wrong words with nothing on screen to say so.
|
|
2067
|
+
// Refuse rather than truncate: the whole point of the field is that the
|
|
2068
|
+
// WORDS are exact.
|
|
2069
|
+
if (input.audioReferenceSpokenText &&
|
|
2070
|
+
input.audioReferenceSpokenText.length !== (input.audioReferenceAssetIds?.length ?? 0)) {
|
|
2071
|
+
return ok({
|
|
2072
|
+
requires_clarification: true,
|
|
2073
|
+
missing: [],
|
|
2074
|
+
message: `audioReferenceSpokenText must be the same length as audioReferenceAssetIds ` +
|
|
2075
|
+
`(${input.audioReferenceSpokenText.length} vs ${input.audioReferenceAssetIds?.length ?? 0}) ` +
|
|
2076
|
+
`— it pairs by position. Use "" for a clip with no speech.`,
|
|
2077
|
+
});
|
|
2078
|
+
}
|
|
2036
2079
|
// fal: "Audio cannot be the only reference input; provide at least one
|
|
2037
2080
|
// reference image or video with it."
|
|
2038
2081
|
const audioRefs = (input.audioReferenceAssetIds?.length ?? 0) + (input.audioReferenceAssetId ? 1 : 0);
|
|
@@ -2269,6 +2312,13 @@ export const generateVideo = {
|
|
|
2269
2312
|
audioReferenceAssetId: input.audioReferenceAssetId,
|
|
2270
2313
|
videoReferenceAssetIds: input.videoReferenceAssetIds,
|
|
2271
2314
|
audioReferenceAssetIds: input.audioReferenceAssetIds,
|
|
2315
|
+
// The route keys this by ASSET ID, not by position, because its own
|
|
2316
|
+
// singular/plural merge appends to the END of the list — an index would
|
|
2317
|
+
// mean different clips depending on which shape the caller used. The op
|
|
2318
|
+
// takes the friendlier positional array and re-keys it here, AFTER
|
|
2319
|
+
// `rids()` has resolved badge codes, so the keys are the ids the route
|
|
2320
|
+
// will resolve. A blank entry is dropped rather than sent as "".
|
|
2321
|
+
audioReferenceSpokenText: spokenTextByAssetId(input.audioReferenceAssetIds, input.audioReferenceSpokenText),
|
|
2272
2322
|
sound: input.sound,
|
|
2273
2323
|
audioLanguage: input.audioLanguage,
|
|
2274
2324
|
generateMusic: input.generateMusic,
|
|
@@ -3472,6 +3522,33 @@ function resolveGuideTopic(topic) {
|
|
|
3472
3522
|
if (t === 'slates-character-turnaround' || t === 'character-turnaround') {
|
|
3473
3523
|
return 'slates-character-identity';
|
|
3474
3524
|
}
|
|
3525
|
+
// ⚠️ Previs aliases sit HIGH, before the model-prefix rules below. `dialogue`
|
|
3526
|
+
// already resolves to the Seed Audio guide, and `style`/`camera` words are a
|
|
3527
|
+
// hair away from the style-prompting and model blocks — anchoring these here
|
|
3528
|
+
// keeps a previs ask out of an audio guide.
|
|
3529
|
+
if (t === 'previs' ||
|
|
3530
|
+
t === 'pre-vis' ||
|
|
3531
|
+
t === 'previz' ||
|
|
3532
|
+
t === 'blocking' ||
|
|
3533
|
+
t === 'blockout' ||
|
|
3534
|
+
t === 'greybox' ||
|
|
3535
|
+
t === 'grey-box' ||
|
|
3536
|
+
t === 'graybox' ||
|
|
3537
|
+
t === 'blender') {
|
|
3538
|
+
return 'slates-previs-blocking';
|
|
3539
|
+
}
|
|
3540
|
+
if (t === 'camera' || t === 'camera-moves' || t === 'camera moves' || t === 'shot-list' || t === 'shot list') {
|
|
3541
|
+
return 'slates-camera-language';
|
|
3542
|
+
}
|
|
3543
|
+
if (t === 'blocking-to-prompt' || t === 'previs-prompt' || t === 'reference-video' || t === 'video-to-video' || t === 'v2v') {
|
|
3544
|
+
return 'slates-blocking-to-prompt';
|
|
3545
|
+
}
|
|
3546
|
+
if (t === 'dialogue-blocking' || t === 'dialogue blocking' || t === '180-rule' || t === 'eyelines' || t === 'seating') {
|
|
3547
|
+
return 'slates-dialogue-blocking';
|
|
3548
|
+
}
|
|
3549
|
+
if (t === 'restyle' || t === 're-style' || t === 'style-swap' || t === 'style swap' || t === 'style-variants') {
|
|
3550
|
+
return 'slates-restyle-from-blocking';
|
|
3551
|
+
}
|
|
3475
3552
|
if (t === 'model-selection' ||
|
|
3476
3553
|
t === 'model selection' ||
|
|
3477
3554
|
t === 'which-model' ||
|
|
@@ -3560,7 +3637,7 @@ export const getPromptingGuide = {
|
|
|
3560
3637
|
topic: z
|
|
3561
3638
|
.string()
|
|
3562
3639
|
.min(1)
|
|
3563
|
-
.describe('Guide name, model id, or style name. Guides: slates-model-selection (which model for which job — read before choosing any model), slates-cost-discipline, slates-content-policy, slates-style-prompting, slates-prompting-nano-banana-2, slates-prompting-veo-3, slates-prompting-kling-v3, slates-prompting-seedance, slates-prompting-seed-audio, slates-prompting-elevenlabs, slates-prompting-lip-sync, slates-prompting-motion-transfer, slates-prompting-flux-2-max, slates-prompting-seedream-5-lite, slates-edit-and-iterate, slates-vision-feedback-loop, slates-character-identity, slates-storyboard-from-script, slates-direct-response-ad, slates-one-prompt-film. Style names (photoreal, anime, painterly, 3d-render) resolve to slates-style-prompting.'),
|
|
3640
|
+
.describe('Guide name, model id, or style name. Guides: slates-model-selection (which model for which job — read before choosing any model), slates-cost-discipline, slates-content-policy, slates-style-prompting, slates-prompting-nano-banana-2, slates-prompting-veo-3, slates-prompting-kling-v3, slates-prompting-seedance, slates-prompting-seed-audio, slates-prompting-elevenlabs, slates-prompting-lip-sync, slates-prompting-motion-transfer, slates-prompting-flux-2-max, slates-prompting-seedream-5-lite, slates-edit-and-iterate, slates-vision-feedback-loop, slates-character-identity, slates-storyboard-from-script, slates-direct-response-ad, slates-one-prompt-film. Blender previs (3D-blocked camera control): slates-previs-blocking (start here), slates-camera-language, slates-blocking-to-prompt, slates-dialogue-blocking, slates-restyle-from-blocking. Style names (photoreal, anime, painterly, 3d-render) resolve to slates-style-prompting.'),
|
|
3564
3641
|
}),
|
|
3565
3642
|
async run(input) {
|
|
3566
3643
|
const resolved = resolveGuideTopic(input.topic);
|
|
@@ -3574,6 +3651,159 @@ export const getPromptingGuide = {
|
|
|
3574
3651
|
};
|
|
3575
3652
|
},
|
|
3576
3653
|
};
|
|
3654
|
+
// ── Blender previs ──────────────────────────────────────────────
|
|
3655
|
+
//
|
|
3656
|
+
// The only ops that talk to a third transport: a localhost socket into a
|
|
3657
|
+
// running Blender carrying the Slates add-on. They exist to produce ONE
|
|
3658
|
+
// artifact — a grey-box blocking clip whose asset id goes straight into
|
|
3659
|
+
// `slates_generate_video`'s `videoReferenceAssetIds`, so the model renders a
|
|
3660
|
+
// world around a camera path instead of inventing one.
|
|
3661
|
+
//
|
|
3662
|
+
// 🚨 There is deliberately no camera-move library here. Camera work is written
|
|
3663
|
+
// as `bpy` by the agent through `slates_blender_execute`, against the Blender
|
|
3664
|
+
// API reference the add-on ships. A fixed menu of moves would cap the workflow
|
|
3665
|
+
// at whatever we thought of; code execution plus real docs does not.
|
|
3666
|
+
// The setup sentence and the download URL live in `clients/blender.ts`, beside
|
|
3667
|
+
// the port range they belong to — one home, so a moved page is one edit.
|
|
3668
|
+
const BLENDER_UNAVAILABLE_HINT = `Blender previs needs the Slates Blender add-on running. ${BLENDER_SETUP_HINT}`;
|
|
3669
|
+
export const blenderStatus = {
|
|
3670
|
+
id: 'slates_blender_status',
|
|
3671
|
+
description: 'Check whether a Blender running the Slates add-on is reachable, and if so return its scene summary (timing, camera, collection tree). Call this FIRST in any previs workflow — every other Blender op fails with the same setup message when the bridge is down, and knowing the frame range and fps up front is what keeps the blocking and the prompt timings in agreement.',
|
|
3672
|
+
input: z.object({}),
|
|
3673
|
+
async run() {
|
|
3674
|
+
const client = new BlenderBridgeClient();
|
|
3675
|
+
if (!(await client.isReachable())) {
|
|
3676
|
+
return ok({ connected: false, hint: BLENDER_UNAVAILABLE_HINT });
|
|
3677
|
+
}
|
|
3678
|
+
return ok({ connected: true, scene: await client.call('result = _mod("scene").summary()') });
|
|
3679
|
+
},
|
|
3680
|
+
};
|
|
3681
|
+
export const blenderExecute = {
|
|
3682
|
+
id: 'slates_blender_execute',
|
|
3683
|
+
description: 'Run Python (`bpy`) inside the connected Blender and return whatever the code assigns to a dict named `result`. This is how blocking gets built: primitives, empties, constraints, camera rigs, keyframes, markers. Anything Blender can do, this can do. Assign a dict to `result` to get data back (e.g. `result = {"camera": cam.name}`); print() output comes back separately as stdout. On an exception you get the full traceback — read it, fix the code, retry. Before writing an unfamiliar call, look up its real signature with slates_blender_docs rather than guessing: the add-on ships the Blender 5.1 API reference precisely so you do not have to recall it.',
|
|
3684
|
+
input: z.object({
|
|
3685
|
+
code: z
|
|
3686
|
+
.string()
|
|
3687
|
+
.min(1)
|
|
3688
|
+
.describe('Python source. Assign a JSON-serialisable dict to `result` to return data.'),
|
|
3689
|
+
timeoutSeconds: z
|
|
3690
|
+
.number()
|
|
3691
|
+
.int()
|
|
3692
|
+
.min(5)
|
|
3693
|
+
.max(900)
|
|
3694
|
+
.optional()
|
|
3695
|
+
.describe('How long to wait for Blender (default 60). Raise it for heavy geometry.'),
|
|
3696
|
+
}),
|
|
3697
|
+
async run(input) {
|
|
3698
|
+
const client = new BlenderBridgeClient();
|
|
3699
|
+
const { result, stdout, stderr } = await client.execute(input.code, {
|
|
3700
|
+
timeoutMs: input.timeoutSeconds ? input.timeoutSeconds * 1000 : undefined,
|
|
3701
|
+
});
|
|
3702
|
+
return ok({ result, ...(stdout ? { stdout } : {}), ...(stderr ? { stderr } : {}) });
|
|
3703
|
+
},
|
|
3704
|
+
};
|
|
3705
|
+
export const blenderScene = {
|
|
3706
|
+
id: 'slates_blender_scene',
|
|
3707
|
+
description: 'Scene summary from the connected Blender: frame range, fps, duration, render resolution, the active camera with its keyframe times in both frames and seconds, and the full collection/object tree with transforms and constraints. Cheap — call it freely between edits. The camera keyframe times ARE the cut structure, so read them before writing any shot-by-shot prompt.',
|
|
3708
|
+
input: z.object({}),
|
|
3709
|
+
async run() {
|
|
3710
|
+
return ok(await new BlenderBridgeClient().call('result = _mod("scene").summary()'));
|
|
3711
|
+
},
|
|
3712
|
+
};
|
|
3713
|
+
export const blenderDocs = {
|
|
3714
|
+
id: 'slates_blender_docs',
|
|
3715
|
+
description: 'Look up a dotted Blender Python API identifier in the bundled 5.1 reference — e.g. "bpy.ops.object", "bpy.types.Camera", "bpy.types.FollowPathConstraint". Pass "*" for top-level modules or "bpy.ops.*" to list a namespace. Use this instead of recalling a signature from memory: invented operator names and wrong enum values are the most common way previs code fails, and they fail silently often enough to be worth the lookup.',
|
|
3716
|
+
input: z.object({
|
|
3717
|
+
identifier: z.string().min(1).describe('Dotted identifier, or a namespace wildcard like "bpy.ops.*".'),
|
|
3718
|
+
}),
|
|
3719
|
+
async run(input) {
|
|
3720
|
+
const code = `result = _mod("docs").lookup(${JSON.stringify(input.identifier)})`;
|
|
3721
|
+
return ok(await new BlenderBridgeClient().call(code));
|
|
3722
|
+
},
|
|
3723
|
+
};
|
|
3724
|
+
export const blenderSearchDocs = {
|
|
3725
|
+
id: 'slates_blender_search_docs',
|
|
3726
|
+
description: 'Full-text search of the bundled Blender documentation for when you do not know the identifier yet. scope "api" searches the Python reference; "manual" searches the user manual for concepts and workflow ("how does Follow Path work", "bezier interpolation handles"). Use slates_blender_docs when you know the name and this when you do not.',
|
|
3727
|
+
input: z.object({
|
|
3728
|
+
query: z.string().min(2),
|
|
3729
|
+
scope: z.enum(['api', 'manual']).optional().describe('Default "api".'),
|
|
3730
|
+
maxResults: z.number().int().min(1).max(20).optional().describe('Default 8.'),
|
|
3731
|
+
}),
|
|
3732
|
+
async run(input) {
|
|
3733
|
+
const args = [
|
|
3734
|
+
JSON.stringify(input.query),
|
|
3735
|
+
JSON.stringify(input.scope ?? 'api'),
|
|
3736
|
+
String(input.maxResults ?? 8),
|
|
3737
|
+
].join(', ');
|
|
3738
|
+
return ok(await new BlenderBridgeClient().call(`result = _mod("docs").search(${args})`));
|
|
3739
|
+
},
|
|
3740
|
+
};
|
|
3741
|
+
export const blenderRenderBlocking = {
|
|
3742
|
+
id: 'slates_blender_render_blocking',
|
|
3743
|
+
description: "Render the connected Blender scene camera to a grey-box mp4 — the blocking clip that locks camera motion for generation — and, when projectId is given, import it into that Slates project as a video asset in the same call. The returned asset id goes into slates_generate_video's videoReferenceAssetIds and the returned durationSeconds into videoReferenceSecondsEach. Renders through the SCENE camera using scene render settings, never the user's viewport, so output does not depend on where they left their mouse. Untextured is correct: the clip supplies camera path and timing, the references supply the look. Colour in the blocking is NOTATION, not look — a distinct viewport colour per character is what binds a proxy to its reference image across cuts, and a marked face encodes which way a featureless proxy is facing. State every such mapping in the generation prompt AND state that the colours themselves are not inherited, or the model renders a literally red person. See the slates-previs-blocking and slates-blocking-to-prompt skills. Keep it at or under 30s — seedance-2.5 accepts reference videos up to 30s, the other reference-video models up to 15s.",
|
|
3744
|
+
input: z.object({
|
|
3745
|
+
projectId: z
|
|
3746
|
+
.string()
|
|
3747
|
+
.uuid()
|
|
3748
|
+
.optional()
|
|
3749
|
+
.describe('Import the clip into this project and return its asset. Omit to just get a file path.'),
|
|
3750
|
+
resolutionX: z.number().int().min(256).max(4096).optional().describe('Default 1920.'),
|
|
3751
|
+
resolutionY: z.number().int().min(256).max(4096).optional().describe('Default 1080.'),
|
|
3752
|
+
fps: z.number().int().min(1).max(120).optional().describe('Default 24. Match what the shot list assumes.'),
|
|
3753
|
+
frameStart: z.number().int().optional().describe("Default: the scene's own frame_start."),
|
|
3754
|
+
frameEnd: z.number().int().optional().describe("Default: the scene's own frame_end."),
|
|
3755
|
+
basename: z.string().min(1).max(64).optional().describe('Filename stem. Default "blocking".'),
|
|
3756
|
+
}),
|
|
3757
|
+
async run(input, ctx) {
|
|
3758
|
+
const kwargs = [];
|
|
3759
|
+
if (input.resolutionX !== undefined)
|
|
3760
|
+
kwargs.push(`resolution_x=${input.resolutionX}`);
|
|
3761
|
+
if (input.resolutionY !== undefined)
|
|
3762
|
+
kwargs.push(`resolution_y=${input.resolutionY}`);
|
|
3763
|
+
if (input.fps !== undefined)
|
|
3764
|
+
kwargs.push(`fps=${input.fps}`);
|
|
3765
|
+
if (input.frameStart !== undefined)
|
|
3766
|
+
kwargs.push(`frame_start=${input.frameStart}`);
|
|
3767
|
+
if (input.frameEnd !== undefined)
|
|
3768
|
+
kwargs.push(`frame_end=${input.frameEnd}`);
|
|
3769
|
+
if (input.basename !== undefined)
|
|
3770
|
+
kwargs.push(`basename=${JSON.stringify(input.basename)}`);
|
|
3771
|
+
// 🚨 THE RENDER IS DEFERRED, AND THAT IS WHY THIS IS NOT ONE LINE.
|
|
3772
|
+
// In an interactive Blender `render_blocking` INVOKES the render rather
|
|
3773
|
+
// than executing it — a synchronous animation render driven from the
|
|
3774
|
+
// bridge's own `bpy.app.timers` callback would re-enter the main loop
|
|
3775
|
+
// running it. So it hands back a `check_is_finished` callable instead of
|
|
3776
|
+
// the clip, and assigning that to `check_is_finished` is the bridge's
|
|
3777
|
+
// documented convention for "hold the socket open and answer when the job
|
|
3778
|
+
// lands" (see the add-on's `bridge/deferred.py`). The deferred path wraps
|
|
3779
|
+
// the eventual dict in the SAME `{status, result}` envelope, so everything
|
|
3780
|
+
// downstream of this call is identical either way. Headless Blender has no
|
|
3781
|
+
// job system to poll, renders synchronously, and takes the `else`.
|
|
3782
|
+
const render = (await new BlenderBridgeClient().call(`_previs_result = _mod("previs").render_blocking(${kwargs.join(', ')})\n` +
|
|
3783
|
+
'if callable(_previs_result):\n' +
|
|
3784
|
+
' check_is_finished = _previs_result\n' +
|
|
3785
|
+
'else:\n' +
|
|
3786
|
+
' result = _previs_result\n', { timeoutMs: RENDER_TIMEOUT_MS }));
|
|
3787
|
+
if (!input.projectId) {
|
|
3788
|
+
return ok({
|
|
3789
|
+
...render,
|
|
3790
|
+
next: 'Pass projectId to import this into a Slates project, or call slates_upload_reference_image with type "video".',
|
|
3791
|
+
});
|
|
3792
|
+
}
|
|
3793
|
+
// The same desktop route slates_upload_reference_image uses — the file is
|
|
3794
|
+
// probed on ingest, so duration and dimensions are known immediately.
|
|
3795
|
+
const uploaded = await ctx.desktop().post('/agent/assets/upload', {
|
|
3796
|
+
projectId: input.projectId,
|
|
3797
|
+
filePath: render.filePath,
|
|
3798
|
+
type: 'video',
|
|
3799
|
+
});
|
|
3800
|
+
return ok({
|
|
3801
|
+
...render,
|
|
3802
|
+
asset: uploaded.asset ?? uploaded,
|
|
3803
|
+
next: "Pass the asset's id in slates_generate_video videoReferenceAssetIds, with videoReferenceSecondsEach set to durationSeconds.",
|
|
3804
|
+
});
|
|
3805
|
+
},
|
|
3806
|
+
};
|
|
3577
3807
|
// ── Aggregation ─────────────────────────────────────────────────
|
|
3578
3808
|
export const ALL_OPERATIONS = [
|
|
3579
3809
|
getWorkspaceState,
|
|
@@ -3652,5 +3882,18 @@ export const ALL_OPERATIONS = [
|
|
|
3652
3882
|
batchUpdateFrames,
|
|
3653
3883
|
deleteFrame,
|
|
3654
3884
|
getPromptingGuide,
|
|
3885
|
+
// ── Blender previs, LAST and deliberately ────────────────────────────
|
|
3886
|
+
// This order is not cosmetic: `slate/src/main/studio-agent/ops.ts` maps this
|
|
3887
|
+
// array straight into the Anthropic `tools` array, and that block sits inside
|
|
3888
|
+
// the desktop Studio Agent's PROMPT-CACHED PREFIX. These six landed at the
|
|
3889
|
+
// TOP, which put a third transport nobody without Blender can reach ahead of
|
|
3890
|
+
// `slates_get_workspace_state` in every conversation the app has. They are a
|
|
3891
|
+
// niche lane off the end of the surface, and the list should read that way.
|
|
3892
|
+
blenderStatus,
|
|
3893
|
+
blenderExecute,
|
|
3894
|
+
blenderScene,
|
|
3895
|
+
blenderDocs,
|
|
3896
|
+
blenderSearchDocs,
|
|
3897
|
+
blenderRenderBlocking,
|
|
3655
3898
|
];
|
|
3656
3899
|
//# sourceMappingURL=index.js.map
|
|
@@ -235,7 +235,7 @@ export const MODEL_FACTS = [
|
|
|
235
235
|
// No reference caps: fal publishes no reference-to-video endpoint for this
|
|
236
236
|
// row, so `caps()` returns nulls and the composer refuses references.
|
|
237
237
|
...caps('minimax-h3-max'),
|
|
238
|
-
notes: 'THE SPEED SEAT, and the EXPENSIVE one at the tier they share — never the cheap H3 and never the default. fal\'s own post-train of the open H3 weights, self-hosted. 🚨 MEASURED 2026-08-27, same prompt and params on both rows: a 5s 768p text-to-video took **4.8 seconds** on Max against **57 seconds** on base H3 — **about 12x faster**, queue to finished file. That is the seat\'s whole case and it is now our own number, not fal\'s (fal claims under 3s; the literal claim did not hold at 4.8s wall-clock, the order of magnitude did). It also carries a thin quality edge on the with-audio Arena boards (1,204 vs 1,184 image-to-video, 1,235 vs 1,226 text-to-video — real, but 20 and 9 ELO, and vendor-reported). It gives up everything above 768p (no 2K, no 4K — the upscaler is not in the open weights)
|
|
238
|
+
notes: 'THE SPEED SEAT, and the EXPENSIVE one at the tier they share — never the cheap H3 and never the default. fal\'s own post-train of the open H3 weights, self-hosted. 🚨 MEASURED 2026-08-27, same prompt and params on both rows: a 5s 768p text-to-video took **4.8 seconds** on Max against **57 seconds** on base H3 — **about 12x faster**, queue to finished file. That is the seat\'s whole case and it is now our own number, not fal\'s (fal claims under 3s; the literal claim did not hold at 4.8s wall-clock, the order of magnitude did). It also carries a thin quality edge on the with-audio Arena boards (1,204 vs 1,184 image-to-video, 1,235 vs 1,226 text-to-video — real, but 20 and 9 ELO, and vendor-reported). It gives up everything above 768p (no 2K, no 4K — the upscaler is not in the open weights). 🚨 FRAMES ARE UNAFFECTED — it takes a start frame and an end frame exactly like base H3, on `minimax/h3-max/image-to-video`, which is the route the image-to-video Arena score above is measured on. What it lacks is the REFERENCE endpoint (`minimax/h3-max/reference-to-video` 404s), so the omni-reference set — up to 9 identity/style/environment images plus reference video and audio — is base-H3 only. Never describe this row as taking no image input: an image-to-video shot is one of the two things it is FOR. It costs $0.080/s at 768p against base H3\'s $0.060/s: 33% more for a shorter ladder. So route here when a fast turnaround on a 480p/768p text-to-video or start-frame shot is worth the premium, and to base H3 for resolution, references, or the same tier cheaper. Same native audio, same 5-15s window, same six aspect ratios.',
|
|
239
239
|
},
|
|
240
240
|
{
|
|
241
241
|
id: 'seed-audio',
|
|
@@ -583,7 +583,7 @@ const MINIMAX_H3 = {
|
|
|
583
583
|
label: 'MiniMax H3',
|
|
584
584
|
intro: [
|
|
585
585
|
'MiniMax H3 generates picture and sound in one pass — 24fps, 32kHz stereo, 5-15 seconds, 11 stably-supported languages. It is the only video model in Slates where audio is AUTHORED rather than switched on: synchronised dialogue and action sounds go in the body of the prompt, ambience goes in a soundscape section, and audience-only music goes in a score section. Put a sound in the wrong section and it is dropped, doubled, or attributed to the wrong source.',
|
|
586
|
-
'Two seats. Base H3 runs 480p / 768p / 2K / 4K and reads up to 9 reference images plus 3 video and 3 audio clips. H3 Max is fal\'s faster post-train: 768p ceiling,
|
|
586
|
+
'Two seats. Base H3 runs 480p / 768p / 2K / 4K and reads up to 9 reference images plus 3 video and 3 audio clips. H3 Max is fal\'s faster post-train: 768p ceiling, and dearer than base H3 at 768p — a deliberate speed pick, never the cheap one. It still animates a start frame and an end frame; what it does not have is the reference set (the extra identity, style, environment, video and audio references), which is base-H3 only. 768p is the default on both because it is the tier the model natively generates; 2K and 4K are upscales of a 768p base.',
|
|
587
587
|
],
|
|
588
588
|
columns: [
|
|
589
589
|
[
|