@slatesvideo/shared 0.6.3 β†’ 0.6.5

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 (79) hide show
  1. package/dist/api-url.d.ts +11 -0
  2. package/dist/api-url.js +11 -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 +7 -2
  9. package/dist/index.js +20 -2
  10. package/dist/manual/content.d.ts +2 -0
  11. package/dist/manual/content.js +3 -0
  12. package/dist/manual/index.d.ts +5 -0
  13. package/dist/manual/index.js +20 -0
  14. package/dist/operations/index.d.ts +253 -30
  15. package/dist/operations/index.js +1387 -141
  16. package/dist/operations/surface.d.ts +69 -0
  17. package/dist/operations/surface.js +227 -0
  18. package/dist/prompts/agent-doctrine.js +8 -0
  19. package/dist/prompts/asset-label.d.ts +23 -0
  20. package/dist/prompts/asset-label.js +70 -0
  21. package/dist/prompts/banned-tokens.d.ts +15 -3
  22. package/dist/prompts/banned-tokens.js +76 -9
  23. package/dist/prompts/character-sheet.d.ts +0 -2
  24. package/dist/prompts/character-sheet.js +0 -2
  25. package/dist/prompts/craft-cards.d.ts +20 -0
  26. package/dist/prompts/craft-cards.js +82 -0
  27. package/dist/prompts/environment-sheet.js +16 -0
  28. package/dist/prompts/index.d.ts +1 -0
  29. package/dist/prompts/index.js +4 -0
  30. package/dist/prompts/model-capabilities.d.ts +52 -0
  31. package/dist/prompts/model-capabilities.js +42 -0
  32. package/dist/prompts/model-facts.d.ts +0 -4
  33. package/dist/prompts/model-facts.js +8 -4
  34. package/dist/prompts/partials.generated.js +2 -1
  35. package/dist/prompts/prompting-tips.d.ts +1 -1
  36. package/dist/prompts/prompting-tips.js +58 -0
  37. package/dist/prompts/reference-composer.d.ts +36 -7
  38. package/dist/prompts/reference-composer.js +75 -20
  39. package/dist/prompts/reference-rules.d.ts +15 -26
  40. package/dist/prompts/reference-rules.js +15 -93
  41. package/dist/prompts/shot-grammar.d.ts +154 -0
  42. package/dist/prompts/shot-grammar.js +184 -0
  43. package/dist/prompts/shot-spec.d.ts +278 -0
  44. package/dist/prompts/shot-spec.js +319 -0
  45. package/dist/skills/content.js +25 -23
  46. package/exports/slates-prompt-builder/generated/reference-content-policy.md +6 -0
  47. package/exports/slates-prompt-builder/generated/reference-kling.md +22 -0
  48. package/exports/slates-prompt-builder/generated/reference-nano-banana.md +17 -0
  49. package/exports/slates-prompt-builder/generated/reference-seedance.md +17 -0
  50. package/exports/slates-prompt-builder/generated/slates-prompt-builder-manifest.json +15 -15
  51. package/exports/slates-prompt-builder/generated/slates-prompt-builder.skill +0 -0
  52. package/package.json +83 -73
  53. package/skills/_partials/decision-log.md +5 -4
  54. package/skills/_partials/thresholds.md +19 -0
  55. package/skills/slates-content-policy.md +15 -1
  56. package/skills/slates-cost-discipline.md +26 -4
  57. package/skills/slates-model-selection.md +4 -3
  58. package/skills/slates-one-prompt-film.md +20 -12
  59. package/skills/slates-project-organization.md +1 -1
  60. package/skills/slates-prompting-elevenlabs.md +61 -2
  61. package/skills/slates-prompting-flux-2-max.md +39 -0
  62. package/skills/slates-prompting-gpt-image-2.md +109 -70
  63. package/skills/slates-prompting-inworld-tts.md +174 -0
  64. package/skills/slates-prompting-kling-v3.md +39 -0
  65. package/skills/slates-prompting-lip-sync.md +38 -0
  66. package/skills/slates-prompting-ltx-2-5.md +38 -0
  67. package/skills/slates-prompting-minimax-h3.md +39 -0
  68. package/skills/slates-prompting-motion-transfer.md +38 -0
  69. package/skills/slates-prompting-nano-banana-2.md +26 -0
  70. package/skills/slates-prompting-omni-flash.md +41 -0
  71. package/skills/slates-prompting-seed-audio.md +39 -1
  72. package/skills/slates-prompting-seedance-2-5.md +38 -0
  73. package/skills/slates-prompting-seedance.md +26 -0
  74. package/skills/slates-prompting-seedream-5-lite.md +38 -0
  75. package/skills/slates-prompting-veo-3.md +39 -0
  76. package/skills/slates-shot-variety.md +53 -0
  77. package/skills/slates-storyboard-from-script.md +31 -15
  78. package/skills/slates-style-prompting.md +1 -1
  79. package/skills/slates-vision-feedback-loop.md +1 -1
@@ -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.`;
@@ -0,0 +1,154 @@
1
+ /**
2
+ * SHOT GRAMMAR β€” the buckets that COUNT framing, and the measured speech rate.
3
+ *
4
+ * 🚨 KEEP THIS A DEPENDENCY-FREE LEAF, exported from its own subpath
5
+ * (`@slatesvideo/shared/shot-grammar`). The desktop imports it from
6
+ * `slate/src/shared/` for the variety counter and the fit check, and the same
7
+ * two reasons that made `model-capabilities.ts` a leaf apply verbatim: the root
8
+ * barrel re-exports `auth.js` β†’ `node:fs` (breaks renderer bundling) and
9
+ * `/prompts` drags the 5,000-word tips corpus in with no `require` condition.
10
+ *
11
+ * 🚨 BUCKETS EXIST TO COUNT, NEVER TO CONSTRAIN. `rules/ads/cinematic-storyboard.md`
12
+ * Β§4e, reconciled against ByteDance's own ModelArk docs: *"Camera vocabulary is
13
+ * open standard language and includes shot size (close-up / medium / wide) β€”
14
+ * not a closed list of eight moves."* Authoring `shotSize` and `camera` is FREE
15
+ * TEXT. A value matching no bucket counts as `other` and is never rejected,
16
+ * rewritten, or warned about. This module is the ONE home for these names β€”
17
+ * a second bucket list anywhere downstream is the drift `MODEL_CAPABILITIES`
18
+ * was created to end.
19
+ */
20
+ export declare const SHOT_SIZE_BUCKETS: readonly ["wide", "medium", "close", "extreme-close", "other"];
21
+ export type ShotSizeBucket = (typeof SHOT_SIZE_BUCKETS)[number];
22
+ export declare const CAMERA_MOVE_BUCKETS: readonly ["static", "push", "pull", "handheld", "orbit", "crane", "other"];
23
+ export type CameraMoveBucket = (typeof CAMERA_MOVE_BUCKETS)[number];
24
+ /** Which shot-size bucket a free-text value counts in. Never rejects. */
25
+ export declare function bucketShotSize(raw: string | null | undefined): ShotSizeBucket;
26
+ /** Which camera-move bucket a free-text value counts in. Never rejects. */
27
+ export declare function bucketCameraMove(raw: string | null | undefined): CameraMoveBucket;
28
+ /** Human label for a bucket, for a header strip or an op result. Derived from
29
+ * the bucket name so a seventh bucket cannot ship without a label. */
30
+ export declare function bucketLabel(bucket: ShotSizeBucket | CameraMoveBucket): string;
31
+ /**
32
+ * 🚨 MEASURED, NOT CITED β€” and the corpus is ours.
33
+ *
34
+ * An earlier draft of the plan that introduced this said no source existed and
35
+ * deferred the fit check. That was a claim about the world made without looking:
36
+ * `second-brain/business/projects/slates/content-strategy/examples/ad-research.db`
37
+ * carries `transcript` and `duration_seconds` on every row and always did.
38
+ *
39
+ * ⚠️ IT IS A CORPUS STATISTIC, SO IT DECAYS AND MUST STAY RE-DERIVABLE.
40
+ * `SPEECH_RATE_QUERY` below is the exact derivation; a constant whose query no
41
+ * longer reproduces it is STALE, not wrong β€” update the number and the `n`
42
+ * together. Same discipline as the pSEO `verifiedOn` dates. What it must never
43
+ * become is a hand-typed figure nobody can reproduce.
44
+ *
45
+ * 🚨 THE SAMPLE IS OPT-IN, AND THAT IS THE WHOLE POINT (2026-09-04, Eric).
46
+ * A row counts ONLY if its `speech_rate` column names a register. The query
47
+ * used to take every row carrying a transcript, which coupled two things that
48
+ * have no business touching: **researching a new ad turned a Slates RELEASE
49
+ * BUILD red.** `predist` β†’ `check:all` β†’ `check:shot-list` re-derives from the
50
+ * vault, so two ads landing in the corpus blocked `npm run dist` on work that
51
+ * had nothing to do with the desktop app. Opt-in decouples them β€” new research
52
+ * changes nothing until it is marked, so a red check is a deliberate act again
53
+ * rather than noise, and blocking on it is finally correct.
54
+ *
55
+ * Three things the old unfiltered query got wrong, all fixed by the mark:
56
+ * 1. **wpm is words Γ· TOTAL RUNTIME, not speaking time**, so a silence-heavy or
57
+ * music-only ad reads as a slow talker. The corpus really held a 179s row at
58
+ * 55 wpm and a 16s row at 26 wpm. Nobody speaks at 26 wpm.
59
+ * 2. **`has_voiceover` was SELECTed and never filtered on** β€” 9 of the 74 rows
60
+ * had no voiceover at all. (One row's value is the free text
61
+ * `'yes (scripted dialogue/interviews)'`, so a naive boolean filter would
62
+ * have dropped a real read as well.)
63
+ * 3. **The register was a creator-name regex on free text.** `%AI Video
64
+ * Bootcamp%` happened to catch three different spellings of one person, and
65
+ * `conversational` was the median over EVERYTHING β€” the other two registers
66
+ * included. Each ad now names its own register and belongs to exactly one.
67
+ *
68
+ * Honest about the size of it: the medians barely moved (156 β†’ 159) and the
69
+ * CEILING β€” the only number that ever flags a line β€” is 283 under every
70
+ * definition tried, because the fastest read was always a genuine voiceover
71
+ * row. The accuracy was never really the problem. The coupling was.
72
+ */
73
+ export interface SpeechRate {
74
+ /** Words per minute. */
75
+ readonly wpm: number;
76
+ /** How many ads in the corpus this segment covers. */
77
+ readonly n: number;
78
+ /** Which rows `SPEECH_RATE_QUERY` was segmented to. */
79
+ readonly segment: string;
80
+ }
81
+ /**
82
+ * The derivation, verbatim, so gate 13a can re-run it.
83
+ *
84
+ * Word count is a WHITESPACE SPLIT of the trimmed transcript (`/\s+/`), not a
85
+ * space count β€” transcripts carry newlines, and counting only ' ' undercounts
86
+ * every multi-line row and drags the median down about ten wpm.
87
+ */
88
+ export declare const SPEECH_RATE_QUERY: string;
89
+ /** When the numbers below were last derived from the query above. */
90
+ export declare const SPEECH_RATE_MEASURED_ON = "2026-09-04";
91
+ /**
92
+ * πŸ”‘ THE REGISTER SPLIT IS REAL AND MEASURED, which is why there is no single
93
+ * number. the performed, genre-acted reads run a median of 133 wpm;
94
+ * the hard-sell direct response runs 169 β€” thirty-six words a
95
+ * minute apart on the same runtime. A single point value would have been worse
96
+ * than none.
97
+ */
98
+ export declare const SPEECH_RATE: {
99
+ readonly performed: {
100
+ readonly wpm: 133;
101
+ readonly n: 4;
102
+ readonly segment: "speech_rate = 'performed'";
103
+ };
104
+ readonly conversational: {
105
+ readonly wpm: 159;
106
+ readonly n: 41;
107
+ readonly segment: "speech_rate = 'conversational'";
108
+ };
109
+ readonly direct_response: {
110
+ readonly wpm: 169;
111
+ readonly n: 20;
112
+ readonly segment: "speech_rate = 'direct_response'";
113
+ };
114
+ /**
115
+ * The fastest read in the whole corpus. **The fit check flags only ABOVE
116
+ * this** β€” that is what "cannot fit at any plausible delivery" means. Not
117
+ * 250, not p90: those are rates real ads actually hit, and flagging an
118
+ * achievable read is exactly how a check gets ignored.
119
+ */
120
+ readonly ceiling: {
121
+ readonly wpm: 283;
122
+ readonly n: 65;
123
+ readonly segment: "max over every marked row";
124
+ };
125
+ };
126
+ export type SpeechRegister = Exclude<keyof typeof SPEECH_RATE, 'ceiling'>;
127
+ /**
128
+ * The default register, and there is deliberately no picker for it.
129
+ *
130
+ * It is the corpus median and the safest of the three. A control would be a
131
+ * preference widget in a feature whose whole argument is that fewer required
132
+ * choices is better. If a project's own measured rate ever justifies one, that
133
+ * is a later change with its own evidence.
134
+ */
135
+ export declare const DEFAULT_SPEECH_REGISTER: SpeechRegister;
136
+ /** How many words fit in `seconds` at a register's pace. `null` when there is
137
+ * no duration β€” a cut with no model has none, and inventing one would lie. */
138
+ export declare function wordsForDuration(seconds: number | null | undefined, register?: SpeechRegister): number | null;
139
+ /** Words in a spoken line. One definition, shared by the header and the flag. */
140
+ export declare function countWords(line: string | null | undefined): number;
141
+ /**
142
+ * Does this line fit this cut? `false` ONLY when it cannot fit at ANY plausible
143
+ * delivery β€” above the fastest read in every ad marked for the sample.
144
+ *
145
+ * 🚨 THE CLAIM IS DELIBERATELY WEAK SO THE CITATION CAN BE WEAK. Never flag "a
146
+ * bit long"; a check that nags gets ignored, and then it is worse than absent.
147
+ * Returns `null` when it cannot be decided (no line, or no duration).
148
+ */
149
+ export declare function lineFitsCut(line: string | null | undefined, seconds: number | null | undefined): {
150
+ fits: boolean;
151
+ words: number;
152
+ requiredWpm: number;
153
+ } | null;
154
+ //# sourceMappingURL=shot-grammar.d.ts.map
@@ -0,0 +1,184 @@
1
+ /**
2
+ * SHOT GRAMMAR β€” the buckets that COUNT framing, and the measured speech rate.
3
+ *
4
+ * 🚨 KEEP THIS A DEPENDENCY-FREE LEAF, exported from its own subpath
5
+ * (`@slatesvideo/shared/shot-grammar`). The desktop imports it from
6
+ * `slate/src/shared/` for the variety counter and the fit check, and the same
7
+ * two reasons that made `model-capabilities.ts` a leaf apply verbatim: the root
8
+ * barrel re-exports `auth.js` β†’ `node:fs` (breaks renderer bundling) and
9
+ * `/prompts` drags the 5,000-word tips corpus in with no `require` condition.
10
+ *
11
+ * 🚨 BUCKETS EXIST TO COUNT, NEVER TO CONSTRAIN. `rules/ads/cinematic-storyboard.md`
12
+ * Β§4e, reconciled against ByteDance's own ModelArk docs: *"Camera vocabulary is
13
+ * open standard language and includes shot size (close-up / medium / wide) β€”
14
+ * not a closed list of eight moves."* Authoring `shotSize` and `camera` is FREE
15
+ * TEXT. A value matching no bucket counts as `other` and is never rejected,
16
+ * rewritten, or warned about. This module is the ONE home for these names β€”
17
+ * a second bucket list anywhere downstream is the drift `MODEL_CAPABILITIES`
18
+ * was created to end.
19
+ */
20
+ // ── Shot size ──────────────────────────────────────────────────────
21
+ export const SHOT_SIZE_BUCKETS = [
22
+ 'wide',
23
+ 'medium',
24
+ 'close',
25
+ 'extreme-close',
26
+ 'other',
27
+ ];
28
+ // ── Camera move ────────────────────────────────────────────────────
29
+ export const CAMERA_MOVE_BUCKETS = [
30
+ 'static',
31
+ 'push',
32
+ 'pull',
33
+ 'handheld',
34
+ 'orbit',
35
+ 'crane',
36
+ 'other',
37
+ ];
38
+ /**
39
+ * The vocabulary each bucket answers to, as whole-word patterns.
40
+ *
41
+ * 🚨 NO TWO-LETTER ABBREVIATIONS β€” no `CU`, `ECU`, `MS`, `WS`, `LS`. They
42
+ * collide with ordinary words, they collide with EACH OTHER across shooting
43
+ * conventions (`MS` is a medium shot to one crew and a master to another), and
44
+ * a value bucketed WRONG is worse than one bucketed `other`: the whole claim of
45
+ * the variety check is that it is arithmetic anyone can verify by eye. `other`
46
+ * says "I did not recognise this"; a wrong bucket says something false.
47
+ * `cinematic-storyboard.md` Β§1's own example β€” `long-lens CU, other head
48
+ * blurred` β€” is deliberately `other`, and that is the correct answer.
49
+ *
50
+ * `satisfies Record<Exclude<Bucket, 'other'>, string[]>` on both records is
51
+ * what makes a SEVENTH bucket a compile error here rather than a name nothing
52
+ * ever matches. `other` is excluded because it is the fallback, by definition
53
+ * the bucket with no vocabulary.
54
+ */
55
+ const SHOT_SIZE_VOCABULARY = {
56
+ // Longest/most specific first β€” `extreme close` must win over `close`.
57
+ 'extreme-close': ['extreme close', 'extreme-close', 'macro', 'insert'],
58
+ close: ['close up', 'close-up', 'closeup', 'close on', 'close', 'tight'],
59
+ medium: ['medium', 'mid shot', 'mid-shot', 'waist', 'cowboy', 'two shot', 'two-shot'],
60
+ wide: ['wide', 'establishing', 'long shot', 'long-shot', 'full shot', 'full-shot', 'master'],
61
+ };
62
+ const CAMERA_MOVE_VOCABULARY = {
63
+ // `pull` before `push` so "dolly out" is never eaten by a looser "dolly".
64
+ // The bare bucket name is in every list: `push` alone matched while `pull`
65
+ // alone did not, which is the kind of asymmetry nobody notices until a count
66
+ // is quietly wrong.
67
+ pull: ['pull back', 'pull-back', 'pull out', 'pull away', 'dolly out', 'track out', 'zoom out', 'pull'],
68
+ push: ['push in', 'push-in', 'push', 'dolly in', 'track in', 'punch in', 'creep in', 'zoom in'],
69
+ // No trailing space on `arc` β€” matching is word-boundary based, so a space in
70
+ // the needle is both redundant and a trap for the next editor.
71
+ orbit: ['orbit', 'arc', 'circle', 'revolve', 'around the'],
72
+ crane: ['crane', 'jib', 'boom', 'drone', 'aerial', 'overhead descend'],
73
+ handheld: ['handheld', 'hand-held', 'shaky', 'shoulder', 'verite', 'vΓ©ritΓ©'],
74
+ static: ['static', 'locked off', 'locked-off', 'lock off', 'tripod', 'still', 'no movement'],
75
+ };
76
+ /** Case- and punctuation-insensitive containment, on word boundaries so
77
+ * `wide` does not match `widescreen` and `close` does not match `closer`. */
78
+ function matches(haystack, needle) {
79
+ const escaped = needle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
80
+ return new RegExp(`(^|[^a-z0-9])${escaped}([^a-z0-9]|$)`, 'i').test(haystack);
81
+ }
82
+ function bucketBy(raw, vocabulary, fallback) {
83
+ if (!raw || !raw.trim())
84
+ return fallback;
85
+ const text = ` ${raw.toLowerCase().trim()} `;
86
+ for (const [bucket, words] of Object.entries(vocabulary)) {
87
+ for (const word of words)
88
+ if (matches(text, word))
89
+ return bucket;
90
+ }
91
+ return fallback;
92
+ }
93
+ /** Which shot-size bucket a free-text value counts in. Never rejects. */
94
+ export function bucketShotSize(raw) {
95
+ return bucketBy(raw, SHOT_SIZE_VOCABULARY, 'other');
96
+ }
97
+ /** Which camera-move bucket a free-text value counts in. Never rejects. */
98
+ export function bucketCameraMove(raw) {
99
+ return bucketBy(raw, CAMERA_MOVE_VOCABULARY, 'other');
100
+ }
101
+ /** Human label for a bucket, for a header strip or an op result. Derived from
102
+ * the bucket name so a seventh bucket cannot ship without a label. */
103
+ export function bucketLabel(bucket) {
104
+ return bucket.replace(/-/g, ' ');
105
+ }
106
+ /**
107
+ * The derivation, verbatim, so gate 13a can re-run it.
108
+ *
109
+ * Word count is a WHITESPACE SPLIT of the trimmed transcript (`/\s+/`), not a
110
+ * space count β€” transcripts carry newlines, and counting only ' ' undercounts
111
+ * every multi-line row and drags the median down about ten wpm.
112
+ */
113
+ export const SPEECH_RATE_QUERY = `
114
+ -- words-per-minute per ad, from second-brain/business/projects/slates/
115
+ -- content-strategy/examples/ad-research.db
116
+ -- words = transcript.trim().split(/\s+/).length (applied to each row)
117
+ -- wpm = words / (duration_seconds / 60)
118
+ SELECT id, speech_rate, duration_seconds, transcript
119
+ FROM ads
120
+ WHERE speech_rate IS NOT NULL
121
+ AND transcript IS NOT NULL AND TRIM(transcript) <> ''
122
+ AND duration_seconds IS NOT NULL AND duration_seconds > 0;
123
+ -- segments: speech_rate, which is one register per ad and never overlaps
124
+ -- statistic: median per register; ceiling = max over every marked row
125
+ `.trim();
126
+ /** When the numbers below were last derived from the query above. */
127
+ export const SPEECH_RATE_MEASURED_ON = '2026-09-04';
128
+ /**
129
+ * πŸ”‘ THE REGISTER SPLIT IS REAL AND MEASURED, which is why there is no single
130
+ * number. the performed, genre-acted reads run a median of 133 wpm;
131
+ * the hard-sell direct response runs 169 β€” thirty-six words a
132
+ * minute apart on the same runtime. A single point value would have been worse
133
+ * than none.
134
+ */
135
+ export const SPEECH_RATE = {
136
+ performed: { wpm: 133, n: 4, segment: "speech_rate = 'performed'" },
137
+ conversational: { wpm: 159, n: 41, segment: "speech_rate = 'conversational'" },
138
+ direct_response: { wpm: 169, n: 20, segment: "speech_rate = 'direct_response'" },
139
+ /**
140
+ * The fastest read in the whole corpus. **The fit check flags only ABOVE
141
+ * this** β€” that is what "cannot fit at any plausible delivery" means. Not
142
+ * 250, not p90: those are rates real ads actually hit, and flagging an
143
+ * achievable read is exactly how a check gets ignored.
144
+ */
145
+ ceiling: { wpm: 283, n: 65, segment: 'max over every marked row' },
146
+ };
147
+ /**
148
+ * The default register, and there is deliberately no picker for it.
149
+ *
150
+ * It is the corpus median and the safest of the three. A control would be a
151
+ * preference widget in a feature whose whole argument is that fewer required
152
+ * choices is better. If a project's own measured rate ever justifies one, that
153
+ * is a later change with its own evidence.
154
+ */
155
+ export const DEFAULT_SPEECH_REGISTER = 'conversational';
156
+ /** How many words fit in `seconds` at a register's pace. `null` when there is
157
+ * no duration β€” a cut with no model has none, and inventing one would lie. */
158
+ export function wordsForDuration(seconds, register = DEFAULT_SPEECH_REGISTER) {
159
+ if (seconds == null || !(seconds > 0))
160
+ return null;
161
+ return Math.round((SPEECH_RATE[register].wpm * seconds) / 60);
162
+ }
163
+ /** Words in a spoken line. One definition, shared by the header and the flag. */
164
+ export function countWords(line) {
165
+ if (!line)
166
+ return 0;
167
+ return line.trim().split(/\s+/).filter(Boolean).length;
168
+ }
169
+ /**
170
+ * Does this line fit this cut? `false` ONLY when it cannot fit at ANY plausible
171
+ * delivery β€” above the fastest read in every ad marked for the sample.
172
+ *
173
+ * 🚨 THE CLAIM IS DELIBERATELY WEAK SO THE CITATION CAN BE WEAK. Never flag "a
174
+ * bit long"; a check that nags gets ignored, and then it is worse than absent.
175
+ * Returns `null` when it cannot be decided (no line, or no duration).
176
+ */
177
+ export function lineFitsCut(line, seconds) {
178
+ const words = countWords(line);
179
+ if (words === 0 || seconds == null || !(seconds > 0))
180
+ return null;
181
+ const requiredWpm = Math.round(words / (seconds / 60));
182
+ return { fits: requiredWpm <= SPEECH_RATE.ceiling.wpm, words, requiredWpm };
183
+ }
184
+ //# sourceMappingURL=shot-grammar.js.map