@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
@@ -640,6 +640,127 @@ const MINIMAX_H3 = {
640
640
  'Slates disables the provider\'s prompt expander, so what you write is what the model reads — nothing will pad a thin prompt. Aim for a 350-500 word body on a reference-carrying shot, and let dialogue-heavy scenes run longer if that is what fits the spoken timeline.',
641
641
  ],
642
642
  };
643
+ const LTX_2_5 = {
644
+ label: 'LTX-2.5',
645
+ intro: [
646
+ 'LTX-2.5 scores the picture on the same pass that draws it, so SOUND IS THE FIRST THING YOU WRITE, not the last. Lightricks ranks the six parts of a prompt in this order: sound, camera, character detail, shot type and scene, then scene dressing — and scene dressing is the first thing to cut when a prompt sprawls. Everything goes in ONE flowing paragraph, not a list of labelled sections.',
647
+ 'Two seats. Base LTX-2.5 is the distilled build: 720p / 1080p / 1440p / 4K and clips from 6 to 20 seconds, and it is the cheapest native 1080p second in Slates. LTX-2.5 Pro is the full diffusion build ("Diffusion Fidelity Rendering" spends extra compute on busy frames) but reaches a SHORTER ladder — 1080p and 10 seconds maximum — while costing about a third more. Pro is for a dense final render; base is for iteration, long takes and 4K.',
648
+ ],
649
+ columns: [
650
+ [
651
+ {
652
+ heading: 'Anchor every sound to something in frame',
653
+ example: 'the rope creaks against the cleat, gulls somewhere off the port bow',
654
+ note: 'Write the audio line last, then check each cue has a visible or at least locatable source. Anything unanchored gets invented for you. "Not visible but locatable" passes — a whistle is fine if you name the marshal post it comes from.',
655
+ critical: true,
656
+ },
657
+ {
658
+ heading: 'Never write mood words for sound',
659
+ example: 'Bad: "tense atmosphere, sense of dread"\nGood: "a loose shutter knocks twice against the frame"',
660
+ note: 'Atmosphere adjectives produce nothing. If a scene feels thin, add one more MOVING OBJECT with a sound attached to it rather than another adjective.',
661
+ },
662
+ {
663
+ heading: 'Dialogue takes quotes, language and accent',
664
+ example: '"We should not have come back," in English with a slight German accent.',
665
+ note: 'Give the character a beat of stillness before they speak so the lip sync has something to lock against. Describe the beat: looks, waits, speaks, looks away.',
666
+ },
667
+ {
668
+ heading: 'Emotion is physical, not abstract',
669
+ example: 'Bad: "she looks anxious"\nGood: "her jaw sets, she turns the ring on her finger twice"',
670
+ note: 'The model renders actions, not adjectives. Tension in the jaw, weight shifts, fidgeting hands — these read; "anxious" does not.',
671
+ },
672
+ ],
673
+ [
674
+ {
675
+ heading: 'Multishot: two to four shots, and re-establish at every cut',
676
+ example: 'wide establishing shot — hard cut — macro close-up — match cut — medium shot',
677
+ note: 'One generation can carry several connected shots holding character, light and voice across the cuts. Name the edit ("hard cut", "dissolve") in the prose, then RESET scale, angle, lens and light. Two to four is the working range.',
678
+ critical: true,
679
+ },
680
+ {
681
+ heading: 'Re-identify characters at every cut',
682
+ example: 'Good: "the woman in the bronze gown"\nBad: "she"',
683
+ note: 'Pronouns lose the character across a cut. Repeat the original descriptor every time. Also state what the SOUND does at the cut — silence is not assumed.',
684
+ },
685
+ {
686
+ heading: 'Write the camera into the sentence',
687
+ example: 'a slow push-in settles as she reaches the door, then holds',
688
+ note: 'Slates does not expose the camera_motion enum, and prose is the better tool anyway: a written move can be tied to a specific moment, an enum value cannot. Name lens, framing and the moment the move resolves.',
689
+ },
690
+ {
691
+ heading: 'Durations are even numbers only, from six',
692
+ note: '6, 8, 10, 12, 14, 16, 18 or 20 seconds — there is no 5s or 7s LTX clip. And the long end is 1080p-and-below only: at 1440p and 4K the ceiling drops to 10s. Slates always sends an explicit length rather than letting the model pick one, so what you choose is what you are billed for.',
693
+ critical: true,
694
+ },
695
+ {
696
+ heading: 'Do not ask for text on screen',
697
+ note: 'Neither the spelling nor its stability frame to frame can be relied on. Signage, labels and captions belong in post.',
698
+ },
699
+ ],
700
+ ],
701
+ footer: [
702
+ 'Frames, not references. LTX takes a start frame and an optional end frame (which generates a transition between the two) — it has no reference endpoint at all, so identity, style and environment reference images are not available on this model. For character consistency across separate shots, use MiniMax H3 or Kling.',
703
+ 'Aspect ratios are 16:9 and 9:16 only, and native audio is included free at every resolution — there is no sound surcharge on either seat.',
704
+ 'In image-to-video, do not cut away from the opening frame too early: you have paid for that frame, so let it play before the first move.',
705
+ ],
706
+ };
707
+ const INWORLD_TTS = {
708
+ label: 'Inworld TTS-2',
709
+ intro: [
710
+ 'The voice seat: one named voice saying one line. Unlike every other surface in Slates, the prompt is not a description of what you want — it IS the words that get spoken, verbatim, and its length is what you are billed for.',
711
+ 'A voice belongs to a character, the same way a face does. Build it once from a clip or a description, then send it lines.',
712
+ ],
713
+ columns: [
714
+ [
715
+ {
716
+ heading: 'Direction goes in SQUARE BRACKETS',
717
+ example: '\u2717 (quietly) I hope nobody notices\n\u2713 [whispering] I hope nobody notices',
718
+ note: 'Brackets are read as direction and never spoken. PARENTHESES ARE SPOKEN ALOUD \u2014 a parenthetical stage direction comes back with the narrator saying the word "quietly". This is the single easiest way to ruin a take.',
719
+ critical: true,
720
+ },
721
+ {
722
+ heading: 'Plain English inside the brackets',
723
+ example: '[very quiet] \u00b7 [very slow] \u00b7 [say excitedly] \u00b7 [whisper in a hushed style]',
724
+ note: 'It is natural-language steering across emotion, volume, pitch, speed, articulation and vocal style \u2014 not a fixed vocabulary. Write the direction the way you would say it to an actor.',
725
+ },
726
+ {
727
+ heading: 'Non-verbals are their own tags',
728
+ example: '[laugh] \u00b7 [sigh] \u00b7 [breathe] \u00b7 [cough] \u00b7 [yawn] \u00b7 [clear throat]',
729
+ note: 'They land inline, where they occur in the line.',
730
+ },
731
+ {
732
+ heading: 'A mistyped tag fails silently',
733
+ note: 'An instruction it does not recognise is still swallowed and still changes the delivery \u2014 it is never spoken and never errors. So a typo produces a strange read with no warning. If a take sounds off, suspect the tag before the voice.',
734
+ critical: true,
735
+ },
736
+ ],
737
+ [
738
+ {
739
+ heading: 'Tags persist until changed',
740
+ example: '[very slow] First line. Second line is still slow. [reset] Third is normal.',
741
+ note: 'A direction governs everything after it, across sentences. Use [reset] to return to a normal read rather than assuming the next sentence starts clean.',
742
+ },
743
+ {
744
+ heading: 'Punctuation is the timing',
745
+ example: 'Wait. Stop. \u2260 Wait, stop.',
746
+ note: 'Full stops buy a beat; commas do not. Write the line the way it is said.',
747
+ },
748
+ {
749
+ heading: 'One line, one take',
750
+ note: 'Split a paragraph into separate generations so a bad clause costs one re-roll instead of the whole speech. Max 2,000 characters per take.',
751
+ },
752
+ {
753
+ heading: 'Spell out anything ambiguous',
754
+ example: 'twenty twenty-six \u00b7 Doctor Reyes',
755
+ note: 'Numbers, dates and abbreviations are read literally. Write them as they should sound.',
756
+ },
757
+ {
758
+ heading: 'Know its seat',
759
+ note: 'One voice, cleanly. Dialogue mixed with effects and room tone in one pass is Seed Audio; a single non-speech sound is Sound Effects.',
760
+ },
761
+ ],
762
+ ],
763
+ };
643
764
  export const PROMPTING_TIPS = {
644
765
  seedance: SEEDANCE,
645
766
  'seedance-2-5': SEEDANCE_25,
@@ -650,10 +771,12 @@ export const PROMPTING_TIPS = {
650
771
  'omni-flash': OMNI_FLASH,
651
772
  'omni-flash-edit': OMNI_FLASH_EDIT,
652
773
  'minimax-h3': MINIMAX_H3,
774
+ 'ltx-2-5': LTX_2_5,
653
775
  'nano-banana': NANO_BANANA,
654
776
  'nano-banana-lite': NANO_BANANA_LITE,
655
777
  'seed-audio': SEED_AUDIO,
656
778
  'eleven-sfx': ELEVEN_SFX,
779
+ 'inworld-tts-2': INWORLD_TTS,
657
780
  };
658
781
  /** Null when no tips exist for the key — callers render an honest fallback. */
659
782
  export function getPromptingTips(key) {
@@ -50,16 +50,45 @@ export interface ComposedReferences {
50
50
  * Tokens written in the prompt that matched NO reference group, as authored
51
51
  * (`'#noir'`, `'@bob'`), first-appearance order, deduped case-insensitively.
52
52
  *
53
- * 🚨 THIS FIELD EXISTS BECAUSE THE ALTERNATIVE IS A SILENT EDIT. A `#tag` with
54
- * no matching style is DELETED from the text — a raw tag confuses every model,
55
- * so removing it is right, but removing it without saying so changes what the
56
- * user asked for behind their back. Prompt-transparency doctrine (slate
57
- * `CLAUDE.md`) requires the surface to be able to say "this went nowhere", and
58
- * this is the only record that the token was ever there. Callers that render a
59
- * composed-prompt preview MUST surface it.
53
+ * 🚨 IT IS A REPORT, NOT A RECEIPT FOR A DELETION. These tokens are left in
54
+ * `prompt` EXACTLY as the user typed them (see step 2) — nothing is removed
55
+ * and nothing is humanised. What the field says is narrower and more useful:
56
+ * "no reference is attached for this word", which is the difference between a
57
+ * mistyped mention and a `@handle` you meant to typeset. Callers that render a
58
+ * composed-prompt preview SHOULD surface it — as a note, never as an error,
59
+ * because nothing has gone wrong.
60
60
  */
61
61
  unresolvedTokens: string[];
62
62
  }
63
+ /**
64
+ * 🚨 A HEX COLOUR IS NOT A TAG. `#000000` is to `#` what `eric@gmail.com` is to
65
+ * `@`: an everyday literal that happens to open with our sigil, written by
66
+ * someone who was not reaching for our feature at all.
67
+ *
68
+ * The TEXT was already safe — an unresolved token is returned byte-identical
69
+ * (see TOKEN_RE) — so what this guards is the REPORT. The `@` half of that
70
+ * grammar gets its everyday literal excluded for free, because an email's `@`
71
+ * sits after a word character and the boundary lookbehind kills it. A hex
72
+ * colour gets no such help: `#` at a word boundary is exactly how a real tag is
73
+ * written too, so `#000000` reaches `noteUnresolved` and a palette prompt
74
+ * ("pure black #000000 with #c8ff00 accents") comes back with every colour
75
+ * listed as *matching nothing saved* — an accusation about words that were
76
+ * never mentions. That is the asymmetry this closes, and it is the same
77
+ * complaint, in the same place, as `#3a3a3c` being eaten for months.
78
+ *
79
+ * 🚨 IT GATES THE REPORT, NOT THE GRAMMAR, and that is the whole design.
80
+ * `fade`, `cafe`, `dead`, `beef`, `decade` and `facade` are all valid hex
81
+ * digits AND plausible style names, so excluding hex shapes from TOKEN_RE would
82
+ * quietly stop `#fade` from attaching a style called "Fade" — trading a noisy
83
+ * note for a silent drop, which is the worse half of the trade every time.
84
+ * Resolution is checked first and always wins; only a token that binds to
85
+ * nothing ever reaches here.
86
+ *
87
+ * Lengths are CSS's: 3 (`#fff`), 4 (`#fff8`), 6 (`#c8ff00`), 8 (`#c8ff00ff`).
88
+ * `#1` and `#2026` are deliberately NOT covered — an ordinal is not a colour,
89
+ * and widening this to "anything numeric" would start swallowing tags.
90
+ */
91
+ export declare function isHexColorToken(sigil: string, token: string): boolean;
63
92
  /**
64
93
  * Compose the raw prompt (mentions intact) + an ORDERED list of reference groups
65
94
  * into the named prompt text + ordered media the API receives. The group order
@@ -44,13 +44,66 @@ function joinNums(nums) {
44
44
  return `${nums[0]} and ${nums[1]}`;
45
45
  return `${nums.slice(0, -1).join(', ')} and ${nums[nums.length - 1]}`;
46
46
  }
47
- // Humanize an unresolved @token to plain words (preserve the legacy cleanPrompt
48
- // fallback: @big_red → "Big Red", @forest3 → "Forest3"). Never send a raw token.
49
- function humanizeToken(raw) {
50
- return raw
51
- .split(/[_-]/)
52
- .map((w) => (w ? w.charAt(0).toUpperCase() + w.slice(1) : w))
53
- .join(' ');
47
+ /**
48
+ * 🚨 A SIGIL IS A MENTION ONLY IF IT RESOLVES. Everything else is prose, and
49
+ * prose is not ours to edit.
50
+ *
51
+ * This grammar used to be `/([@#])([\w-]+)/g` with "unresolved" as a rewrite
52
+ * branch: an unknown `@word` was humanised (`@woodland.candle` → "Woodland" +
53
+ * ".candle") and an unknown `#word` was deleted outright. Both assumed anyone
54
+ * typing a sigil was reaching for OUR feature, so the app quietly rewrote the
55
+ * one thing the composer exists to protect — and it failed precisely where the
56
+ * literal characters matter most: a poster's `@handle`, an email address, a hex
57
+ * colour. That last one is not hypothetical: `#3a3a3c` was eaten out of every
58
+ * identity-sheet prompt for months (`reference-rules.ts`, 2026-07-30), which is
59
+ * the same bug wearing a different sigil.
60
+ *
61
+ * Two changes make the sigil safe to type:
62
+ *
63
+ * 1. THE GRAMMAR IS NARROW. A sigil only opens a token at a boundary, so
64
+ * `eric@gmail.com` is never even a candidate, and a token may carry interior
65
+ * dots, so `@woodland.candle` is ONE token rather than a mention with debris
66
+ * stuck to it. Interior only — a trailing `.` stays with the sentence.
67
+ * 2. AN UNRESOLVED TOKEN IS RETURNED UNTOUCHED. A mention is a BINDING; when it
68
+ * binds to nothing there is nothing to translate, so there is nothing to
69
+ * rewrite. It is reported through `unresolvedTokens` and sent as written.
70
+ *
71
+ * There is deliberately NO escape syntax (`\@`, quoting). Making someone learn
72
+ * our grammar to opt OUT of a feature they never invoked is the shoehorn this
73
+ * change exists to remove — the sandbox rule from the root `CLAUDE.md`: a
74
+ * capability attaches to the primitive, it does not stand in the way of it.
75
+ */
76
+ const TOKEN_RE = /(?<![\w.@#])([@#])([\w-]+(?:\.[\w-]+)*)/g;
77
+ /**
78
+ * 🚨 A HEX COLOUR IS NOT A TAG. `#000000` is to `#` what `eric@gmail.com` is to
79
+ * `@`: an everyday literal that happens to open with our sigil, written by
80
+ * someone who was not reaching for our feature at all.
81
+ *
82
+ * The TEXT was already safe — an unresolved token is returned byte-identical
83
+ * (see TOKEN_RE) — so what this guards is the REPORT. The `@` half of that
84
+ * grammar gets its everyday literal excluded for free, because an email's `@`
85
+ * sits after a word character and the boundary lookbehind kills it. A hex
86
+ * colour gets no such help: `#` at a word boundary is exactly how a real tag is
87
+ * written too, so `#000000` reaches `noteUnresolved` and a palette prompt
88
+ * ("pure black #000000 with #c8ff00 accents") comes back with every colour
89
+ * listed as *matching nothing saved* — an accusation about words that were
90
+ * never mentions. That is the asymmetry this closes, and it is the same
91
+ * complaint, in the same place, as `#3a3a3c` being eaten for months.
92
+ *
93
+ * 🚨 IT GATES THE REPORT, NOT THE GRAMMAR, and that is the whole design.
94
+ * `fade`, `cafe`, `dead`, `beef`, `decade` and `facade` are all valid hex
95
+ * digits AND plausible style names, so excluding hex shapes from TOKEN_RE would
96
+ * quietly stop `#fade` from attaching a style called "Fade" — trading a noisy
97
+ * note for a silent drop, which is the worse half of the trade every time.
98
+ * Resolution is checked first and always wins; only a token that binds to
99
+ * nothing ever reaches here.
100
+ *
101
+ * Lengths are CSS's: 3 (`#fff`), 4 (`#fff8`), 6 (`#c8ff00`), 8 (`#c8ff00ff`).
102
+ * `#1` and `#2026` are deliberately NOT covered — an ordinal is not a colour,
103
+ * and widening this to "anything numeric" would start swallowing tags.
104
+ */
105
+ export function isHexColorToken(sigil, token) {
106
+ return sigil === '#' && /^(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(token);
54
107
  }
55
108
  export function composeReferences(rawPrompt, groups, opts = {}) {
56
109
  // ── 1. Assign global numbers by walking the list in order ──
@@ -99,11 +152,16 @@ export function composeReferences(rawPrompt, groups, opts = {}) {
99
152
  const seenFirst = new Set();
100
153
  const matchedInPrompt = new Set();
101
154
  // Every token that named nothing, recorded as authored and deduped by the
102
- // same normalisation used for matching. This is what makes the deletion
103
- // below reportable instead of silent — see ComposedReferences.unresolvedTokens.
155
+ // same normalisation used for matching. Nothing is removed on its account —
156
+ // it is the "no reference is attached for this word" report. See
157
+ // ComposedReferences.unresolvedTokens.
104
158
  const unresolvedTokens = [];
105
159
  const unresolvedSeen = new Set();
106
160
  const noteUnresolved = (sigil, tok) => {
161
+ // A hex colour that binds to no style is a colour, not a mistyped tag.
162
+ // See isHexColorToken — the text is unchanged either way; this is the note.
163
+ if (isHexColorToken(sigil, tok))
164
+ return;
107
165
  const key = normToken(`${sigil}${tok}`);
108
166
  if (unresolvedSeen.has(key))
109
167
  return;
@@ -112,27 +170,24 @@ export function composeReferences(rawPrompt, groups, opts = {}) {
112
170
  };
113
171
  // First strip "in/with the style of #tag" phrases so the style reads as a
114
172
  // clean trailing clause, not a dangling preposition (legacy cleanPrompt
115
- // behaviour). An UNRESOLVED token in that phrase is stripped too: leaving the
116
- // preposition behind ("…a portrait in the style of") was the worse half of the
117
- // silent edit — the tag vanished a step later anyway and the sentence was left
118
- // broken. Stripped or not, the token is reported.
119
- let body = rawPrompt.replace(/\s+(with|in)\s+the\s+style\s+of\s+([@#])([\w-]+)/gi, (_full, _prep, sigil, tok) => {
173
+ // behaviour). ONLY when the tag resolves: an unresolved one leaves the whole
174
+ // phrase exactly as authored and falls through to the token pass below, which
175
+ // reports it and sends it as written.
176
+ let body = rawPrompt.replace(/\s+(with|in)\s+the\s+style\s+of\s+([@#])([\w-]+(?:\.[\w-]+)*)/gi, (_full, _prep, sigil, tok) => {
120
177
  const g = byNorm.get(normToken(`${sigil}${tok}`));
121
178
  if (g && g.kind === 'style') {
122
179
  matchedInPrompt.add(normToken(`${sigil}${tok}`));
123
180
  return '';
124
181
  }
125
- noteUnresolved(sigil, tok);
126
- return '';
182
+ return _full;
127
183
  });
128
- body = body.replace(/([@#])([\w-]+)/g, (_full, _sigil, tok) => {
184
+ body = body.replace(TOKEN_RE, (_full, _sigil, tok) => {
129
185
  const key = normToken(`${_sigil}${tok}`);
130
186
  const g = byNorm.get(key);
131
187
  if (!g) {
132
- // Unresolved token. A #unknown vanishes; an @unknown humanizes to words.
133
- // Both are edits the user never asked for, so both are reported.
188
+ // Not a mention — see TOKEN_RE. Reported, returned byte-identical.
134
189
  noteUnresolved(_sigil, tok);
135
- return _sigil === '#' ? '' : humanizeToken(tok);
190
+ return _full;
136
191
  }
137
192
  matchedInPrompt.add(key);
138
193
  if (g.kind === 'style')
@@ -1,17 +1,4 @@
1
1
  export { PARTIALS } from './partials.generated.js';
2
- export type SourceGrade = 'Eric-test' | 'community' | 'code-verified' | 'creator-demo';
3
- export interface ReferenceRule {
4
- id: string;
5
- title: string;
6
- rule: string;
7
- why: string;
8
- grade: SourceGrade;
9
- }
10
- /**
11
- * The 10 verified reference rules. These are the WHY; the fragments below
12
- * are the reusable text the templates compose.
13
- */
14
- export declare const REFERENCE_RULES: ReferenceRule[];
15
2
  /** Flat, even, shadowless identity lighting on a deep neutral-grey plate. */
16
3
  export declare const IDENTITY_LIGHTING = "flat, even, shadowless lighting";
17
4
  /**
@@ -21,21 +8,23 @@ export declare const IDENTITY_LIGHTING = "flat, even, shadowless lighting";
21
8
  */
22
9
  export declare const IDENTITY_PLATE_HEX = "#3a3a3c";
23
10
  /**
24
- * 🚨 THE HEX IS EMITTED WITHOUT ITS `#` AND THAT IS LOAD-BEARING (2026-07-30).
25
- *
26
- * `#` is a REFERENCE-TOKEN SIGIL in the desktop's prompt composer
27
- * (`slate/src/shared/promptComposition.ts` — `/([@#])([\w-]+)/g`), and an
28
- * unresolved `#token` is SILENTLY DELETED. So `#3a3a3c` never survived to any
29
- * model: fal echoed back `deep neutral-grey background ()` on a real 2026-07-30
30
- * request. The plate value has been doing nothing since the composer shipped.
11
+ * THE HEX IS EMITTED WITHOUT ITS `#`. This USED to be load-bearing (2026-07-30):
12
+ * `#` is a reference-token sigil in the prompt composer, and an unresolved
13
+ * `#token` was SILENTLY DELETED, so `#3a3a3c` never survived to any model — fal
14
+ * echoed back `deep neutral-grey background ()` on a real 2026-07-30 request and
15
+ * the plate value had been doing nothing since the composer shipped.
31
16
  *
32
- * Writing it bare keeps the value in the prompt. `IDENTITY_PLATE_HEX` keeps its
33
- * `#` because it is a colour constant and may have non-prompt consumers.
17
+ * 🚨 THE HAZARD IS GONE, AND THE OLD GENERAL RULE WITH IT. The composer now
18
+ * treats a sigil as a mention ONLY when it resolves and passes every other
19
+ * `@`/`#` through byte-identically (`reference-composer.ts` → `TOKEN_RE`) — the
20
+ * exact "HOW YOU'D KNOW THIS IS BEATEN" this comment used to name. Emitting a
21
+ * `#hex` or an `@handle` into prompt text is safe again.
34
22
  *
35
- * GENERAL RULE FOR THIS FILE: never emit `#` or `@` into prompt text. Both are
36
- * sigils downstream and both mangle silently — no error, no log, just missing
37
- * words. HOW YOU'D KNOW THIS IS BEATEN: the composer stops treating bare
38
- * `#hex` as a token, or starts leaving unresolved tokens intact.
23
+ * It stays bare here anyway: "hex 3a3a3c" reads at least as clearly to a model,
24
+ * and rewording it would churn the generated skills/docs corpus for no gain.
25
+ * That is now a style choice, NOT a workaround — do not re-derive a "never emit
26
+ * a sigil" law from it. `IDENTITY_PLATE_HEX` keeps its `#` because it is a
27
+ * colour constant with possible non-prompt consumers.
39
28
  */
40
29
  export declare const IDENTITY_BACKGROUND: string;
41
30
  export declare const IDENTITY_LIGHTING_CLAUSE: string;
@@ -1,90 +1,10 @@
1
1
  // Reference best-practices — the canonical "how to use reference images"
2
2
  // knowledge for every Slates surface (desktop templates, MCP skills, the
3
3
  // lead-magnet .skill). Authored ONCE here; consumers derive from it.
4
- //
5
- // Source grades: [Eric-test] = Eric's own hands-on result (doctrine-grade);
6
- // [community] = multi-guide consensus; [code-verified] = verified against
7
- // the slate codebase; [creator-demo] = single creator demonstration.
8
4
  // The generated partial store — one entry per skills/_partials/*.md file.
9
5
  // Re-exported so consumers can reach any partial, not just the rules block.
10
6
  export { PARTIALS } from './partials.generated.js';
11
7
  import { PARTIALS } from './partials.generated.js';
12
- /**
13
- * The 10 verified reference rules. These are the WHY; the fragments below
14
- * are the reusable text the templates compose.
15
- */
16
- export const REFERENCE_RULES = [
17
- {
18
- id: 'two-to-four-refs',
19
- title: '2-4 strong references beat both extremes',
20
- rule: 'Use 2-4 strong, focused references — not 1 (warps), not 12 (averages worse). Start with 2-3.',
21
- why: 'One reference warps toward itself; a dozen averages everyone into mush. One tester cut drift ~60% going 6→2.',
22
- grade: 'community',
23
- },
24
- {
25
- id: 'one-ref-per-role',
26
- title: 'One reference per ROLE, labeled in the prompt',
27
- rule: 'Use one reference per role — identity, style-grade, environment — and label each role explicitly in the prompt text. The model does not infer roles from order.',
28
- why: 'Same-role competitors drift; two "identity" refs of different people blend into a third face.',
29
- grade: 'community',
30
- },
31
- {
32
- id: 'identity-name-as-one-entity',
33
- title: 'One identity sheet per character, named inline',
34
- rule: 'Attach the character\'s single identity sheet (dominant portrait + body panels) rather than a pile of views — fewer competing renderings of a face is better, because the model cannot tell which is authoritative and averages them. Cite it inline as the subject ("Marcus (image 1)"). Do not inject a role essay ("use for identity, ignore the outfit/lighting, render neutral"): the user\'s prompt owns wardrobe, expression, and lighting.',
35
- why: 'Distinct inline naming is each model\'s own consistency lever — NB2 "assign a distinct name to each character/object"; Seedance "Reference <Subject_N> in <Image_N>"; Kling "reuse a fixed label verbatim". One canonical identity image removes same-role competition before it starts.',
36
- grade: 'Eric-test',
37
- },
38
- {
39
- id: 'flat-light-identity',
40
- title: 'Flat-light identity refs',
41
- rule: 'Prep identity refs with flat, even, shadowless lighting on a plain neutral background. Studio-lit or scene-lit sheets bleed their lighting into every generation.',
42
- why: "Eric's Norse-woman test: a studio-lit sheet produced a subject that looked green-screen-pasted in front of mountains. Reference prep beats prompting here.",
43
- grade: 'Eric-test',
44
- },
45
- {
46
- id: 'describe-environment',
47
- title: 'Environment: describe it, don\'t feed a grid',
48
- rule: 'Default to describing the environment in words. Reserve an environment reference for a mandatory exact-match location, and when you do, use ONE clean establishing image — never a multi-panel grid fed whole.',
49
- why: "Eric's test: character sheet + 3x3 mountain grid → pasted-in mess; the same character + a described background → believable scene with natural lighting the model invented to fit.",
50
- grade: 'Eric-test',
51
- },
52
- {
53
- id: 'grids-explore-not-input',
54
- title: 'Grids: generation/exploration = fine; INPUT reference = bad',
55
- rule: 'Use grids to EXPLORE compositions (cheap multi-angle generation), then pick a cell. Do NOT feed a grid back in as a reference image — cells share a split detail budget and generate jointly, so flaws propagate.',
56
- why: 'A picked cell re-generated with a "preserve the exact composition" prompt keeps its flaws faithfully (that is the budget path, kept on purpose). A loose prompt can fix anatomy but drifts off the picked composition.',
57
- grade: 'code-verified',
58
- },
59
- {
60
- id: 'reuse-refs',
61
- title: 'Reuse the same refs across all shots',
62
- rule: 'Lock a reference set and reuse it across every shot in a sequence. Swapping refs mid-sequence causes drift.',
63
- why: 'The model adapts environment refs to each prompt rather than copying them, so swapping compounds inconsistency shot to shot.',
64
- grade: 'community',
65
- },
66
- {
67
- id: 'text-as-start-frame',
68
- title: 'Legible in-shot text → image start-frame, never trust text-to-video',
69
- rule: 'When a shot needs legible on-screen text, bake it into a still start frame (NB2) and animate from that. Do not expect a text-to-video model to render clean text.',
70
- why: 'Video models smear text; an image model holds it, and the video inherits the locked frame.',
71
- grade: 'community',
72
- },
73
- {
74
- id: 'i2v-own-footage',
75
- title: 'I2V / own-footage superpower — describe only what changes',
76
- rule: 'Restyle your own clip while keeping the performance; "video one" delayed-VFX; marker-object insertion; video-as-ref for a series. In all cases describe ONLY what changes, not the whole scene.',
77
- why: 'The source clip already carries motion, timing, and performance; re-describing them fights the model. Narrate the delta.',
78
- grade: 'creator-demo',
79
- },
80
- {
81
- id: 'style-transform-nl',
82
- title: 'Style transform by natural language',
83
- rule: 'Default: keep the source\'s artistic medium/style. To change it, add a plain-text instruction ("anime → real person") — no preset pickers.',
84
- why: 'Paste/natural-language over preset menus is the product philosophy; the medium is inherited unless the user explicitly asks to transform it.',
85
- grade: 'Eric-test',
86
- },
87
- ];
88
8
  // ── Reusable text fragments (the template-assembly building blocks) ──
89
9
  // These are the exact strings the desktop prompt templates, MCP skills,
90
10
  // and lead-magnet compose. Change a rule HERE and every consumer follows.
@@ -97,21 +17,23 @@ export const IDENTITY_LIGHTING = 'flat, even, shadowless lighting';
97
17
  */
98
18
  export const IDENTITY_PLATE_HEX = '#3a3a3c';
99
19
  /**
100
- * 🚨 THE HEX IS EMITTED WITHOUT ITS `#` AND THAT IS LOAD-BEARING (2026-07-30).
101
- *
102
- * `#` is a REFERENCE-TOKEN SIGIL in the desktop's prompt composer
103
- * (`slate/src/shared/promptComposition.ts` — `/([@#])([\w-]+)/g`), and an
104
- * unresolved `#token` is SILENTLY DELETED. So `#3a3a3c` never survived to any
105
- * model: fal echoed back `deep neutral-grey background ()` on a real 2026-07-30
106
- * request. The plate value has been doing nothing since the composer shipped.
20
+ * THE HEX IS EMITTED WITHOUT ITS `#`. This USED to be load-bearing (2026-07-30):
21
+ * `#` is a reference-token sigil in the prompt composer, and an unresolved
22
+ * `#token` was SILENTLY DELETED, so `#3a3a3c` never survived to any model — fal
23
+ * echoed back `deep neutral-grey background ()` on a real 2026-07-30 request and
24
+ * the plate value had been doing nothing since the composer shipped.
107
25
  *
108
- * Writing it bare keeps the value in the prompt. `IDENTITY_PLATE_HEX` keeps its
109
- * `#` because it is a colour constant and may have non-prompt consumers.
26
+ * 🚨 THE HAZARD IS GONE, AND THE OLD GENERAL RULE WITH IT. The composer now
27
+ * treats a sigil as a mention ONLY when it resolves and passes every other
28
+ * `@`/`#` through byte-identically (`reference-composer.ts` → `TOKEN_RE`) — the
29
+ * exact "HOW YOU'D KNOW THIS IS BEATEN" this comment used to name. Emitting a
30
+ * `#hex` or an `@handle` into prompt text is safe again.
110
31
  *
111
- * GENERAL RULE FOR THIS FILE: never emit `#` or `@` into prompt text. Both are
112
- * sigils downstream and both mangle silently — no error, no log, just missing
113
- * words. HOW YOU'D KNOW THIS IS BEATEN: the composer stops treating bare
114
- * `#hex` as a token, or starts leaving unresolved tokens intact.
32
+ * It stays bare here anyway: "hex 3a3a3c" reads at least as clearly to a model,
33
+ * and rewording it would churn the generated skills/docs corpus for no gain.
34
+ * That is now a style choice, NOT a workaround — do not re-derive a "never emit
35
+ * a sigil" law from it. `IDENTITY_PLATE_HEX` keeps its `#` because it is a
36
+ * colour constant with possible non-prompt consumers.
115
37
  */
116
38
  export const IDENTITY_BACKGROUND = `a plain, deep neutral-grey background (hex ${IDENTITY_PLATE_HEX.replace('#', '')})`;
117
39
  export const IDENTITY_LIGHTING_CLAUSE = `Render on ${IDENTITY_BACKGROUND} with ${IDENTITY_LIGHTING} so the sheet captures the character's identity, not scene lighting.`;