@gentbajko/slopify 2.4.0 → 3.0.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 (469) hide show
  1. package/LICENSE +201 -21
  2. package/NOTICE +12 -0
  3. package/README.md +21 -40
  4. package/SUBTITLES.md +1 -1
  5. package/dist/adapter-registry.js +11 -1
  6. package/dist/adapters/alignment/cache.js +4 -1
  7. package/dist/adapters/alignment/ctc.js +25 -23
  8. package/dist/adapters/alignment/index.js +37 -6
  9. package/dist/adapters/alignment/multilingual.js +267 -0
  10. package/dist/adapters/alignment/protocol.js +11 -0
  11. package/dist/adapters/alignment/quality.js +4 -3
  12. package/dist/adapters/alignment/sentences.js +95 -0
  13. package/dist/adapters/alignment/spec.js +24 -0
  14. package/dist/adapters/alignment/spell.js +769 -0
  15. package/dist/adapters/alignment/text.js +56 -9
  16. package/dist/adapters/alignment/window.js +34 -30
  17. package/dist/adapters/alignment/worker.js +23 -21
  18. package/dist/adapters/explain.js +11 -11
  19. package/dist/adapters/host-cli/index.js +55 -5
  20. package/dist/adapters/host-cli/transport.js +13 -6
  21. package/dist/adapters/image/bytes.js +2 -2
  22. package/dist/adapters/image/codex-output.js +60 -18
  23. package/dist/adapters/image/codex.js +166 -25
  24. package/dist/adapters/image/fal.js +31 -3
  25. package/dist/adapters/image/google.js +17 -2
  26. package/dist/adapters/image/openai.js +45 -16
  27. package/dist/adapters/image/reference.js +41 -0
  28. package/dist/adapters/image/replicate.js +14 -2
  29. package/dist/adapters/image/video.js +2 -2
  30. package/dist/adapters/key-probes.js +36 -0
  31. package/dist/adapters/llm/claude-code.js +168 -32
  32. package/dist/adapters/llm/cli-login-error.js +1 -1
  33. package/dist/adapters/llm/codex-limits.js +157 -0
  34. package/dist/adapters/llm/codex-models.js +2 -1
  35. package/dist/adapters/llm/codex-server-models.js +2 -1
  36. package/dist/adapters/llm/codex.js +99 -12
  37. package/dist/adapters/llm/document-workspace.js +2 -2
  38. package/dist/adapters/llm/gemini-workspace.js +1 -1
  39. package/dist/adapters/llm/gemini.js +16 -3
  40. package/dist/adapters/llm/image-workspace.js +58 -0
  41. package/dist/adapters/llm/openrouter.js +11 -3
  42. package/dist/adapters/llm/run-cli.js +26 -9
  43. package/dist/adapters/tts/cartesia.js +10 -0
  44. package/dist/adapters/tts/elevenlabs.js +31 -9
  45. package/dist/adapters/tts/inworld-async.js +9 -5
  46. package/dist/adapters/tts/inworld.js +17 -3
  47. package/dist/adapters/tts/openai.js +3 -0
  48. package/dist/adapters/tts/voice-languages.js +80 -0
  49. package/dist/assets/fonts/noto/NotoSans-Regular.ttf +0 -0
  50. package/dist/assets/fonts/noto/NotoSansArabic-Regular.ttf +0 -0
  51. package/dist/assets/fonts/noto/NotoSansDevanagari-Regular.ttf +0 -0
  52. package/dist/assets/fonts/noto/NotoSansHebrew-Regular.ttf +0 -0
  53. package/dist/assets/fonts/noto/NotoSansThai-Regular.ttf +0 -0
  54. package/dist/assets/fonts/noto/OFL.txt +93 -0
  55. package/dist/assets/fonts/noto/SOURCE.txt +18 -0
  56. package/dist/assets/models.yaml +7 -0
  57. package/dist/assets/sample/sample-audiobook.tar +0 -0
  58. package/dist/assets/sample/sample-podcast.tar +0 -0
  59. package/dist/assets/sample/sample-project.tar +0 -0
  60. package/dist/catalog/merge.js +132 -0
  61. package/dist/catalog/registry.js +33 -12
  62. package/dist/catalog/runtime-models.js +1 -1
  63. package/dist/catalog/schema.js +12 -1
  64. package/dist/catalog/store.js +147 -8
  65. package/dist/catalog/validate.js +17 -2
  66. package/dist/compose.yaml +71 -0
  67. package/dist/edge/autostart/docker-record.js +32 -0
  68. package/dist/edge/autostart/docker.js +50 -0
  69. package/dist/edge/autostart/index.js +78 -0
  70. package/dist/edge/autostart/launcher.js +176 -0
  71. package/dist/edge/autostart/model.js +3 -0
  72. package/dist/edge/autostart/native.js +210 -0
  73. package/dist/edge/autostart/prompt.js +66 -0
  74. package/dist/edge/autostart/service.js +81 -0
  75. package/dist/edge/cli.js +57 -51
  76. package/dist/edge/docker-install/activation.js +56 -0
  77. package/dist/edge/docker-install/apply.js +458 -0
  78. package/dist/edge/docker-install/engine.js +362 -0
  79. package/dist/edge/docker-install/run.js +154 -0
  80. package/dist/edge/docker-install/state.js +146 -0
  81. package/dist/edge/{docker-projects → docker-install}/volume.js +1 -5
  82. package/dist/edge/docker.js +14 -22
  83. package/dist/edge/events/hub.js +11 -0
  84. package/dist/edge/host-cli.js +1 -1
  85. package/dist/edge/http/actions.js +32 -10
  86. package/dist/edge/http/app.js +60 -2
  87. package/dist/edge/http/auditions.js +70 -0
  88. package/dist/edge/http/autostart.js +37 -0
  89. package/dist/edge/http/backups.js +40 -0
  90. package/dist/edge/http/channel-memory.js +92 -0
  91. package/dist/edge/http/channels.js +128 -0
  92. package/dist/edge/http/entries.js +22 -3
  93. package/dist/edge/http/files.js +1 -1
  94. package/dist/edge/http/folder-location.js +1 -1
  95. package/dist/edge/http/home.js +39 -0
  96. package/dist/edge/http/host-cli.js +45 -6
  97. package/dist/edge/http/narration-peaks.js +76 -0
  98. package/dist/edge/http/onboarding.js +152 -0
  99. package/dist/edge/http/open-folder.js +1 -1
  100. package/dist/edge/http/patch-notes.js +45 -0
  101. package/dist/edge/http/planning.js +4 -2
  102. package/dist/edge/http/problem.js +1 -0
  103. package/dist/edge/http/project-create.js +87 -0
  104. package/dist/edge/http/projects.js +31 -99
  105. package/dist/edge/http/prompts.js +40 -3
  106. package/dist/edge/http/pronunciations.js +23 -4
  107. package/dist/edge/http/providers.js +86 -0
  108. package/dist/edge/http/reviews.js +121 -0
  109. package/dist/edge/http/revision-files.js +1 -1
  110. package/dist/edge/http/revisions.js +1 -1
  111. package/dist/edge/http/run-cost.js +25 -0
  112. package/dist/edge/http/schedules.js +111 -6
  113. package/dist/edge/http/settings.js +128 -6
  114. package/dist/edge/http/storage-files.js +50 -0
  115. package/dist/edge/http/storage.js +29 -2
  116. package/dist/edge/http/studio.js +207 -0
  117. package/dist/edge/http/style-preview.js +100 -0
  118. package/dist/edge/http/subtitles.js +1 -1
  119. package/dist/edge/http/trash.js +69 -0
  120. package/dist/edge/http/update.js +24 -3
  121. package/dist/edge/http/whats-new.js +15 -0
  122. package/dist/edge/http/youtube-edits.js +83 -0
  123. package/dist/edge/native-update.js +62 -0
  124. package/dist/host-cli/open-folder.js +10 -10
  125. package/dist/host-cli/runtime.js +12 -3
  126. package/dist/kernel/db/migrations/0022-drop-duplicate-held-work.sql +56 -0
  127. package/dist/kernel/db/migrations/0024-reference-attachments.sql +18 -0
  128. package/dist/kernel/db/migrations/0025-automatic-reviews.sql +40 -0
  129. package/dist/kernel/db/migrations/0026-schedule-topic-generation.sql +23 -0
  130. package/dist/kernel/db/migrations/0027-run-cost.sql +58 -0
  131. package/dist/kernel/db/migrations/0030-automatic-retries.sql +18 -0
  132. package/dist/kernel/db/migrations/0031-prompt-history-and-description-edits.sql +33 -0
  133. package/dist/kernel/db/migrations/0032-channels.sql +65 -0
  134. package/dist/kernel/db/migrations/0034-script-prompts.sql +23 -0
  135. package/dist/kernel/db/migrations/0035-narration-aliases.sql +13 -0
  136. package/dist/kernel/db/migrations/0037-project-uploads.sql +8 -0
  137. package/dist/kernel/db/migrations/0038-voice-languages.sql +7 -0
  138. package/dist/kernel/db/migrations/0039-channel-essentials.sql +64 -0
  139. package/dist/kernel/db/migrations/0040-standalone-usage.sql +35 -0
  140. package/dist/kernel/paths.js +27 -6
  141. package/dist/kernel/pipeline.js +4 -1
  142. package/dist/kernel/ports/host-cli.js +139 -16
  143. package/dist/kernel/ports/image.js +3 -1
  144. package/dist/kernel/ports/key-probe.js +1 -0
  145. package/dist/kernel/ports/languages.js +157 -0
  146. package/dist/kernel/ports/llm.js +3 -1
  147. package/dist/kernel/ports/model.js +5 -2
  148. package/dist/kernel/ports/narration-aliases.js +60 -0
  149. package/dist/kernel/ports/plan-limits.js +36 -0
  150. package/dist/kernel/runner/attempt.js +18 -9
  151. package/dist/kernel/runner/audition.js +36 -0
  152. package/dist/kernel/runner/graph.js +24 -5
  153. package/dist/kernel/runner/index.js +35 -5
  154. package/dist/kernel/runner/meter.js +1 -0
  155. package/dist/kernel/runner/providers.js +164 -9
  156. package/dist/kernel/runner/retry-policy.js +43 -0
  157. package/dist/kernel/runner/standalone.js +134 -0
  158. package/dist/kernel/runner/work-authority.js +28 -2
  159. package/dist/main.js +321 -20
  160. package/dist/patch-notes/0.4.2-to-1.0.md +93 -0
  161. package/dist/patch-notes/1.0-to-2.0.md +74 -0
  162. package/dist/patch-notes/2.0-to-3.0.md +257 -0
  163. package/dist/patch-notes/3.0.0.md +70 -0
  164. package/dist/patch-notes/index.json +26 -0
  165. package/dist/slices/admission/model.js +35 -0
  166. package/dist/slices/admission/repo.js +24 -4
  167. package/dist/slices/admission/rules.js +126 -1
  168. package/dist/slices/admission/schema.js +68 -1
  169. package/dist/slices/admission/short-mode.js +47 -0
  170. package/dist/slices/admission/start.js +90 -16
  171. package/dist/slices/article/continuation.js +2 -2
  172. package/dist/slices/article/provided-entries.js +2 -2
  173. package/dist/slices/article/run.js +6 -2
  174. package/dist/slices/article/segments.js +3 -3
  175. package/dist/slices/backups/files.js +135 -0
  176. package/dist/slices/backups/folder.js +43 -0
  177. package/dist/slices/backups/model.js +65 -0
  178. package/dist/slices/backups/repo.js +28 -0
  179. package/dist/slices/backups/schedule.js +60 -0
  180. package/dist/slices/backups/service.js +311 -0
  181. package/dist/slices/batch/index.js +9 -2
  182. package/dist/slices/cancel/index.js +1 -1
  183. package/dist/slices/channels/cast-images.js +110 -0
  184. package/dist/slices/channels/cast-match.js +32 -0
  185. package/dist/slices/channels/images.js +19 -0
  186. package/dist/slices/channels/model.js +5 -0
  187. package/dist/slices/channels/rebrand.js +100 -0
  188. package/dist/slices/channels/repo.js +218 -0
  189. package/dist/slices/channels/runs.js +129 -0
  190. package/dist/slices/channels/schema.js +129 -0
  191. package/dist/slices/channels/service.js +180 -0
  192. package/dist/slices/channels/studio-csv.js +110 -0
  193. package/dist/slices/channels/videos.js +120 -0
  194. package/dist/slices/checkpoints/recovery.js +10 -0
  195. package/dist/slices/control/index.js +2 -1
  196. package/dist/slices/document/fonts.js +1 -1
  197. package/dist/slices/document/render.js +1 -1
  198. package/dist/slices/episodes/related.js +90 -0
  199. package/dist/slices/episodes/repo.js +60 -0
  200. package/dist/slices/episodes/service.js +58 -0
  201. package/dist/slices/episodes/summarize.js +170 -0
  202. package/dist/slices/estimate/index.js +113 -24
  203. package/dist/slices/estimate/requests.js +60 -2
  204. package/dist/slices/fixes/rules.js +73 -0
  205. package/dist/slices/fonts/coverage.js +82 -0
  206. package/dist/slices/fonts/sfnt.js +74 -0
  207. package/dist/slices/images/run.js +8 -5
  208. package/dist/slices/images/scale.js +84 -0
  209. package/dist/slices/library/diff.js +122 -0
  210. package/dist/slices/library/history.js +100 -0
  211. package/dist/slices/library/model.js +3 -0
  212. package/dist/slices/library/photorealistic.js +49 -0
  213. package/dist/slices/library/repo.js +19 -14
  214. package/dist/slices/library/save.js +62 -12
  215. package/dist/slices/library/slots.js +15 -3
  216. package/dist/slices/library/snapshot.js +8 -2
  217. package/dist/slices/library/used-by.js +153 -0
  218. package/dist/slices/loudness/level-pieces.js +41 -0
  219. package/dist/slices/loudness/loudnorm.js +164 -0
  220. package/dist/slices/loudness/model.js +153 -0
  221. package/dist/slices/model-upkeep/model.js +10 -0
  222. package/dist/slices/model-upkeep/switch.js +217 -0
  223. package/dist/slices/model-upkeep/usage.js +183 -0
  224. package/dist/slices/narration/aliases-library.js +31 -0
  225. package/dist/slices/narration/aliases-schema.js +53 -0
  226. package/dist/slices/narration/aliases.js +49 -0
  227. package/dist/slices/narration/blocks.js +352 -0
  228. package/dist/slices/narration/chunk.js +19 -7
  229. package/dist/slices/narration/concat.js +2 -2
  230. package/dist/slices/narration/describe.js +45 -0
  231. package/dist/slices/narration/pauses-model.js +63 -0
  232. package/dist/slices/narration/pauses.js +269 -0
  233. package/dist/slices/narration/peaks-model.js +1 -0
  234. package/dist/slices/narration/peaks.js +94 -0
  235. package/dist/slices/narration/preparation.js +21 -3
  236. package/dist/slices/narration/pronunciation-chunks.js +3 -3
  237. package/dist/slices/narration/pronunciation.js +72 -25
  238. package/dist/slices/narration/run.js +6 -6
  239. package/dist/slices/narration/shared-glossary.js +1 -1
  240. package/dist/slices/narration/steering.js +15 -4
  241. package/dist/slices/notifications/notifier.js +94 -0
  242. package/dist/slices/notifications/rules.js +102 -0
  243. package/dist/slices/notifications/send.js +40 -0
  244. package/dist/slices/notifications/settings.js +34 -0
  245. package/dist/slices/onboarding/copy.js +143 -0
  246. package/dist/slices/onboarding/first-run.js +46 -0
  247. package/dist/slices/onboarding/install.js +119 -0
  248. package/dist/slices/onboarding/model.js +64 -0
  249. package/dist/slices/onboarding/packs.js +491 -0
  250. package/dist/slices/onboarding/quick-short.js +129 -0
  251. package/dist/slices/onboarding/sample.js +132 -0
  252. package/dist/slices/onboarding/state.js +68 -0
  253. package/dist/slices/onboarding/template.js +73 -0
  254. package/dist/slices/patch-notes/library.js +73 -0
  255. package/dist/slices/patch-notes/seen.js +77 -0
  256. package/dist/slices/play-drafts/convert.js +160 -9
  257. package/dist/slices/play-drafts/readiness.js +3 -0
  258. package/dist/slices/play-drafts/repo.js +9 -1
  259. package/dist/slices/play-drafts/review-inputs.js +22 -3
  260. package/dist/slices/play-drafts/schema.js +109 -2
  261. package/dist/slices/play-drafts/service.js +10 -1
  262. package/dist/slices/play-drafts/start.js +43 -18
  263. package/dist/slices/play-drafts/uploads.js +2 -1
  264. package/dist/slices/project-templates/from-project.js +70 -4
  265. package/dist/slices/project-templates/one-off.js +26 -0
  266. package/dist/slices/project-templates/repo.js +2 -1
  267. package/dist/slices/project-templates/service.js +15 -4
  268. package/dist/slices/project-templates/setup.js +29 -2
  269. package/dist/slices/rebuild/admission-repo.js +1 -1
  270. package/dist/slices/rebuild/model.js +6 -0
  271. package/dist/slices/rebuild/preview-details.js +67 -2
  272. package/dist/slices/rebuild/preview-plan.js +13 -6
  273. package/dist/slices/rebuild/recipe-audio-parts.js +40 -7
  274. package/dist/slices/rebuild/recipe-audio.js +66 -12
  275. package/dist/slices/rebuild/recipe-build.js +15 -5
  276. package/dist/slices/rebuild/recipe-cast.js +18 -0
  277. package/dist/slices/rebuild/recipe-checkpoints.js +11 -9
  278. package/dist/slices/rebuild/recipe-describe.js +93 -0
  279. package/dist/slices/rebuild/recipe-edit.js +13 -1
  280. package/dist/slices/rebuild/recipe-exports.js +62 -8
  281. package/dist/slices/rebuild/recipe-input-schema.js +43 -1
  282. package/dist/slices/rebuild/recipe-legacy.js +12 -0
  283. package/dist/slices/rebuild/recipe-loudness.js +45 -0
  284. package/dist/slices/rebuild/recipe-model.js +25 -6
  285. package/dist/slices/rebuild/recipe-pauses.js +7 -0
  286. package/dist/slices/rebuild/recipe-preparation.js +3 -3
  287. package/dist/slices/rebuild/recipe-provider-choice.js +11 -1
  288. package/dist/slices/rebuild/recipe-reference.js +58 -0
  289. package/dist/slices/rebuild/recipe-reviews.js +117 -0
  290. package/dist/slices/rebuild/recipe-save.js +4 -1
  291. package/dist/slices/rebuild/recipe-shorts.js +32 -5
  292. package/dist/slices/rebuild/recipe-text.js +114 -19
  293. package/dist/slices/rebuild/recipe-validation.js +29 -3
  294. package/dist/slices/rebuild/recipe-visual.js +104 -29
  295. package/dist/slices/rebuild/recipe-voices.js +286 -0
  296. package/dist/slices/rebuild/recipe-work.js +77 -6
  297. package/dist/slices/rebuild/recipe-youtube.js +2 -0
  298. package/dist/slices/rebuild/recovery-conflict.js +7 -3
  299. package/dist/slices/rebuild/recovery-model.js +3 -0
  300. package/dist/slices/rebuild/recovery-selection.js +28 -2
  301. package/dist/slices/rebuild/recovery.js +35 -17
  302. package/dist/slices/rebuild/repo.js +3 -3
  303. package/dist/slices/rebuild/retry.js +18 -0
  304. package/dist/slices/rebuild/review-redo.js +110 -0
  305. package/dist/slices/rebuild/runtime-actions.js +14 -2
  306. package/dist/slices/rebuild/runtime-admission.js +4 -1
  307. package/dist/slices/rebuild/runtime-animate.js +4 -4
  308. package/dist/slices/rebuild/runtime-article.js +5 -5
  309. package/dist/slices/rebuild/runtime-document.js +4 -4
  310. package/dist/slices/rebuild/runtime-export-bed.js +43 -0
  311. package/dist/slices/rebuild/runtime-export-edit.js +70 -11
  312. package/dist/slices/rebuild/runtime-export-inputs.js +52 -10
  313. package/dist/slices/rebuild/runtime-export-short.js +85 -0
  314. package/dist/slices/rebuild/runtime-export.js +84 -19
  315. package/dist/slices/rebuild/runtime-figure-card.js +87 -0
  316. package/dist/slices/rebuild/runtime-image.js +69 -0
  317. package/dist/slices/rebuild/runtime-local.js +154 -27
  318. package/dist/slices/rebuild/runtime-materialize.js +8 -0
  319. package/dist/slices/rebuild/runtime-narration-reuse.js +2 -1
  320. package/dist/slices/rebuild/runtime-narration-text.js +7 -7
  321. package/dist/slices/rebuild/runtime-pictures.js +28 -0
  322. package/dist/slices/rebuild/runtime-piece-label.js +29 -0
  323. package/dist/slices/rebuild/runtime-provider.js +134 -19
  324. package/dist/slices/rebuild/runtime-publication.js +21 -5
  325. package/dist/slices/rebuild/runtime-review.js +237 -0
  326. package/dist/slices/rebuild/runtime-run.js +17 -11
  327. package/dist/slices/rebuild/runtime-shorts.js +48 -23
  328. package/dist/slices/rebuild/runtime-store.js +39 -10
  329. package/dist/slices/rebuild/runtime-subtitles.js +72 -19
  330. package/dist/slices/rebuild/runtime-voices.js +103 -0
  331. package/dist/slices/rebuild/runtime-youtube.js +7 -6
  332. package/dist/slices/rebuild/service-readiness.js +15 -1
  333. package/dist/slices/rebuild/soften.js +49 -0
  334. package/dist/slices/rebuild/transition-repo.js +10 -3
  335. package/dist/slices/research/run.js +4 -4
  336. package/dist/slices/research/synthesis.js +2 -2
  337. package/dist/slices/reviews/model.js +105 -0
  338. package/dist/slices/reviews/outcome.js +22 -0
  339. package/dist/slices/reviews/repo.js +106 -0
  340. package/dist/slices/reviews/rules.js +63 -0
  341. package/dist/slices/reviews/verdict.js +71 -0
  342. package/dist/slices/reviews/view.js +30 -0
  343. package/dist/slices/revisions/adopt-content.js +12 -2
  344. package/dist/slices/revisions/adopt.js +9 -2
  345. package/dist/slices/revisions/downloads.js +7 -1
  346. package/dist/slices/revisions/image-plan.js +183 -0
  347. package/dist/slices/revisions/mutation-assets.js +43 -9
  348. package/dist/slices/revisions/mutation-cues.js +71 -0
  349. package/dist/slices/revisions/mutation-prepare.js +8 -10
  350. package/dist/slices/revisions/mutation-request.js +2 -1
  351. package/dist/slices/revisions/mutations.js +97 -3
  352. package/dist/slices/revisions/schema.js +16 -2
  353. package/dist/slices/run-cost/limits.js +128 -0
  354. package/dist/slices/run-cost/meter.js +47 -0
  355. package/dist/slices/run-cost/panel.js +234 -0
  356. package/dist/slices/run-cost/pricing.js +92 -0
  357. package/dist/slices/run-cost/week.js +108 -0
  358. package/dist/slices/schedules/agenda.js +158 -0
  359. package/dist/slices/schedules/repo.js +58 -18
  360. package/dist/slices/schedules/scheduler.js +67 -16
  361. package/dist/slices/schedules/schema.js +138 -1
  362. package/dist/slices/schedules/service.js +48 -4
  363. package/dist/slices/schedules/similar.js +90 -0
  364. package/dist/slices/schedules/topic-list.js +163 -0
  365. package/dist/slices/schedules/topics.js +422 -0
  366. package/dist/slices/settings/first-run.js +102 -0
  367. package/dist/slices/settings/health.js +190 -0
  368. package/dist/slices/settings/key-guides.js +128 -0
  369. package/dist/slices/settings/key-test.js +85 -0
  370. package/dist/slices/settings/model.js +11 -1
  371. package/dist/slices/settings/playback.js +15 -0
  372. package/dist/slices/settings/repo.js +17 -3
  373. package/dist/slices/settings/voices.js +64 -4
  374. package/dist/slices/settings/whats-new.js +64 -0
  375. package/dist/slices/shorts/captions.js +9 -0
  376. package/dist/slices/shorts/pick.js +3 -3
  377. package/dist/slices/shorts/prompts.js +1 -1
  378. package/dist/slices/shorts/render.js +54 -5
  379. package/dist/slices/storage/asset-name.js +3 -0
  380. package/dist/slices/storage/backup-export.js +48 -9
  381. package/dist/slices/storage/backup-format.js +38 -1
  382. package/dist/slices/storage/backup-import.js +191 -19
  383. package/dist/slices/storage/delete-project.js +7 -4
  384. package/dist/slices/storage/documents.js +79 -0
  385. package/dist/slices/storage/files-location.js +531 -0
  386. package/dist/slices/storage/layout.js +20 -0
  387. package/dist/slices/storage/model.js +14 -0
  388. package/dist/slices/storage/portable.js +95 -12
  389. package/dist/slices/storage/reconcile.js +19 -2
  390. package/dist/slices/storage/repo.js +4 -1
  391. package/dist/slices/storage/schema.js +11 -0
  392. package/dist/slices/storage/trim.js +146 -0
  393. package/dist/slices/studio/disclosure.js +74 -0
  394. package/dist/slices/studio/model.js +28 -0
  395. package/dist/slices/studio/pack.js +209 -0
  396. package/dist/slices/studio/settings.js +133 -0
  397. package/dist/slices/style-preview/images.js +61 -0
  398. package/dist/slices/style-preview/render.js +161 -0
  399. package/dist/slices/style-preview/schema.js +45 -0
  400. package/dist/slices/style-preview/service.js +93 -0
  401. package/dist/slices/style-preview/settings.js +89 -0
  402. package/dist/slices/subtitles/captions.js +42 -9
  403. package/dist/slices/subtitles/model.js +22 -0
  404. package/dist/slices/subtitles/prepare.js +27 -16
  405. package/dist/slices/subtitles/transcript.js +1 -1
  406. package/dist/slices/thumbnail/run.js +7 -7
  407. package/dist/slices/trash/model.js +25 -0
  408. package/dist/slices/trash/service.js +273 -0
  409. package/dist/slices/uploads/repo.js +20 -0
  410. package/dist/slices/video/ambient-bed-schema.js +15 -0
  411. package/dist/slices/video/ambient-bed.js +95 -0
  412. package/dist/slices/video/ambient-mix.js +72 -0
  413. package/dist/slices/video/audio-inputs.js +2 -2
  414. package/dist/slices/video/cards.js +5 -2
  415. package/dist/slices/video/edit-list.js +23 -2
  416. package/dist/slices/video/edit-settings.js +13 -3
  417. package/dist/slices/video/ffmpeg.js +63 -9
  418. package/dist/slices/video/figure-card.js +492 -0
  419. package/dist/slices/video/figure-spans.js +51 -0
  420. package/dist/slices/video/plan.js +125 -13
  421. package/dist/slices/video/run.js +14 -12
  422. package/dist/slices/video/slideshow.js +39 -4
  423. package/dist/slices/voices/attribution.js +69 -0
  424. package/dist/slices/voices/audio-files.js +82 -0
  425. package/dist/slices/voices/audition.js +25 -0
  426. package/dist/slices/voices/cast.js +56 -0
  427. package/dist/slices/voices/grouping.js +49 -0
  428. package/dist/slices/voices/join.js +35 -0
  429. package/dist/slices/voices/languages.js +25 -0
  430. package/dist/slices/voices/model.js +179 -0
  431. package/dist/slices/voices/palette.js +26 -0
  432. package/dist/slices/voices/panel.js +91 -0
  433. package/dist/slices/voices/portraits.js +55 -0
  434. package/dist/slices/voices/script.js +136 -0
  435. package/dist/slices/voices/timing.js +58 -0
  436. package/dist/slices/youtube/answer.js +1 -1
  437. package/dist/slices/youtube/chapters.js +126 -0
  438. package/dist/slices/youtube/edits-repo.js +69 -0
  439. package/dist/slices/youtube/edits.js +89 -0
  440. package/dist/slices/youtube/placeholders.js +98 -0
  441. package/dist/slices/youtube/transcript.js +11 -4
  442. package/dist/updater/service.js +66 -21
  443. package/dist/web/assets/index-CUA2sUxU.css +1 -0
  444. package/dist/web/assets/index-CX590kE-.js +303 -0
  445. package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  446. package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  447. package/dist/web/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  448. package/dist/web/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  449. package/dist/web/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  450. package/dist/web/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  451. package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  452. package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  453. package/dist/web/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  454. package/dist/web/assets/{pdf-CAJNT254.js → pdf-BG8n_uc5.js} +1 -1
  455. package/dist/web/index.html +2 -2
  456. package/package.json +4 -4
  457. package/dist/edge/docker-launch.js +0 -81
  458. package/dist/edge/docker-projects/activation.js +0 -35
  459. package/dist/edge/docker-projects/claims.js +0 -85
  460. package/dist/edge/docker-projects/committed.js +0 -40
  461. package/dist/edge/docker-projects/engine.js +0 -461
  462. package/dist/edge/docker-projects/install.js +0 -254
  463. package/dist/edge/docker-projects/prune.js +0 -66
  464. package/dist/edge/docker-projects/recover.js +0 -100
  465. package/dist/edge/docker-projects/state.js +0 -225
  466. package/dist/web/assets/index-BGgYMm2u.css +0 -1
  467. package/dist/web/assets/index-BVVgtntL.js +0 -171
  468. package/scripts/docker-run.sh +0 -3
  469. /package/dist/edge/{docker-projects → docker-install}/tree.js +0 -0
@@ -0,0 +1,237 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { z } from "zod";
3
+ import { thinkingModes } from "../../kernel/ports/llm.js";
4
+ import { plainText } from "../article/plain.js";
5
+ import { defaultReviewPrompts, narrationItemKey, reviewStages, } from "../reviews/model.js";
6
+ import { nextAttempt, reviewOutcome } from "../reviews/outcome.js";
7
+ import { latestVerdict, saveVerdict } from "../reviews/repo.js";
8
+ import { parseVerdict, reviewMessages } from "../reviews/verdict.js";
9
+ import { pickedShortsOf } from "../shorts/clips.js";
10
+ import { outputPath } from "../storage/layout.js";
11
+ import { exportSnapshot } from "./runtime-export-inputs.js";
12
+ import { savedCatalogue } from "./runtime-plan.js";
13
+ import { publishResult } from "./runtime-publication.js";
14
+ import { wordsSchema } from "./runtime-subtitles.js";
15
+ // What `recipe-reviews.ts` saves in the step's values, by position.
16
+ const valuesSchema = z.tuple([
17
+ z.enum(reviewStages),
18
+ z.string(),
19
+ z.array(z.tuple([z.string(), z.unknown()])),
20
+ z.string(),
21
+ z.string(),
22
+ z.string().nullable(),
23
+ z.string(),
24
+ z.enum(["flag", "redo"]),
25
+ z.number().int().nonnegative(),
26
+ ]);
27
+ // ceiling: enough of a long article or research for a reviewer to judge it, and well inside
28
+ // every supported model's context.
29
+ const textMax = 60_000;
30
+ // Asks the reviewer about one finished item and saves its verdict. A malformed answer is a
31
+ // failed attempt the provider wrapper asks again; a verdict that sends the item back is
32
+ // saved as a pending redo, which `review-redo.ts` starts once this step has finished, and
33
+ // until then the item's dependents are held (`reviewHold`).
34
+ export async function executeReviewRecipe(deps, context, providers, piece) {
35
+ if (deps.db
36
+ .prepare("SELECT state FROM revision_work_pieces WHERE id=? AND work_id=?")
37
+ .get(piece.id, context.work.workId)?.state === "done")
38
+ return "done";
39
+ if (!context.maySubmit(piece.id))
40
+ return "held";
41
+ context.signal.throwIfAborted();
42
+ const input = piece.input;
43
+ const values = input.kind === "local" && input.operation === "review-v1"
44
+ ? valuesSchema.safeParse(input.values)
45
+ : undefined;
46
+ if (values === undefined || !values.success)
47
+ throw new Error("Slopify hit an internal error (a review step was set up wrongly). Try again; if it happens again, use Download diagnostics in Settings and report it.");
48
+ const [stage, itemKey, , provider, model, thinking, prompt, mode, retries] = values.data;
49
+ const { view, plan } = exportSnapshot(deps, context, piece);
50
+ const material = reviewMaterial(deps, context, view, plan.recipes, stage, itemKey, prompt);
51
+ const previous = latestVerdict(deps.db, context.work.projectId, itemKey);
52
+ const attempt = nextAttempt(previous, material.itemFingerprint);
53
+ const row = deps.db
54
+ .prepare("SELECT recipe_context FROM revision_work WHERE id=?")
55
+ .get(context.work.workId);
56
+ const catalogued = savedCatalogue(row?.recipe_context).llm.find((one) => one.provider === provider && one.id === model);
57
+ const level = thinking === null ? undefined : z.enum(thinkingModes).parse(thinking);
58
+ if (!context.maySubmit(piece.id))
59
+ return "held";
60
+ const answer = await providers.forPiece(piece.id).llm({
61
+ provider,
62
+ model,
63
+ ...(level === undefined ? {} : { thinking: level }),
64
+ thinkingConfig: level === undefined ? null : (catalogued?.llm.thinking?.[level] ?? null),
65
+ messages: reviewMessages(material.request),
66
+ webSearch: false,
67
+ ...(material.images.length === 0 ? {} : { images: material.images }),
68
+ previewLabel: piece.key,
69
+ check: (value) => {
70
+ const parsed = parseVerdict(value.text);
71
+ return parsed.ok ? undefined : parsed.reason;
72
+ },
73
+ });
74
+ if (!answer.ok)
75
+ return "held";
76
+ const verdict = parseVerdict(answer.value.text);
77
+ if (!verdict.ok)
78
+ throw new Error(verdict.reason);
79
+ const outcome = reviewOutcome({ passed: verdict.value.passed, mode, attempt, retries });
80
+ const saved = saveVerdict(deps.db, {
81
+ id: deps.ids.next(),
82
+ projectId: context.work.projectId,
83
+ revisionId: context.work.revisionId,
84
+ itemKey,
85
+ stage,
86
+ itemFingerprint: material.itemFingerprint,
87
+ reviewFingerprint: piece.fingerprint,
88
+ passed: verdict.value.passed,
89
+ reasons: verdict.value.reasons,
90
+ outcome,
91
+ attempt,
92
+ action: null,
93
+ actionAt: null,
94
+ redoState: outcome === "redo" ? "pending" : null,
95
+ redoError: null,
96
+ createdAt: deps.clock.now().toISOString(),
97
+ });
98
+ await publishResult(deps, context, piece, [], {
99
+ verdictId: saved.id,
100
+ passed: saved.passed,
101
+ reasons: saved.reasons,
102
+ outcome: saved.outcome,
103
+ attempt: saved.attempt,
104
+ });
105
+ deps.count?.("stage.completed", {
106
+ provider,
107
+ model,
108
+ tokensIn: answer.value.usage?.inputTokens ?? 0,
109
+ tokensOut: answer.value.usage?.outputTokens ?? 0,
110
+ });
111
+ return "done";
112
+ }
113
+ function reviewMaterial(deps, context, view, recipes, stage, itemKey, prompt) {
114
+ const config = view.revision.config;
115
+ const output = (workKey, role) => view.outputs.find((row) => row.workKey === workKey &&
116
+ row.selected &&
117
+ row.available &&
118
+ row.state === "ready" &&
119
+ (role === undefined || row.output.role === role));
120
+ const path = (row) => outputPath(deps.paths, context.work.projectId, row.output.path);
121
+ const text = (row) => clip(readFileSync(path(row), "utf8"));
122
+ const missing = (what, fix) => new Error(`The ${what} this review looks at is missing, so it could not be reviewed. ${fix}, then use Try again.`);
123
+ const request = (sections, images = []) => ({
124
+ stage,
125
+ instruction: prompt.trim() === "" ? defaultReviewPrompts[stage] : prompt,
126
+ sections,
127
+ images,
128
+ });
129
+ switch (stage) {
130
+ case "article": {
131
+ const article = output(itemKey, "article_md");
132
+ if (article === undefined)
133
+ throw missing("article", "Use More → Write the article again in the Article section");
134
+ const notes = output("research:notes", "notes");
135
+ const research = notes !== undefined ? text(notes) : clip(config.provided.research?.trim() ?? "");
136
+ return {
137
+ request: request([
138
+ {
139
+ label: "Article prompt the article was written from",
140
+ text: clip(config.rendered.article ?? ""),
141
+ },
142
+ ...(research === "" ? [] : [{ label: "Research notes", text: research }]),
143
+ { label: "Article", text: text(article) },
144
+ ]),
145
+ images: [],
146
+ itemFingerprint: article.fingerprint,
147
+ };
148
+ }
149
+ case "images":
150
+ case "thumbnail": {
151
+ const image = output(itemKey);
152
+ if (image === undefined)
153
+ throw missing(stage === "images" ? "image" : "thumbnail", stage === "images"
154
+ ? "Use More → make it again in its section on Images"
155
+ : "Use More → make it again in its section on Thumbnail");
156
+ const recipe = recipes.find((one) => one.key === itemKey);
157
+ const brief = recipe?.input.kind === "image" ? recipe.input.prompt : "";
158
+ const reference = stage === "images" ? output("reference:image") : undefined;
159
+ return {
160
+ request: request([
161
+ ...(stage === "thumbnail" ? [{ label: "Video title", text: config.title }] : []),
162
+ ...(brief === "" ? [] : [{ label: "Brief the image was drawn from", text: brief }]),
163
+ ], [
164
+ stage === "images" ? "the image to review" : "the thumbnail to review",
165
+ ...(reference === undefined ? [] : ["the establishing image it must match"]),
166
+ ]),
167
+ images: [
168
+ {
169
+ path: path(image),
170
+ name: stage === "images" ? "the image to review" : "the thumbnail to review",
171
+ },
172
+ ...(reference === undefined
173
+ ? []
174
+ : [{ path: path(reference), name: "the establishing image it must match" }]),
175
+ ],
176
+ itemFingerprint: image.fingerprint,
177
+ };
178
+ }
179
+ case "narration": {
180
+ const timing = output("subtitles:timing", "subtitle_words");
181
+ if (timing === undefined || itemKey !== narrationItemKey)
182
+ throw missing("narration's word timing", "Use More → Render the video again in the Video section");
183
+ const { words, omissions } = wordsSchema.parse(JSON.parse(readFileSync(path(timing), "utf8")));
184
+ const unsure = words.filter((word) => (word.confidence ?? 1) < 0.3).length;
185
+ return {
186
+ request: request([
187
+ {
188
+ label: "Text the narration was read from",
189
+ text: clip(plainText(view.articleMarkdown ?? "")),
190
+ },
191
+ { label: "What the timing heard", text: clip(words.map((word) => word.text).join(" ")) },
192
+ {
193
+ label: "Stretches of text the timing could not hear",
194
+ text: omissions.length === 0
195
+ ? "None."
196
+ : omissions.map((gap) => `At ${gap.start.toFixed(1)} s: ${gap.text}`).join("\n"),
197
+ },
198
+ {
199
+ label: "Words heard with low confidence",
200
+ text: `${String(unsure)} of ${String(words.length)}.`,
201
+ },
202
+ ]),
203
+ images: [],
204
+ itemFingerprint: timing.fingerprint,
205
+ };
206
+ }
207
+ case "shorts": {
208
+ const render = output(`${itemKey}:render`);
209
+ if (render === undefined)
210
+ throw missing("short", "Use Make this short again in Edit project → Shorts");
211
+ const number = Number(itemKey.slice("shorts:".length));
212
+ const pick = view.pieces.find((row) => row.key === "shorts:pick" && row.selected && row.piece.state === "done");
213
+ const title = pickedShortsOf(pick?.piece.payload)?.shorts.find((clip) => clip.number === number)?.title ??
214
+ "";
215
+ const stills = view.outputs
216
+ .filter((row) => row.workKey.startsWith(`${itemKey}:image:`) &&
217
+ row.selected &&
218
+ row.available &&
219
+ row.state === "ready")
220
+ .slice(0, 6);
221
+ if (stills.length === 0)
222
+ throw missing("short's images", "Use Make this short again in Edit project → Shorts");
223
+ const names = stills.map((_, index) => `image ${String(index + 1)} of the short`);
224
+ return {
225
+ request: request([{ label: "Title of the short", text: title }], names),
226
+ images: stills.map((row, index) => ({
227
+ path: path(row),
228
+ name: names[index] ?? "an image of the short",
229
+ })),
230
+ itemFingerprint: render.fingerprint,
231
+ };
232
+ }
233
+ }
234
+ }
235
+ function clip(value) {
236
+ return value.length <= textMax ? value : `${value.slice(0, textMax)}\n[... cut for length]`;
237
+ }
@@ -4,8 +4,10 @@ import { executeExportRecipe } from "./runtime-export.js";
4
4
  import { executeLocalRecipe } from "./runtime-local.js";
5
5
  import { pieceLabel } from "./runtime-piece-label.js";
6
6
  import { executeProviderRecipe } from "./runtime-provider.js";
7
+ import { executeReviewRecipe } from "./runtime-review.js";
7
8
  import { executeShortsRecipe } from "./runtime-shorts.js";
8
9
  import { executeSubtitleRecipe } from "./runtime-subtitles.js";
10
+ import { executeVoicesRecipe } from "./runtime-voices.js";
9
11
  import { executeYoutubeRecipe } from "./runtime-youtube.js";
10
12
  import { workPieces } from "./work-records.js";
11
13
  export async function runRevisionInvocation(deps, context, providers) {
@@ -22,17 +24,21 @@ export async function runRevisionInvocation(deps, context, providers) {
22
24
  ? await executeExportRecipe(deps, context, piece)
23
25
  : piece.key.startsWith("document:")
24
26
  ? await executeDocumentRecipe(deps, context, piece)
25
- : piece.key.startsWith("youtube:")
26
- ? await executeYoutubeRecipe(deps, context, providers, piece)
27
- : piece.key.startsWith("shorts:")
28
- ? await executeShortsRecipe(deps, context, providers, piece)
29
- : piece.key.startsWith("animate:")
30
- ? await executeAnimateRecipe(deps, context, providers, piece)
31
- : piece.input.kind === "llm" ||
32
- piece.input.kind === "tts" ||
33
- piece.input.kind === "image"
34
- ? await executeProviderRecipe(deps, context, providers, piece)
35
- : await executeLocalRecipe(deps, context, piece);
27
+ : piece.key.startsWith("review:")
28
+ ? await executeReviewRecipe(deps, context, providers, piece)
29
+ : piece.key.startsWith("voices:")
30
+ ? await executeVoicesRecipe(deps, context, piece)
31
+ : piece.key.startsWith("youtube:")
32
+ ? await executeYoutubeRecipe(deps, context, providers, piece)
33
+ : piece.key.startsWith("shorts:")
34
+ ? await executeShortsRecipe(deps, context, providers, piece)
35
+ : piece.key.startsWith("animate:")
36
+ ? await executeAnimateRecipe(deps, context, providers, piece)
37
+ : piece.input.kind === "llm" ||
38
+ piece.input.kind === "tts" ||
39
+ piece.input.kind === "image"
40
+ ? await executeProviderRecipe(deps, context, providers, piece)
41
+ : await executeLocalRecipe(deps, context, piece);
36
42
  if (outcome === "held")
37
43
  return outcome;
38
44
  }
@@ -1,7 +1,10 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { z } from "zod";
3
- import { usesShorts } from "../admission/rules.js";
3
+ import { withLanguage, wordTimingUnavailable } from "../../kernel/ports/languages.js";
4
+ import { usesFigureCards, usesShorts } from "../admission/rules.js";
5
+ import { captionBoldFont } from "../fonts/coverage.js";
4
6
  import { resolveBoldFont } from "../fonts/index.js";
7
+ import { masterGoal } from "../loudness/model.js";
5
8
  import { clipWords } from "../shorts/captions.js";
6
9
  import { effectiveClips, pickedShortsOf } from "../shorts/clips.js";
7
10
  import { defaultShortsPrompt, musicVolumeOf, shortImageCount, shortsSpeedOf, } from "../shorts/model.js";
@@ -11,15 +14,18 @@ import { renderShort } from "../shorts/render.js";
11
14
  import { allocateAsset, discardPreparedAssets, sealAsset, writeAsset } from "../storage/assets.js";
12
15
  import { outputPath, projectDir } from "../storage/layout.js";
13
16
  import { probeDurationMs } from "../video/ffmpeg.js";
17
+ import { usesVoices } from "../voices/model.js";
14
18
  import { sentencesText, transcriptSentences } from "../youtube/transcript.js";
15
19
  import { shortsPickKey } from "./recipe-shorts.js";
20
+ import { figureShots } from "./runtime-export-edit.js";
16
21
  import { exportSnapshot, revisionAudio } from "./runtime-export-inputs.js";
22
+ import { imageCall } from "./runtime-image.js";
17
23
  import { savedCatalogue } from "./runtime-plan.js";
18
24
  import { preparedResult, preparedTexts, publishResult } from "./runtime-publication.js";
19
25
  import { wordsSchema } from "./runtime-subtitles.js";
20
26
  // The Shorts step's four kinds of work (`recipe-shorts.ts`): the pick, each clip's image
21
27
  // prompts, each image, and each clip's render. Each is one piece of its own, so a failure
22
- // names the clip it belongs to and Retry stage redoes only what did not finish.
28
+ // names the clip it belongs to and Try again redoes only what did not finish.
23
29
  export async function executeShortsRecipe(deps, context, providers, piece) {
24
30
  if (deps.db
25
31
  .prepare("SELECT state FROM revision_work_pieces WHERE id=? AND work_id=?")
@@ -39,7 +45,7 @@ export async function executeShortsRecipe(deps, context, providers, piece) {
39
45
  throw setupError();
40
46
  const clip = savedClips(snapshot).find((one) => one.number === number);
41
47
  if (clip === undefined)
42
- throw new Error("The clip this short was cut from is no longer among the picked shorts. Use Pick different moments in Edit project → Shorts, then Retry stage.");
48
+ throw new Error("The clip this short was cut from is no longer among the picked shorts. Use Pick different moments in Edit project → Shorts, then Try again.");
43
49
  if (matched[2] === "prompts")
44
50
  return prompts(deps, context, providers, piece, snapshot, clip);
45
51
  if (matched[2] === "render")
@@ -57,10 +63,10 @@ async function pick(deps, context, providers, piece, snapshot) {
57
63
  throw setupError();
58
64
  const llm = config.llm;
59
65
  if (llm === undefined || llm.provider.trim() === "" || llm.model.trim() === "")
60
- throw new Error("Shorts need an AI text model to pick the clips, and none is chosen. Choose one in Edit project → Providers, then Retry stage.");
66
+ throw new Error("Shorts need an AI text model to pick the clips, and none is chosen. Choose one in Edit project → Providers, then Try again.");
61
67
  const sentences = transcriptSentences(timingWords(deps, context, view));
62
68
  if (sentences.length === 0)
63
- throw new Error("The narration's word timing holds no words, so there is nothing to cut shorts from. Use Re-run section on Video, then Retry stage.");
69
+ throw new Error("The narration's word timing holds no words, so there is nothing to cut shorts from. Use More → Render the video again in the Video section, then Try again.");
64
70
  const audio = await revisionAudio(deps, context, view);
65
71
  const durationSeconds = audio.reduce((sum, segment) => sum + segment.seconds, 0);
66
72
  const limits = {
@@ -80,13 +86,17 @@ async function pick(deps, context, providers, piece, snapshot) {
80
86
  count: shorts.count,
81
87
  minSeconds: shorts.minSeconds,
82
88
  maxSeconds: shorts.maxSeconds,
83
- sentences: sentencesText(sentences),
89
+ sentences: sentencesText(sentences, usesVoices(config) && config.voices !== undefined
90
+ ? Object.fromEntries(config.voices.speakers.map((speaker) => [speaker.id, speaker.name.trim()]))
91
+ : undefined),
84
92
  };
85
93
  const ask = llmCall(deps, context, piece, view, (text) => {
86
94
  const checked = checkPicks(text, sentences, limits);
87
95
  return checked.ok ? undefined : checked.reason;
88
96
  });
89
- const first = await providers.forPiece(piece.id).llm(ask(pickMessages(brief)));
97
+ const first = await providers
98
+ .forPiece(piece.id)
99
+ .llm(ask(withLanguage(pickMessages(brief), config.language)));
90
100
  if (!first.ok)
91
101
  return "held";
92
102
  let checked = checkPicks(first.value.text, sentences, limits);
@@ -99,7 +109,7 @@ async function pick(deps, context, providers, piece, snapshot) {
99
109
  return "held";
100
110
  const second = await providers
101
111
  .forPiece(piece.id)
102
- .llm(ask(pickRetryMessages(brief, first.value.text, checked.problems)));
112
+ .llm(ask(withLanguage(pickRetryMessages(brief, first.value.text, checked.problems), config.language)));
103
113
  if (!second.ok)
104
114
  return "held";
105
115
  const again = checkPicks(second.value.text, sentences, limits);
@@ -163,12 +173,9 @@ async function image(deps, context, providers, piece, clip, index) {
163
173
  const input = piece.input;
164
174
  if (input.kind !== "image")
165
175
  throw setupError();
166
- const made = await providers.forPiece(piece.id).image({
167
- provider: input.provider,
168
- model: input.model,
169
- prompt: input.prompt,
170
- aspect: input.aspect,
171
- });
176
+ const made = await providers
177
+ .forPiece(piece.id)
178
+ .image(imageCall(deps, context.work.projectId, input, `Short ${String(clip.number)} image ${String(index)}`));
172
179
  if (!made.ok)
173
180
  return "held";
174
181
  const asset = writeAsset(deps, context.work.projectId, made.value.mime === "image/jpeg" ? "short-image.jpg" : "short-image.png", made.value.bytes);
@@ -193,13 +200,27 @@ async function render(deps, context, piece, snapshot, clip) {
193
200
  .map((recipe) => {
194
201
  const row = view.outputs.find((one) => one.workKey === recipe.key && one.selected && one.available && one.state === "ready");
195
202
  if (row === undefined)
196
- throw new Error(`Short ${String(clip.number)} is missing one of its images. Retry stage to make it again; if it keeps happening, use Make this short again in Edit project → Shorts.`);
203
+ throw new Error(`Short ${String(clip.number)} is missing one of its images. Try again to make it again; if it keeps happening, use Make this short again in Edit project → Shorts.`);
197
204
  return outputPath(deps.paths, context.work.projectId, row.output.path);
198
205
  });
199
- const words = clipWords(timingWords(deps, context, view), clip.start, clip.end);
200
- const timeline = await revisionAudio(deps, context, view);
206
+ const allWords = timingWords(deps, context, view);
207
+ const words = clipWords(allWords, clip.start, clip.end);
208
+ // The upright cards whose descriptions fall in the clip, on the clip's own timeline.
209
+ const figures = usesFigureCards(config)
210
+ ? figureShots(deps, context, view, "9:16", allWords).flatMap((figure) => figure.end <= clip.start || figure.start >= clip.end
211
+ ? []
212
+ : [
213
+ {
214
+ path: figure.path,
215
+ start: Math.max(0, figure.start - clip.start),
216
+ end: Math.min(clip.end, figure.end) - clip.start,
217
+ },
218
+ ])
219
+ : [];
220
+ const timeline = await revisionAudio(deps, context, view, { levelled: true });
201
221
  // The bundled Barlow is drawn in its own Bold face; any other font is emboldened.
202
- const font = await resolveBoldFont(deps.paths, config.subtitles?.fontId ?? "default");
222
+ // In another language, a font that has its letters (`fonts/coverage.ts`).
223
+ const font = await captionBoldFont(deps.paths, config.subtitles?.fontId ?? "default", config.language);
203
224
  const shorts = config.shorts;
204
225
  const speed = shorts === undefined ? 1 : shortsSpeedOf(shorts);
205
226
  const music = await backgroundMusic(deps, context, view);
@@ -209,8 +230,9 @@ async function render(deps, context, piece, snapshot, clip) {
209
230
  const prepared = [];
210
231
  const seconds = (clip.end - clip.start) / speed;
211
232
  try {
212
- await renderShort({
233
+ const master = await renderShort({
213
234
  bin: deps.ffmpeg,
235
+ master: masterGoal(config, "video"),
214
236
  timeline,
215
237
  start: clip.start,
216
238
  end: clip.end,
@@ -219,8 +241,10 @@ async function render(deps, context, piece, snapshot, clip) {
219
241
  motionStyle: config.motionStyle,
220
242
  zoomPercent: config.zoomPercent,
221
243
  words,
244
+ ...(wordTimingUnavailable(config.language) === undefined ? {} : { wordByWord: false }),
222
245
  font,
223
246
  ...(shorts?.titleOnScreen === true ? { title: clip.title } : {}),
247
+ ...(figures.length === 0 ? {} : { figures }),
224
248
  speed,
225
249
  music,
226
250
  output: pending.absolutePath,
@@ -241,6 +265,7 @@ async function render(deps, context, piece, snapshot, clip) {
241
265
  prepared.push(preparedResult(deps, context, piece, "short_video", asset, durationMs, {
242
266
  short: clip.number,
243
267
  sentences: [clip.first, clip.last],
268
+ ...(master === undefined ? {} : { master }),
244
269
  }));
245
270
  await publishResult(deps, context, piece, prepared, { short: clip.number, start: clip.start, end: clip.end, durationMs }, asset);
246
271
  // One per rendered short, with no stage: stage "video" would read as a finished video.
@@ -273,7 +298,7 @@ async function backgroundMusic(deps, context, view) {
273
298
  const shorts = view.revision.config.shorts;
274
299
  if (assetId === undefined || shorts === undefined)
275
300
  return undefined;
276
- const fix = "Choose another file under Background music in Edit project → Shorts, or remove the music, then Retry stage.";
301
+ const fix = "Choose another file under Background music in Edit project → Shorts, or remove the music, then Try again.";
277
302
  const row = z
278
303
  .object({ path: z.string() })
279
304
  .safeParse(deps.db
@@ -294,14 +319,14 @@ async function backgroundMusic(deps, context, view) {
294
319
  throw new Error(`The shorts' background music file holds no sound. ${fix}`);
295
320
  return { path, volume: musicVolumeOf(shorts) };
296
321
  }
297
- function timingWords(deps, context, view) {
322
+ export function timingWords(deps, context, view) {
298
323
  const timing = view.outputs.find((row) => row.selected &&
299
324
  row.available &&
300
325
  row.state === "ready" &&
301
326
  row.workKey === "subtitles:timing" &&
302
327
  row.output.role === "subtitle_words");
303
328
  if (timing === undefined)
304
- throw new Error("The narration's word timing, which the shorts are cut and captioned from, is missing. Use Re-run section on Video, then Retry stage.");
329
+ throw new Error("The narration's word timing, which the shorts are cut and captioned from, is missing. Use More → Render the video again in the Video section, then Try again.");
305
330
  return wordsSchema.parse(JSON.parse(readFileSync(outputPath(deps.paths, context.work.projectId, timing.output.path), "utf8"))).words;
306
331
  }
307
332
  // The project's text model, with the thinking settings saved for this step's catalogue.
@@ -326,5 +351,5 @@ function imageIndex(key) {
326
351
  return Number(key.split(":").at(-1));
327
352
  }
328
353
  function setupError() {
329
- return new Error("Slopify hit an internal error (a Shorts step was set up wrongly). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
354
+ return new Error("Slopify hit an internal error (a Shorts step was set up wrongly). Try again; if it happens again, use Download diagnostics in Settings and report it.");
330
355
  }
@@ -2,6 +2,7 @@ import { z } from "zod";
2
2
  import { stageKinds, stageStates } from "../../kernel/pipeline.js";
3
3
  import { sourceOf } from "../admission/model.js";
4
4
  import { usesShorts, usesYoutubeDescription } from "../admission/rules.js";
5
+ import { reviewsNarration } from "../reviews/rules.js";
5
6
  import { currentRevisionId } from "../revisions/repo.js";
6
7
  import { getRevisionView } from "../revisions/view.js";
7
8
  import { settleScheduleRunsForProject } from "../schedules/repo.js";
@@ -40,7 +41,8 @@ export function executionStages(deps, projectId) {
40
41
  view?.revision.content.subtitleCues !== undefined &&
41
42
  !usesYoutubeDescription(view.revision.config) &&
42
43
  !usesShorts(view.revision.config) &&
43
- !editNeedsTiming(view.revision.config))
44
+ !editNeedsTiming(view.revision.config) &&
45
+ !reviewsNarration(view.revision.config))
44
46
  return false;
45
47
  if (piece.input.kind === "deferred" && piece.key.endsWith(":future")) {
46
48
  const cacheKey = String(row.id);
@@ -78,10 +80,13 @@ export function executionStages(deps, projectId) {
78
80
  }
79
81
  export function invocationReady(deps, work) {
80
82
  const row = deps.db
81
- .prepare("SELECT recipe_context FROM revision_work WHERE id=? AND project_id=? AND revision_id=?")
83
+ .prepare("SELECT recipe_context,retry_at FROM revision_work WHERE id=? AND project_id=? AND revision_id=?")
82
84
  .get(work.workId, work.projectId, work.revisionId);
83
85
  if (row?.recipe_context === null || row === undefined)
84
86
  return false;
87
+ // Waiting out a failure time can fix: `dueRetries` clears the wait when it runs out.
88
+ if (row.retry_at !== null)
89
+ return false;
85
90
  const view = executionView(deps, work.projectId, work.revisionId);
86
91
  if (view === undefined)
87
92
  return false;
@@ -129,6 +134,9 @@ export function executionStandings(deps, projectId, derived) {
129
134
  return retained ? { ...entry, state: "done" } : entry;
130
135
  });
131
136
  let state = source === "off" ? "skipped" : source === "provide" ? "provided" : "pending";
137
+ const retryAt = group.some((entry) => entry.state === "failed")
138
+ ? null
139
+ : latestRetry(deps, group);
132
140
  if (group.some((entry) => entry.state === "running"))
133
141
  state = "running";
134
142
  else if (group.some((entry) => entry.state === "failed"))
@@ -141,9 +149,23 @@ export function executionStandings(deps, projectId, derived) {
141
149
  ? "provided"
142
150
  : "done"
143
151
  : "pending";
144
- return { kind, state };
152
+ return state === "pending" && retryAt !== null ? { kind, state, retryAt } : { kind, state };
145
153
  });
146
154
  }
155
+ // The latest wait among a stage's steps that went back to wait after a failure time can fix.
156
+ function latestRetry(deps, group) {
157
+ let latest = null;
158
+ for (const entry of group) {
159
+ if (entry.state !== "pending")
160
+ continue;
161
+ const at = deps.db
162
+ .prepare("SELECT retry_at FROM revision_work WHERE id=?")
163
+ .get(entry.work.workId)?.retry_at;
164
+ if (typeof at === "string" && (latest === null || at > latest))
165
+ latest = at;
166
+ }
167
+ return latest;
168
+ }
147
169
  export function projectStandings(deps, projectId) {
148
170
  // One derivation serves every stage: each builds the whole execution plan.
149
171
  const work = executionStages(deps, projectId);
@@ -159,15 +181,19 @@ export function projectStandings(deps, projectId) {
159
181
  .get(entry.work.workId);
160
182
  return total + fraction(progress?.progress_current, progress?.progress_total);
161
183
  }, 0);
162
- const failed = all.find((work) => work.state === "failed");
184
+ // A failed step, or else one waiting to run again, says why on the stage row.
185
+ const failed = all.find((work) => work.state === "failed") ??
186
+ (stage.retryAt === undefined || stage.retryAt === null
187
+ ? undefined
188
+ : all.find((work) => work.state === "pending"));
163
189
  const failure = failed === undefined
164
- ? null
165
- : (deps.db
166
- .prepare("SELECT failure_reason FROM revision_work WHERE id=?")
167
- .get(failed.work.workId)?.failure_reason ?? null);
190
+ ? undefined
191
+ : deps.db
192
+ .prepare("SELECT failure_reason,failure_kind FROM revision_work WHERE id=?")
193
+ .get(failed.work.workId);
168
194
  deps.db
169
- .prepare("UPDATE stages SET state=?,failure_reason=?,progress_current=?,progress_total=?,finished_at=CASE WHEN ?='done' THEN ? ELSE NULL END WHERE project_id=? AND kind=?")
170
- .run(stage.state, failure, invocations.length === 0 ? null : current, invocations.length === 0 ? null : invocations.length, stage.state, deps.clock.now().toISOString(), projectId, stage.kind);
195
+ .prepare("UPDATE stages SET state=?,failure_reason=?,failure_kind=?,retry_at=?,progress_current=?,progress_total=?,finished_at=CASE WHEN ?='done' THEN ? ELSE NULL END WHERE project_id=? AND kind=?")
196
+ .run(stage.state, textOf(failure?.failure_reason), textOf(failure?.failure_kind), stage.retryAt ?? null, invocations.length === 0 ? null : current, invocations.length === 0 ? null : invocations.length, stage.state, deps.clock.now().toISOString(), projectId, stage.kind);
171
197
  }
172
198
  settleScheduleRunsForProject(deps.db, projectId, deps.clock.now().toISOString());
173
199
  }
@@ -225,6 +251,9 @@ const spokenKey = /^audio:(?:intro|body|outro)(?::(?!concat$|future$).+)?$/;
225
251
  function spoken(deps, workId) {
226
252
  return workPieces(deps.db, workId).some((piece) => spokenKey.test(piece.key));
227
253
  }
254
+ function textOf(value) {
255
+ return typeof value === "string" ? value : null;
256
+ }
228
257
  function fraction(current, total) {
229
258
  if (typeof current !== "number" || typeof total !== "number" || total <= 0)
230
259
  return 0;