@officexapp/vidfarm-devcli 0.21.26 → 0.21.27

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.
@@ -0,0 +1,463 @@
1
+ // ── Storyboard-driven AI sequence grammar ───────────────────────────────────
2
+ // The prompt architecture behind the "character card → storyboard sheet →
3
+ // Seedance/AI-video" workflow. This module is PURE (no I/O, no network): it
4
+ // turns a `sequence.json` plan into the three prompt strings the pipeline
5
+ // needs, so `vidfarm sequence` stays a thin orchestrator and the grammar has a
6
+ // single source of truth (also reused by docs + the local-agent task file).
7
+ //
8
+ // The grammar is distilled from the workflows that actually ship this look in
9
+ // public (aimikoda's THE WIND CALLS / Mei Lin boards, insMind's 9-grid
10
+ // commercial board, the ViralOps "board first, then burn credits" rule):
11
+ //
12
+ // 1. CHARACTER CARD — one identity sheet per character. Locks face, body,
13
+ // wardrobe and art style. Never regenerated once approved.
14
+ // 2. STORYBOARD SHEET — one grid image per part, written with an explicit
15
+ // block grammar ([SCENE PACKET], [IDENTITY CONSISTENCY], [SPATIAL
16
+ // CONTINUITY LOCK], [DIRECTOR STRIP], …). Cheap, and it is where you
17
+ // catch a bad sequence BEFORE paying for video.
18
+ // 3. SHOT PROMPT — the video-model prompt. It cites the character card as
19
+ // the identity authority and the board as the staging authority, then
20
+ // restates the panels as PANEL BEATS so the model can't drift off order.
21
+ //
22
+ // Two rules carry most of the quality and are enforced by the emitters below:
23
+ // • REFERENCE PRIORITY — identity reference controls identity; the
24
+ // storyboard controls staging/motion/geography/continuity. Stating the
25
+ // split stops the model from copying a face off the board's rough panels.
26
+ // • "Do not render the storyboard sheet itself" — without it, video models
27
+ // happily animate the grid, borders and all.
28
+ export const SEQUENCE_PLAN_VERSION = 1;
29
+ // ── small helpers ───────────────────────────────────────────────────────────
30
+ export function panelId(n) {
31
+ return `P${String(n).padStart(2, "0")}`;
32
+ }
33
+ function arrow(items) {
34
+ return items.filter(Boolean).join(" -> ");
35
+ }
36
+ function block(name, body) {
37
+ return `[${name}]\n${body}`;
38
+ }
39
+ /** "part-03" → 3. Falls back to 1 so a hand-written id never crashes an emit. */
40
+ export function partIndex(part) {
41
+ const m = /(\d+)\s*$/.exec(part.id);
42
+ return m ? Number(m[1]) : 1;
43
+ }
44
+ // ── 1. character card ───────────────────────────────────────────────────────
45
+ /**
46
+ * Identity sheet for one character. Deliberately close to the shared
47
+ * CHARACTER_CARD_PROMPT_SNIPPET used by /tools/image, but pinned to the plan's
48
+ * style lock so the card and the final video agree on art style — a card in a
49
+ * different style is the most common cause of drift at the video step.
50
+ */
51
+ export function buildCharacterCardPrompt(character, plan) {
52
+ return [
53
+ `CHARACTER REFERENCE SHEET for ${character.name} (${character.code}).`,
54
+ `SUBJECT: ${character.appearance} Wardrobe: ${character.wardrobe} Movement quality: ${character.movement_quality}`,
55
+ `ART STYLE (must match the final film): ${plan.style_lock}`,
56
+ "LAYOUT: one single consistent character on a plain neutral light-gray studio background, laid out as a clean labeled grid — " +
57
+ "BODY SHOTS (full body, three-quarter, half body, head-and-shoulders neutral expression); " +
58
+ "a TURNAROUND row (front, side profile, back); " +
59
+ "an EXPRESSION SHEET of face close-ups (neutral, determined, joyful, strained); " +
60
+ "and a POSE SHEET of 3 dynamic action poses drawn from this character's movement quality.",
61
+ "CONSISTENCY: the SAME face, hairstyle, wardrobe, colors, body proportions and art style in EVERY view. " +
62
+ "Even flat studio lighting. Small text label under each view. No background scenery, no props beyond the character's own key prop, no logos, no watermark.",
63
+ "This sheet is an identity authority for downstream shots: prioritize readable face structure, silhouette and costume detail over mood or composition."
64
+ ].join("\n\n");
65
+ }
66
+ // ── 2. storyboard sheet ─────────────────────────────────────────────────────
67
+ /**
68
+ * The block-grammar storyboard prompt. Every block earns its place:
69
+ * • PROJECT CARD / CONTINUITY HEADER — masthead + the reference-priority split.
70
+ * • SCENE PACKET — premise, geography, start→end, action chain, prop state.
71
+ * • CHARACTER SANITIZATION — strips un-drawable psychology and backstory.
72
+ * • IDENTITY CONSISTENCY / SPATIAL CONTINUITY LOCK — the two anti-drift rules.
73
+ * • STORYBOARD PURITY — keeps labels OUT of the panel art (the video model
74
+ * will animate any text it sees inside a frame).
75
+ * • DIRECTOR STRIP — rhythm/escalation tracks under the grid; this is what
76
+ * makes the board readable as timing rather than as nine unrelated stills.
77
+ */
78
+ export function buildStoryboardPrompt(part, plan) {
79
+ const panels = part.panels;
80
+ const chars = plan.characters;
81
+ const purity = plan.board_style === "final"
82
+ ? "Panel images are finished-look frames rendered in the final film style (this board doubles as a style fit-check). " +
83
+ "Put panel numbers, beat names and lens tags in the header strip OUTSIDE each panel image. " +
84
+ "No labels, arrows, captions, subtitles, logos, watermarks, timing marks, diagrams, UI, ghost poses, duplicate bodies or technical overlays INSIDE panels."
85
+ : "Panel images are visual-only low-detail monochrome light-gray rough sketches: fast gesture-drawing energy, simple anatomy construction, strong silhouette readability, unfinished previz feel. " +
86
+ "Put panel numbers, beat names and lens tags in the header strip OUTSIDE each panel image. " +
87
+ "No color, labels, arrows, captions, subtitles, logos, watermarks, timing marks, diagrams, UI, ghost poses, duplicate bodies or technical overlays INSIDE panels.";
88
+ return [
89
+ `Create a ${plan.aspect_ratio} image.`,
90
+ block("PROJECT CARD", [
91
+ "Create a compact designed masthead, not a table.",
92
+ `TITLE: ${part.title.toUpperCase()}`,
93
+ `META LINE: ${part.meta_line}`,
94
+ `PRIORITY: ${part.priority}`,
95
+ `MICRO BRIEF: ${part.micro_brief}`
96
+ ].join("\n")),
97
+ block("CONTINUITY HEADER", [
98
+ `SEQUENCE ID: ${part.sequence_id}`,
99
+ "REFERENCE PRIORITY: identity reference controls identity; this storyboard controls staging, motion, geography, continuity"
100
+ ].join("\n")),
101
+ block("SCENE PACKET", [
102
+ `PREMISE: ${part.premise}`,
103
+ `LOCATION: ${part.location}`,
104
+ `START -> END: ${part.start_end}`,
105
+ `ACTION CHAIN: ${arrow(part.action_chain)}`,
106
+ `PROP / EFFECT STATE: ${part.prop_effect_state}`,
107
+ `MUST READ: ${part.must_read}`
108
+ ].join("\n")),
109
+ block("CHARACTER SANITIZATION", [
110
+ ...chars.map((c) => `${c.code}: ${c.appearance} ${c.wardrobe} Movement quality: ${c.movement_quality}`),
111
+ "Remove contradictory traits, invisible psychology, excessive costume detail, and backstory that cannot appear in a panel."
112
+ ].join("\n")),
113
+ block("IDENTITY CONSISTENCY", [
114
+ "Identity reference controls face, body, wardrobe, and proportions.",
115
+ `Keep ${chars.map((c) => c.code).join(", ")} on-model — silhouette, hair, wardrobe and key props consistent across all panels.`,
116
+ "Do not redesign, age-shift, or merge characters. Hold screen direction; do not flip travel direction between panels."
117
+ ].join("\n")),
118
+ block("STORYBOARD PURITY", purity),
119
+ block("MASTER SHOT RULE", `Panel 01 is the master: ${part.master_shot}`),
120
+ block("EMOTIONAL ARC", part.emotional_arc),
121
+ block("STYLE LOCKS", [
122
+ `STYLE LOCK: final video style is ${plan.style_lock}; ${plan.board_style === "final"
123
+ ? "render panels in that same style"
124
+ : "inside storyboard panels render only monochrome light-gray rough sketch"}.`,
125
+ `EFFECT LOCK: ${plan.effect_lock}`,
126
+ `ENVIRONMENT LOCK: ${plan.environment_lock}`
127
+ ].join("\n")),
128
+ block("SPATIAL CONTINUITY LOCK", part.spatial_continuity_lock),
129
+ block("DIRECTOR STRIP", [
130
+ "Bottom animatic track board aligned to panel columns. Tracks: BEAT LINE, CAMERA PATH, ACTION PATH, RHYTHM TRACK, ESCALATION MAP, STATE TRACK, STYLE TRACK.",
131
+ "Use shot chips, thin lines, rhythm blocks, small intensity bars, one-to-three-word labels. No seconds or timestamps.",
132
+ "RHYTHM TRACK format: `RHY P##: [hold|slow reveal|build|burst|impact|pause|recover|final hit] / [short block|medium block|long block] / [clean beat|match beat|smash beat|held beat|whip beat]`.",
133
+ "ESCALATION MAP format: `ESC P##: [L1 calm|L2 tension|L3 rise|L4 surge|L5 peak] / [flat|rise|spike|drop|release|unresolved]`.",
134
+ `PANEL HEADERS: ${arrow(panels.map((p) => `${panelId(p.n)} / ${p.lens} / ${p.beat}`))}`
135
+ ].join("\n")),
136
+ `CAMERA + LENS PLAN: ${arrow(panels.map((p) => `${panelId(p.n)} ${p.camera}`))}`,
137
+ `ACTION PATH: ${arrow(panels.map((p) => `${panelId(p.n)} ${p.action}`))}`,
138
+ `RHYTHM TRACK: ${arrow(panels.map((p) => `${panelId(p.n)} RHY ${panelId(p.n)}: ${p.rhythm.kind} / ${p.rhythm.block} / ${p.rhythm.beat}`))}`,
139
+ `ESCALATION MAP: ${arrow(panels.map((p) => `${panelId(p.n)} ESC ${panelId(p.n)}: ${p.escalation.level} / ${p.escalation.shape}`))}`,
140
+ `STATE TRACK: ${arrow(panels.map((p) => `${panelId(p.n)} ${p.state}`))}`,
141
+ `STYLE TRACK: ${arrow(panels.map((p) => `${panelId(p.n)} ${p.style}`))}`,
142
+ block("SEQUENCE", `Grid: ${panels.length} panels in a ${part.grid.cols}x${part.grid.rows} grid; read left-to-right, top-to-bottom as one continuous sequence.`)
143
+ ].join("\n\n");
144
+ }
145
+ /**
146
+ * The video-model prompt. Ordered the way the models actually weight input:
147
+ * authority statements first, then hard negatives, then look, then the beat
148
+ * list. PANEL BEATS restates the board in words so the sequence survives even
149
+ * when the model reads the board loosely.
150
+ */
151
+ export function buildShotPrompt(part, plan, refs) {
152
+ const lines = [];
153
+ if (part.extend_from) {
154
+ lines.push(`extend ${refs.extendFromUrl ? `@[${refs.extendFromUrl}]` : `@[${part.extend_from} video]`} by ${part.duration_seconds}s, continuing the same shot world without restating the setup.`);
155
+ }
156
+ else {
157
+ lines.push(`Create a ${part.duration_seconds}-second cinematic sequence.`);
158
+ }
159
+ lines.push(`Use ${refs.boardUrl ? `@[${refs.boardUrl}]` : "@[storyboard reference]"} as the authoritative director-approved storyboard blueprint for this sequence. ` +
160
+ "Treat every storyboard panel as a consecutive shot within a single continuous sequence. Follow panel order exactly and do not invent alternative coverage. " +
161
+ "DO NOT RENDER THE STORYBOARD SHEET ITSELF — recreate the filmed sequence implied by the panels, not the physical board artwork, and never treat the board as a single image. " +
162
+ "Preserve camera placement, framing, lens intent, shot scale, character staging, screen direction, environmental geography, prop placement, action choreography, continuity and emotional escalation shown by the board. The storyboard is the primary source of truth for visual storytelling.");
163
+ for (const card of refs.characterCards) {
164
+ lines.push(`Use ${card.url ? `@[${card.url}]` : `@[${card.name} character card]`} as the fixed character-sheet reference for ${card.name} (${card.code}). ` +
165
+ "The character must strictly match the sheet: face, hair, proportions, wardrobe and art style. Do not redesign the character, change the costume, or alter the face.");
166
+ }
167
+ lines.push(`VISUAL STYLE: ${plan.style_lock}`);
168
+ lines.push(`ENVIRONMENT: ${part.location} ${plan.environment_lock}`);
169
+ lines.push(`EFFECTS: ${plan.effect_lock}`);
170
+ lines.push(`EMOTIONAL GUIDANCE: ${part.emotional_arc} Camera rhythm follows the board's escalation map; the piece must read as ${part.priority}.`);
171
+ lines.push(`AUDIO: ${plan.audio_lock}`);
172
+ if (plan.laban) {
173
+ lines.push([
174
+ "Use Laban movement logic throughout:",
175
+ `weight: ${plan.laban.weight}`,
176
+ `time: ${plan.laban.time}`,
177
+ `space: ${plan.laban.space}`,
178
+ `flow: ${plan.laban.flow}`
179
+ ].join("\n"));
180
+ }
181
+ lines.push("PANEL BEATS:\n" +
182
+ part.panels.map((p) => `${panelId(p.n)}: ${p.camera}. ${p.action}. ${p.state}.`).join("\n"));
183
+ lines.push("Do not add text, captions, storyboard labels, arrows, panel borders, grids, UI, logos, subtitles or watermarks. " +
184
+ "Do not cut to unrelated coverage, add characters that are not in the board, or resolve the sequence early.");
185
+ return lines.join("\n\n");
186
+ }
187
+ // ── deterministic starter plan (headless / --auto path) ─────────────────────
188
+ const DEFAULT_RHYTHM = [
189
+ { kind: "hold", block: "long block", beat: "held beat" },
190
+ { kind: "slow reveal", block: "medium block", beat: "clean beat" },
191
+ { kind: "build", block: "medium block", beat: "clean beat" },
192
+ { kind: "burst", block: "short block", beat: "match beat" },
193
+ { kind: "build", block: "medium block", beat: "whip beat" },
194
+ { kind: "impact", block: "short block", beat: "smash beat" },
195
+ { kind: "recover", block: "short block", beat: "held beat" },
196
+ { kind: "final hit", block: "long block", beat: "whip beat" }
197
+ ];
198
+ const DEFAULT_ESCALATION = [
199
+ { level: "L1 calm", shape: "flat" },
200
+ { level: "L2 tension", shape: "rise" },
201
+ { level: "L3 rise", shape: "rise" },
202
+ { level: "L3 rise", shape: "spike" },
203
+ { level: "L4 surge", shape: "rise" },
204
+ { level: "L5 peak", shape: "spike" },
205
+ { level: "L4 surge", shape: "drop" },
206
+ { level: "L5 peak", shape: "release" }
207
+ ];
208
+ const DEFAULT_LENSES = [
209
+ "24mm wide", "85mm portrait", "macro insert", "50mm", "35mm", "24mm wide", "50mm", "24mm wide",
210
+ "35mm", "macro insert", "overhead", "24mm wide"
211
+ ];
212
+ /** Panel-count → the grid the board should be laid out on. */
213
+ export function gridForPanelCount(count) {
214
+ if (count <= 4)
215
+ return { cols: 2, rows: 2 };
216
+ if (count <= 6)
217
+ return { cols: 3, rows: 2 };
218
+ if (count <= 9)
219
+ return { cols: 3, rows: 3 };
220
+ if (count <= 12)
221
+ return { cols: 4, rows: 3 };
222
+ return { cols: 4, rows: Math.ceil(count / 4) };
223
+ }
224
+ export function slugify(value) {
225
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48) || "sequence";
226
+ }
227
+ /**
228
+ * A structurally-complete plan built from the brief alone. It is intentionally
229
+ * generic in its prose — the point is that every required field exists and every
230
+ * emitter produces a valid prompt, so `sequence run` works with no agent in the
231
+ * loop. `sequence plan` (agent-assisted) overwrites this with real direction.
232
+ */
233
+ export function buildStarterPlan(opts) {
234
+ const brief = opts.brief.trim();
235
+ const title = (opts.title ?? brief.split(/[.!?\n]/)[0] ?? brief).trim().slice(0, 70) || "Untitled sequence";
236
+ const partCount = Math.max(1, Math.min(6, opts.parts ?? 1));
237
+ const panelCount = Math.max(4, Math.min(16, opts.panels ?? 8));
238
+ // Short, human-readable continuity stem (BREE-S01-…), not the whole title.
239
+ const stem = slugify(title).split("-").filter((w) => w.length > 2).slice(0, 2).join("-").toUpperCase() || "SEQ";
240
+ const characterName = opts.characterName ?? "Lead";
241
+ const characters = [{
242
+ slug: slugify(characterName),
243
+ code: "C1",
244
+ name: characterName,
245
+ appearance: opts.characterDescription
246
+ ?? "Single lead subject with a clear, readable silhouette; consistent face and proportions across every panel.",
247
+ wardrobe: "One consistent outfit throughout; no costume changes between panels.",
248
+ movement_quality: "Purposeful and physically grounded; weight visible in every pose."
249
+ }];
250
+ const parts = Array.from({ length: partCount }, (_unused, i) => {
251
+ const n = i + 1;
252
+ const panels = Array.from({ length: panelCount }, (_p, j) => ({
253
+ n: j + 1,
254
+ lens: DEFAULT_LENSES[j % DEFAULT_LENSES.length],
255
+ beat: j === 0 ? "Master" : j === panelCount - 1 ? "Final beat" : `Beat ${j + 1}`,
256
+ camera: j === 0
257
+ ? "High wide master establishing the full geography of the location"
258
+ : `${DEFAULT_LENSES[j % DEFAULT_LENSES.length]} coverage continuing the same action`,
259
+ action: j === 0
260
+ ? `${characters[0].code} is introduced in the situation described by the brief`
261
+ : j === panelCount - 1
262
+ ? `${characters[0].code} lands the final beat of the sequence`
263
+ : `${characters[0].code} escalates the action one clear step`,
264
+ rhythm: DEFAULT_RHYTHM[Math.min(j, DEFAULT_RHYTHM.length - 1)],
265
+ escalation: DEFAULT_ESCALATION[Math.min(j, DEFAULT_ESCALATION.length - 1)],
266
+ state: "Continuity holds: same location, same wardrobe, same travel direction as the previous panel",
267
+ style: "Consistent lighting and palette with the panel before it"
268
+ }));
269
+ return {
270
+ id: `part-${String(n).padStart(2, "0")}`,
271
+ sequence_id: `${stem}-S${String(n).padStart(2, "0")}`,
272
+ title: partCount === 1 ? title : `${title} — part ${n}`,
273
+ meta_line: "single continuous sequence / escalating action / clear start-to-finish read",
274
+ priority: "one readable escalation from the opening state to the final beat",
275
+ micro_brief: brief,
276
+ premise: brief,
277
+ location: "The single location implied by the brief, held for the whole part; no location changes mid-sequence.",
278
+ start_end: "Opening state established in panel 01 -> the sequence's strongest beat in the final panel.",
279
+ action_chain: [
280
+ "establish the situation",
281
+ "the first change lands",
282
+ "the action escalates",
283
+ "the strongest beat hits",
284
+ "the sequence resolves or exits on forward motion"
285
+ ],
286
+ prop_effect_state: "Key props stay physically consistent across panels; effects grow with the escalation map.",
287
+ must_read: "The escalation must read without any text on screen.",
288
+ master_shot: "full geography of the location with the lead subject clearly placed inside it.",
289
+ emotional_arc: "settled -> alert -> committed -> peak -> release; shown only through posture, pace and framing.",
290
+ spatial_continuity_lock: "All panels share one location and one screen direction. Allowed changes between panels: camera distance, camera height, subject pose and position, effect intensity. Not allowed: new locations, flipped travel direction, redesigned props.",
291
+ grid: gridForPanelCount(panelCount),
292
+ duration_seconds: Math.max(4, Math.min(30, opts.durationSeconds ?? 15)),
293
+ panels,
294
+ ...(n > 1 ? { extend_from: `part-${String(n - 1).padStart(2, "0")}` } : {})
295
+ };
296
+ });
297
+ return {
298
+ version: SEQUENCE_PLAN_VERSION,
299
+ title,
300
+ brief,
301
+ aspect_ratio: opts.aspectRatio ?? "16:9",
302
+ style_lock: opts.style
303
+ ?? "stylized cinematic realism, rich cinematic lighting, controlled color palette, natural motion blur, premium feature-film aesthetic",
304
+ effect_lock: "Effects are motivated by physical movement and stay grounded in the environment; no glow, no superhero energy, no motion-blur smears.",
305
+ environment_lock: "Hold one consistent environment, light direction and palette for the whole part; no texture or style drift.",
306
+ audio_lock: "No background music or score. Diegetic ambience, foley, impacts, texture and silence only.",
307
+ board_style: opts.boardStyle ?? "rough",
308
+ ...(opts.laban === false ? {} : {
309
+ laban: {
310
+ weight: "strong and grounded on impacts, brief lightness through jumps and lifts",
311
+ time: "quick through strikes, turns and drops; sustained through holds and recoveries",
312
+ space: "direct during committed action; indirect during turns and transitions",
313
+ flow: "bound in rooted stances and precise beats; free in aerial and release moments"
314
+ }
315
+ }),
316
+ characters,
317
+ parts
318
+ };
319
+ }
320
+ // ── plan validation ─────────────────────────────────────────────────────────
321
+ /** Structural check that every emitter's required field is present. */
322
+ export function validatePlan(plan) {
323
+ const errors = [];
324
+ const p = plan;
325
+ if (!p || typeof p !== "object")
326
+ return { ok: false, errors: ["plan is not an object"] };
327
+ if (p.version !== SEQUENCE_PLAN_VERSION)
328
+ errors.push(`version must be ${SEQUENCE_PLAN_VERSION}`);
329
+ for (const key of ["title", "brief", "aspect_ratio", "style_lock", "environment_lock", "audio_lock"]) {
330
+ if (!p[key] || typeof p[key] !== "string")
331
+ errors.push(`missing ${key}`);
332
+ }
333
+ if (!Array.isArray(p.characters) || p.characters.length === 0)
334
+ errors.push("characters must be a non-empty array");
335
+ else {
336
+ const codes = new Set();
337
+ for (const c of p.characters) {
338
+ if (!c?.code || !c?.name || !c?.slug)
339
+ errors.push("each character needs slug, code and name");
340
+ else if (codes.has(c.code))
341
+ errors.push(`duplicate character code ${c.code}`);
342
+ else
343
+ codes.add(c.code);
344
+ }
345
+ }
346
+ if (!Array.isArray(p.parts) || p.parts.length === 0)
347
+ errors.push("parts must be a non-empty array");
348
+ else {
349
+ const ids = new Set();
350
+ for (const part of p.parts) {
351
+ const where = part?.id ?? "<unnamed part>";
352
+ if (!part?.id)
353
+ errors.push("each part needs an id");
354
+ else if (ids.has(part.id))
355
+ errors.push(`duplicate part id ${part.id}`);
356
+ else
357
+ ids.add(part.id);
358
+ if (!Array.isArray(part?.panels) || part.panels.length < 2)
359
+ errors.push(`${where}: needs at least 2 panels`);
360
+ else {
361
+ part.panels.forEach((panel, i) => {
362
+ if (panel?.n !== i + 1)
363
+ errors.push(`${where}: panel ${i + 1} has n=${panel?.n}; panels must be numbered 1..N in order`);
364
+ for (const key of ["lens", "beat", "camera", "action", "state", "style"]) {
365
+ if (!panel?.[key])
366
+ errors.push(`${where}: panel ${i + 1} missing ${key}`);
367
+ }
368
+ if (!panel?.rhythm?.kind || !panel?.escalation?.level)
369
+ errors.push(`${where}: panel ${i + 1} missing rhythm/escalation`);
370
+ });
371
+ }
372
+ if (part?.extend_from && !ids.has(part.extend_from)) {
373
+ errors.push(`${where}: extend_from "${part.extend_from}" must name an EARLIER part`);
374
+ }
375
+ if (!part?.duration_seconds || part.duration_seconds <= 0)
376
+ errors.push(`${where}: duration_seconds must be > 0`);
377
+ }
378
+ }
379
+ return { ok: errors.length === 0, errors };
380
+ }
381
+ // ── agent task file (the "plan" step) ───────────────────────────────────────
382
+ /**
383
+ * Task doc staged into the project folder so a desktop coding agent (Claude
384
+ * Code / Codex) can upgrade the starter plan into real direction. Same model as
385
+ * `vidfarm decompose --local`: the devcli owns the deterministic + billed
386
+ * steps, the agent owns the writing.
387
+ */
388
+ export function buildPlanTaskMarkdown(plan, planPath) {
389
+ return `# Sequence plan task — ${plan.title}
390
+
391
+ You are the director. Rewrite \`${planPath}\` in place so it stops being a
392
+ skeleton and becomes real direction for this brief:
393
+
394
+ > ${plan.brief}
395
+
396
+ Then stop. Do not generate images or video — \`vidfarm sequence\` does that.
397
+
398
+ ## What you are writing
399
+
400
+ \`sequence.json\` drives three generated prompts, so every field is load-bearing:
401
+
402
+ | Plan field | Ends up in |
403
+ |---|---|
404
+ | \`characters[]\` | the CHARACTER CARD image prompt (one identity sheet each) |
405
+ | \`parts[].*\` + \`parts[].panels[]\` | the STORYBOARD SHEET image prompt (block grammar) |
406
+ | \`style_lock\`, \`effect_lock\`, \`environment_lock\`, \`audio_lock\`, \`laban\` | both the board and the video prompt |
407
+ | \`parts[].panels[]\` again | the video model's PANEL BEATS list |
408
+
409
+ Preview any of them without spending a credit:
410
+
411
+ \`\`\`bash
412
+ vidfarm sequence prompts --dir . --print board:part-01
413
+ vidfarm sequence prompts --dir . --print shot:part-01
414
+ vidfarm sequence prompts --dir . --print card:<character-slug>
415
+ \`\`\`
416
+
417
+ ## Rules that carry the quality
418
+
419
+ 1. **Panels are motion, not mood.** Every \`action\` must describe a body or a
420
+ camera doing something. "She feels nervous" is not a panel; "she backpedals
421
+ and plants, keeping the blade between them" is.
422
+ 2. **One location per part.** Write \`spatial_continuity_lock\` as an explicit
423
+ list of what may change between panels (camera distance/height, pose, effect
424
+ intensity) and what may not (location, screen direction, prop design).
425
+ 3. **Hold screen direction.** If the subject travels left-to-right, say so in
426
+ \`state\` on every panel. Flipped direction is the most visible drift there is.
427
+ 4. **Escalate deliberately.** \`escalation\` should climb to exactly one L5 peak
428
+ per part. A board where everything is a peak animates as noise.
429
+ 5. **Start in motion** unless the brief wants a slow open. Panel 01 is the
430
+ master shot — full geography — but it can still be moving.
431
+ 6. **Characters are codes.** Panels refer to C1/C2, never to names. Keep
432
+ \`appearance\` to what a drawing can show: silhouette, hair, build, wardrobe.
433
+ Delete backstory and psychology.
434
+ 7. **\`audio_lock\` stays diegetic** unless the brief asks for score. Music is
435
+ added at the assemble step, where you control it.
436
+ 8. **Multi-part = \`extend_from\`.** Part 2 continuing part 1 must set
437
+ \`extend_from: "part-01"\`; the video prompt then opens with \`extend\` instead
438
+ of re-establishing the world. Budget ~${plan.parts[0]?.duration_seconds ?? 15}s per part.
439
+
440
+ ## Panel field cheatsheet
441
+
442
+ - \`lens\` — coverage tag: \`24mm wide\`, \`35mm\`, \`50mm\`, \`85mm portrait\`, \`macro insert\`, \`overhead\`
443
+ - \`camera\` — placement + move ("low side track at board level")
444
+ - \`action\` — what physically happens
445
+ - \`state\` — continuity: prop state, effect intensity, travel direction
446
+ - \`style\` — one-line look note ("crest backlight", "impact spray burst")
447
+ - \`rhythm.kind\` — hold | slow reveal | build | burst | impact | pause | recover | final hit
448
+ - \`rhythm.block\` — short block | medium block | long block
449
+ - \`rhythm.beat\` — clean beat | match beat | smash beat | held beat | whip beat
450
+ - \`escalation.level\` — L1 calm | L2 tension | L3 rise | L4 surge | L5 peak
451
+ - \`escalation.shape\` — flat | rise | spike | drop | release | unresolved
452
+
453
+ ## When you are done
454
+
455
+ \`\`\`bash
456
+ vidfarm sequence plan --dir . --check # structural validation
457
+ \`\`\`
458
+
459
+ Fix anything it reports, then hand back to the user (or run
460
+ \`vidfarm sequence run --dir .\` to execute the whole pipeline).
461
+ `;
462
+ }
463
+ //# sourceMappingURL=sequence-prompts.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@officexapp/vidfarm-devcli",
3
- "version": "0.21.26",
3
+ "version": "0.21.27",
4
4
  "description": "Local bridge for the Vidfarm Trackpad Editor. `vidfarm serve <template_id>` boots the FULL editor on localhost (disk-backed records/storage, free in-process render); edit composition.html on disk (Claude Code, Codex, etc.) and the browser live-morphs it.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -24,6 +24,7 @@
24
24
  "dist/src/devcli/local-render.js",
25
25
  "dist/src/devcli/port-utils.js",
26
26
  "dist/src/devcli/process-scan.js",
27
+ "dist/src/devcli/sequence.js",
27
28
  "dist/src/devcli/skills.js",
28
29
  "dist/src/devcli/speech.js",
29
30
  "dist/src/devcli/stills.js",
@@ -35,6 +36,7 @@
35
36
  "dist/src/services/captions.js",
36
37
  "dist/src/services/composition-lint.js",
37
38
  "dist/src/services/provider-errors.js",
39
+ "dist/src/services/sequence-prompts.js",
38
40
  "dist/src/services/speech.js",
39
41
  "dist/src/services/clip-curation/**/*.js",
40
42
  "dist/src/services/clip-curation/**/*.json",