@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.
- 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 +8 -2
- package/dist/index.js +44 -1
- package/dist/operations/index.d.ts +243 -31
- package/dist/operations/index.js +1483 -154
- package/dist/operations/surface.d.ts +69 -0
- package/dist/operations/surface.js +227 -0
- package/dist/prompts/agent-doctrine.d.ts +36 -0
- package/dist/prompts/agent-doctrine.js +201 -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 +40 -0
- package/dist/prompts/banned-tokens.js +219 -0
- 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 +65 -1
- package/dist/prompts/model-capabilities.js +139 -2
- package/dist/prompts/model-facts.d.ts +20 -4
- package/dist/prompts/model-facts.js +95 -27
- package/dist/prompts/partials.generated.js +2 -1
- package/dist/prompts/prompting-tips.d.ts +1 -1
- package/dist/prompts/prompting-tips.js +123 -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 -22
- package/exports/slates-prompt-builder/generated/SKILL.md +3 -3
- 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 +19 -1
- package/exports/slates-prompt-builder/generated/slates-prompt-builder-manifest.json +17 -17
- 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 +218 -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 +36 -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 +36 -1
- 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,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
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Role an attachment carries in the composer tray. User-set, never inferred.
|
|
3
|
+
*
|
|
4
|
+
* ๐จ THIS UNION IS THE ONE ROLE LIST. The desktop's
|
|
5
|
+
* `slate/src/shared/attachmentRoles.ts` derives every map from it with
|
|
6
|
+
* `satisfies Record<AttachmentRole, โฆ>`, so adding a role here is a COMPILE
|
|
7
|
+
* ERROR in the store buckets, the group kinds, the group names, the storage
|
|
8
|
+
* writer and the ops โ never a silent gap. It used to be defined in the
|
|
9
|
+
* desktop renderer; it moved here when `ShotSpec` needed to be keyed by it in
|
|
10
|
+
* both repos.
|
|
11
|
+
*
|
|
12
|
+
* `video-reference` / `audio-reference` are REFERENCE roles, not modes:
|
|
13
|
+
* attaching a clip as a reference leaves the create surface alone, while "Edit
|
|
14
|
+
* with AI" is the separate, deliberate choice that swaps the surface.
|
|
15
|
+
*/
|
|
16
|
+
export type AttachmentRole = 'reference' | 'first-frame' | 'last-frame' | 'subject' | 'style' | 'video-reference' | 'audio-reference';
|
|
17
|
+
/**
|
|
18
|
+
* The roles that hold an ORDERED, multi-occupancy list.
|
|
19
|
+
*
|
|
20
|
+
* Derived from the union by subtraction, never retyped: the two frame slots are
|
|
21
|
+
* scalars (single-occupancy, no order to change and no index to address), and a
|
|
22
|
+
* hand-written second union would be the exact hand-typed re-derivation this
|
|
23
|
+
* module exists to prevent.
|
|
24
|
+
*/
|
|
25
|
+
export type OrderedAttachmentRole = Exclude<AttachmentRole, 'first-frame' | 'last-frame'>;
|
|
26
|
+
/**
|
|
27
|
+
* Emission ORDER of the ordered roles โ the order `buildReferenceGroups` pushes
|
|
28
|
+
* them in, which is the order the composer numbers them in, which is the order
|
|
29
|
+
* the rail badges them in. Changing a number here changes what the model is
|
|
30
|
+
* told, so treat it exactly like the composer's own ordering.
|
|
31
|
+
*
|
|
32
|
+
* `satisfies Record<OrderedAttachmentRole, number>` is what makes a new role a
|
|
33
|
+
* compile error here rather than a missing bucket at runtime.
|
|
34
|
+
*/
|
|
35
|
+
export declare const ORDERED_ROLE_EMISSION: {
|
|
36
|
+
readonly reference: 0;
|
|
37
|
+
readonly subject: 1;
|
|
38
|
+
readonly style: 2;
|
|
39
|
+
readonly 'video-reference': 3;
|
|
40
|
+
readonly 'audio-reference': 4;
|
|
41
|
+
};
|
|
42
|
+
/** The ordered roles, in emission order. Sorted from the map above so the two
|
|
43
|
+
* cannot disagree โ never a second hand-written array. */
|
|
44
|
+
export declare const ORDERED_ATTACHMENT_ROLES: readonly OrderedAttachmentRole[];
|
|
45
|
+
/**
|
|
46
|
+
* Everything on the prompt bar that is NOT the prompt, the model or an
|
|
47
|
+
* attachment. Every field is optional and every field is a value the composer
|
|
48
|
+
* already persists into `settings_json` today โ this is a rename, not a new
|
|
49
|
+
* vocabulary.
|
|
50
|
+
*
|
|
51
|
+
* ๐จ NO INDEX SIGNATURE, DELIBERATELY. An open record would let a param be
|
|
52
|
+
* written that nothing downstream restores, which is the shape of the live
|
|
53
|
+
* reuse bug this whole plan starts from: persisted, never read, invisible.
|
|
54
|
+
* Adding a param to the prompt bar means adding it HERE and to the desktop's
|
|
55
|
+
* `applyShotParams`, in the same pass.
|
|
56
|
+
*/
|
|
57
|
+
export interface ShotParams {
|
|
58
|
+
aspectRatio?: string;
|
|
59
|
+
/** Image models (Nano Banana 2 &co) โ `1k` / `2k` / `4k`. */
|
|
60
|
+
imageResolution?: string;
|
|
61
|
+
videoResolution?: string;
|
|
62
|
+
quality?: string;
|
|
63
|
+
/** gpt-image-2's tier. Always sent explicitly: fal's own default is `high`. */
|
|
64
|
+
gptQuality?: 'medium' | 'high';
|
|
65
|
+
duration?: number;
|
|
66
|
+
imageQuantity?: number;
|
|
67
|
+
gridMode?: 'off' | '2x2' | '3x3';
|
|
68
|
+
negativePrompt?: string;
|
|
69
|
+
sound?: boolean;
|
|
70
|
+
audioLanguage?: string;
|
|
71
|
+
audioAccent?: string;
|
|
72
|
+
generateMusic?: boolean;
|
|
73
|
+
seedanceFace?: boolean;
|
|
74
|
+
multiShot?: boolean;
|
|
75
|
+
multiShotSegments?: Array<{
|
|
76
|
+
prompt: string;
|
|
77
|
+
duration: number;
|
|
78
|
+
camera: string;
|
|
79
|
+
shotSize: string;
|
|
80
|
+
}> | null;
|
|
81
|
+
cameraControls?: {
|
|
82
|
+
horizontal: number;
|
|
83
|
+
vertical: number;
|
|
84
|
+
pan: number;
|
|
85
|
+
tilt: number;
|
|
86
|
+
roll: number;
|
|
87
|
+
zoom: number;
|
|
88
|
+
};
|
|
89
|
+
/** Audio lane. On seed-audio the requested duration IS the bill. */
|
|
90
|
+
audioDurationSeconds?: number;
|
|
91
|
+
audioLoop?: boolean;
|
|
92
|
+
audioPromptInfluence?: number;
|
|
93
|
+
audioMultilingual?: boolean;
|
|
94
|
+
}
|
|
95
|
+
/** Prompt-owned identity โ the entities the prompt text NAMES. */
|
|
96
|
+
export interface ShotMentions {
|
|
97
|
+
characterIds: string[];
|
|
98
|
+
environmentIds: string[];
|
|
99
|
+
styleIds: string[];
|
|
100
|
+
}
|
|
101
|
+
export interface ShotSpec {
|
|
102
|
+
/** RAW prompt, `@mentions` intact. Never a composed one: the composer is the
|
|
103
|
+
* only thing that may number anything, and a stored "image 3" would be a
|
|
104
|
+
* lie the moment a reference is added, removed or reordered. */
|
|
105
|
+
prompt: string;
|
|
106
|
+
model: string | null;
|
|
107
|
+
/**
|
|
108
|
+
* The model the PROMPT WAS AUTHORED FOR. Never auto-rewritten.
|
|
109
|
+
*
|
|
110
|
+
* MiniMax H3 takes tagged `<Subject N>` references, `(S1)` speaker labels and
|
|
111
|
+
* `<d>[lang]โฆ</d>` dialogue; Seedance does not. A prompt authored for one and
|
|
112
|
+
* replayed on another is not merely suboptimal โ it can carry literal syntax
|
|
113
|
+
* the new model reads as text. Rewriting it would be prompt enhancement, the
|
|
114
|
+
* thing this codebase deleted on 2026-08-01. Record it, show it when it
|
|
115
|
+
* diverges from `model`, and leave the user's words alone.
|
|
116
|
+
*/
|
|
117
|
+
authoredFor: string | null;
|
|
118
|
+
params: ShotParams;
|
|
119
|
+
/** ENTITY ids, never flattened paths โ update the character and every Shot
|
|
120
|
+
* that mentions it updates with it. */
|
|
121
|
+
mentions: ShotMentions;
|
|
122
|
+
/** Attachment-owned refs as ASSET IDS, ordered within each role. Keyed by
|
|
123
|
+
* `OrderedAttachmentRole` so `ORDERED_ROLE_EMISSION` stays the ONE role
|
|
124
|
+
* list; a sixth role is a compile error here too. */
|
|
125
|
+
refs: Record<OrderedAttachmentRole, string[]>;
|
|
126
|
+
firstFrameAssetId: string | null;
|
|
127
|
+
lastFrameAssetId: string | null;
|
|
128
|
+
/**
|
|
129
|
+
* What is SAID in each reference-audio clip, keyed by its ASSET ID.
|
|
130
|
+
*
|
|
131
|
+
* ๐จ KEYED, NOT INDEX-ALIGNED. A parallel array desyncs on a single drag and
|
|
132
|
+
* then tells the model one clip's words over another clip โ silently. Its
|
|
133
|
+
* worst failure keyed is a stale entry, which composes as nothing.
|
|
134
|
+
*/
|
|
135
|
+
audioRefSpokenText: Record<string, string>;
|
|
136
|
+
/** Who speaks: an entity id, a bare name, the literal `VO`, or null. A name
|
|
137
|
+
* matching no character is a working state, not an error โ it renders as
|
|
138
|
+
* plain text and offers "make this a character". */
|
|
139
|
+
speaker: string | null;
|
|
140
|
+
/** What is said, verbatim. No camera, no scene, no prompt bloat โ this is
|
|
141
|
+
* the half a person reads aloud. */
|
|
142
|
+
line: string | null;
|
|
143
|
+
/** The parenthetical: how it is said. `(flat, exhausted)` */
|
|
144
|
+
delivery: string | null;
|
|
145
|
+
/** What happens in the shot, screenplay-style. ONE field: an action line
|
|
146
|
+
* already describes everyone in frame, and splitting out the non-speakers
|
|
147
|
+
* invents a distinction writers do not make. */
|
|
148
|
+
action: string | null;
|
|
149
|
+
/** The one readable object carrying the beat. */
|
|
150
|
+
prop: string | null;
|
|
151
|
+
/** Framing, FREE TEXT. Bucketed for counting by
|
|
152
|
+
* `@slatesvideo/shared/shot-grammar`; never constrained by it. */
|
|
153
|
+
shotSize: string | null;
|
|
154
|
+
/** Camera move, FREE TEXT. Same rule as `shotSize`. */
|
|
155
|
+
camera: string | null;
|
|
156
|
+
/**
|
|
157
|
+
* This row's line runs on from the previous row's โ one sentence, two cuts.
|
|
158
|
+
*
|
|
159
|
+
* ONE FLAG, NO OFFSETS. It says "these two rows are one sentence" without
|
|
160
|
+
* either row pointing into the other's text. Set by a mid-sentence split,
|
|
161
|
+
* editable by hand, and cleared on both sides when a move breaks the run.
|
|
162
|
+
* `heinrich-ad-prompting.md` ยง0a is built on exactly this move, so script โ
|
|
163
|
+
* cut is many-to-many and must stay that way.
|
|
164
|
+
*/
|
|
165
|
+
continues: boolean;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* What each script field MEANS, in one sentence โ the prose the op surface
|
|
169
|
+
* shows an agent for that parameter.
|
|
170
|
+
*
|
|
171
|
+
* It lives beside the fields because `satisfies Record<ScriptField, string>` is
|
|
172
|
+
* what makes a new field a compile error in the DESCRIPTIONS too. An op that
|
|
173
|
+
* hand-typed these would ship a ninth field with no explanation, which is the
|
|
174
|
+
* same failure as a column nothing renders.
|
|
175
|
+
*/
|
|
176
|
+
export declare const SCRIPT_FIELD_DESCRIPTION: {
|
|
177
|
+
readonly speaker: "Who says the line โ a character id, a bare name (a character that does not exist yet is fine), or \"VO\". Null for a shot with no words.";
|
|
178
|
+
readonly line: "What is SAID, verbatim. Never camera, scene or prompt language โ this is the half a person reads aloud.";
|
|
179
|
+
readonly delivery: "The parenthetical: how it is said. \"(flat, exhausted)\"";
|
|
180
|
+
readonly action: "What happens in the shot, screenplay-style. One line covering everyone in frame.";
|
|
181
|
+
readonly prop: "The one readable object carrying the beat.";
|
|
182
|
+
readonly shotSize: "Framing, in your own words โ \"wide\", \"long-lens CU, other head blurred\". FREE TEXT: it is bucketed for the variety count and never rejected or rewritten.";
|
|
183
|
+
readonly camera: "Camera move, in your own words โ \"slow push in\", \"through the rearview, eyes only\". FREE TEXT, same rule as shotSize.";
|
|
184
|
+
readonly continues: "True when this row's line runs on from the previous row's โ one sentence split across two cuts. The signature VO move; set it deliberately.";
|
|
185
|
+
};
|
|
186
|
+
/** The script fields, as a list. Sorted from the description map so the two
|
|
187
|
+
* cannot disagree โ never a second hand-written array. */
|
|
188
|
+
export type ScriptField = 'speaker' | 'line' | 'delivery' | 'action' | 'prop' | 'shotSize' | 'camera' | 'continues';
|
|
189
|
+
/** The seven free-text script fields (everything but the `continues` flag) โ
|
|
190
|
+
* the set an op accepts as `string | null` and a layer renders as text. */
|
|
191
|
+
export declare const SCRIPT_TEXT_FIELDS: readonly ["speaker", "line", "delivery", "action", "prop", "shotSize", "camera"];
|
|
192
|
+
export type ScriptTextField = (typeof SCRIPT_TEXT_FIELDS)[number];
|
|
193
|
+
/**
|
|
194
|
+
* The script, composed into prompt prose. **The one derivation, mirrored in
|
|
195
|
+
* both repos**, so the desktop, `slates_get_shot` and the generation handler
|
|
196
|
+
* cannot disagree about what a scripted Shot sends.
|
|
197
|
+
*
|
|
198
|
+
* ๐จ IT IS A TEMPLATE, NOT A WRITER. Deterministic, inspectable, no model. It
|
|
199
|
+
* orders the fields the way a shot is actually described โ what the camera is
|
|
200
|
+
* doing, what happens, then who says what โ and does nothing else. It never
|
|
201
|
+
* invents adjectives, never "enhances", and never reorders a sentence the user
|
|
202
|
+
* wrote. That is the prompt-transparency invariant: Slates may compose, and
|
|
203
|
+
* every composed character has to be visible in the composer before Generate.
|
|
204
|
+
*
|
|
205
|
+
* ๐จ AND IT ONLY FIRES WHEN `prompt` IS EMPTY. A Shot that carries an authored
|
|
206
|
+
* prompt keeps it byte-for-byte โ which is what makes this safe to ship to live
|
|
207
|
+
* users with no migration. `prompt` wins because it is the more specific
|
|
208
|
+
* statement of intent; the script still counts, still fits, still reads.
|
|
209
|
+
*
|
|
210
|
+
* Dialogue is quoted so a model receives it as speech rather than as
|
|
211
|
+
* description โ the one piece of grammar this adds, and the reason it is not
|
|
212
|
+
* just `join(' ')`.
|
|
213
|
+
*/
|
|
214
|
+
export declare function scriptPromptBody(spec: Pick<ShotSpec, ScriptTextField>): string;
|
|
215
|
+
/**
|
|
216
|
+
* What this Shot actually sends: the authored prompt, or the script composed
|
|
217
|
+
* into one. Every consumer calls THIS, never `spec.prompt` directly โ a reader
|
|
218
|
+
* that reached past it would be the surface that still says "No prompt yet"
|
|
219
|
+
* while the row plainly has a script in it.
|
|
220
|
+
*/
|
|
221
|
+
export declare function effectivePrompt(spec: ShotSpec): string;
|
|
222
|
+
/** An empty Shot: a prompt bar nobody has touched. Every reader starts here and
|
|
223
|
+
* overlays what it actually found, so a missing field is never `undefined`
|
|
224
|
+
* leaking into a request. */
|
|
225
|
+
export declare function emptyShotSpec(): ShotSpec;
|
|
226
|
+
/**
|
|
227
|
+
* Read a `ShotSpec` out of whatever is on disk โ a row written by an older
|
|
228
|
+
* build, a partial object from an op, `null`.
|
|
229
|
+
*
|
|
230
|
+
* TOLERANT ON PURPOSE (invariant 7: a Shot that cannot currently fire still
|
|
231
|
+
* loads). A missing role, an unknown key, a string where an array belongs โ
|
|
232
|
+
* none of them may throw, because the one thing worse than a degraded Shot is a
|
|
233
|
+
* Shots list that will not open.
|
|
234
|
+
*/
|
|
235
|
+
export declare function normalizeShotSpec(raw: unknown): ShotSpec;
|
|
236
|
+
/** Every asset id a Shot references, deduped, in emission order then frames.
|
|
237
|
+
*
|
|
238
|
+
* ๐จ THIS IS THE INPUT TO THE FK MIRROR. `refs_json` is a JSON blob and is
|
|
239
|
+
* therefore INVISIBLE to the desktop's runtime FK classifier
|
|
240
|
+
* (`storage/assetReferences.ts` walks `PRAGMA foreign_key_list`), so a Shot's
|
|
241
|
+
* references would not block a cross-project move and the asset's file would
|
|
242
|
+
* be relocated out from under it. `shot_assets` is the visible mirror, and
|
|
243
|
+
* this function is the ONE place its row set is derived. */
|
|
244
|
+
export declare function shotAssetIds(spec: ShotSpec): string[];
|
|
245
|
+
/** Total attachment count โ what a list row shows without composing anything. */
|
|
246
|
+
export declare function shotRefCount(spec: ShotSpec): number;
|
|
247
|
+
/**
|
|
248
|
+
* What each role MEANS, in one sentence โ the prose the op surface shows an
|
|
249
|
+
* agent for that parameter.
|
|
250
|
+
*
|
|
251
|
+
* It lives here, beside the union, because `satisfies Record<AttachmentRole,
|
|
252
|
+
* string>` is what makes a new role a compile error in the DESCRIPTIONS too. An
|
|
253
|
+
* op that hand-typed these would silently ship a seventh role with no
|
|
254
|
+
* explanation, which is the same failure as a bucket nobody wired up.
|
|
255
|
+
*/
|
|
256
|
+
export declare const ATTACHMENT_ROLE_DESCRIPTION: {
|
|
257
|
+
readonly reference: "Plain reference images, in send order โ cited in the prompt as \"image 1\", \"image 2\"โฆ";
|
|
258
|
+
readonly subject: "Reference images that ARE the subject โ composed as \"Image N is the subject\", so the model knows who the shot is about.";
|
|
259
|
+
readonly style: "Reference images the look is taken from โ composed as one trailing \"Render in the visual style of image N\" clause.";
|
|
260
|
+
readonly 'video-reference': "Reference CLIPS read alongside the images โ cited as \"video 1\", \"video 2\"โฆ";
|
|
261
|
+
readonly 'audio-reference': "Reference AUDIO clips โ cited as \"audio 1\", \"audio 2\"โฆ. Pair each with audioRefSpokenText when it contains speech.";
|
|
262
|
+
readonly 'first-frame': "The starting frame for image-to-video.";
|
|
263
|
+
readonly 'last-frame': "The ending frame for image-to-video.";
|
|
264
|
+
};
|
|
265
|
+
//# sourceMappingURL=shot-spec.d.ts.map
|