@slatesvideo/shared 0.7.2 → 0.7.3

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 (84) hide show
  1. package/dist/clients/cloud.d.ts +4 -0
  2. package/dist/clients/cloud.js +11 -3
  3. package/dist/index.d.ts +1 -0
  4. package/dist/index.js +1 -0
  5. package/dist/manual/content.d.ts +1 -1
  6. package/dist/manual/content.js +1 -1
  7. package/dist/operations/index.d.ts +12 -13
  8. package/dist/operations/index.js +158 -133
  9. package/dist/operations/surface.d.ts +6 -2
  10. package/dist/operations/surface.js +29 -5
  11. package/dist/prompts/agent-doctrine.d.ts +4 -4
  12. package/dist/prompts/agent-doctrine.js +17 -28
  13. package/dist/prompts/guide-discovery.d.ts +23 -0
  14. package/dist/prompts/guide-discovery.js +39 -0
  15. package/dist/prompts/guide-retrieval.js +1 -1
  16. package/dist/prompts/model-capabilities.d.ts +8 -9
  17. package/dist/prompts/model-capabilities.js +11 -51
  18. package/dist/prompts/model-facts.d.ts +2 -2
  19. package/dist/prompts/model-facts.js +15 -26
  20. package/dist/prompts/partials.generated.js +6 -3
  21. package/dist/prompts/prompting-tips.d.ts +1 -1
  22. package/dist/prompts/prompting-tips.js +21 -63
  23. package/dist/prompts/search-terms.d.ts +3 -0
  24. package/dist/prompts/search-terms.js +24 -0
  25. package/dist/skills/content.js +36 -37
  26. package/dist/skills/metadata.d.ts +7 -0
  27. package/dist/skills/metadata.js +29 -0
  28. package/exports/slates-chatgpt-images/generated/SKILL.md +7 -1
  29. package/exports/slates-chatgpt-images/generated/slates-chatgpt-images.skill +0 -0
  30. package/exports/slates-prompt-builder/generated/SKILL.md +28 -16
  31. package/exports/slates-prompt-builder/generated/reference-character.md +12 -13
  32. package/exports/slates-prompt-builder/generated/reference-content-policy.md +2 -2
  33. package/exports/slates-prompt-builder/generated/reference-gpt-image-2-5.md +191 -0
  34. package/exports/slates-prompt-builder/generated/reference-kling.md +32 -11
  35. package/exports/slates-prompt-builder/generated/reference-nano-banana.md +24 -6
  36. package/exports/slates-prompt-builder/generated/reference-omni-flash.md +65 -0
  37. package/exports/slates-prompt-builder/generated/reference-seedance-2-5.md +362 -0
  38. package/exports/slates-prompt-builder/generated/reference-seedance.md +34 -4
  39. package/exports/slates-prompt-builder/generated/slates-prompt-builder-manifest.json +77 -23
  40. package/exports/slates-prompt-builder/generated/slates-prompt-builder.skill +0 -0
  41. package/package.json +2 -1
  42. package/skills/_partials/blender-action-curves.md +24 -0
  43. package/skills/_partials/iteration-diagnosis.md +5 -0
  44. package/skills/_partials/model-routing.md +35 -0
  45. package/skills/_partials/seedance-25-timestamps.md +2 -2
  46. package/skills/_partials/still-gate.md +2 -2
  47. package/skills/_partials/thresholds.md +1 -1
  48. package/skills/slates-blocking-to-prompt.md +15 -13
  49. package/skills/slates-camera-language.md +45 -7
  50. package/skills/slates-character-identity.md +8 -6
  51. package/skills/slates-chatgpt-images.md +7 -1
  52. package/skills/slates-cinematic-look.md +1 -1
  53. package/skills/slates-content-policy.md +4 -6
  54. package/skills/slates-cost-discipline.md +18 -12
  55. package/skills/slates-dialogue-blocking.md +6 -6
  56. package/skills/slates-direct-response-ad.md +1 -1
  57. package/skills/slates-edit-and-iterate.md +12 -4
  58. package/skills/slates-model-selection.md +82 -90
  59. package/skills/slates-one-prompt-film.md +1 -1
  60. package/skills/slates-previs-blocking.md +44 -13
  61. package/skills/slates-project-organization.md +2 -2
  62. package/skills/slates-prompting-elevenlabs.md +4 -4
  63. package/skills/slates-prompting-flux-2-max.md +2 -3
  64. package/skills/slates-prompting-gpt-image-2-5.md +2 -2
  65. package/skills/slates-prompting-inworld-tts.md +174 -174
  66. package/skills/slates-prompting-kling-v3.md +11 -9
  67. package/skills/slates-prompting-lip-sync.md +15 -15
  68. package/skills/slates-prompting-ltx-2-5.md +5 -6
  69. package/skills/slates-prompting-minimax-h3.md +11 -11
  70. package/skills/slates-prompting-motion-transfer.md +8 -8
  71. package/skills/slates-prompting-nano-banana-2.md +8 -4
  72. package/skills/slates-prompting-omni-flash.md +9 -9
  73. package/skills/slates-prompting-seed-audio.md +24 -4
  74. package/skills/slates-prompting-seedance-2-5.md +40 -30
  75. package/skills/slates-prompting-seedance.md +4 -4
  76. package/skills/slates-prompting-seedream-5-lite.md +6 -6
  77. package/skills/slates-restyle-from-blocking.md +2 -2
  78. package/skills/slates-script-craft.md +1 -1
  79. package/skills/slates-shot-variety.md +1 -1
  80. package/skills/slates-storyboard-from-script.md +1 -1
  81. package/skills/slates-style-prompting.md +56 -54
  82. package/skills/slates-ugc-influencer-ad.md +1 -1
  83. package/skills/slates-vision-feedback-loop.md +118 -110
  84. package/skills/slates-prompting-veo-3.md +0 -224
@@ -61,12 +61,16 @@ export declare function toolDefinition(op: SurfaceOp): ToolDefinition;
61
61
  * Render a tool surface.
62
62
  *
63
63
  * `desktop` sends `core` plus whatever groups have been loaded this run; `mcp`
64
- * renders all definitions, and the MCP server lists all of them unless started with
65
- * `--tools=compact`.
64
+ * renders every definition for the fixed MCP list.
66
65
  */
67
66
  export declare function toolDefinitions(ops: readonly SurfaceOp[], opts: {
68
67
  surface: 'desktop' | 'mcp';
69
68
  groups?: readonly OperationGroup[];
70
69
  names?: readonly string[];
71
70
  }): ToolDefinition[];
71
+ /** Rank exact task words by rarity across the current registry, returning at most ten. */
72
+ export declare function searchTools<T extends Pick<SurfaceOp, 'id' | 'description'>>(operations: readonly T[], query: string): Array<{
73
+ op: T;
74
+ score: number;
75
+ }>;
72
76
  //# sourceMappingURL=surface.d.ts.map
@@ -15,8 +15,7 @@
15
15
  // mechanism: we build every turn there, so a load reaches the model. The
16
16
  // MCP server lists every op and leaves hiding definitions to the host
17
17
  // (tiering there left Claude and Codex users unable to generate in 0.6.0;
18
- // see the TOOLS comment in packages/mcp/src/server.ts). `--tools=compact`
19
- // is the opt-in exception.
18
+ // see the TOOLS comment in packages/mcp/src/server.ts).
20
19
  //
21
20
  // 3. ONE SCHEMA RENDERER. The desktop rendered `$refStrategy: 'none'` and
22
21
  // the MCP server rendered `target: 'openApi3'`, so "the two surfaces
@@ -29,6 +28,7 @@
29
28
  // an op whose `run` body posts is a lie that lets a host auto-approve a
30
29
  // mutation, so the check has to be able to catch it, and it is mutation-tested.
31
30
  // ============================================================
31
+ import { searchTerms } from '../prompts/search-terms.js';
32
32
  import { zodToJsonSchema } from 'zod-to-json-schema';
33
33
  // ── 1. Annotations ──────────────────────────────────────────────────────────
34
34
  /** Ops that only read. Prefixes, because the verb IS the first word of the id. */
@@ -221,7 +221,7 @@ export function tierFor(id) {
221
221
  export const GROUP_SUMMARY = {
222
222
  library: 'folders, the Library (user-named categories of saved references: characters, locations, products, looks), moving and copying assets and Library items between projects, moving a project into the current projects folder, revealing files on disk',
223
223
  script: 'rich script documents, anchored sections and their saved versions, reusable passages, variations and take input history',
224
- timeline: 'named cuts and selected exports; the timeline (tracks, clips, markers, settings), video export and export for DaVinci, Premiere or Final Cut (XML), clip trimming',
224
+ timeline: 'named cuts and selected exports; the timeline (tracks, clips, markers, settings), video export and FCP7 XML export for DaVinci Resolve or Premiere, clip trimming',
225
225
  admin: 'rename / delete / reorder for projects, characters, locations, looks, boards, scenes and frames; shot duplicate, split and merge',
226
226
  blender: 'the Blender previs bridge — scene inspection, bpy execution, API docs, grey-box render',
227
227
  };
@@ -247,8 +247,7 @@ export function toolDefinition(op) {
247
247
  * Render a tool surface.
248
248
  *
249
249
  * `desktop` sends `core` plus whatever groups have been loaded this run; `mcp`
250
- * renders all definitions, and the MCP server lists all of them unless started with
251
- * `--tools=compact`.
250
+ * renders every definition for the fixed MCP list.
252
251
  */
253
252
  export function toolDefinitions(ops, opts) {
254
253
  const loaded = new Set(opts.groups ?? []);
@@ -261,4 +260,29 @@ export function toolDefinitions(ops, opts) {
261
260
  })
262
261
  .map(toolDefinition);
263
262
  }
263
+ // Natural brief words for capabilities whose API names use technical terms.
264
+ const TASK_ALIASES = {
265
+ slates_generate_video: 'create animate photo picture film movie ad advert advertisement',
266
+ slates_generate_from_shots: 'create make film movie ad advert advertisement',
267
+ slates_generate_image: 'photo picture illustration',
268
+ slates_edit_image: 'photo picture',
269
+ slates_use_pictures_as_first_frames: 'photo animate',
270
+ slates_generate_audio: 'voiceover narration speech ambience sound effect',
271
+ };
272
+ /** Rank exact task words by rarity across the current registry, returning at most ten. */
273
+ export function searchTools(operations, query) {
274
+ const terms = searchTerms(query);
275
+ const documents = operations.map(op => ({
276
+ op,
277
+ name: new Set(searchTerms(op.id)),
278
+ words: new Set(searchTerms(`${op.description} ${TASK_ALIASES[op.id] ?? ''}`)),
279
+ }));
280
+ const frequency = new Map(terms.map(term => [term, documents.filter(doc => doc.name.has(term) || doc.words.has(term)).length]));
281
+ return documents.map(doc => ({
282
+ op: doc.op,
283
+ score: terms.reduce((sum, term) => sum + (doc.name.has(term) || doc.words.has(term)
284
+ ? Math.log(1 + documents.length / (1 + (frequency.get(term) ?? 0))) * (doc.name.has(term) ? 2 : 1)
285
+ : 0), 0),
286
+ })).filter(match => match.score > 0).sort((a, b) => b.score - a.score || a.op.id.localeCompare(b.op.id)).slice(0, 10);
287
+ }
264
288
  //# sourceMappingURL=surface.js.map
@@ -4,16 +4,16 @@
4
4
  * `desktop` — the in-app Studio Agent. Its loop enforces plan approval in
5
5
  * code, auto-polls generation status, and displays orchestration cost itself.
6
6
  * `mcp` — Claude Code / Claude Desktop / Cursor / Codex over stdio. No
7
- * `present_plan` tool, no auto-poll, no app chrome; the consent gate is the
8
- * op-level `requires_confirm` threshold plus the host client's own per-call
9
- * tool approval. The asymmetry is DELIBERATE — see slates-mcp/CLAUDE.md.
7
+ * `present_plan` tool, no auto-poll, no app chrome. The user approves the
8
+ * itemized spend in chat; op-level `requires_confirm` is a threshold backstop.
9
+ * Host tool approval depends on the client. See slates-mcp/CLAUDE.md.
10
10
  */
11
11
  export type AgentSurface = 'desktop' | 'mcp';
12
12
  interface SkillIndexEntry {
13
13
  name: string;
14
14
  description: string;
15
15
  }
16
- /** Parse `name:`/`description:` out of each embedded skill's frontmatter. */
16
+ /** Generated from the same validated metadata used by discovery and installation. */
17
17
  export declare function buildSkillIndex(): SkillIndexEntry[];
18
18
  export declare const WORKING_METHOD: ReadonlyArray<Record<AgentSurface, string>>;
19
19
  export declare const HARD_RULES: ReadonlyArray<Record<AgentSurface, string>>;
@@ -50,6 +50,7 @@
50
50
  // the whole embedded SKILLS record. Root barrel only.
51
51
  // ============================================================
52
52
  import { SKILLS } from '../skills/content.js';
53
+ import { guideCatalog } from './guide-discovery.js';
53
54
  import { VIDEO_MODELS, AUDIO_MODELS } from '../operations/index.js';
54
55
  /** A line that is identical on both surfaces. The default. */
55
56
  function both(text) {
@@ -59,21 +60,9 @@ function both(text) {
59
60
  function fork(desktop, mcp) {
60
61
  return { desktop, mcp };
61
62
  }
62
- /** Parse `name:`/`description:` out of each embedded skill's frontmatter. */
63
+ /** Generated from the same validated metadata used by discovery and installation. */
63
64
  export function buildSkillIndex() {
64
- const entries = [];
65
- for (const key of Object.keys(SKILLS).sort()) {
66
- const content = SKILLS[key];
67
- const fm = /^---\n([\s\S]*?)\n---/.exec(content);
68
- let description = '';
69
- if (fm) {
70
- const m = /^description:\s*(.+)$/m.exec(fm[1]);
71
- if (m)
72
- description = m[1].trim().replace(/^['"]|['"]$/g, '');
73
- }
74
- entries.push({ name: key, description });
75
- }
76
- return entries;
65
+ return guideCatalog(SKILLS).map(({ name, description }) => ({ name, description }));
77
66
  }
78
67
  // ── Preamble ───────────────────────────────────────────────────────
79
68
  // FORKED, and the MCP side is a SUMMARY THAT MUST FIT IN THE CUT. Claude Code
@@ -83,16 +72,16 @@ export function buildSkillIndex() {
83
72
  // find a tool when the host shows names only, the spend gate, real numbers, the
84
73
  // current project — sits here; the sections below expand it for clients that
85
74
  // read everything. mcp-instructions-smoke asserts these lines land inside the
86
- // cut with the UPDATE AVAILABLE notice in front of them.
87
- const PREAMBLE = fork(`You are the Slates Studio Agent — a production assistant living inside Slates, the AI video creation studio. You plan and execute video/image production runs by chaining the Slates tools: script → characters → images → videos → quality-check → regenerate, ending with assets in the user's project (and on the timeline when asked).`, `You are connected to Slates, the AI video creation studio. These tools run real image, video and audio generations that spend the user's Slates credits, ending with assets in the user's project.
75
+ // cut when UPDATE AVAILABLE follows the first doctrine paragraph.
76
+ const PREAMBLE = fork(`You are the Slates Studio Agent — a production assistant living inside Slates, the AI video creation studio. Turn the user's vision into finished work using the optional Slates tools and relevant production craft. Choose the workflow for the brief; start from the user's existing material and bring results into their project or timeline when asked.`, `Turn the user's vision into finished work in Slates. Find craft with slates_get_prompting_guide(query: the brief); reuse current guidance. Tools generate real media and spend credits. Before generation, estimate every step, show the itemized total and wait for approval. Work in the current project. Choose tools to fit the brief.
88
77
 
89
78
  ## Essentials (the sections below expand on these)
90
79
 
91
- - FINDING TOOLS: every Slates capability is its own slates_* tool. If your client shows tool names only, search your tools for the task (for example "slates generate video") and load that tool before calling it. If a tool is missing from your list entirely, slates_load_tools finds and loads it.
80
+ - FINDING TOOLS: every Slates capability is its own slates_* tool. If your client shows tool names only, search your tools for the task (for example "slates generate video") and load that tool before calling it. slates_load_tools searches Slates tools by task and returns exact schemas.
92
81
  - SPENDING: before ANY generation, price every step with slates_estimate_generation_cost, show the user the itemized total in ONE message, and wait for their OK. Pass confirm: true only to relay an explicit user OK for that exact spend.
93
82
  - REAL NUMBERS ONLY: every cost, balance or count you state is copied from a tool result in this session. Never describe how a generation looks unless you fetched it this session.
94
83
  - PROJECT: call slates_get_workspace_state once, work in the user's current project, and never create a project unless asked.
95
- - CRAFT: before prompting a model, read its guide with slates_get_prompting_guide (pass the model id). For how or where in the app, use topic "app-manual".
84
+ - CRAFT: discover relevant guides with slates_get_prompting_guide(query: the brief). Reuse current cards and guidance; fetch sections for missing craft. For UI help, use topic "app-manual".
96
85
  - Speak in the app's words and name assets by code and label (IMG-A12), never by tool name or UUID.`);
97
86
  // ── The working method ─────────────────────────────────────────────
98
87
  export const WORKING_METHOD = [
@@ -103,15 +92,15 @@ export const WORKING_METHOD = [
103
92
  // so its agent must load before calling. The MCP server lists every tool and
104
93
  // the host decides what to show (Essentials, FINDING TOOLS), so telling an MCP
105
94
  // client to load first costs it a turn for a tool it already has.
106
- fork(`3. LOAD KNOWLEDGE ON DEMAND: slates_get_prompting_guide returns a short card by default; query a section or technique when needed. Use slates_load_tools with query to find a capability, then names to load its exact schema. A load replaces the previous optional selection. Before quoting a model, load its tool schema or routing guide. Use workspace generationDefaults when the user has no preference. Read the model card delivered by the estimate; fetch a section or full guide for an unfamiliar mode or missing detail. Reuse guidance already in context; retrieve it again when omitted or stale.`, `3. LOAD KNOWLEDGE ON DEMAND: slates_get_prompting_guide returns a short card by default; query a section or technique when needed. Before quoting a model, read its tool schema or routing guide. Use workspace generationDefaults when the user has no preference. Read the model card delivered by the estimate; fetch a section or full guide for an unfamiliar mode or missing detail. Reuse guidance already in context; retrieve it again when omitted or stale.`),
95
+ fork(`3. LOAD KNOWLEDGE ON DEMAND: discover craft from the brief with slates_get_prompting_guide(query: the need); no topic returns ranked guides, no query returns the catalog. With a topic, read a short card or query a section/technique when needed. Use slates_load_tools with query to find a capability, then names to load its exact schema. A load replaces the previous optional selection. Before quoting a model, load its tool schema or routing guide. Use workspace generationDefaults when the user has no preference. Read the model card delivered by the estimate; fetch a section or full guide for an unfamiliar mode or missing detail. Reuse guidance already in context; retrieve it again when omitted or stale.`, `3. LOAD KNOWLEDGE ON DEMAND: discover craft from the brief with slates_get_prompting_guide(query: the need); no topic returns ranked guides, no query returns the catalog. With a topic, read a short card or query a section/technique when needed. Before quoting a model, read its tool schema or routing guide. Use workspace generationDefaults when the user has no preference. Read the model card delivered by the estimate; fetch a section or full guide for an unfamiliar mode or missing detail. Reuse guidance already in context; retrieve it again when omitted or stale.`),
107
96
  // FORKED: `present_plan` is a loop-level DESKTOP tool, deliberately not in
108
97
  // ALL_OPERATIONS, so MCP never sees it and has no plan gate at all. Its
109
- // substitute is the per-op `requires_confirm` threshold plus the host
110
- // client's own per-call approval UI. Describing the desktop gate to an MCP
98
+ // substitute is the itemized quote and explicit user approval in chat,
99
+ // backed by per-op `requires_confirm`. Describing the desktop gate to an MCP
111
100
  // client would name a tool that does not exist.
112
101
  fork(`4. PLAN + GET APPROVAL: before ANY generation, call present_plan with itemized credit costs (slates_estimate_generation_cost per step). One approval covers the plan's listed steps ONLY. Generation tools are rejected without an approved plan — and any user revision, question, or new instruction after an approval means you MUST re-present the plan BEFORE the next generation call (calling a generation op first just gets BLOCKED and wastes a turn).`, `4. PLAN + GET APPROVAL: before ANY generation, price every step with slates_estimate_generation_cost and put the itemized total in front of the user in ONE message, then wait for their answer. There is no present_plan tool on this surface — consent is per call: a generation over the confirm threshold returns requires_confirm, and confirm: true is only ever a relay of an explicit user OK for that exact spend. Never pass confirm: true to clear a gate the user has not seen.`),
113
102
  fork(`5. EXECUTE: after approval, pass confirm: true (the approval IS the consent — never re-ask per step). Use background: true + status polling for video.`, `5. EXECUTE: run the approved steps, passing confirm: true only for the spend the user actually OK'd. Use background: true + status polling for video.`),
114
- both(`6. QUALITY-CHECK: you have vision. After key generations, fetch the result (slates_get_asset_image / slates_get_asset_video_frames) and review it against the brief (slates-vision-feedback-loop). Fix real problems; don't churn credits polishing what works. Change ONE variable per regeneration.`),
103
+ both(`6. QUALITY-CHECK: you have vision. Inspect images or sampled video frames against the brief (slates-vision-feedback-loop). Frames establish visible appearance, not continuous motion, lip sync or sound: review those through actual playback or an audio-capable host when available; otherwise state they remain unreviewed. Fix real problems and change ONE variable per regeneration.`),
115
104
  both(`7. REPORT: when done, summarize what was made and where it landed. Concise and concrete.`),
116
105
  ];
117
106
  // ── Hard rules ─────────────────────────────────────────────────────
@@ -125,7 +114,7 @@ export const HARD_RULES = [
125
114
  both(`- PROMPT IS LAW (reference doctrine): references are cited inline by name and image number ("Marcus (images 1 and 2)"); the prompt text leads. Never write role-essays about what each reference is "for".`),
126
115
  both(`- NAMES AND DESCRIPTIONS ARE THE USER'S UI, NOT YOUR NOTEPAD. A project, board or shot name is rendered at the top of the user's screen at all times. Name it after the piece ("Kaiju selfie"), never after your process, and NEVER append your own status or housekeeping — no "shot list", no "v2", no "Written from IMG-A172 and IMG-A182", no "Delete freely", no "scratch". Leave the description empty unless the user gave you one worth keeping: a note-to-self at the top of the screen reads as part of the product, and the user has to look at it every day. Say that kind of thing in your reply to them instead.`),
127
116
  both(`- SAY THE APP'S WORDS. Tool and parameter names keep older nouns; the screen does not, and the user only ever sees the screen. Say board (not storyboard), location (not environment) and look (not style) for Library items, timeline (a saved version of it is a cut), version (a saved rewrite of a script section; not alternative), Words or Words + shots (the Script page's switch), the Generate panel (not the quote), and tab (Media, Script and Board; not lens). Describe what you did in plain words ("Checked your project", "Priced 6 clips"), never a tool or parameter name.`),
128
- both(`- RESOLUTION DEFAULT IS UNIFORM: 1080p on the best available video model. Do not crank resolution the user didn't ask for.`),
117
+ both(`- RESOLUTION DEFAULT: each model's own default. Do not crank resolution the user didn't ask for.`),
129
118
  // ⛔ THE MODEL ROUTING BLOCK IS GONE FROM HERE, DELIBERATELY (2026-08-30).
130
119
  //
131
120
  // It was 17,700 characters — 47% of the whole doctrine — and every lane of it
@@ -141,9 +130,9 @@ export const HARD_RULES = [
141
130
  // The old `buildModelRouting()` renderer was DELETED rather than left
142
131
  // exported "in case": an export nothing calls is an export nothing keeps
143
132
  // honest. `describeRouting()` in model-facts.ts is the renderer now.
144
- both(`- MODEL KINDS: image, video and audio models are disjoint — no image model makes a video, no video model makes a standalone image, no image or video model makes audio. Which SEAT to pick inside a kind is on each generate op's own \`model\` description, and the full table is the slates-model-selection skill.`),
133
+ both(`- MODEL KINDS: image, video and audio models are disjoint — no image model makes a video, no video model makes a standalone image, no image or video model makes a standalone audio file (video models can generate sound inside the clip). Which SEAT to pick inside a kind is on each generate op's own \`model\` description, and the full table is the slates-model-selection skill.`),
145
134
  both(`- ASSET CODES + STALENESS: every asset carries a badge code (IMG-A12 / VID-V3 / AUD-S1, top-left of its gallery card); speak about assets by code + label. When the user says "these" or "this one", call slates_get_selection rather than asking which. Asset lists go STALE — the user creates assets in the Slates UI mid-conversation. When the user names a code you have NOT seen in a tool result this session, resolve it with slates_list_assets (use the search filter) BEFORE using it. NEVER guess an asset id or reuse a nearby UUID — pass the exact id a tool returned for that exact code. A wrong start frame burns real credits.`),
146
- both(`- CREDITS ONLY: every generation you drive bills Slates credits (your tool calls enforce this). Never suggest BYOK keys for agent work.`),
135
+ both(`- CREDITS ONLY: every generation you drive bills Slates credits (your tool calls enforce this), except ChatGPT images, which use the person's ChatGPT account. Never suggest BYOK keys for agent work.`),
147
136
  both(`- Do not invent tools, asset ids, or credit prices. If a tool errors, read the error and fix that exact issue; don't repeat the same call unchanged and never switch models to route around a parameter mistake.`),
148
137
  // FORKED: only the desktop loop auto-polls generation status
149
138
  // (loop.ts → autoPollUntilTerminal). Telling an MCP client "the app keeps
@@ -166,12 +155,12 @@ export const HARD_RULES = [
166
155
  // ── Surface-only footers ───────────────────────────────────────────
167
156
  //
168
157
  // MCP clients have no Slates chrome and may have no skill FILES installed
169
- // (`slates install-skills` covers Claude Code; Claude Desktop, Cursor and
170
- // Codex have no equivalent mechanism). The desktop always has the embedded
158
+ // (`slates install-skills` covers Claude Code and Codex; other hosts may
159
+ // use different native skill locations). The desktop always has the embedded
171
160
  // record, so this paragraph would be dead weight in its cached prefix.
172
161
  const MCP_FOOTER = `## Working without skill files
173
162
 
174
- If slates-* skill files are not installed in this client, every one of them is still reachable as data: call slates_get_prompting_guide with the skill name (or a model id — it resolves aliases). The guide index above is the complete list. "No skill installed" is never a reason to prompt a model blind.
163
+ If slates-* skill files are not installed in this client, every one of them is still reachable as data: call slates_get_prompting_guide with the skill name (or a model id — it resolves aliases). Query the brief or browse topic "catalog" for generated discovery and entitled member playbooks. "No skill installed" is never a reason to prompt a model blind.
175
164
  `;
176
165
  /**
177
166
  * THE doctrine string for a surface. The desktop's whole system prompt and the
@@ -0,0 +1,23 @@
1
+ export interface GuideCatalogEntry {
2
+ name: string;
3
+ description: string;
4
+ tier: 'free' | 'paid';
5
+ }
6
+ /** Metadata is the catalog; headings supply technique vocabulary without returning whole guides. */
7
+ export declare function guideCatalog(skills: Readonly<Record<string, string>>): GuideCatalogEntry[];
8
+ export declare function discoverGuides(entries: readonly GuideCatalogEntry[], skills: Readonly<Record<string, string>>, query?: string, limit?: number, offset?: number): {
9
+ query: string | null;
10
+ fallback: boolean;
11
+ total: number;
12
+ offset: number;
13
+ nextOffset: number | null;
14
+ guides: {
15
+ matched: string[];
16
+ sections: string[];
17
+ name: string;
18
+ description: string;
19
+ tier: "free" | "paid";
20
+ }[];
21
+ rest: GuideCatalogEntry[];
22
+ };
23
+ //# sourceMappingURL=guide-discovery.d.ts.map
@@ -0,0 +1,39 @@
1
+ import { parseSkillMetadata } from '../skills/metadata.js';
2
+ import { guideSections } from './guide-retrieval.js';
3
+ import { searchTerms } from './search-terms.js';
4
+ // Brief filler that matches guides by accident ("30 second", "how much", "I need").
5
+ const GUIDE_FILLER = ['create', 'make', 'use', 'video', 'image', 'need', 'how', 'much', 'turn', 'keep', 'get', 'like', 'just', 'about', 'every', 'second', 'minute'];
6
+ const words = (text) => searchTerms(text, GUIDE_FILLER).filter(word => !/^\d{1,2}$/.test(word));
7
+ /** Metadata is the catalog; headings supply technique vocabulary without returning whole guides. */
8
+ export function guideCatalog(skills) {
9
+ return Object.keys(skills).sort().map(name => ({ ...parseSkillMetadata(skills[name], name), tier: 'free' }));
10
+ }
11
+ export function discoverGuides(entries, skills, query, limit, offset = 0) {
12
+ const terms = words(query ?? '');
13
+ const documents = entries.map(entry => {
14
+ const sections = skills[entry.name] ? guideSections(skills[entry.name]) : [];
15
+ const metadata = new Set(words(`${entry.name.replace(/-/g, ' ')} ${entry.description}`));
16
+ const headings = sections.map(section => ({ heading: section.title, terms: new Set(words(section.title)) }));
17
+ return { entry, metadata, headings };
18
+ });
19
+ // Rare terms in the corpus carry more weight than generic production vocabulary.
20
+ const frequency = new Map(terms.map(term => [term, documents.filter(doc => doc.metadata.has(term) || doc.headings.some(section => section.terms.has(term))).length]));
21
+ const ranked = documents.map(doc => {
22
+ const matched = terms.filter(term => doc.metadata.has(term) || doc.headings.some(section => section.terms.has(term)));
23
+ const score = matched.reduce((sum, term) => sum + Math.log(1 + documents.length / (1 + (frequency.get(term) ?? 0))) * (doc.metadata.has(term) ? 3 : 1), 0);
24
+ const sections = doc.headings.filter(section => matched.some(term => section.terms.has(term))).slice(0, 3).map(section => section.heading);
25
+ return { ...doc.entry, matched, sections, score };
26
+ }).filter(entry => !terms.length || entry.score > 0).sort((a, b) => b.score - a.score || a.name.localeCompare(b.name));
27
+ // An unfamiliar brief still exposes the catalog and its pagination; it never asks the user to route it.
28
+ const fallback = terms.length > 0 && ranked.length === 0;
29
+ const candidates = fallback ? documents.map(doc => ({ ...doc.entry, matched: [], sections: [], score: 0 })) : ranked;
30
+ // Browsing, or a brief with no keyword match, returns the whole catalog unless the caller pages it.
31
+ const size = limit ?? (terms.length && !fallback ? 8 : candidates.length);
32
+ const page = candidates.slice(offset, offset + size).map(({ score: _score, ...entry }) => entry);
33
+ // Keyword ranking only sees shared words, so the first page of a search also carries
34
+ // every other guide's description: the model chooses by the brief, as with native skills.
35
+ const shown = new Set(page.map(entry => entry.name));
36
+ const rest = terms.length && !fallback && offset === 0 ? entries.filter(entry => !shown.has(entry.name)) : [];
37
+ return { query: query ?? null, fallback, total: candidates.length, offset, nextOffset: offset + page.length < candidates.length ? offset + page.length : null, guides: page, rest };
38
+ }
39
+ //# sourceMappingURL=guide-discovery.js.map
@@ -1,7 +1,7 @@
1
1
  import { craftCard } from './craft-cards.js';
2
2
  /** Parse headings outside code fences; maintainer comments never reach agents. */
3
3
  export function guideSections(content) {
4
- const clean = content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, '').replace(/<!--[\s\S]*?-->/g, '').trim();
4
+ const clean = content.replace(/<!--[\s\S]*?-->/g, '').trimStart().replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, '').trim();
5
5
  const sections = [];
6
6
  let current = { title: 'Overview', body: '' };
7
7
  let fenced = false;
@@ -35,9 +35,9 @@ export interface DurationCapability {
35
35
  mode: 'continuous' | 'discrete';
36
36
  /** For discrete mode: the exact allowed durations. */
37
37
  values?: number[];
38
- /** Resolution-dependent narrowing (Veo forces 8s at 1080p AND 4k). */
38
+ /** Resolution-dependent narrowing (retired Veo forced 8s at 1080p AND 4k). */
39
39
  resolutionOverrides?: Record<string, Pick<DurationCapability, 'min' | 'max' | 'mode' | 'values'>>;
40
- /** Prompt-mode narrowing (Veo's reference-to-video endpoint is 8s only). */
40
+ /** Prompt-mode narrowing (retired Veo's reference-to-video endpoint was 8s only). */
41
41
  modeOverrides?: Record<string, Pick<DurationCapability, 'min' | 'max' | 'mode' | 'values'>>;
42
42
  }
43
43
  /** Video resolution constraints. */
@@ -188,15 +188,14 @@ export interface ModelCapability {
188
188
  voiceClone?: VoiceCloneCapability;
189
189
  }
190
190
  /**
191
- * The provider every AGENT generation actually lands on for Kling and Veo.
191
+ * The provider every AGENT generation actually lands on for Kling.
192
192
  *
193
193
  * 🚨 THIS IS WHY `providerAspectRatios` MATTERS TO THE OP. MCP/CLI/Studio-Agent
194
194
  * generations are credits-only (BYOK is retired on the agent surface), and the
195
- * credits route carries Kling and Veo on fal: `slate/src/main/agent/routes.ts`
196
- * never sends `klingProvider`, so `handlers/video.ts` defaults it to `'fal'`,
197
- * and `generateVeoVideo`'s proxy arm builds a fal request
198
- * (`buildFalVeoRequest`). So an agent gets Kling's THREE fal ratios and Veo's
199
- * TWO — not the eight and ten those models take on their direct APIs. Validating
195
+ * credits route carries Kling on fal: `slate/src/main/agent/routes.ts`
196
+ * never sends `klingProvider`, so `handlers/video.ts` defaults it to `'fal'`.
197
+ * So an agent gets Kling's THREE fal ratios, not the eight it takes on its
198
+ * direct API (retired Veo had the same split, two against ten). Validating
200
199
  * against the direct sets would accept a ratio fal rejects, which is the exact
201
200
  * failure this module exists to delete.
202
201
  */
@@ -257,7 +256,7 @@ export declare function checkDuration(model: string, duration: number | undefine
257
256
  export declare function describeAspectRatios(models: readonly string[], provider?: string): string;
258
257
  /** e.g. "seedance-2: 480p, 720p, 1080p, 4k (default 1080p) · omni-flash: 720p only (fixed)" */
259
258
  export declare function describeVideoResolutions(models: readonly string[]): string;
260
- /** e.g. "kling-v3.0-std: 3-15s · veo-3.1-fast: 4s/6s/8s (1080p/4k: 8s only; with reference images: 8s only)" */
259
+ /** e.g. "kling-v3.0-std: 3-15s · ltx-2-5: 6s/8s/10s/12s/14s/16s/18s/20s (1440p/4k: 6s/8s/10s only)" */
261
260
  export declare function describeDurations(models: readonly string[]): string;
262
261
  /** e.g. "seedance-2: 9 · seedance-2.5: 30 · omni-flash: 7 · seedance-2.5-edit: 0 (prompt + source clip only)" */
263
262
  export declare function describeReferenceImageCaps(models: readonly string[]): string;
@@ -67,8 +67,6 @@ const KLING_DIRECT_ASPECT_RATIOS = [
67
67
  ];
68
68
  /** Kling carried on fal: three. This is the set the CREDITS route uses. */
69
69
  const KLING_FAL_ASPECT_RATIOS = ['16:9', '9:16', '1:1'];
70
- /** Veo carried on fal: two. The credits route again — Veo direct takes all ten. */
71
- const VEO_FAL_ASPECT_RATIOS = ['16:9', '9:16'];
72
70
  /** Gemini Omni Flash (fal schema, 16:9 default): two. */
73
71
  const OMNI_FLASH_ASPECT_RATIOS = ['16:9', '9:16'];
74
72
  /** Seedance (both seats, and the edit row): six — notably NO `4:5`. */
@@ -91,7 +89,7 @@ const MINIMAX_H3_ASPECT_RATIOS = ['21:9', '16:9', '4:3', '1:1', '3:4', '9:16'];
91
89
  /**
92
90
  * LTX-2.5, all four integrated endpoints: TWO. Read off fal's live OpenAPI
93
91
  * 2026-08-29 — `text-to-video/{fast,pro}` declare exactly `['16:9','9:16']`,
94
- * the narrowest video set in the roster alongside Veo-on-fal.
92
+ * the narrowest video set in the roster.
95
93
  *
96
94
  * `image-to-video/{fast,pro}` additionally offer `auto` (follow the start
97
95
  * frame). We never send it and it is not an `AspectRatio` in this vocabulary —
@@ -201,15 +199,14 @@ export function falImageSize(model, aspectRatio, resolution) {
201
199
  return computeFalDimensions(ar, 4);
202
200
  }
203
201
  /**
204
- * The provider every AGENT generation actually lands on for Kling and Veo.
202
+ * The provider every AGENT generation actually lands on for Kling.
205
203
  *
206
204
  * 🚨 THIS IS WHY `providerAspectRatios` MATTERS TO THE OP. MCP/CLI/Studio-Agent
207
205
  * generations are credits-only (BYOK is retired on the agent surface), and the
208
- * credits route carries Kling and Veo on fal: `slate/src/main/agent/routes.ts`
209
- * never sends `klingProvider`, so `handlers/video.ts` defaults it to `'fal'`,
210
- * and `generateVeoVideo`'s proxy arm builds a fal request
211
- * (`buildFalVeoRequest`). So an agent gets Kling's THREE fal ratios and Veo's
212
- * TWO — not the eight and ten those models take on their direct APIs. Validating
206
+ * credits route carries Kling on fal: `slate/src/main/agent/routes.ts`
207
+ * never sends `klingProvider`, so `handlers/video.ts` defaults it to `'fal'`.
208
+ * So an agent gets Kling's THREE fal ratios, not the eight it takes on its
209
+ * direct API (retired Veo had the same split, two against ten). Validating
213
210
  * against the direct sets would accept a ratio fal rejects, which is the exact
214
211
  * failure this module exists to delete.
215
212
  */
@@ -366,43 +363,6 @@ export const MODEL_CAPABILITIES = {
366
363
  duration: { min: 3, max: 10, mode: 'continuous' },
367
364
  maxIngredientImages: 0,
368
365
  },
369
- // ── Veo ────────────────────────────────────────────────────────────────────
370
- 'veo-3.1-fast': {
371
- aspectRatios: FULL_ASPECT_RATIOS,
372
- providerAspectRatios: { fal: VEO_FAL_ASPECT_RATIOS },
373
- videoResolution: { options: ['720p', '1080p', '4k'] },
374
- duration: {
375
- min: 4, max: 8, mode: 'discrete',
376
- values: [4, 6, 8],
377
- // BOTH 1080p and 4k force 8s. The op said "4K only at 8s" and quoted 4s
378
- // at 1080p, which the provider rejects.
379
- resolutionOverrides: {
380
- '1080p': { min: 8, max: 8, mode: 'discrete', values: [8] },
381
- '4k': { min: 8, max: 8, mode: 'discrete', values: [8] },
382
- },
383
- modeOverrides: {
384
- ingredients: { min: 8, max: 8, mode: 'discrete', values: [8] },
385
- },
386
- },
387
- maxIngredientImages: 3,
388
- },
389
- 'veo-3.1-standard': {
390
- aspectRatios: FULL_ASPECT_RATIOS,
391
- providerAspectRatios: { fal: VEO_FAL_ASPECT_RATIOS },
392
- videoResolution: { options: ['720p', '1080p', '4k'] },
393
- duration: {
394
- min: 4, max: 8, mode: 'discrete',
395
- values: [4, 6, 8],
396
- resolutionOverrides: {
397
- '1080p': { min: 8, max: 8, mode: 'discrete', values: [8] },
398
- '4k': { min: 8, max: 8, mode: 'discrete', values: [8] },
399
- },
400
- modeOverrides: {
401
- ingredients: { min: 8, max: 8, mode: 'discrete', values: [8] },
402
- },
403
- },
404
- maxIngredientImages: 3,
405
- },
406
366
  // ── Seedance ───────────────────────────────────────────────────────────────
407
367
  'seedance-2': {
408
368
  aspectRatios: SEEDANCE_ASPECT_RATIOS,
@@ -933,7 +893,7 @@ export function describeVideoResolutions(models) {
933
893
  function fmtWindow(d) {
934
894
  return d.mode === 'discrete' && d.values ? `${d.values.join('s/')}s` : `${d.min}-${d.max}s`;
935
895
  }
936
- /** e.g. "kling-v3.0-std: 3-15s · veo-3.1-fast: 4s/6s/8s (1080p/4k: 8s only; with reference images: 8s only)" */
896
+ /** e.g. "kling-v3.0-std: 3-15s · ltx-2-5: 6s/8s/10s/12s/14s/16s/18s/20s (1440p/4k: 6s/8s/10s only)" */
937
897
  export function describeDurations(models) {
938
898
  return groupBy(models, (m) => {
939
899
  const d = MODEL_CAPABILITIES[m]?.duration;
@@ -963,10 +923,10 @@ export function describeReferenceImageCaps(models) {
963
923
  const cap = MODEL_CAPABILITIES[m];
964
924
  if (!cap)
965
925
  return '';
966
- const n = cap.maxIngredientImages ?? cap.maxRefImages;
967
- if (n == null)
968
- return '';
969
- return n === 0 ? '0 (prompt + source clip only)' : String(n);
926
+ // A generate row with no reference cap (LTX) takes none: say so rather than
927
+ // dropping the row, which read as "no limit stated".
928
+ const n = cap.maxIngredientImages ?? cap.maxRefImages ?? 0;
929
+ return n === 0 ? '0 (prompt and frames only)' : String(n);
970
930
  });
971
931
  }
972
932
  /** H3 Max reference accounting, fal's worked tables read 2026-09-09.
@@ -112,12 +112,12 @@ export declare function seedanceTaskIntentWords(prompt: string): string[];
112
112
  export declare function multimodalRefModels(): string[];
113
113
  export declare const MODEL_FACTS: ModelFact[];
114
114
  /**
115
- * The one `default` seat for a kind on the generate route, READ from `tier`.
115
+ * The one `default` seat for a kind and route, READ from `tier`.
116
116
  * Anything that needs "the default image model" calls this instead of naming
117
117
  * one: the op surface and slates-web both hand-typed nano-banana-2 for six days
118
118
  * after the app picker moved to Sunburst.
119
119
  */
120
- export declare function defaultModelFor(kind: ModelFact['kind']): string;
120
+ export declare function defaultModelFor(kind: ModelFact['kind'], route?: ModelFact['route']): string;
121
121
  /**
122
122
  * The seat a built-in tool renders on when its caller names no model. Resolves
123
123
  * `TOOL_SEAT` (generation-policy.ts, THE home): `'default-image'` follows the
@@ -49,11 +49,10 @@ export function composeChatGptFraming(prompt, aspectRatio) {
49
49
  * Reference caps for a fact, read out of the capability SSOT.
50
50
  *
51
51
  * The argument is a `MODEL_CAPABILITIES` key, which is a REGISTRY model id — and
52
- * three facts here are FAMILY-level (`kling-v3`, `kling-v3-edit`, `veo-3.1`)
52
+ * two facts here are FAMILY-level (`kling-v3`, `kling-v3-edit`)
53
53
  * with no registry row of their own, so they name a representative variant.
54
54
  * That is safe because the caps are identical across the variants in each
55
- * family (std/pro/omni/omni-pro all take 4; both edit rows take 4; both Veo
56
- * seats take 3) — and if a future variant diverges, this becomes a wrong number
55
+ * family (std/pro/omni/omni-pro all take 4; both edit rows take 4) — and if a future variant diverges, this becomes a wrong number
57
56
  * rather than a crash, so split the fact rather than picking a side.
58
57
  *
59
58
  * Throws on an unknown id: a typo must fail the build, not ship as `null` caps
@@ -201,7 +200,7 @@ export const MODEL_FACTS = [
201
200
  label: 'Seedream 5 Lite',
202
201
  kind: 'image',
203
202
  ...caps('seedream-5-lite'),
204
- notes: 'CHEAPEST image seat, flat-priced. Less censored. Routes to its edit endpoint when references are present.',
203
+ notes: 'Cheapest flat-priced image seat (GPT Image 2.5 at low quality costs less per image). Less censored. Routes to its edit endpoint when references are present.',
205
204
  },
206
205
  {
207
206
  id: 'seedance-2',
@@ -232,7 +231,7 @@ export const MODEL_FACTS = [
232
231
  kind: 'video',
233
232
  // 0 ingredients: prompt + source clip only on slates_edit_video.
234
233
  ...caps('seedance-2.5-edit'),
235
- notes: 'VIDEO-TO-VIDEO EDIT via slates_edit_video, and the only edit engine that takes a clip longer than the other two reach — that length is the whole reason to route here. Inside their range, compare on fidelity instead: Omni Flash edit won the prompt-only head-to-head, and Kling edit is the one that takes reference images. Edits audio on the same row (re-voice, re-accent, translate with re-fitted lips, replace BGM). Costs roughly double a plain 2.5 generation of the same length, because an edit bills input plus output seconds.',
234
+ notes: 'VIDEO-TO-VIDEO EDIT via slates_edit_video, and the only edit engine that takes a clip longer than the other two reach — that length is the whole reason to route here. Inside their range, compare on fidelity instead: Omni Flash edit won the prompt-only head-to-head, and Kling edit is the one that takes reference images. Edits audio on the same row (re-voice, re-accent, translate with re-fitted lips, replace BGM). Costs about 1.2x a plain 2.5 generation of the same length: an edit bills at twice the reduced video-reference rate.',
236
235
  },
237
236
  {
238
237
  id: 'kling-v3',
@@ -242,7 +241,7 @@ export const MODEL_FACTS = [
242
241
  kind: 'video',
243
242
  // Family-level fact — caps are identical across std/pro/omni/omni-pro.
244
243
  ...caps('kling-v3.0-std'),
245
- notes: 'THE COST-EFFECTIVE SEAT — strong start-frame adherence (identity, layout, text), acting, dialogue, lip-sync and the widest aspect-ratio set; pick it when the budget matters and the shot is a performance or a start-frame animation. Kling is also the ONLY engine behind the Motion Transfer and Lip Sync tools.',
244
+ notes: 'THE COST-EFFECTIVE SEAT — strong start-frame adherence (identity, layout, text), acting, dialogue and lip-sync; pick it when the budget matters and the shot is a performance or a start-frame animation. Kling is also the ONLY engine behind the Motion Transfer and Lip Sync tools.',
246
245
  },
247
246
  {
248
247
  id: 'kling-v3-edit',
@@ -252,17 +251,7 @@ export const MODEL_FACTS = [
252
251
  kind: 'video',
253
252
  // Family-level fact; 4 = combined subject elements + style refs per edit.
254
253
  ...caps('kling-v3.0-omni-edit'),
255
- notes: 'VIDEO-TO-VIDEO EDIT, the REF-DRIVEN one: it is the only edit seat that takes element/style reference images to lock subject identity, and its keep_audio preserves the original audio verbatim. Route here when an edit NEEDS reference images or bit-exact audio; for prompt-only footage-synced VFX, omni-flash-edit won the fidelity head-to-head. One instruction beat per pass — multi-beat prompts get under-executed.',
256
- },
257
- {
258
- id: 'veo-3.1',
259
- route: 'generate',
260
- tier: 'niche',
261
- label: 'Veo 3.1',
262
- kind: 'video',
263
- // Family-level fact — fast and standard declare the same caps.
264
- ...caps('veo-3.1-fast'),
265
- notes: 'NICHE, never the default — pick only when native synchronized audio must generate WITH the video in one pass, and the narrowest aspect-ratio and duration sets in the catalogue are acceptable. Otherwise Seedance 2.5 (the default) or Kling (cost-effective performance) win.',
254
+ notes: 'VIDEO-TO-VIDEO EDIT, the REF-DRIVEN one: it is the only edit seat that takes element/style reference images to lock subject identity, and its keepAudio preserves the original audio verbatim. Route here when an edit NEEDS reference images or bit-exact audio; for prompt-only footage-synced VFX, omni-flash-edit won the fidelity head-to-head. One instruction beat per pass — multi-beat prompts get under-executed.',
266
255
  },
267
256
  {
268
257
  id: 'omni-flash',
@@ -272,7 +261,7 @@ export const MODEL_FACTS = [
272
261
  kind: 'video',
273
262
  // 7 ref2v image_urls — mirrors Google's own reference limit.
274
263
  ...caps('omni-flash'),
275
- notes: 'CHEAP tier with native synced audio included. Route here for cheap drafts, audio-in-one-pass at low cost, and reference-to-video character-consistency trials. VIDEO-ONLY. Quality against Kling/Seedance is unproven — do not route hero shots here.',
264
+ notes: '720p seat with native synced audio included. Route here for drafts with sound in one pass and reference-to-video character-consistency trials; LTX, H3 and H3 Max Turbo cost less per second. VIDEO-ONLY. Quality against Kling/Seedance is unproven — do not route hero shots here.',
276
265
  },
277
266
  {
278
267
  id: 'omni-flash-edit',
@@ -282,7 +271,7 @@ export const MODEL_FACTS = [
282
271
  kind: 'video',
283
272
  // 0: prompt + source clip ONLY — no element/style refs on this endpoint.
284
273
  ...caps('omni-flash-edit'),
285
- notes: 'VIDEO-TO-VIDEO EDIT, prompt-only — THE EDIT-FIDELITY WINNER (head-to-head vs Kling edit on real talking footage: lips held, audio near-identical, both action beats landed) and the cheapest edit seat. Footage-synced prop, effect, environment and lighting swaps. Takes NO reference images — identity swaps needing refs go to Kling edit. Fidelity is EARNED by prompt discipline; the exact form is in slates-prompting-omni-flash.',
274
+ notes: 'VIDEO-TO-VIDEO EDIT, prompt-only — THE EDIT-FIDELITY WINNER (head-to-head vs Kling edit on real talking footage: lips held, audio near-identical, both action beats landed), priced level with Kling O3 Edit Standard. Footage-synced prop, effect, environment and lighting swaps. Takes NO reference images — identity swaps needing refs go to Kling edit. Fidelity is EARNED by prompt discipline; the exact form is in slates-prompting-omni-flash.',
286
275
  },
287
276
  {
288
277
  id: 'minimax-h3',
@@ -295,7 +284,7 @@ export const MODEL_FACTS = [
295
284
  // be the only reference input; provide at least one reference image or
296
285
  // video with it." Same behavioural rule as Seedance 2.0.
297
286
  audioRefNeedsCompanion: true,
298
- notes: 'THE AUTHORED-AUDIO SEAT — reach for H3 when the sound is part of the shot rather than a switch on it: synchronised dialogue, scene sound and an audience-only score directed as three separate layers in ONE pass, across eleven languages. Kling and Seedance treat audio as on/off; Veo generates it but gives you no way to direct the layers. Only H3 also carries a DECLARED REFERENCE RELATIONSHIP (kept whole, partly kept, transferred, or a loose echo). VIDEO-ONLY. Its top two resolution tiers are UPSCALES of the native render, not larger generations — judge at native and upscale in post. Reference images past the fifth are a PAID key dimension: pass referenceImages when quoting.',
287
+ notes: 'THE AUTHORED-AUDIO SEAT — reach for H3 when the sound is part of the shot rather than a switch on it: synchronised dialogue, scene sound and an audience-only score directed as three separate layers in ONE pass, across eleven languages. Kling and Seedance treat audio as on/off. Only H3 also carries a DECLARED REFERENCE RELATIONSHIP (kept whole, partly kept, transferred, or a loose echo). VIDEO-ONLY. Its top two resolution tiers are UPSCALES of the native render, not larger generations — judge at native and upscale in post. Reference images past the fifth are a PAID key dimension: pass referenceImages when quoting.',
299
288
  },
300
289
  {
301
290
  id: 'minimax-h3-max',
@@ -312,7 +301,7 @@ export const MODEL_FACTS = [
312
301
  // from the base row: "Audio cannot be the only reference input; provide at
313
302
  // least one reference image or video with it."
314
303
  audioRefNeedsCompanion: true,
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.',
304
+ notes: 'THE SPEED SEAT, dearer than base H3 at 768p and equal at 480p — 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, the same as base H3: frames and references go to different endpoints. 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
305
  },
317
306
  {
318
307
  id: 'minimax-h3-max-turbo',
@@ -336,7 +325,7 @@ export const MODEL_FACTS = [
336
325
  // and no reference endpoint at all, so `caps()` returns nulls and the
337
326
  // composer refuses references. Start/end FRAMES are unaffected.
338
327
  ...caps('ltx-2-5'),
339
- notes: 'THE VOLUME SEAT — the cheapest native 1080p second in the catalogue, and the row for MANY takes rather than one hero shot. Native synced audio is included free at every tier, unlike Kling where sound is a paid key dimension. It also reaches the highest resolution tier below 4K and makes the LONGEST clips in the catalogue. VIDEO-ONLY. INPUTS ARE FRAMES, NOT REFERENCES: start frame plus an optional end frame, and no reference endpoint at all — for character consistency across shots use H3 or Kling. Route here for batch coverage, long takes, and anything where the credit budget is the binding constraint.',
328
+ notes: 'THE VOLUME SEAT — the cheapest 1080p second with sound included, and the row for MANY takes rather than one hero shot. Native synced audio is included free at every tier, unlike Kling where sound is a paid key dimension. It supports native high-resolution output and longer takes than most seats; use the capability surface for its resolution-dependent duration limits. VIDEO-ONLY. INPUTS ARE FRAMES, NOT REFERENCES: start frame plus an optional end frame, and no reference endpoint at all — for character consistency across shots use H3 or Kling. Route here for batch coverage, long takes, and anything where the credit budget is the binding constraint.',
340
329
  },
341
330
  {
342
331
  id: 'ltx-2-5-pro',
@@ -394,15 +383,15 @@ for (const kind of ['image', 'video', 'audio']) {
394
383
  }
395
384
  }
396
385
  /**
397
- * The one `default` seat for a kind on the generate route, READ from `tier`.
386
+ * The one `default` seat for a kind and route, READ from `tier`.
398
387
  * Anything that needs "the default image model" calls this instead of naming
399
388
  * one: the op surface and slates-web both hand-typed nano-banana-2 for six days
400
389
  * after the app picker moved to Sunburst.
401
390
  */
402
- export function defaultModelFor(kind) {
403
- const fact = MODEL_FACTS.find((f) => f.kind === kind && f.route === 'generate' && f.tier === 'default');
391
+ export function defaultModelFor(kind, route = 'generate') {
392
+ const fact = MODEL_FACTS.find((f) => f.kind === kind && f.route === route && f.tier === 'default');
404
393
  if (!fact)
405
- throw new Error(`MODEL_FACTS: no default ${kind} seat on the generate route`);
394
+ throw new Error(`MODEL_FACTS: no default ${kind} seat on the ${route} route`);
406
395
  return fact.id;
407
396
  }
408
397
  /**