@slatesvideo/shared 0.6.2 → 0.6.4

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 (77) hide show
  1. package/dist/api-url.d.ts +9 -0
  2. package/dist/api-url.js +9 -0
  3. package/dist/auth.d.ts +13 -1
  4. package/dist/auth.js +9 -5
  5. package/dist/clients/cloud.d.ts +3 -0
  6. package/dist/clients/cloud.js +34 -3
  7. package/dist/clients/desktop.js +3 -0
  8. package/dist/index.d.ts +8 -2
  9. package/dist/index.js +44 -1
  10. package/dist/operations/index.d.ts +243 -31
  11. package/dist/operations/index.js +1483 -154
  12. package/dist/operations/surface.d.ts +69 -0
  13. package/dist/operations/surface.js +227 -0
  14. package/dist/prompts/agent-doctrine.d.ts +36 -0
  15. package/dist/prompts/agent-doctrine.js +201 -0
  16. package/dist/prompts/asset-label.d.ts +23 -0
  17. package/dist/prompts/asset-label.js +70 -0
  18. package/dist/prompts/banned-tokens.d.ts +40 -0
  19. package/dist/prompts/banned-tokens.js +219 -0
  20. package/dist/prompts/character-sheet.d.ts +0 -2
  21. package/dist/prompts/character-sheet.js +0 -2
  22. package/dist/prompts/craft-cards.d.ts +20 -0
  23. package/dist/prompts/craft-cards.js +82 -0
  24. package/dist/prompts/environment-sheet.js +16 -0
  25. package/dist/prompts/index.d.ts +1 -0
  26. package/dist/prompts/index.js +4 -0
  27. package/dist/prompts/model-capabilities.d.ts +65 -1
  28. package/dist/prompts/model-capabilities.js +139 -2
  29. package/dist/prompts/model-facts.d.ts +20 -4
  30. package/dist/prompts/model-facts.js +95 -27
  31. package/dist/prompts/partials.generated.js +2 -1
  32. package/dist/prompts/prompting-tips.d.ts +1 -1
  33. package/dist/prompts/prompting-tips.js +123 -0
  34. package/dist/prompts/reference-composer.d.ts +36 -7
  35. package/dist/prompts/reference-composer.js +75 -20
  36. package/dist/prompts/reference-rules.d.ts +15 -26
  37. package/dist/prompts/reference-rules.js +15 -93
  38. package/dist/prompts/shot-grammar.d.ts +154 -0
  39. package/dist/prompts/shot-grammar.js +184 -0
  40. package/dist/prompts/shot-spec.d.ts +265 -0
  41. package/dist/prompts/shot-spec.js +303 -0
  42. package/dist/skills/content.js +25 -22
  43. package/exports/slates-prompt-builder/generated/SKILL.md +3 -3
  44. package/exports/slates-prompt-builder/generated/reference-content-policy.md +6 -0
  45. package/exports/slates-prompt-builder/generated/reference-kling.md +22 -0
  46. package/exports/slates-prompt-builder/generated/reference-nano-banana.md +17 -0
  47. package/exports/slates-prompt-builder/generated/reference-seedance.md +19 -1
  48. package/exports/slates-prompt-builder/generated/slates-prompt-builder-manifest.json +17 -17
  49. package/exports/slates-prompt-builder/generated/slates-prompt-builder.skill +0 -0
  50. package/package.json +83 -73
  51. package/skills/_partials/decision-log.md +5 -4
  52. package/skills/_partials/thresholds.md +19 -0
  53. package/skills/slates-content-policy.md +15 -1
  54. package/skills/slates-cost-discipline.md +26 -4
  55. package/skills/slates-model-selection.md +2 -2
  56. package/skills/slates-one-prompt-film.md +20 -12
  57. package/skills/slates-project-organization.md +1 -1
  58. package/skills/slates-prompting-elevenlabs.md +61 -2
  59. package/skills/slates-prompting-flux-2-max.md +39 -0
  60. package/skills/slates-prompting-gpt-image-2.md +109 -70
  61. package/skills/slates-prompting-inworld-tts.md +166 -0
  62. package/skills/slates-prompting-kling-v3.md +39 -0
  63. package/skills/slates-prompting-lip-sync.md +38 -0
  64. package/skills/slates-prompting-ltx-2-5.md +218 -0
  65. package/skills/slates-prompting-minimax-h3.md +39 -0
  66. package/skills/slates-prompting-motion-transfer.md +38 -0
  67. package/skills/slates-prompting-nano-banana-2.md +36 -0
  68. package/skills/slates-prompting-omni-flash.md +41 -0
  69. package/skills/slates-prompting-seed-audio.md +38 -0
  70. package/skills/slates-prompting-seedance-2-5.md +38 -0
  71. package/skills/slates-prompting-seedance.md +36 -1
  72. package/skills/slates-prompting-seedream-5-lite.md +38 -0
  73. package/skills/slates-prompting-veo-3.md +39 -0
  74. package/skills/slates-shot-variety.md +53 -0
  75. package/skills/slates-storyboard-from-script.md +31 -15
  76. package/skills/slates-style-prompting.md +1 -1
  77. package/skills/slates-vision-feedback-loop.md +1 -1
@@ -0,0 +1,40 @@
1
+ /** Which generate op a list applies to. */
2
+ export type BannedTokenScope = 'image' | 'video';
3
+ export interface BannedToken {
4
+ /** The literal phrase, verbatim from the skill. */
5
+ token: string;
6
+ /** Skill file it was read from — cited in every warning. */
7
+ skill: string;
8
+ scope: BannedTokenScope;
9
+ }
10
+ /** Every banned token, in skill-document order. Integrity asserted at load. */
11
+ export declare const BANNED_PROMPT_TOKENS: readonly BannedToken[];
12
+ export declare function bannedTokensFor(scope: BannedTokenScope): readonly BannedToken[];
13
+ /** The never-use list a single skill declares. Empty for a skill with no block. */
14
+ export declare function bannedTokensForSkill(skill: string): readonly BannedToken[];
15
+ /**
16
+ * A single model's never-use list, for the estimate RESULT.
17
+ *
18
+ * Deliberately NOT the cross-model list — that one already rides the op
19
+ * description on every call. This is the half that could not: the quirk that is
20
+ * true of Veo and false of Kling.
21
+ */
22
+ export declare function describeBannedTokensForSkill(skill: string): string;
23
+ /** The tokens a submitted prompt actually contains. `skill` adds that model's
24
+ * own list to the modality-wide one — the two overlap for nano-banana-2 and
25
+ * seedance, so hits are deduplicated by token. */
26
+ export declare function findBannedTokens(prompt: string, scope: BannedTokenScope, skill?: string): BannedToken[];
27
+ /**
28
+ * The list as it appears INSIDE an op description — always in context, on both
29
+ * surfaces, with no call required to see it. Generated, never hand-typed.
30
+ */
31
+ export declare function describeBannedTokens(scope: BannedTokenScope): string;
32
+ /**
33
+ * Non-blocking warning for a submitted prompt. Empty string when clean.
34
+ *
35
+ * Returned in the op RESULT — the one place the agent cannot avoid reading —
36
+ * rather than raised as an error. The generation proceeds either way: the
37
+ * sandbox doctrine says make state visible, never block.
38
+ */
39
+ export declare function bannedTokenWarning(prompt: string, scope: BannedTokenScope, skill?: string): string;
40
+ //# sourceMappingURL=banned-tokens.d.ts.map
@@ -0,0 +1,219 @@
1
+ // ============================================================
2
+ // BANNED PROMPT TOKENS — the "load the guide" rule, made structural.
3
+ //
4
+ // THE PROBLEM: "before prompting any model, load the matching guide" is a
5
+ // sentence in a system prompt with nothing checking it. Measured 2026-08-30 in
6
+ // a real Studio Agent session: slates_get_prompting_guide was called ZERO
7
+ // times, and the prompt that shipped tripped the skill's own never-use list
8
+ // twice (`photorealistic`, `cinematic`). A rule with no check is a suggestion,
9
+ // and an LLM is the least reliable enforcer you could pick.
10
+ //
11
+ // THE FIX, in two halves, neither of which the model can skip:
12
+ // (a) the never-use list is INLINED into the generate ops' descriptions.
13
+ // Op descriptions are always in context on BOTH surfaces — there is no
14
+ // call to omit and no discretion to exercise.
15
+ // (b) the submitted prompt is MATCHED against the list and a warning comes
16
+ // back in the op result. Non-blocking: the generation proceeds
17
+ // (PRODUCT_PHILOSOPHY.md → make state visible, never block).
18
+ //
19
+ // 🚨 THE LIST IS NEVER HAND-TYPED HERE. It is extracted from the skill files
20
+ // themselves, between `<!-- @banned:start -->` / `<!-- @banned:end -->`
21
+ // markers. That is this workspace's LLM-docs doctrine — never hand-type a fact
22
+ // an LLM will read — and it is the only way the op description and the skill
23
+ // cannot drift apart. Change the skill; the description follows on the next
24
+ // build. scripts/agent-surface-lockstep-check.mjs fails the build if a token
25
+ // in a description no longer appears in its source skill.
26
+ //
27
+ // Deterministic by construction (document order, no sorting, no dedupe
28
+ // reshuffle) because these strings land in the desktop's prompt-cached tool
29
+ // prefix, whose byte-stability IS the cache mechanism.
30
+ // ============================================================
31
+ import { SKILLS } from '../skills/content.js';
32
+ /**
33
+ * Where each scope's CROSS-MODEL list lives.
34
+ *
35
+ * One skill per scope on purpose: these are the two lists that are GENERIC to
36
+ * their modality (Stable-Diffusion-era tag soup for images, quality
37
+ * incantations for video), not model-specific quirks. A per-model list cannot
38
+ * ride the op DESCRIPTION — a description is one static string for every call,
39
+ * so it cannot change with the `model` argument. That is what
40
+ * `bannedTokensForSkill` below is for: the per-model list rides the estimate
41
+ * RESULT, where the model has just been named.
42
+ */
43
+ const BANNED_TOKEN_SOURCES = [
44
+ { skill: 'slates-prompting-nano-banana-2', scope: 'image' },
45
+ { skill: 'slates-prompting-seedance', scope: 'video' },
46
+ ];
47
+ /**
48
+ * 🚨 THE ENFORCEMENT THAT WORKED COVERED TWO SKILLS OF FIFTEEN.
49
+ *
50
+ * `describeBannedTokens('image')` was Nano Banana's list and `('video')` was
51
+ * Seedance's, so a Veo, Kling, LTX, MiniMax, FLUX, Seedream, GPT-Image or audio
52
+ * generation was matched against another model's never-use list and its own was
53
+ * enforced by nothing — while four skills (content-policy, lip-sync,
54
+ * minimax-h3, motion-transfer) carried never-use prose with no markers at all,
55
+ * which is a rule an LLM has to notice.
56
+ *
57
+ * Every skill that carries an `@banned` block now contributes to a per-skill
58
+ * list, delivered on the estimate result beside the craft card. The two above
59
+ * stay ALSO on the op descriptions, because a modality-wide list is true of
60
+ * every call that op can make.
61
+ */
62
+ function extractPerSkill() {
63
+ const out = new Map();
64
+ for (const [skill, content] of Object.entries(SKILLS)) {
65
+ // Cheap pre-test: only pay the regex for files that carry the marker.
66
+ if (!content.includes('@banned:start'))
67
+ continue;
68
+ const scope = inferScope(skill);
69
+ out.set(skill, extractFromSkill(skill).map((token) => ({ token, skill, scope })));
70
+ }
71
+ return out;
72
+ }
73
+ /** Image-lane skills prompt for pixels; everything else is a time-based lane.
74
+ * Only used to tag a token for the warning text — the per-skill list is
75
+ * matched by SKILL, never by scope, so a wrong guess here cannot mis-enforce. */
76
+ function inferScope(skill) {
77
+ return /nano-banana|gpt-image|flux|seedream/.test(skill) ? 'image' : 'video';
78
+ }
79
+ const FENCE_RE = /<!--\s*@banned:start\s*-->([\s\S]*?)<!--\s*@banned:end\s*-->/g;
80
+ const HTML_COMMENT_RE = /<!--[\s\S]*?-->/g;
81
+ const BACKTICKED_RE = /`([^`\n]+)`/g;
82
+ /**
83
+ * Pull every backticked phrase out of a skill's fenced block(s).
84
+ *
85
+ * HTML comments are stripped FIRST: the fence carries a "MACHINE-READ" note to
86
+ * whoever edits the skill next, and that note itself contains backticks.
87
+ */
88
+ function extractFromSkill(skill) {
89
+ const content = SKILLS[skill];
90
+ if (content === undefined) {
91
+ throw new Error(`[banned-tokens] no such skill: ${skill}. BANNED_TOKEN_SOURCES must name files in packages/shared/skills/.`);
92
+ }
93
+ const out = [];
94
+ let fence;
95
+ FENCE_RE.lastIndex = 0;
96
+ let fences = 0;
97
+ while ((fence = FENCE_RE.exec(content)) !== null) {
98
+ fences += 1;
99
+ const body = fence[1].replace(HTML_COMMENT_RE, '');
100
+ let m;
101
+ BACKTICKED_RE.lastIndex = 0;
102
+ while ((m = BACKTICKED_RE.exec(body)) !== null) {
103
+ const token = m[1].trim();
104
+ if (token && !out.includes(token))
105
+ out.push(token);
106
+ }
107
+ }
108
+ if (fences === 0) {
109
+ throw new Error(`[banned-tokens] ${skill}.md has no <!-- @banned:start --> / <!-- @banned:end --> block. ` +
110
+ `The op description and the prompt warnings are GENERATED from it — restore the markers, ` +
111
+ `or drop the skill from BANNED_TOKEN_SOURCES.`);
112
+ }
113
+ if (out.length === 0) {
114
+ throw new Error(`[banned-tokens] ${skill}.md has a @banned block with no backticked tokens in it. ` +
115
+ `An empty list would silently disable the check instead of failing it.`);
116
+ }
117
+ return out;
118
+ }
119
+ /** Every banned token, in skill-document order. Integrity asserted at load. */
120
+ export const BANNED_PROMPT_TOKENS = BANNED_TOKEN_SOURCES.flatMap(({ skill, scope }) => extractFromSkill(skill).map((token) => ({ token, skill, scope })));
121
+ const byScope = new Map();
122
+ for (const entry of BANNED_PROMPT_TOKENS) {
123
+ const list = byScope.get(entry.scope) ?? [];
124
+ list.push(entry);
125
+ byScope.set(entry.scope, list);
126
+ }
127
+ export function bannedTokensFor(scope) {
128
+ return byScope.get(scope) ?? [];
129
+ }
130
+ /** Regex cache — one word-boundary matcher per token, built once. */
131
+ const matchers = new Map();
132
+ function matcherFor(token) {
133
+ let re = matchers.get(token);
134
+ if (!re) {
135
+ const escaped = token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
136
+ // \b at both ends so `4k` does not fire inside "84king" and `flawless`
137
+ // does not fire inside "flawlessly". Every token starts and ends with a
138
+ // word character today; if one ever starts with punctuation, \b would
139
+ // anchor wrong — the load-time check below is what would catch it.
140
+ re = new RegExp(`\\b${escaped}\\b`, 'i');
141
+ matchers.set(token, re);
142
+ }
143
+ return re;
144
+ }
145
+ for (const { token, skill } of BANNED_PROMPT_TOKENS) {
146
+ if (!/^\w/.test(token) || !/\w$/.test(token)) {
147
+ throw new Error(`[banned-tokens] ${skill}: token ${JSON.stringify(token)} does not start and end with a word ` +
148
+ `character, so the \\b word-boundary match would never fire. Rewrite the entry or teach ` +
149
+ `matcherFor() the new shape.`);
150
+ }
151
+ }
152
+ /** Every per-skill list, keyed by skill name. */
153
+ const bySkill = extractPerSkill();
154
+ /** The never-use list a single skill declares. Empty for a skill with no block. */
155
+ export function bannedTokensForSkill(skill) {
156
+ return bySkill.get(skill) ?? [];
157
+ }
158
+ /**
159
+ * A single model's never-use list, for the estimate RESULT.
160
+ *
161
+ * Deliberately NOT the cross-model list — that one already rides the op
162
+ * description on every call. This is the half that could not: the quirk that is
163
+ * true of Veo and false of Kling.
164
+ */
165
+ export function describeBannedTokensForSkill(skill) {
166
+ const list = bannedTokensForSkill(skill);
167
+ if (list.length === 0)
168
+ return '';
169
+ return (`NEVER put these in a ${skill.replace('slates-prompting-', '')} prompt: ` +
170
+ list.map((b) => `"${b.token}"`).join(', ') +
171
+ `. Describe specifically instead (${skill}).`);
172
+ }
173
+ /** The tokens a submitted prompt actually contains. `skill` adds that model's
174
+ * own list to the modality-wide one — the two overlap for nano-banana-2 and
175
+ * seedance, so hits are deduplicated by token. */
176
+ export function findBannedTokens(prompt, scope, skill) {
177
+ const candidates = [...bannedTokensFor(scope), ...(skill ? bannedTokensForSkill(skill) : [])];
178
+ const seen = new Set();
179
+ const hits = [];
180
+ for (const b of candidates) {
181
+ if (seen.has(b.token))
182
+ continue;
183
+ seen.add(b.token);
184
+ if (matcherFor(b.token).test(prompt))
185
+ hits.push(b);
186
+ }
187
+ return hits;
188
+ }
189
+ /**
190
+ * The list as it appears INSIDE an op description — always in context, on both
191
+ * surfaces, with no call required to see it. Generated, never hand-typed.
192
+ */
193
+ export function describeBannedTokens(scope) {
194
+ const list = bannedTokensFor(scope);
195
+ if (list.length === 0)
196
+ return '';
197
+ const skills = [...new Set(list.map((b) => b.skill))].join(' / ');
198
+ return (`NEVER put these in a prompt (they measurably degrade output — full rationale in ${skills}): ` +
199
+ list.map((b) => `"${b.token}"`).join(', ') +
200
+ `. Describe specifically instead.`);
201
+ }
202
+ /**
203
+ * Non-blocking warning for a submitted prompt. Empty string when clean.
204
+ *
205
+ * Returned in the op RESULT — the one place the agent cannot avoid reading —
206
+ * rather than raised as an error. The generation proceeds either way: the
207
+ * sandbox doctrine says make state visible, never block.
208
+ */
209
+ export function bannedTokenWarning(prompt, scope, skill) {
210
+ const hits = findBannedTokens(prompt, scope, skill);
211
+ if (hits.length === 0)
212
+ return '';
213
+ const skills = [...new Set(hits.map((b) => b.skill))].join(', ');
214
+ return (`⚠️ PROMPT WARNING: your prompt contains ${hits.map((b) => `"${b.token}"`).join(', ')} — ` +
215
+ `on the never-use list in ${skills}. Not blocked, and this generation ran as submitted. ` +
216
+ `Load that guide with slates_get_prompting_guide and rewrite with specific description ` +
217
+ `(named lens, light direction, stock, composition) before the next generation.`);
218
+ }
219
+ //# sourceMappingURL=banned-tokens.js.map
@@ -13,8 +13,6 @@
13
13
  * removal and must be SCOPED TO THE FACE rather than to the whole body.
14
14
  */
15
15
  export declare const CHARACTER_SHEET_PANELS_DESC: string;
16
- /** Panel identifiers, in sheet order. */
17
- export declare const BODY_POSE_LABELS: readonly ["portrait", "front", "back"];
18
16
  /**
19
17
  * The character identity sheet — one asset, three panels.
20
18
  *
@@ -139,8 +139,6 @@ export const CHARACTER_SHEET_PANELS_DESC = 'a large chest-up portrait on the lef
139
139
  'a full-body front view in a relaxed A-pose in the centre, cropped at the collarbone — ' +
140
140
  'an invisible-mannequin presentation with just the face cropped out, ' +
141
141
  'and a full-body back view on the right with the head and hair fully visible';
142
- /** Panel identifiers, in sheet order. */
143
- export const BODY_POSE_LABELS = ['portrait', 'front', 'back'];
144
142
  // The sheet's style directive: a user transform REPLACES the inherit-source
145
143
  // instruction (so the model isn't told to both preserve the medium AND change
146
144
  // it); otherwise inherit the source medium.
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Hard ceiling per card, in characters.
3
+ *
4
+ * A card is a CARD. The largest skill is 5,736 words and loading it whole is
5
+ * exactly the cost this mechanism exists to avoid — if a card needs more than
6
+ * this, the extra belongs in the body of the skill, which is one
7
+ * `slates_get_prompting_guide` call away. Asserted at load, so an over-long
8
+ * card fails the build rather than quietly inflating every estimate result.
9
+ */
10
+ export declare const CRAFT_CARD_CEILING = 2400;
11
+ /** Every card, keyed by skill name. Built once at load; integrity asserted. */
12
+ export declare const CRAFT_CARDS: Readonly<Record<string, string>>;
13
+ /** The card for a skill, or null when that skill carries none. */
14
+ export declare function craftCard(skill: string): string | null;
15
+ /**
16
+ * The card as it appears in an op RESULT: the body, plus one line naming where
17
+ * the rest lives so the agent knows the card is a summary and not the guide.
18
+ */
19
+ export declare function describeCraftCard(skill: string): string;
20
+ //# sourceMappingURL=craft-cards.d.ts.map
@@ -0,0 +1,82 @@
1
+ // ============================================================
2
+ // CRAFT CARDS — the POSITIVE half of "load the guide", made structural.
3
+ //
4
+ // THE MEASUREMENT THIS ANSWERS (2026-08-30, 48 trials at k=8, same brain, same
5
+ // scorer). Inlining each model's NEVER-USE list into the generate ops'
6
+ // descriptions moved `no_banned_tokens` from 0/8 to 30/32 — 94%. Over the same
7
+ // runs, `guide_before_generate` was 13% before and 13% after: the agent still
8
+ // did not fetch the skill. So the skill's NEGATIVE half became enforced and its
9
+ // POSITIVE half — the named lens, the light direction, the stock, the
10
+ // composition, the levers that make a shot GOOD rather than merely UN-BAD —
11
+ // still only arrived if the model chose to go and get it, and it usually did
12
+ // not.
13
+ //
14
+ // The lesson generalised in the CLAUDE.md rules: put the FACT where it cannot
15
+ // be skipped, not a POINTER to the fact. A card is that fact for the positive
16
+ // half — 250-400 words of the levers that matter for ONE model, delivered
17
+ // where the agent is already looking.
18
+ //
19
+ // 🚨 WHERE IT IS DELIVERED, AND WHY THERE. On the RESULT of
20
+ // `slates_estimate_generation_cost`, which the doctrine already tells the agent
21
+ // to call before every generation. That placement costs ZERO prefix bytes — the
22
+ // desktop's cached tool prefix is unchanged — and it arrives at the one moment
23
+ // the model has just named the model it is about to use. Putting it in the
24
+ // `model` param description instead would have grown the largest op on the
25
+ // surface by 15 cards on every turn of every session, to be read once.
26
+ //
27
+ // 🚨 THE CARD IS NEVER WRITTEN HERE. It is EXTRACTED from the skill file
28
+ // itself, between `<!-- @card:start -->` / `<!-- @card:end -->` markers — the
29
+ // same mechanism, and the same reason, as banned-tokens.ts: a card authored
30
+ // downstream is a second copy of the skill that drifts from it. Edit the skill;
31
+ // the card follows on the next build.
32
+ // ============================================================
33
+ import { SKILLS } from '../skills/content.js';
34
+ /**
35
+ * Hard ceiling per card, in characters.
36
+ *
37
+ * A card is a CARD. The largest skill is 5,736 words and loading it whole is
38
+ * exactly the cost this mechanism exists to avoid — if a card needs more than
39
+ * this, the extra belongs in the body of the skill, which is one
40
+ * `slates_get_prompting_guide` call away. Asserted at load, so an over-long
41
+ * card fails the build rather than quietly inflating every estimate result.
42
+ */
43
+ export const CRAFT_CARD_CEILING = 2400;
44
+ const CARD_FENCE_RE = /<!--\s*@card:start\s*-->([\s\S]*?)<!--\s*@card:end\s*-->/g;
45
+ /** Author notes to whoever edits the skill next; never part of the card. */
46
+ const HTML_COMMENT_RE = /<!--[\s\S]*?-->/g;
47
+ function extractCard(skill, content) {
48
+ CARD_FENCE_RE.lastIndex = 0;
49
+ const parts = [];
50
+ let m;
51
+ while ((m = CARD_FENCE_RE.exec(content)) !== null) {
52
+ parts.push(m[1].replace(HTML_COMMENT_RE, '').trim());
53
+ }
54
+ if (parts.length === 0)
55
+ return null;
56
+ const body = parts.join('\n\n').trim();
57
+ if (body.length > CRAFT_CARD_CEILING) {
58
+ throw new Error(`[craft-cards] ${skill}.md's @card block is ${body.length} chars, over the ` +
59
+ `${CRAFT_CARD_CEILING} ceiling. A card rides EVERY estimate result for that model — ` +
60
+ `move the overflow into the body of the skill, which slates_get_prompting_guide returns whole.`);
61
+ }
62
+ return body;
63
+ }
64
+ /** Every card, keyed by skill name. Built once at load; integrity asserted. */
65
+ export const CRAFT_CARDS = Object.freeze(Object.fromEntries(Object.entries(SKILLS)
66
+ .map(([skill, content]) => [skill, extractCard(skill, content)])
67
+ .filter((entry) => entry[1] !== null)));
68
+ /** The card for a skill, or null when that skill carries none. */
69
+ export function craftCard(skill) {
70
+ return CRAFT_CARDS[skill] ?? null;
71
+ }
72
+ /**
73
+ * The card as it appears in an op RESULT: the body, plus one line naming where
74
+ * the rest lives so the agent knows the card is a summary and not the guide.
75
+ */
76
+ export function describeCraftCard(skill) {
77
+ const card = craftCard(skill);
78
+ if (!card)
79
+ return '';
80
+ return `${card}\n\nFull guide (examples, failure modes, sources): slates_get_prompting_guide("${skill}").`;
81
+ }
82
+ //# sourceMappingURL=craft-cards.js.map
@@ -25,6 +25,22 @@ export function buildEnvironmentEstablishingPrompt(userStyle) {
25
25
  `If the reference image contains people or characters, generate the location as an empty space — ignore the figures. ` +
26
26
  `No text, no labels, no captions.`);
27
27
  }
28
+ // ⚠️ STATUS: authored doctrine that currently reaches NO model. Adjudicated
29
+ // 2026-09-04 (dead-code sweep §5 D1). Both constants below have zero consumers
30
+ // anywhere in the workspace, and — unlike IMAGE_PROMPT_FORMULA, which was a
31
+ // verbatim second copy of slates-prompting-nano-banana-2.md and was deleted —
32
+ // nothing in skills/*.md or the craft cards carries this content. So it is a
33
+ // capability gap, not a duplicate: deleting it would lose the only copy.
34
+ //
35
+ // The fix is NOT to wire a TS consumer. craft-cards.ts states the rule: "THE
36
+ // CARD IS NEVER WRITTEN HERE… a card authored downstream is a second copy of
37
+ // the skill that drifts from it." Environment plates have no skill of their own
38
+ // yet (there is a Characters/Environments/Styles tab in the product, and a
39
+ // slates-character-identity skill, but no environment counterpart). Wiring this
40
+ // in means AUTHORING that skill section — a content + token-budget decision —
41
+ // after which these two constants should be deleted, not kept alongside it.
42
+ //
43
+ // Until then: EDITING THE TEXT BELOW CHANGES NOTHING THAT SHIPS.
28
44
  /** Guidance shown to users/agents: prefer describing the environment in text. */
29
45
  export const ENVIRONMENT_DESCRIBE_FIRST = 'Default to describing the environment in words and let the model build it to fit the shot. Generate an establishing plate only when a location must be locked exactly across shots. ' +
30
46
  'When you do: frame it three-quarter, never dead-on (a frontal facade turns the location into a backdrop characters stand in FRONT of; a three-quarter exposes side geometry and usable floor), ' +
@@ -7,4 +7,5 @@ export * from './model-facts.js';
7
7
  export * from './character-sheet.js';
8
8
  export * from './environment-sheet.js';
9
9
  export * from './prompting-tips.js';
10
+ export * from './asset-label.js';
10
11
  //# sourceMappingURL=index.d.ts.map
@@ -24,4 +24,8 @@ export * from './environment-sheet.js';
24
24
  // desktop RENDERER imports the tips — the root barrel re-exports auth.js
25
25
  // (node:fs/os/path), which breaks browser bundling. `./prompts` stays Node-free.
26
26
  export * from './prompting-tips.js';
27
+ // Asset captions — one rule for the op surface's compactAsset and the desktop
28
+ // gallery. Also its own leaf subpath (`@slatesvideo/shared/asset-label`) for the
29
+ // renderer, which cannot import the root barrel.
30
+ export * from './asset-label.js';
27
31
  //# sourceMappingURL=index.js.map
@@ -6,8 +6,20 @@ export type AspectRatio = '1:1' | '2:3' | '3:2' | '3:4' | '4:3' | '4:5' | '5:4'
6
6
  * and prices between 480p and 2K ($0.060/s vs 720p Seedance's $0.15/s — a
7
7
  * different tier of a different model, not a rename); 2K is H3's upscaled tier.
8
8
  * Aliasing either onto 720p/1080p would build a cost key that does not exist.
9
+ *
10
+ * 🚨 `1440p` entered with LTX-2.5 (2026-08-29) and is likewise NOT an alias —
11
+ * specifically it is NOT `2k`, despite both being ~1440 lines tall. `2k` is
12
+ * H3's UPSCALED tier ($0.130/s, an H3-Regenerate-2K pass over a 768p base);
13
+ * `1440p` is LTX's NATIVELY GENERATED tier ($0.190/s). Different models,
14
+ * different mechanisms, different prices, and `ltx-2-5-2k-6s` is a key that
15
+ * exists nowhere. Co-height is not sameness.
16
+ *
17
+ * ⚠️ Our `4k` is LTX's `2160p` ON THE WIRE. fal's enum literal for that tier is
18
+ * `2160p` while its own pricing copy calls it "4K". `4k` stays the token here
19
+ * because it is what every cost key, `is4kVideoKey` and the Pro gate already
20
+ * speak; the handler translates at the request boundary.
9
21
  */
10
- export type VideoResolution = '480p' | '720p' | '768p' | '1080p' | '2k' | '4k';
22
+ export type VideoResolution = '480p' | '720p' | '768p' | '1080p' | '1440p' | '2k' | '4k';
11
23
  /**
12
24
  * The full ten, in display order. `9:21` was in the MCP op's enum and in NO
13
25
  * model — it was invented downstream. Do not add a ratio here that no model
@@ -37,6 +49,41 @@ export interface VideoResolutionCapability {
37
49
  default?: VideoResolution;
38
50
  }
39
51
  /** Everything a model will ACCEPT. Capability only — never a price. */
52
+ /**
53
+ * What a TEXT-TO-SPEECH surface accepts. Every number here was MEASURED against
54
+ * the live API on 2026-09-05, not read from documentation — the vendor's docs
55
+ * omit the rate limit entirely and its API accepts an unknown `audioEncoding`
56
+ * with a 200 rather than a 400, so anything taken on trust here is a guess that
57
+ * bills.
58
+ *
59
+ * 🚨 `clonesPerMinute` IS A PRODUCT CONSTRAINT, NOT A TUNING KNOB. The vendor
60
+ * rate-limits voice cloning WORKSPACE-WIDE (every Slates user shares our one
61
+ * key), so it caps how many people can mint a voice in the same minute across
62
+ * the whole product. It is surfaced here so the seat can say so in words rather
63
+ * than failing opaquely.
64
+ */
65
+ export interface VoiceCloneCapability {
66
+ /** Reference-audio duration the clone endpoint accepts, in seconds. */
67
+ minSeconds: number;
68
+ maxSeconds: number;
69
+ /** Ceiling on ONE reference sample, in bytes. */
70
+ maxBytes: number;
71
+ /** Container formats the clone endpoint decodes. */
72
+ formats: readonly string[];
73
+ /** Clone requests per minute, WORKSPACE-WIDE (measured: a 429 names the limit). */
74
+ clonesPerMinute: number;
75
+ /**
76
+ * Stored custom voices the plan allows. The seat holds the steady-state count
77
+ * near ZERO by deleting each voice after it renders (mint → synthesize →
78
+ * delete), so this is the wall that argument exists to never reach.
79
+ */
80
+ maxStoredVoices: number;
81
+ /** Bounds on the voice-DESIGN prompt, the path that needs no reference audio. */
82
+ designPromptChars: {
83
+ min: number;
84
+ max: number;
85
+ };
86
+ }
40
87
  export interface ModelCapability {
41
88
  aspectRatios: AspectRatio[];
42
89
  /** Provider-keyed overrides. `fal` is the one that matters — see AGENT_ROUTE_PROVIDER. */
@@ -57,6 +104,14 @@ export interface ModelCapability {
57
104
  maxReferenceVideoSeconds?: number;
58
105
  /** Combined seconds across every reference audio clip. */
59
106
  maxReferenceAudioSeconds?: number;
107
+ /**
108
+ * Max characters in ONE synthesis request. Present ⟺ the surface is TTS.
109
+ * This is the number the character BILLING BUCKET is sized against, so it
110
+ * must never be hand-typed downstream — `slate`'s registry spreads it in.
111
+ */
112
+ maxCharacters?: number;
113
+ /** Reference-audio and voice-design spec. Present ⟺ the surface can clone. */
114
+ voiceClone?: VoiceCloneCapability;
60
115
  }
61
116
  /**
62
117
  * The provider every AGENT generation actually lands on for Kling and Veo.
@@ -74,6 +129,15 @@ export interface ModelCapability {
74
129
  export declare const AGENT_ROUTE_PROVIDER = "fal";
75
130
  export declare const MODEL_CAPABILITIES: Record<string, ModelCapability>;
76
131
  export declare function getModelCapability(model: string): ModelCapability | undefined;
132
+ /**
133
+ * The voice-cloning spec for a TTS surface, or undefined for anything else.
134
+ *
135
+ * Exists so the desktop reads the reference-audio bounds, the design-prompt
136
+ * bounds and the clone rate limit from HERE rather than retyping them into a
137
+ * form control. A control whose limit disagrees with the vendor's is a limit
138
+ * the user first meets AFTER pressing the button.
139
+ */
140
+ export declare function voiceCloneFor(model: string): VoiceCloneCapability | undefined;
77
141
  /** Aspect ratios a model accepts, honouring the provider override. */
78
142
  export declare function aspectRatiosFor(model: string, provider?: string): AspectRatio[];
79
143
  /** Video resolutions a model accepts. A FIXED model reports exactly its one value. */