@skyphusion-labs/vivijure-core 0.9.0

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 (267) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +87 -0
  3. package/dist/audio-routing.d.ts +2 -0
  4. package/dist/audio-routing.d.ts.map +1 -0
  5. package/dist/audio-routing.js +4 -0
  6. package/dist/audio-routing.js.map +1 -0
  7. package/dist/audio-stage.d.ts +4 -0
  8. package/dist/audio-stage.d.ts.map +1 -0
  9. package/dist/audio-stage.js +25 -0
  10. package/dist/audio-stage.js.map +1 -0
  11. package/dist/beat-analyze.d.ts +22 -0
  12. package/dist/beat-analyze.d.ts.map +1 -0
  13. package/dist/beat-analyze.js +111 -0
  14. package/dist/beat-analyze.js.map +1 -0
  15. package/dist/beat-sync-types.d.ts +33 -0
  16. package/dist/beat-sync-types.d.ts.map +1 -0
  17. package/dist/beat-sync-types.js +4 -0
  18. package/dist/beat-sync-types.js.map +1 -0
  19. package/dist/bundle-assembler.d.ts +54 -0
  20. package/dist/bundle-assembler.d.ts.map +1 -0
  21. package/dist/bundle-assembler.js +376 -0
  22. package/dist/bundle-assembler.js.map +1 -0
  23. package/dist/bundle-durations.d.ts +5 -0
  24. package/dist/bundle-durations.d.ts.map +1 -0
  25. package/dist/bundle-durations.js +68 -0
  26. package/dist/bundle-durations.js.map +1 -0
  27. package/dist/bundle-storyboard.d.ts +11 -0
  28. package/dist/bundle-storyboard.d.ts.map +1 -0
  29. package/dist/bundle-storyboard.js +82 -0
  30. package/dist/bundle-storyboard.js.map +1 -0
  31. package/dist/captions.d.ts +25 -0
  32. package/dist/captions.d.ts.map +1 -0
  33. package/dist/captions.js +56 -0
  34. package/dist/captions.js.map +1 -0
  35. package/dist/cast-db.d.ts +61 -0
  36. package/dist/cast-db.d.ts.map +1 -0
  37. package/dist/cast-db.js +324 -0
  38. package/dist/cast-db.js.map +1 -0
  39. package/dist/cast-lora-train.d.ts +18 -0
  40. package/dist/cast-lora-train.d.ts.map +1 -0
  41. package/dist/cast-lora-train.js +215 -0
  42. package/dist/cast-lora-train.js.map +1 -0
  43. package/dist/cast-loras.d.ts +21 -0
  44. package/dist/cast-loras.d.ts.map +1 -0
  45. package/dist/cast-loras.js +69 -0
  46. package/dist/cast-loras.js.map +1 -0
  47. package/dist/clip-content-validate.d.ts +43 -0
  48. package/dist/clip-content-validate.d.ts.map +1 -0
  49. package/dist/clip-content-validate.js +115 -0
  50. package/dist/clip-content-validate.js.map +1 -0
  51. package/dist/clip-job-model.d.ts +42 -0
  52. package/dist/clip-job-model.d.ts.map +1 -0
  53. package/dist/clip-job-model.js +8 -0
  54. package/dist/clip-job-model.js.map +1 -0
  55. package/dist/clip-validate.d.ts +33 -0
  56. package/dist/clip-validate.d.ts.map +1 -0
  57. package/dist/clip-validate.js +247 -0
  58. package/dist/clip-validate.js.map +1 -0
  59. package/dist/d1-retry.d.ts +20 -0
  60. package/dist/d1-retry.d.ts.map +1 -0
  61. package/dist/d1-retry.js +67 -0
  62. package/dist/d1-retry.js.map +1 -0
  63. package/dist/db-env.d.ts +6 -0
  64. package/dist/db-env.d.ts.map +1 -0
  65. package/dist/db-env.js +2 -0
  66. package/dist/db-env.js.map +1 -0
  67. package/dist/dialogue-lines.d.ts +23 -0
  68. package/dist/dialogue-lines.d.ts.map +1 -0
  69. package/dist/dialogue-lines.js +84 -0
  70. package/dist/dialogue-lines.js.map +1 -0
  71. package/dist/film-advance-lease.d.ts +9 -0
  72. package/dist/film-advance-lease.d.ts.map +1 -0
  73. package/dist/film-advance-lease.js +19 -0
  74. package/dist/film-advance-lease.js.map +1 -0
  75. package/dist/film-model.d.ts +415 -0
  76. package/dist/film-model.d.ts.map +1 -0
  77. package/dist/film-model.js +541 -0
  78. package/dist/film-model.js.map +1 -0
  79. package/dist/film-orchestrator.d.ts +216 -0
  80. package/dist/film-orchestrator.d.ts.map +1 -0
  81. package/dist/film-orchestrator.js +2204 -0
  82. package/dist/film-orchestrator.js.map +1 -0
  83. package/dist/film-render-bridge.d.ts +26 -0
  84. package/dist/film-render-bridge.d.ts.map +1 -0
  85. package/dist/film-render-bridge.js +176 -0
  86. package/dist/film-render-bridge.js.map +1 -0
  87. package/dist/finish-hash.d.ts +10 -0
  88. package/dist/finish-hash.d.ts.map +1 -0
  89. package/dist/finish-hash.js +60 -0
  90. package/dist/finish-hash.js.map +1 -0
  91. package/dist/index.d.ts +48 -0
  92. package/dist/index.d.ts.map +1 -0
  93. package/dist/index.js +45 -0
  94. package/dist/index.js.map +1 -0
  95. package/dist/key-safety.d.ts +5 -0
  96. package/dist/key-safety.d.ts.map +1 -0
  97. package/dist/key-safety.js +23 -0
  98. package/dist/key-safety.js.map +1 -0
  99. package/dist/lora-bundle.d.ts +19 -0
  100. package/dist/lora-bundle.d.ts.map +1 -0
  101. package/dist/lora-bundle.js +75 -0
  102. package/dist/lora-bundle.js.map +1 -0
  103. package/dist/lora-keys.d.ts +2 -0
  104. package/dist/lora-keys.d.ts.map +1 -0
  105. package/dist/lora-keys.js +4 -0
  106. package/dist/lora-keys.js.map +1 -0
  107. package/dist/modules/conformance.d.ts +58 -0
  108. package/dist/modules/conformance.d.ts.map +1 -0
  109. package/dist/modules/conformance.js +369 -0
  110. package/dist/modules/conformance.js.map +1 -0
  111. package/dist/modules/manifest-validate.d.ts +5 -0
  112. package/dist/modules/manifest-validate.d.ts.map +1 -0
  113. package/dist/modules/manifest-validate.js +60 -0
  114. package/dist/modules/manifest-validate.js.map +1 -0
  115. package/dist/modules/registry.d.ts +184 -0
  116. package/dist/modules/registry.d.ts.map +1 -0
  117. package/dist/modules/registry.js +653 -0
  118. package/dist/modules/registry.js.map +1 -0
  119. package/dist/modules/render-pipeline.d.ts +38 -0
  120. package/dist/modules/render-pipeline.d.ts.map +1 -0
  121. package/dist/modules/render-pipeline.js +40 -0
  122. package/dist/modules/render-pipeline.js.map +1 -0
  123. package/dist/modules/types.d.ts +544 -0
  124. package/dist/modules/types.d.ts.map +1 -0
  125. package/dist/modules/types.js +57 -0
  126. package/dist/modules/types.js.map +1 -0
  127. package/dist/operator-config.d.ts +20 -0
  128. package/dist/operator-config.d.ts.map +1 -0
  129. package/dist/operator-config.js +85 -0
  130. package/dist/operator-config.js.map +1 -0
  131. package/dist/output-extract.d.ts +8 -0
  132. package/dist/output-extract.d.ts.map +1 -0
  133. package/dist/output-extract.js +129 -0
  134. package/dist/output-extract.js.map +1 -0
  135. package/dist/planner-prompt.d.ts +12 -0
  136. package/dist/planner-prompt.d.ts.map +1 -0
  137. package/dist/planner-prompt.js +337 -0
  138. package/dist/planner-prompt.js.map +1 -0
  139. package/dist/planner-yaml.d.ts +15 -0
  140. package/dist/planner-yaml.d.ts.map +1 -0
  141. package/dist/planner-yaml.js +157 -0
  142. package/dist/planner-yaml.js.map +1 -0
  143. package/dist/platform/fetcher.d.ts +3 -0
  144. package/dist/platform/fetcher.d.ts.map +1 -0
  145. package/dist/platform/fetcher.js +7 -0
  146. package/dist/platform/fetcher.js.map +1 -0
  147. package/dist/platform/index.d.ts +6 -0
  148. package/dist/platform/index.d.ts.map +1 -0
  149. package/dist/platform/index.js +5 -0
  150. package/dist/platform/index.js.map +1 -0
  151. package/dist/platform/object-store-r2.d.ts +26 -0
  152. package/dist/platform/object-store-r2.d.ts.map +1 -0
  153. package/dist/platform/object-store-r2.js +74 -0
  154. package/dist/platform/object-store-r2.js.map +1 -0
  155. package/dist/platform/orchestrator-context.d.ts +20 -0
  156. package/dist/platform/orchestrator-context.d.ts.map +1 -0
  157. package/dist/platform/orchestrator-context.js +28 -0
  158. package/dist/platform/orchestrator-context.js.map +1 -0
  159. package/dist/platform/r2-types.d.ts +41 -0
  160. package/dist/platform/r2-types.d.ts.map +1 -0
  161. package/dist/platform/r2-types.js +3 -0
  162. package/dist/platform/r2-types.js.map +1 -0
  163. package/dist/platform/types.d.ts +90 -0
  164. package/dist/platform/types.d.ts.map +1 -0
  165. package/dist/platform/types.js +23 -0
  166. package/dist/platform/types.js.map +1 -0
  167. package/dist/preflight.d.ts +57 -0
  168. package/dist/preflight.d.ts.map +1 -0
  169. package/dist/preflight.js +243 -0
  170. package/dist/preflight.js.map +1 -0
  171. package/dist/presign.d.ts +5 -0
  172. package/dist/presign.d.ts.map +1 -0
  173. package/dist/presign.js +24 -0
  174. package/dist/presign.js.map +1 -0
  175. package/dist/public-id.d.ts +8 -0
  176. package/dist/public-id.d.ts.map +1 -0
  177. package/dist/public-id.js +25 -0
  178. package/dist/public-id.js.map +1 -0
  179. package/dist/render-adopt.d.ts +3 -0
  180. package/dist/render-adopt.d.ts.map +1 -0
  181. package/dist/render-adopt.js +92 -0
  182. package/dist/render-adopt.js.map +1 -0
  183. package/dist/render-log.d.ts +25 -0
  184. package/dist/render-log.d.ts.map +1 -0
  185. package/dist/render-log.js +139 -0
  186. package/dist/render-log.js.map +1 -0
  187. package/dist/render-module-config.d.ts +28 -0
  188. package/dist/render-module-config.d.ts.map +1 -0
  189. package/dist/render-module-config.js +124 -0
  190. package/dist/render-module-config.js.map +1 -0
  191. package/dist/render-mux.d.ts +18 -0
  192. package/dist/render-mux.d.ts.map +1 -0
  193. package/dist/render-mux.js +69 -0
  194. package/dist/render-mux.js.map +1 -0
  195. package/dist/render-orchestrator.d.ts +71 -0
  196. package/dist/render-orchestrator.d.ts.map +1 -0
  197. package/dist/render-orchestrator.js +376 -0
  198. package/dist/render-orchestrator.js.map +1 -0
  199. package/dist/render-sweep.d.ts +5 -0
  200. package/dist/render-sweep.d.ts.map +1 -0
  201. package/dist/render-sweep.js +62 -0
  202. package/dist/render-sweep.js.map +1 -0
  203. package/dist/renders-db.d.ts +129 -0
  204. package/dist/renders-db.d.ts.map +1 -0
  205. package/dist/renders-db.js +752 -0
  206. package/dist/renders-db.js.map +1 -0
  207. package/dist/runpod-submit.d.ts +151 -0
  208. package/dist/runpod-submit.d.ts.map +1 -0
  209. package/dist/runpod-submit.js +469 -0
  210. package/dist/runpod-submit.js.map +1 -0
  211. package/dist/runpod-types.d.ts +2 -0
  212. package/dist/runpod-types.d.ts.map +1 -0
  213. package/dist/runpod-types.js +4 -0
  214. package/dist/runpod-types.js.map +1 -0
  215. package/dist/scatter-notify.d.ts +4 -0
  216. package/dist/scatter-notify.d.ts.map +1 -0
  217. package/dist/scatter-notify.js +43 -0
  218. package/dist/scatter-notify.js.map +1 -0
  219. package/dist/scatter-orchestrator-types.d.ts +56 -0
  220. package/dist/scatter-orchestrator-types.d.ts.map +1 -0
  221. package/dist/scatter-orchestrator-types.js +3 -0
  222. package/dist/scatter-orchestrator-types.js.map +1 -0
  223. package/dist/scatter-orchestrator.d.ts +70 -0
  224. package/dist/scatter-orchestrator.d.ts.map +1 -0
  225. package/dist/scatter-orchestrator.js +614 -0
  226. package/dist/scatter-orchestrator.js.map +1 -0
  227. package/dist/scatter.d.ts +35 -0
  228. package/dist/scatter.d.ts.map +1 -0
  229. package/dist/scatter.js +171 -0
  230. package/dist/scatter.js.map +1 -0
  231. package/dist/secret-store.d.ts +5 -0
  232. package/dist/secret-store.d.ts.map +1 -0
  233. package/dist/secret-store.js +19 -0
  234. package/dist/secret-store.js.map +1 -0
  235. package/dist/shot-durations-parse.d.ts +3 -0
  236. package/dist/shot-durations-parse.d.ts.map +1 -0
  237. package/dist/shot-durations-parse.js +43 -0
  238. package/dist/shot-durations-parse.js.map +1 -0
  239. package/dist/srt.d.ts +11 -0
  240. package/dist/srt.d.ts.map +1 -0
  241. package/dist/srt.js +54 -0
  242. package/dist/srt.js.map +1 -0
  243. package/dist/storyboard-ids.d.ts +2 -0
  244. package/dist/storyboard-ids.d.ts.map +1 -0
  245. package/dist/storyboard-ids.js +12 -0
  246. package/dist/storyboard-ids.js.map +1 -0
  247. package/dist/storyboard-projects-db.d.ts +31 -0
  248. package/dist/storyboard-projects-db.d.ts.map +1 -0
  249. package/dist/storyboard-projects-db.js +153 -0
  250. package/dist/storyboard-projects-db.js.map +1 -0
  251. package/dist/storyboard-validate.d.ts +72 -0
  252. package/dist/storyboard-validate.d.ts.map +1 -0
  253. package/dist/storyboard-validate.js +595 -0
  254. package/dist/storyboard-validate.js.map +1 -0
  255. package/dist/structured-events.d.ts +6 -0
  256. package/dist/structured-events.d.ts.map +1 -0
  257. package/dist/structured-events.js +15 -0
  258. package/dist/structured-events.js.map +1 -0
  259. package/dist/tar.d.ts +9 -0
  260. package/dist/tar.d.ts.map +1 -0
  261. package/dist/tar.js +151 -0
  262. package/dist/tar.js.map +1 -0
  263. package/dist/voices.d.ts +12 -0
  264. package/dist/voices.d.ts.map +1 -0
  265. package/dist/voices.js +37 -0
  266. package/dist/voices.js.map +1 -0
  267. package/package.json +71 -0
@@ -0,0 +1,72 @@
1
+ export { coerceShotId } from "./storyboard-ids.js";
2
+ export type SlotId = "A" | "B" | "C" | "D";
3
+ export declare const SLOT_IDS: readonly SlotId[];
4
+ export declare const SCENE_PROMPT_MAX_WORDS = 50;
5
+ export declare const STORYBOARD_MAX_SCENES = 50;
6
+ export declare const FULL_PROMPT_MAX_CHARS = 1024;
7
+ export declare const STYLE_PREFIX_MAX_CHARS = 256;
8
+ export declare const SCENE_MAX_SECONDS = 60;
9
+ export declare const STORYBOARD_MAX_SECONDS: number;
10
+ export declare const DIALOGUE_MAX_CHARS = 300;
11
+ export interface StoryboardScene {
12
+ id?: string;
13
+ prompt: string;
14
+ character_slots?: SlotId[];
15
+ start?: number;
16
+ end?: number;
17
+ target_seconds?: number;
18
+ act?: string;
19
+ start_image?: string;
20
+ dialogue?: {
21
+ slot: SlotId;
22
+ text: string;
23
+ };
24
+ }
25
+ export interface StoryboardInput {
26
+ title: string;
27
+ full_prompt?: string;
28
+ duration_seconds?: number;
29
+ clip_seconds?: number;
30
+ style_prefix?: string;
31
+ style_category?: string | null;
32
+ style_preset?: string | null;
33
+ use_characters?: SlotId[];
34
+ cast_rules?: string;
35
+ refs_dir?: string;
36
+ scenes: StoryboardScene[];
37
+ }
38
+ export interface StoryboardValidated {
39
+ title: string;
40
+ projectName: string;
41
+ full_prompt: string;
42
+ duration_seconds: number | undefined;
43
+ clip_seconds: number | undefined;
44
+ style_prefix: string;
45
+ style_category: string;
46
+ style_preset: string;
47
+ use_characters: SlotId[];
48
+ cast_rules: string;
49
+ refs_dir?: string;
50
+ scenes: StoryboardScene[];
51
+ }
52
+ export type ValidationResult = {
53
+ ok: true;
54
+ value: StoryboardValidated;
55
+ } | {
56
+ ok: false;
57
+ errors: string[];
58
+ };
59
+ export declare function normalizeProjectName(title: string | undefined | null): string;
60
+ export declare function validateStoryboard(input: unknown): ValidationResult;
61
+ export declare function normalizePerShotModels(raw: unknown, allowedModelIds: ReadonlySet<string>): {
62
+ perShot: Record<string, string>;
63
+ errors: string[];
64
+ };
65
+ export declare function normalizeHybridBackends(raw: unknown, allowedModelIds: ReadonlySet<string>): {
66
+ backends: Record<string, {
67
+ backend: "gpu" | "cloud";
68
+ model?: string;
69
+ }>;
70
+ errors: string[];
71
+ };
72
+ //# sourceMappingURL=storyboard-validate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storyboard-validate.d.ts","sourceRoot":"","sources":["../src/storyboard-validate.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,595 @@
1
+ // Storyboard input validator for the planning Worker (v0.27.0).
2
+ //
3
+ // Pure, synchronous, no I/O. Validates the structural shape of a planner
4
+ // output before it is serialized to storyboard.yaml and bundled for the
5
+ // vivijure-serverless GPU worker. Slot readiness (registry prompt plus
6
+ // >=8 reference images on disk) is checked separately as a pre-flight
7
+ // against R2; this validator does not touch the filesystem or network.
8
+ //
9
+ // Schema source of truth: vivijure-serverless/build/vivijure-src/
10
+ // storyboard.example.yaml, plus what orchestrator.build_render_payload
11
+ // and core.build_manifest in that repo actually consume. Slot IDs
12
+ // ("A", "B", "C", "D") mirror characters.SLOTS in the same repo.
13
+ // normalizeProjectName mirrors studio_service.norm_project.
14
+ //
15
+ // Minimal-dep convention (matches src/env.ts, src/longrun-params.ts):
16
+ // hand-authored interfaces, no zod / ajv at runtime, no codegen.
17
+ import { isSafeRelKey, sanitizeKeySegment } from "./key-safety.js";
18
+ export { coerceShotId } from "./storyboard-ids.js";
19
+ import { coerceShotId } from "./storyboard-ids.js";
20
+ export const SLOT_IDS = ["A", "B", "C", "D"];
21
+ const SLOT_SET = new Set(SLOT_IDS);
22
+ // v0.80.0: content-shape guards that the GPU renderer relies on but the
23
+ // structural validator (lines below) historically did not enforce.
24
+ // These caps protect downstream paths from LLM Assist outputs that pass
25
+ // the structural schema but break renderer constraints. See the audit
26
+ // memo for the four gaps these close.
27
+ //
28
+ // SCENE_PROMPT_MAX_TOKENS: SDXL's CLIP-L and OpenCLIP-G text encoders
29
+ // each cap at 77 tokens. Diffusers silently truncates above that. The
30
+ // pod's regional path prepends 2-4 trigger tokens + ~10 style_prefix
31
+ // tokens, leaving ~60 tokens of scene-prompt budget before truncation
32
+ // starts dropping setting clauses. Word-to-token ratio for English in
33
+ // the BPE tokenizers SDXL uses is ~1.3 tokens per word, so the word
34
+ // count check uses 60 / 1.3 = ~46 words. We round to 50 with a small
35
+ // margin and surface the offending scene + word count in the error.
36
+ export const SCENE_PROMPT_MAX_WORDS = 50;
37
+ // Hard cap on scenes per storyboard. The pod's max_scenes default is
38
+ // 100; below 50 is the preflight comfort zone. A 50-scene render with
39
+ // Wan I2V at ~30s per shot is already ~25 minutes of GPU time, beyond
40
+ // which RunPod's per-job timeout starts mattering more than schema.
41
+ export const STORYBOARD_MAX_SCENES = 50;
42
+ // Length caps on the two top-level free-text strings the renderer
43
+ // reads at manifest-build time. style_prefix is what the pod's 0.4.38
44
+ // background_prompt() now leans on - an LLM Assist style_prefix > 256
45
+ // chars would itself overflow CLIP 77 even before any scene-prompt
46
+ // tokens are added.
47
+ export const FULL_PROMPT_MAX_CHARS = 1024;
48
+ export const STYLE_PREFIX_MAX_CHARS = 256;
49
+ // Per-shot and total duration caps. A shot's render cost is ~linear in its seconds
50
+ // (Wan I2V), so an LLM Assist storyboard with a 600s shot, or a huge duration_seconds,
51
+ // becomes real GPU minutes that pass the structural schema. Cap a single shot at 60s
52
+ // (well past any one i2v clip the model makes) and the whole film at scenes x per-shot.
53
+ export const SCENE_MAX_SECONDS = 60;
54
+ export const STORYBOARD_MAX_SECONDS = STORYBOARD_MAX_SCENES * SCENE_MAX_SECONDS;
55
+ // Per-shot dialogue line cap. The line is voiced by Deepgram Aura-1 (billed per 1k chars) and a
56
+ // shot lip-syncs one spoken line, so this bounds both TTS cost and how much speech must fit a clip.
57
+ // 300 chars is ~20s of speech, well past any single-shot line, while staying firmly bounded.
58
+ export const DIALOGUE_MAX_CHARS = 300;
59
+ // Scene-id format the renderer expects (core.py shot manifest looks up
60
+ // scenes by this id). LLM Assist outputs like "scene_dramatic_sunset"
61
+ // silently break downstream tools that expect the shot_NN pattern.
62
+ // Coerce rather than reject so the LLM doesn't have to know the format;
63
+ // the validator renumbers in declaration order.
64
+ function countWords(prompt) {
65
+ return prompt.trim().split(/\s+/).filter((w) => w.length > 0).length;
66
+ }
67
+ // studio_service.norm_project (vivijure-serverless studio_service.py):
68
+ // return (name or "project").strip().replace(" ", "_") or "project"
69
+ // Collapses internal whitespace runs (\s+) rather than only literal
70
+ // spaces, since YAML parsers can hand us tabs or other whitespace too.
71
+ export function normalizeProjectName(title) {
72
+ const raw = typeof title === "string" ? title : "";
73
+ const slug = raw.trim().replace(/\s+/g, "_");
74
+ // Guarantee a path-safe single segment: a hostile title (".." , "a/b", "x://y") otherwise reaches
75
+ // bundles/<projectName>.tar.gz and could steer the key off-bucket. Normal titles (letters, digits,
76
+ // _ . -) are unchanged, so the value stays in sync with the backend `project` field. (security #6)
77
+ return sanitizeKeySegment(slug, "project");
78
+ }
79
+ function isPlainObject(value) {
80
+ return typeof value === "object" && value !== null && !Array.isArray(value);
81
+ }
82
+ function isPositiveFiniteNumber(value) {
83
+ return typeof value === "number" && Number.isFinite(value) && value > 0;
84
+ }
85
+ function isNonNegativeFiniteNumber(value) {
86
+ return typeof value === "number" && Number.isFinite(value) && value >= 0;
87
+ }
88
+ function describeType(value) {
89
+ if (value === null)
90
+ return "null";
91
+ if (Array.isArray(value))
92
+ return "array";
93
+ return typeof value;
94
+ }
95
+ function sceneLabel(scene, index) {
96
+ const id = typeof scene.id === "string" && scene.id.trim().length > 0
97
+ ? scene.id.trim()
98
+ : null;
99
+ return id ? `scenes[${index}] (id="${id}")` : `scenes[${index}]`;
100
+ }
101
+ // missing / null / empty / whitespace-only collapse to "None" (the literal
102
+ // string the renderer treats as "no style lookup"). Non-string types
103
+ // silently collapse too; the planner's TypeScript types disallow them at
104
+ // authoring time, so reaching this branch at runtime is a programmer
105
+ // error worth defending against rather than rejecting.
106
+ function normalizeStyleNone(value) {
107
+ if (typeof value !== "string")
108
+ return "None";
109
+ const trimmed = value.trim();
110
+ return trimmed.length === 0 ? "None" : trimmed; // return the trimmed value, not the raw (issue #17)
111
+ }
112
+ // ---- validateStoryboard, decomposed (S6 split) -----------------------------------------------
113
+ // One section, one function; every error message and check moved VERBATIM from the former
114
+ // 455-line body, so behavior (and the tests pinning these strings) is unchanged. Each helper
115
+ // pushes into the shared `errors` and returns its validated slice.
116
+ function validateTitleSection(input, errors) {
117
+ let title = "";
118
+ let projectName = "project";
119
+ const rawTitle = input.title;
120
+ if (typeof rawTitle !== "string" || rawTitle.trim().length === 0) {
121
+ errors.push("title is required and must be a non-empty string");
122
+ }
123
+ else {
124
+ title = rawTitle;
125
+ projectName = normalizeProjectName(rawTitle);
126
+ }
127
+ return { title, projectName };
128
+ }
129
+ function validateUseCharactersSection(input, errors) {
130
+ const useCharacters = [];
131
+ if (input.use_characters !== undefined) {
132
+ if (!Array.isArray(input.use_characters)) {
133
+ errors.push(`use_characters must be an array of slot ids if provided (got ${describeType(input.use_characters)})`);
134
+ }
135
+ else {
136
+ const seen = new Set();
137
+ input.use_characters.forEach((slot, i) => {
138
+ if (typeof slot !== "string") {
139
+ errors.push(`use_characters[${i}] must be a string (got ${describeType(slot)})`);
140
+ return;
141
+ }
142
+ if (!SLOT_SET.has(slot)) {
143
+ errors.push(`use_characters[${i}] = "${slot}" is not a valid slot id (allowed: ${SLOT_IDS.join(", ")})`);
144
+ return;
145
+ }
146
+ if (seen.has(slot)) {
147
+ errors.push(`use_characters[${i}] = "${slot}" is duplicated`);
148
+ return;
149
+ }
150
+ seen.add(slot);
151
+ useCharacters.push(slot);
152
+ });
153
+ }
154
+ }
155
+ return useCharacters;
156
+ }
157
+ // scenes[i].character_slots: optional array of SlotId, must be a subset of use_characters.
158
+ function validateSceneSlots(scene, label, useCharacters, out, errors) {
159
+ if (scene.character_slots !== undefined) {
160
+ if (!Array.isArray(scene.character_slots)) {
161
+ errors.push(`${label} character_slots must be an array if provided (got ${describeType(scene.character_slots)})`);
162
+ }
163
+ else {
164
+ const slotsOut = [];
165
+ const seenLocal = new Set();
166
+ scene.character_slots.forEach((slot, j) => {
167
+ if (typeof slot !== "string") {
168
+ errors.push(`${label} character_slots[${j}] must be a string (got ${describeType(slot)})`);
169
+ return;
170
+ }
171
+ if (!SLOT_SET.has(slot)) {
172
+ errors.push(`${label} character_slots[${j}] = "${slot}" is not a valid slot id (allowed: ${SLOT_IDS.join(", ")})`);
173
+ return;
174
+ }
175
+ if (seenLocal.has(slot)) {
176
+ errors.push(`${label} character_slots[${j}] = "${slot}" is duplicated within the scene`);
177
+ return;
178
+ }
179
+ // Subset rule: every per-scene slot must be loaded for the render.
180
+ if (!useCharacters.includes(slot)) {
181
+ const loaded = useCharacters.length > 0 ? useCharacters.join(", ") : "(none)";
182
+ errors.push(`${label} character_slots references slot "${slot}" which is not in use_characters (loaded: ${loaded})`);
183
+ return;
184
+ }
185
+ seenLocal.add(slot);
186
+ slotsOut.push(slot);
187
+ });
188
+ out.character_slots = slotsOut;
189
+ }
190
+ }
191
+ }
192
+ // dialogue: optional { slot, text }. The speaker must be in this shot (one of its validated
193
+ // character_slots, which already enforces the use_characters subset rule), and the line is a
194
+ // non-empty string capped to bound TTS cost + how much speech must fit the clip.
195
+ function validateSceneDialogue(scene, label, out, errors) {
196
+ if (scene.dialogue !== undefined) {
197
+ if (!isPlainObject(scene.dialogue)) {
198
+ errors.push(`${label} dialogue must be an object { slot, text } if provided (got ${describeType(scene.dialogue)})`);
199
+ }
200
+ else {
201
+ const dlgSlot = scene.dialogue.slot;
202
+ const dlgText = scene.dialogue.text;
203
+ let slotOk = false;
204
+ if (typeof dlgSlot !== "string" || !SLOT_SET.has(dlgSlot)) {
205
+ errors.push(`${label} dialogue.slot must be a valid slot id (allowed: ${SLOT_IDS.join(", ")})`);
206
+ }
207
+ else if (!(out.character_slots ?? []).includes(dlgSlot)) {
208
+ errors.push(`${label} dialogue.slot "${dlgSlot}" must be one of this shot's character_slots (the speaker has to be in the shot)`);
209
+ }
210
+ else {
211
+ slotOk = true;
212
+ }
213
+ let textOk = false;
214
+ if (typeof dlgText !== "string" || dlgText.trim().length === 0) {
215
+ errors.push(`${label} dialogue.text must be a non-empty string`);
216
+ }
217
+ else if (dlgText.length > DIALOGUE_MAX_CHARS) {
218
+ errors.push(`${label} dialogue.text is ${dlgText.length} chars; cap is ${DIALOGUE_MAX_CHARS} (one spoken line per shot)`);
219
+ }
220
+ else {
221
+ textOk = true;
222
+ }
223
+ if (slotOk && textOk) {
224
+ out.dialogue = { slot: dlgSlot, text: dlgText.trim() };
225
+ }
226
+ }
227
+ }
228
+ }
229
+ // start / end / target_seconds + the cross-field span rule (per-shot GPU-billing caps).
230
+ function validateSceneTiming(scene, label, out, errors) {
231
+ // start: optional non-negative number (0.0 is a legal film-time origin)
232
+ if (scene.start !== undefined) {
233
+ if (!isNonNegativeFiniteNumber(scene.start)) {
234
+ errors.push(`${label} start must be a non-negative finite number if provided`);
235
+ }
236
+ else {
237
+ out.start = scene.start;
238
+ }
239
+ }
240
+ // end: optional positive film-time position (absolute, not a duration, so no cap)
241
+ if (scene.end !== undefined) {
242
+ if (!isPositiveFiniteNumber(scene.end)) {
243
+ errors.push(`${label} end must be a positive finite number if provided`);
244
+ }
245
+ else {
246
+ out.end = scene.end;
247
+ }
248
+ }
249
+ // target_seconds: optional positive per-shot DURATION, capped so one shot cannot
250
+ // bill an unbounded GPU render.
251
+ if (scene.target_seconds !== undefined) {
252
+ if (!isPositiveFiniteNumber(scene.target_seconds)) {
253
+ errors.push(`${label} target_seconds must be a positive finite number if provided`);
254
+ }
255
+ else if (scene.target_seconds > SCENE_MAX_SECONDS) {
256
+ errors.push(`${label} target_seconds is ${scene.target_seconds}s; cap is ${SCENE_MAX_SECONDS}s per shot`);
257
+ }
258
+ else {
259
+ out.target_seconds = scene.target_seconds;
260
+ }
261
+ }
262
+ // Cross-field: if both start and end are valid, end must be > start, and the
263
+ // span (a per-shot duration) is capped like target_seconds.
264
+ if (typeof out.start === "number" &&
265
+ typeof out.end === "number" &&
266
+ out.end <= out.start) {
267
+ errors.push(`${label} end (${out.end}) must be greater than start (${out.start})`);
268
+ }
269
+ else if (typeof out.start === "number" &&
270
+ typeof out.end === "number" &&
271
+ out.end - out.start > SCENE_MAX_SECONDS) {
272
+ errors.push(`${label} span (end - start = ${Math.round((out.end - out.start) * 100) / 100}s) exceeds the per-shot cap of ${SCENE_MAX_SECONDS}s`);
273
+ }
274
+ }
275
+ // One scene: prompt (CLIP token guard), id coercion, slots, dialogue, timing, act/start_image.
276
+ function validateScene(scene, i, useCharacters, errors) {
277
+ if (!isPlainObject(scene)) {
278
+ errors.push(`scenes[${i}] must be an object (got ${describeType(scene)})`);
279
+ return null;
280
+ }
281
+ const label = sceneLabel(scene, i);
282
+ const out = { prompt: "" };
283
+ // prompt: required, non-empty after trim
284
+ if (typeof scene.prompt !== "string" ||
285
+ scene.prompt.trim().length === 0) {
286
+ errors.push(`${label} is missing prompt (must be a non-empty string)`);
287
+ }
288
+ else {
289
+ out.prompt = scene.prompt;
290
+ // v0.80.0: token-count guard so LLM Assist outputs that
291
+ // overflow SDXL's CLIP 77-token limit get caught here
292
+ // rather than silently truncating at render time. The pod's
293
+ // regional path prepends triggers + style_prefix, leaving
294
+ // ~60 tokens of headroom, ~46 words at 1.3 tokens/word; we
295
+ // cap at 50 with a small margin.
296
+ const wc = countWords(scene.prompt);
297
+ if (wc > SCENE_PROMPT_MAX_WORDS) {
298
+ errors.push(`${label} prompt is ${wc} words; cap is ${SCENE_PROMPT_MAX_WORDS} to fit within SDXL CLIP 77 tokens after triggers + style_prefix. Tighten the prompt or move appearance details to the cast bible.`);
299
+ }
300
+ }
301
+ // id: optional; v0.80.0 always coerces to the shot_NN pattern
302
+ // the renderer expects. LLM Assist outputs like
303
+ // "scene_dramatic_sunset" become "shot_NN" in declaration
304
+ // order. A valid scene.id like "shot_07" survives intact.
305
+ // Non-string ids are tolerated (coerced to the default).
306
+ if (scene.id !== undefined && typeof scene.id !== "string") {
307
+ errors.push(`${label} id must be a string if provided (got ${describeType(scene.id)})`);
308
+ }
309
+ out.id = coerceShotId(typeof scene.id === "string" ? scene.id : undefined, i);
310
+ validateSceneSlots(scene, label, useCharacters, out, errors);
311
+ validateSceneDialogue(scene, label, out, errors);
312
+ validateSceneTiming(scene, label, out, errors);
313
+ // act / start_image: optional strings. start_image is a path/key consumed downstream as an R2
314
+ // object, so it must be a safe relative key (no traversal / absolute / scheme). (security #6)
315
+ for (const key of ["act", "start_image"]) {
316
+ const v = scene[key];
317
+ if (v !== undefined) {
318
+ if (typeof v !== "string") {
319
+ errors.push(`${label} ${key} must be a string if provided (got ${describeType(v)})`);
320
+ }
321
+ else if (key === "start_image" && !isSafeRelKey(v)) {
322
+ errors.push(`${label} start_image must be a safe relative path (letters, digits, . _ - /, no "..", no leading "/")`);
323
+ }
324
+ else {
325
+ out[key] = v;
326
+ }
327
+ }
328
+ }
329
+ return out;
330
+ }
331
+ function validateScenesSection(input, useCharacters, errors) {
332
+ const validatedScenes = [];
333
+ if (!Array.isArray(input.scenes)) {
334
+ errors.push(`scenes is required and must be a non-empty array (got ${describeType(input.scenes)})`);
335
+ }
336
+ else if (input.scenes.length === 0) {
337
+ errors.push("scenes is required and must be a non-empty array (got empty array)");
338
+ }
339
+ else if (input.scenes.length > STORYBOARD_MAX_SCENES) {
340
+ // v0.80.0: hard cap on scene count. Preflight warns at 24; this
341
+ // is the firm ceiling. Catches LLM Assist outputs that try to
342
+ // produce a 100-shot epic on a draft pass.
343
+ errors.push(`scenes count ${input.scenes.length} exceeds the hard cap of ${STORYBOARD_MAX_SCENES} (preflight warns at 24; consider splitting the storyboard or shortening the duration)`);
344
+ }
345
+ else {
346
+ input.scenes.forEach((scene, i) => {
347
+ const out = validateScene(scene, i, useCharacters, errors);
348
+ if (out)
349
+ validatedScenes.push(out);
350
+ });
351
+ }
352
+ // Duplicate shot ids: coerceShotId renumbers unlabeled scenes by index, which can
353
+ // collide with an authored id (an explicit "shot_05" plus the 5th unlabeled scene ->
354
+ // two "shot_05"). core.py looks scenes up by id, so a dup silently drops a shot from
355
+ // the render. Reject so the collision is visible.
356
+ {
357
+ const seenIds = new Set();
358
+ for (const s of validatedScenes) {
359
+ const id = s.id;
360
+ if (!id)
361
+ continue;
362
+ if (seenIds.has(id)) {
363
+ errors.push(`duplicate shot id "${id}" (an authored id collided with an auto-numbered one; rename or renumber the scene)`);
364
+ }
365
+ else {
366
+ seenIds.add(id);
367
+ }
368
+ }
369
+ }
370
+ return validatedScenes;
371
+ }
372
+ function validateTopLevelFields(input, errors) {
373
+ let fullPrompt = "";
374
+ if (input.full_prompt !== undefined) {
375
+ if (typeof input.full_prompt !== "string") {
376
+ errors.push(`full_prompt must be a string if provided (got ${describeType(input.full_prompt)})`);
377
+ }
378
+ else if (input.full_prompt.length > FULL_PROMPT_MAX_CHARS) {
379
+ // v0.80.0: cap free-text length so a runaway LLM Assist
380
+ // synopsis can't bloat the manifest. full_prompt is read at
381
+ // manifest-build time on the pod.
382
+ errors.push(`full_prompt is ${input.full_prompt.length} chars; cap is ${FULL_PROMPT_MAX_CHARS}`);
383
+ }
384
+ else {
385
+ fullPrompt = input.full_prompt;
386
+ }
387
+ }
388
+ let stylePrefix = "";
389
+ if (input.style_prefix !== undefined) {
390
+ if (typeof input.style_prefix !== "string") {
391
+ errors.push(`style_prefix must be a string if provided (got ${describeType(input.style_prefix)})`);
392
+ }
393
+ else if (input.style_prefix.length > STYLE_PREFIX_MAX_CHARS) {
394
+ // v0.80.0: style_prefix is what the pod's 0.4.38 background_
395
+ // prompt() builds the bg backplate from; an LLM Assist output
396
+ // over 256 chars would itself overflow CLIP 77 before any
397
+ // scene-prompt tokens are added.
398
+ errors.push(`style_prefix is ${input.style_prefix.length} chars; cap is ${STYLE_PREFIX_MAX_CHARS} (the pod's bg-pass uses style_prefix verbatim and SDXL CLIP truncates at 77 tokens)`);
399
+ }
400
+ else {
401
+ stylePrefix = input.style_prefix;
402
+ }
403
+ }
404
+ let castRules = "";
405
+ if (input.cast_rules !== undefined) {
406
+ if (typeof input.cast_rules !== "string") {
407
+ errors.push(`cast_rules must be a string if provided (got ${describeType(input.cast_rules)})`);
408
+ }
409
+ else {
410
+ castRules = input.cast_rules;
411
+ }
412
+ }
413
+ let durationSeconds;
414
+ if (input.duration_seconds !== undefined) {
415
+ if (!isPositiveFiniteNumber(input.duration_seconds)) {
416
+ errors.push("duration_seconds must be a positive finite number if provided");
417
+ }
418
+ else if (input.duration_seconds > STORYBOARD_MAX_SECONDS) {
419
+ errors.push(`duration_seconds is ${input.duration_seconds}s; cap is ${STORYBOARD_MAX_SECONDS}s (${STORYBOARD_MAX_SCENES} shots x ${SCENE_MAX_SECONDS}s)`);
420
+ }
421
+ else {
422
+ durationSeconds = input.duration_seconds;
423
+ }
424
+ }
425
+ let clipSeconds;
426
+ if (input.clip_seconds !== undefined) {
427
+ if (!isPositiveFiniteNumber(input.clip_seconds)) {
428
+ errors.push("clip_seconds must be a positive finite number if provided");
429
+ }
430
+ else if (input.clip_seconds > SCENE_MAX_SECONDS) {
431
+ errors.push(`clip_seconds is ${input.clip_seconds}s; cap is ${SCENE_MAX_SECONDS}s per shot`);
432
+ }
433
+ else {
434
+ clipSeconds = input.clip_seconds;
435
+ }
436
+ }
437
+ let refsDir;
438
+ if (input.refs_dir !== undefined) {
439
+ if (typeof input.refs_dir !== "string" ||
440
+ input.refs_dir.trim().length === 0) {
441
+ errors.push("refs_dir must be a non-empty string if provided");
442
+ }
443
+ else if (!isSafeRelKey(input.refs_dir)) {
444
+ // refs_dir is used downstream as an R2 key prefix; reject traversal / absolute / scheme. (security #6)
445
+ errors.push('refs_dir must be a safe relative path (letters, digits, . _ - /, no "..", no leading "/")');
446
+ }
447
+ else {
448
+ refsDir = input.refs_dir;
449
+ }
450
+ }
451
+ return { fullPrompt, stylePrefix, castRules, durationSeconds, clipSeconds, refsDir };
452
+ }
453
+ // v0.134.3: backfill each scene's target_seconds when the model omitted the
454
+ // (optional) field, so the planner's scene editor shows an explicit per-shot
455
+ // duration instead of a blank box and the value is explicit downstream (beat
456
+ // snap, YAML, render) rather than relying on a silent fallback. Priority: an
457
+ // explicit start/end span, else the storyboard's clip_seconds (the per-shot
458
+ // default), else an even split of duration_seconds across the scenes. This is
459
+ // the same clip_seconds fallback markers.ts / preflight apply at render time,
460
+ // just materialized into the data. No-op when there's nothing to derive from.
461
+ function backfillTargetSeconds(validatedScenes, clipSeconds, durationSeconds) {
462
+ const perShotFallback = typeof clipSeconds === "number" && clipSeconds > 0
463
+ ? clipSeconds
464
+ : typeof durationSeconds === "number" &&
465
+ durationSeconds > 0 &&
466
+ validatedScenes.length > 0
467
+ ? Math.round((durationSeconds / validatedScenes.length) * 100) / 100
468
+ : undefined;
469
+ for (const s of validatedScenes) {
470
+ if (typeof s.target_seconds === "number")
471
+ continue;
472
+ if (typeof s.start === "number" && typeof s.end === "number" && s.end > s.start) {
473
+ s.target_seconds = Math.round((s.end - s.start) * 100) / 100;
474
+ }
475
+ else if (perShotFallback !== undefined) {
476
+ s.target_seconds = perShotFallback;
477
+ }
478
+ }
479
+ }
480
+ export function validateStoryboard(input) {
481
+ const errors = [];
482
+ if (!isPlainObject(input)) {
483
+ return {
484
+ ok: false,
485
+ errors: [
486
+ `storyboard must be an object (got ${describeType(input)})`,
487
+ ],
488
+ };
489
+ }
490
+ const { title, projectName } = validateTitleSection(input, errors);
491
+ const useCharacters = validateUseCharactersSection(input, errors);
492
+ const validatedScenes = validateScenesSection(input, useCharacters, errors);
493
+ const { fullPrompt, stylePrefix, castRules, durationSeconds, clipSeconds, refsDir } = validateTopLevelFields(input, errors);
494
+ // None-normalization for the two style fields. The renderer disables on
495
+ // the literal string "None", not on null/undefined, so we collapse to it.
496
+ const styleCategory = normalizeStyleNone(input.style_category);
497
+ const stylePreset = normalizeStyleNone(input.style_preset);
498
+ if (errors.length > 0) {
499
+ return { ok: false, errors };
500
+ }
501
+ backfillTargetSeconds(validatedScenes, clipSeconds, durationSeconds);
502
+ const value = {
503
+ title,
504
+ projectName,
505
+ full_prompt: fullPrompt,
506
+ duration_seconds: durationSeconds,
507
+ clip_seconds: clipSeconds,
508
+ style_prefix: stylePrefix,
509
+ style_category: styleCategory,
510
+ style_preset: stylePreset,
511
+ use_characters: useCharacters,
512
+ cast_rules: castRules,
513
+ scenes: validatedScenes,
514
+ };
515
+ if (refsDir !== undefined)
516
+ value.refs_dir = refsDir;
517
+ return { ok: true, value };
518
+ }
519
+ // v0.147.0: per-shot cloud i2v model overrides for animate-cloud (Phase 4a).
520
+ // Maps shot_id -> model id so one cloud animation can mix models across shots
521
+ // (e.g. the standoff on Runway, the atmosphere on Seedance). Pure + testable;
522
+ // the route supplies `allowedModelIds` (the image-input video catalog) so this
523
+ // stays free of the model catalog import. Returns the accepted overrides plus a
524
+ // list of human errors; the route 400s when any entry is bad rather than
525
+ // silently dropping it (a shot the caller meant to override would otherwise run
526
+ // on the default model unnoticed). A missing/empty input is valid (no overrides).
527
+ export function normalizePerShotModels(raw, allowedModelIds) {
528
+ const perShot = {};
529
+ const errors = [];
530
+ if (raw === undefined || raw === null)
531
+ return { perShot, errors };
532
+ if (typeof raw !== "object" || Array.isArray(raw)) {
533
+ errors.push("perShot must be an object mapping shot_id to a model id");
534
+ return { perShot, errors };
535
+ }
536
+ for (const [shotId, modelId] of Object.entries(raw)) {
537
+ if (!shotId.trim())
538
+ continue;
539
+ if (typeof modelId !== "string" || !modelId) {
540
+ errors.push(`perShot["${shotId}"] must be a model id string`);
541
+ continue;
542
+ }
543
+ if (!allowedModelIds.has(modelId)) {
544
+ errors.push(`perShot["${shotId}"] "${modelId}" is not an image-input video model`);
545
+ continue;
546
+ }
547
+ perShot[shotId] = modelId;
548
+ }
549
+ return { perShot, errors };
550
+ }
551
+ // v0.151.0 (Phase 4 hybrid): per-shot BACKEND assignment for animate-hybrid.
552
+ // Maps shot_id -> { backend: "gpu" | "cloud", model? } so one film can route
553
+ // some shots to the pod's Wan i2v and others to a cloud i2v model. Pure +
554
+ // testable; the route supplies `allowedModelIds` (the image-input video catalog)
555
+ // for the cloud model check. A cloud entry may omit `model` (the route's
556
+ // defaultCloudModel applies). Bad entries error (route 400s) rather than
557
+ // silently dropping. Missing/empty input is valid (route falls back to
558
+ // defaultBackend for every shot).
559
+ export function normalizeHybridBackends(raw, allowedModelIds) {
560
+ const backends = {};
561
+ const errors = [];
562
+ if (raw === undefined || raw === null)
563
+ return { backends, errors };
564
+ if (typeof raw !== "object" || Array.isArray(raw)) {
565
+ errors.push("backends must be an object mapping shot_id to { backend, model? }");
566
+ return { backends, errors };
567
+ }
568
+ for (const [shotId, v] of Object.entries(raw)) {
569
+ if (!shotId.trim())
570
+ continue;
571
+ if (!v || typeof v !== "object" || Array.isArray(v)) {
572
+ errors.push(`backends["${shotId}"] must be an object { backend, model? }`);
573
+ continue;
574
+ }
575
+ const backend = v.backend;
576
+ if (backend !== "gpu" && backend !== "cloud") {
577
+ errors.push(`backends["${shotId}"].backend must be "gpu" or "cloud"`);
578
+ continue;
579
+ }
580
+ const entry = { backend };
581
+ if (backend === "cloud") {
582
+ const model = v.model;
583
+ if (model !== undefined) {
584
+ if (typeof model !== "string" || !allowedModelIds.has(model)) {
585
+ errors.push(`backends["${shotId}"].model "${String(model)}" is not an image-input video model`);
586
+ continue;
587
+ }
588
+ entry.model = model;
589
+ }
590
+ }
591
+ backends[shotId] = entry;
592
+ }
593
+ return { backends, errors };
594
+ }
595
+ //# sourceMappingURL=storyboard-validate.js.map