@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
@@ -0,0 +1,278 @@
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
+ * The VOICE a text-to-speech Shot speaks in — exactly one of the three, the
96
+ * same three `slates_generate_audio` takes. A Shot that carried the words but
97
+ * not the voice would fire in whatever voice happened to be on the bar, which
98
+ * is not the recipe that was saved.
99
+ */
100
+ voiceId?: string;
101
+ voiceReferenceAssetId?: string;
102
+ voiceDescription?: string;
103
+ }
104
+ /** Prompt-owned identity — the entities the prompt text NAMES. */
105
+ export interface ShotMentions {
106
+ characterIds: string[];
107
+ environmentIds: string[];
108
+ styleIds: string[];
109
+ }
110
+ export interface ShotSpec {
111
+ /** RAW prompt, `@mentions` intact. Never a composed one: the composer is the
112
+ * only thing that may number anything, and a stored "image 3" would be a
113
+ * lie the moment a reference is added, removed or reordered. */
114
+ prompt: string;
115
+ model: string | null;
116
+ /**
117
+ * The model the PROMPT WAS AUTHORED FOR. Never auto-rewritten.
118
+ *
119
+ * MiniMax H3 takes tagged `<Subject N>` references, `(S1)` speaker labels and
120
+ * `<d>[lang]…</d>` dialogue; Seedance does not. A prompt authored for one and
121
+ * replayed on another is not merely suboptimal — it can carry literal syntax
122
+ * the new model reads as text. Rewriting it would be prompt enhancement, the
123
+ * thing this codebase deleted on 2026-08-01. Record it, show it when it
124
+ * diverges from `model`, and leave the user's words alone.
125
+ */
126
+ authoredFor: string | null;
127
+ params: ShotParams;
128
+ /** ENTITY ids, never flattened paths — update the character and every Shot
129
+ * that mentions it updates with it. */
130
+ mentions: ShotMentions;
131
+ /** Attachment-owned refs as ASSET IDS, ordered within each role. Keyed by
132
+ * `OrderedAttachmentRole` so `ORDERED_ROLE_EMISSION` stays the ONE role
133
+ * list; a sixth role is a compile error here too. */
134
+ refs: Record<OrderedAttachmentRole, string[]>;
135
+ firstFrameAssetId: string | null;
136
+ lastFrameAssetId: string | null;
137
+ /**
138
+ * What is SAID in each reference-audio clip, keyed by its ASSET ID.
139
+ *
140
+ * 🚨 KEYED, NOT INDEX-ALIGNED. A parallel array desyncs on a single drag and
141
+ * then tells the model one clip's words over another clip — silently. Its
142
+ * worst failure keyed is a stale entry, which composes as nothing.
143
+ */
144
+ audioRefSpokenText: Record<string, string>;
145
+ /** Who speaks: an entity id, a bare name, the literal `VO`, or null. A name
146
+ * matching no character is a working state, not an error — it renders as
147
+ * plain text and offers "make this a character". */
148
+ speaker: string | null;
149
+ /** What is said, verbatim. No camera, no scene, no prompt bloat — this is
150
+ * the half a person reads aloud. */
151
+ line: string | null;
152
+ /** The parenthetical: how it is said. `(flat, exhausted)` */
153
+ delivery: string | null;
154
+ /** What happens in the shot, screenplay-style. ONE field: an action line
155
+ * already describes everyone in frame, and splitting out the non-speakers
156
+ * invents a distinction writers do not make. */
157
+ action: string | null;
158
+ /** The one readable object carrying the beat. */
159
+ prop: string | null;
160
+ /** Framing, FREE TEXT. Bucketed for counting by
161
+ * `@slatesvideo/shared/shot-grammar`; never constrained by it. */
162
+ shotSize: string | null;
163
+ /** Camera move, FREE TEXT. Same rule as `shotSize`. */
164
+ camera: string | null;
165
+ /**
166
+ * This row's line runs on from the previous row's — one sentence, two cuts.
167
+ *
168
+ * ONE FLAG, NO OFFSETS. It says "these two rows are one sentence" without
169
+ * either row pointing into the other's text. Set by a mid-sentence split,
170
+ * editable by hand, and cleared on both sides when a move breaks the run.
171
+ * `heinrich-ad-prompting.md` §0a is built on exactly this move, so script →
172
+ * cut is many-to-many and must stay that way.
173
+ */
174
+ continues: boolean;
175
+ }
176
+ /**
177
+ * What each script field MEANS, in one sentence — the prose the op surface
178
+ * shows an agent for that parameter.
179
+ *
180
+ * It lives beside the fields because `satisfies Record<ScriptField, string>` is
181
+ * what makes a new field a compile error in the DESCRIPTIONS too. An op that
182
+ * hand-typed these would ship a ninth field with no explanation, which is the
183
+ * same failure as a column nothing renders.
184
+ */
185
+ export declare const SCRIPT_FIELD_DESCRIPTION: {
186
+ readonly speaker: "Speaker: character id, bare name (including a new character), or \"VO\". Null when silent.";
187
+ readonly line: "Words spoken verbatim; no camera or scene instructions.";
188
+ readonly delivery: "Optional performance note. Leave null unless a specific direction is needed; do not fill every line with stock adjectives. Not sent to TTS: put supported inline cues in the spoken text using the selected model's prompting guide.";
189
+ readonly action: "Screenplay action covering everyone in frame.";
190
+ readonly prop: "The readable object carrying the beat.";
191
+ readonly shotSize: "Free-text framing, e.g. \"wide\" or \"long-lens CU, other head blurred\"; bucketed only for variety counts.";
192
+ readonly camera: "Free-text camera move, e.g. \"slow push in\"; never rejected or rewritten.";
193
+ readonly continues: "True when this line continues the previous row: one sentence across two cuts.";
194
+ };
195
+ /** The script fields, as a list. Sorted from the description map so the two
196
+ * cannot disagree — never a second hand-written array. */
197
+ export type ScriptField = 'speaker' | 'line' | 'delivery' | 'action' | 'prop' | 'shotSize' | 'camera' | 'continues';
198
+ /** The seven free-text script fields (everything but the `continues` flag) —
199
+ * the set an op accepts as `string | null` and a layer renders as text. */
200
+ export declare const SCRIPT_TEXT_FIELDS: readonly ["speaker", "line", "delivery", "action", "prop", "shotSize", "camera"];
201
+ export type ScriptTextField = (typeof SCRIPT_TEXT_FIELDS)[number];
202
+ /**
203
+ * The script, composed into prompt prose. **The one derivation, mirrored in
204
+ * both repos**, so the desktop, `slates_get_shot` and the generation handler
205
+ * cannot disagree about what a scripted Shot sends.
206
+ *
207
+ * 🚨 IT IS A TEMPLATE, NOT A WRITER. Deterministic, inspectable, no model. It
208
+ * orders the fields the way a shot is actually described — what the camera is
209
+ * doing, what happens, then who says what — and does nothing else. It never
210
+ * invents adjectives, never "enhances", and never reorders a sentence the user
211
+ * wrote. That is the prompt-transparency invariant: Slates may compose, and
212
+ * every composed character has to be visible in the composer before Generate.
213
+ *
214
+ * 🚨 AND IT ONLY FIRES WHEN `prompt` IS EMPTY. A Shot that carries an authored
215
+ * prompt keeps it byte-for-byte — which is what makes this safe to ship to live
216
+ * users with no migration. `prompt` wins because it is the more specific
217
+ * statement of intent; the script still counts, still fits, still reads.
218
+ *
219
+ * Dialogue is quoted so a model receives it as speech rather than as
220
+ * description — the one piece of grammar this adds, and the reason it is not
221
+ * just `join(' ')`.
222
+ */
223
+ export declare function scriptPromptBody(spec: Pick<ShotSpec, ScriptTextField>): string;
224
+ /**
225
+ * What this Shot actually sends: the authored prompt, or the script composed
226
+ * into one. Every consumer calls THIS, never `spec.prompt` directly — a reader
227
+ * that reached past it would be the surface that still says "No prompt yet"
228
+ * while the row plainly has a script in it.
229
+ */
230
+ export declare function effectivePrompt(spec: ShotSpec): string;
231
+ /** An empty Shot: a prompt bar nobody has touched. Every reader starts here and
232
+ * overlays what it actually found, so a missing field is never `undefined`
233
+ * leaking into a request. */
234
+ export declare function emptyShotSpec(): ShotSpec;
235
+ /** The mutually exclusive TTS source fields, shared by readers and patch merging. */
236
+ export declare const VOICE_SOURCE_FIELDS: readonly ["voiceId", "voiceReferenceAssetId", "voiceDescription"];
237
+ /** Setting a voice replaces the previous source; unrelated parameter edits preserve it. */
238
+ export declare function mergeShotParams(existing: ShotParams, patch: Record<string, unknown>): ShotParams;
239
+ /**
240
+ * Read a `ShotSpec` out of whatever is on disk — a row written by an older
241
+ * build, a partial object from an op, `null`.
242
+ *
243
+ * TOLERANT ON PURPOSE (invariant 7: a Shot that cannot currently fire still
244
+ * loads). A missing role, an unknown key, a string where an array belongs —
245
+ * none of them may throw, because the one thing worse than a degraded Shot is a
246
+ * Shots list that will not open.
247
+ */
248
+ export declare function normalizeShotSpec(raw: unknown): ShotSpec;
249
+ /** Every asset id a Shot references, deduped, in emission order then frames.
250
+ *
251
+ * 🚨 THIS IS THE INPUT TO THE FK MIRROR. `refs_json` is a JSON blob and is
252
+ * therefore INVISIBLE to the desktop's runtime FK classifier
253
+ * (`storage/assetReferences.ts` walks `PRAGMA foreign_key_list`), so a Shot's
254
+ * references would not block a cross-project move and the asset's file would
255
+ * be relocated out from under it. `shot_assets` is the visible mirror, and
256
+ * this function is the ONE place its row set is derived. */
257
+ export declare function shotAssetIds(spec: ShotSpec): string[];
258
+ /** Total attachment count — what a list row shows without composing anything. */
259
+ export declare function shotRefCount(spec: ShotSpec): number;
260
+ /**
261
+ * What each role MEANS, in one sentence — the prose the op surface shows an
262
+ * agent for that parameter.
263
+ *
264
+ * It lives here, beside the union, because `satisfies Record<AttachmentRole,
265
+ * string>` is what makes a new role a compile error in the DESCRIPTIONS too. An
266
+ * op that hand-typed these would silently ship a seventh role with no
267
+ * explanation, which is the same failure as a bucket nobody wired up.
268
+ */
269
+ export declare const ATTACHMENT_ROLE_DESCRIPTION: {
270
+ readonly reference: "Plain reference images, in send order — cited in the prompt as \"image 1\", \"image 2\"…";
271
+ readonly subject: "Reference images that ARE the subject — composed as \"Image N is the subject\", so the model knows who the shot is about.";
272
+ readonly style: "Reference images the look is taken from — composed as one trailing \"Render in the visual style of image N\" clause.";
273
+ readonly 'video-reference': "Reference CLIPS read alongside the images — cited as \"video 1\", \"video 2\"…";
274
+ readonly 'audio-reference': "Reference AUDIO clips — cited as \"audio 1\", \"audio 2\"…. Pair each with audioRefSpokenText when it contains speech.";
275
+ readonly 'first-frame': "The starting frame for image-to-video.";
276
+ readonly 'last-frame': "The ending frame for image-to-video.";
277
+ };
278
+ //# sourceMappingURL=shot-spec.d.ts.map
@@ -0,0 +1,319 @@
1
+ // The Shot — the prompt bar, serialized.
2
+ //
3
+ // THE PRINCIPLE: a generation's full recipe already exists (the desktop writes
4
+ // `referenceGroups` into every `settings_json`), but only as a byproduct of
5
+ // spending money on it. This module gives that structure a NAME, so it can be
6
+ // listed, forked, agent-authored and restored without loss — before anything
7
+ // has been generated.
8
+ //
9
+ // This is the canonical implementation. It is mirrored byte-for-byte into the
10
+ // desktop app's `slate/src/shared/shotSpec.ts` (the desktop installs the
11
+ // published @slatesvideo/shared from npm and cannot file-import this source, so
12
+ // the mirror carries a header pointing here — the same rule
13
+ // `reference-composer.ts` follows). `slate/scripts/composer-mirror-check.mjs`
14
+ // asserts the two agree; do not invent a second sync mechanism.
15
+ //
16
+ // 🚨 KEEP THIS A DEPENDENCY-FREE LEAF. It imports nothing, in either repo. The
17
+ // desktop's renderer bundles its mirror, the desktop's MAIN process reads it,
18
+ // and the op surface here builds Zod schemas from it — a single `node:` import
19
+ // would break the first of those.
20
+ /**
21
+ * Emission ORDER of the ordered roles — the order `buildReferenceGroups` pushes
22
+ * them in, which is the order the composer numbers them in, which is the order
23
+ * the rail badges them in. Changing a number here changes what the model is
24
+ * told, so treat it exactly like the composer's own ordering.
25
+ *
26
+ * `satisfies Record<OrderedAttachmentRole, number>` is what makes a new role a
27
+ * compile error here rather than a missing bucket at runtime.
28
+ */
29
+ export const ORDERED_ROLE_EMISSION = {
30
+ reference: 0,
31
+ subject: 1,
32
+ style: 2,
33
+ 'video-reference': 3,
34
+ 'audio-reference': 4,
35
+ };
36
+ /** The ordered roles, in emission order. Sorted from the map above so the two
37
+ * cannot disagree — never a second hand-written array. */
38
+ export const ORDERED_ATTACHMENT_ROLES = Object.keys(ORDERED_ROLE_EMISSION).sort((a, b) => ORDERED_ROLE_EMISSION[a] - ORDERED_ROLE_EMISSION[b]);
39
+ /**
40
+ * What each script field MEANS, in one sentence — the prose the op surface
41
+ * shows an agent for that parameter.
42
+ *
43
+ * It lives beside the fields because `satisfies Record<ScriptField, string>` is
44
+ * what makes a new field a compile error in the DESCRIPTIONS too. An op that
45
+ * hand-typed these would ship a ninth field with no explanation, which is the
46
+ * same failure as a column nothing renders.
47
+ */
48
+ export const SCRIPT_FIELD_DESCRIPTION = {
49
+ speaker: 'Speaker: character id, bare name (including a new character), or "VO". Null when silent.',
50
+ line: 'Words spoken verbatim; no camera or scene instructions.',
51
+ delivery: 'Optional performance note. Leave null unless a specific direction is needed; do not fill every line with stock adjectives. Not sent to TTS: put supported inline cues in the spoken text using the selected model\'s prompting guide.',
52
+ action: 'Screenplay action covering everyone in frame.',
53
+ prop: 'The readable object carrying the beat.',
54
+ shotSize: 'Free-text framing, e.g. "wide" or "long-lens CU, other head blurred"; bucketed only for variety counts.',
55
+ camera: 'Free-text camera move, e.g. "slow push in"; never rejected or rewritten.',
56
+ continues: 'True when this line continues the previous row: one sentence across two cuts.',
57
+ };
58
+ /** The seven free-text script fields (everything but the `continues` flag) —
59
+ * the set an op accepts as `string | null` and a layer renders as text. */
60
+ export const SCRIPT_TEXT_FIELDS = [
61
+ 'speaker',
62
+ 'line',
63
+ 'delivery',
64
+ 'action',
65
+ 'prop',
66
+ 'shotSize',
67
+ 'camera',
68
+ ];
69
+ /**
70
+ * The script, composed into prompt prose. **The one derivation, mirrored in
71
+ * both repos**, so the desktop, `slates_get_shot` and the generation handler
72
+ * cannot disagree about what a scripted Shot sends.
73
+ *
74
+ * 🚨 IT IS A TEMPLATE, NOT A WRITER. Deterministic, inspectable, no model. It
75
+ * orders the fields the way a shot is actually described — what the camera is
76
+ * doing, what happens, then who says what — and does nothing else. It never
77
+ * invents adjectives, never "enhances", and never reorders a sentence the user
78
+ * wrote. That is the prompt-transparency invariant: Slates may compose, and
79
+ * every composed character has to be visible in the composer before Generate.
80
+ *
81
+ * 🚨 AND IT ONLY FIRES WHEN `prompt` IS EMPTY. A Shot that carries an authored
82
+ * prompt keeps it byte-for-byte — which is what makes this safe to ship to live
83
+ * users with no migration. `prompt` wins because it is the more specific
84
+ * statement of intent; the script still counts, still fits, still reads.
85
+ *
86
+ * Dialogue is quoted so a model receives it as speech rather than as
87
+ * description — the one piece of grammar this adds, and the reason it is not
88
+ * just `join(' ')`.
89
+ */
90
+ export function scriptPromptBody(spec) {
91
+ const clean = (v) => (v ?? '').trim();
92
+ const parts = [];
93
+ // Framing first: it is the camera instruction, and every model's own docs
94
+ // put shot size and movement at the head of the prompt.
95
+ const framing = [clean(spec.shotSize), clean(spec.camera)].filter(Boolean).join(', ');
96
+ if (framing)
97
+ parts.push(`${framing}.`);
98
+ const action = clean(spec.action);
99
+ if (action)
100
+ parts.push(/[.!?]$/.test(action) ? action : `${action}.`);
101
+ const prop = clean(spec.prop);
102
+ if (prop)
103
+ parts.push(`${prop} is visible in frame.`);
104
+ const line = clean(spec.line);
105
+ if (line) {
106
+ const speaker = clean(spec.speaker);
107
+ const delivery = clean(spec.delivery).replace(/^\(|\)$/g, '').trim();
108
+ // "VO" is a screenplay abbreviation, not something to send to a model.
109
+ const who = !speaker || speaker.toUpperCase() === 'VO' ? 'A voice' : speaker;
110
+ const how = delivery ? `, ${delivery},` : '';
111
+ parts.push(`${who} says${how} "${line}"`);
112
+ }
113
+ return parts.join(' ');
114
+ }
115
+ /**
116
+ * What this Shot actually sends: the authored prompt, or the script composed
117
+ * into one. Every consumer calls THIS, never `spec.prompt` directly — a reader
118
+ * that reached past it would be the surface that still says "No prompt yet"
119
+ * while the row plainly has a script in it.
120
+ */
121
+ export function effectivePrompt(spec) {
122
+ const authored = spec.prompt.trim();
123
+ return authored || scriptPromptBody(spec);
124
+ }
125
+ /** An empty Shot: a prompt bar nobody has touched. Every reader starts here and
126
+ * overlays what it actually found, so a missing field is never `undefined`
127
+ * leaking into a request. */
128
+ export function emptyShotSpec() {
129
+ const refs = {};
130
+ for (const role of ORDERED_ATTACHMENT_ROLES)
131
+ refs[role] = [];
132
+ return {
133
+ prompt: '',
134
+ model: null,
135
+ authoredFor: null,
136
+ params: {},
137
+ mentions: { characterIds: [], environmentIds: [], styleIds: [] },
138
+ refs,
139
+ firstFrameAssetId: null,
140
+ lastFrameAssetId: null,
141
+ audioRefSpokenText: {},
142
+ speaker: null,
143
+ line: null,
144
+ delivery: null,
145
+ action: null,
146
+ prop: null,
147
+ shotSize: null,
148
+ camera: null,
149
+ continues: false,
150
+ };
151
+ }
152
+ const str = (v) => (typeof v === 'string' && v.length > 0 ? v : null);
153
+ const strArray = (v) => Array.isArray(v) ? v.filter((x) => typeof x === 'string' && x.length > 0) : [];
154
+ /** The mutually exclusive TTS source fields, shared by readers and patch merging. */
155
+ export const VOICE_SOURCE_FIELDS = ['voiceId', 'voiceReferenceAssetId', 'voiceDescription'];
156
+ /** Setting a voice replaces the previous source; unrelated parameter edits preserve it. */
157
+ export function mergeShotParams(existing, patch) {
158
+ const next = { ...existing };
159
+ if (VOICE_SOURCE_FIELDS.some((k) => typeof patch[k] === 'string' && patch[k].trim())) {
160
+ for (const k of VOICE_SOURCE_FIELDS)
161
+ delete next[k];
162
+ }
163
+ return readParams({ ...next, ...patch });
164
+ }
165
+ function readParams(v) {
166
+ if (!v || typeof v !== 'object')
167
+ return {};
168
+ const raw = v;
169
+ const out = {};
170
+ const s = (k) => {
171
+ if (typeof raw[k] === 'string')
172
+ out[k] = raw[k];
173
+ };
174
+ const n = (k) => {
175
+ if (typeof raw[k] === 'number' && Number.isFinite(raw[k]))
176
+ out[k] = raw[k];
177
+ };
178
+ const b = (k) => {
179
+ if (typeof raw[k] === 'boolean')
180
+ out[k] = raw[k];
181
+ };
182
+ s('aspectRatio');
183
+ s('imageResolution');
184
+ s('videoResolution');
185
+ s('quality');
186
+ s('audioLanguage');
187
+ s('audioAccent');
188
+ s('negativePrompt');
189
+ s('voiceId');
190
+ s('voiceReferenceAssetId');
191
+ s('voiceDescription');
192
+ n('duration');
193
+ n('imageQuantity');
194
+ n('audioDurationSeconds');
195
+ n('audioPromptInfluence');
196
+ b('sound');
197
+ b('generateMusic');
198
+ b('seedanceFace');
199
+ b('multiShot');
200
+ b('audioLoop');
201
+ b('audioMultilingual');
202
+ if (raw.gptQuality === 'medium' || raw.gptQuality === 'high')
203
+ out.gptQuality = raw.gptQuality;
204
+ if (raw.gridMode === 'off' || raw.gridMode === '2x2' || raw.gridMode === '3x3')
205
+ out.gridMode = raw.gridMode;
206
+ if (Array.isArray(raw.multiShotSegments)) {
207
+ out.multiShotSegments = raw.multiShotSegments;
208
+ }
209
+ const cc = raw.cameraControls;
210
+ if (cc && typeof cc === 'object') {
211
+ const keys = ['horizontal', 'vertical', 'pan', 'tilt', 'roll', 'zoom'];
212
+ if (keys.every((k) => typeof cc[k] === 'number')) {
213
+ out.cameraControls = {
214
+ horizontal: cc.horizontal, vertical: cc.vertical,
215
+ pan: cc.pan, tilt: cc.tilt,
216
+ roll: cc.roll, zoom: cc.zoom,
217
+ };
218
+ }
219
+ }
220
+ return out;
221
+ }
222
+ /**
223
+ * Read a `ShotSpec` out of whatever is on disk — a row written by an older
224
+ * build, a partial object from an op, `null`.
225
+ *
226
+ * TOLERANT ON PURPOSE (invariant 7: a Shot that cannot currently fire still
227
+ * loads). A missing role, an unknown key, a string where an array belongs —
228
+ * none of them may throw, because the one thing worse than a degraded Shot is a
229
+ * Shots list that will not open.
230
+ */
231
+ export function normalizeShotSpec(raw) {
232
+ const base = emptyShotSpec();
233
+ if (!raw || typeof raw !== 'object')
234
+ return base;
235
+ const v = raw;
236
+ const mentions = (v.mentions ?? {});
237
+ const refs = (v.refs ?? {});
238
+ const spoken = {};
239
+ if (v.audioRefSpokenText && typeof v.audioRefSpokenText === 'object') {
240
+ for (const [k, text] of Object.entries(v.audioRefSpokenText)) {
241
+ // Trimmed on the way in, exactly as the composer trims it on the way out,
242
+ // so a value that round-trips through a save cannot come back different.
243
+ if (typeof text === 'string' && text.trim())
244
+ spoken[k] = text.trim();
245
+ }
246
+ }
247
+ const out = {
248
+ prompt: typeof v.prompt === 'string' ? v.prompt : '',
249
+ model: str(v.model),
250
+ authoredFor: str(v.authoredFor),
251
+ params: readParams(v.params),
252
+ mentions: {
253
+ characterIds: strArray(mentions.characterIds),
254
+ environmentIds: strArray(mentions.environmentIds),
255
+ styleIds: strArray(mentions.styleIds),
256
+ },
257
+ refs: base.refs,
258
+ firstFrameAssetId: str(v.firstFrameAssetId),
259
+ lastFrameAssetId: str(v.lastFrameAssetId),
260
+ audioRefSpokenText: spoken,
261
+ // The script layer reads through the same tolerant `str()` as everything
262
+ // else: a row written before these existed comes back with nulls, which is
263
+ // exactly right — it had no line and never claimed one.
264
+ speaker: str(v.speaker),
265
+ line: str(v.line),
266
+ delivery: str(v.delivery),
267
+ action: str(v.action),
268
+ prop: str(v.prop),
269
+ shotSize: str(v.shotSize),
270
+ camera: str(v.camera),
271
+ continues: v.continues === true,
272
+ };
273
+ for (const role of ORDERED_ATTACHMENT_ROLES)
274
+ out.refs[role] = strArray(refs[role]);
275
+ return out;
276
+ }
277
+ /** Every asset id a Shot references, deduped, in emission order then frames.
278
+ *
279
+ * 🚨 THIS IS THE INPUT TO THE FK MIRROR. `refs_json` is a JSON blob and is
280
+ * therefore INVISIBLE to the desktop's runtime FK classifier
281
+ * (`storage/assetReferences.ts` walks `PRAGMA foreign_key_list`), so a Shot's
282
+ * references would not block a cross-project move and the asset's file would
283
+ * be relocated out from under it. `shot_assets` is the visible mirror, and
284
+ * this function is the ONE place its row set is derived. */
285
+ export function shotAssetIds(spec) {
286
+ const ids = [];
287
+ for (const role of ORDERED_ATTACHMENT_ROLES)
288
+ ids.push(...spec.refs[role]);
289
+ if (spec.firstFrameAssetId)
290
+ ids.push(spec.firstFrameAssetId);
291
+ if (spec.lastFrameAssetId)
292
+ ids.push(spec.lastFrameAssetId);
293
+ if (spec.params.voiceReferenceAssetId)
294
+ ids.push(spec.params.voiceReferenceAssetId);
295
+ return [...new Set(ids.filter(Boolean))];
296
+ }
297
+ /** Total attachment count — what a list row shows without composing anything. */
298
+ export function shotRefCount(spec) {
299
+ return shotAssetIds(spec).length;
300
+ }
301
+ /**
302
+ * What each role MEANS, in one sentence — the prose the op surface shows an
303
+ * agent for that parameter.
304
+ *
305
+ * It lives here, beside the union, because `satisfies Record<AttachmentRole,
306
+ * string>` is what makes a new role a compile error in the DESCRIPTIONS too. An
307
+ * op that hand-typed these would silently ship a seventh role with no
308
+ * explanation, which is the same failure as a bucket nobody wired up.
309
+ */
310
+ export const ATTACHMENT_ROLE_DESCRIPTION = {
311
+ reference: 'Plain reference images, in send order — cited in the prompt as "image 1", "image 2"…',
312
+ subject: 'Reference images that ARE the subject — composed as "Image N is the subject", so the model knows who the shot is about.',
313
+ style: 'Reference images the look is taken from — composed as one trailing "Render in the visual style of image N" clause.',
314
+ 'video-reference': 'Reference CLIPS read alongside the images — cited as "video 1", "video 2"…',
315
+ 'audio-reference': 'Reference AUDIO clips — cited as "audio 1", "audio 2"…. Pair each with audioRefSpokenText when it contains speech.',
316
+ 'first-frame': 'The starting frame for image-to-video.',
317
+ 'last-frame': 'The ending frame for image-to-video.',
318
+ };
319
+ //# sourceMappingURL=shot-spec.js.map