@vibeframe/cli 0.27.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 (420) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/.turbo/turbo-lint.log +21 -0
  3. package/.turbo/turbo-test.log +689 -0
  4. package/dist/agent/adapters/claude.d.ts +15 -0
  5. package/dist/agent/adapters/claude.d.ts.map +1 -0
  6. package/dist/agent/adapters/claude.js +119 -0
  7. package/dist/agent/adapters/claude.js.map +1 -0
  8. package/dist/agent/adapters/gemini.d.ts +15 -0
  9. package/dist/agent/adapters/gemini.d.ts.map +1 -0
  10. package/dist/agent/adapters/gemini.js +132 -0
  11. package/dist/agent/adapters/gemini.js.map +1 -0
  12. package/dist/agent/adapters/index.d.ts +27 -0
  13. package/dist/agent/adapters/index.d.ts.map +1 -0
  14. package/dist/agent/adapters/index.js +38 -0
  15. package/dist/agent/adapters/index.js.map +1 -0
  16. package/dist/agent/adapters/ollama.d.ts +20 -0
  17. package/dist/agent/adapters/ollama.d.ts.map +1 -0
  18. package/dist/agent/adapters/ollama.js +186 -0
  19. package/dist/agent/adapters/ollama.js.map +1 -0
  20. package/dist/agent/adapters/openai.d.ts +15 -0
  21. package/dist/agent/adapters/openai.d.ts.map +1 -0
  22. package/dist/agent/adapters/openai.js +92 -0
  23. package/dist/agent/adapters/openai.js.map +1 -0
  24. package/dist/agent/adapters/xai.d.ts +15 -0
  25. package/dist/agent/adapters/xai.d.ts.map +1 -0
  26. package/dist/agent/adapters/xai.js +95 -0
  27. package/dist/agent/adapters/xai.js.map +1 -0
  28. package/dist/agent/index.d.ts +69 -0
  29. package/dist/agent/index.d.ts.map +1 -0
  30. package/dist/agent/index.js +180 -0
  31. package/dist/agent/index.js.map +1 -0
  32. package/dist/agent/memory/index.d.ts +70 -0
  33. package/dist/agent/memory/index.d.ts.map +1 -0
  34. package/dist/agent/memory/index.js +132 -0
  35. package/dist/agent/memory/index.js.map +1 -0
  36. package/dist/agent/prompts/system.d.ts +6 -0
  37. package/dist/agent/prompts/system.d.ts.map +1 -0
  38. package/dist/agent/prompts/system.js +103 -0
  39. package/dist/agent/prompts/system.js.map +1 -0
  40. package/dist/agent/tools/ai-editing.d.ts +15 -0
  41. package/dist/agent/tools/ai-editing.d.ts.map +1 -0
  42. package/dist/agent/tools/ai-editing.js +763 -0
  43. package/dist/agent/tools/ai-editing.js.map +1 -0
  44. package/dist/agent/tools/ai-generation.d.ts +13 -0
  45. package/dist/agent/tools/ai-generation.d.ts.map +1 -0
  46. package/dist/agent/tools/ai-generation.js +973 -0
  47. package/dist/agent/tools/ai-generation.js.map +1 -0
  48. package/dist/agent/tools/ai-pipeline.d.ts +14 -0
  49. package/dist/agent/tools/ai-pipeline.d.ts.map +1 -0
  50. package/dist/agent/tools/ai-pipeline.js +961 -0
  51. package/dist/agent/tools/ai-pipeline.js.map +1 -0
  52. package/dist/agent/tools/ai.d.ts +13 -0
  53. package/dist/agent/tools/ai.d.ts.map +1 -0
  54. package/dist/agent/tools/ai.js +19 -0
  55. package/dist/agent/tools/ai.js.map +1 -0
  56. package/dist/agent/tools/batch.d.ts +6 -0
  57. package/dist/agent/tools/batch.d.ts.map +1 -0
  58. package/dist/agent/tools/batch.js +383 -0
  59. package/dist/agent/tools/batch.js.map +1 -0
  60. package/dist/agent/tools/e2e.test.d.ts +26 -0
  61. package/dist/agent/tools/e2e.test.d.ts.map +1 -0
  62. package/dist/agent/tools/e2e.test.js +397 -0
  63. package/dist/agent/tools/e2e.test.js.map +1 -0
  64. package/dist/agent/tools/export.d.ts +6 -0
  65. package/dist/agent/tools/export.d.ts.map +1 -0
  66. package/dist/agent/tools/export.js +171 -0
  67. package/dist/agent/tools/export.js.map +1 -0
  68. package/dist/agent/tools/filesystem.d.ts +6 -0
  69. package/dist/agent/tools/filesystem.d.ts.map +1 -0
  70. package/dist/agent/tools/filesystem.js +212 -0
  71. package/dist/agent/tools/filesystem.js.map +1 -0
  72. package/dist/agent/tools/index.d.ts +65 -0
  73. package/dist/agent/tools/index.d.ts.map +1 -0
  74. package/dist/agent/tools/index.js +120 -0
  75. package/dist/agent/tools/index.js.map +1 -0
  76. package/dist/agent/tools/integration.test.d.ts +11 -0
  77. package/dist/agent/tools/integration.test.d.ts.map +1 -0
  78. package/dist/agent/tools/integration.test.js +659 -0
  79. package/dist/agent/tools/integration.test.js.map +1 -0
  80. package/dist/agent/tools/media.d.ts +6 -0
  81. package/dist/agent/tools/media.d.ts.map +1 -0
  82. package/dist/agent/tools/media.js +616 -0
  83. package/dist/agent/tools/media.js.map +1 -0
  84. package/dist/agent/tools/project.d.ts +6 -0
  85. package/dist/agent/tools/project.d.ts.map +1 -0
  86. package/dist/agent/tools/project.js +284 -0
  87. package/dist/agent/tools/project.js.map +1 -0
  88. package/dist/agent/tools/timeline.d.ts +6 -0
  89. package/dist/agent/tools/timeline.d.ts.map +1 -0
  90. package/dist/agent/tools/timeline.js +873 -0
  91. package/dist/agent/tools/timeline.js.map +1 -0
  92. package/dist/agent/types.d.ts +59 -0
  93. package/dist/agent/types.d.ts.map +1 -0
  94. package/dist/agent/types.js +5 -0
  95. package/dist/agent/types.js.map +1 -0
  96. package/dist/commands/agent.d.ts +21 -0
  97. package/dist/commands/agent.d.ts.map +1 -0
  98. package/dist/commands/agent.js +290 -0
  99. package/dist/commands/agent.js.map +1 -0
  100. package/dist/commands/ai-analyze.d.ts +106 -0
  101. package/dist/commands/ai-analyze.d.ts.map +1 -0
  102. package/dist/commands/ai-analyze.js +327 -0
  103. package/dist/commands/ai-analyze.js.map +1 -0
  104. package/dist/commands/ai-animated-caption.d.ts +64 -0
  105. package/dist/commands/ai-animated-caption.d.ts.map +1 -0
  106. package/dist/commands/ai-animated-caption.js +272 -0
  107. package/dist/commands/ai-animated-caption.js.map +1 -0
  108. package/dist/commands/ai-audio.d.ts +20 -0
  109. package/dist/commands/ai-audio.d.ts.map +1 -0
  110. package/dist/commands/ai-audio.js +808 -0
  111. package/dist/commands/ai-audio.js.map +1 -0
  112. package/dist/commands/ai-broll.d.ts +15 -0
  113. package/dist/commands/ai-broll.d.ts.map +1 -0
  114. package/dist/commands/ai-broll.js +406 -0
  115. package/dist/commands/ai-broll.js.map +1 -0
  116. package/dist/commands/ai-edit-cli.d.ts +14 -0
  117. package/dist/commands/ai-edit-cli.d.ts.map +1 -0
  118. package/dist/commands/ai-edit-cli.js +579 -0
  119. package/dist/commands/ai-edit-cli.js.map +1 -0
  120. package/dist/commands/ai-edit.d.ts +398 -0
  121. package/dist/commands/ai-edit.d.ts.map +1 -0
  122. package/dist/commands/ai-edit.js +1019 -0
  123. package/dist/commands/ai-edit.js.map +1 -0
  124. package/dist/commands/ai-fill-gaps.d.ts +14 -0
  125. package/dist/commands/ai-fill-gaps.d.ts.map +1 -0
  126. package/dist/commands/ai-fill-gaps.js +451 -0
  127. package/dist/commands/ai-fill-gaps.js.map +1 -0
  128. package/dist/commands/ai-helpers.d.ts +20 -0
  129. package/dist/commands/ai-helpers.d.ts.map +1 -0
  130. package/dist/commands/ai-helpers.js +59 -0
  131. package/dist/commands/ai-helpers.js.map +1 -0
  132. package/dist/commands/ai-highlights.d.ts +127 -0
  133. package/dist/commands/ai-highlights.d.ts.map +1 -0
  134. package/dist/commands/ai-highlights.js +1026 -0
  135. package/dist/commands/ai-highlights.js.map +1 -0
  136. package/dist/commands/ai-image.d.ts +34 -0
  137. package/dist/commands/ai-image.d.ts.map +1 -0
  138. package/dist/commands/ai-image.js +653 -0
  139. package/dist/commands/ai-image.js.map +1 -0
  140. package/dist/commands/ai-motion.d.ts +50 -0
  141. package/dist/commands/ai-motion.d.ts.map +1 -0
  142. package/dist/commands/ai-motion.js +271 -0
  143. package/dist/commands/ai-motion.js.map +1 -0
  144. package/dist/commands/ai-narrate.d.ts +66 -0
  145. package/dist/commands/ai-narrate.d.ts.map +1 -0
  146. package/dist/commands/ai-narrate.js +329 -0
  147. package/dist/commands/ai-narrate.js.map +1 -0
  148. package/dist/commands/ai-review.d.ts +57 -0
  149. package/dist/commands/ai-review.d.ts.map +1 -0
  150. package/dist/commands/ai-review.js +251 -0
  151. package/dist/commands/ai-review.js.map +1 -0
  152. package/dist/commands/ai-script-pipeline-cli.d.ts +9 -0
  153. package/dist/commands/ai-script-pipeline-cli.d.ts.map +1 -0
  154. package/dist/commands/ai-script-pipeline-cli.js +1494 -0
  155. package/dist/commands/ai-script-pipeline-cli.js.map +1 -0
  156. package/dist/commands/ai-script-pipeline.d.ts +259 -0
  157. package/dist/commands/ai-script-pipeline.d.ts.map +1 -0
  158. package/dist/commands/ai-script-pipeline.js +1027 -0
  159. package/dist/commands/ai-script-pipeline.js.map +1 -0
  160. package/dist/commands/ai-suggest-edit.d.ts +14 -0
  161. package/dist/commands/ai-suggest-edit.d.ts.map +1 -0
  162. package/dist/commands/ai-suggest-edit.js +220 -0
  163. package/dist/commands/ai-suggest-edit.js.map +1 -0
  164. package/dist/commands/ai-video-fx.d.ts +14 -0
  165. package/dist/commands/ai-video-fx.d.ts.map +1 -0
  166. package/dist/commands/ai-video-fx.js +395 -0
  167. package/dist/commands/ai-video-fx.js.map +1 -0
  168. package/dist/commands/ai-video.d.ts +15 -0
  169. package/dist/commands/ai-video.d.ts.map +1 -0
  170. package/dist/commands/ai-video.js +785 -0
  171. package/dist/commands/ai-video.js.map +1 -0
  172. package/dist/commands/ai-viral.d.ts +15 -0
  173. package/dist/commands/ai-viral.d.ts.map +1 -0
  174. package/dist/commands/ai-viral.js +519 -0
  175. package/dist/commands/ai-viral.js.map +1 -0
  176. package/dist/commands/ai-visual-fx.d.ts +14 -0
  177. package/dist/commands/ai-visual-fx.d.ts.map +1 -0
  178. package/dist/commands/ai-visual-fx.js +505 -0
  179. package/dist/commands/ai-visual-fx.js.map +1 -0
  180. package/dist/commands/ai.d.ts +38 -0
  181. package/dist/commands/ai.d.ts.map +1 -0
  182. package/dist/commands/ai.js +225 -0
  183. package/dist/commands/ai.js.map +1 -0
  184. package/dist/commands/ai.test.d.ts +2 -0
  185. package/dist/commands/ai.test.d.ts.map +1 -0
  186. package/dist/commands/ai.test.js +554 -0
  187. package/dist/commands/ai.test.js.map +1 -0
  188. package/dist/commands/analyze.d.ts +16 -0
  189. package/dist/commands/analyze.d.ts.map +1 -0
  190. package/dist/commands/analyze.js +247 -0
  191. package/dist/commands/analyze.js.map +1 -0
  192. package/dist/commands/audio.d.ts +18 -0
  193. package/dist/commands/audio.d.ts.map +1 -0
  194. package/dist/commands/audio.js +539 -0
  195. package/dist/commands/audio.js.map +1 -0
  196. package/dist/commands/batch.d.ts +3 -0
  197. package/dist/commands/batch.d.ts.map +1 -0
  198. package/dist/commands/batch.js +366 -0
  199. package/dist/commands/batch.js.map +1 -0
  200. package/dist/commands/batch.test.d.ts +2 -0
  201. package/dist/commands/batch.test.d.ts.map +1 -0
  202. package/dist/commands/batch.test.js +203 -0
  203. package/dist/commands/batch.test.js.map +1 -0
  204. package/dist/commands/detect.d.ts +3 -0
  205. package/dist/commands/detect.d.ts.map +1 -0
  206. package/dist/commands/detect.js +273 -0
  207. package/dist/commands/detect.js.map +1 -0
  208. package/dist/commands/doctor.d.ts +6 -0
  209. package/dist/commands/doctor.d.ts.map +1 -0
  210. package/dist/commands/doctor.js +191 -0
  211. package/dist/commands/doctor.js.map +1 -0
  212. package/dist/commands/edit-cmd.d.ts +26 -0
  213. package/dist/commands/edit-cmd.d.ts.map +1 -0
  214. package/dist/commands/edit-cmd.js +870 -0
  215. package/dist/commands/edit-cmd.js.map +1 -0
  216. package/dist/commands/export.d.ts +39 -0
  217. package/dist/commands/export.d.ts.map +1 -0
  218. package/dist/commands/export.js +730 -0
  219. package/dist/commands/export.js.map +1 -0
  220. package/dist/commands/generate.d.ts +25 -0
  221. package/dist/commands/generate.d.ts.map +1 -0
  222. package/dist/commands/generate.js +1885 -0
  223. package/dist/commands/generate.js.map +1 -0
  224. package/dist/commands/media.d.ts +3 -0
  225. package/dist/commands/media.d.ts.map +1 -0
  226. package/dist/commands/media.js +165 -0
  227. package/dist/commands/media.js.map +1 -0
  228. package/dist/commands/output.d.ts +45 -0
  229. package/dist/commands/output.d.ts.map +1 -0
  230. package/dist/commands/output.js +122 -0
  231. package/dist/commands/output.js.map +1 -0
  232. package/dist/commands/pipeline.d.ts +19 -0
  233. package/dist/commands/pipeline.d.ts.map +1 -0
  234. package/dist/commands/pipeline.js +345 -0
  235. package/dist/commands/pipeline.js.map +1 -0
  236. package/dist/commands/project.d.ts +3 -0
  237. package/dist/commands/project.d.ts.map +1 -0
  238. package/dist/commands/project.js +139 -0
  239. package/dist/commands/project.js.map +1 -0
  240. package/dist/commands/project.test.d.ts +2 -0
  241. package/dist/commands/project.test.d.ts.map +1 -0
  242. package/dist/commands/project.test.js +105 -0
  243. package/dist/commands/project.test.js.map +1 -0
  244. package/dist/commands/sanitize.d.ts +21 -0
  245. package/dist/commands/sanitize.d.ts.map +1 -0
  246. package/dist/commands/sanitize.js +56 -0
  247. package/dist/commands/sanitize.js.map +1 -0
  248. package/dist/commands/schema.d.ts +11 -0
  249. package/dist/commands/schema.d.ts.map +1 -0
  250. package/dist/commands/schema.js +101 -0
  251. package/dist/commands/schema.js.map +1 -0
  252. package/dist/commands/setup.d.ts +6 -0
  253. package/dist/commands/setup.d.ts.map +1 -0
  254. package/dist/commands/setup.js +440 -0
  255. package/dist/commands/setup.js.map +1 -0
  256. package/dist/commands/timeline.d.ts +3 -0
  257. package/dist/commands/timeline.d.ts.map +1 -0
  258. package/dist/commands/timeline.js +469 -0
  259. package/dist/commands/timeline.js.map +1 -0
  260. package/dist/commands/timeline.test.d.ts +2 -0
  261. package/dist/commands/timeline.test.d.ts.map +1 -0
  262. package/dist/commands/timeline.test.js +320 -0
  263. package/dist/commands/timeline.test.js.map +1 -0
  264. package/dist/commands/validate.d.ts +32 -0
  265. package/dist/commands/validate.d.ts.map +1 -0
  266. package/dist/commands/validate.js +63 -0
  267. package/dist/commands/validate.js.map +1 -0
  268. package/dist/config/config.test.d.ts +2 -0
  269. package/dist/config/config.test.d.ts.map +1 -0
  270. package/dist/config/config.test.js +164 -0
  271. package/dist/config/config.test.js.map +1 -0
  272. package/dist/config/index.d.ts +35 -0
  273. package/dist/config/index.d.ts.map +1 -0
  274. package/dist/config/index.js +101 -0
  275. package/dist/config/index.js.map +1 -0
  276. package/dist/config/schema.d.ts +43 -0
  277. package/dist/config/schema.d.ts.map +1 -0
  278. package/dist/config/schema.js +42 -0
  279. package/dist/config/schema.js.map +1 -0
  280. package/dist/engine/index.d.ts +3 -0
  281. package/dist/engine/index.d.ts.map +1 -0
  282. package/dist/engine/index.js +2 -0
  283. package/dist/engine/index.js.map +1 -0
  284. package/dist/engine/project.d.ts +84 -0
  285. package/dist/engine/project.d.ts.map +1 -0
  286. package/dist/engine/project.js +355 -0
  287. package/dist/engine/project.js.map +1 -0
  288. package/dist/engine/project.test.d.ts +2 -0
  289. package/dist/engine/project.test.d.ts.map +1 -0
  290. package/dist/engine/project.test.js +599 -0
  291. package/dist/engine/project.test.js.map +1 -0
  292. package/dist/index.d.ts +7 -0
  293. package/dist/index.d.ts.map +1 -0
  294. package/dist/index.js +131 -0
  295. package/dist/index.js.map +1 -0
  296. package/dist/utils/api-key.d.ts +36 -0
  297. package/dist/utils/api-key.d.ts.map +1 -0
  298. package/dist/utils/api-key.js +211 -0
  299. package/dist/utils/api-key.js.map +1 -0
  300. package/dist/utils/api-key.test.d.ts +2 -0
  301. package/dist/utils/api-key.test.d.ts.map +1 -0
  302. package/dist/utils/api-key.test.js +35 -0
  303. package/dist/utils/api-key.test.js.map +1 -0
  304. package/dist/utils/audio.d.ts +23 -0
  305. package/dist/utils/audio.d.ts.map +1 -0
  306. package/dist/utils/audio.js +79 -0
  307. package/dist/utils/audio.js.map +1 -0
  308. package/dist/utils/exec-safe.d.ts +22 -0
  309. package/dist/utils/exec-safe.d.ts.map +1 -0
  310. package/dist/utils/exec-safe.js +62 -0
  311. package/dist/utils/exec-safe.js.map +1 -0
  312. package/dist/utils/first-run.d.ts +13 -0
  313. package/dist/utils/first-run.d.ts.map +1 -0
  314. package/dist/utils/first-run.js +48 -0
  315. package/dist/utils/first-run.js.map +1 -0
  316. package/dist/utils/provider-resolver.d.ts +15 -0
  317. package/dist/utils/provider-resolver.d.ts.map +1 -0
  318. package/dist/utils/provider-resolver.js +42 -0
  319. package/dist/utils/provider-resolver.js.map +1 -0
  320. package/dist/utils/remotion.d.ts +210 -0
  321. package/dist/utils/remotion.d.ts.map +1 -0
  322. package/dist/utils/remotion.js +731 -0
  323. package/dist/utils/remotion.js.map +1 -0
  324. package/dist/utils/subtitle.d.ts +65 -0
  325. package/dist/utils/subtitle.d.ts.map +1 -0
  326. package/dist/utils/subtitle.js +135 -0
  327. package/dist/utils/subtitle.js.map +1 -0
  328. package/dist/utils/subtitle.test.d.ts +2 -0
  329. package/dist/utils/subtitle.test.d.ts.map +1 -0
  330. package/dist/utils/subtitle.test.js +175 -0
  331. package/dist/utils/subtitle.test.js.map +1 -0
  332. package/dist/utils/tty.d.ts +45 -0
  333. package/dist/utils/tty.d.ts.map +1 -0
  334. package/dist/utils/tty.js +172 -0
  335. package/dist/utils/tty.js.map +1 -0
  336. package/package.json +102 -0
  337. package/src/agent/adapters/claude.ts +143 -0
  338. package/src/agent/adapters/gemini.ts +159 -0
  339. package/src/agent/adapters/index.ts +61 -0
  340. package/src/agent/adapters/ollama.ts +231 -0
  341. package/src/agent/adapters/openai.ts +116 -0
  342. package/src/agent/adapters/xai.ts +119 -0
  343. package/src/agent/index.ts +251 -0
  344. package/src/agent/memory/index.ts +151 -0
  345. package/src/agent/prompts/system.ts +106 -0
  346. package/src/agent/tools/ai-editing.ts +845 -0
  347. package/src/agent/tools/ai-generation.ts +1073 -0
  348. package/src/agent/tools/ai-pipeline.ts +1055 -0
  349. package/src/agent/tools/ai.ts +21 -0
  350. package/src/agent/tools/batch.ts +429 -0
  351. package/src/agent/tools/e2e.test.ts +545 -0
  352. package/src/agent/tools/export.ts +184 -0
  353. package/src/agent/tools/filesystem.ts +237 -0
  354. package/src/agent/tools/index.ts +150 -0
  355. package/src/agent/tools/integration.test.ts +775 -0
  356. package/src/agent/tools/media.ts +697 -0
  357. package/src/agent/tools/project.ts +313 -0
  358. package/src/agent/tools/timeline.ts +951 -0
  359. package/src/agent/types.ts +68 -0
  360. package/src/commands/agent.ts +340 -0
  361. package/src/commands/ai-analyze.ts +429 -0
  362. package/src/commands/ai-animated-caption.ts +390 -0
  363. package/src/commands/ai-audio.ts +941 -0
  364. package/src/commands/ai-broll.ts +490 -0
  365. package/src/commands/ai-edit-cli.ts +658 -0
  366. package/src/commands/ai-edit.ts +1542 -0
  367. package/src/commands/ai-fill-gaps.ts +566 -0
  368. package/src/commands/ai-helpers.ts +65 -0
  369. package/src/commands/ai-highlights.ts +1303 -0
  370. package/src/commands/ai-image.ts +761 -0
  371. package/src/commands/ai-motion.ts +347 -0
  372. package/src/commands/ai-narrate.ts +451 -0
  373. package/src/commands/ai-review.ts +309 -0
  374. package/src/commands/ai-script-pipeline-cli.ts +1710 -0
  375. package/src/commands/ai-script-pipeline.ts +1365 -0
  376. package/src/commands/ai-suggest-edit.ts +264 -0
  377. package/src/commands/ai-video-fx.ts +445 -0
  378. package/src/commands/ai-video.ts +915 -0
  379. package/src/commands/ai-viral.ts +595 -0
  380. package/src/commands/ai-visual-fx.ts +601 -0
  381. package/src/commands/ai.test.ts +627 -0
  382. package/src/commands/ai.ts +307 -0
  383. package/src/commands/analyze.ts +282 -0
  384. package/src/commands/audio.ts +644 -0
  385. package/src/commands/batch.test.ts +279 -0
  386. package/src/commands/batch.ts +440 -0
  387. package/src/commands/detect.ts +329 -0
  388. package/src/commands/doctor.ts +237 -0
  389. package/src/commands/edit-cmd.ts +1014 -0
  390. package/src/commands/export.ts +918 -0
  391. package/src/commands/generate.ts +2146 -0
  392. package/src/commands/media.ts +177 -0
  393. package/src/commands/output.ts +142 -0
  394. package/src/commands/pipeline.ts +398 -0
  395. package/src/commands/project.test.ts +127 -0
  396. package/src/commands/project.ts +149 -0
  397. package/src/commands/sanitize.ts +60 -0
  398. package/src/commands/schema.ts +130 -0
  399. package/src/commands/setup.ts +509 -0
  400. package/src/commands/timeline.test.ts +499 -0
  401. package/src/commands/timeline.ts +529 -0
  402. package/src/commands/validate.ts +77 -0
  403. package/src/config/config.test.ts +197 -0
  404. package/src/config/index.ts +125 -0
  405. package/src/config/schema.ts +82 -0
  406. package/src/engine/index.ts +2 -0
  407. package/src/engine/project.test.ts +702 -0
  408. package/src/engine/project.ts +439 -0
  409. package/src/index.ts +146 -0
  410. package/src/utils/api-key.test.ts +41 -0
  411. package/src/utils/api-key.ts +247 -0
  412. package/src/utils/audio.ts +83 -0
  413. package/src/utils/exec-safe.ts +75 -0
  414. package/src/utils/first-run.ts +52 -0
  415. package/src/utils/provider-resolver.ts +56 -0
  416. package/src/utils/remotion.ts +951 -0
  417. package/src/utils/subtitle.test.ts +227 -0
  418. package/src/utils/subtitle.ts +169 -0
  419. package/src/utils/tty.ts +196 -0
  420. package/tsconfig.json +20 -0
@@ -0,0 +1,1494 @@
1
+ /**
2
+ * @module ai-script-pipeline-cli
3
+ * @description CLI command registration for the script-to-video pipeline and
4
+ * scene regeneration commands. Execute functions and helpers live in
5
+ * ai-script-pipeline.ts; this file wires them up as Commander.js subcommands.
6
+ */
7
+ import { readFile, writeFile, mkdir, stat } from "node:fs/promises";
8
+ import { resolve, dirname, extname } from "node:path";
9
+ import { existsSync } from "node:fs";
10
+ import chalk from "chalk";
11
+ import ora from "ora";
12
+ import { GeminiProvider, OpenAIProvider, OpenAIImageProvider, ClaudeProvider, ElevenLabsProvider, KlingProvider, RunwayProvider, GrokProvider, } from "@vibeframe/ai-providers";
13
+ import { getApiKey, loadEnv } from "../utils/api-key.js";
14
+ import { getApiKeyFromConfig } from "../config/index.js";
15
+ import { Project } from "../engine/index.js";
16
+ import { getAudioDuration } from "../utils/audio.js";
17
+ import { applyTextOverlays } from "./ai-edit.js";
18
+ import { executeReview } from "./ai-review.js";
19
+ import { DEFAULT_VIDEO_RETRIES, RETRY_DELAY_MS, sleep, uploadToImgbb, extendVideoToTarget, generateVideoWithRetryKling, generateVideoWithRetryRunway, } from "./ai-script-pipeline.js";
20
+ import { downloadVideo } from "./ai-helpers.js";
21
+ export function registerScriptPipelineCommands(aiCommand) {
22
+ // Script-to-Video command
23
+ aiCommand
24
+ .command("script-to-video")
25
+ .alias("s2v")
26
+ .description("Generate complete video from text script using AI pipeline")
27
+ .argument("<script>", "Script text or file path (use -f for file)")
28
+ .option("-f, --file", "Treat script argument as file path")
29
+ .option("-o, --output <path>", "Output project file path", "script-video.vibe.json")
30
+ .option("-d, --duration <seconds>", "Target total duration in seconds")
31
+ .option("-v, --voice <id>", "ElevenLabs voice ID for narration")
32
+ .option("-g, --generator <engine>", "Video generator: kling | runway | veo", "kling")
33
+ .option("-i, --image-provider <provider>", "Image provider: gemini | openai | grok", "gemini")
34
+ .option("-a, --aspect-ratio <ratio>", "Aspect ratio: 16:9 | 9:16 | 1:1", "16:9")
35
+ .option("--images-only", "Generate images only, skip video generation")
36
+ .option("--no-voiceover", "Skip voiceover generation")
37
+ .option("--output-dir <dir>", "Directory for generated assets", "script-video-output")
38
+ .option("--retries <count>", "Number of retries for video generation failures", String(DEFAULT_VIDEO_RETRIES))
39
+ .option("--sequential", "Generate videos one at a time (slower but more reliable)")
40
+ .option("--concurrency <count>", "Max concurrent video tasks in parallel mode (default: 3)", "3")
41
+ .option("-c, --creativity <level>", "Creativity level: low (default, consistent) or high (varied, unexpected)", "low")
42
+ .option("-s, --storyboard-provider <provider>", "Storyboard provider: claude (default), openai, or gemini", "claude")
43
+ .option("--no-text-overlay", "Skip text overlay step")
44
+ .option("--text-style <style>", "Text overlay style: lower-third, center-bold, subtitle, minimal", "lower-third")
45
+ .option("--review", "Run AI review after assembly (requires GOOGLE_API_KEY)")
46
+ .option("--review-auto-apply", "Auto-apply fixable issues from AI review")
47
+ .action(async (script, options) => {
48
+ try {
49
+ // Load environment variables from .env file
50
+ loadEnv();
51
+ // Get storyboard provider API key
52
+ const storyboardProvider = (options.storyboardProvider || "claude");
53
+ let storyboardApiKey;
54
+ if (storyboardProvider === "openai") {
55
+ storyboardApiKey = (await getApiKey("OPENAI_API_KEY", "OpenAI")) ?? undefined;
56
+ if (!storyboardApiKey) {
57
+ console.error(chalk.red("OpenAI API key required for storyboard generation (-s openai). Set OPENAI_API_KEY in .env or run: vibe setup"));
58
+ process.exit(1);
59
+ }
60
+ }
61
+ else if (storyboardProvider === "gemini") {
62
+ storyboardApiKey = (await getApiKey("GOOGLE_API_KEY", "Google")) ?? undefined;
63
+ if (!storyboardApiKey) {
64
+ console.error(chalk.red("Google API key required for storyboard generation (-s gemini). Set GOOGLE_API_KEY in .env or run: vibe setup"));
65
+ process.exit(1);
66
+ }
67
+ }
68
+ else if (storyboardProvider === "claude") {
69
+ storyboardApiKey = (await getApiKey("ANTHROPIC_API_KEY", "Anthropic")) ?? undefined;
70
+ if (!storyboardApiKey) {
71
+ console.error(chalk.red("Anthropic API key required for storyboard generation. Set ANTHROPIC_API_KEY in .env or run: vibe setup"));
72
+ process.exit(1);
73
+ }
74
+ }
75
+ else {
76
+ console.error(chalk.red(`Unknown storyboard provider: ${storyboardProvider}. Use claude, openai, or gemini`));
77
+ process.exit(1);
78
+ }
79
+ // Get image provider API key
80
+ let imageApiKey;
81
+ const imageProvider = options.imageProvider || "openai";
82
+ if (imageProvider === "openai" || imageProvider === "dalle") {
83
+ imageApiKey = (await getApiKey("OPENAI_API_KEY", "OpenAI")) ?? undefined;
84
+ if (!imageApiKey) {
85
+ console.error(chalk.red("OpenAI API key required for DALL-E image generation. Set OPENAI_API_KEY in .env or run: vibe setup"));
86
+ process.exit(1);
87
+ }
88
+ }
89
+ else if (imageProvider === "gemini") {
90
+ imageApiKey = (await getApiKey("GOOGLE_API_KEY", "Google")) ?? undefined;
91
+ if (!imageApiKey) {
92
+ console.error(chalk.red("Google API key required for Gemini image generation. Set GOOGLE_API_KEY in .env or run: vibe setup"));
93
+ process.exit(1);
94
+ }
95
+ }
96
+ else if (imageProvider === "grok") {
97
+ imageApiKey = (await getApiKey("XAI_API_KEY", "xAI")) ?? undefined;
98
+ if (!imageApiKey) {
99
+ console.error(chalk.red("xAI API key required for Grok image generation. Set XAI_API_KEY in .env or run: vibe setup"));
100
+ process.exit(1);
101
+ }
102
+ }
103
+ else {
104
+ console.error(chalk.red(`Unknown image provider: ${imageProvider}. Use openai, gemini, or grok`));
105
+ process.exit(1);
106
+ }
107
+ let elevenlabsApiKey;
108
+ if (options.voiceover !== false) {
109
+ const key = await getApiKey("ELEVENLABS_API_KEY", "ElevenLabs");
110
+ if (!key) {
111
+ console.error(chalk.red("ElevenLabs API key required for voiceover (or use --no-voiceover). Set ELEVENLABS_API_KEY in .env or run: vibe setup"));
112
+ process.exit(1);
113
+ }
114
+ elevenlabsApiKey = key;
115
+ }
116
+ let videoApiKey;
117
+ if (!options.imagesOnly) {
118
+ if (options.generator === "kling") {
119
+ const key = await getApiKey("KLING_API_KEY", "Kling");
120
+ if (!key) {
121
+ console.error(chalk.red("Kling API key required (or use --images-only). Set KLING_API_KEY in .env or run: vibe setup"));
122
+ process.exit(1);
123
+ }
124
+ videoApiKey = key;
125
+ }
126
+ else {
127
+ const key = await getApiKey("RUNWAY_API_SECRET", "Runway");
128
+ if (!key) {
129
+ console.error(chalk.red("Runway API key required (or use --images-only). Set RUNWAY_API_SECRET in .env or run: vibe setup"));
130
+ process.exit(1);
131
+ }
132
+ videoApiKey = key;
133
+ }
134
+ }
135
+ // Read script content
136
+ let scriptContent = script;
137
+ if (options.file) {
138
+ const filePath = resolve(process.cwd(), script);
139
+ scriptContent = await readFile(filePath, "utf-8");
140
+ }
141
+ // Determine output directory for assets
142
+ // If -o looks like a directory and --output-dir is not explicitly set, use -o directory for assets
143
+ let effectiveOutputDir = options.outputDir;
144
+ const outputLooksLikeDirectory = options.output.endsWith("/") ||
145
+ (!options.output.endsWith(".json") && !options.output.endsWith(".vibe.json"));
146
+ if (outputLooksLikeDirectory && options.outputDir === "script-video-output") {
147
+ // User specified a directory for -o but didn't set --output-dir, use -o directory for assets
148
+ effectiveOutputDir = options.output;
149
+ }
150
+ // Create output directory
151
+ const outputDir = resolve(process.cwd(), effectiveOutputDir);
152
+ if (!existsSync(outputDir)) {
153
+ await mkdir(outputDir, { recursive: true });
154
+ }
155
+ // Validate creativity level
156
+ const creativity = options.creativity?.toLowerCase();
157
+ if (creativity && creativity !== "low" && creativity !== "high") {
158
+ console.error(chalk.red("Invalid creativity level. Use 'low' or 'high'."));
159
+ process.exit(1);
160
+ }
161
+ console.log();
162
+ console.log(chalk.bold.cyan("🎬 Script-to-Video Pipeline"));
163
+ console.log(chalk.dim("─".repeat(60)));
164
+ if (creativity === "high") {
165
+ console.log(chalk.yellow("🎨 High creativity mode: Generating varied, unexpected scenes"));
166
+ }
167
+ console.log();
168
+ // Step 1: Generate storyboard
169
+ const providerLabel = storyboardProvider.charAt(0).toUpperCase() + storyboardProvider.slice(1);
170
+ const storyboardSpinnerText = creativity === "high"
171
+ ? `Analyzing script with ${providerLabel} (high creativity)...`
172
+ : `Analyzing script with ${providerLabel}...`;
173
+ const storyboardSpinner = ora(storyboardSpinnerText).start();
174
+ let segments;
175
+ const creativityOpts = { creativity: creativity };
176
+ const durationOpt = options.duration ? parseFloat(options.duration) : undefined;
177
+ if (storyboardProvider === "openai") {
178
+ const openai = new OpenAIProvider();
179
+ await openai.initialize({ apiKey: storyboardApiKey });
180
+ segments = await openai.analyzeContent(scriptContent, durationOpt, creativityOpts);
181
+ }
182
+ else if (storyboardProvider === "gemini") {
183
+ const gemini = new GeminiProvider();
184
+ await gemini.initialize({ apiKey: storyboardApiKey });
185
+ segments = await gemini.analyzeContent(scriptContent, durationOpt, creativityOpts);
186
+ }
187
+ else {
188
+ const claude = new ClaudeProvider();
189
+ await claude.initialize({ apiKey: storyboardApiKey });
190
+ segments = await claude.analyzeContent(scriptContent, durationOpt, creativityOpts);
191
+ }
192
+ if (segments.length === 0) {
193
+ storyboardSpinner.fail(chalk.red("Failed to generate storyboard (check API key and error above)"));
194
+ process.exit(1);
195
+ }
196
+ let totalDuration = segments.reduce((sum, seg) => sum + seg.duration, 0);
197
+ storyboardSpinner.succeed(chalk.green(`Generated ${segments.length} scenes (total: ${totalDuration}s)`));
198
+ // Save storyboard
199
+ const storyboardPath = resolve(outputDir, "storyboard.json");
200
+ await writeFile(storyboardPath, JSON.stringify(segments, null, 2), "utf-8");
201
+ console.log(chalk.dim(` → Saved: ${storyboardPath}`));
202
+ console.log();
203
+ // Step 2: Generate per-scene voiceovers with ElevenLabs
204
+ const perSceneTTS = [];
205
+ const failedNarrations = [];
206
+ if (options.voiceover !== false && elevenlabsApiKey) {
207
+ const ttsSpinner = ora("🎙️ Generating voiceovers with ElevenLabs...").start();
208
+ const elevenlabs = new ElevenLabsProvider();
209
+ await elevenlabs.initialize({ apiKey: elevenlabsApiKey });
210
+ let totalCharacters = 0;
211
+ for (let i = 0; i < segments.length; i++) {
212
+ const segment = segments[i];
213
+ const narrationText = segment.narration || segment.description;
214
+ if (!narrationText)
215
+ continue;
216
+ ttsSpinner.text = `🎙️ Generating narration ${i + 1}/${segments.length}...`;
217
+ let ttsResult = await elevenlabs.textToSpeech(narrationText, {
218
+ voiceId: options.voice,
219
+ });
220
+ if (!ttsResult.success || !ttsResult.audioBuffer) {
221
+ const errorMsg = ttsResult.error || "Unknown error";
222
+ failedNarrations.push({ sceneNum: i + 1, error: errorMsg });
223
+ ttsSpinner.text = `🎙️ Generating narration ${i + 1}/${segments.length}... (failed)`;
224
+ console.log(chalk.yellow(`\n ⚠ Narration ${i + 1} failed: ${errorMsg}`));
225
+ continue;
226
+ }
227
+ const audioPath = resolve(outputDir, `narration-${i + 1}.mp3`);
228
+ await writeFile(audioPath, ttsResult.audioBuffer);
229
+ // Get actual audio duration using ffprobe
230
+ let actualDuration = await getAudioDuration(audioPath);
231
+ // Auto speed-adjust if narration slightly exceeds video bracket (5s or 10s)
232
+ const videoBracket = segment.duration > 5 ? 10 : 5;
233
+ const overageRatio = actualDuration / videoBracket;
234
+ if (overageRatio > 1.0 && overageRatio <= 1.15) {
235
+ // Narration exceeds bracket by 0-15% — regenerate slightly faster
236
+ const adjustedSpeed = Math.min(1.2, parseFloat(overageRatio.toFixed(2)));
237
+ ttsSpinner.text = `🎙️ Narration ${i + 1}: adjusting speed to ${adjustedSpeed}x...`;
238
+ const speedResult = await elevenlabs.textToSpeech(narrationText, {
239
+ voiceId: options.voice,
240
+ speed: adjustedSpeed,
241
+ });
242
+ if (speedResult.success && speedResult.audioBuffer) {
243
+ await writeFile(audioPath, speedResult.audioBuffer);
244
+ actualDuration = await getAudioDuration(audioPath);
245
+ ttsResult = speedResult;
246
+ console.log(chalk.dim(` → Speed-adjusted narration ${i + 1}: ${adjustedSpeed}x → ${actualDuration.toFixed(1)}s`));
247
+ }
248
+ }
249
+ // Update segment duration to match actual narration length
250
+ segment.duration = actualDuration;
251
+ perSceneTTS.push({ path: audioPath, duration: actualDuration, segmentIndex: i });
252
+ totalCharacters += ttsResult.characterCount || 0;
253
+ console.log(chalk.dim(` → Saved: ${audioPath} (${actualDuration.toFixed(1)}s)`));
254
+ }
255
+ // Recalculate startTime for all segments based on updated durations
256
+ let currentTime = 0;
257
+ for (const segment of segments) {
258
+ segment.startTime = currentTime;
259
+ currentTime += segment.duration;
260
+ }
261
+ // Update total duration
262
+ totalDuration = segments.reduce((sum, seg) => sum + seg.duration, 0);
263
+ // Show success with failed count if any
264
+ if (failedNarrations.length > 0) {
265
+ ttsSpinner.warn(chalk.yellow(`Generated ${perSceneTTS.length}/${segments.length} narrations (${failedNarrations.length} failed)`));
266
+ }
267
+ else {
268
+ ttsSpinner.succeed(chalk.green(`Generated ${perSceneTTS.length}/${segments.length} narrations (${totalCharacters} chars, ${totalDuration.toFixed(1)}s total)`));
269
+ }
270
+ // Re-save storyboard with updated durations
271
+ await writeFile(storyboardPath, JSON.stringify(segments, null, 2), "utf-8");
272
+ console.log(chalk.dim(` → Updated storyboard: ${storyboardPath}`));
273
+ console.log();
274
+ }
275
+ // Step 3: Generate images with selected provider
276
+ const providerNames = {
277
+ openai: "OpenAI GPT Image 1.5",
278
+ dalle: "OpenAI GPT Image 1.5", // backward compatibility
279
+ gemini: "Gemini",
280
+ grok: "xAI Grok",
281
+ };
282
+ const imageSpinner = ora(`🎨 Generating visuals with ${providerNames[imageProvider]}...`).start();
283
+ // Determine image size/aspect ratio based on provider
284
+ const dalleImageSizes = {
285
+ "16:9": "1536x1024",
286
+ "9:16": "1024x1536",
287
+ "1:1": "1024x1024",
288
+ };
289
+ const imagePaths = [];
290
+ // Store first scene image for style continuity
291
+ let firstSceneImage;
292
+ // Initialize the selected provider
293
+ let openaiImageInstance;
294
+ let geminiInstance;
295
+ let grokInstance;
296
+ if (imageProvider === "openai" || imageProvider === "dalle") {
297
+ openaiImageInstance = new OpenAIImageProvider();
298
+ await openaiImageInstance.initialize({ apiKey: imageApiKey });
299
+ }
300
+ else if (imageProvider === "gemini") {
301
+ geminiInstance = new GeminiProvider();
302
+ await geminiInstance.initialize({ apiKey: imageApiKey });
303
+ }
304
+ else if (imageProvider === "grok") {
305
+ grokInstance = new GrokProvider();
306
+ await grokInstance.initialize({ apiKey: imageApiKey });
307
+ }
308
+ // Get character description from first segment (should be same across all)
309
+ const characterDescription = segments[0]?.characterDescription;
310
+ for (let i = 0; i < segments.length; i++) {
311
+ const segment = segments[i];
312
+ imageSpinner.text = `🎨 Generating image ${i + 1}/${segments.length}: ${segment.description.slice(0, 30)}...`;
313
+ // Build comprehensive image prompt with character description
314
+ let imagePrompt = segment.visuals;
315
+ // Add character description to ensure consistency
316
+ if (characterDescription) {
317
+ imagePrompt = `CHARACTER (must match exactly): ${characterDescription}. SCENE: ${imagePrompt}`;
318
+ }
319
+ // Add visual style
320
+ if (segment.visualStyle) {
321
+ imagePrompt = `${imagePrompt}. STYLE: ${segment.visualStyle}`;
322
+ }
323
+ // For scenes after the first, add extra continuity instruction (OpenAI)
324
+ // Gemini uses editImage with reference instead
325
+ if (i > 0 && firstSceneImage && imageProvider !== "gemini") {
326
+ imagePrompt = `${imagePrompt}. CRITICAL: The character must look IDENTICAL to the first scene - same face, hair, clothing, accessories.`;
327
+ }
328
+ try {
329
+ let imageBuffer;
330
+ let imageUrl;
331
+ let imageError;
332
+ if ((imageProvider === "openai" || imageProvider === "dalle") && openaiImageInstance) {
333
+ const imageResult = await openaiImageInstance.generateImage(imagePrompt, {
334
+ size: dalleImageSizes[options.aspectRatio] || "1536x1024",
335
+ quality: "standard",
336
+ });
337
+ if (imageResult.success && imageResult.images && imageResult.images.length > 0) {
338
+ // GPT Image 1.5 returns base64, DALL-E 3 returns URL
339
+ const img = imageResult.images[0];
340
+ if (img.base64) {
341
+ imageBuffer = Buffer.from(img.base64, "base64");
342
+ }
343
+ else if (img.url) {
344
+ imageUrl = img.url;
345
+ }
346
+ }
347
+ else {
348
+ imageError = imageResult.error;
349
+ }
350
+ }
351
+ else if (imageProvider === "gemini" && geminiInstance) {
352
+ // Gemini: use editImage with first scene reference for subsequent scenes
353
+ if (i > 0 && firstSceneImage) {
354
+ // Use editImage to maintain style continuity with first scene
355
+ const editPrompt = `Create a new scene for a video: ${imagePrompt}. IMPORTANT: Maintain the exact same character appearance, clothing, environment style, color palette, and art style as the reference image.`;
356
+ const imageResult = await geminiInstance.editImage([firstSceneImage], editPrompt, {
357
+ aspectRatio: options.aspectRatio,
358
+ });
359
+ if (imageResult.success && imageResult.images && imageResult.images.length > 0) {
360
+ const img = imageResult.images[0];
361
+ if (img.base64) {
362
+ imageBuffer = Buffer.from(img.base64, "base64");
363
+ }
364
+ }
365
+ else {
366
+ imageError = imageResult.error;
367
+ }
368
+ }
369
+ else {
370
+ // First scene: use regular generateImage
371
+ const imageResult = await geminiInstance.generateImage(imagePrompt, {
372
+ aspectRatio: options.aspectRatio,
373
+ });
374
+ if (imageResult.success && imageResult.images && imageResult.images.length > 0) {
375
+ const img = imageResult.images[0];
376
+ if (img.base64) {
377
+ imageBuffer = Buffer.from(img.base64, "base64");
378
+ }
379
+ }
380
+ else {
381
+ imageError = imageResult.error;
382
+ }
383
+ }
384
+ }
385
+ else if (imageProvider === "grok" && grokInstance) {
386
+ const imageResult = await grokInstance.generateImage(imagePrompt, {
387
+ aspectRatio: options.aspectRatio || "16:9",
388
+ });
389
+ if (imageResult.success && imageResult.images && imageResult.images.length > 0) {
390
+ const img = imageResult.images[0];
391
+ if (img.base64) {
392
+ imageBuffer = Buffer.from(img.base64, "base64");
393
+ }
394
+ else if (img.url) {
395
+ imageUrl = img.url;
396
+ }
397
+ }
398
+ else {
399
+ imageError = imageResult.error;
400
+ }
401
+ }
402
+ // Save the image
403
+ const imagePath = resolve(outputDir, `scene-${i + 1}.png`);
404
+ if (imageBuffer) {
405
+ await writeFile(imagePath, imageBuffer);
406
+ imagePaths.push(imagePath);
407
+ // Store first successful image for style continuity
408
+ if (!firstSceneImage) {
409
+ firstSceneImage = imageBuffer;
410
+ }
411
+ }
412
+ else if (imageUrl) {
413
+ const response = await fetch(imageUrl);
414
+ const buffer = Buffer.from(await response.arrayBuffer());
415
+ await writeFile(imagePath, buffer);
416
+ imagePaths.push(imagePath);
417
+ // Store first successful image for style continuity
418
+ if (!firstSceneImage) {
419
+ firstSceneImage = buffer;
420
+ }
421
+ }
422
+ else {
423
+ const errorMsg = imageError || "Unknown error";
424
+ console.log(chalk.yellow(`\n ⚠ Failed to generate image for scene ${i + 1}: ${errorMsg}`));
425
+ imagePaths.push("");
426
+ }
427
+ }
428
+ catch (err) {
429
+ console.log(chalk.yellow(`\n ⚠ Error generating image for scene ${i + 1}: ${err}`));
430
+ imagePaths.push("");
431
+ }
432
+ // Small delay to avoid rate limiting
433
+ if (i < segments.length - 1) {
434
+ await new Promise((r) => setTimeout(r, 500));
435
+ }
436
+ }
437
+ const successfulImages = imagePaths.filter((p) => p !== "").length;
438
+ imageSpinner.succeed(chalk.green(`Generated ${successfulImages}/${segments.length} images with ${providerNames[imageProvider]}`));
439
+ console.log();
440
+ // Step 4: Generate videos (if not images-only)
441
+ const videoPaths = [];
442
+ const failedScenes = []; // Track failed scenes for summary
443
+ const maxRetries = parseInt(options.retries) || DEFAULT_VIDEO_RETRIES;
444
+ if (!options.imagesOnly && videoApiKey) {
445
+ const videoSpinner = ora(`🎬 Generating videos with ${options.generator === "kling" ? "Kling" : "Runway"}...`).start();
446
+ if (options.generator === "kling") {
447
+ const kling = new KlingProvider();
448
+ await kling.initialize({ apiKey: videoApiKey });
449
+ if (!kling.isConfigured()) {
450
+ videoSpinner.fail(chalk.red("Invalid Kling API key format. Use ACCESS_KEY:SECRET_KEY"));
451
+ process.exit(1);
452
+ }
453
+ // Check for ImgBB API key for image-to-video support (from config or env)
454
+ const imgbbApiKey = await getApiKeyFromConfig("imgbb") || process.env.IMGBB_API_KEY;
455
+ const useImageToVideo = !!imgbbApiKey;
456
+ if (useImageToVideo) {
457
+ videoSpinner.text = `🎬 Uploading images to ImgBB for image-to-video...`;
458
+ }
459
+ // Upload images to ImgBB if API key is available (for Kling v2.x image-to-video)
460
+ const imageUrls = [];
461
+ if (useImageToVideo) {
462
+ for (let i = 0; i < imagePaths.length; i++) {
463
+ if (imagePaths[i] && imagePaths[i] !== "") {
464
+ try {
465
+ const imageBuffer = await readFile(imagePaths[i]);
466
+ const uploadResult = await uploadToImgbb(imageBuffer, imgbbApiKey);
467
+ if (uploadResult.success && uploadResult.url) {
468
+ imageUrls[i] = uploadResult.url;
469
+ }
470
+ else {
471
+ console.log(chalk.yellow(`\n ⚠ Failed to upload image ${i + 1}: ${uploadResult.error}`));
472
+ imageUrls[i] = undefined;
473
+ }
474
+ }
475
+ catch {
476
+ imageUrls[i] = undefined;
477
+ }
478
+ }
479
+ else {
480
+ imageUrls[i] = undefined;
481
+ }
482
+ }
483
+ const uploadedCount = imageUrls.filter((u) => u).length;
484
+ if (uploadedCount > 0) {
485
+ videoSpinner.text = `🎬 Uploaded ${uploadedCount}/${imagePaths.length} images to ImgBB`;
486
+ }
487
+ }
488
+ // Sequential mode: generate one video at a time (slower but more reliable)
489
+ if (options.sequential) {
490
+ for (let i = 0; i < segments.length; i++) {
491
+ const segment = segments[i];
492
+ videoSpinner.text = `🎬 Scene ${i + 1}/${segments.length}: Starting...`;
493
+ const videoDuration = (segment.duration > 5 ? 10 : 5);
494
+ const referenceImage = imageUrls[i];
495
+ let completed = false;
496
+ for (let attempt = 0; attempt <= maxRetries && !completed; attempt++) {
497
+ const result = await generateVideoWithRetryKling(kling, segment, {
498
+ duration: videoDuration,
499
+ aspectRatio: options.aspectRatio,
500
+ referenceImage,
501
+ }, 0, // Handle retries at this level
502
+ (msg) => {
503
+ videoSpinner.text = `🎬 Scene ${i + 1}/${segments.length}: ${msg}`;
504
+ });
505
+ if (!result) {
506
+ if (attempt < maxRetries) {
507
+ videoSpinner.text = `🎬 Scene ${i + 1}: Submit failed, retry ${attempt + 1}/${maxRetries}...`;
508
+ await sleep(RETRY_DELAY_MS);
509
+ continue;
510
+ }
511
+ console.log(chalk.yellow(`\n ⚠ Failed to start video generation for scene ${i + 1}`));
512
+ videoPaths[i] = "";
513
+ failedScenes.push(i + 1);
514
+ break;
515
+ }
516
+ try {
517
+ const waitResult = await kling.waitForCompletion(result.taskId, result.type, (status) => {
518
+ videoSpinner.text = `🎬 Scene ${i + 1}/${segments.length}: ${status.status}...`;
519
+ }, 600000);
520
+ if (waitResult.status === "completed" && waitResult.videoUrl) {
521
+ const videoPath = resolve(outputDir, `scene-${i + 1}.mp4`);
522
+ const buffer = await downloadVideo(waitResult.videoUrl, videoApiKey);
523
+ await writeFile(videoPath, buffer);
524
+ // Extend video to match narration duration if needed
525
+ await extendVideoToTarget(videoPath, segment.duration, outputDir, `Scene ${i + 1}`, {
526
+ kling,
527
+ videoId: waitResult.videoId,
528
+ onProgress: (msg) => { videoSpinner.text = `🎬 ${msg}`; },
529
+ });
530
+ videoPaths[i] = videoPath;
531
+ completed = true;
532
+ console.log(chalk.green(`\n ✓ Scene ${i + 1} completed`));
533
+ }
534
+ else if (attempt < maxRetries) {
535
+ videoSpinner.text = `🎬 Scene ${i + 1}: Failed, retry ${attempt + 1}/${maxRetries}...`;
536
+ await sleep(RETRY_DELAY_MS);
537
+ }
538
+ else {
539
+ videoPaths[i] = "";
540
+ failedScenes.push(i + 1);
541
+ }
542
+ }
543
+ catch (err) {
544
+ if (attempt < maxRetries) {
545
+ videoSpinner.text = `🎬 Scene ${i + 1}: Error, retry ${attempt + 1}/${maxRetries}...`;
546
+ await sleep(RETRY_DELAY_MS);
547
+ }
548
+ else {
549
+ console.log(chalk.yellow(`\n ⚠ Error for scene ${i + 1}: ${err}`));
550
+ videoPaths[i] = "";
551
+ failedScenes.push(i + 1);
552
+ }
553
+ }
554
+ }
555
+ }
556
+ }
557
+ else {
558
+ // Parallel mode (default): batch-based submission respecting concurrency limit
559
+ const concurrency = Math.max(1, parseInt(options.concurrency) || 3);
560
+ for (let batchStart = 0; batchStart < segments.length; batchStart += concurrency) {
561
+ const batchEnd = Math.min(batchStart + concurrency, segments.length);
562
+ const batchNum = Math.floor(batchStart / concurrency) + 1;
563
+ const totalBatches = Math.ceil(segments.length / concurrency);
564
+ if (totalBatches > 1) {
565
+ videoSpinner.text = `🎬 Batch ${batchNum}/${totalBatches}: submitting scenes ${batchStart + 1}-${batchEnd}...`;
566
+ }
567
+ // Phase 1: Submit batch
568
+ const tasks = [];
569
+ for (let i = batchStart; i < batchEnd; i++) {
570
+ const segment = segments[i];
571
+ videoSpinner.text = `🎬 Submitting video task ${i + 1}/${segments.length}...`;
572
+ const videoDuration = (segment.duration > 5 ? 10 : 5);
573
+ const referenceImage = imageUrls[i];
574
+ const result = await generateVideoWithRetryKling(kling, segment, {
575
+ duration: videoDuration,
576
+ aspectRatio: options.aspectRatio,
577
+ referenceImage,
578
+ }, maxRetries, (msg) => {
579
+ videoSpinner.text = `🎬 Scene ${i + 1}: ${msg}`;
580
+ });
581
+ if (result) {
582
+ tasks.push({ taskId: result.taskId, index: i, segment, type: result.type });
583
+ if (!videoPaths[i])
584
+ videoPaths[i] = "";
585
+ }
586
+ else {
587
+ console.log(chalk.yellow(`\n ⚠ Failed to start video generation for scene ${i + 1} (after ${maxRetries} retries)`));
588
+ videoPaths[i] = "";
589
+ failedScenes.push(i + 1);
590
+ }
591
+ }
592
+ // Phase 2: Wait for batch completion
593
+ videoSpinner.text = `🎬 Waiting for batch ${batchNum} (${tasks.length} video${tasks.length > 1 ? "s" : ""})...`;
594
+ for (const task of tasks) {
595
+ let completed = false;
596
+ let currentTaskId = task.taskId;
597
+ let currentType = task.type;
598
+ for (let attempt = 0; attempt <= maxRetries && !completed; attempt++) {
599
+ try {
600
+ const result = await kling.waitForCompletion(currentTaskId, currentType, (status) => {
601
+ videoSpinner.text = `🎬 Scene ${task.index + 1}: ${status.status}...`;
602
+ }, 600000);
603
+ if (result.status === "completed" && result.videoUrl) {
604
+ const videoPath = resolve(outputDir, `scene-${task.index + 1}.mp4`);
605
+ const buffer = await downloadVideo(result.videoUrl, videoApiKey);
606
+ await writeFile(videoPath, buffer);
607
+ // Extend video to match narration duration if needed
608
+ await extendVideoToTarget(videoPath, task.segment.duration, outputDir, `Scene ${task.index + 1}`, {
609
+ kling,
610
+ videoId: result.videoId,
611
+ onProgress: (msg) => { videoSpinner.text = `🎬 ${msg}`; },
612
+ });
613
+ videoPaths[task.index] = videoPath;
614
+ completed = true;
615
+ }
616
+ else if (attempt < maxRetries) {
617
+ videoSpinner.text = `🎬 Scene ${task.index + 1}: Retry ${attempt + 1}/${maxRetries}...`;
618
+ await sleep(RETRY_DELAY_MS);
619
+ const videoDuration = (task.segment.duration > 5 ? 10 : 5);
620
+ const retryReferenceImage = imageUrls[task.index];
621
+ const retryResult = await generateVideoWithRetryKling(kling, task.segment, {
622
+ duration: videoDuration,
623
+ aspectRatio: options.aspectRatio,
624
+ referenceImage: retryReferenceImage,
625
+ }, 0);
626
+ if (retryResult) {
627
+ currentTaskId = retryResult.taskId;
628
+ currentType = retryResult.type;
629
+ }
630
+ else {
631
+ videoPaths[task.index] = "";
632
+ failedScenes.push(task.index + 1);
633
+ completed = true;
634
+ }
635
+ }
636
+ else {
637
+ videoPaths[task.index] = "";
638
+ failedScenes.push(task.index + 1);
639
+ }
640
+ }
641
+ catch (err) {
642
+ if (attempt >= maxRetries) {
643
+ console.log(chalk.yellow(`\n ⚠ Error completing video for scene ${task.index + 1}: ${err}`));
644
+ videoPaths[task.index] = "";
645
+ failedScenes.push(task.index + 1);
646
+ }
647
+ else {
648
+ videoSpinner.text = `🎬 Scene ${task.index + 1}: Error, retry ${attempt + 1}/${maxRetries}...`;
649
+ await sleep(RETRY_DELAY_MS);
650
+ }
651
+ }
652
+ }
653
+ }
654
+ if (totalBatches > 1 && batchEnd < segments.length) {
655
+ console.log(chalk.dim(` → Batch ${batchNum}/${totalBatches} complete`));
656
+ }
657
+ }
658
+ }
659
+ }
660
+ else {
661
+ // Runway
662
+ const runway = new RunwayProvider();
663
+ await runway.initialize({ apiKey: videoApiKey });
664
+ // Submit all video generation tasks with retry logic
665
+ const tasks = [];
666
+ for (let i = 0; i < segments.length; i++) {
667
+ if (!imagePaths[i]) {
668
+ videoPaths.push("");
669
+ continue;
670
+ }
671
+ const segment = segments[i];
672
+ videoSpinner.text = `🎬 Submitting video task ${i + 1}/${segments.length}...`;
673
+ const imageBuffer = await readFile(imagePaths[i]);
674
+ const ext = extname(imagePaths[i]).toLowerCase().slice(1);
675
+ const mimeType = ext === "jpg" || ext === "jpeg" ? "image/jpeg" : "image/png";
676
+ const referenceImage = `data:${mimeType};base64,${imageBuffer.toString("base64")}`;
677
+ // Use 10s video if narration > 5s to avoid video ending before narration
678
+ const videoDuration = (segment.duration > 5 ? 10 : 5);
679
+ const result = await generateVideoWithRetryRunway(runway, segment, referenceImage, {
680
+ duration: videoDuration,
681
+ aspectRatio: options.aspectRatio === "1:1" ? "16:9" : options.aspectRatio,
682
+ }, maxRetries, (msg) => {
683
+ videoSpinner.text = `🎬 Scene ${i + 1}: ${msg}`;
684
+ });
685
+ if (result) {
686
+ tasks.push({ taskId: result.taskId, index: i, imagePath: imagePaths[i], referenceImage, segment });
687
+ }
688
+ else {
689
+ console.log(chalk.yellow(`\n ⚠ Failed to start video generation for scene ${i + 1} (after ${maxRetries} retries)`));
690
+ videoPaths[i] = "";
691
+ failedScenes.push(i + 1);
692
+ }
693
+ }
694
+ // Wait for all tasks to complete with retry logic
695
+ videoSpinner.text = `🎬 Waiting for ${tasks.length} video(s) to complete...`;
696
+ for (const task of tasks) {
697
+ let completed = false;
698
+ let currentTaskId = task.taskId;
699
+ for (let attempt = 0; attempt <= maxRetries && !completed; attempt++) {
700
+ try {
701
+ const result = await runway.waitForCompletion(currentTaskId, (status) => {
702
+ const progress = status.progress !== undefined ? `${status.progress}%` : status.status;
703
+ videoSpinner.text = `🎬 Scene ${task.index + 1}: ${progress}...`;
704
+ }, 300000 // 5 minute timeout per video
705
+ );
706
+ if (result.status === "completed" && result.videoUrl) {
707
+ const videoPath = resolve(outputDir, `scene-${task.index + 1}.mp4`);
708
+ const buffer = await downloadVideo(result.videoUrl, videoApiKey);
709
+ await writeFile(videoPath, buffer);
710
+ // Extend video to match narration duration if needed
711
+ await extendVideoToTarget(videoPath, task.segment.duration, outputDir, `Scene ${task.index + 1}`, {
712
+ onProgress: (msg) => { videoSpinner.text = `🎬 ${msg}`; },
713
+ });
714
+ videoPaths[task.index] = videoPath;
715
+ completed = true;
716
+ }
717
+ else if (attempt < maxRetries) {
718
+ // Resubmit task on failure
719
+ videoSpinner.text = `🎬 Scene ${task.index + 1}: Retry ${attempt + 1}/${maxRetries}...`;
720
+ await sleep(RETRY_DELAY_MS);
721
+ const videoDuration = (task.segment.duration > 5 ? 10 : 5);
722
+ const retryResult = await generateVideoWithRetryRunway(runway, task.segment, task.referenceImage, {
723
+ duration: videoDuration,
724
+ aspectRatio: options.aspectRatio === "1:1" ? "16:9" : options.aspectRatio,
725
+ }, 0, // No nested retries
726
+ (msg) => {
727
+ videoSpinner.text = `🎬 Scene ${task.index + 1}: ${msg}`;
728
+ });
729
+ if (retryResult) {
730
+ currentTaskId = retryResult.taskId;
731
+ }
732
+ else {
733
+ videoPaths[task.index] = "";
734
+ failedScenes.push(task.index + 1);
735
+ completed = true; // Exit retry loop
736
+ }
737
+ }
738
+ else {
739
+ videoPaths[task.index] = "";
740
+ failedScenes.push(task.index + 1);
741
+ }
742
+ }
743
+ catch (err) {
744
+ if (attempt >= maxRetries) {
745
+ console.log(chalk.yellow(`\n ⚠ Error completing video for scene ${task.index + 1}: ${err}`));
746
+ videoPaths[task.index] = "";
747
+ failedScenes.push(task.index + 1);
748
+ }
749
+ else {
750
+ videoSpinner.text = `🎬 Scene ${task.index + 1}: Error, retry ${attempt + 1}/${maxRetries}...`;
751
+ await sleep(RETRY_DELAY_MS);
752
+ }
753
+ }
754
+ }
755
+ }
756
+ }
757
+ const successfulVideos = videoPaths.filter((p) => p && p !== "").length;
758
+ videoSpinner.succeed(chalk.green(`Generated ${successfulVideos}/${segments.length} videos`));
759
+ console.log();
760
+ }
761
+ // Step 4.5: Apply text overlays (if segments have textOverlays)
762
+ if (options.textOverlay !== false) {
763
+ const overlaySegments = segments.filter((s, i) => s.textOverlays && s.textOverlays.length > 0 && videoPaths[i] && videoPaths[i] !== "");
764
+ if (overlaySegments.length > 0) {
765
+ const overlaySpinner = ora(`Applying text overlays to ${overlaySegments.length} scene(s)...`).start();
766
+ let overlayCount = 0;
767
+ for (let i = 0; i < segments.length; i++) {
768
+ const segment = segments[i];
769
+ if (segment.textOverlays && segment.textOverlays.length > 0 && videoPaths[i] && videoPaths[i] !== "") {
770
+ try {
771
+ const overlayOutput = videoPaths[i].replace(/(\.[^.]+)$/, "-overlay$1");
772
+ const overlayResult = await applyTextOverlays({
773
+ videoPath: videoPaths[i],
774
+ texts: segment.textOverlays,
775
+ outputPath: overlayOutput,
776
+ style: options.textStyle || "lower-third",
777
+ });
778
+ if (overlayResult.success && overlayResult.outputPath) {
779
+ videoPaths[i] = overlayResult.outputPath;
780
+ overlayCount++;
781
+ }
782
+ }
783
+ catch {
784
+ // Silent fallback: keep original video
785
+ }
786
+ }
787
+ }
788
+ overlaySpinner.succeed(chalk.green(`Applied text overlays to ${overlayCount} scene(s)`));
789
+ console.log();
790
+ }
791
+ }
792
+ // Step 5: Assemble project
793
+ const assembleSpinner = ora("Assembling project...").start();
794
+ const project = new Project("Script-to-Video Output");
795
+ project.setAspectRatio(options.aspectRatio);
796
+ // Clear default tracks and create new ones
797
+ const defaultTracks = project.getTracks();
798
+ for (const track of defaultTracks) {
799
+ project.removeTrack(track.id);
800
+ }
801
+ const videoTrack = project.addTrack({
802
+ name: "Video",
803
+ type: "video",
804
+ order: 1,
805
+ isMuted: false,
806
+ isLocked: false,
807
+ isVisible: true,
808
+ });
809
+ const audioTrack = project.addTrack({
810
+ name: "Audio",
811
+ type: "audio",
812
+ order: 0,
813
+ isMuted: false,
814
+ isLocked: false,
815
+ isVisible: true,
816
+ });
817
+ // Add per-scene narration sources and clips
818
+ for (const tts of perSceneTTS) {
819
+ const segment = segments[tts.segmentIndex];
820
+ const audioSource = project.addSource({
821
+ name: `Narration ${tts.segmentIndex + 1}`,
822
+ url: tts.path,
823
+ type: "audio",
824
+ duration: tts.duration,
825
+ });
826
+ project.addClip({
827
+ sourceId: audioSource.id,
828
+ trackId: audioTrack.id,
829
+ startTime: segment.startTime,
830
+ duration: tts.duration,
831
+ sourceStartOffset: 0,
832
+ sourceEndOffset: tts.duration,
833
+ });
834
+ }
835
+ // Add video/image sources and clips
836
+ let currentTime = 0;
837
+ const videoClipIds = [];
838
+ const fadeDuration = 0.3; // Fade duration in seconds for smooth transitions
839
+ for (let i = 0; i < segments.length; i++) {
840
+ const segment = segments[i];
841
+ const hasVideo = videoPaths[i] && videoPaths[i] !== "";
842
+ const hasImage = imagePaths[i] && imagePaths[i] !== "";
843
+ if (!hasVideo && !hasImage) {
844
+ // Skip if no visual asset
845
+ currentTime += segment.duration;
846
+ continue;
847
+ }
848
+ const assetPath = hasVideo ? videoPaths[i] : imagePaths[i];
849
+ const mediaType = hasVideo ? "video" : "image";
850
+ const source = project.addSource({
851
+ name: `Scene ${i + 1}`,
852
+ url: assetPath,
853
+ type: mediaType,
854
+ duration: segment.duration,
855
+ });
856
+ const clip = project.addClip({
857
+ sourceId: source.id,
858
+ trackId: videoTrack.id,
859
+ startTime: currentTime,
860
+ duration: segment.duration,
861
+ sourceStartOffset: 0,
862
+ sourceEndOffset: segment.duration,
863
+ });
864
+ videoClipIds.push(clip.id);
865
+ currentTime += segment.duration;
866
+ }
867
+ // Add fade effects to video clips for smoother scene transitions
868
+ for (let i = 0; i < videoClipIds.length; i++) {
869
+ const clipId = videoClipIds[i];
870
+ const clip = project.getClips().find(c => c.id === clipId);
871
+ if (!clip)
872
+ continue;
873
+ // Add fadeIn effect (except for first clip)
874
+ if (i > 0) {
875
+ project.addEffect(clipId, {
876
+ type: "fadeIn",
877
+ startTime: 0,
878
+ duration: fadeDuration,
879
+ params: {},
880
+ });
881
+ }
882
+ // Add fadeOut effect (except for last clip)
883
+ if (i < videoClipIds.length - 1) {
884
+ project.addEffect(clipId, {
885
+ type: "fadeOut",
886
+ startTime: clip.duration - fadeDuration,
887
+ duration: fadeDuration,
888
+ params: {},
889
+ });
890
+ }
891
+ }
892
+ // Save project file
893
+ let outputPath = resolve(process.cwd(), options.output);
894
+ // Detect if output looks like a directory (ends with / or no .json extension)
895
+ const looksLikeDirectory = options.output.endsWith("/") ||
896
+ (!options.output.endsWith(".json") &&
897
+ !options.output.endsWith(".vibe.json"));
898
+ if (looksLikeDirectory) {
899
+ // Create directory if it doesn't exist
900
+ if (!existsSync(outputPath)) {
901
+ await mkdir(outputPath, { recursive: true });
902
+ }
903
+ outputPath = resolve(outputPath, "project.vibe.json");
904
+ }
905
+ else if (existsSync(outputPath) &&
906
+ (await stat(outputPath)).isDirectory()) {
907
+ // Existing directory without trailing slash
908
+ outputPath = resolve(outputPath, "project.vibe.json");
909
+ }
910
+ else {
911
+ // File path - ensure parent directory exists
912
+ const parentDir = dirname(outputPath);
913
+ if (!existsSync(parentDir)) {
914
+ await mkdir(parentDir, { recursive: true });
915
+ }
916
+ }
917
+ await writeFile(outputPath, JSON.stringify(project.toJSON(), null, 2), "utf-8");
918
+ assembleSpinner.succeed(chalk.green("Project assembled"));
919
+ // Step 6: AI Review (optional)
920
+ if (options.review) {
921
+ const reviewSpinner = ora("Reviewing video with Gemini AI...").start();
922
+ try {
923
+ const reviewTarget = videoPaths.find((p) => p && p !== "");
924
+ if (reviewTarget) {
925
+ const storyboardFile = resolve(effectiveOutputDir, "storyboard.json");
926
+ const reviewResult = await executeReview({
927
+ videoPath: reviewTarget,
928
+ storyboardPath: existsSync(storyboardFile) ? storyboardFile : undefined,
929
+ autoApply: options.reviewAutoApply,
930
+ model: "flash",
931
+ });
932
+ if (reviewResult.success && reviewResult.feedback) {
933
+ reviewSpinner.succeed(chalk.green(`AI Review: ${reviewResult.feedback.overallScore}/10`));
934
+ if (reviewResult.appliedFixes && reviewResult.appliedFixes.length > 0) {
935
+ for (const fix of reviewResult.appliedFixes) {
936
+ console.log(chalk.green(` + ${fix}`));
937
+ }
938
+ }
939
+ if (reviewResult.feedback.recommendations.length > 0) {
940
+ for (const rec of reviewResult.feedback.recommendations) {
941
+ console.log(chalk.dim(` * ${rec}`));
942
+ }
943
+ }
944
+ }
945
+ else {
946
+ reviewSpinner.warn(chalk.yellow("AI review completed but no actionable feedback"));
947
+ }
948
+ }
949
+ else {
950
+ reviewSpinner.warn(chalk.yellow("No videos available for review"));
951
+ }
952
+ }
953
+ catch {
954
+ reviewSpinner.warn(chalk.yellow("AI review skipped (non-critical error)"));
955
+ }
956
+ console.log();
957
+ }
958
+ // Final summary
959
+ console.log();
960
+ console.log(chalk.bold.green("Script-to-Video complete!"));
961
+ console.log(chalk.dim("─".repeat(60)));
962
+ console.log();
963
+ console.log(` 📄 Project: ${chalk.cyan(outputPath)}`);
964
+ console.log(` 🎬 Scenes: ${segments.length}`);
965
+ console.log(` ⏱️ Duration: ${totalDuration}s`);
966
+ console.log(` 📁 Assets: ${effectiveOutputDir}/`);
967
+ if (perSceneTTS.length > 0 || failedNarrations.length > 0) {
968
+ const narrationInfo = `${perSceneTTS.length}/${segments.length}`;
969
+ if (failedNarrations.length > 0) {
970
+ const failedSceneNums = failedNarrations.map((f) => f.sceneNum).join(", ");
971
+ console.log(` 🎙️ Narrations: ${narrationInfo} narration-*.mp3`);
972
+ console.log(chalk.yellow(` ⚠ Failed: scene ${failedSceneNums}`));
973
+ }
974
+ else {
975
+ console.log(` 🎙️ Narrations: ${perSceneTTS.length} narration-*.mp3`);
976
+ }
977
+ }
978
+ console.log(` 🖼️ Images: ${successfulImages} scene-*.png`);
979
+ if (!options.imagesOnly) {
980
+ const videoCount = videoPaths.filter((p) => p && p !== "").length;
981
+ console.log(` 🎥 Videos: ${videoCount}/${segments.length} scene-*.mp4`);
982
+ if (failedScenes.length > 0) {
983
+ const uniqueFailedScenes = [...new Set(failedScenes)].sort((a, b) => a - b);
984
+ console.log(chalk.yellow(` ⚠ Failed: scene ${uniqueFailedScenes.join(", ")} (fallback to image)`));
985
+ }
986
+ }
987
+ console.log();
988
+ console.log(chalk.dim("Next steps:"));
989
+ console.log(chalk.dim(` vibe project info ${options.output}`));
990
+ console.log(chalk.dim(` vibe export ${options.output} -o final.mp4`));
991
+ // Show regeneration hint if there were failures
992
+ if (!options.imagesOnly && failedScenes.length > 0) {
993
+ const uniqueFailedScenes = [...new Set(failedScenes)].sort((a, b) => a - b);
994
+ console.log();
995
+ console.log(chalk.dim("💡 To regenerate failed scenes:"));
996
+ for (const sceneNum of uniqueFailedScenes) {
997
+ console.log(chalk.dim(` vibe ai regenerate-scene ${effectiveOutputDir}/ --scene ${sceneNum} --video-only`));
998
+ }
999
+ }
1000
+ console.log();
1001
+ }
1002
+ catch (error) {
1003
+ console.error(chalk.red("Script-to-Video failed"));
1004
+ console.error(error);
1005
+ process.exit(1);
1006
+ }
1007
+ });
1008
+ // Regenerate Scene command
1009
+ aiCommand
1010
+ .command("regenerate-scene")
1011
+ .description("Regenerate a specific scene in a script-to-video project")
1012
+ .argument("<project-dir>", "Path to the script-to-video output directory")
1013
+ .requiredOption("--scene <numbers>", "Scene number(s) to regenerate (1-based), e.g., 3 or 3,4,5")
1014
+ .option("--video-only", "Only regenerate video")
1015
+ .option("--narration-only", "Only regenerate narration")
1016
+ .option("--image-only", "Only regenerate image")
1017
+ .option("-g, --generator <engine>", "Video generator: kling | runway | veo", "kling")
1018
+ .option("-i, --image-provider <provider>", "Image provider: gemini | openai | grok", "gemini")
1019
+ .option("-v, --voice <id>", "ElevenLabs voice ID for narration")
1020
+ .option("-a, --aspect-ratio <ratio>", "Aspect ratio: 16:9 | 9:16 | 1:1", "16:9")
1021
+ .option("--retries <count>", "Number of retries for video generation failures", String(DEFAULT_VIDEO_RETRIES))
1022
+ .option("--reference-scene <num>", "Use another scene's image as reference for character consistency")
1023
+ .action(async (projectDir, options) => {
1024
+ try {
1025
+ const outputDir = resolve(process.cwd(), projectDir);
1026
+ const storyboardPath = resolve(outputDir, "storyboard.json");
1027
+ const projectPath = resolve(outputDir, "project.vibe.json");
1028
+ // Validate project directory
1029
+ if (!existsSync(outputDir)) {
1030
+ console.error(chalk.red(`Project directory not found: ${outputDir}`));
1031
+ process.exit(1);
1032
+ }
1033
+ if (!existsSync(storyboardPath)) {
1034
+ console.error(chalk.red(`Storyboard not found: ${storyboardPath}`));
1035
+ console.error(chalk.dim("This command requires a storyboard.json file from script-to-video output"));
1036
+ process.exit(1);
1037
+ }
1038
+ // Parse scene number(s) - supports "3" or "3,4,5"
1039
+ const sceneNums = options.scene.split(",").map((s) => parseInt(s.trim())).filter((n) => !isNaN(n) && n >= 1);
1040
+ if (sceneNums.length === 0) {
1041
+ console.error(chalk.red("Scene number must be a positive integer (1-based), e.g., --scene 3 or --scene 3,4,5"));
1042
+ process.exit(1);
1043
+ }
1044
+ // Load storyboard
1045
+ const storyboardContent = await readFile(storyboardPath, "utf-8");
1046
+ const segments = JSON.parse(storyboardContent);
1047
+ // Validate all scene numbers
1048
+ for (const sceneNum of sceneNums) {
1049
+ if (sceneNum > segments.length) {
1050
+ console.error(chalk.red(`Scene ${sceneNum} does not exist. Storyboard has ${segments.length} scenes.`));
1051
+ process.exit(1);
1052
+ }
1053
+ }
1054
+ // Determine what to regenerate
1055
+ const regenerateVideo = options.videoOnly || (!options.narrationOnly && !options.imageOnly);
1056
+ const regenerateNarration = options.narrationOnly || (!options.videoOnly && !options.imageOnly);
1057
+ const regenerateImage = options.imageOnly || (!options.videoOnly && !options.narrationOnly);
1058
+ console.log();
1059
+ console.log(chalk.bold.cyan(`🔄 Regenerating Scene${sceneNums.length > 1 ? "s" : ""} ${sceneNums.join(", ")}`));
1060
+ console.log(chalk.dim("─".repeat(60)));
1061
+ console.log();
1062
+ console.log(` 📁 Project: ${outputDir}`);
1063
+ console.log(` 🎬 Scenes: ${sceneNums.join(", ")} of ${segments.length}`);
1064
+ console.log();
1065
+ // Get required API keys (once, before processing scenes)
1066
+ let imageApiKey;
1067
+ let videoApiKey;
1068
+ let elevenlabsApiKey;
1069
+ if (regenerateImage) {
1070
+ const imageProvider = options.imageProvider || "openai";
1071
+ if (imageProvider === "openai" || imageProvider === "dalle") {
1072
+ imageApiKey = (await getApiKey("OPENAI_API_KEY", "OpenAI")) ?? undefined;
1073
+ if (!imageApiKey) {
1074
+ console.error(chalk.red("OpenAI API key required for image generation. Set OPENAI_API_KEY in .env or run: vibe setup"));
1075
+ process.exit(1);
1076
+ }
1077
+ }
1078
+ else if (imageProvider === "gemini") {
1079
+ imageApiKey = (await getApiKey("GOOGLE_API_KEY", "Google")) ?? undefined;
1080
+ if (!imageApiKey) {
1081
+ console.error(chalk.red("Google API key required for Gemini image generation. Set GOOGLE_API_KEY in .env or run: vibe setup"));
1082
+ process.exit(1);
1083
+ }
1084
+ }
1085
+ else if (imageProvider === "grok") {
1086
+ imageApiKey = (await getApiKey("XAI_API_KEY", "xAI")) ?? undefined;
1087
+ if (!imageApiKey) {
1088
+ console.error(chalk.red("xAI API key required for Grok image generation. Set XAI_API_KEY in .env or run: vibe setup"));
1089
+ process.exit(1);
1090
+ }
1091
+ }
1092
+ }
1093
+ if (regenerateVideo) {
1094
+ if (options.generator === "kling") {
1095
+ const key = await getApiKey("KLING_API_KEY", "Kling");
1096
+ if (!key) {
1097
+ console.error(chalk.red("Kling API key required. Set KLING_API_KEY in .env or run: vibe setup"));
1098
+ process.exit(1);
1099
+ }
1100
+ videoApiKey = key;
1101
+ }
1102
+ else {
1103
+ const key = await getApiKey("RUNWAY_API_SECRET", "Runway");
1104
+ if (!key) {
1105
+ console.error(chalk.red("Runway API key required. Set RUNWAY_API_SECRET in .env or run: vibe setup"));
1106
+ process.exit(1);
1107
+ }
1108
+ videoApiKey = key;
1109
+ }
1110
+ }
1111
+ if (regenerateNarration) {
1112
+ const key = await getApiKey("ELEVENLABS_API_KEY", "ElevenLabs");
1113
+ if (!key) {
1114
+ console.error(chalk.red("ElevenLabs API key required for narration. Set ELEVENLABS_API_KEY in .env or run: vibe setup"));
1115
+ process.exit(1);
1116
+ }
1117
+ elevenlabsApiKey = key;
1118
+ }
1119
+ // Process each scene
1120
+ for (const sceneNum of sceneNums) {
1121
+ const segment = segments[sceneNum - 1];
1122
+ console.log(chalk.cyan(`\n── Scene ${sceneNum} ──`));
1123
+ console.log(chalk.dim(` ${segment.description.slice(0, 50)}...`));
1124
+ // Step 1: Regenerate narration if needed
1125
+ const narrationPath = resolve(outputDir, `narration-${sceneNum}.mp3`);
1126
+ let narrationDuration = segment.duration;
1127
+ if (regenerateNarration && elevenlabsApiKey) {
1128
+ const ttsSpinner = ora(`🎙️ Regenerating narration for scene ${sceneNum}...`).start();
1129
+ const elevenlabs = new ElevenLabsProvider();
1130
+ await elevenlabs.initialize({ apiKey: elevenlabsApiKey });
1131
+ const narrationText = segment.narration || segment.description;
1132
+ const ttsResult = await elevenlabs.textToSpeech(narrationText, {
1133
+ voiceId: options.voice,
1134
+ });
1135
+ if (!ttsResult.success || !ttsResult.audioBuffer) {
1136
+ ttsSpinner.fail(chalk.red(`Failed to generate narration: ${ttsResult.error || "Unknown error"}`));
1137
+ process.exit(1);
1138
+ }
1139
+ await writeFile(narrationPath, ttsResult.audioBuffer);
1140
+ narrationDuration = await getAudioDuration(narrationPath);
1141
+ // Update segment duration in storyboard
1142
+ segment.duration = narrationDuration;
1143
+ ttsSpinner.succeed(chalk.green(`Generated narration (${narrationDuration.toFixed(1)}s)`));
1144
+ }
1145
+ // Step 2: Regenerate image if needed
1146
+ const imagePath = resolve(outputDir, `scene-${sceneNum}.png`);
1147
+ if (regenerateImage && imageApiKey) {
1148
+ const imageSpinner = ora(`🎨 Regenerating image for scene ${sceneNum}...`).start();
1149
+ const imageProvider = options.imageProvider || "gemini";
1150
+ // Build prompt with character description for consistency
1151
+ const characterDesc = segment.characterDescription || segments[0]?.characterDescription;
1152
+ let imagePrompt = segment.visualStyle
1153
+ ? `${segment.visuals}. Style: ${segment.visualStyle}`
1154
+ : segment.visuals;
1155
+ // Add character description to prompt if available
1156
+ if (characterDesc) {
1157
+ imagePrompt = `${imagePrompt}\n\nIMPORTANT - Character appearance must match exactly: ${characterDesc}`;
1158
+ }
1159
+ // Check if we should use reference-based generation for character consistency
1160
+ const refSceneNum = options.referenceScene ? parseInt(options.referenceScene) : null;
1161
+ let referenceImageBuffer;
1162
+ if (refSceneNum && refSceneNum >= 1 && refSceneNum <= segments.length && refSceneNum !== sceneNum) {
1163
+ const refImagePath = resolve(outputDir, `scene-${refSceneNum}.png`);
1164
+ if (existsSync(refImagePath)) {
1165
+ referenceImageBuffer = await readFile(refImagePath);
1166
+ imageSpinner.text = `🎨 Regenerating image for scene ${sceneNum} (using scene ${refSceneNum} as reference)...`;
1167
+ }
1168
+ }
1169
+ else if (!refSceneNum) {
1170
+ // Auto-detect: use the first available scene image as reference
1171
+ for (let i = 1; i <= segments.length; i++) {
1172
+ if (i !== sceneNum) {
1173
+ const otherImagePath = resolve(outputDir, `scene-${i}.png`);
1174
+ if (existsSync(otherImagePath)) {
1175
+ referenceImageBuffer = await readFile(otherImagePath);
1176
+ imageSpinner.text = `🎨 Regenerating image for scene ${sceneNum} (using scene ${i} as reference)...`;
1177
+ break;
1178
+ }
1179
+ }
1180
+ }
1181
+ }
1182
+ // Determine image size/aspect ratio based on provider
1183
+ const dalleImageSizes = {
1184
+ "16:9": "1536x1024",
1185
+ "9:16": "1024x1536",
1186
+ "1:1": "1024x1024",
1187
+ };
1188
+ let imageBuffer;
1189
+ let imageUrl;
1190
+ let imageError;
1191
+ if (imageProvider === "openai" || imageProvider === "dalle") {
1192
+ const openaiImage = new OpenAIImageProvider();
1193
+ await openaiImage.initialize({ apiKey: imageApiKey });
1194
+ const imageResult = await openaiImage.generateImage(imagePrompt, {
1195
+ size: dalleImageSizes[options.aspectRatio] || "1536x1024",
1196
+ quality: "standard",
1197
+ });
1198
+ if (imageResult.success && imageResult.images && imageResult.images.length > 0) {
1199
+ imageUrl = imageResult.images[0].url;
1200
+ }
1201
+ else {
1202
+ imageError = imageResult.error;
1203
+ }
1204
+ }
1205
+ else if (imageProvider === "gemini") {
1206
+ const gemini = new GeminiProvider();
1207
+ await gemini.initialize({ apiKey: imageApiKey });
1208
+ // Use editImage with reference for character consistency
1209
+ if (referenceImageBuffer) {
1210
+ // Extract the main action from the scene description (take first action if multiple)
1211
+ const simplifiedVisuals = segment.visuals.split(/[,.]/).find((part) => part.includes("standing") || part.includes("sitting") || part.includes("walking") ||
1212
+ part.includes("lying") || part.includes("reaching") || part.includes("looking") ||
1213
+ part.includes("working") || part.includes("coding") || part.includes("typing")) || segment.visuals.split(".")[0];
1214
+ const editPrompt = `Generate a new image showing the SAME SINGLE person from the reference image in a new scene.
1215
+
1216
+ REFERENCE: Look at the person in the reference image - their face, hair, build, and overall appearance.
1217
+
1218
+ NEW SCENE: ${simplifiedVisuals}
1219
+
1220
+ CRITICAL RULES:
1221
+ 1. Show ONLY ONE person - the exact same individual from the reference image
1222
+ 2. The person must have the IDENTICAL face, hair style, and body type
1223
+ 3. Do NOT show multiple people or duplicate the character
1224
+ 4. Create a single moment in time, one pose, one action
1225
+ 5. Match the art style and quality of the reference image
1226
+
1227
+ Generate the single-person scene image now.`;
1228
+ const imageResult = await gemini.editImage([referenceImageBuffer], editPrompt, {
1229
+ aspectRatio: options.aspectRatio,
1230
+ });
1231
+ if (imageResult.success && imageResult.images && imageResult.images.length > 0) {
1232
+ const img = imageResult.images[0];
1233
+ if (img.base64) {
1234
+ imageBuffer = Buffer.from(img.base64, "base64");
1235
+ }
1236
+ }
1237
+ else {
1238
+ imageError = imageResult.error;
1239
+ }
1240
+ }
1241
+ else {
1242
+ // No reference image, use regular generation
1243
+ const imageResult = await gemini.generateImage(imagePrompt, {
1244
+ aspectRatio: options.aspectRatio,
1245
+ });
1246
+ if (imageResult.success && imageResult.images && imageResult.images.length > 0) {
1247
+ const img = imageResult.images[0];
1248
+ if (img.base64) {
1249
+ imageBuffer = Buffer.from(img.base64, "base64");
1250
+ }
1251
+ }
1252
+ else {
1253
+ imageError = imageResult.error;
1254
+ }
1255
+ }
1256
+ }
1257
+ else if (imageProvider === "grok") {
1258
+ const { GrokProvider } = await import("@vibeframe/ai-providers");
1259
+ const grok = new GrokProvider();
1260
+ await grok.initialize({ apiKey: imageApiKey });
1261
+ const imageResult = await grok.generateImage(imagePrompt, {
1262
+ aspectRatio: options.aspectRatio || "16:9",
1263
+ });
1264
+ if (imageResult.success && imageResult.images && imageResult.images.length > 0) {
1265
+ const img = imageResult.images[0];
1266
+ if (img.base64) {
1267
+ imageBuffer = Buffer.from(img.base64, "base64");
1268
+ }
1269
+ else if (img.url) {
1270
+ imageUrl = img.url;
1271
+ }
1272
+ }
1273
+ else {
1274
+ imageError = imageResult.error;
1275
+ }
1276
+ }
1277
+ if (imageBuffer) {
1278
+ await writeFile(imagePath, imageBuffer);
1279
+ imageSpinner.succeed(chalk.green("Generated image"));
1280
+ }
1281
+ else if (imageUrl) {
1282
+ const response = await fetch(imageUrl);
1283
+ const buffer = Buffer.from(await response.arrayBuffer());
1284
+ await writeFile(imagePath, buffer);
1285
+ imageSpinner.succeed(chalk.green("Generated image"));
1286
+ }
1287
+ else {
1288
+ const errorMsg = imageError || "Unknown error";
1289
+ imageSpinner.fail(chalk.red(`Failed to generate image: ${errorMsg}`));
1290
+ process.exit(1);
1291
+ }
1292
+ }
1293
+ // Step 3: Regenerate video if needed
1294
+ const videoPath = resolve(outputDir, `scene-${sceneNum}.mp4`);
1295
+ if (regenerateVideo && videoApiKey) {
1296
+ const videoSpinner = ora(`🎬 Regenerating video for scene ${sceneNum}...`).start();
1297
+ // Check if image exists
1298
+ if (!existsSync(imagePath)) {
1299
+ videoSpinner.fail(chalk.red(`Reference image not found: ${imagePath}`));
1300
+ console.error(chalk.dim("Generate an image first with --image-only or regenerate all assets"));
1301
+ process.exit(1);
1302
+ }
1303
+ const imageBuffer = await readFile(imagePath);
1304
+ const ext = extname(imagePath).toLowerCase().slice(1);
1305
+ const mimeType = ext === "jpg" || ext === "jpeg" ? "image/jpeg" : "image/png";
1306
+ const referenceImage = `data:${mimeType};base64,${imageBuffer.toString("base64")}`;
1307
+ const videoDuration = (segment.duration > 5 ? 10 : 5);
1308
+ const maxRetries = parseInt(options.retries) || DEFAULT_VIDEO_RETRIES;
1309
+ let videoGenerated = false;
1310
+ if (options.generator === "kling") {
1311
+ const kling = new KlingProvider();
1312
+ await kling.initialize({ apiKey: videoApiKey });
1313
+ if (!kling.isConfigured()) {
1314
+ videoSpinner.fail(chalk.red("Invalid Kling API key format. Use ACCESS_KEY:SECRET_KEY"));
1315
+ process.exit(1);
1316
+ }
1317
+ // Try to use image-to-video if ImgBB API key is available
1318
+ const imgbbApiKey = await getApiKeyFromConfig("imgbb") || process.env.IMGBB_API_KEY;
1319
+ let imageUrl;
1320
+ if (imgbbApiKey) {
1321
+ videoSpinner.text = `🎬 Uploading image to ImgBB...`;
1322
+ const uploadResult = await uploadToImgbb(imageBuffer, imgbbApiKey);
1323
+ if (uploadResult.success && uploadResult.url) {
1324
+ imageUrl = uploadResult.url;
1325
+ videoSpinner.text = `🎬 Starting image-to-video generation...`;
1326
+ }
1327
+ else {
1328
+ console.log(chalk.yellow(`\n ⚠ ImgBB upload failed, falling back to text-to-video`));
1329
+ }
1330
+ }
1331
+ const result = await generateVideoWithRetryKling(kling, segment, {
1332
+ duration: videoDuration,
1333
+ aspectRatio: options.aspectRatio,
1334
+ referenceImage: imageUrl, // Use uploaded URL for image-to-video
1335
+ }, maxRetries);
1336
+ if (result) {
1337
+ videoSpinner.text = `🎬 Waiting for video to complete...`;
1338
+ for (let attempt = 0; attempt <= maxRetries && !videoGenerated; attempt++) {
1339
+ try {
1340
+ const waitResult = await kling.waitForCompletion(result.taskId, result.type, (status) => {
1341
+ videoSpinner.text = `🎬 Scene ${sceneNum}: ${status.status}...`;
1342
+ }, 600000);
1343
+ if (waitResult.status === "completed" && waitResult.videoUrl) {
1344
+ const buffer = await downloadVideo(waitResult.videoUrl, videoApiKey);
1345
+ await writeFile(videoPath, buffer);
1346
+ // Extend video to match narration duration if needed
1347
+ await extendVideoToTarget(videoPath, segment.duration, outputDir, `Scene ${sceneNum}`, {
1348
+ kling,
1349
+ videoId: waitResult.videoId,
1350
+ onProgress: (msg) => { videoSpinner.text = `🎬 ${msg}`; },
1351
+ });
1352
+ videoGenerated = true;
1353
+ }
1354
+ else if (attempt < maxRetries) {
1355
+ videoSpinner.text = `🎬 Scene ${sceneNum}: Retry ${attempt + 1}/${maxRetries}...`;
1356
+ await sleep(RETRY_DELAY_MS);
1357
+ }
1358
+ }
1359
+ catch (err) {
1360
+ if (attempt >= maxRetries) {
1361
+ throw err;
1362
+ }
1363
+ videoSpinner.text = `🎬 Scene ${sceneNum}: Error, retry ${attempt + 1}/${maxRetries}...`;
1364
+ await sleep(RETRY_DELAY_MS);
1365
+ }
1366
+ }
1367
+ }
1368
+ }
1369
+ else {
1370
+ // Runway
1371
+ const runway = new RunwayProvider();
1372
+ await runway.initialize({ apiKey: videoApiKey });
1373
+ const result = await generateVideoWithRetryRunway(runway, segment, referenceImage, {
1374
+ duration: videoDuration,
1375
+ aspectRatio: options.aspectRatio === "1:1" ? "16:9" : options.aspectRatio,
1376
+ }, maxRetries, (msg) => {
1377
+ videoSpinner.text = `🎬 Scene ${sceneNum}: ${msg}`;
1378
+ });
1379
+ if (result) {
1380
+ videoSpinner.text = `🎬 Waiting for video to complete...`;
1381
+ for (let attempt = 0; attempt <= maxRetries && !videoGenerated; attempt++) {
1382
+ try {
1383
+ const waitResult = await runway.waitForCompletion(result.taskId, (status) => {
1384
+ const progress = status.progress !== undefined ? `${status.progress}%` : status.status;
1385
+ videoSpinner.text = `🎬 Scene ${sceneNum}: ${progress}...`;
1386
+ }, 300000);
1387
+ if (waitResult.status === "completed" && waitResult.videoUrl) {
1388
+ const buffer = await downloadVideo(waitResult.videoUrl, videoApiKey);
1389
+ await writeFile(videoPath, buffer);
1390
+ // Extend video to match narration duration if needed (Runway - no Kling extend)
1391
+ await extendVideoToTarget(videoPath, segment.duration, outputDir, `Scene ${sceneNum}`, {
1392
+ onProgress: (msg) => { videoSpinner.text = `🎬 ${msg}`; },
1393
+ });
1394
+ videoGenerated = true;
1395
+ }
1396
+ else if (attempt < maxRetries) {
1397
+ videoSpinner.text = `🎬 Scene ${sceneNum}: Retry ${attempt + 1}/${maxRetries}...`;
1398
+ await sleep(RETRY_DELAY_MS);
1399
+ }
1400
+ }
1401
+ catch (err) {
1402
+ if (attempt >= maxRetries) {
1403
+ throw err;
1404
+ }
1405
+ videoSpinner.text = `🎬 Scene ${sceneNum}: Error, retry ${attempt + 1}/${maxRetries}...`;
1406
+ await sleep(RETRY_DELAY_MS);
1407
+ }
1408
+ }
1409
+ }
1410
+ }
1411
+ if (videoGenerated) {
1412
+ videoSpinner.succeed(chalk.green("Generated video"));
1413
+ }
1414
+ else {
1415
+ videoSpinner.fail(chalk.red("Failed to generate video after all retries"));
1416
+ process.exit(1);
1417
+ }
1418
+ }
1419
+ // Step 4: Recalculate startTime for ALL segments and re-save storyboard
1420
+ {
1421
+ let currentTime = 0;
1422
+ for (const seg of segments) {
1423
+ seg.startTime = currentTime;
1424
+ currentTime += seg.duration;
1425
+ }
1426
+ await writeFile(storyboardPath, JSON.stringify(segments, null, 2), "utf-8");
1427
+ console.log(chalk.dim(` → Updated storyboard: ${storyboardPath}`));
1428
+ }
1429
+ // Step 5: Update project.vibe.json if it exists — update ALL clips' startTime/duration
1430
+ if (existsSync(projectPath)) {
1431
+ const updateSpinner = ora("📦 Updating project file...").start();
1432
+ try {
1433
+ const projectContent = await readFile(projectPath, "utf-8");
1434
+ const projectData = JSON.parse(projectContent);
1435
+ // Find and update the source for this scene
1436
+ const sceneName = `Scene ${sceneNum}`;
1437
+ const narrationName = `Narration ${sceneNum}`;
1438
+ // Update video/image source
1439
+ const videoSource = projectData.state.sources.find((s) => s.name === sceneName);
1440
+ if (videoSource) {
1441
+ const hasVideo = existsSync(videoPath);
1442
+ videoSource.url = hasVideo ? videoPath : imagePath;
1443
+ videoSource.type = hasVideo ? "video" : "image";
1444
+ videoSource.duration = segment.duration;
1445
+ }
1446
+ // Update narration source
1447
+ const narrationSource = projectData.state.sources.find((s) => s.name === narrationName);
1448
+ if (narrationSource && regenerateNarration) {
1449
+ narrationSource.duration = narrationDuration;
1450
+ }
1451
+ // Update ALL clips' startTime and duration based on recalculated segments
1452
+ for (const clip of projectData.state.clips) {
1453
+ const source = projectData.state.sources.find((s) => s.id === clip.sourceId);
1454
+ if (!source)
1455
+ continue;
1456
+ // Match source name to segment (e.g., "Scene 1" → segment 0, "Narration 2" → segment 1)
1457
+ const sceneMatch = source.name.match(/^Scene (\d+)$/);
1458
+ const narrationMatch = source.name.match(/^Narration (\d+)$/);
1459
+ const segIdx = sceneMatch ? parseInt(sceneMatch[1]) - 1 : narrationMatch ? parseInt(narrationMatch[1]) - 1 : -1;
1460
+ if (segIdx >= 0 && segIdx < segments.length) {
1461
+ const seg = segments[segIdx];
1462
+ clip.startTime = seg.startTime;
1463
+ clip.duration = seg.duration;
1464
+ clip.sourceEndOffset = seg.duration;
1465
+ // Also update the source duration to match segment
1466
+ source.duration = seg.duration;
1467
+ }
1468
+ }
1469
+ await writeFile(projectPath, JSON.stringify(projectData, null, 2), "utf-8");
1470
+ updateSpinner.succeed(chalk.green("Updated project file (all clips synced)"));
1471
+ }
1472
+ catch (err) {
1473
+ updateSpinner.warn(chalk.yellow(`Could not update project file: ${err}`));
1474
+ }
1475
+ }
1476
+ console.log(chalk.green(` ✓ Scene ${sceneNum} done`));
1477
+ } // End of for loop over sceneNums
1478
+ // Final summary
1479
+ console.log();
1480
+ console.log(chalk.bold.green(`✅ ${sceneNums.length} scene${sceneNums.length > 1 ? "s" : ""} regenerated successfully!`));
1481
+ console.log(chalk.dim("─".repeat(60)));
1482
+ console.log();
1483
+ console.log(chalk.dim("Next steps:"));
1484
+ console.log(chalk.dim(` vibe export ${outputDir}/ -o final.mp4`));
1485
+ console.log();
1486
+ }
1487
+ catch (error) {
1488
+ console.error(chalk.red("Scene regeneration failed"));
1489
+ console.error(error);
1490
+ process.exit(1);
1491
+ }
1492
+ });
1493
+ }
1494
+ //# sourceMappingURL=ai-script-pipeline-cli.js.map