@slatesvideo/shared 0.6.11 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/dist/auth.js +2 -2
  2. package/dist/clients/cloud.js +1 -1
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.js +1 -1
  5. package/dist/manual/content.d.ts +1 -1
  6. package/dist/manual/content.js +1 -1
  7. package/dist/operations/index.d.ts +817 -16
  8. package/dist/operations/index.js +1410 -360
  9. package/dist/operations/surface.d.ts +3 -1
  10. package/dist/operations/surface.js +37 -10
  11. package/dist/prompts/ad-presets.d.ts +77 -0
  12. package/dist/prompts/ad-presets.js +43 -0
  13. package/dist/prompts/agent-doctrine.js +5 -4
  14. package/dist/prompts/banned-tokens.d.ts +4 -29
  15. package/dist/prompts/banned-tokens.js +29 -204
  16. package/dist/prompts/craft-cards.js +2 -2
  17. package/dist/prompts/generation-policy.d.ts +41 -0
  18. package/dist/prompts/generation-policy.js +53 -0
  19. package/dist/prompts/guide-retrieval.d.ts +9 -0
  20. package/dist/prompts/guide-retrieval.js +53 -0
  21. package/dist/prompts/index.d.ts +1 -0
  22. package/dist/prompts/index.js +1 -0
  23. package/dist/prompts/model-capabilities.d.ts +18 -1
  24. package/dist/prompts/model-capabilities.js +72 -19
  25. package/dist/prompts/model-facts.d.ts +34 -2
  26. package/dist/prompts/model-facts.js +66 -5
  27. package/dist/prompts/partials.generated.js +8 -2
  28. package/dist/prompts/prompting-tips.d.ts +1 -1
  29. package/dist/prompts/prompting-tips.js +61 -16
  30. package/dist/prompts/reference-composer.d.ts +2 -0
  31. package/dist/prompts/reference-composer.js +51 -50
  32. package/dist/prompts/script-document.d.ts +165 -0
  33. package/dist/prompts/script-document.js +11 -0
  34. package/dist/prompts/shot-grammar.d.ts +4 -4
  35. package/dist/prompts/shot-grammar.js +3 -3
  36. package/dist/prompts/shot-spec.d.ts +13 -0
  37. package/dist/prompts/shot-spec.js +23 -5
  38. package/dist/skills/content.js +26 -23
  39. package/exports/slates-chatgpt-images/generated/SKILL.md +107 -0
  40. package/exports/slates-chatgpt-images/generated/slates-chatgpt-images.skill +0 -0
  41. package/exports/slates-prompt-builder/generated/SKILL.md +1 -1
  42. package/exports/slates-prompt-builder/generated/reference-character.md +9 -1
  43. package/exports/slates-prompt-builder/generated/reference-kling.md +3 -3
  44. package/exports/slates-prompt-builder/generated/reference-nano-banana.md +22 -10
  45. package/exports/slates-prompt-builder/generated/reference-seedance.md +4 -4
  46. package/exports/slates-prompt-builder/generated/slates-prompt-builder-manifest.json +17 -17
  47. package/exports/slates-prompt-builder/generated/slates-prompt-builder.skill +0 -0
  48. package/package.json +10 -4
  49. package/skills/_partials/cinematic-card.md +8 -0
  50. package/skills/_partials/cinematic-routes-short.md +2 -0
  51. package/skills/_partials/cinematic-tips-short.md +2 -0
  52. package/skills/_partials/decision-log.md +1 -13
  53. package/skills/_partials/image-defaults.md +11 -0
  54. package/skills/_partials/lens-video-split.md +1 -0
  55. package/skills/_partials/reference-rules-core.md +1 -1
  56. package/skills/_partials/sheet-tool-defaults.md +6 -0
  57. package/skills/slates-character-identity.md +9 -1
  58. package/skills/slates-chatgpt-images.md +107 -0
  59. package/skills/slates-cinematic-look.md +237 -0
  60. package/skills/slates-cost-discipline.md +18 -12
  61. package/skills/slates-direct-response-ad.md +13 -53
  62. package/skills/slates-edit-and-iterate.md +1 -1
  63. package/skills/slates-model-selection.md +20 -14
  64. package/skills/slates-one-prompt-film.md +19 -77
  65. package/skills/slates-project-organization.md +7 -3
  66. package/skills/slates-prompting-flux-2-max.md +15 -4
  67. package/skills/slates-prompting-gpt-image-2-5.md +41 -28
  68. package/skills/slates-prompting-kling-v3.md +3 -3
  69. package/skills/slates-prompting-lip-sync.md +1 -1
  70. package/skills/slates-prompting-minimax-h3.md +30 -17
  71. package/skills/slates-prompting-motion-transfer.md +1 -1
  72. package/skills/slates-prompting-nano-banana-2.md +24 -11
  73. package/skills/slates-prompting-seedance-2-5.md +7 -6
  74. package/skills/slates-prompting-seedance.md +5 -5
  75. package/skills/slates-prompting-seedream-5-lite.md +14 -3
  76. package/skills/slates-prompting-veo-3.md +1 -1
  77. package/skills/slates-script-craft.md +45 -0
  78. package/skills/slates-shot-variety.md +11 -40
  79. package/skills/slates-storyboard-from-script.md +14 -66
  80. package/skills/slates-style-prompting.md +54 -54
  81. package/skills/slates-ugc-influencer-ad.md +32 -309
  82. package/skills/slates-vision-feedback-loop.md +2 -1
@@ -0,0 +1,41 @@
1
+ /** Product fan-out policy: one provider request and debit per output.
2
+ * This is NOT a model capability or provider batch limit. Desktop mirror is
3
+ * generated by slate/scripts/sync-generation-policy.mjs; never edit it there. */
4
+ export declare const IMAGE_QUANTITIES: readonly [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
5
+ export declare const MAX_IMAGE_VARIATIONS: 1 | 2 | 4 | 5 | 3 | 10 | 8 | 7 | 6 | 9;
6
+ export type ImageQuantity = typeof IMAGE_QUANTITIES[number];
7
+ export declare const clampImageQuantity: (count?: number) => ImageQuantity;
8
+ /** Default saved-recipe framing, shared by composer restore, quote and dispatch. */
9
+ export declare const DEFAULT_SHOT_ASPECT_RATIO: "16:9";
10
+ /**
11
+ * The built-in tools, and the seat each one renders on when its caller names no
12
+ * model. THE ONE HOME, for both repos: the desktop reads it through its generated
13
+ * mirror (`toolModel` in slate/src/shared/pricing.ts), this package reads it for
14
+ * the op descriptions and the generated skill partial.
15
+ *
16
+ * `'default-image'` means "follow the app's default image model"
17
+ * (`defaultModelFor('image')` in model-facts.ts), so promoting a better image
18
+ * model moves these tools with it and nothing else is edited. A model id pins a
19
+ * tool instead, for a reason recorded beside it. This file stays a
20
+ * dependency-free leaf (it is copied verbatim into the desktop), which is why
21
+ * the seat is a token each consumer resolves rather than a call made here.
22
+ *
23
+ * Before 2026-09-21 the sheet tools' model was typed at every use on the desktop
24
+ * and twice more here (an op description and a skill), so three screens quoted
25
+ * a sheet from one typed id while the handler billed from another.
26
+ */
27
+ export declare const BUILT_IN_TOOLS: readonly ["character-sheet", "environment-plate", "grid-extract", "image-edit"];
28
+ export type BuiltInTool = typeof BUILT_IN_TOOLS[number];
29
+ export declare const DEFAULT_IMAGE_SEAT: "default-image";
30
+ export declare const TOOL_SEAT: Record<BuiltInTool, typeof DEFAULT_IMAGE_SEAT | string>;
31
+ /** Both sheet tools frame one wide image. On GPT Image the aspect is part of the price. */
32
+ export declare const SHEET_TOOL_ASPECT_RATIO: "16:9";
33
+ /**
34
+ * The GPT Image tier a sheet tool pins. It read 'medium' until 2026-09-09: GPT
35
+ * Image 2.5 renamed the ladder, so the tier GPT Image 2 called `medium` is now
36
+ * `high`, and a stored 'medium' would have kept compiling while naming a
37
+ * materially cheaper tier. Sheets are the identity lane (every downstream shot
38
+ * inherits this frame's likeness), so this is deliberately not the draft tier.
39
+ */
40
+ export declare const SHEET_TOOL_GPT_QUALITY: "high";
41
+ //# sourceMappingURL=generation-policy.d.ts.map
@@ -0,0 +1,53 @@
1
+ /** Product fan-out policy: one provider request and debit per output.
2
+ * This is NOT a model capability or provider batch limit. Desktop mirror is
3
+ * generated by slate/scripts/sync-generation-policy.mjs; never edit it there. */
4
+ export const IMAGE_QUANTITIES = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
5
+ export const MAX_IMAGE_VARIATIONS = IMAGE_QUANTITIES[IMAGE_QUANTITIES.length - 1];
6
+ export const clampImageQuantity = (count = 1) => Math.max(1, Math.min(MAX_IMAGE_VARIATIONS, Number.isFinite(count) ? Math.floor(count) : 1));
7
+ /** Default saved-recipe framing, shared by composer restore, quote and dispatch. */
8
+ export const DEFAULT_SHOT_ASPECT_RATIO = '16:9';
9
+ /**
10
+ * The built-in tools, and the seat each one renders on when its caller names no
11
+ * model. THE ONE HOME, for both repos: the desktop reads it through its generated
12
+ * mirror (`toolModel` in slate/src/shared/pricing.ts), this package reads it for
13
+ * the op descriptions and the generated skill partial.
14
+ *
15
+ * `'default-image'` means "follow the app's default image model"
16
+ * (`defaultModelFor('image')` in model-facts.ts), so promoting a better image
17
+ * model moves these tools with it and nothing else is edited. A model id pins a
18
+ * tool instead, for a reason recorded beside it. This file stays a
19
+ * dependency-free leaf (it is copied verbatim into the desktop), which is why
20
+ * the seat is a token each consumer resolves rather than a call made here.
21
+ *
22
+ * Before 2026-09-21 the sheet tools' model was typed at every use on the desktop
23
+ * and twice more here (an op description and a skill), so three screens quoted
24
+ * a sheet from one typed id while the handler billed from another.
25
+ */
26
+ export const BUILT_IN_TOOLS = ['character-sheet', 'environment-plate', 'grid-extract', 'image-edit'];
27
+ export const DEFAULT_IMAGE_SEAT = 'default-image';
28
+ export const TOOL_SEAT = {
29
+ 'character-sheet': DEFAULT_IMAGE_SEAT,
30
+ 'environment-plate': DEFAULT_IMAGE_SEAT,
31
+ // Pinned: the image viewer offers grid extraction a fixed short list of seats,
32
+ // and the cell-extraction prompt has a variant per seat family. Moving it is a
33
+ // picker and a prompt change first.
34
+ 'grid-extract': 'nano-banana-2',
35
+ // Follows the default image model (2026-09-29). The viewer's Edit box lists
36
+ // every image model from the registry, and the edit handler sends references
37
+ // to each one up to its own cap, so nothing ties this tool to one family. Until
38
+ // then it was pinned to Nano Banana 2 while the app's default was GPT Image 2.5
39
+ // Sunburst, so Edit and the prompt bar opened on different models for the
40
+ // same picture.
41
+ 'image-edit': DEFAULT_IMAGE_SEAT,
42
+ };
43
+ /** Both sheet tools frame one wide image. On GPT Image the aspect is part of the price. */
44
+ export const SHEET_TOOL_ASPECT_RATIO = '16:9';
45
+ /**
46
+ * The GPT Image tier a sheet tool pins. It read 'medium' until 2026-09-09: GPT
47
+ * Image 2.5 renamed the ladder, so the tier GPT Image 2 called `medium` is now
48
+ * `high`, and a stored 'medium' would have kept compiling while naming a
49
+ * materially cheaper tier. Sheets are the identity lane (every downstream shot
50
+ * inherits this frame's likeness), so this is deliberately not the draft tier.
51
+ */
52
+ export const SHEET_TOOL_GPT_QUALITY = 'high';
53
+ //# sourceMappingURL=generation-policy.js.map
@@ -0,0 +1,9 @@
1
+ export type GuideDepth = 'card' | 'index' | 'section' | 'full';
2
+ /** Parse headings outside code fences; maintainer comments never reach agents. */
3
+ export declare function guideSections(content: string): Array<{
4
+ title: string;
5
+ body: string;
6
+ }>;
7
+ /** Bounded retrieval. A missing card never silently expands to the full guide. */
8
+ export declare function retrieveGuide(skill: string, content: string, depth: GuideDepth, query?: string): string;
9
+ //# sourceMappingURL=guide-retrieval.d.ts.map
@@ -0,0 +1,53 @@
1
+ import { craftCard } from './craft-cards.js';
2
+ /** Parse headings outside code fences; maintainer comments never reach agents. */
3
+ export function guideSections(content) {
4
+ const clean = content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, '').replace(/<!--[\s\S]*?-->/g, '').trim();
5
+ const sections = [];
6
+ let current = { title: 'Overview', body: '' };
7
+ let fenced = false;
8
+ for (const line of clean.split(/\r?\n/)) {
9
+ if (/^\s*```/.test(line))
10
+ fenced = !fenced;
11
+ if (!fenced && /^#{1,3} /.test(line)) {
12
+ if (current.body.trim())
13
+ sections.push(current);
14
+ current = { title: line.replace(/^#+ /, ''), body: line };
15
+ }
16
+ else
17
+ current.body += '\n' + line;
18
+ }
19
+ if (current.body.trim())
20
+ sections.push(current);
21
+ return sections;
22
+ }
23
+ /** Bounded retrieval. A missing card never silently expands to the full guide. */
24
+ export function retrieveGuide(skill, content, depth, query) {
25
+ const sections = guideSections(content);
26
+ const index = sections.map((s) => `- ${s.title}`).join('\n');
27
+ if (depth === 'full')
28
+ return sections.map((s) => s.body.trim()).join('\n\n');
29
+ if (depth === 'index')
30
+ return index;
31
+ if (query?.trim()) {
32
+ const q = query.trim().toLowerCase();
33
+ // Exact technique IDs return one complete row with its source heading.
34
+ for (const s of sections) {
35
+ const row = s.body.split('\n').find((line) => line.toLowerCase().startsWith(`| \`${q}\` |`));
36
+ if (row)
37
+ return `${s.title}\n\n| Technique | Evidence | What it does | Reach for · skip | Say |\n|---|---|---|---|---|\n${row}`;
38
+ }
39
+ const words = q.split(/[^\p{L}\p{N}-]+/u).filter(Boolean);
40
+ const ranked = sections.map((s) => ({ s, score: words.reduce((n, w) => n + (s.title.toLowerCase().includes(w) ? 4 : s.body.toLowerCase().includes(w) ? 1 : 0), 0) }))
41
+ .filter((x) => x.score > 0).sort((a, b) => b.score - a.score);
42
+ if (!ranked.length)
43
+ return `No section matches "${query}". Available sections:\n${index}`;
44
+ const body = ranked[0].s.body.trim();
45
+ if (body.length <= 6000)
46
+ return body;
47
+ // Never cut a table row or a worked prompt mid-sentence.
48
+ return `Section "${ranked[0].s.title}" is too large for a selective response. Use a technique ID or depth "full".\n${index}`;
49
+ }
50
+ const card = craftCard(skill);
51
+ return `${card ?? sections[0]?.body.trim().slice(0, 1600) ?? 'No overview available.'}\n\nSections (request with query, or depth "full"):\n${index}`;
52
+ }
53
+ //# sourceMappingURL=guide-retrieval.js.map
@@ -8,4 +8,5 @@ export * from './character-sheet.js';
8
8
  export * from './environment-sheet.js';
9
9
  export * from './prompting-tips.js';
10
10
  export * from './asset-label.js';
11
+ export * from './generation-policy.js';
11
12
  //# sourceMappingURL=index.d.ts.map
@@ -28,4 +28,5 @@ export * from './prompting-tips.js';
28
28
  // gallery. Also its own leaf subpath (`@slatesvideo/shared/asset-label`) for the
29
29
  // renderer, which cannot import the root barrel.
30
30
  export * from './asset-label.js';
31
+ export * from './generation-policy.js';
31
32
  //# sourceMappingURL=index.js.map
@@ -47,6 +47,14 @@ export interface VideoResolutionCapability {
47
47
  fixed?: VideoResolution;
48
48
  /** Default when this model is chosen (falls back to `options[0]`). */
49
49
  default?: VideoResolution;
50
+ /**
51
+ * Tiers the provider makes by UPSCALING a smaller native render, keyed to that
52
+ * render. They stay selectable; every surface that offers one says so and says
53
+ * we do not recommend it (Eric, 2026-09-30: "more expensive and they actually
54
+ * look worse"). A refinement pass the provider documents as its own stage, such
55
+ * as H3 Max's 1080p, is not an upscale and is not listed.
56
+ */
57
+ upscaledFrom?: Partial<Record<VideoResolution, VideoResolution>>;
50
58
  }
51
59
  /** Everything a model will ACCEPT. Capability only — never a price. */
52
60
  /**
@@ -86,6 +94,7 @@ export interface VoiceCloneCapability {
86
94
  }
87
95
  export declare const GPT_QUALITY_TIERS: readonly ["low", "medium", "high", "xhigh", "max"];
88
96
  export type GptQuality = (typeof GPT_QUALITY_TIERS)[number];
97
+ export declare const DEFAULT_GPT_QUALITY: GptQuality;
89
98
  export declare const GPT_BACKGROUNDS: readonly ["auto", "transparent", "opaque"];
90
99
  export type GptBackground = (typeof GPT_BACKGROUNDS)[number];
91
100
  export type ImageResolution = '1k' | '2k' | '3k' | '4k';
@@ -128,6 +137,8 @@ export declare function falImageSize(model: string, aspectRatio?: string, resolu
128
137
  };
129
138
  export interface ModelCapability {
130
139
  imageResolutions?: ImageResolution[];
140
+ /** Product default shared by estimates and generation. */
141
+ defaultImageResolution?: ImageResolution;
131
142
  /**
132
143
  * Images ONE request may ask the provider for in a single batch.
133
144
  *
@@ -204,6 +215,8 @@ export declare function voiceCloneFor(model: string): VoiceCloneCapability | und
204
215
  /** Aspect ratios a model accepts, honouring the provider override. */
205
216
  export declare function aspectRatiosFor(model: string, provider?: string): AspectRatio[];
206
217
  /** Video resolutions a model accepts. A FIXED model reports exactly its one value. */
218
+ /** The native render an upscaled tier is made from, or undefined for a native tier. */
219
+ export declare function upscaledFrom(model: string, resolution: string | undefined): VideoResolution | undefined;
207
220
  export declare function videoResolutionsFor(model: string): VideoResolution[];
208
221
  /** The resolution a model would actually run at. Fixed wins; else keep a legal
209
222
  * current value; else the model's own default. Mirrors `clampVideoResolution`. */
@@ -250,7 +263,9 @@ export declare function describeDurations(models: readonly string[]): string;
250
263
  export declare function describeReferenceImageCaps(models: readonly string[]): string;
251
264
  /** H3 Max reference accounting, fal's worked tables read 2026-09-09.
252
265
  * https://fal.ai/models/minimax/h3-max/reference-to-video
253
- * 1080p video-reference pricing is unpublished; never infer it from output rates.
266
+ * 1080p: fal's page, read 2026-09-29: "768p and 1080p output use the same
267
+ * reference-video token counts." So 1080p takes the 768p figure; it was absent
268
+ * until fal published that sentence, and must never be inferred from output rates.
254
269
  */
255
270
  export declare const MINIMAX_MAX_REFERENCE: {
256
271
  readonly freeTokens: 4096;
@@ -265,4 +280,6 @@ export declare function minimaxMaxReferenceTokens(input: {
265
280
  audioSeconds: number;
266
281
  resolution: string;
267
282
  }): number;
283
+ /** Quotes and generation use the same model default. */
284
+ export declare function defaultImageResolutionFor(model: string): ImageResolution;
268
285
  //# sourceMappingURL=model-capabilities.d.ts.map
@@ -74,9 +74,9 @@ const OMNI_FLASH_ASPECT_RATIOS = ['16:9', '9:16'];
74
74
  /** Seedance (both seats, and the edit row): six — notably NO `4:5`. */
75
75
  const SEEDANCE_ASPECT_RATIOS = ['21:9', '16:9', '4:3', '1:1', '3:4', '9:16'];
76
76
  /**
77
- * MiniMax H3, both seats: six. Read off fal's live OpenAPI 2026-08-27 for
78
- * `minimax/h3/text-to-video` and `minimax/h3-max/text-to-video` — identical
79
- * enums. It happens to be the same six Seedance takes; kept as its OWN constant
77
+ * MiniMax H3, all three seats: six. Read off fal's live OpenAPI 2026-08-27 for
78
+ * `minimax/h3/text-to-video` and `minimax/h3-max/text-to-video`, and 2026-09-29
79
+ * for `minimax/h3-max-turbo/text-to-video` — identical enums. It happens to be the same six Seedance takes; kept as its OWN constant
80
80
  * because a provider that adds a ratio adds it to ITS family, and sharing the
81
81
  * Seedance constant would silently move H3 the next time ByteDance moves.
82
82
  *
@@ -101,6 +101,7 @@ const MINIMAX_H3_ASPECT_RATIOS = ['21:9', '16:9', '4:3', '1:1', '3:4', '9:16'];
101
101
  */
102
102
  const LTX_2_5_ASPECT_RATIOS = ['16:9', '9:16'];
103
103
  export const GPT_QUALITY_TIERS = ['low', 'medium', 'high', 'xhigh', 'max'];
104
+ export const DEFAULT_GPT_QUALITY = 'high';
104
105
  export const GPT_BACKGROUNDS = ['auto', 'transparent', 'opaque'];
105
106
  // Product output sizes; schema bounds and metering receipt live in the GPT harvest.
106
107
  export const GPT_IMAGE_25_SIZES = {
@@ -221,6 +222,7 @@ export const AGENT_ROUTE_PROVIDER = 'fal';
221
222
  export const MODEL_CAPABILITIES = {
222
223
  // ── Image models ───────────────────────────────────────────────────────────
223
224
  'nano-banana-2': {
225
+ defaultImageResolution: '2k',
224
226
  imageResolutions: ['1k', '2k', '4k'],
225
227
  // fal's nano-banana-2 schema caps `num_images` at 4 (read 2026-09-09). This
226
228
  // is the ONLY model that batches: the MCP's headless path (no projectId) asks
@@ -231,11 +233,13 @@ export const MODEL_CAPABILITIES = {
231
233
  maxRefImages: 14,
232
234
  },
233
235
  'nano-banana-2-lite': {
236
+ defaultImageResolution: '1k',
234
237
  imageResolutions: ['1k'],
235
238
  aspectRatios: FULL_ASPECT_RATIOS,
236
239
  maxRefImages: 4, // fal edit endpoint caps input images at 4
237
240
  },
238
241
  'nano-banana-pro': {
242
+ defaultImageResolution: '2k',
239
243
  imageResolutions: ['1k', '2k', '4k'],
240
244
  aspectRatios: FULL_ASPECT_RATIOS,
241
245
  maxRefImages: 14,
@@ -276,30 +280,39 @@ export const MODEL_CAPABILITIES = {
276
280
  // limits either: the MCP's 4,000-character prompt against fal's 32,000, and
277
281
  // image quantity, which is a fan-out and has no provider ceiling at all.
278
282
  'gpt-image-2-5-flare': {
283
+ defaultImageResolution: '2k',
279
284
  imageResolutions: ['2k', '3k', '4k'],
280
285
  aspectRatios: ['1:1', '16:9', '9:16', '4:3', '3:4'],
281
286
  maxRefImages: 16,
282
287
  },
283
288
  'gpt-image-2-5-sunburst': {
289
+ defaultImageResolution: '3k',
284
290
  imageResolutions: ['2k', '3k', '4k'],
285
291
  aspectRatios: ['1:1', '16:9', '9:16', '4:3', '3:4'],
286
292
  maxRefImages: 16,
287
293
  },
288
294
  'flux-2-max': {
295
+ defaultImageResolution: '1k',
289
296
  imageResolutions: ['1k', '2k', '4k'],
290
297
  aspectRatios: FULL_ASPECT_RATIOS,
291
298
  maxRefImages: 4,
292
299
  },
293
300
  'seedream-5-lite': {
301
+ defaultImageResolution: '2k',
294
302
  imageResolutions: ['2k', '3k', '4k'],
295
303
  aspectRatios: FULL_ASPECT_RATIOS,
296
304
  maxRefImages: 10,
297
305
  },
298
306
  // ── Kling video ────────────────────────────────────────────────────────────
307
+ // Standard is 720p and Pro is 1080p; 4K is a separate endpoint shared by all
308
+ // four. Kling's own API: "std: ... The output video resolution is 720P.
309
+ // pro: ... The output video resolution is 1080P." The fal Standard endpoint
310
+ // takes no resolution parameter, and every Standard and Omni render we
311
+ // measured came back 1280x720 (2026-09-29). This row said 1080p until then.
299
312
  'kling-v3.0-std': {
300
313
  aspectRatios: KLING_DIRECT_ASPECT_RATIOS,
301
314
  providerAspectRatios: { fal: KLING_FAL_ASPECT_RATIOS },
302
- videoResolution: { options: ['1080p', '4k'] },
315
+ videoResolution: { options: ['720p', '4k'] },
303
316
  // 3, not 5. The op claimed "Kling: 5-15" and refused legal 3-4s takes.
304
317
  duration: { min: 3, max: 15, mode: 'continuous' },
305
318
  maxIngredientImages: 4,
@@ -314,7 +327,8 @@ export const MODEL_CAPABILITIES = {
314
327
  'kling-v3.0-omni': {
315
328
  aspectRatios: KLING_DIRECT_ASPECT_RATIOS,
316
329
  providerAspectRatios: { fal: KLING_FAL_ASPECT_RATIOS },
317
- videoResolution: { options: ['1080p', '4k'] },
330
+ // O3 Standard: 720p, as Standard above. Omni Pro is the 1080p tier.
331
+ videoResolution: { options: ['720p', '4k'] },
318
332
  duration: { min: 3, max: 15, mode: 'continuous' },
319
333
  maxIngredientImages: 4,
320
334
  },
@@ -435,10 +449,9 @@ export const MODEL_CAPABILITIES = {
435
449
  // ['480p','720p','1080p']. There is still NO 4K on 2.5 (2.0 is the only
436
450
  // Seedance with one), which is what keeps `is4kVideoKey` version-blind.
437
451
  //
438
- // DEFAULT STAYS 720p, deliberately: a 30s take at 1080p is ~614 credits
439
- // against a 1,000-credit welcome grant, and that is at the promotional
440
- // 1080p rate — it rises when the promo lapses. Reaching a tier and
441
- // defaulting to it are different decisions.
452
+ // DEFAULT STAYS 720p, deliberately: a 30s take at 1080p is ~853 credits
453
+ // against a 1,000-credit welcome grant. Reaching a tier and defaulting to it
454
+ // are different decisions.
442
455
  videoResolution: { options: ['480p', '720p', '1080p'], default: '720p' },
443
456
  duration: { min: 4, max: 30, mode: 'continuous' },
444
457
  maxIngredientImages: 30,
@@ -472,21 +485,25 @@ export const MODEL_CAPABILITIES = {
472
485
  // NO multimodal reference caps, deliberately: on an edit row the clip IS the
473
486
  // canvas and arrives through `sourceVideo`, not as a reference.
474
487
  },
475
- // ── MiniMax H3 (both seats on fal — added 2026-08-27) ──────────────────────
488
+ // ── MiniMax H3 (three seats on fal — base and Max added 2026-08-27, Max
489
+ // Turbo 2026-09-29) ─────────────────────────────────────────────────────
476
490
  //
477
- // Every value below is READ OFF fal's live OpenAPI, fetched 2026-08-27:
491
+ // Every value below is READ OFF fal's live OpenAPI, fetched 2026-08-27, and
492
+ // re-read 2026-09-29 for the Max 1080p tier and the Turbo row:
478
493
  // minimax/h3/{text-to-video,image-to-video,reference-to-video}
479
494
  // minimax/h3-max/{text-to-video,image-to-video,reference-to-video}
495
+ // minimax/h3-max-turbo/{text-to-video,image-to-video} (reference-to-video 404s)
480
496
  // 🚨 CORRECTED 2026-09-09: `minimax/h3-max/reference-to-video` DOES exist —
481
497
  // 9 images, 3 videos, 3 audio. The earlier note here said it 404s; the schema
482
498
  // had never been read. Both rows now declare the full omni-reference set, and
483
499
  // every cap on the Max row was re-read on the Max endpoint rather than copied
484
500
  // down from the base row.
485
501
  //
486
- // 🚨 NEVER PREFIX-MATCH THESE TWO IDS. `minimax-h3-max` starts with
487
- // `minimax-h3`, so any `startsWith('minimax-h3')` swallows the Max row into
488
- // the base row's branch — a different ladder AND a different price at the one
489
- // tier they share. Every lookup downstream is an exact-id map, not a prefix.
502
+ // 🚨 NEVER PREFIX-MATCH THESE THREE IDS. `minimax-h3-max-turbo` starts with
503
+ // `minimax-h3-max`, which starts with `minimax-h3`, so any `startsWith`
504
+ // swallows a row into its neighbour's branch — a different ladder AND a
505
+ // different price at the tiers they share. Every lookup downstream is an
506
+ // exact-id map, not a prefix.
490
507
  'minimax-h3': {
491
508
  // fal reference-to-video schema, 2026-09-09: each audio clip is 2-15s.
492
509
  referenceAudioDuration: { min: 2, max: 15 },
@@ -501,7 +518,7 @@ export const MODEL_CAPABILITIES = {
501
518
  // trained to output and the one every benchmark quotes; 2K is a 2.2x price
502
519
  // step and 4K a 2.7x step, and reaching a tier is a different decision from
503
520
  // defaulting to it (same reasoning that keeps Seedance 2.5 on 720p).
504
- videoResolution: { options: ['480p', '768p', '2k', '4k'], default: '768p' },
521
+ videoResolution: { options: ['480p', '768p', '2k', '4k'], default: '768p', upscaledFrom: { '2k': '768p', '4k': '768p' } },
505
522
  // 5, not 4. MiniMax's own model card says 4-15s; fal's schema — which is
506
523
  // what our request actually hits — says `minimum: 5`. The endpoint wins.
507
524
  duration: { min: 5, max: 15, mode: 'continuous' },
@@ -548,8 +565,28 @@ export const MODEL_CAPABILITIES = {
548
565
  // arms — quoted off this endpoint, not inherited.
549
566
  maxReferenceVideoSeconds: 15,
550
567
  maxReferenceAudioSeconds: 15,
551
- videoResolution: { options: ['480p', '768p'], default: '768p' },
568
+ // 1080P is on all three h3-max endpoints' enum (fal schema, 2026-09-09 and
569
+ // 2026-09-29), described as "1080P latent refinement from a native 768P
570
+ // source": a refinement, not a native generation and not base H3's
571
+ // 2K/4K upscaler. It was keyed 2026-09-09 and pulled 2026-09-10 because the
572
+ // 1080p video-reference token rate was unpublished; fal now states it
573
+ // (see MINIMAX_MAX_REFERENCE). Default stays 768p, the native tier.
574
+ videoResolution: { options: ['480p', '768p', '1080p'], default: '768p' },
575
+ duration: { min: 5, max: 15, mode: 'continuous' },
576
+ },
577
+ 'minimax-h3-max-turbo': {
578
+ aspectRatios: MINIMAX_H3_ASPECT_RATIOS,
579
+ // fal schema for both Turbo endpoints, 2026-09-29: enum
580
+ // ["480P","768P","1080P"], default "768P", the same 1080P "latent
581
+ // refinement from a native 768P source" as the Max row.
582
+ videoResolution: { options: ['480p', '768p', '1080p'], default: '768p' },
583
+ // duration: integer, minimum 5, maximum 15, default 5 (both endpoints).
552
584
  duration: { min: 5, max: 15, mode: 'continuous' },
585
+ // NO reference capacity: `minimax/h3-max-turbo/reference-to-video` returns
586
+ // 404 from fal's OpenAPI endpoint (2026-09-29), and neither published
587
+ // endpoint carries a reference array. Start and end frames ride
588
+ // image-to-video and live in MODEL_REGISTRY.features.lastFrame.
589
+ maxIngredientImages: 0,
553
590
  },
554
591
  // ── LTX-2.5 (both seats on fal — added 2026-08-29) ─────────────────────────
555
592
  //
@@ -706,6 +743,12 @@ export function aspectRatiosFor(model, provider) {
706
743
  return cap.aspectRatios;
707
744
  }
708
745
  /** Video resolutions a model accepts. A FIXED model reports exactly its one value. */
746
+ /** The native render an upscaled tier is made from, or undefined for a native tier. */
747
+ export function upscaledFrom(model, resolution) {
748
+ if (!resolution)
749
+ return undefined;
750
+ return MODEL_CAPABILITIES[model]?.videoResolution?.upscaledFrom?.[resolution];
751
+ }
709
752
  export function videoResolutionsFor(model) {
710
753
  const vr = MODEL_CAPABILITIES[model]?.videoResolution;
711
754
  if (!vr)
@@ -928,14 +971,16 @@ export function describeReferenceImageCaps(models) {
928
971
  }
929
972
  /** H3 Max reference accounting, fal's worked tables read 2026-09-09.
930
973
  * https://fal.ai/models/minimax/h3-max/reference-to-video
931
- * 1080p video-reference pricing is unpublished; never infer it from output rates.
974
+ * 1080p: fal's page, read 2026-09-29: "768p and 1080p output use the same
975
+ * reference-video token counts." So 1080p takes the 768p figure; it was absent
976
+ * until fal published that sentence, and must never be inferred from output rates.
932
977
  */
933
978
  export const MINIMAX_MAX_REFERENCE = {
934
979
  freeTokens: 4096,
935
980
  imagePixelsPerToken: 1024,
936
981
  normalizedImageEdge: 1024,
937
982
  audioTokensPerSecond: 80,
938
- videoTokensPerSecond: { '480p': 2886, '768p': 7459.2 },
983
+ videoTokensPerSecond: { '480p': 2886, '768p': 7459.2, '1080p': 7459.2 },
939
984
  };
940
985
  export function minimaxMaxReferenceTokens(input) {
941
986
  const rate = MINIMAX_MAX_REFERENCE.videoTokensPerSecond[input.resolution];
@@ -950,4 +995,12 @@ export function minimaxMaxReferenceTokens(input) {
950
995
  input.videoSeconds * (rate ?? 0) + input.audioSeconds * MINIMAX_MAX_REFERENCE.audioTokensPerSecond -
951
996
  MINIMAX_MAX_REFERENCE.freeTokens));
952
997
  }
998
+ /** Quotes and generation use the same model default. */
999
+ export function defaultImageResolutionFor(model) {
1000
+ const c = MODEL_CAPABILITIES[model];
1001
+ if (!c?.defaultImageResolution || !c.imageResolutions?.includes(c.defaultImageResolution)) {
1002
+ throw new Error(`Missing or invalid image default for ${model}`);
1003
+ }
1004
+ return c.defaultImageResolution;
1005
+ }
953
1006
  //# sourceMappingURL=model-capabilities.js.map
@@ -1,3 +1,14 @@
1
+ /** Connected host, not an API model: capabilities are checked at runtime. */
2
+ export declare const CHATGPT_IMAGE_HOST: {
3
+ readonly id: "chatgpt-account";
4
+ readonly label: "ChatGPT";
5
+ readonly note: "Generate with your connected ChatGPT account";
6
+ readonly usage: "Uses your ChatGPT account limits";
7
+ };
8
+ import { type BuiltInTool } from './generation-policy.js';
9
+ /** Authoring presets only: these do not claim hosted ChatGPT API capabilities. */
10
+ export declare const CHATGPT_FRAMING_RATIOS: import("./model-capabilities.js").AspectRatio[];
11
+ export declare function composeChatGptFraming(prompt: string, aspectRatio?: string): string;
1
12
  export interface ModelFact {
2
13
  id: string;
3
14
  label: string;
@@ -32,8 +43,14 @@ export interface ModelFact {
32
43
  * best in the world". 2.0 is the specialist for native 4K and for the same
33
44
  * resolution cheaper; Kling is the specialist for cost-effective start-frame,
34
45
  * performance and lip-sync work and the only engine behind Motion Transfer and
35
- * Lip Sync. Recorded in the vault's prompting-ssot.md the same day. Changing the
36
- * default again lands there first, then here, then in slates-model-selection.md. slates-web reads this to order its model lineup
46
+ * Lip Sync. Recorded in the vault's prompting-ssot.md the same day.
47
+ *
48
+ * IMAGE (Eric, 2026-09-15): GPT IMAGE 2.5 SUNBURST IS THE DEFAULT IMAGE MODEL, at
49
+ * `high` and 3k, in agent routing as it has been in the app picker since
50
+ * 2026-09-09. Nano Banana 2 keeps the only headless path, so an image call
51
+ * with no project still runs there. Changing the
52
+ * default lives in this tier field; quality/resolution live in model-capabilities.ts.
53
+ * Consumers and generated skill partials derive them. slates-web reads this to order its model lineup
37
54
  * and to fail its build when a niche seat is named more often than the default.
38
55
  */
39
56
  tier: 'default' | 'specialist' | 'niche';
@@ -94,6 +111,21 @@ export declare function seedanceTaskIntentWords(prompt: string): string[];
94
111
  /** Every model that reads reference video and/or audio, for op descriptions. */
95
112
  export declare function multimodalRefModels(): string[];
96
113
  export declare const MODEL_FACTS: ModelFact[];
114
+ /**
115
+ * The one `default` seat for a kind on the generate route, READ from `tier`.
116
+ * Anything that needs "the default image model" calls this instead of naming
117
+ * one: the op surface and slates-web both hand-typed nano-banana-2 for six days
118
+ * after the app picker moved to Sunburst.
119
+ */
120
+ export declare function defaultModelFor(kind: ModelFact['kind']): string;
121
+ /**
122
+ * The seat a built-in tool renders on when its caller names no model. Resolves
123
+ * `TOOL_SEAT` (generation-policy.ts, THE home): `'default-image'` follows the
124
+ * default image seat above, a model id pins the tool. The desktop resolves the
125
+ * same table through its generated mirror, so an op description, the skill
126
+ * partial and the handler that bills cannot name different models.
127
+ */
128
+ export declare function toolModelFor(tool: BuiltInTool): string;
97
129
  /**
98
130
  * Routing prose for one lane, generated from the SSOT.
99
131
  *
@@ -1,3 +1,9 @@
1
+ /** Connected host, not an API model: capabilities are checked at runtime. */
2
+ export const CHATGPT_IMAGE_HOST = {
3
+ id: 'chatgpt-account', label: 'ChatGPT',
4
+ note: 'Generate with your connected ChatGPT account',
5
+ usage: 'Uses your ChatGPT account limits',
6
+ };
1
7
  // Per-model prompting facts — routing doctrine and the prompt formula, as
2
8
  // KNOWLEDGE (for skills + lead-magnet + op descriptions).
3
9
  //
@@ -26,6 +32,19 @@
26
32
  // Relative cost claims STAY ("dearer than 2.0 at every shared tier") — that is
27
33
  // routing. The figures go, because those are data.
28
34
  import { MODEL_CAPABILITIES } from './model-capabilities.js';
35
+ import { DEFAULT_IMAGE_SEAT, TOOL_SEAT } from './generation-policy.js';
36
+ /** Authoring presets only: these do not claim hosted ChatGPT API capabilities. */
37
+ export const CHATGPT_FRAMING_RATIOS = MODEL_CAPABILITIES['gpt-image-2-5-sunburst'].aspectRatios;
38
+ export function composeChatGptFraming(prompt, aspectRatio) {
39
+ if (aspectRatio === undefined)
40
+ return prompt;
41
+ if (!CHATGPT_FRAMING_RATIOS.includes(aspectRatio)) {
42
+ throw new Error('Choose a supported ChatGPT framing preset');
43
+ }
44
+ // Reuse may contain our previous appended request. Replace only this exact suffix.
45
+ const base = prompt.replace(/\n\nRequested output framing: \d+:\d+ aspect ratio\.$/, '');
46
+ return `${base}\n\nRequested output framing: ${aspectRatio} aspect ratio.`;
47
+ }
29
48
  /**
30
49
  * Reference caps for a fact, read out of the capability SSOT.
31
50
  *
@@ -120,7 +139,7 @@ export const MODEL_FACTS = [
120
139
  {
121
140
  id: 'nano-banana-2',
122
141
  route: 'generate',
123
- tier: 'default',
142
+ tier: 'specialist',
124
143
  // Gemini 3.1 FLASH Image — verified against the runtime slug map in
125
144
  // slate/src/main/api/google.ts. Nano Banana PRO is a different model
126
145
  // (gemini-3-pro-image-preview); do not conflate them.
@@ -128,7 +147,7 @@ export const MODEL_FACTS = [
128
147
  kind: 'image',
129
148
  // 14 = 10 object-fidelity + 4 character-consistency; the categories don't trade.
130
149
  ...caps('nano-banana-2'),
131
- notes: 'DEFAULT image model and the all-rounder — route here unless another seat\'s speciality is the point. Best start-frame for legible in-scene text. Knowledge cutoff Jan 2025: anything later needs reference images.',
150
+ notes: 'The all-rounder and the only image seat with a headless path: holds many subjects coherently in one frame, and the start-frame for legible in-scene text. Knowledge cutoff Jan 2025: anything later needs reference images.',
132
151
  },
133
152
  {
134
153
  id: 'nano-banana-2-lite',
@@ -160,11 +179,11 @@ export const MODEL_FACTS = [
160
179
  {
161
180
  id: 'gpt-image-2-5-sunburst',
162
181
  route: 'generate',
163
- tier: 'specialist',
182
+ tier: 'default',
164
183
  label: 'GPT Image 2.5 Sunburst',
165
184
  kind: 'image',
166
185
  ...caps('gpt-image-2-5-sunburst'),
167
- notes: 'THE QUALITY GPT IMAGE SEAT — OpenAI\'s most capable image model, higher quality than GPT Image 2, same price as Flare, deliberately SLOWER. Route here whenever quality outranks speed: finals, hero frames, photoreal people, and multi-reference edits where every reference must survive into one frame — its widest lead. Not for drafts; you pay latency on every frame. Explore on Flare, finish on Sunburst.',
186
+ notes: 'THE QUALITY GPT IMAGE SEAT — OpenAI\'s most capable image model, higher quality than GPT Image 2, same price as Flare, deliberately SLOWER. Route here unless speed is the point: finals, hero frames, photoreal people, and multi-reference edits where every reference must survive into one frame — its widest lead. Explore on Flare, finish on Sunburst.',
168
187
  },
169
188
  {
170
189
  id: 'flux-2-max',
@@ -293,7 +312,19 @@ export const MODEL_FACTS = [
293
312
  // from the base row: "Audio cannot be the only reference input; provide at
294
313
  // least one reference image or video with it."
295
314
  audioRefNeedsCompanion: true,
296
- notes: 'THE SPEED SEAT, and the DEARER one at the tier they share — never the cheap H3 and never the default. fal\'s post-train of the H3 weights: MEASURED 2026-08-27 at about 12x faster than base H3 on the same prompt and params, queue to finished file, plus a thin vendor-reported quality edge. It gives up the upper resolution tiers. It takes the same omni-reference set as base H3 and animates start and end frames — but not both in one call: its reference endpoint has no start/end-frame fields, where base H3\'s does. Never describe this row as taking no image or reference input. Route here when a fast turnaround on text-to-video or a start-frame shot is worth the premium.',
315
+ notes: 'THE SPEED SEAT, and the DEARER one at the tier they share — never the cheap H3 and never the default. fal\'s post-train of the H3 weights: MEASURED 2026-08-27 at about 12x faster than base H3 on the same prompt and params, queue to finished file, plus a thin vendor-reported quality edge. It tops out at a 1080p refinement of its 768p render. It takes the same omni-reference set as base H3 and animates start and end frames — but not both in one call: its reference endpoint has no start/end-frame fields, where base H3\'s does. Never describe this row as taking no image or reference input. Route here when a fast turnaround on text-to-video or a start-frame shot is worth the premium.',
316
+ },
317
+ {
318
+ id: 'minimax-h3-max-turbo',
319
+ route: 'generate',
320
+ tier: 'specialist',
321
+ label: 'MiniMax H3 Max Turbo',
322
+ kind: 'video',
323
+ // Added 2026-09-29. No reference caps: fal publishes text-to-video and
324
+ // image-to-video for Turbo and its reference-to-video returns 404, so
325
+ // `caps()` returns nulls and the composer refuses references.
326
+ ...caps('minimax-h3-max-turbo'),
327
+ notes: 'THE BUDGET SEAT of the MiniMax family: a second fal post-train of the H3 weights, billed at half H3 Max\'s rate at every tier. Its 1080p is a refinement of the native 768p render, not a native 1080p generation. INPUTS ARE FRAMES, NOT REFERENCES: text-to-video and start/end frames only, with no reference endpoint, so reference-driven consistency goes to H3 Max or base H3. Route here for drafts, volume and cheap coverage, then re-run the keeper on H3 Max or a hero seat.',
297
328
  },
298
329
  {
299
330
  id: 'ltx-2-5',
@@ -362,7 +393,37 @@ for (const kind of ['image', 'video', 'audio']) {
362
393
  }
363
394
  }
364
395
  }
396
+ /**
397
+ * The one `default` seat for a kind on the generate route, READ from `tier`.
398
+ * Anything that needs "the default image model" calls this instead of naming
399
+ * one: the op surface and slates-web both hand-typed nano-banana-2 for six days
400
+ * after the app picker moved to Sunburst.
401
+ */
402
+ export function defaultModelFor(kind) {
403
+ const fact = MODEL_FACTS.find((f) => f.kind === kind && f.route === 'generate' && f.tier === 'default');
404
+ if (!fact)
405
+ throw new Error(`MODEL_FACTS: no default ${kind} seat on the generate route`);
406
+ return fact.id;
407
+ }
408
+ /**
409
+ * The seat a built-in tool renders on when its caller names no model. Resolves
410
+ * `TOOL_SEAT` (generation-policy.ts, THE home): `'default-image'` follows the
411
+ * default image seat above, a model id pins the tool. The desktop resolves the
412
+ * same table through its generated mirror, so an op description, the skill
413
+ * partial and the handler that bills cannot name different models.
414
+ */
415
+ export function toolModelFor(tool) {
416
+ const seat = TOOL_SEAT[tool];
417
+ return seat === DEFAULT_IMAGE_SEAT ? defaultModelFor('image') : seat;
418
+ }
365
419
  const FACT_BY_ID = new Map(MODEL_FACTS.map((m) => [m.id, m]));
420
+ // A pinned tool seat must be a live image model, or the tool quotes and fires
421
+ // an id nothing can render. Asserted at load, like the one-default-per-kind rule.
422
+ for (const [tool, seat] of Object.entries(TOOL_SEAT)) {
423
+ if (seat !== DEFAULT_IMAGE_SEAT && FACT_BY_ID.get(seat)?.kind !== 'image') {
424
+ throw new Error(`TOOL_SEAT: ${tool} is pinned to "${seat}", which is not an image model in MODEL_FACTS`);
425
+ }
426
+ }
366
427
  /**
367
428
  * Routing prose for one lane, generated from the SSOT.
368
429
  *