venice-video-harness 2.4.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 (264) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/LICENSE +21 -0
  3. package/README.md +598 -0
  4. package/dist/assembly/manifest-builder.d.ts +8 -0
  5. package/dist/assembly/manifest-builder.d.ts.map +1 -0
  6. package/dist/assembly/manifest-builder.js +196 -0
  7. package/dist/assembly/manifest-builder.js.map +1 -0
  8. package/dist/assembly/remotion-scaffold.d.ts +14 -0
  9. package/dist/assembly/remotion-scaffold.d.ts.map +1 -0
  10. package/dist/assembly/remotion-scaffold.js +292 -0
  11. package/dist/assembly/remotion-scaffold.js.map +1 -0
  12. package/dist/assembly/types.d.ts +67 -0
  13. package/dist/assembly/types.d.ts.map +1 -0
  14. package/dist/assembly/types.js +5 -0
  15. package/dist/assembly/types.js.map +1 -0
  16. package/dist/characters/describer.d.ts +32 -0
  17. package/dist/characters/describer.d.ts.map +1 -0
  18. package/dist/characters/describer.js +309 -0
  19. package/dist/characters/describer.js.map +1 -0
  20. package/dist/characters/extractor.d.ts +27 -0
  21. package/dist/characters/extractor.d.ts.map +1 -0
  22. package/dist/characters/extractor.js +274 -0
  23. package/dist/characters/extractor.js.map +1 -0
  24. package/dist/characters/reference-manager.d.ts +95 -0
  25. package/dist/characters/reference-manager.d.ts.map +1 -0
  26. package/dist/characters/reference-manager.js +346 -0
  27. package/dist/characters/reference-manager.js.map +1 -0
  28. package/dist/cli.d.ts +3 -0
  29. package/dist/cli.d.ts.map +1 -0
  30. package/dist/cli.js +310 -0
  31. package/dist/cli.js.map +1 -0
  32. package/dist/config.d.ts +22 -0
  33. package/dist/config.d.ts.map +1 -0
  34. package/dist/config.js +66 -0
  35. package/dist/config.js.map +1 -0
  36. package/dist/editing/aligner.d.ts +44 -0
  37. package/dist/editing/aligner.d.ts.map +1 -0
  38. package/dist/editing/aligner.js +201 -0
  39. package/dist/editing/aligner.js.map +1 -0
  40. package/dist/editing/edl.d.ts +45 -0
  41. package/dist/editing/edl.d.ts.map +1 -0
  42. package/dist/editing/edl.js +133 -0
  43. package/dist/editing/edl.js.map +1 -0
  44. package/dist/editing/overlays.d.ts +97 -0
  45. package/dist/editing/overlays.d.ts.map +1 -0
  46. package/dist/editing/overlays.js +41 -0
  47. package/dist/editing/overlays.js.map +1 -0
  48. package/dist/editing/packer.d.ts +52 -0
  49. package/dist/editing/packer.d.ts.map +1 -0
  50. package/dist/editing/packer.js +170 -0
  51. package/dist/editing/packer.js.map +1 -0
  52. package/dist/editing/providers/whisper-cpp.d.ts +32 -0
  53. package/dist/editing/providers/whisper-cpp.d.ts.map +1 -0
  54. package/dist/editing/providers/whisper-cpp.js +239 -0
  55. package/dist/editing/providers/whisper-cpp.js.map +1 -0
  56. package/dist/editing/render.d.ts +36 -0
  57. package/dist/editing/render.d.ts.map +1 -0
  58. package/dist/editing/render.js +227 -0
  59. package/dist/editing/render.js.map +1 -0
  60. package/dist/editing/self-eval.d.ts +48 -0
  61. package/dist/editing/self-eval.d.ts.map +1 -0
  62. package/dist/editing/self-eval.js +293 -0
  63. package/dist/editing/self-eval.js.map +1 -0
  64. package/dist/editing/silence.d.ts +76 -0
  65. package/dist/editing/silence.d.ts.map +1 -0
  66. package/dist/editing/silence.js +189 -0
  67. package/dist/editing/silence.js.map +1 -0
  68. package/dist/editing/types.d.ts +178 -0
  69. package/dist/editing/types.d.ts.map +1 -0
  70. package/dist/editing/types.js +13 -0
  71. package/dist/editing/types.js.map +1 -0
  72. package/dist/index.d.ts +6 -0
  73. package/dist/index.d.ts.map +1 -0
  74. package/dist/index.js +5 -0
  75. package/dist/index.js.map +1 -0
  76. package/dist/interactive.d.ts +12 -0
  77. package/dist/interactive.d.ts.map +1 -0
  78. package/dist/interactive.js +78 -0
  79. package/dist/interactive.js.map +1 -0
  80. package/dist/mini-drama/assembler.d.ts +61 -0
  81. package/dist/mini-drama/assembler.d.ts.map +1 -0
  82. package/dist/mini-drama/assembler.js +466 -0
  83. package/dist/mini-drama/assembler.js.map +1 -0
  84. package/dist/mini-drama/audio-mix.d.ts +65 -0
  85. package/dist/mini-drama/audio-mix.d.ts.map +1 -0
  86. package/dist/mini-drama/audio-mix.js +118 -0
  87. package/dist/mini-drama/audio-mix.js.map +1 -0
  88. package/dist/mini-drama/cli.d.ts +3 -0
  89. package/dist/mini-drama/cli.d.ts.map +1 -0
  90. package/dist/mini-drama/cli.js +2869 -0
  91. package/dist/mini-drama/cli.js.map +1 -0
  92. package/dist/mini-drama/fcpxml-export.d.ts +2 -0
  93. package/dist/mini-drama/fcpxml-export.d.ts.map +1 -0
  94. package/dist/mini-drama/fcpxml-export.js +5 -0
  95. package/dist/mini-drama/fcpxml-export.js.map +1 -0
  96. package/dist/mini-drama/generation-planner.d.ts +24 -0
  97. package/dist/mini-drama/generation-planner.d.ts.map +1 -0
  98. package/dist/mini-drama/generation-planner.js +304 -0
  99. package/dist/mini-drama/generation-planner.js.map +1 -0
  100. package/dist/mini-drama/location-generator.d.ts +23 -0
  101. package/dist/mini-drama/location-generator.d.ts.map +1 -0
  102. package/dist/mini-drama/location-generator.js +144 -0
  103. package/dist/mini-drama/location-generator.js.map +1 -0
  104. package/dist/mini-drama/music-cues.d.ts +96 -0
  105. package/dist/mini-drama/music-cues.d.ts.map +1 -0
  106. package/dist/mini-drama/music-cues.js +332 -0
  107. package/dist/mini-drama/music-cues.js.map +1 -0
  108. package/dist/mini-drama/panel-fixer.d.ts +12 -0
  109. package/dist/mini-drama/panel-fixer.d.ts.map +1 -0
  110. package/dist/mini-drama/panel-fixer.js +319 -0
  111. package/dist/mini-drama/panel-fixer.js.map +1 -0
  112. package/dist/mini-drama/prompt-builder.d.ts +149 -0
  113. package/dist/mini-drama/prompt-builder.d.ts.map +1 -0
  114. package/dist/mini-drama/prompt-builder.js +722 -0
  115. package/dist/mini-drama/prompt-builder.js.map +1 -0
  116. package/dist/mini-drama/reference-slots.d.ts +31 -0
  117. package/dist/mini-drama/reference-slots.d.ts.map +1 -0
  118. package/dist/mini-drama/reference-slots.js +161 -0
  119. package/dist/mini-drama/reference-slots.js.map +1 -0
  120. package/dist/mini-drama/shot-paths.d.ts +77 -0
  121. package/dist/mini-drama/shot-paths.d.ts.map +1 -0
  122. package/dist/mini-drama/shot-paths.js +101 -0
  123. package/dist/mini-drama/shot-paths.js.map +1 -0
  124. package/dist/mini-drama/storyboard-reference-generator.d.ts +47 -0
  125. package/dist/mini-drama/storyboard-reference-generator.d.ts.map +1 -0
  126. package/dist/mini-drama/storyboard-reference-generator.js +293 -0
  127. package/dist/mini-drama/storyboard-reference-generator.js.map +1 -0
  128. package/dist/mini-drama/subtitle-generator.d.ts +12 -0
  129. package/dist/mini-drama/subtitle-generator.d.ts.map +1 -0
  130. package/dist/mini-drama/subtitle-generator.js +139 -0
  131. package/dist/mini-drama/subtitle-generator.js.map +1 -0
  132. package/dist/mini-drama/timeline-export/davinci-fcpxml.d.ts +3 -0
  133. package/dist/mini-drama/timeline-export/davinci-fcpxml.d.ts.map +1 -0
  134. package/dist/mini-drama/timeline-export/davinci-fcpxml.js +150 -0
  135. package/dist/mini-drama/timeline-export/davinci-fcpxml.js.map +1 -0
  136. package/dist/mini-drama/timeline-export/fcpxml.d.ts +11 -0
  137. package/dist/mini-drama/timeline-export/fcpxml.d.ts.map +1 -0
  138. package/dist/mini-drama/timeline-export/fcpxml.js +140 -0
  139. package/dist/mini-drama/timeline-export/fcpxml.js.map +1 -0
  140. package/dist/mini-drama/timeline-export/index.d.ts +13 -0
  141. package/dist/mini-drama/timeline-export/index.d.ts.map +1 -0
  142. package/dist/mini-drama/timeline-export/index.js +26 -0
  143. package/dist/mini-drama/timeline-export/index.js.map +1 -0
  144. package/dist/mini-drama/timeline-export/premiere-xmeml.d.ts +3 -0
  145. package/dist/mini-drama/timeline-export/premiere-xmeml.d.ts.map +1 -0
  146. package/dist/mini-drama/timeline-export/premiere-xmeml.js +211 -0
  147. package/dist/mini-drama/timeline-export/premiere-xmeml.js.map +1 -0
  148. package/dist/mini-drama/timeline-export/probe.d.ts +53 -0
  149. package/dist/mini-drama/timeline-export/probe.d.ts.map +1 -0
  150. package/dist/mini-drama/timeline-export/probe.js +89 -0
  151. package/dist/mini-drama/timeline-export/probe.js.map +1 -0
  152. package/dist/mini-drama/timeline-export/types.d.ts +57 -0
  153. package/dist/mini-drama/timeline-export/types.d.ts.map +1 -0
  154. package/dist/mini-drama/timeline-export/types.js +14 -0
  155. package/dist/mini-drama/timeline-export/types.js.map +1 -0
  156. package/dist/mini-drama/video-generator.d.ts +22 -0
  157. package/dist/mini-drama/video-generator.d.ts.map +1 -0
  158. package/dist/mini-drama/video-generator.js +1305 -0
  159. package/dist/mini-drama/video-generator.js.map +1 -0
  160. package/dist/mini-drama/voice-reference.d.ts +42 -0
  161. package/dist/mini-drama/voice-reference.d.ts.map +1 -0
  162. package/dist/mini-drama/voice-reference.js +170 -0
  163. package/dist/mini-drama/voice-reference.js.map +1 -0
  164. package/dist/output/html-renderer.d.ts +2 -0
  165. package/dist/output/html-renderer.d.ts.map +1 -0
  166. package/dist/output/html-renderer.js +37 -0
  167. package/dist/output/html-renderer.js.map +1 -0
  168. package/dist/parsers/fountain-parser.d.ts +34 -0
  169. package/dist/parsers/fountain-parser.d.ts.map +1 -0
  170. package/dist/parsers/fountain-parser.js +71 -0
  171. package/dist/parsers/fountain-parser.js.map +1 -0
  172. package/dist/parsers/pdf-parser.d.ts +24 -0
  173. package/dist/parsers/pdf-parser.d.ts.map +1 -0
  174. package/dist/parsers/pdf-parser.js +237 -0
  175. package/dist/parsers/pdf-parser.js.map +1 -0
  176. package/dist/parsers/scene-extractor.d.ts +41 -0
  177. package/dist/parsers/scene-extractor.d.ts.map +1 -0
  178. package/dist/parsers/scene-extractor.js +400 -0
  179. package/dist/parsers/scene-extractor.js.map +1 -0
  180. package/dist/series/manager.d.ts +64 -0
  181. package/dist/series/manager.d.ts.map +1 -0
  182. package/dist/series/manager.js +282 -0
  183. package/dist/series/manager.js.map +1 -0
  184. package/dist/series/types.d.ts +641 -0
  185. package/dist/series/types.d.ts.map +1 -0
  186. package/dist/series/types.js +304 -0
  187. package/dist/series/types.js.map +1 -0
  188. package/dist/storyboard/assembler.d.ts +134 -0
  189. package/dist/storyboard/assembler.d.ts.map +1 -0
  190. package/dist/storyboard/assembler.js +319 -0
  191. package/dist/storyboard/assembler.js.map +1 -0
  192. package/dist/storyboard/prompt-builder.d.ts +113 -0
  193. package/dist/storyboard/prompt-builder.d.ts.map +1 -0
  194. package/dist/storyboard/prompt-builder.js +578 -0
  195. package/dist/storyboard/prompt-builder.js.map +1 -0
  196. package/dist/storyboard/shot-planner.d.ts +34 -0
  197. package/dist/storyboard/shot-planner.d.ts.map +1 -0
  198. package/dist/storyboard/shot-planner.js +446 -0
  199. package/dist/storyboard/shot-planner.js.map +1 -0
  200. package/dist/user-config.d.ts +15 -0
  201. package/dist/user-config.d.ts.map +1 -0
  202. package/dist/user-config.js +103 -0
  203. package/dist/user-config.js.map +1 -0
  204. package/dist/venice/audio-preflight.d.ts +80 -0
  205. package/dist/venice/audio-preflight.d.ts.map +1 -0
  206. package/dist/venice/audio-preflight.js +142 -0
  207. package/dist/venice/audio-preflight.js.map +1 -0
  208. package/dist/venice/audio.d.ts +88 -0
  209. package/dist/venice/audio.d.ts.map +1 -0
  210. package/dist/venice/audio.js +234 -0
  211. package/dist/venice/audio.js.map +1 -0
  212. package/dist/venice/client.d.ts +86 -0
  213. package/dist/venice/client.d.ts.map +1 -0
  214. package/dist/venice/client.js +375 -0
  215. package/dist/venice/client.js.map +1 -0
  216. package/dist/venice/edit.d.ts +57 -0
  217. package/dist/venice/edit.d.ts.map +1 -0
  218. package/dist/venice/edit.js +100 -0
  219. package/dist/venice/edit.js.map +1 -0
  220. package/dist/venice/generate.d.ts +47 -0
  221. package/dist/venice/generate.d.ts.map +1 -0
  222. package/dist/venice/generate.js +150 -0
  223. package/dist/venice/generate.js.map +1 -0
  224. package/dist/venice/image-bytes.d.ts +26 -0
  225. package/dist/venice/image-bytes.d.ts.map +1 -0
  226. package/dist/venice/image-bytes.js +101 -0
  227. package/dist/venice/image-bytes.js.map +1 -0
  228. package/dist/venice/models.d.ts +143 -0
  229. package/dist/venice/models.d.ts.map +1 -0
  230. package/dist/venice/models.js +984 -0
  231. package/dist/venice/models.js.map +1 -0
  232. package/dist/venice/multi-edit.d.ts +40 -0
  233. package/dist/venice/multi-edit.d.ts.map +1 -0
  234. package/dist/venice/multi-edit.js +97 -0
  235. package/dist/venice/multi-edit.js.map +1 -0
  236. package/dist/venice/provenance.d.ts +66 -0
  237. package/dist/venice/provenance.d.ts.map +1 -0
  238. package/dist/venice/provenance.js +151 -0
  239. package/dist/venice/provenance.js.map +1 -0
  240. package/dist/venice/recipe.d.ts +69 -0
  241. package/dist/venice/recipe.d.ts.map +1 -0
  242. package/dist/venice/recipe.js +103 -0
  243. package/dist/venice/recipe.js.map +1 -0
  244. package/dist/venice/rejection.d.ts +63 -0
  245. package/dist/venice/rejection.d.ts.map +1 -0
  246. package/dist/venice/rejection.js +111 -0
  247. package/dist/venice/rejection.js.map +1 -0
  248. package/dist/venice/seedance-preflight.d.ts +38 -0
  249. package/dist/venice/seedance-preflight.d.ts.map +1 -0
  250. package/dist/venice/seedance-preflight.js +29 -0
  251. package/dist/venice/seedance-preflight.js.map +1 -0
  252. package/dist/venice/types.d.ts +212 -0
  253. package/dist/venice/types.d.ts.map +1 -0
  254. package/dist/venice/types.js +9 -0
  255. package/dist/venice/types.js.map +1 -0
  256. package/dist/venice/video.d.ts +77 -0
  257. package/dist/venice/video.d.ts.map +1 -0
  258. package/dist/venice/video.js +163 -0
  259. package/dist/venice/video.js.map +1 -0
  260. package/dist/venice/voices.d.ts +40 -0
  261. package/dist/venice/voices.d.ts.map +1 -0
  262. package/dist/venice/voices.js +194 -0
  263. package/dist/venice/voices.js.map +1 -0
  264. package/package.json +79 -0
@@ -0,0 +1,293 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Storyboard reference (blocking plate) generation
3
+ //
4
+ // A StoryboardReference is a COMPOSED image of a scene beat: multiple
5
+ // characters positioned in a location in relation to each other (and to key
6
+ // props). It is consumed as a reference_image_urls entry on @Image-tag R2V
7
+ // models (Seedance 2.0 family, HappyHorse 1.1 R2V) — NEVER as a start frame —
8
+ // with a role clause restricting it to composition/blocking authority.
9
+ //
10
+ // Composition strategy mirrors panel generation: generateWithReferences with
11
+ // each character's front.png as a face ref plus the location wide.png as an
12
+ // environment anchor, so the plate itself stays consistent with the canonical
13
+ // assets it will sit alongside in the reference array.
14
+ // ---------------------------------------------------------------------------
15
+ import { join, basename } from 'node:path';
16
+ import { existsSync, readFileSync } from 'node:fs';
17
+ import { mkdir, writeFile, rename } from 'node:fs/promises';
18
+ import { getCharacterDir, getLocationDir, getLocation, getStoryboardDir, getStoryboardRefPath, } from '../series/manager.js';
19
+ import { generateWithReferences, generateImage } from '../venice/generate.js';
20
+ import { writeImageBytesSmart } from '../venice/image-bytes.js';
21
+ import { appendRecipePass } from '../venice/recipe.js';
22
+ import { DEFAULT_IMAGE_GENERATION_MODEL } from '../series/types.js';
23
+ function buildAestheticString(aesthetic) {
24
+ return [
25
+ aesthetic.style,
26
+ aesthetic.palette,
27
+ aesthetic.lighting,
28
+ aesthetic.lensCharacteristics,
29
+ aesthetic.filmStock ? `shot on ${aesthetic.filmStock}` : '',
30
+ ]
31
+ .filter(Boolean)
32
+ .join(', ');
33
+ }
34
+ /**
35
+ * Generate (or reuse) the composed blocking plate for a storyboard reference.
36
+ * Writes `storyboards/<slug>.png` plus a `.prompt.json` sidecar, provenance
37
+ * (hasFace matches whether characters were composed), and a recipe pass.
38
+ * Returns the on-disk path.
39
+ */
40
+ export async function generateStoryboardReference(client, series, ref, options = {}) {
41
+ if (!series.aesthetic) {
42
+ throw new Error('Series aesthetic must be set before generating storyboard references.');
43
+ }
44
+ const dir = getStoryboardDir(series);
45
+ await mkdir(dir, { recursive: true });
46
+ const imgPath = getStoryboardRefPath(series, ref.slug);
47
+ if (existsSync(imgPath) && !options.force) {
48
+ return { path: imgPath, skipped: true };
49
+ }
50
+ if (existsSync(imgPath) && options.force) {
51
+ const archive = imgPath.replace(/\.png$/, `-force-archive-${Date.now()}.png`);
52
+ await rename(imgPath, archive);
53
+ }
54
+ const model = options.model
55
+ ?? ref.referenceModel
56
+ ?? series.videoDefaults.imageDefaults?.generationModel
57
+ ?? DEFAULT_IMAGE_GENERATION_MODEL;
58
+ const cfgScale = options.cfgScale ?? 10;
59
+ const aspect = series.storyboardAspectRatio ?? '16:9';
60
+ const aestheticStr = buildAestheticString(series.aesthetic);
61
+ // Gather character face refs (front.png per character).
62
+ const refPaths = [];
63
+ const charRefs = [];
64
+ for (const name of ref.characters) {
65
+ const char = series.characters.find(c => c.name.toUpperCase() === name.toUpperCase());
66
+ if (!char) {
67
+ console.warn(` Storyboard ref "${ref.slug}": character "${name}" not found in series — skipping their face ref.`);
68
+ continue;
69
+ }
70
+ const frontPath = join(getCharacterDir(series, char.name), 'front.png');
71
+ if (!existsSync(frontPath)) {
72
+ console.warn(` Storyboard ref "${ref.slug}": no front.png for ${char.name} — generate character references first.`);
73
+ continue;
74
+ }
75
+ refPaths.push(frontPath);
76
+ charRefs.push({
77
+ name: char.name,
78
+ role: char.description.slice(0, 80),
79
+ base64Image: readFileSync(frontPath).toString('base64'),
80
+ });
81
+ }
82
+ // Location environment ref (wide angle), appended after face refs.
83
+ let locationSuffix = '';
84
+ if (ref.location) {
85
+ const loc = getLocation(series, ref.location);
86
+ if (loc) {
87
+ const locDir = getLocationDir(series, loc.slug);
88
+ const wide = ['wide.png', 'medium.png', 'detail.png']
89
+ .map(f => join(locDir, f))
90
+ .find(p => existsSync(p));
91
+ if (wide) {
92
+ refPaths.push(wide);
93
+ charRefs.push({
94
+ name: 'LOCATION',
95
+ role: 'environment reference — setting, architecture, lighting',
96
+ base64Image: readFileSync(wide).toString('base64'),
97
+ });
98
+ locationSuffix = ' The final reference image is the location environment — match its setting, architecture, and lighting; it is not a character.';
99
+ }
100
+ }
101
+ }
102
+ // Blocking plates prioritize spatial clarity over cinematic drama: a clean
103
+ // medium-wide view where every character, their positions, and the key
104
+ // props are unambiguous. The video model reads geometry from this image.
105
+ const promptParts = [
106
+ `STYLE: ${aestheticStr}.`,
107
+ 'Single cinematic frame, one continuous image, NOT a comic panel layout, NO panel borders, NO speech bubbles, NO text overlays.',
108
+ 'Medium-wide blocking shot: every character fully visible, positions and spatial relationships unambiguous, clear staging.',
109
+ `${ref.description}.`,
110
+ `STYLE REMINDER: ${aestheticStr}.`,
111
+ ];
112
+ const prompt = promptParts.join(' ') + locationSuffix;
113
+ const negativePrompt = [
114
+ 'comic panels', 'multiple panels', 'panel layout', 'panel borders', 'panel grid',
115
+ 'speech bubbles', 'text bubbles', 'split screen', 'multiple frames',
116
+ 'deformed', 'blurry', 'bad anatomy', 'watermark', 'text', 'signature', 'low quality',
117
+ ].join(', ');
118
+ let imgBuffer;
119
+ if (charRefs.length > 0) {
120
+ const result = await generateWithReferences(client, {
121
+ model,
122
+ prompt,
123
+ negative_prompt: negativePrompt,
124
+ resolution: '1K',
125
+ aspect_ratio: aspect,
126
+ steps: 30,
127
+ cfg_scale: cfgScale,
128
+ seed: ref.seed,
129
+ safe_mode: false,
130
+ hide_watermark: true,
131
+ referenceImages: charRefs,
132
+ faceSlots: Math.min(ref.characters.length, 5),
133
+ });
134
+ imgBuffer = Buffer.from(result.base64, 'base64');
135
+ }
136
+ else {
137
+ const response = await generateImage(client, {
138
+ model,
139
+ prompt,
140
+ negative_prompt: negativePrompt,
141
+ resolution: '1K',
142
+ aspect_ratio: aspect,
143
+ steps: 30,
144
+ cfg_scale: cfgScale,
145
+ seed: ref.seed,
146
+ safe_mode: false,
147
+ hide_watermark: true,
148
+ });
149
+ if (!response.images?.[0]) {
150
+ throw new Error(`Storyboard reference "${ref.slug}": no image returned.`);
151
+ }
152
+ imgBuffer = Buffer.from(response.images[0].b64_json, 'base64');
153
+ }
154
+ const finalPath = await writeImageBytesSmart(imgBuffer, imgPath);
155
+ console.log(` Storyboard ref: saved -> ${basename(finalPath)}`);
156
+ await writeFile(imgPath.replace(/\.png$/, '.prompt.json'), JSON.stringify({
157
+ slug: ref.slug,
158
+ episode: ref.episode,
159
+ description: ref.description,
160
+ characters: ref.characters,
161
+ location: ref.location ?? null,
162
+ shotIds: ref.shotIds,
163
+ model,
164
+ prompt,
165
+ negative_prompt: negativePrompt,
166
+ cfg_scale: cfgScale,
167
+ aspect_ratio: aspect,
168
+ resolution: '1K',
169
+ seed: ref.seed,
170
+ generatedAt: new Date().toISOString(),
171
+ }, null, 2), 'utf-8');
172
+ await appendRecipePass(finalPath, {
173
+ kind: 'generate',
174
+ role: 'identity',
175
+ model,
176
+ label: `storyboard blocking plate (${ref.slug})`,
177
+ prompt,
178
+ negativePrompt,
179
+ seed: ref.seed,
180
+ cfgScale,
181
+ aspectRatio: aspect,
182
+ resolution: '1K',
183
+ referenceImagePaths: refPaths.length > 0 ? refPaths : undefined,
184
+ }, { provenance: 'generate', hasFace: ref.characters.length > 0 });
185
+ return { path: finalPath, skipped: false };
186
+ }
187
+ // ---------------------------------------------------------------------------
188
+ // Beat planning
189
+ // ---------------------------------------------------------------------------
190
+ function shotIdOf(shot) {
191
+ return shot.shotIdSuffix ? `${shot.shotNumber}${shot.shotIdSuffix}` : shot.shotNumber;
192
+ }
193
+ function seedFromString(s) {
194
+ return Math.abs([...s].reduce((h, c) => ((h << 5) - h + c.charCodeAt(0)) | 0, 0)) % 999_999_999;
195
+ }
196
+ /**
197
+ * Plan storyboard blocking plates per scene BEAT (user decision 2026-07-30:
198
+ * per-beat granularity, auto-planned, reused across the beat's shots).
199
+ *
200
+ * A beat is a maximal run of consecutive shots that:
201
+ * - involve 2+ characters (single-character shots don't need blocking), and
202
+ * - share the same location, and
203
+ * - keep an overlapping character set (a full cast change starts a new beat).
204
+ *
205
+ * Each beat gets one StoryboardReference whose description is synthesized
206
+ * from the beat's first multi-character shot; shots in the beat get
207
+ * `storyboardRef` set to the plate's slug. Pre-existing per-shot
208
+ * `storyboardRef` values are always respected and never overwritten.
209
+ */
210
+ export function planStoryboardBeats(script) {
211
+ const refs = [];
212
+ const shots = script.shots;
213
+ let i = 0;
214
+ while (i < shots.length) {
215
+ const shot = shots[i];
216
+ const chars = shot.characters.map(c => c.toUpperCase());
217
+ if (chars.length < 2) {
218
+ i += 1;
219
+ continue;
220
+ }
221
+ // Extend the beat window.
222
+ let j = i + 1;
223
+ while (j < shots.length) {
224
+ const next = shots[j];
225
+ const nextChars = next.characters.map(c => c.toUpperCase());
226
+ if (nextChars.length < 2)
227
+ break;
228
+ if ((next.location ?? '') !== (shot.location ?? ''))
229
+ break;
230
+ if (!nextChars.some(c => chars.includes(c)))
231
+ break;
232
+ j += 1;
233
+ }
234
+ const beatShots = shots.slice(i, j);
235
+ const allChars = Array.from(new Set(beatShots.flatMap(s => s.characters)));
236
+ const slug = [
237
+ `e${String(script.episode).padStart(2, '0')}`,
238
+ `beat-${String(shotIdOf(beatShots[0]))}`,
239
+ ...(shot.location ? [shot.location] : []),
240
+ ].join('-').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
241
+ refs.push({
242
+ slug,
243
+ description: shot.panelDescription ?? shot.description,
244
+ characters: allChars,
245
+ ...(shot.location ? { location: shot.location } : {}),
246
+ episode: script.episode,
247
+ shotIds: beatShots.map(shotIdOf),
248
+ seed: seedFromString(slug),
249
+ });
250
+ for (const s of beatShots) {
251
+ if (!s.storyboardRef)
252
+ s.storyboardRef = slug;
253
+ }
254
+ i = j;
255
+ }
256
+ return refs;
257
+ }
258
+ /**
259
+ * Plan beats for an episode (idempotent — respects existing storyboardRefs
260
+ * and previously planned plates), merge them into script.storyboardRefs, and
261
+ * generate any plates missing on disk. Called from workshop-episode after
262
+ * location generation, and best-effort from generate-videos so hand-edited
263
+ * scripts pick up plates too.
264
+ */
265
+ export async function ensureEpisodeStoryboardReferences(client, series, script, options = {}) {
266
+ const planned = planStoryboardBeats(script);
267
+ const existing = script.storyboardRefs ?? [];
268
+ const bySlug = new Map(existing.map(r => [r.slug, r]));
269
+ for (const ref of planned) {
270
+ if (!bySlug.has(ref.slug))
271
+ bySlug.set(ref.slug, ref);
272
+ }
273
+ script.storyboardRefs = Array.from(bySlug.values());
274
+ const generated = [];
275
+ const skipped = [];
276
+ if (!series.aesthetic) {
277
+ if (script.storyboardRefs.length > 0) {
278
+ console.warn(' ⚠ Storyboard beats planned but series aesthetic is not set — skipping plate generation.');
279
+ }
280
+ return { generated, skipped };
281
+ }
282
+ for (const ref of script.storyboardRefs) {
283
+ try {
284
+ const result = await generateStoryboardReference(client, series, ref, options);
285
+ (result.skipped ? skipped : generated).push(result.path);
286
+ }
287
+ catch (err) {
288
+ console.warn(` ⚠ Storyboard plate generation failed for "${ref.slug}": ${err.message}`);
289
+ }
290
+ }
291
+ return { generated, skipped };
292
+ }
293
+ //# sourceMappingURL=storyboard-reference-generator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storyboard-reference-generator.js","sourceRoot":"","sources":["../../src/mini-drama/storyboard-reference-generator.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ import type { ShotScript } from '../series/types.js';
2
+ export interface SubtitleEntry {
3
+ index: number;
4
+ startTime: string;
5
+ endTime: string;
6
+ text: string;
7
+ }
8
+ export declare function generateSubtitles(shots: ShotScript[], sceneDir?: string): SubtitleEntry[];
9
+ export declare function subtitlesToSrt(entries: SubtitleEntry[]): string;
10
+ export declare function saveSrt(entries: SubtitleEntry[], outputPath: string): Promise<string>;
11
+ export declare function burnSubtitles(inputVideo: string, srtPath: string, outputVideo: string): void;
12
+ //# sourceMappingURL=subtitle-generator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"subtitle-generator.d.ts","sourceRoot":"","sources":["../../src/mini-drama/subtitle-generator.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAErD,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACd;AAqDD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,aAAa,EAAE,CAmDzF;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE,aAAa,EAAE,GAAG,MAAM,CAI/D;AAED,wBAAsB,OAAO,CAAC,OAAO,EAAE,aAAa,EAAE,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAK3F;AAED,wBAAgB,aAAa,CAC3B,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,GAClB,IAAI,CA6BN"}
@@ -0,0 +1,139 @@
1
+ import { writeFile, mkdir } from 'node:fs/promises';
2
+ import { dirname, join } from 'node:path';
3
+ import { spawnSync } from 'node:child_process';
4
+ import { existsSync } from 'node:fs';
5
+ function runCommand(command, args) {
6
+ const result = spawnSync(command, args, {
7
+ encoding: 'utf-8',
8
+ stdio: 'pipe',
9
+ });
10
+ if (result.error)
11
+ throw result.error;
12
+ if (result.status !== 0) {
13
+ const stderr = typeof result.stderr === 'string' ? result.stderr.trim() : '';
14
+ const stdout = typeof result.stdout === 'string' ? result.stdout.trim() : '';
15
+ const detail = stderr || stdout || `exit code ${result.status}`;
16
+ throw new Error(`${command} failed: ${detail}`);
17
+ }
18
+ return typeof result.stdout === 'string' ? result.stdout : '';
19
+ }
20
+ function formatSrtTime(seconds) {
21
+ const h = Math.floor(seconds / 3600);
22
+ const m = Math.floor((seconds % 3600) / 60);
23
+ const s = Math.floor(seconds % 60);
24
+ const ms = Math.round((seconds % 1) * 1000);
25
+ return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')},${String(ms).padStart(3, '0')}`;
26
+ }
27
+ function parseDuration(dur) {
28
+ const match = dur.match(/^(\d+)s$/);
29
+ return match ? parseInt(match[1]) : 5;
30
+ }
31
+ function getActualShotDuration(sceneDir, shotNumber, fallbackDuration) {
32
+ if (!sceneDir)
33
+ return parseDuration(fallbackDuration);
34
+ const videoPath = join(sceneDir, `shot-${String(shotNumber).padStart(3, '0')}.mp4`);
35
+ if (!existsSync(videoPath))
36
+ return parseDuration(fallbackDuration);
37
+ try {
38
+ const out = runCommand('ffprobe', [
39
+ '-v',
40
+ 'error',
41
+ '-show_entries',
42
+ 'format=duration',
43
+ '-of',
44
+ 'csv=p=0',
45
+ videoPath,
46
+ ]).trim();
47
+ const duration = parseFloat(out);
48
+ return Number.isFinite(duration) ? duration : parseDuration(fallbackDuration);
49
+ }
50
+ catch {
51
+ return parseDuration(fallbackDuration);
52
+ }
53
+ }
54
+ export function generateSubtitles(shots, sceneDir) {
55
+ const entries = [];
56
+ let currentTime = 0;
57
+ let index = 1;
58
+ for (const shot of shots) {
59
+ const shotDuration = getActualShotDuration(sceneDir, shot.shotNumber, shot.duration);
60
+ if (shot.dialogue) {
61
+ const words = shot.dialogue.line.split(/\s+/);
62
+ const wordsPerSecond = 2.5;
63
+ const speakDuration = Math.min(words.length / wordsPerSecond + 0.5, shotDuration - 0.3);
64
+ // AI dialogue usually starts well after the cut; bias subtitles as late as
65
+ // possible while still letting the full line finish before the shot ends.
66
+ const startOffset = Math.max(0.3, shotDuration - speakDuration - 0.15);
67
+ const start = currentTime + startOffset;
68
+ const end = currentTime + startOffset + speakDuration;
69
+ if (words.length <= 8) {
70
+ entries.push({
71
+ index: index++,
72
+ startTime: formatSrtTime(start),
73
+ endTime: formatSrtTime(end),
74
+ text: shot.dialogue.line,
75
+ });
76
+ }
77
+ else {
78
+ const mid = Math.ceil(words.length / 2);
79
+ const firstHalf = words.slice(0, mid).join(' ');
80
+ const secondHalf = words.slice(mid).join(' ');
81
+ const halfDuration = speakDuration / 2;
82
+ entries.push({
83
+ index: index++,
84
+ startTime: formatSrtTime(start),
85
+ endTime: formatSrtTime(start + halfDuration),
86
+ text: firstHalf,
87
+ });
88
+ entries.push({
89
+ index: index++,
90
+ startTime: formatSrtTime(start + halfDuration),
91
+ endTime: formatSrtTime(end),
92
+ text: secondHalf,
93
+ });
94
+ }
95
+ }
96
+ currentTime += shotDuration;
97
+ }
98
+ return entries;
99
+ }
100
+ export function subtitlesToSrt(entries) {
101
+ return entries
102
+ .map(e => `${e.index}\n${e.startTime} --> ${e.endTime}\n${e.text}\n`)
103
+ .join('\n');
104
+ }
105
+ export async function saveSrt(entries, outputPath) {
106
+ await mkdir(dirname(outputPath), { recursive: true });
107
+ const srtContent = subtitlesToSrt(entries);
108
+ await writeFile(outputPath, srtContent, 'utf-8');
109
+ return outputPath;
110
+ }
111
+ export function burnSubtitles(inputVideo, srtPath, outputVideo) {
112
+ const escapedSrt = srtPath.replace(/'/g, "'\\''").replace(/:/g, '\\:');
113
+ const subtitleFilter = [
114
+ `subtitles='${escapedSrt}'`,
115
+ 'force_style=\'',
116
+ 'FontName=Arial,',
117
+ 'FontSize=22,',
118
+ 'Bold=1,',
119
+ 'PrimaryColour=&H00FFFFFF,',
120
+ 'OutlineColour=&H00000000,',
121
+ 'BackColour=&H80000000,',
122
+ 'Outline=2,',
123
+ 'Shadow=1,',
124
+ 'Alignment=2,',
125
+ 'MarginV=150',
126
+ '\'',
127
+ ].join('');
128
+ runCommand('ffmpeg', [
129
+ '-y',
130
+ '-i',
131
+ inputVideo,
132
+ '-vf',
133
+ subtitleFilter,
134
+ '-c:a',
135
+ 'copy',
136
+ outputVideo,
137
+ ]);
138
+ }
139
+ //# sourceMappingURL=subtitle-generator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"subtitle-generator.js","sourceRoot":"","sources":["../../src/mini-drama/subtitle-generator.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { TimelineExportOptions } from './types.js';
2
+ export declare function exportDavinciFcpxml(opts: TimelineExportOptions): Promise<string>;
3
+ //# sourceMappingURL=davinci-fcpxml.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"davinci-fcpxml.d.ts","sourceRoot":"","sources":["../../../src/mini-drama/timeline-export/davinci-fcpxml.ts"],"names":[],"mappings":"AA8BA,OAAO,KAAK,EAAqB,qBAAqB,EAAmB,MAAM,YAAY,CAAC;AAuB5F,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,qBAAqB,GAAG,OAAO,CAAC,MAAM,CAAC,CAuHtF"}
@@ -0,0 +1,150 @@
1
+ // ---------------------------------------------------------------------------
2
+ // DaVinci Resolve-tuned FCPXML 1.10 export.
3
+ //
4
+ // Resolve imports the FCPXML 1.10 emitted by ./fcpxml.ts today, but the
5
+ // import is lossy in three known ways:
6
+ //
7
+ // 1. The <format colorSpace="1-1-1 (Rec. 709)"/> attribute sometimes makes
8
+ // Resolve flag the timeline as "missing LUT" or apply an extra color
9
+ // transform that doesn't match the source. The Resolve-tuned variant
10
+ // drops the colorSpace attribute and lets Resolve infer.
11
+ // 2. Mono dialogue clips imported via the FCP X path get expanded to
12
+ // stereo Resolve tracks. The tuned variant emits the
13
+ // <audio-channel-source> / track-format hint Resolve actually honors,
14
+ // keeping mono dialogue on a mono track.
15
+ // 3. URI-encoded paths (`%20` for spaces, `%27` for apostrophes) sometimes
16
+ // fail on Linux Resolve builds when the source filename has spaces.
17
+ // The tuned variant uses raw absolute paths inside a plain `file://`
18
+ // prefix — works on macOS Resolve and tested-ok on Linux per the issue
19
+ // thread.
20
+ //
21
+ // Apart from those three differences, the output is byte-for-byte the same
22
+ // as the FCP X path. A future PR can fork further if Resolve ships new
23
+ // import gates we discover during testing.
24
+ //
25
+ // ---------------------------------------------------------------------------
26
+ import { mkdir, writeFile } from 'node:fs/promises';
27
+ import { existsSync } from 'node:fs';
28
+ import { dirname } from 'node:path';
29
+ import { probeAudioInfo, segmentContaining, toRationalTime, xmlEscape } from './probe.js';
30
+ /**
31
+ * DaVinci-tuned variant of `pathToFileUri`. Wraps an absolute path in a
32
+ * `file://` prefix WITHOUT URI-encoding spaces, apostrophes, or other
33
+ * filesystem-legal characters. macOS / Linux Resolve both accept this form;
34
+ * the FCP X path doesn't because FCPXML 1.10 validators sometimes flag it.
35
+ */
36
+ function pathToResolveFileUri(p) {
37
+ return 'file://' + p;
38
+ }
39
+ export async function exportDavinciFcpxml(opts) {
40
+ const fps = opts.fps ?? 24;
41
+ const width = opts.width ?? 1920;
42
+ const height = opts.height ?? 1080;
43
+ const eventName = opts.eventName ?? 'Episode';
44
+ const projectName = opts.projectName ?? `${eventName} — fine-tune`;
45
+ const assetById = new Map();
46
+ let counter = 1;
47
+ const FORMAT_ID = 'r1';
48
+ async function registerAsset(path, hasVideo, durSec, name) {
49
+ const existing = assetById.get(path);
50
+ if (existing)
51
+ return existing;
52
+ const id = `r${++counter + 1}`;
53
+ let audioRate = 48000;
54
+ let audioChannels = 2;
55
+ try {
56
+ const info = await probeAudioInfo(path);
57
+ audioRate = info.sampleRate;
58
+ audioChannels = info.channels;
59
+ }
60
+ catch { /* best-effort */ }
61
+ const rec = {
62
+ id, name, path,
63
+ hasVideo,
64
+ hasAudio: true,
65
+ durRt: toRationalTime(durSec, fps),
66
+ audioRate,
67
+ audioChannels,
68
+ };
69
+ assetById.set(path, rec);
70
+ return rec;
71
+ }
72
+ for (const s of opts.segments) {
73
+ if (!existsSync(s.path)) {
74
+ throw new Error(`exportDavinciFcpxml: missing segment ${s.path}`);
75
+ }
76
+ s._asset =
77
+ await registerAsset(s.path, true, s.durSec, s.label);
78
+ }
79
+ for (const a of opts.audio) {
80
+ if (!existsSync(a.path)) {
81
+ throw new Error(`exportDavinciFcpxml: missing audio file ${a.path}`);
82
+ }
83
+ a._asset =
84
+ await registerAsset(a.path, false, a.audioDur, a.label);
85
+ }
86
+ const buckets = opts.segments.map(() => []);
87
+ for (const a of opts.audio) {
88
+ buckets[segmentContaining(opts.segments, a.startSec)].push(a);
89
+ }
90
+ const masterDur = opts.totalDurationSec
91
+ ?? opts.segments.reduce((acc, s) => Math.max(acc, s.startSec + s.durSec), 0);
92
+ const lines = [];
93
+ lines.push(`<?xml version="1.0" encoding="UTF-8"?>`);
94
+ lines.push(`<!DOCTYPE fcpxml>`);
95
+ lines.push(`<fcpxml version="1.10">`);
96
+ lines.push(` <resources>`);
97
+ // difference (1): drop colorSpace attribute — Resolve infers it
98
+ // from the source media and avoids the false "missing LUT" warning.
99
+ lines.push(` <format id="${FORMAT_ID}" name="FFVideoFormat${width}x${height}p${fps}" frameDuration="1/${fps}s" width="${width}" height="${height}"/>`);
100
+ for (const a of assetById.values()) {
101
+ // difference (3): raw file:// path, no URI-encoding.
102
+ const uri = pathToResolveFileUri(a.path);
103
+ const hasVideo = a.hasVideo ? '1' : '0';
104
+ const formatAttr = a.hasVideo ? ` format="${FORMAT_ID}"` : '';
105
+ lines.push(` <asset id="${a.id}" name="${xmlEscape(a.name)}" start="0s" duration="${a.durRt}" hasVideo="${hasVideo}" hasAudio="1"${formatAttr} audioSources="1" audioChannels="${a.audioChannels}" audioRate="${a.audioRate}">`);
106
+ lines.push(` <media-rep kind="original-media" src="${uri}"/>`);
107
+ lines.push(` </asset>`);
108
+ }
109
+ lines.push(` </resources>`);
110
+ lines.push(` <library>`);
111
+ lines.push(` <event name="${xmlEscape(eventName)}">`);
112
+ lines.push(` <project name="${xmlEscape(projectName)}">`);
113
+ lines.push(` <sequence format="${FORMAT_ID}" duration="${toRationalTime(masterDur, fps)}" tcStart="0s" tcFormat="NDF" audioLayout="stereo" audioRate="48k">`);
114
+ lines.push(` <spine>`);
115
+ for (let i = 0; i < opts.segments.length; i++) {
116
+ const s = opts.segments[i];
117
+ const segAsset = s._asset;
118
+ const children = buckets[i];
119
+ const open = ` <asset-clip name="${xmlEscape(s.label)}" ref="${segAsset.id}" offset="${toRationalTime(s.startSec, fps)}" duration="${toRationalTime(s.durSec, fps)}" start="0s" tcFormat="NDF" audioRole="dialogue.dialogue"`;
120
+ if (children.length === 0) {
121
+ lines.push(open + `>`);
122
+ lines.push(` <adjust-volume amount="-96dB"/>`);
123
+ lines.push(` </asset-clip>`);
124
+ continue;
125
+ }
126
+ lines.push(open + `>`);
127
+ lines.push(` <adjust-volume amount="-96dB"/>`);
128
+ for (const ch of children) {
129
+ const chAsset = ch._asset;
130
+ const localOffset = ch.startSec - s.startSec;
131
+ // difference (2): emit <audio-channel-source> hinting at the
132
+ // actual channel count Resolve should mount the track as. Mono
133
+ // dialogue (channels=1) imports as a mono Resolve track; stereo
134
+ // music (channels=2) imports as stereo. FCP X ignores this attribute.
135
+ const channelHint = chAsset.audioChannels === 1 ? ' srcCh="1"' : ' srcCh="1, 2"';
136
+ lines.push(` <audio name="${xmlEscape(ch.label)}" ref="${chAsset.id}" offset="${toRationalTime(localOffset, fps)}" duration="${toRationalTime(ch.audioDur, fps)}" start="0s" lane="${ch.lane}" role="${ch.role}"${channelHint}/>`);
137
+ }
138
+ lines.push(` </asset-clip>`);
139
+ }
140
+ lines.push(` </spine>`);
141
+ lines.push(` </sequence>`);
142
+ lines.push(` </project>`);
143
+ lines.push(` </event>`);
144
+ lines.push(` </library>`);
145
+ lines.push(`</fcpxml>`);
146
+ await mkdir(dirname(opts.outputPath), { recursive: true });
147
+ await writeFile(opts.outputPath, lines.join('\n') + '\n', 'utf-8');
148
+ return opts.outputPath;
149
+ }
150
+ //# sourceMappingURL=davinci-fcpxml.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"davinci-fcpxml.js","sourceRoot":"","sources":["../../../src/mini-drama/timeline-export/davinci-fcpxml.ts"],"names":[],"mappings":"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"}