@slatesvideo/shared 0.6.3 → 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.
- package/dist/api-url.d.ts +9 -0
- package/dist/api-url.js +9 -0
- package/dist/auth.d.ts +13 -1
- package/dist/auth.js +9 -5
- package/dist/clients/cloud.d.ts +3 -0
- package/dist/clients/cloud.js +34 -3
- package/dist/clients/desktop.js +3 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.js +19 -2
- package/dist/operations/index.d.ts +242 -30
- package/dist/operations/index.js +1275 -133
- package/dist/operations/surface.d.ts +69 -0
- package/dist/operations/surface.js +227 -0
- package/dist/prompts/agent-doctrine.js +7 -0
- package/dist/prompts/asset-label.d.ts +23 -0
- package/dist/prompts/asset-label.js +70 -0
- package/dist/prompts/banned-tokens.d.ts +15 -3
- package/dist/prompts/banned-tokens.js +76 -9
- package/dist/prompts/character-sheet.d.ts +0 -2
- package/dist/prompts/character-sheet.js +0 -2
- package/dist/prompts/craft-cards.d.ts +20 -0
- package/dist/prompts/craft-cards.js +82 -0
- package/dist/prompts/environment-sheet.js +16 -0
- package/dist/prompts/index.d.ts +1 -0
- package/dist/prompts/index.js +4 -0
- package/dist/prompts/model-capabilities.d.ts +52 -0
- package/dist/prompts/model-capabilities.js +42 -0
- package/dist/prompts/model-facts.d.ts +0 -4
- package/dist/prompts/model-facts.js +8 -4
- package/dist/prompts/partials.generated.js +2 -1
- package/dist/prompts/prompting-tips.d.ts +1 -1
- package/dist/prompts/prompting-tips.js +58 -0
- package/dist/prompts/reference-composer.d.ts +36 -7
- package/dist/prompts/reference-composer.js +75 -20
- package/dist/prompts/reference-rules.d.ts +15 -26
- package/dist/prompts/reference-rules.js +15 -93
- package/dist/prompts/shot-grammar.d.ts +154 -0
- package/dist/prompts/shot-grammar.js +184 -0
- package/dist/prompts/shot-spec.d.ts +265 -0
- package/dist/prompts/shot-spec.js +303 -0
- package/dist/skills/content.js +25 -23
- package/exports/slates-prompt-builder/generated/reference-content-policy.md +6 -0
- package/exports/slates-prompt-builder/generated/reference-kling.md +22 -0
- package/exports/slates-prompt-builder/generated/reference-nano-banana.md +17 -0
- package/exports/slates-prompt-builder/generated/reference-seedance.md +17 -0
- package/exports/slates-prompt-builder/generated/slates-prompt-builder-manifest.json +15 -15
- package/exports/slates-prompt-builder/generated/slates-prompt-builder.skill +0 -0
- package/package.json +83 -73
- package/skills/_partials/decision-log.md +5 -4
- package/skills/_partials/thresholds.md +19 -0
- package/skills/slates-content-policy.md +15 -1
- package/skills/slates-cost-discipline.md +26 -4
- package/skills/slates-model-selection.md +2 -2
- package/skills/slates-one-prompt-film.md +20 -12
- package/skills/slates-project-organization.md +1 -1
- package/skills/slates-prompting-elevenlabs.md +61 -2
- package/skills/slates-prompting-flux-2-max.md +39 -0
- package/skills/slates-prompting-gpt-image-2.md +109 -70
- package/skills/slates-prompting-inworld-tts.md +166 -0
- package/skills/slates-prompting-kling-v3.md +39 -0
- package/skills/slates-prompting-lip-sync.md +38 -0
- package/skills/slates-prompting-ltx-2-5.md +38 -0
- package/skills/slates-prompting-minimax-h3.md +39 -0
- package/skills/slates-prompting-motion-transfer.md +38 -0
- package/skills/slates-prompting-nano-banana-2.md +26 -0
- package/skills/slates-prompting-omni-flash.md +41 -0
- package/skills/slates-prompting-seed-audio.md +38 -0
- package/skills/slates-prompting-seedance-2-5.md +38 -0
- package/skills/slates-prompting-seedance.md +26 -0
- package/skills/slates-prompting-seedream-5-lite.md +38 -0
- package/skills/slates-prompting-veo-3.md +39 -0
- package/skills/slates-shot-variety.md +53 -0
- package/skills/slates-storyboard-from-script.md +31 -15
- package/skills/slates-style-prompting.md +1 -1
- package/skills/slates-vision-feedback-loop.md +1 -1
|
@@ -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), ' +
|
package/dist/prompts/index.d.ts
CHANGED
package/dist/prompts/index.js
CHANGED
|
@@ -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
|
|
@@ -49,6 +49,41 @@ export interface VideoResolutionCapability {
|
|
|
49
49
|
default?: VideoResolution;
|
|
50
50
|
}
|
|
51
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
|
+
}
|
|
52
87
|
export interface ModelCapability {
|
|
53
88
|
aspectRatios: AspectRatio[];
|
|
54
89
|
/** Provider-keyed overrides. `fal` is the one that matters — see AGENT_ROUTE_PROVIDER. */
|
|
@@ -69,6 +104,14 @@ export interface ModelCapability {
|
|
|
69
104
|
maxReferenceVideoSeconds?: number;
|
|
70
105
|
/** Combined seconds across every reference audio clip. */
|
|
71
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;
|
|
72
115
|
}
|
|
73
116
|
/**
|
|
74
117
|
* The provider every AGENT generation actually lands on for Kling and Veo.
|
|
@@ -86,6 +129,15 @@ export interface ModelCapability {
|
|
|
86
129
|
export declare const AGENT_ROUTE_PROVIDER = "fal";
|
|
87
130
|
export declare const MODEL_CAPABILITIES: Record<string, ModelCapability>;
|
|
88
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;
|
|
89
141
|
/** Aspect ratios a model accepts, honouring the provider override. */
|
|
90
142
|
export declare function aspectRatiosFor(model: string, provider?: string): AspectRatio[];
|
|
91
143
|
/** Video resolutions a model accepts. A FIXED model reports exactly its one value. */
|
|
@@ -481,11 +481,53 @@ export const MODEL_CAPABILITIES = {
|
|
|
481
481
|
'eleven-sfx': {
|
|
482
482
|
aspectRatios: [],
|
|
483
483
|
},
|
|
484
|
+
// ── Text-to-speech ─────────────────────────────────────────────────────────
|
|
485
|
+
//
|
|
486
|
+
// The TTS seat. `maxCharacters` is the one number the billing bucket is sized
|
|
487
|
+
// against, and it is MEASURED: the API rejects 2,001 characters by name
|
|
488
|
+
// ("text length should not exceed 2000 characters"). Do not raise it from a
|
|
489
|
+
// docs page — raise it from a request that succeeds.
|
|
490
|
+
//
|
|
491
|
+
// ⚠️ NO `durationSeconds` HERE, and that is the shape of the surface rather
|
|
492
|
+
// than an omission: speech length falls out of the text, so this row bills on
|
|
493
|
+
// characters and has no duration dimension at all. Every derivation that
|
|
494
|
+
// switches on an audio surface must read the BILLING UNIT, never assume one.
|
|
495
|
+
'inworld-tts-2': {
|
|
496
|
+
aspectRatios: [],
|
|
497
|
+
maxCharacters: 2000,
|
|
498
|
+
voiceClone: {
|
|
499
|
+
// 5-15s of reference audio, ≤4 MB per sample — the vendor's documented
|
|
500
|
+
// spec, and a 12.6s / 555 KB sample cloned successfully against it.
|
|
501
|
+
minSeconds: 5,
|
|
502
|
+
maxSeconds: 15,
|
|
503
|
+
maxBytes: 4 * 1024 * 1024,
|
|
504
|
+
formats: ['wav', 'mp3', 'webm'],
|
|
505
|
+
// 🚨 MEASURED, and it appears in no documentation: the third clone inside
|
|
506
|
+
// one minute returned 429 "limit: 2, time window: m". This is workspace-
|
|
507
|
+
// wide, so it is shared across every Slates user.
|
|
508
|
+
clonesPerMinute: 2,
|
|
509
|
+
maxStoredVoices: 100,
|
|
510
|
+
// Measured: the design endpoint rejects a prompt outside these bounds by
|
|
511
|
+
// name ("design_prompt (Voice Description) must be between 7 and 1000").
|
|
512
|
+
designPromptChars: { min: 7, max: 1000 },
|
|
513
|
+
},
|
|
514
|
+
},
|
|
484
515
|
};
|
|
485
516
|
// ── Queries ──────────────────────────────────────────────────────────────────
|
|
486
517
|
export function getModelCapability(model) {
|
|
487
518
|
return MODEL_CAPABILITIES[model];
|
|
488
519
|
}
|
|
520
|
+
/**
|
|
521
|
+
* The voice-cloning spec for a TTS surface, or undefined for anything else.
|
|
522
|
+
*
|
|
523
|
+
* Exists so the desktop reads the reference-audio bounds, the design-prompt
|
|
524
|
+
* bounds and the clone rate limit from HERE rather than retyping them into a
|
|
525
|
+
* form control. A control whose limit disagrees with the vendor's is a limit
|
|
526
|
+
* the user first meets AFTER pressing the button.
|
|
527
|
+
*/
|
|
528
|
+
export function voiceCloneFor(model) {
|
|
529
|
+
return MODEL_CAPABILITIES[model]?.voiceClone;
|
|
530
|
+
}
|
|
489
531
|
/** Aspect ratios a model accepts, honouring the provider override. */
|
|
490
532
|
export function aspectRatiosFor(model, provider) {
|
|
491
533
|
const cap = MODEL_CAPABILITIES[model];
|
|
@@ -80,8 +80,4 @@ export declare const MODEL_FACTS: ModelFact[];
|
|
|
80
80
|
*/
|
|
81
81
|
export declare function describeRouting(kind: ModelFact['kind'], route?: ModelFact['route']): string;
|
|
82
82
|
export declare function getModelFact(id: string): ModelFact | undefined;
|
|
83
|
-
/** The official NB2 / general image prompt formula (subject-first). */
|
|
84
|
-
export declare const IMAGE_PROMPT_FORMULA = "[Subject] + [Action] + [Location/context] + [Composition] + [Style]";
|
|
85
|
-
/** The expanded cinematic/photoreal formula for NB2 start frames. */
|
|
86
|
-
export declare const CINEMATIC_IMAGE_FORMULA = "Film still from [DIRECTOR] [GENRE]. Shot on [CAMERA] with [LENS]. [SUBJECT and action]. [3-5 specific visual details]. [LIGHTING \u2014 direction + quality]. [COLOR PALETTE]. [FILM STOCK or sensor language]. [1-2 word emotional tone].";
|
|
87
83
|
//# sourceMappingURL=model-facts.d.ts.map
|
|
@@ -300,6 +300,14 @@ export const MODEL_FACTS = [
|
|
|
300
300
|
...caps('eleven-sfx'),
|
|
301
301
|
notes: 'ONE-SHOT SOUND EFFECT with an EXACT duration — route here for a single hit that must land on a frame (door slam, whoosh, impact, UI blip) or for a seamless loop. AUDIO-ONLY. For layered scenes with dialogue or room tone, seed-audio does it in one pass instead.',
|
|
302
302
|
},
|
|
303
|
+
{
|
|
304
|
+
id: 'inworld-tts-2',
|
|
305
|
+
route: 'generate',
|
|
306
|
+
label: 'Inworld Realtime TTS-2',
|
|
307
|
+
kind: 'audio',
|
|
308
|
+
...caps('inworld-tts-2'),
|
|
309
|
+
notes: 'THE VOICE SEAT — one named voice saying one line, billed per CHARACTER not per second. Route here when WHO is speaking matters. NOT scene audio — that is seed-audio; a single effect is eleven-sfx.',
|
|
310
|
+
},
|
|
303
311
|
];
|
|
304
312
|
const FACT_BY_ID = new Map(MODEL_FACTS.map((m) => [m.id, m]));
|
|
305
313
|
/**
|
|
@@ -321,8 +329,4 @@ export function describeRouting(kind, route = 'generate') {
|
|
|
321
329
|
export function getModelFact(id) {
|
|
322
330
|
return FACT_BY_ID.get(id);
|
|
323
331
|
}
|
|
324
|
-
/** The official NB2 / general image prompt formula (subject-first). */
|
|
325
|
-
export const IMAGE_PROMPT_FORMULA = '[Subject] + [Action] + [Location/context] + [Composition] + [Style]';
|
|
326
|
-
/** The expanded cinematic/photoreal formula for NB2 start frames. */
|
|
327
|
-
export const CINEMATIC_IMAGE_FORMULA = 'Film still from [DIRECTOR] [GENRE]. Shot on [CAMERA] with [LENS]. [SUBJECT and action]. [3-5 specific visual details]. [LIGHTING — direction + quality]. [COLOR PALETTE]. [FILM STOCK or sensor language]. [1-2 word emotional tone].';
|
|
328
332
|
//# sourceMappingURL=model-facts.js.map
|
|
@@ -5,12 +5,13 @@
|
|
|
5
5
|
// per-model skills, so the TS consumers and the markdown consumers can
|
|
6
6
|
// no longer disagree. Edit the partial, not this file, not the skills.
|
|
7
7
|
export const PARTIALS = {
|
|
8
|
-
"decision-log": "When you surface the plan, include a short **decision log** — one line per decision *you* made that the user did not specify
|
|
8
|
+
"decision-log": "When you surface the plan, include a short **decision log** — one line per decision *you* made that the user did not specify **and that no row already records**:\n\n```\nsource phrase or declared default → what you wrote → what it resolves\n\"in a diner\" → warm, and the light is the reason → why the anchor was chosen, not what it is\n(no time of day) → late afternoon, low warm key → default; say the word and it changes\n```\n\n🚨 **Keep it to what is NOT already data — and almost everything now IS.** A Shot holds the references and their roles, the model, every param, the shot size, the camera, the prop, the action and the spoken line, and `slates_list_shots` reads the whole board back in order with its variety counts. Narrating any of those is retelling a row the user can open. **Write the Shot, and let the log carry only the judgement no field holds** — why this world, why this light, why this register.\n\n**Hard rule: never silently add weather, props, style, or camera movement.** Four of those are now FIELDS: put the value on the Shot (`prop`, `camera`, `shotSize`, `action`) so the user can read and change it, and put the *reason* in the log only when you invented it rather than being told it. The rule has not softened — it moved from narration into data, which is stronger, because a field can be corrected and a sentence in chat cannot.\n\n> ❌ **Do NOT turn this into a question gate.** Clarifying questions before optimizing directly fight the locked fast-path rule: *if intent is clear, generate immediately with sane defaults, don't ask questions; only ask for production intent, and batch every question into one message.* Log the decisions, then go. The log is an **output**, not an interrogation — surfaced alongside the plan, never as a separate ceremony, and never as a reason to wait.",
|
|
9
9
|
"reference-rules-core": "Identity = a few flat-lit neutral angles; one reference per role, named inline; 2-4 refs not 12; describe environments instead of feeding a grid.\n\n1. **2-4 strong references beat both extremes.** Not 1 (warps toward itself), not 12 (averages worse). Start with 2-3 focused refs — each one adds context AND another variable to balance.\n2. **One reference per ROLE, named in the prompt** — identity / style-grade / environment. The model does **not** infer a reference's role from its position in the list; the inline name carries it. Same-role competitors drift (two \"identity\" refs of different people blend into a third face). Slates composes the naming for you from your `@mentions` / `#tags` — you never hand-write role labels.\n3. **One identity sheet per character, named inline.** A character's identity is a single asset (dominant portrait + body panels), so attach that one asset rather than a pile of views: **fewer competing renderings of a face is better, because the model cannot tell which one is authoritative and averages them.** Slates cites it as `Marcus (image 1)`. **Do NOT hand-write a \"Reference Image Instructions\" block or role essays** (\"use for identity, ignore the outfit, render a neutral expression\") — that drags the sheet's studio lighting and wardrobe into a scene that asked for neither. The prompt leads; the user's words own wardrobe, expression, lighting, and action.\n4. **Flat-light identity refs.** Prep identity references with flat, even, shadowless lighting on a plain neutral background. A studio-lit or scene-lit character sheet bleeds its lighting into every generation — the failure looks like the subject was green-screen-pasted in front of the location. Reference prep beats prompting here.\n5. **Environment: describe it, don't feed a grid.** Default to describing the location in words and let the model build a space that fits the shot. Reserve an environment reference for a mandatory exact-match, and then use ONE clean establishing image with natural ambient light that reads as the location's real light — never a multi-panel grid fed whole.\n6. **Grids: explore, don't input.** Use grids to explore compositions cheaply, then pick a cell. Never feed a grid back in as a reference — the cells share a split detail budget and were generated jointly, so their flaws propagate.\n7. **Reuse the same refs across every shot** in a sequence. Lock a set and keep it; swapping references mid-sequence causes drift, because the model adapts each reference to the current prompt rather than copying it.\n8. **Legible in-shot text → bake it into a still start frame, never trust text-to-video.** Have an image model render the text, then animate from that locked frame. Video models smear type.\n9. **Working from existing media — describe ONLY what changes.** The source already carries its composition, motion, timing, and performance; re-describing them fights the model. Narrate the delta. (Video lane: restyle your own clip while keeping the performance; delayed-VFX on \"video one\"; marker-object insertion; video-as-reference for a series.)\n10. **Style transforms happen in natural language.** By default the source's artistic medium and visual style are inherited. To change it, add a plain-text instruction (\"anime → real person\"). There are no preset pickers, and there is no style slider.",
|
|
10
10
|
"reference-tips-short": "Name each reference inline; never write role essays. Slates does this for you: `@mention` a subject or environment and it composes `Marcus (image 1) in the cafe (image 2)`, citing them in the exact order it sends them. One canonical identity image avoids competing facial renderings; a \"Reference Image Instructions\" block drags reference lighting into your scene. Start with 2-3 focused refs.",
|
|
11
11
|
"references-read-literally": "> **The general law: the model reads a reference literally.**\n> A reference image is not a suggestion. Whatever is baked into it — lighting, medium, texture, symmetry, competing identities — is read as a **property of the subject** and reproduced downstream. A baked rim light tints every shot made from that sheet. A sheet that looks like a 3D game render gets animated like game footage. Two competing renderings of one face get averaged into a third face.\n\nEvery reference rule below is a corollary of that one sentence, which is why \"prep the reference\" beats \"prompt around the reference\" every time:\n\n- **Flat, plain identity refs** — because scene lighting in the sheet becomes scene lighting in the output (Slates' own receipt: a studio-lit sheet produced a subject that looked green-screen-pasted in front of mountains).\n- **One authoritative rendering per subject** — because the model cannot tell which panel is the real one. ByteDance documents this failure directly: multi-view character assets \"confuse the model's character recognition, causing it to generate duplicate characters of the same appearance.\"\n- **No 3D-game-render look in a reference** — the model recognizes the render mood and inherits its motion character, so the *animation* comes out looking like game footage. This is not a taste rule; it is the same literal-reading mechanism applied to the temporal layer.\n- **Break perfect symmetry** — mirrored faces and dead-square framing read as synthetic, and the model preserves that reading rather than correcting it.\n\n**What this means in practice:** when output is wrong in a way that tracks the *subject* rather than the *scene* — the lighting is wrong the same way in every shot, the face drifts, the material looks synthetic everywhere — fix the reference, not the prompt. Prompting around a baked-in property is the expensive way to lose.",
|
|
12
12
|
"seedance-25-timestamps": "**2.0 does not respond to timestamps and answers only to shot numbers. 2.5 responds to\ninteger-second timestamps.** That is ByteDance's own first line under \"Differences from Seedance\n2.0\", and it is why a 30-second take is usable at all: the length is only worth buying if you can\nsay *when* things happen inside it.\n\nBoth formats are valid on 2.5, and you can mix them — `Shot N` blocks for a storyboard whose\npacing you are happy to leave to the model, timestamps when a beat has to land at a moment.\n\n**Three ways to control time, all first-party:**\n\n| Form | Write it like |\n|---|---|\n| **Interval** | `0-3 seconds… 3-7 seconds… 7-15 seconds` or `[1s-4s]… [4s-8s]… [8s-12s]` |\n| **Time point** | *\"Quick left sideways transition at the 5-second mark.\"* |\n| **Relative** | *\"After 3 seconds, everyone around him shakes their head.\"* · *\"The frame freezes for 1 second after he presses the shutter.\"* |\n\n**The rules that come with them:**\n\n- **One second is the smallest unit.** Integers only — no `2.5s`, no frames.\n- **No gaps in the timeline.** `0-3s… 5-6s…` leaves 3-5s unspecified and the model fills it however\n it likes. Intervals must abut: `0-3s`, `3-7s`, `7-15s`.\n- **Budget the plot to the seconds.** Too little content in a range and the model improvises to\n fill it; too much and you get extra cuts or dropped beats. This is the actual craft of a 30s take.\n- **Never time-code a high-frequency action.** *\"Shake your head three times per second\"* is\n explicitly called out as a misuse — timestamps schedule beats, they don't choreograph frames.\n- **Transitions want both halves:** the moment AND the method — *\"At the 5-second mark, the camera\n transitions leftward with a left wipe into a natural dissolve.\"*\n- **Timestamps work on an EDIT too**, and that is where they earn the most: they scope a change in\n time as well as in content — *\"Change the man's action from drinking coffee to mopping the floor\n from 4-6 seconds in Video 1, and leave the rest of the content unchanged.\"* Without a range, a\n whole-clip instruction is applied to the whole clip.\n\nDo **not** carry this back to 2.0, and do not carry Veo's `[00:00-00:02]` bracket syntax into\neither — 2.0 ignores time entirely, and the cross-model syntax swap is its own known failure.",
|
|
13
13
|
"seedance-25-timestamps-short": "Seedance 2.0 ignores timing and answers only to \"Shot 1 / Shot 2\"; 2.5 acts on whole-second timestamps, and that is what makes a 30-second take controllable rather than just long. Three forms work: intervals (\"0-3 seconds…3-7 seconds\"), a point (\"at the 5-second mark\"), or relative (\"after 3 seconds\"). Whole seconds only, no gaps between intervals, and never to choreograph fast repeated motion. They work on edits too, where a range scopes the change: \"…from 4-6 seconds…\".",
|
|
14
14
|
"still-gate": "**A visible defect in the still is already a STOP.** Do not animate it. Fix the frame first, then move to motion — and go to motion only when the crop passes the still scan and you genuinely need movement to confirm an uncertain edge, reflection, or object.\n\nThis is a **cost** rule as much as a craft rule: a 1080p/10s premium video generation costs many multiples of an image re-roll, and video is where a defect stops being fixable. Anything wrong in the still gets worse in motion — soft geometry mushes, broken-but-plausible objects fall apart, oily textures start crawling. **Animating a known-bad frame is the single most expensive mistake in the pipeline.** Re-rolling the image is the cheap move; re-rolling the video is not.",
|
|
15
|
+
"thresholds": "<!-- GENERATED from @slatesvideo/shared — do not edit between the markers.\n Source: CONFIRM_CREDITS, DEVIATION_FACTOR and the audio bounds in\n packages/shared/src/operations/index.ts. Every number here is REFUSED by an\n op when a prompt gets it wrong, which is why none of them is typed by hand\n any more: this block replaced four claims that contradicted the code. -->\n\n**The thresholds, from the code that enforces them:**\n\n- **Confirm gate:** above **17 credits** an op returns `requires_confirm` and will not\n proceed until you re-call with `confirm: true`. Below it, announce the cost once and go.\n- **Deviation pause:** the desktop Studio Agent stops and re-asks when projected generation spend\n exceeds the approved plan by more than **20%**. You do not trigger this; the app does.\n- **Seed Audio duration:** **3–120 seconds.** There is no duration\n parameter on the model — the number you pass is written into the prompt AND is what the user is\n billed. Outside that range the op refuses rather than clamping.\n- **Sound Effects duration:** **1–22 seconds**, billed per second, never left for the\n model to pick.\n\nNever quote a credit figure from memory: `slates_estimate_generation_cost` returns the real one.",
|
|
15
16
|
};
|
|
16
17
|
//# sourceMappingURL=partials.generated.js.map
|
|
@@ -16,7 +16,7 @@ export interface PromptingTipsEntry {
|
|
|
16
16
|
/** Footer callout paragraphs. */
|
|
17
17
|
footer?: string[];
|
|
18
18
|
}
|
|
19
|
-
export type PromptingTipsKey = 'seedance' | 'seedance-2-5' | 'seedance-2-5-edit' | 'kling' | 'kling-edit' | 'veo' | 'omni-flash' | 'omni-flash-edit' | 'minimax-h3' | 'ltx-2-5' | 'nano-banana' | 'nano-banana-lite' | 'seed-audio' | 'eleven-sfx';
|
|
19
|
+
export type PromptingTipsKey = 'seedance' | 'seedance-2-5' | 'seedance-2-5-edit' | 'kling' | 'kling-edit' | 'veo' | 'omni-flash' | 'omni-flash-edit' | 'minimax-h3' | 'ltx-2-5' | 'nano-banana' | 'nano-banana-lite' | 'seed-audio' | 'eleven-sfx' | 'inworld-tts-2';
|
|
20
20
|
export declare const PROMPTING_TIPS: Record<PromptingTipsKey, PromptingTipsEntry>;
|
|
21
21
|
/** Null when no tips exist for the key — callers render an honest fallback. */
|
|
22
22
|
export declare function getPromptingTips(key: string): PromptingTipsEntry | null;
|
|
@@ -704,6 +704,63 @@ const LTX_2_5 = {
|
|
|
704
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
705
|
],
|
|
706
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
|
+
};
|
|
707
764
|
export const PROMPTING_TIPS = {
|
|
708
765
|
seedance: SEEDANCE,
|
|
709
766
|
'seedance-2-5': SEEDANCE_25,
|
|
@@ -719,6 +776,7 @@ export const PROMPTING_TIPS = {
|
|
|
719
776
|
'nano-banana-lite': NANO_BANANA_LITE,
|
|
720
777
|
'seed-audio': SEED_AUDIO,
|
|
721
778
|
'eleven-sfx': ELEVEN_SFX,
|
|
779
|
+
'inworld-tts-2': INWORLD_TTS,
|
|
722
780
|
};
|
|
723
781
|
/** Null when no tips exist for the key — callers render an honest fallback. */
|
|
724
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
|
-
* 🚨
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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.
|
|
103
|
-
//
|
|
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).
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
|
|
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
|
-
|
|
126
|
-
return '';
|
|
182
|
+
return _full;
|
|
127
183
|
});
|
|
128
|
-
body = body.replace(
|
|
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
|
-
//
|
|
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
|
|
190
|
+
return _full;
|
|
136
191
|
}
|
|
137
192
|
matchedInPrompt.add(key);
|
|
138
193
|
if (g.kind === 'style')
|