@gentbajko/slopify 2.5.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 (446) 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 +3 -0
  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 +44 -5
  20. package/dist/adapters/host-cli/transport.js +7 -5
  21. package/dist/adapters/image/bytes.js +2 -2
  22. package/dist/adapters/image/codex-output.js +1 -1
  23. package/dist/adapters/image/codex.js +87 -22
  24. package/dist/adapters/image/fal.js +13 -5
  25. package/dist/adapters/image/google.js +11 -8
  26. package/dist/adapters/image/openai.js +14 -7
  27. package/dist/adapters/image/reference.js +34 -0
  28. package/dist/adapters/image/replicate.js +7 -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.js +99 -12
  35. package/dist/adapters/llm/document-workspace.js +2 -2
  36. package/dist/adapters/llm/gemini-workspace.js +1 -1
  37. package/dist/adapters/llm/gemini.js +16 -3
  38. package/dist/adapters/llm/image-workspace.js +58 -0
  39. package/dist/adapters/llm/openrouter.js +11 -3
  40. package/dist/adapters/llm/run-cli.js +12 -7
  41. package/dist/adapters/tts/cartesia.js +10 -0
  42. package/dist/adapters/tts/elevenlabs.js +31 -9
  43. package/dist/adapters/tts/inworld-async.js +9 -5
  44. package/dist/adapters/tts/inworld.js +17 -3
  45. package/dist/adapters/tts/openai.js +3 -0
  46. package/dist/adapters/tts/voice-languages.js +80 -0
  47. package/dist/assets/fonts/noto/NotoSans-Regular.ttf +0 -0
  48. package/dist/assets/fonts/noto/NotoSansArabic-Regular.ttf +0 -0
  49. package/dist/assets/fonts/noto/NotoSansDevanagari-Regular.ttf +0 -0
  50. package/dist/assets/fonts/noto/NotoSansHebrew-Regular.ttf +0 -0
  51. package/dist/assets/fonts/noto/NotoSansThai-Regular.ttf +0 -0
  52. package/dist/assets/fonts/noto/OFL.txt +93 -0
  53. package/dist/assets/fonts/noto/SOURCE.txt +18 -0
  54. package/dist/assets/sample/sample-audiobook.tar +0 -0
  55. package/dist/assets/sample/sample-podcast.tar +0 -0
  56. package/dist/assets/sample/sample-project.tar +0 -0
  57. package/dist/catalog/merge.js +132 -0
  58. package/dist/catalog/registry.js +13 -13
  59. package/dist/catalog/schema.js +2 -0
  60. package/dist/catalog/store.js +147 -8
  61. package/dist/compose.yaml +71 -0
  62. package/dist/edge/autostart/docker-record.js +32 -0
  63. package/dist/edge/autostart/docker.js +50 -0
  64. package/dist/edge/autostart/index.js +78 -0
  65. package/dist/edge/autostart/launcher.js +176 -0
  66. package/dist/edge/autostart/model.js +3 -0
  67. package/dist/edge/autostart/native.js +210 -0
  68. package/dist/edge/autostart/prompt.js +66 -0
  69. package/dist/edge/autostart/service.js +81 -0
  70. package/dist/edge/cli.js +57 -51
  71. package/dist/edge/docker-install/activation.js +56 -0
  72. package/dist/edge/docker-install/apply.js +458 -0
  73. package/dist/edge/docker-install/engine.js +362 -0
  74. package/dist/edge/docker-install/run.js +154 -0
  75. package/dist/edge/docker-install/state.js +146 -0
  76. package/dist/edge/{docker-projects → docker-install}/volume.js +1 -5
  77. package/dist/edge/docker.js +14 -22
  78. package/dist/edge/host-cli.js +1 -1
  79. package/dist/edge/http/actions.js +32 -10
  80. package/dist/edge/http/app.js +58 -2
  81. package/dist/edge/http/auditions.js +70 -0
  82. package/dist/edge/http/autostart.js +37 -0
  83. package/dist/edge/http/channel-memory.js +92 -0
  84. package/dist/edge/http/channels.js +128 -0
  85. package/dist/edge/http/entries.js +22 -3
  86. package/dist/edge/http/files.js +1 -1
  87. package/dist/edge/http/folder-location.js +1 -1
  88. package/dist/edge/http/home.js +39 -0
  89. package/dist/edge/http/host-cli.js +29 -5
  90. package/dist/edge/http/narration-peaks.js +76 -0
  91. package/dist/edge/http/onboarding.js +152 -0
  92. package/dist/edge/http/open-folder.js +1 -1
  93. package/dist/edge/http/patch-notes.js +45 -0
  94. package/dist/edge/http/planning.js +4 -2
  95. package/dist/edge/http/project-create.js +87 -0
  96. package/dist/edge/http/projects.js +31 -99
  97. package/dist/edge/http/prompts.js +40 -3
  98. package/dist/edge/http/pronunciations.js +23 -4
  99. package/dist/edge/http/providers.js +86 -0
  100. package/dist/edge/http/reviews.js +121 -0
  101. package/dist/edge/http/revision-files.js +1 -1
  102. package/dist/edge/http/revisions.js +1 -1
  103. package/dist/edge/http/run-cost.js +25 -0
  104. package/dist/edge/http/schedules.js +111 -6
  105. package/dist/edge/http/settings.js +84 -6
  106. package/dist/edge/http/storage-files.js +50 -0
  107. package/dist/edge/http/storage.js +29 -2
  108. package/dist/edge/http/studio.js +207 -0
  109. package/dist/edge/http/style-preview.js +100 -0
  110. package/dist/edge/http/subtitles.js +1 -1
  111. package/dist/edge/http/trash.js +69 -0
  112. package/dist/edge/http/update.js +24 -3
  113. package/dist/edge/http/whats-new.js +15 -0
  114. package/dist/edge/http/youtube-edits.js +83 -0
  115. package/dist/edge/native-update.js +62 -0
  116. package/dist/host-cli/open-folder.js +10 -10
  117. package/dist/host-cli/runtime.js +9 -1
  118. package/dist/kernel/db/migrations/0025-automatic-reviews.sql +40 -0
  119. package/dist/kernel/db/migrations/0026-schedule-topic-generation.sql +23 -0
  120. package/dist/kernel/db/migrations/0027-run-cost.sql +58 -0
  121. package/dist/kernel/db/migrations/0030-automatic-retries.sql +18 -0
  122. package/dist/kernel/db/migrations/0031-prompt-history-and-description-edits.sql +33 -0
  123. package/dist/kernel/db/migrations/0032-channels.sql +65 -0
  124. package/dist/kernel/db/migrations/0034-script-prompts.sql +23 -0
  125. package/dist/kernel/db/migrations/0035-narration-aliases.sql +13 -0
  126. package/dist/kernel/db/migrations/0037-project-uploads.sql +8 -0
  127. package/dist/kernel/db/migrations/0038-voice-languages.sql +7 -0
  128. package/dist/kernel/db/migrations/0039-channel-essentials.sql +64 -0
  129. package/dist/kernel/db/migrations/0040-standalone-usage.sql +35 -0
  130. package/dist/kernel/paths.js +27 -6
  131. package/dist/kernel/pipeline.js +4 -1
  132. package/dist/kernel/ports/host-cli.js +122 -14
  133. package/dist/kernel/ports/key-probe.js +1 -0
  134. package/dist/kernel/ports/languages.js +157 -0
  135. package/dist/kernel/ports/model.js +5 -2
  136. package/dist/kernel/ports/narration-aliases.js +60 -0
  137. package/dist/kernel/ports/plan-limits.js +36 -0
  138. package/dist/kernel/runner/attempt.js +17 -8
  139. package/dist/kernel/runner/audition.js +36 -0
  140. package/dist/kernel/runner/graph.js +24 -5
  141. package/dist/kernel/runner/index.js +35 -5
  142. package/dist/kernel/runner/meter.js +1 -0
  143. package/dist/kernel/runner/providers.js +142 -7
  144. package/dist/kernel/runner/retry-policy.js +43 -0
  145. package/dist/kernel/runner/standalone.js +134 -0
  146. package/dist/kernel/runner/work-authority.js +28 -2
  147. package/dist/main.js +272 -16
  148. package/dist/patch-notes/0.4.2-to-1.0.md +93 -0
  149. package/dist/patch-notes/1.0-to-2.0.md +74 -0
  150. package/dist/patch-notes/2.0-to-3.0.md +257 -0
  151. package/dist/patch-notes/3.0.0.md +70 -0
  152. package/dist/patch-notes/index.json +26 -0
  153. package/dist/slices/admission/model.js +27 -0
  154. package/dist/slices/admission/repo.js +24 -4
  155. package/dist/slices/admission/rules.js +79 -1
  156. package/dist/slices/admission/schema.js +58 -1
  157. package/dist/slices/admission/short-mode.js +47 -0
  158. package/dist/slices/admission/start.js +41 -5
  159. package/dist/slices/article/continuation.js +2 -2
  160. package/dist/slices/article/provided-entries.js +2 -2
  161. package/dist/slices/article/run.js +6 -2
  162. package/dist/slices/article/segments.js +3 -3
  163. package/dist/slices/backups/folder.js +14 -6
  164. package/dist/slices/batch/index.js +7 -2
  165. package/dist/slices/cancel/index.js +1 -1
  166. package/dist/slices/channels/cast-images.js +110 -0
  167. package/dist/slices/channels/cast-match.js +32 -0
  168. package/dist/slices/channels/images.js +19 -0
  169. package/dist/slices/channels/model.js +5 -0
  170. package/dist/slices/channels/rebrand.js +100 -0
  171. package/dist/slices/channels/repo.js +218 -0
  172. package/dist/slices/channels/runs.js +129 -0
  173. package/dist/slices/channels/schema.js +129 -0
  174. package/dist/slices/channels/service.js +180 -0
  175. package/dist/slices/channels/studio-csv.js +110 -0
  176. package/dist/slices/channels/videos.js +120 -0
  177. package/dist/slices/checkpoints/recovery.js +10 -0
  178. package/dist/slices/control/index.js +2 -1
  179. package/dist/slices/document/fonts.js +1 -1
  180. package/dist/slices/document/render.js +1 -1
  181. package/dist/slices/episodes/related.js +90 -0
  182. package/dist/slices/episodes/repo.js +60 -0
  183. package/dist/slices/episodes/service.js +58 -0
  184. package/dist/slices/episodes/summarize.js +170 -0
  185. package/dist/slices/estimate/index.js +109 -28
  186. package/dist/slices/estimate/requests.js +60 -2
  187. package/dist/slices/fixes/rules.js +73 -0
  188. package/dist/slices/fonts/coverage.js +82 -0
  189. package/dist/slices/fonts/sfnt.js +74 -0
  190. package/dist/slices/images/run.js +8 -5
  191. package/dist/slices/images/scale.js +84 -0
  192. package/dist/slices/library/diff.js +122 -0
  193. package/dist/slices/library/history.js +100 -0
  194. package/dist/slices/library/model.js +3 -0
  195. package/dist/slices/library/photorealistic.js +49 -0
  196. package/dist/slices/library/repo.js +19 -14
  197. package/dist/slices/library/save.js +62 -12
  198. package/dist/slices/library/slots.js +7 -1
  199. package/dist/slices/library/used-by.js +153 -0
  200. package/dist/slices/loudness/level-pieces.js +41 -0
  201. package/dist/slices/loudness/loudnorm.js +164 -0
  202. package/dist/slices/loudness/model.js +153 -0
  203. package/dist/slices/model-upkeep/model.js +10 -0
  204. package/dist/slices/model-upkeep/switch.js +217 -0
  205. package/dist/slices/model-upkeep/usage.js +183 -0
  206. package/dist/slices/narration/aliases-library.js +31 -0
  207. package/dist/slices/narration/aliases-schema.js +53 -0
  208. package/dist/slices/narration/aliases.js +49 -0
  209. package/dist/slices/narration/blocks.js +352 -0
  210. package/dist/slices/narration/chunk.js +19 -7
  211. package/dist/slices/narration/concat.js +2 -2
  212. package/dist/slices/narration/describe.js +45 -0
  213. package/dist/slices/narration/pauses-model.js +63 -0
  214. package/dist/slices/narration/pauses.js +269 -0
  215. package/dist/slices/narration/peaks-model.js +1 -0
  216. package/dist/slices/narration/peaks.js +94 -0
  217. package/dist/slices/narration/preparation.js +21 -3
  218. package/dist/slices/narration/pronunciation-chunks.js +3 -3
  219. package/dist/slices/narration/pronunciation.js +72 -25
  220. package/dist/slices/narration/run.js +6 -6
  221. package/dist/slices/narration/shared-glossary.js +1 -1
  222. package/dist/slices/narration/steering.js +15 -4
  223. package/dist/slices/notifications/notifier.js +21 -3
  224. package/dist/slices/notifications/rules.js +22 -1
  225. package/dist/slices/onboarding/copy.js +143 -0
  226. package/dist/slices/onboarding/first-run.js +46 -0
  227. package/dist/slices/onboarding/install.js +119 -0
  228. package/dist/slices/onboarding/model.js +64 -0
  229. package/dist/slices/onboarding/packs.js +491 -0
  230. package/dist/slices/onboarding/quick-short.js +129 -0
  231. package/dist/slices/onboarding/sample.js +132 -0
  232. package/dist/slices/onboarding/state.js +68 -0
  233. package/dist/slices/onboarding/template.js +73 -0
  234. package/dist/slices/patch-notes/library.js +73 -0
  235. package/dist/slices/patch-notes/seen.js +77 -0
  236. package/dist/slices/play-drafts/convert.js +125 -6
  237. package/dist/slices/play-drafts/readiness.js +3 -0
  238. package/dist/slices/play-drafts/repo.js +2 -0
  239. package/dist/slices/play-drafts/review-inputs.js +20 -3
  240. package/dist/slices/play-drafts/schema.js +89 -0
  241. package/dist/slices/play-drafts/service.js +4 -1
  242. package/dist/slices/play-drafts/start.js +43 -18
  243. package/dist/slices/project-templates/from-project.js +50 -4
  244. package/dist/slices/project-templates/one-off.js +26 -0
  245. package/dist/slices/project-templates/repo.js +2 -1
  246. package/dist/slices/project-templates/service.js +15 -4
  247. package/dist/slices/project-templates/setup.js +18 -2
  248. package/dist/slices/rebuild/admission-repo.js +1 -1
  249. package/dist/slices/rebuild/model.js +6 -0
  250. package/dist/slices/rebuild/preview-details.js +46 -3
  251. package/dist/slices/rebuild/preview-plan.js +13 -6
  252. package/dist/slices/rebuild/recipe-audio-parts.js +40 -7
  253. package/dist/slices/rebuild/recipe-audio.js +66 -12
  254. package/dist/slices/rebuild/recipe-build.js +8 -4
  255. package/dist/slices/rebuild/recipe-cast.js +18 -0
  256. package/dist/slices/rebuild/recipe-checkpoints.js +11 -9
  257. package/dist/slices/rebuild/recipe-describe.js +93 -0
  258. package/dist/slices/rebuild/recipe-edit.js +13 -1
  259. package/dist/slices/rebuild/recipe-exports.js +62 -8
  260. package/dist/slices/rebuild/recipe-input-schema.js +35 -0
  261. package/dist/slices/rebuild/recipe-legacy.js +9 -0
  262. package/dist/slices/rebuild/recipe-loudness.js +45 -0
  263. package/dist/slices/rebuild/recipe-model.js +25 -6
  264. package/dist/slices/rebuild/recipe-pauses.js +7 -0
  265. package/dist/slices/rebuild/recipe-preparation.js +3 -3
  266. package/dist/slices/rebuild/recipe-provider-choice.js +11 -1
  267. package/dist/slices/rebuild/recipe-reference.js +15 -1
  268. package/dist/slices/rebuild/recipe-reviews.js +117 -0
  269. package/dist/slices/rebuild/recipe-save.js +4 -1
  270. package/dist/slices/rebuild/recipe-shorts.js +23 -3
  271. package/dist/slices/rebuild/recipe-text.js +114 -19
  272. package/dist/slices/rebuild/recipe-validation.js +14 -2
  273. package/dist/slices/rebuild/recipe-visual.js +96 -33
  274. package/dist/slices/rebuild/recipe-voices.js +286 -0
  275. package/dist/slices/rebuild/recipe-work.js +77 -6
  276. package/dist/slices/rebuild/recipe-youtube.js +2 -0
  277. package/dist/slices/rebuild/recovery-conflict.js +7 -3
  278. package/dist/slices/rebuild/recovery-model.js +3 -0
  279. package/dist/slices/rebuild/recovery-selection.js +28 -2
  280. package/dist/slices/rebuild/recovery.js +35 -17
  281. package/dist/slices/rebuild/repo.js +3 -3
  282. package/dist/slices/rebuild/retry.js +18 -0
  283. package/dist/slices/rebuild/review-redo.js +110 -0
  284. package/dist/slices/rebuild/runtime-actions.js +5 -0
  285. package/dist/slices/rebuild/runtime-admission.js +4 -1
  286. package/dist/slices/rebuild/runtime-animate.js +4 -4
  287. package/dist/slices/rebuild/runtime-article.js +5 -5
  288. package/dist/slices/rebuild/runtime-document.js +4 -4
  289. package/dist/slices/rebuild/runtime-export-bed.js +43 -0
  290. package/dist/slices/rebuild/runtime-export-edit.js +70 -11
  291. package/dist/slices/rebuild/runtime-export-inputs.js +52 -10
  292. package/dist/slices/rebuild/runtime-export-short.js +85 -0
  293. package/dist/slices/rebuild/runtime-export.js +84 -19
  294. package/dist/slices/rebuild/runtime-figure-card.js +87 -0
  295. package/dist/slices/rebuild/runtime-image.js +21 -2
  296. package/dist/slices/rebuild/runtime-local.js +148 -24
  297. package/dist/slices/rebuild/runtime-materialize.js +8 -0
  298. package/dist/slices/rebuild/runtime-narration-reuse.js +2 -1
  299. package/dist/slices/rebuild/runtime-narration-text.js +7 -7
  300. package/dist/slices/rebuild/runtime-pictures.js +28 -0
  301. package/dist/slices/rebuild/runtime-piece-label.js +27 -0
  302. package/dist/slices/rebuild/runtime-provider.js +126 -18
  303. package/dist/slices/rebuild/runtime-publication.js +21 -5
  304. package/dist/slices/rebuild/runtime-review.js +237 -0
  305. package/dist/slices/rebuild/runtime-run.js +17 -11
  306. package/dist/slices/rebuild/runtime-shorts.js +44 -17
  307. package/dist/slices/rebuild/runtime-store.js +39 -10
  308. package/dist/slices/rebuild/runtime-subtitles.js +72 -19
  309. package/dist/slices/rebuild/runtime-voices.js +103 -0
  310. package/dist/slices/rebuild/runtime-youtube.js +7 -6
  311. package/dist/slices/rebuild/soften.js +49 -0
  312. package/dist/slices/rebuild/transition-repo.js +9 -2
  313. package/dist/slices/research/run.js +4 -4
  314. package/dist/slices/research/synthesis.js +2 -2
  315. package/dist/slices/reviews/model.js +105 -0
  316. package/dist/slices/reviews/outcome.js +22 -0
  317. package/dist/slices/reviews/repo.js +106 -0
  318. package/dist/slices/reviews/rules.js +63 -0
  319. package/dist/slices/reviews/verdict.js +71 -0
  320. package/dist/slices/reviews/view.js +30 -0
  321. package/dist/slices/revisions/adopt-content.js +5 -2
  322. package/dist/slices/revisions/adopt.js +3 -1
  323. package/dist/slices/revisions/downloads.js +7 -1
  324. package/dist/slices/revisions/image-plan.js +183 -0
  325. package/dist/slices/revisions/mutation-assets.js +10 -0
  326. package/dist/slices/revisions/mutation-cues.js +71 -0
  327. package/dist/slices/revisions/mutation-request.js +2 -1
  328. package/dist/slices/revisions/mutations.js +97 -3
  329. package/dist/slices/revisions/schema.js +11 -1
  330. package/dist/slices/run-cost/limits.js +128 -0
  331. package/dist/slices/run-cost/meter.js +47 -0
  332. package/dist/slices/run-cost/panel.js +234 -0
  333. package/dist/slices/run-cost/pricing.js +92 -0
  334. package/dist/slices/run-cost/week.js +108 -0
  335. package/dist/slices/schedules/agenda.js +158 -0
  336. package/dist/slices/schedules/repo.js +58 -18
  337. package/dist/slices/schedules/scheduler.js +67 -16
  338. package/dist/slices/schedules/schema.js +138 -1
  339. package/dist/slices/schedules/service.js +43 -4
  340. package/dist/slices/schedules/similar.js +90 -0
  341. package/dist/slices/schedules/topic-list.js +163 -0
  342. package/dist/slices/schedules/topics.js +422 -0
  343. package/dist/slices/settings/first-run.js +102 -0
  344. package/dist/slices/settings/health.js +190 -0
  345. package/dist/slices/settings/key-guides.js +128 -0
  346. package/dist/slices/settings/key-test.js +85 -0
  347. package/dist/slices/settings/model.js +6 -1
  348. package/dist/slices/settings/playback.js +15 -0
  349. package/dist/slices/settings/repo.js +17 -3
  350. package/dist/slices/settings/voices.js +64 -4
  351. package/dist/slices/settings/whats-new.js +64 -0
  352. package/dist/slices/shorts/captions.js +9 -0
  353. package/dist/slices/shorts/pick.js +3 -3
  354. package/dist/slices/shorts/prompts.js +1 -1
  355. package/dist/slices/shorts/render.js +54 -5
  356. package/dist/slices/storage/asset-name.js +3 -0
  357. package/dist/slices/storage/backup-export.js +48 -9
  358. package/dist/slices/storage/backup-format.js +38 -1
  359. package/dist/slices/storage/backup-import.js +191 -19
  360. package/dist/slices/storage/delete-project.js +7 -4
  361. package/dist/slices/storage/documents.js +79 -0
  362. package/dist/slices/storage/files-location.js +531 -0
  363. package/dist/slices/storage/layout.js +14 -4
  364. package/dist/slices/storage/model.js +11 -0
  365. package/dist/slices/storage/portable.js +92 -12
  366. package/dist/slices/storage/reconcile.js +14 -1
  367. package/dist/slices/storage/repo.js +4 -1
  368. package/dist/slices/storage/schema.js +11 -0
  369. package/dist/slices/storage/trim.js +146 -0
  370. package/dist/slices/studio/disclosure.js +74 -0
  371. package/dist/slices/studio/model.js +28 -0
  372. package/dist/slices/studio/pack.js +209 -0
  373. package/dist/slices/studio/settings.js +133 -0
  374. package/dist/slices/style-preview/images.js +61 -0
  375. package/dist/slices/style-preview/render.js +161 -0
  376. package/dist/slices/style-preview/schema.js +45 -0
  377. package/dist/slices/style-preview/service.js +93 -0
  378. package/dist/slices/style-preview/settings.js +89 -0
  379. package/dist/slices/subtitles/captions.js +42 -9
  380. package/dist/slices/subtitles/model.js +22 -0
  381. package/dist/slices/subtitles/prepare.js +27 -16
  382. package/dist/slices/subtitles/transcript.js +1 -1
  383. package/dist/slices/thumbnail/run.js +7 -7
  384. package/dist/slices/trash/model.js +25 -0
  385. package/dist/slices/trash/service.js +273 -0
  386. package/dist/slices/uploads/repo.js +20 -0
  387. package/dist/slices/video/ambient-bed-schema.js +15 -0
  388. package/dist/slices/video/ambient-bed.js +95 -0
  389. package/dist/slices/video/ambient-mix.js +72 -0
  390. package/dist/slices/video/audio-inputs.js +2 -2
  391. package/dist/slices/video/cards.js +5 -2
  392. package/dist/slices/video/edit-list.js +23 -2
  393. package/dist/slices/video/edit-settings.js +13 -3
  394. package/dist/slices/video/ffmpeg.js +63 -9
  395. package/dist/slices/video/figure-card.js +492 -0
  396. package/dist/slices/video/figure-spans.js +51 -0
  397. package/dist/slices/video/plan.js +125 -13
  398. package/dist/slices/video/run.js +14 -12
  399. package/dist/slices/video/slideshow.js +39 -4
  400. package/dist/slices/voices/attribution.js +69 -0
  401. package/dist/slices/voices/audio-files.js +82 -0
  402. package/dist/slices/voices/audition.js +25 -0
  403. package/dist/slices/voices/cast.js +56 -0
  404. package/dist/slices/voices/grouping.js +49 -0
  405. package/dist/slices/voices/join.js +35 -0
  406. package/dist/slices/voices/languages.js +25 -0
  407. package/dist/slices/voices/model.js +179 -0
  408. package/dist/slices/voices/palette.js +26 -0
  409. package/dist/slices/voices/panel.js +91 -0
  410. package/dist/slices/voices/portraits.js +55 -0
  411. package/dist/slices/voices/script.js +136 -0
  412. package/dist/slices/voices/timing.js +58 -0
  413. package/dist/slices/youtube/answer.js +1 -1
  414. package/dist/slices/youtube/chapters.js +126 -0
  415. package/dist/slices/youtube/edits-repo.js +69 -0
  416. package/dist/slices/youtube/edits.js +89 -0
  417. package/dist/slices/youtube/placeholders.js +98 -0
  418. package/dist/slices/youtube/transcript.js +11 -4
  419. package/dist/updater/service.js +66 -21
  420. package/dist/web/assets/index-CUA2sUxU.css +1 -0
  421. package/dist/web/assets/index-CX590kE-.js +303 -0
  422. package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  423. package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  424. package/dist/web/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  425. package/dist/web/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  426. package/dist/web/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  427. package/dist/web/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  428. package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  429. package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  430. package/dist/web/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  431. package/dist/web/assets/{pdf-BLF2i8c2.js → pdf-BG8n_uc5.js} +1 -1
  432. package/dist/web/index.html +2 -2
  433. package/package.json +4 -4
  434. package/dist/edge/docker-launch.js +0 -81
  435. package/dist/edge/docker-projects/activation.js +0 -35
  436. package/dist/edge/docker-projects/claims.js +0 -85
  437. package/dist/edge/docker-projects/committed.js +0 -40
  438. package/dist/edge/docker-projects/engine.js +0 -461
  439. package/dist/edge/docker-projects/install.js +0 -254
  440. package/dist/edge/docker-projects/prune.js +0 -66
  441. package/dist/edge/docker-projects/recover.js +0 -100
  442. package/dist/edge/docker-projects/state.js +0 -225
  443. package/dist/web/assets/index-CktpxlJd.js +0 -171
  444. package/dist/web/assets/index-DB-vOnGU.css +0 -1
  445. package/scripts/docker-run.sh +0 -3
  446. /package/dist/edge/{docker-projects → docker-install}/tree.js +0 -0
@@ -10,7 +10,7 @@ import { recipeProviderChoice } from "./recipe-provider-choice.js";
10
10
  import { acceptedJobMessage, activeConflict, conflictRefusal } from "./recovery-conflict.js";
11
11
  import { recoveryRequestSchema, } from "./recovery-model.js";
12
12
  import { readRecovery, recoveryAuthority, recoveryKey, rememberRecovery, reserveRecovery, } from "./recovery-repo.js";
13
- import { dependentClosure, recoverySelection, regenerationEdit, sectionRoots, } from "./recovery-selection.js";
13
+ import { dependentClosure, recoverySelection, redoTarget, regenerationEdit, sectionRoots, } from "./recovery-selection.js";
14
14
  import { previewById, storePreview } from "./repo.js";
15
15
  import { admitCheckedPreview, wakeRebuild } from "./service.js";
16
16
  import { checkReadiness } from "./service-readiness.js";
@@ -27,7 +27,10 @@ function credentialsStamp(deps, plan, view) {
27
27
  .filter((row) => providers.has(String(row.provider)));
28
28
  return requestHash("recovery-credentials", view.revision.id, metadata);
29
29
  }
30
- export async function recoverProject(deps, projectId, raw) {
30
+ export async function recoverProject(deps, projectId, raw,
31
+ // Set only by an automatic review redo (`review-redo.ts`): see `activeConflict`.
32
+ options = {}) {
33
+ const superseded = options.pendingSuperseded === true;
31
34
  const parsed = recoveryRequestSchema.safeParse(raw);
32
35
  if (!parsed.success)
33
36
  return {
@@ -39,6 +42,12 @@ export async function recoverProject(deps, projectId, raw) {
39
42
  })),
40
43
  };
41
44
  const input = parsed.data;
45
+ // Where a rerun or a redo starts: the stage's generated work, or the reviewed item's.
46
+ const rootsOf = (view, plan) => input.action.kind === "rerun"
47
+ ? sectionRoots(view, plan, input.action.stage)
48
+ : input.action.kind === "redo"
49
+ ? (redoTarget(view, plan, input.action.item)?.roots ?? [])
50
+ : [];
42
51
  const prepared = await withProjectControl(deps.db, projectId, async () => {
43
52
  const found = readRecovery(deps, projectId, input);
44
53
  if (found !== undefined && "ok" in found)
@@ -54,7 +63,11 @@ export async function recoverProject(deps, projectId, raw) {
54
63
  if (!base.current)
55
64
  return { ok: false, reason: "conflict" };
56
65
  const plan = retainedPreviewPlan(deps, base, deps.catalogue.read());
57
- const edit = input.action.kind === "rerun" ? regenerationEdit(base, plan, input.action.stage) : null;
66
+ const edit = input.action.kind === "rerun"
67
+ ? regenerationEdit(base, plan, input.action.stage)
68
+ : input.action.kind === "redo"
69
+ ? redoTarget(base, plan, input.action.item)?.edit
70
+ : null;
58
71
  if (edit === undefined)
59
72
  return {
60
73
  ok: false,
@@ -62,7 +75,9 @@ export async function recoverProject(deps, projectId, raw) {
62
75
  fields: [
63
76
  {
64
77
  field: "stage",
65
- message: "This section has no generated work to rerun. Use Edit project to change its source.",
78
+ message: input.action.kind === "redo"
79
+ ? "This item is no longer made by the project (it was removed, or its source changed), so it can't be made again. Reload the project page."
80
+ : "This section has no generated work to rerun. Use Edit project to change its source.",
66
81
  },
67
82
  ],
68
83
  };
@@ -76,14 +91,15 @@ export async function recoverProject(deps, projectId, raw) {
76
91
  let view = base;
77
92
  if (record.edit !== null) {
78
93
  const prior = retainedPreviewPlan(deps, base, deps.catalogue.read());
79
- const action = input.action;
80
- if (action.kind !== "rerun")
94
+ if (input.action.kind !== "rerun" && input.action.kind !== "redo")
81
95
  throw new Error("Slopify hit an internal error (a recovery edit has nothing to re-run). Try again; if it happens again, use Download diagnostics in Settings and report it.");
82
- const affected = dependentClosure(prior.recipes, sectionRoots(base, prior, action.stage));
96
+ const affected = dependentClosure(prior.recipes, rootsOf(base, prior));
83
97
  const savedIntent = deps.db
84
98
  .prepare("SELECT result_revision_id FROM revision_mutations WHERE project_id=? AND idempotency_key=?")
85
99
  .get(projectId, recoveryKey(input, "save"));
86
- const conflict = savedIntent === undefined ? activeConflict(deps, projectId, affected) : undefined;
100
+ const conflict = savedIntent === undefined
101
+ ? activeConflict(deps, projectId, affected, superseded)
102
+ : undefined;
87
103
  if (conflict !== undefined)
88
104
  return finish(conflictRefusal(conflict));
89
105
  const saved = await saveRevision(deps, {
@@ -92,7 +108,7 @@ export async function recoverProject(deps, projectId, raw) {
92
108
  idempotencyKey: recoveryKey(input, "save"),
93
109
  edit: record.edit,
94
110
  beforeCommit: () => {
95
- const late = activeConflict(deps, projectId, affected);
111
+ const late = activeConflict(deps, projectId, affected, superseded);
96
112
  return late === undefined
97
113
  ? undefined
98
114
  : {
@@ -121,10 +137,10 @@ export async function recoverProject(deps, projectId, raw) {
121
137
  if (currentRevisionId(deps.db, projectId) !== view.revision.id)
122
138
  return finish({ ok: false, reason: "conflict" });
123
139
  const plan = retainedPreviewPlan(deps, view, deps.catalogue.read());
124
- const rerunKeys = input.action.kind === "rerun"
125
- ? dependentClosure(plan.recipes, sectionRoots(view, plan, input.action.stage))
140
+ const rerunKeys = input.action.kind === "rerun" || input.action.kind === "redo"
141
+ ? dependentClosure(plan.recipes, rootsOf(view, plan))
126
142
  : [];
127
- const selection = input.action.kind === "rerun"
143
+ const selection = input.action.kind === "rerun" || input.action.kind === "redo"
128
144
  ? { kind: "selected", workKeys: rerunKeys }
129
145
  : recoverySelection(plan, input.action.kind === "retry" ? input.action.stage : undefined);
130
146
  if (selection.kind === "selected" && selection.workKeys.length === 0)
@@ -167,15 +183,15 @@ export async function recoverProject(deps, projectId, raw) {
167
183
  input.action.stage === "video"
168
184
  ? {
169
185
  field: "video",
170
- message: "Source media is incomplete. Use Resume to recover it before rerunning the export.",
186
+ message: "Source media is incomplete. Use Continue the run to recover it before rerunning the export.",
171
187
  }
172
188
  : {
173
189
  field: "document",
174
- message: "The article or thumbnail the document is made from isn't finished. Use Resume to finish it, then Re-run section on Document.",
190
+ message: "The article or thumbnail the document is made from isn't finished. Use Continue the run to finish it, then More → make it again in its section on Document.",
175
191
  },
176
192
  ],
177
193
  });
178
- const conflict = rerunKeys.length > 0 ? activeConflict(deps, projectId, rerunKeys) : undefined;
194
+ const conflict = rerunKeys.length > 0 ? activeConflict(deps, projectId, rerunKeys, superseded) : undefined;
179
195
  if (conflict !== undefined)
180
196
  return finish(conflictRefusal(conflict));
181
197
  storePreview(deps, preview.value.preview, preview.value.execution);
@@ -199,7 +215,9 @@ export async function recoverProject(deps, projectId, raw) {
199
215
  catch {
200
216
  deps.log.write("warn", "project.recovery", {
201
217
  projectId,
202
- ...(input.action.kind === "resume" ? {} : { stage: input.action.stage }),
218
+ ...(input.action.kind === "resume" || input.action.kind === "redo"
219
+ ? {}
220
+ : { stage: input.action.stage }),
203
221
  detail: "Provider readiness could not be loaded.",
204
222
  });
205
223
  ready = {
@@ -232,7 +250,7 @@ export async function recoverProject(deps, projectId, raw) {
232
250
  if (ready.fields.length > 0)
233
251
  return finish({ ok: false, reason: "readiness", fields: [...ready.fields] });
234
252
  const conflict = prepared.rerunKeys.length > 0
235
- ? activeConflict(deps, projectId, prepared.rerunKeys)
253
+ ? activeConflict(deps, projectId, prepared.rerunKeys, superseded)
236
254
  : undefined;
237
255
  if (conflict !== undefined)
238
256
  return finish(conflictRefusal(conflict));
@@ -7,7 +7,7 @@ export { transitionRevisionWork } from "./transition-repo.js";
7
7
  export function publicationTargets(db, publication, workKey, workFingerprint) {
8
8
  const { work, pieceId, publicationId } = publication;
9
9
  if (publicationId !== (pieceId ?? work.workId))
10
- throw new Error("Slopify hit an internal error (a finished result is filed under the wrong step). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
10
+ throw new Error("Slopify hit an internal error (a finished result is filed under the wrong step). Try again; if it happens again, use Download diagnostics in Settings and report it.");
11
11
  if (!workExists(db, work))
12
12
  return [];
13
13
  if (pieceId !== null) {
@@ -15,9 +15,9 @@ export function publicationTargets(db, publication, workKey, workFingerprint) {
15
15
  .prepare("SELECT work_key,fingerprint FROM revision_work_pieces WHERE id=? AND work_id=?")
16
16
  .get(pieceId, work.workId);
17
17
  if (piece === undefined)
18
- throw new Error("Slopify hit an internal error (a finished result belongs to a different step). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
18
+ throw new Error("Slopify hit an internal error (a finished result belongs to a different step). Try again; if it happens again, use Download diagnostics in Settings and report it.");
19
19
  if (piece.work_key !== workKey || piece.fingerprint !== workFingerprint)
20
- throw new Error("Slopify hit an internal error (a finished result doesn't match its saved step). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
20
+ throw new Error("Slopify hit an internal error (a finished result doesn't match its saved step). Try again; if it happens again, use Download diagnostics in Settings and report it.");
21
21
  }
22
22
  const currentId = currentRevisionId(db, work.projectId);
23
23
  const owns = (revisionId) => {
@@ -0,0 +1,18 @@
1
+ import { autoRetryDelay } from "../../kernel/runner/retry-policy.js";
2
+ import { autoRetriesOf, deferWork, dueRetries } from "../../kernel/runner/work-authority.js";
3
+ // The runner's store for the second tier of the retry policy (`kernel/runner/retry-policy.ts`):
4
+ // a failure time can fix puts the step back to wait, with the time it may run again.
5
+ export function waitToRetry(deps, work, fault, reason, random) {
6
+ const delay = autoRetryDelay(fault, autoRetriesOf(deps.db, work), random);
7
+ if (delay === undefined || fault === undefined)
8
+ return undefined;
9
+ const at = new Date(deps.clock.now().getTime() + delay).toISOString();
10
+ return deferWork(deps.db, work, at, reason, fault.kind) ? at : undefined;
11
+ }
12
+ // Ticks every project with a wait that has run out. Returns them, for the log and tests.
13
+ export function wakeRetries(db, clock, runner) {
14
+ const due = dueRetries(db, clock.now().toISOString());
15
+ for (const projectId of due)
16
+ runner.tick(projectId);
17
+ return due;
18
+ }
@@ -0,0 +1,110 @@
1
+ import { createHash } from "node:crypto";
2
+ import { reviewKey } from "../reviews/model.js";
3
+ import { pendingRedos, setRedoState } from "../reviews/repo.js";
4
+ import { currentRevisionId } from "../revisions/repo.js";
5
+ import { recoverProject } from "./recovery.js";
6
+ import { dependentClosure } from "./recovery-selection.js";
7
+ import { executionPlan, executionView, savedCatalogue } from "./runtime-plan.js";
8
+ import { workPieces } from "./work-records.js";
9
+ // While a review's verdict waits to send its item back, what depends on the item waits too:
10
+ // the automatic redo replaces that work, and starting it on the failed item would be wasted.
11
+ // The same claim-time hold a review checkpoint uses, answered by the reviewer.
12
+ export function reviewHold(deps, work) {
13
+ const waiting = pendingRedos(deps.db, work.projectId).filter((row) => row.revisionId === work.revisionId);
14
+ if (waiting.length === 0)
15
+ return { kind: "eligible" };
16
+ const view = executionView(deps, work.projectId, work.revisionId);
17
+ const row = deps.db
18
+ .prepare("SELECT recipe_context FROM revision_work WHERE id=?")
19
+ .get(work.workId);
20
+ if (view === undefined || row === undefined)
21
+ return { kind: "eligible" };
22
+ const plan = executionPlan(deps, view, savedCatalogue(row.recipe_context));
23
+ const keys = new Set(workPieces(deps.db, work.workId).map((piece) => piece.key));
24
+ const held = waiting.filter((verdict) => {
25
+ const review = reviewKey(verdict.itemKey);
26
+ return dependentClosure(plan.recipes, [review]).some((key) => key !== review && keys.has(key));
27
+ });
28
+ return held.length === 0
29
+ ? { kind: "eligible" }
30
+ : { kind: "held", checkpointIds: held.map((verdict) => `review:${verdict.id}`) };
31
+ }
32
+ // Starts the redos reviews asked for, once their review step has finished: the item is made
33
+ // again through Re-run's path (a new regeneration token saved as a new project version, then
34
+ // its dependents rebuilt), so the redone item, its new review and everything after it are
35
+ // ordinary recipe work. One at a time per verdict; a redo that cannot start leaves the item
36
+ // flagged with the reason and releases what was waiting on it.
37
+ export function createReviewRedos() {
38
+ let bound;
39
+ const running = new Set();
40
+ const kick = (projectId) => {
41
+ const deps = bound;
42
+ if (deps === undefined)
43
+ return;
44
+ for (const verdict of pendingRedos(deps.db, projectId)) {
45
+ if (running.has(verdict.id))
46
+ continue;
47
+ running.add(verdict.id);
48
+ void startRedo(deps, verdict)
49
+ .catch((error) => {
50
+ deps.log.write("error", "review.redo", {
51
+ projectId: verdict.projectId,
52
+ detail: error instanceof Error ? error.message : String(error),
53
+ });
54
+ setRedoState(deps.db, verdict.id, "failed", "Slopify hit an internal error while making this item again. Use Redo beside it; if it happens again, use Download diagnostics in Settings and report it.");
55
+ })
56
+ .finally(() => {
57
+ running.delete(verdict.id);
58
+ try {
59
+ deps.runner.tick(verdict.projectId);
60
+ }
61
+ catch {
62
+ // The next tick of this project picks the released work up.
63
+ }
64
+ });
65
+ }
66
+ };
67
+ return {
68
+ bind: (deps) => {
69
+ bound = deps;
70
+ },
71
+ kick,
72
+ };
73
+ }
74
+ export async function startRedo(deps, verdict) {
75
+ const base = currentRevisionId(deps.db, verdict.projectId);
76
+ if (base === undefined) {
77
+ setRedoState(deps.db, verdict.id, "failed", "The project no longer exists.");
78
+ return;
79
+ }
80
+ const result = await recoverProject(deps, verdict.projectId, {
81
+ baseRevisionId: base,
82
+ idempotencyKey: redoKey(verdict.id),
83
+ action: { kind: "redo", item: verdict.itemKey },
84
+ }, { pendingSuperseded: true });
85
+ if (result.ok)
86
+ setRedoState(deps.db, verdict.id, "started");
87
+ else
88
+ setRedoState(deps.db, verdict.id, "failed", redoRefusal(result));
89
+ deps.emit(verdict.projectId, { type: "project.updated", projectId: verdict.projectId });
90
+ }
91
+ // A stable request identity per verdict, so a restart that starts the same redo again gets
92
+ // the first attempt's answer instead of a second redo.
93
+ export function redoKey(verdictId) {
94
+ const hex = createHash("sha256").update(`review-redo:${verdictId}`).digest("hex");
95
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-4${hex.slice(13, 16)}-8${hex.slice(17, 20)}-${hex.slice(20, 32)}`;
96
+ }
97
+ export function redoRefusal(result) {
98
+ const detail = result.fields?.map((field) => field.message).join(" ");
99
+ switch (result.reason) {
100
+ case "running":
101
+ return "Slopify couldn't make this item again automatically because work that depends on it was already running. Use Redo beside it once the run has finished.";
102
+ case "readiness":
103
+ return `Slopify couldn't make this item again automatically: ${detail ?? "a provider isn't ready"}. Fix that in Settings → Providers, then use Redo beside it.`;
104
+ case "conflict":
105
+ case "control-changed":
106
+ return "Slopify couldn't make this item again automatically because the project changed at the same moment (an edit, Pause or Cancel). Use Redo beside it.";
107
+ default:
108
+ return `Slopify couldn't make this item again automatically${detail ? `: ${detail}` : "."} Use Redo beside it.`;
109
+ }
110
+ }
@@ -15,6 +15,11 @@ export async function revisionAction(deps, projectId, action) {
15
15
  ? view.outputs.find((row) => row.selected &&
16
16
  row.output.id === action.outputId &&
17
17
  (row.output.role === "image" ||
18
+ // One of the generated thumbnails is made again on its own, the others kept.
19
+ (action.kind === "regenerate-image" &&
20
+ row.output.role === "thumbnail" &&
21
+ (config.sources.thumbnail === "from_prompt" ||
22
+ config.sources.thumbnail === "prompt_by_llm")) ||
18
23
  // A generated establishing image is made again like any image; it is never deleted
19
24
  // on its own (Edit project sets it Off), and an uploaded one has nothing to redo.
20
25
  (action.kind === "regenerate-image" &&
@@ -12,7 +12,10 @@ export function reservationKey(recipe, view) {
12
12
  ? `audio:${recipe.input.segment}:future`
13
13
  : recipe.input.kind === "llm" && recipe.input.preparation
14
14
  ? `narration:prepare:${recipe.input.preparation.segment}:future`
15
- : key;
15
+ : (recipe.input.kind === "llm" && recipe.input.describe) ||
16
+ recipe.key.startsWith("figure:card:")
17
+ ? "narration:describe:future"
18
+ : key;
16
19
  return view.revision.fingerprints[future] === undefined ? key : future;
17
20
  }
18
21
  export function admitInitialRevision(deps, view, catalogue) {
@@ -22,7 +22,7 @@ export async function executeAnimateRecipe(deps, context, providers, piece) {
22
22
  return "done";
23
23
  const input = piece.input;
24
24
  if (input.kind !== "image" || input.animate === undefined)
25
- throw new Error("Slopify hit an internal error (an animated image was set up wrongly). Use Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
25
+ throw new Error("Slopify hit an internal error (an animated image was set up wrongly). Use Try again; if it happens again, use Download diagnostics in Settings and report it.");
26
26
  const imageKey = piece.key.slice("animate:".length);
27
27
  const view = executionView(deps, context.work.projectId, context.work.revisionId);
28
28
  const index = (view?.revision.content.imageOrder.indexOf(imageKey) ?? -1) + 1;
@@ -35,10 +35,10 @@ export async function executeAnimateRecipe(deps, context, providers, piece) {
35
35
  return "done";
36
36
  };
37
37
  if (still === undefined)
38
- return fallback("its image wasn't ready to animate. Use Re-run section on Video after the image is made.");
38
+ return fallback("its image wasn't ready to animate. Use More → Render the video again in the Video section after the image is made.");
39
39
  const animate = providers.forPiece(piece.id).animate;
40
40
  if (animate === undefined)
41
- throw new Error("Slopify hit an internal error (animating images isn't wired up in this build). Use Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
41
+ throw new Error("Slopify hit an internal error (animating images isn't wired up in this build). Use Try again; if it happens again, use Download diagnostics in Settings and report it.");
42
42
  if (!context.maySubmit(piece.id))
43
43
  return "held";
44
44
  let made;
@@ -55,7 +55,7 @@ export async function executeAnimateRecipe(deps, context, providers, piece) {
55
55
  catch (error) {
56
56
  if (context.signal.aborted)
57
57
  throw error;
58
- return fallback(`${error instanceof Error ? error.message : String(error)} To try again, use Re-run section on Video.`);
58
+ return fallback(`${error instanceof Error ? error.message : String(error)} To try again, use More → Render the video again in the Video section.`);
59
59
  }
60
60
  if (!made.ok)
61
61
  return "held";
@@ -12,14 +12,14 @@ const answerSchema = z.object({
12
12
  });
13
13
  export async function executeArticleRequests(deps, context, providers, initial) {
14
14
  if (initial.input.kind !== "llm")
15
- throw new Error("Slopify hit an internal error (the article step has no AI request). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
15
+ throw new Error("Slopify hit an internal error (the article step has no AI request). Try again; if it happens again, use Download diagnostics in Settings and report it.");
16
16
  let piece = initial;
17
17
  let text = "";
18
18
  let inputTokens = 0;
19
19
  let outputTokens = 0;
20
20
  for (let part = 0; part <= continuationLimit; part += 1) {
21
21
  if (piece.input.kind !== "llm")
22
- throw new Error("Slopify hit an internal error (an article continuation has no AI request). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
22
+ throw new Error("Slopify hit an internal error (an article continuation has no AI request). Try again; if it happens again, use Download diagnostics in Settings and report it.");
23
23
  const saved = deps.db
24
24
  .prepare("SELECT result_json FROM revision_work_pieces WHERE id=? AND work_id=?")
25
25
  .get(piece.id, context.work.workId);
@@ -38,9 +38,9 @@ export async function executeArticleRequests(deps, context, providers, initial)
38
38
  webSearch: input.webSearch,
39
39
  previewLabel: part === 0 ? "Article" : `Continuation ${part}`,
40
40
  check: (value) => value.text.trim() === ""
41
- ? "The AI model returned an empty article. Retry stage; if it keeps happening, choose a different model in Edit project → Providers."
41
+ ? "The AI model returned an empty article. Try again; if it keeps happening, choose a different model in Edit project → Providers."
42
42
  : part === continuationLimit && value.finishReason === "length"
43
- ? `The article was still unfinished after ${continuationLimit} continuations because the AI model kept stopping at its length limit. Ask for a shorter article in Edit project → Prompts, or choose a model with a larger output limit in Edit project → Providers, then Retry stage.`
43
+ ? `The article was still unfinished after ${continuationLimit} continuations because the AI model kept stopping at its length limit. Ask for a shorter article in Edit project → Prompts, or choose a model with a larger output limit in Edit project → Providers, then Try again.`
44
44
  : undefined,
45
45
  }, (event) => {
46
46
  if (event.type === "delta")
@@ -72,7 +72,7 @@ export async function executeArticleRequests(deps, context, providers, initial)
72
72
  return { ok: false, reason: "held" };
73
73
  piece = next;
74
74
  }
75
- throw new Error(`The article was still unfinished after ${continuationLimit} continuations because the AI model kept stopping at its length limit. Ask for a shorter article in Edit project → Prompts, or choose a model with a larger output limit in Edit project → Providers, then Retry stage.`);
75
+ throw new Error(`The article was still unfinished after ${continuationLimit} continuations because the AI model kept stopping at its length limit. Ask for a shorter article in Edit project → Prompts, or choose a model with a larger output limit in Edit project → Providers, then Try again.`);
76
76
  }
77
77
  function continuationPiece(deps, context, initial, part, text) {
78
78
  return transact(deps.db, () => {
@@ -28,7 +28,7 @@ export async function executeDocumentRecipe(deps, context, piece) {
28
28
  ? view.articleMarkdown
29
29
  : text("article_md", "article:body");
30
30
  if (article === null)
31
- throw new Error("The article isn't finished yet, so there's nothing to make the document from. Let the Article stage finish (Resume, or Retry stage on Article), then use Retry stage on Document.");
31
+ throw new Error("The article isn't finished yet, so there's nothing to make the document from. Let the Article stage finish (Resume, or Try again on Article), then use Try again on Document.");
32
32
  const thumbnail = ready(view, "thumbnail", "thumbnail:image");
33
33
  let cover = null;
34
34
  if (config.sources.thumbnail !== "off" && thumbnail !== undefined) {
@@ -36,7 +36,7 @@ export async function executeDocumentRecipe(deps, context, piece) {
36
36
  cover = new Uint8Array(readFileSync(outputPath(deps.paths, projectId, thumbnail.output.path)));
37
37
  }
38
38
  catch (error) {
39
- throw new Error(`The document's cover is the project's thumbnail, and its file can't be read (${error instanceof Error ? error.message : String(error)}). Regenerate or upload the thumbnail again (Re-run section on Thumbnail, or Edit project → Thumbnail), then use Retry stage on Document.`, { cause: error });
39
+ throw new Error(`The document's cover is the project's thumbnail, and its file can't be read (${error instanceof Error ? error.message : String(error)}). Regenerate or upload the thumbnail again (More → make it again in its section on Thumbnail, or Edit project → Thumbnail), then use Try again on Document.`, { cause: error });
40
40
  }
41
41
  }
42
42
  const rendered = renderDocument({
@@ -69,10 +69,10 @@ function documentView(deps, context, piece) {
69
69
  .prepare("SELECT recipe_context FROM revision_work WHERE id=?")
70
70
  .get(context.work.workId);
71
71
  if (view === undefined || row === undefined)
72
- throw new Error("Slopify hit an internal error (the project version the document belongs to is missing). Use Retry stage on Document; if it happens again, use Download diagnostics in Settings and report it.");
72
+ throw new Error("Slopify hit an internal error (the project version the document belongs to is missing). Use Try again on Document; if it happens again, use Download diagnostics in Settings and report it.");
73
73
  const recipe = executionPlan(deps, view, savedCatalogue(row.recipe_context)).recipes.find((one) => one.key === piece.key && one.fingerprint === piece.fingerprint);
74
74
  if (recipe === undefined || recipe.unresolved)
75
- throw new Error("The project changed after this document was queued, so it no longer matches the saved article. Use Rebuild affected outputs (or Retry stage on Document) to make it from the current version.");
75
+ throw new Error("The project changed after this document was queued, so it no longer matches the saved article. Use Choose what to remake (in the project's More menu) (or Try again on Document) to make it from the current version.");
76
76
  return view;
77
77
  }
78
78
  function ready(view, role, workKey) {
@@ -0,0 +1,43 @@
1
+ import { z } from "zod";
2
+ import { outputPath } from "../storage/layout.js";
3
+ import { usesAmbientBed } from "../video/ambient-bed.js";
4
+ import { probeDurationMs } from "../video/ffmpeg.js";
5
+ // The long video's ambient bed as the plan takes it (`video/ambient-bed.ts`), or undefined for
6
+ // none. The user's own file is checked readable before the render, so a bad file is named as
7
+ // the ambient sound rather than as a failed render.
8
+ export async function exportBed(deps, context, view) {
9
+ const config = view.revision.config;
10
+ const bed = config.ambientBed;
11
+ if (bed === undefined || !usesAmbientBed(config))
12
+ return undefined;
13
+ const settings = {
14
+ levelDb: bed.levelDb,
15
+ fadeInSeconds: bed.fadeInSeconds,
16
+ tailSeconds: bed.tailSeconds,
17
+ };
18
+ if (bed.source !== "upload")
19
+ return { source: { kind: "noise", preset: bed.source }, ...settings };
20
+ const fix = "Make the video again from Play with another file under Outputs → Export → Ambient sound, or pick Rain, Fireplace or Wind there instead.";
21
+ const assetId = view.revision.content.ambientBed;
22
+ const row = z
23
+ .object({ path: z.string() })
24
+ .safeParse(assetId === undefined
25
+ ? undefined
26
+ : deps.db
27
+ .prepare("SELECT path FROM project_assets WHERE project_id=? AND id=?")
28
+ .get(context.work.projectId, assetId));
29
+ if (!row.success)
30
+ throw new Error(`The ambient sound's audio file is no longer in the project. ${fix}`);
31
+ const path = outputPath(deps.paths, context.work.projectId, row.data.path);
32
+ let durationMs;
33
+ try {
34
+ durationMs = await probeDurationMs(deps.ffmpeg, path, context.signal, deps.log);
35
+ }
36
+ catch (error) {
37
+ context.signal.throwIfAborted();
38
+ throw new Error(`The ambient sound's audio file couldn't be read as audio (${error instanceof Error ? error.message : String(error)}). ${fix}`);
39
+ }
40
+ if (durationMs <= 0)
41
+ throw new Error(`The ambient sound's audio file holds no sound. ${fix}`);
42
+ return { source: { kind: "file", path }, ...settings };
43
+ }
@@ -1,13 +1,15 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { extname } from "node:path";
3
3
  import { z } from "zod";
4
- import { usesYoutubeDescription } from "../admission/rules.js";
4
+ import { usesFigureCards, usesYoutubeDescription } from "../admission/rules.js";
5
+ import { captionFont, captionFontDeps } from "../fonts/coverage.js";
5
6
  import { resolveFont } from "../fonts/index.js";
6
7
  import { outputPath } from "../storage/layout.js";
7
8
  import { headingChapters } from "../video/chapters.js";
8
9
  import { sentenceCutPoints } from "../video/cuts.js";
9
10
  import { usesChapterCards, usesNarrationCuts, videoEditOf, } from "../video/edit-settings.js";
10
11
  import { probeDurationMs } from "../video/ffmpeg.js";
12
+ import { passageSpans } from "../video/figure-spans.js";
11
13
  import { wordsSchema } from "./runtime-subtitles.js";
12
14
  // The containers an uploaded clip may come in; anything else in the images is a still.
13
15
  const clipExtensions = new Set([".mp4", ".mov", ".m4v", ".webm", ".mkv"]);
@@ -39,23 +41,31 @@ export async function exportEdit(deps, context, view, images) {
39
41
  .safeParse(JSON.parse(row.piece.payload ?? "null"));
40
42
  return parsed.success ? [parsed.data.fallback] : [];
41
43
  });
42
- if (config.videoEdit === undefined)
44
+ const endScreen = config.endScreen?.text.trim() ?? "";
45
+ const figures = usesFigureCards(config) ? figureShots(deps, context, view) : [];
46
+ const withFigures = figures.length === 0 ? {} : { figures };
47
+ if (config.videoEdit === undefined) {
48
+ const titles = endScreen === "" ? undefined : await titleFont(deps, config);
43
49
  return {
44
- edit: clips.some((clip) => clip !== undefined) ? { clips } : undefined,
50
+ edit: titles !== undefined
51
+ ? { clips, cards: { chapters: [], ...titles, endScreen }, ...withFigures }
52
+ : clips.some((clip) => clip !== undefined) || figures.length > 0
53
+ ? { clips, ...withFigures }
54
+ : undefined,
45
55
  warnings,
46
56
  settings: undefined,
47
57
  };
58
+ }
48
59
  const settings = videoEditOf(config);
49
60
  const cuts = usesNarrationCuts(config);
50
61
  const cards = usesChapterCards(config);
51
62
  const words = cuts || cards ? timingWords(deps, context, view) : [];
52
63
  const chapters = cuts || cards ? chaptersOf(view, config, words) : [];
53
- const font = cards
54
- ? await resolveFont(deps.paths, config.subtitles?.fontId ?? "default")
55
- : undefined;
64
+ const titles = cards || endScreen !== "" ? await titleFont(deps, config) : undefined;
56
65
  return {
57
66
  edit: {
58
67
  clips,
68
+ ...withFigures,
59
69
  ...(cuts
60
70
  ? {
61
71
  narration: {
@@ -73,22 +83,40 @@ export async function exportEdit(deps, context, view, images) {
73
83
  grade: settings.grade,
74
84
  atmosphere: settings.atmosphere,
75
85
  },
76
- ...(font === undefined
86
+ ...(titles === undefined
77
87
  ? {}
78
- : { cards: { chapters, font: { path: font.path, name: font.assName } } }),
88
+ : {
89
+ cards: {
90
+ chapters: cards ? chapters : [],
91
+ ...titles,
92
+ ...(endScreen === "" ? {} : { endScreen }),
93
+ },
94
+ }),
79
95
  },
80
96
  warnings,
81
97
  settings,
82
98
  };
83
99
  }
84
- function timingWords(deps, context, view) {
100
+ // The cards' font and colour: the brand kit's title style, else the caption font in white.
101
+ async function titleFont(deps, config) {
102
+ const fontId = config.titleStyle?.fontId ?? config.subtitles?.fontId ?? "default";
103
+ // Chapter titles are in the project language too, so they need its letters.
104
+ const font = config.language === undefined || config.language === "en"
105
+ ? await resolveFont(deps.paths, fontId)
106
+ : await captionFont(captionFontDeps(deps.paths), fontId, config.language);
107
+ return {
108
+ font: { path: font.path, name: font.assName },
109
+ ...(config.titleStyle?.color === undefined ? {} : { color: config.titleStyle.color }),
110
+ };
111
+ }
112
+ export function timingWords(deps, context, view) {
85
113
  const timing = view.outputs.find((row) => row.selected &&
86
114
  row.available &&
87
115
  row.state === "ready" &&
88
116
  row.workKey === "subtitles:timing" &&
89
117
  row.output.role === "subtitle_words");
90
118
  if (timing === undefined)
91
- throw new Error("The narration's word timing, which the video's cuts and chapter cards follow, is missing. Use Re-run section on Video, then Retry stage.");
119
+ throw new Error("The narration's word timing, which the video's cuts and chapter cards follow, is missing. Use More → Render the video again in the Video section, then Try again.");
92
120
  return wordsSchema.parse(JSON.parse(readFileSync(outputPath(deps.paths, context.work.projectId, timing.output.path), "utf8"))).words;
93
121
  }
94
122
  // The YouTube description's chapters when that step runs, the article's headings otherwise.
@@ -114,6 +142,37 @@ async function clipSeconds(deps, context, path, place) {
114
142
  ms = 0;
115
143
  }
116
144
  if (!(ms > 0))
117
- throw new Error(`Image ${String(place)} is a video clip Slopify can't read, so the video can't show it. Replace it with another clip or an image in Edit project → Images, then Retry stage.`);
145
+ throw new Error(`Image ${String(place)} is a video clip Slopify can't read, so the video can't show it. Replace it with another clip or an image in Edit project → Images, then Try again.`);
118
146
  return ms / 1000;
119
147
  }
148
+ // Each card in the video's frame, shown where its description is spoken: the description's
149
+ // answer found in the word timing (`video/figure-spans.ts`). A card whose description can't be
150
+ // found in the timing is left out rather than shown at the wrong moment.
151
+ export function figureShots(deps, context, view, format = view.revision.config.format, words = timingWords(deps, context, view)) {
152
+ const cards = view.outputs
153
+ .filter((row) => row.selected &&
154
+ row.available &&
155
+ row.state === "ready" &&
156
+ row.output.role === "figure_card" &&
157
+ row.workKey.startsWith("figure:card:") &&
158
+ (row.output.meta.format ?? view.revision.config.format) === format)
159
+ .map((row) => ({
160
+ index: Number(row.workKey.slice("figure:card:".length)),
161
+ path: outputPath(deps.paths, context.work.projectId, row.output.path),
162
+ }))
163
+ .toSorted((a, b) => a.index - b.index);
164
+ const passages = cards.map((card) => {
165
+ const row = view.pieces.find((one) => one.key === `narration:describe:${String(card.index)}` &&
166
+ one.selected &&
167
+ one.piece.state === "done");
168
+ const parsed = z
169
+ .object({ text: z.string() })
170
+ .safeParse(JSON.parse(row?.piece.payload ?? "null"));
171
+ return parsed.success ? parsed.data.text : "";
172
+ });
173
+ const spans = passageSpans(words, passages);
174
+ return cards.flatMap((card, at) => {
175
+ const span = spans[at];
176
+ return span === undefined ? [] : [{ path: card.path, start: span.start, end: span.end }];
177
+ });
178
+ }