@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
@@ -1,14 +1,22 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { z } from "zod";
3
3
  import { subtitleModelDir } from "../../kernel/paths.js";
4
+ import { projectLanguage } from "../../kernel/ports/languages.js";
4
5
  import { SubtitleMismatch } from "../../kernel/ports/subtitles.js";
6
+ import { narrationAliasesOf } from "../admission/rules.js";
7
+ import { captionFont, captionFontDeps } from "../fonts/coverage.js";
5
8
  import { resolveFont } from "../fonts/index.js";
6
9
  import { validateCues } from "../revisions/rules.js";
7
10
  import { discardPreparedAssets, writeAsset } from "../storage/assets.js";
8
11
  import { outputPath } from "../storage/layout.js";
9
12
  import { captionCues, serializeAss, serializeSrt, serializeVtt } from "../subtitles/captions.js";
10
13
  import { spoken } from "../video/plan.js";
11
- import { exportSnapshot, retainedOutput, revisionAudio, revisionTranscript, } from "./runtime-export-inputs.js";
14
+ import { usesVoices } from "../voices/model.js";
15
+ import { speakerColour } from "../voices/palette.js";
16
+ import { speakerPanelEvents, usesSpeakerPanel } from "../voices/panel.js";
17
+ import { panelPortraits } from "../voices/portraits.js";
18
+ import { attributeWords } from "../voices/timing.js";
19
+ import { exportSnapshot, retainedOutput, revisionAudio, revisionTranscript, revisionTurns, } from "./runtime-export-inputs.js";
12
20
  import { narrationChunks, narrationTextParts, } from "./runtime-narration-text.js";
13
21
  import { preparedResult, preparedText, publishResult } from "./runtime-publication.js";
14
22
  // The saved `subtitles.json`: every word at its time in the final video. The YouTube
@@ -20,6 +28,9 @@ export const wordsSchema = z.object({
20
28
  start: z.number().finite().nonnegative(),
21
29
  end: z.number().finite().positive(),
22
30
  confidence: z.number().optional(),
31
+ // A multi-voice run's speaker id and script turn (`voices/timing.ts`).
32
+ speaker: z.string().optional(),
33
+ turn: z.number().int().positive().optional(),
23
34
  })),
24
35
  omissions: z
25
36
  .array(z.object({ start: z.number().finite().nonnegative(), text: z.string() }))
@@ -30,6 +41,7 @@ const cuesSchema = z.object({
30
41
  text: z.string(),
31
42
  start: z.number().finite().nonnegative(),
32
43
  end: z.number().finite().positive(),
44
+ speaker: z.string().optional(),
33
45
  })),
34
46
  omissions: wordsSchema.shape.omissions,
35
47
  });
@@ -44,19 +56,21 @@ export async function executeSubtitleRecipe(deps, context, piece) {
44
56
  return cues(deps, context, piece, snapshot);
45
57
  if (piece.key === "subtitles:files")
46
58
  return files(deps, context, piece, snapshot);
47
- throw new Error("Slopify hit an internal error (unknown kind of caption step). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
59
+ throw new Error("Slopify hit an internal error (unknown kind of caption step). Try again; if it happens again, use Download diagnostics in Settings and report it.");
48
60
  }
49
61
  async function timing(deps, context, piece, snapshot) {
50
62
  const audio = await revisionAudio(deps, context, snapshot.view);
51
63
  if (audio.length === 0)
52
- throw new Error("Captions and the YouTube description need narration audio, but this project has none. Turn captions and the YouTube description off in Edit project, or turn narration on, then Retry stage.");
64
+ throw new Error("Captions and the YouTube description need narration audio, but this project has none. Turn captions and the YouTube description off in Edit project, or turn narration on, then Try again.");
53
65
  const alignSubtitles = deps.alignSubtitles;
54
66
  if (alignSubtitles === undefined)
55
- throw new Error("Slopify hit an internal error (the caption timing tool is missing from this build). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
67
+ throw new Error("Slopify hit an internal error (the caption timing tool is missing from this build). Try again; if it happens again, use Download diagnostics in Settings and report it.");
56
68
  if (!context.maySubmit(piece.id))
57
69
  return "held";
58
70
  const words = [];
59
71
  const omissions = [];
72
+ const turns = revisionTurns(snapshot);
73
+ const language = projectLanguage(snapshot.view.revision.config);
60
74
  let offset = 0;
61
75
  const total = audio.reduce((sum, segment) => sum + segment.seconds, 0);
62
76
  for (const segment of audio) {
@@ -66,7 +80,9 @@ async function timing(deps, context, piece, snapshot) {
66
80
  const aligned = await located(snapshot, kind, () => alignSubtitles({
67
81
  audioPath: path,
68
82
  text: revisionTranscript(deps, snapshot, kind),
69
- cacheDir: subtitleModelDir(deps.paths.dataDir),
83
+ aliases: narrationAliasesOf(snapshot.view.revision.config),
84
+ cacheDir: subtitleModelDir(deps.paths.dataDir, language),
85
+ ...(language === "en" ? {} : { language }),
70
86
  ffmpeg: deps.ffmpeg,
71
87
  signal: context.signal,
72
88
  onOmission: (value) => omissions.push({ ...value, start: value.start + offset }),
@@ -78,9 +94,11 @@ async function timing(deps, context, piece, snapshot) {
78
94
  total: 100,
79
95
  }),
80
96
  }));
81
- for (const word of aligned) {
97
+ for (const word of kind === "body" && turns.length > 0
98
+ ? attributeWords(turns, aligned)
99
+ : aligned) {
82
100
  if (word.end > segment.seconds + 0.1)
83
- throw new Error("Caption timing came out longer than the narration audio. Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
101
+ throw new Error("Caption timing came out longer than the narration audio. Try again; if it happens again, use Download diagnostics in Settings and report it.");
84
102
  words.push({
85
103
  ...word,
86
104
  start: word.start + offset,
@@ -91,7 +109,7 @@ async function timing(deps, context, piece, snapshot) {
91
109
  offset += segment.seconds;
92
110
  }
93
111
  if (captionCues(words).length === 0)
94
- throw new Error("None of the narration could be matched to the article text, so captions can't be timed. Captions only work for English narration; if you uploaded your own audio, make sure it reads the article text, then Retry stage.");
112
+ throw new Error("None of the narration could be matched to the article text, so captions can't be timed. The narration must be in the project language set in Edit project → Language; if you uploaded your own audio, make sure it reads the article text, then Try again.");
95
113
  context.signal.throwIfAborted();
96
114
  const output = preparedText(deps, context, piece, "subtitle_words", "subtitles.json", JSON.stringify({ key: piece.logicalFingerprint ?? piece.fingerprint, words, omissions }));
97
115
  await publishResult(deps, context, piece, [output], { words, omissions }, output.asset);
@@ -103,7 +121,7 @@ async function cues(deps, context, piece, snapshot) {
103
121
  let value;
104
122
  if (manual !== undefined) {
105
123
  if (plan.work.find((one) => one.key === piece.key)?.disposition === "review")
106
- throw new Error("Your edited captions were written for an older version of the narration. Review them in Edit project → Captions and save, then Retry stage.");
124
+ throw new Error("Your edited captions were written for an older version of the narration. Review them in Edit project → Captions and save, then Try again.");
107
125
  const audio = await revisionAudio(deps, context, view);
108
126
  const errors = validateCues(manual.cues, audio.reduce((sum, segment) => sum + segment.seconds, 0));
109
127
  if (errors.length > 0)
@@ -116,7 +134,7 @@ async function cues(deps, context, piece, snapshot) {
116
134
  one.state === "ready" &&
117
135
  one.workKey === "subtitles:timing");
118
136
  if (output === undefined)
119
- throw new Error("Caption timing is missing. Use Re-run section on Video, then Retry stage.");
137
+ throw new Error("Caption timing is missing. Use More → Render the video again in the Video section, then Try again.");
120
138
  const timed = wordsSchema.parse(JSON.parse(readFileSync(outputPath(deps.paths, context.work.projectId, output.output.path), "utf8")));
121
139
  value = { cues: [...captionCues(timed.words)], omissions: timed.omissions };
122
140
  }
@@ -130,10 +148,10 @@ async function files(deps, context, piece, snapshot) {
130
148
  const { view } = snapshot;
131
149
  const config = view.revision.config.subtitles;
132
150
  if (config === undefined || config.mode === "off")
133
- throw new Error("Slopify hit an internal error (captions are off for this version of the project). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
151
+ throw new Error("Slopify hit an internal error (captions are off for this version of the project). Try again; if it happens again, use Download diagnostics in Settings and report it.");
134
152
  const row = view.pieces.find((one) => one.key === "subtitles:cues" && one.selected && one.available && one.piece.state === "done");
135
153
  if (row?.piece.payload === null || row?.piece.payload === undefined)
136
- throw new Error("The captions haven't been prepared yet. Use Re-run section on Video, then Retry stage.");
154
+ throw new Error("The captions haven't been prepared yet. Use More → Render the video again in the Video section, then Try again.");
137
155
  const value = cuesSchema.parse(JSON.parse(row.piece.payload));
138
156
  const previous = view.outputs.find((one) => one.output.role === "subtitle_font" && one.selected && one.available);
139
157
  const previousFiles = view.pieces.find((one) => one.key === "subtitles:files" &&
@@ -152,9 +170,17 @@ async function files(deps, context, piece, snapshot) {
152
170
  .optional(),
153
171
  })
154
172
  .parse(JSON.parse(previousFiles?.piece.payload ?? "{}")).font;
155
- const font = savedFont?.id === config.fontId && previous !== undefined
156
- ? { ...savedFont, path: outputPath(deps.paths, context.work.projectId, previous.output.path) }
157
- : await resolveFont(deps.paths, config.fontId);
173
+ // Another language's captions may need a font with its letters (`fonts/coverage.ts`); an
174
+ // English project keeps the saved font as it always did.
175
+ const language = projectLanguage(view.revision.config);
176
+ const font = language !== "en"
177
+ ? await captionFont(captionFontDeps(deps.paths), config.fontId, language)
178
+ : savedFont?.id === config.fontId && previous !== undefined
179
+ ? {
180
+ ...savedFont,
181
+ path: outputPath(deps.paths, context.work.projectId, previous.output.path),
182
+ }
183
+ : await resolveFont(deps.paths, config.fontId);
158
184
  if (!context.maySubmit(piece.id))
159
185
  return "held";
160
186
  const prepared = [];
@@ -162,9 +188,20 @@ async function files(deps, context, piece, snapshot) {
162
188
  const frame = view.revision.config.format === "16:9"
163
189
  ? { width: 1920, height: 1080 }
164
190
  : { width: 1080, height: 1920 };
191
+ const voices = captionVoices(view.revision.config);
192
+ const portraits = panelPortraits(deps.db, voices);
193
+ const overlay = voices !== undefined && usesSpeakerPanel(voices.format)
194
+ ? speakerPanelEvents(value.cues, voices.speakers.map((speaker, index) => ({
195
+ id: speaker.id,
196
+ name: speaker.name.trim(),
197
+ colour: speakerColour(index),
198
+ ...(portraits[index] === undefined ? {} : { portrait: true }),
199
+ })), frame, (await revisionAudio(deps, context, view)).reduce((sum, segment) => sum + segment.seconds, 0))
200
+ : [];
201
+ const speakers = voices === undefined ? undefined : captionSpeakers(voices);
165
202
  for (const [role, filename, text] of [
166
- ["subtitles_srt", "subtitles.srt", serializeSrt(value.cues)],
167
- ["subtitles_vtt", "subtitles.vtt", serializeVtt(value.cues)],
203
+ ["subtitles_srt", "subtitles.srt", serializeSrt(value.cues, speakers)],
204
+ ["subtitles_vtt", "subtitles.vtt", serializeVtt(value.cues, speakers)],
168
205
  [
169
206
  "subtitle_ass",
170
207
  "subtitles.ass",
@@ -173,6 +210,10 @@ async function files(deps, context, piece, snapshot) {
173
210
  fontName: font.assName,
174
211
  fontSize: config.fontSize,
175
212
  position: config.position,
213
+ color: config.color,
214
+ outlineColor: config.outlineColor,
215
+ speakers,
216
+ overlay,
176
217
  }),
177
218
  ],
178
219
  ])
@@ -242,8 +283,8 @@ export function describeMismatch(chunks, segment, mismatch) {
242
283
  const expected = mismatch.expected === "" ? "the end of the text" : `"${mismatch.expected}…"`;
243
284
  const heard = mismatch.heard === "" ? "no more speech" : `"${mismatch.heard.toLowerCase()}…"`;
244
285
  const fix = chunk === undefined
245
- ? "Check that part of the narration, regenerate it in Edit project → Narration, then Resume."
246
- : `In Edit project → Narration, regenerate narration chunk ${String(at + 1)}, then Resume.`;
286
+ ? "Check that part of the narration, regenerate it in Edit project → Narration, then Continue the run."
287
+ : `In Edit project → Narration, regenerate narration chunk ${String(at + 1)}, then Continue the run.`;
247
288
  return `Subtitles stopped matching the audio at ${where}. The text expected ${expected} but the audio has ${heard} The recording there probably skips or changes words. ${fix}`;
248
289
  }
249
290
  function comparable(text) {
@@ -266,3 +307,15 @@ function clock(seconds) {
266
307
  ? `${String(hours)}:${String(minutes).padStart(2, "0")}:${rest}`
267
308
  : `${String(minutes)}:${rest}`;
268
309
  }
310
+ function captionVoices(config) {
311
+ return usesVoices(config) ? config.voices : undefined;
312
+ }
313
+ function captionSpeakers(voices) {
314
+ return {
315
+ styles: Object.fromEntries(voices.speakers.map((speaker, index) => [
316
+ speaker.id,
317
+ { name: speaker.name.trim(), colour: speakerColour(index) },
318
+ ])),
319
+ nameTags: voices.nameTags,
320
+ };
321
+ }
@@ -0,0 +1,103 @@
1
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { z } from "zod";
4
+ import { masterFile, masterReport } from "../loudness/loudnorm.js";
5
+ import { masterGoal } from "../loudness/model.js";
6
+ import { allocateAsset, discardPreparedAssets, sealAsset } from "../storage/assets.js";
7
+ import { outputPath, projectDir } from "../storage/layout.js";
8
+ import { audioExportArgs } from "../video/audio-export-args.js";
9
+ import { runFfmpeg } from "../video/ffmpeg.js";
10
+ import { audioChapters, audioFileArgs, ffmetadata } from "../voices/audio-files.js";
11
+ import { turnStarts } from "../voices/timing.js";
12
+ import { exportSnapshot, revisionAudio } from "./runtime-export-inputs.js";
13
+ import { preparedResult, publishResult } from "./runtime-publication.js";
14
+ import { wordsSchema } from "./runtime-subtitles.js";
15
+ // The listening files of a multi-voice run: the MP3 and the M4B of the narration timeline,
16
+ // with a chapter at each section of the script where its first turn is heard.
17
+ export async function executeVoicesRecipe(deps, context, piece) {
18
+ if (deps.db
19
+ .prepare("SELECT state FROM revision_work_pieces WHERE id=? AND work_id=?")
20
+ .get(piece.id, context.work.workId)?.state === "done")
21
+ return "done";
22
+ if (!context.maySubmit(piece.id))
23
+ return "held";
24
+ if (piece.key !== "voices:files" || piece.input.kind !== "local")
25
+ throw new Error("Slopify hit an internal error (unknown kind of listening-file step). Try again; if it happens again, use Download diagnostics in Settings and report it.");
26
+ context.signal.throwIfAborted();
27
+ const { view } = exportSnapshot(deps, context, piece);
28
+ const sections = z
29
+ .tuple([z.unknown(), z.unknown(), z.array(z.tuple([z.string(), z.number()])), z.string()])
30
+ // Level the volume's master may follow (`recipe-loudness.ts`).
31
+ .rest(z.unknown())
32
+ .parse(piece.input.values)[2]
33
+ .map(([title, firstTurn]) => ({ title, firstTurn }));
34
+ const timing = view.outputs.find((row) => row.selected &&
35
+ row.available &&
36
+ row.state === "ready" &&
37
+ row.workKey === "subtitles:timing" &&
38
+ row.output.role === "subtitle_words");
39
+ if (timing === undefined)
40
+ throw new Error("The narration's word timing, which the chapter markers come from, is missing. Use More → Render the video again in the Video section, then Try again.");
41
+ const { words } = wordsSchema.parse(JSON.parse(readFileSync(outputPath(deps.paths, context.work.projectId, timing.output.path), "utf8")));
42
+ const audio = await revisionAudio(deps, context, view, { levelled: true });
43
+ if (audio.length === 0)
44
+ throw new Error("The MP3 and M4B files need narration audio, but this project has none. Turn narration on in Edit project, or turn Audio files off under Speakers (Play → Audio, or Edit project → Providers), then Try again.");
45
+ const totalSeconds = audio.reduce((sum, segment) => sum + segment.seconds, 0);
46
+ const chapters = audioChapters({
47
+ title: view.revision.config.title,
48
+ sections,
49
+ turnStarts: turnStarts(words),
50
+ totalSeconds,
51
+ });
52
+ const directory = mkdtempSync(join(projectDir(deps.paths, context.work.projectId), "audio-"));
53
+ const pending = [
54
+ allocateAsset(deps, context.work.projectId, "narration.mp3"),
55
+ allocateAsset(deps, context.work.projectId, "audiobook.m4b"),
56
+ ];
57
+ const prepared = [];
58
+ try {
59
+ const metadata = join(directory, "chapters.txt");
60
+ writeFileSync(metadata, ffmetadata(view.revision.config.title, chapters), { mode: 0o600 });
61
+ // Level the volume: the sound is mixed once and mastered to the audio files' target, and both
62
+ // files are encoded from that (`loudness/model.ts`).
63
+ const goal = masterGoal(view.revision.config, "audioFiles");
64
+ let sound = audio;
65
+ if (goal !== undefined) {
66
+ const mixed = join(directory, "mix.wav");
67
+ await runFfmpeg({
68
+ bin: deps.ffmpeg,
69
+ args: audioExportArgs(audio, mixed),
70
+ signal: context.signal,
71
+ log: deps.log,
72
+ onProgress: () => { },
73
+ });
74
+ const mastered = join(directory, "master.wav");
75
+ await masterFile({ bin: deps.ffmpeg, log: deps.log, signal: context.signal }, mixed, mastered, goal, { sampleRate: 44100, channels: 2 });
76
+ sound = [{ path: mastered, seconds: totalSeconds }];
77
+ }
78
+ for (const [at, kind] of ["mp3", "m4b"].entries()) {
79
+ const target = pending[at];
80
+ if (target === undefined)
81
+ continue;
82
+ await runFfmpeg({
83
+ bin: deps.ffmpeg,
84
+ args: audioFileArgs(sound, metadata, target.absolutePath, kind),
85
+ signal: context.signal,
86
+ log: deps.log,
87
+ onProgress: () => { },
88
+ });
89
+ context.signal.throwIfAborted();
90
+ // What each finished file measures, after its encode.
91
+ const master = goal === undefined
92
+ ? undefined
93
+ : await masterReport({ bin: deps.ffmpeg, log: deps.log, signal: context.signal }, target.absolutePath, goal);
94
+ prepared.push(preparedResult(deps, context, piece, kind === "mp3" ? "audio_mp3" : "audio_m4b", sealAsset(deps, target), Math.round(totalSeconds * 1000), master === undefined ? {} : { master }));
95
+ }
96
+ await publishResult(deps, context, piece, prepared, { chapters });
97
+ return "done";
98
+ }
99
+ finally {
100
+ discardPreparedAssets(deps, [...pending, ...prepared.map((one) => one.asset)]);
101
+ rmSync(directory, { recursive: true, force: true });
102
+ }
103
+ }
@@ -1,4 +1,5 @@
1
1
  import { readFileSync } from "node:fs";
2
+ import { withLanguage } from "../../kernel/ports/languages.js";
2
3
  import { outputPath } from "../storage/layout.js";
3
4
  import { assembleDescription, checkDescriptionAnswer, descriptionMessages, tagsText, } from "../youtube/answer.js";
4
5
  import { defaultDescriptionPrompt } from "../youtube/model.js";
@@ -21,35 +22,35 @@ export async function executeYoutubeRecipe(deps, context, providers, piece) {
21
22
  context.signal.throwIfAborted();
22
23
  const input = piece.input;
23
24
  if (input.kind !== "local" || input.operation !== "youtube-description-v1")
24
- throw new Error("Slopify hit an internal error (the YouTube description step was set up wrongly). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
25
+ throw new Error("Slopify hit an internal error (the YouTube description step was set up wrongly). Try again; if it happens again, use Download diagnostics in Settings and report it.");
25
26
  const { view } = exportSnapshot(deps, context, piece);
26
27
  const config = view.revision.config;
27
28
  const llm = config.llm;
28
29
  if (llm === undefined || llm.provider.trim() === "" || llm.model.trim() === "")
29
- throw new Error("The YouTube description needs an AI text model, and none is chosen. Choose one in Edit project → Providers, then Retry stage.");
30
+ throw new Error("The YouTube description needs an AI text model, and none is chosen. Choose one in Edit project → Providers, then Try again.");
30
31
  const timing = view.outputs.find((row) => row.selected &&
31
32
  row.available &&
32
33
  row.state === "ready" &&
33
34
  row.workKey === "subtitles:timing" &&
34
35
  row.output.role === "subtitle_words");
35
36
  if (timing === undefined)
36
- throw new Error("The narration's word timing, which the YouTube description's chapters come from, is missing. Use Re-run section on Video, then Retry stage.");
37
+ throw new Error("The narration's word timing, which the YouTube description's chapters come from, is missing. Use More → Render the video again in the Video section, then Try again.");
37
38
  const { words } = wordsSchema.parse(JSON.parse(readFileSync(outputPath(deps.paths, context.work.projectId, timing.output.path), "utf8")));
38
39
  const passages = transcriptPassages(words);
39
40
  if (passages.length === 0)
40
- throw new Error("The narration's word timing holds no words, so there is nothing to write the YouTube description from. Use Re-run section on Video, then Retry stage.");
41
+ throw new Error("The narration's word timing holds no words, so there is nothing to write the YouTube description from. Use More → Render the video again in the Video section, then Try again.");
41
42
  // The same timeline the timing walked: silence at the start, intro, gaps, body, outro and
42
43
  // silence at the end.
43
44
  const audio = await revisionAudio(deps, context, view);
44
45
  const durationSeconds = audio.reduce((sum, segment) => sum + segment.seconds, 0);
45
46
  const saved = Array.isArray(input.values) ? input.values[4] : undefined;
46
47
  const instruction = typeof saved === "string" && saved.trim() !== "" ? saved : defaultDescriptionPrompt;
47
- const messages = descriptionMessages({
48
+ const messages = withLanguage(descriptionMessages({
48
49
  instruction,
49
50
  title: config.title,
50
51
  durationSeconds,
51
52
  transcript: transcriptText(passages),
52
- });
53
+ }), config.language);
53
54
  const row = deps.db
54
55
  .prepare("SELECT recipe_context FROM revision_work WHERE id=?")
55
56
  .get(context.work.workId);
@@ -1,4 +1,5 @@
1
1
  import { checkRuntimeModel } from "../../catalog/runtime-models.js";
2
+ import { referenceRefusal, takesReferenceImage } from "../../catalog/schema.js";
2
3
  import { readinessIsUsable } from "../../kernel/ports/model.js";
3
4
  import { cliPathChanged } from "../settings/cli-paths.js";
4
5
  import { isLocalCliProvider } from "../settings/model.js";
@@ -38,7 +39,7 @@ export async function checkReadiness(deps, snapshot, view) {
38
39
  });
39
40
  continue;
40
41
  }
41
- const result = await checkRuntimeModel(deps.modelsFor, choice.provider, choice.family, choice.model, choice.family === "llm" ? (choice.thinking ?? undefined) : undefined);
42
+ const result = await checkRuntimeModel(deps.modelsFor, choice.provider, choice.family, choice.model, choice.family === "tts" ? undefined : (choice.thinking ?? undefined));
42
43
  if (result === "missing")
43
44
  fields.push({
44
45
  field: `${choice.family}.model`,
@@ -112,6 +113,19 @@ export function localReadiness(deps, snapshot, view, providers, catalogue) {
112
113
  field: `work.${recipe.key}.aspect`,
113
114
  message: "The model does not support this output shape.",
114
115
  });
116
+ // Only the keyed providers' models are listed; the Codex CLI always takes a reference.
117
+ if (model !== undefined &&
118
+ input.reference !== undefined &&
119
+ !takesReferenceImage(model) &&
120
+ !fields.some((row) => row.field === "reference.source"))
121
+ fields.push({ field: "reference.source", message: referenceRefusal });
122
+ if (model !== undefined &&
123
+ input.thinking !== undefined &&
124
+ !fields.some((row) => row.field === "images.thinking"))
125
+ fields.push({
126
+ field: "images.thinking",
127
+ message: "This image model has no effort setting. Set the images' Effort back to Model default under Providers.",
128
+ });
115
129
  }
116
130
  }
117
131
  return fields;
@@ -0,0 +1,49 @@
1
+ // Soften and retry: an image prompt a content filter refused is reworded by the project's
2
+ // AI model, inside the image step's own run (so the call goes through the retry policy and
3
+ // the new wording streams onto the step's live panel), then drawn again. A request is kept
4
+ // per step until an image is made from the softened prompt.
5
+ // The steps of a stage whose last attempt a content filter refused, on the current run.
6
+ export function refusedKeys(db, projectId, stage) {
7
+ return db
8
+ .prepare(`SELECT DISTINCT p.work_key FROM revision_work_pieces p
9
+ JOIN revision_work w ON w.id=p.work_id
10
+ JOIN revision_work_reservations r ON r.work_id=w.id
11
+ JOIN project_heads h ON h.project_id=r.project_id AND h.revision_id=r.revision_id
12
+ WHERE w.project_id=? AND w.kind=? AND w.state='failed' AND p.state!='done'
13
+ AND (SELECT a.outcome FROM attempts a WHERE a.work_piece_id=p.id ORDER BY a.rowid DESC LIMIT 1)='refusal'
14
+ ORDER BY p.work_key`)
15
+ .all(projectId, stage)
16
+ .map((row) => String(row.work_key));
17
+ }
18
+ export function requestSoftening(db, projectId, keys, at) {
19
+ for (const key of keys)
20
+ db.prepare("INSERT INTO prompt_softening(project_id,work_key,requested_at) VALUES (?,?,?) ON CONFLICT(project_id,work_key) DO UPDATE SET requested_at=excluded.requested_at").run(projectId, key, at);
21
+ }
22
+ export function softeningRequested(db, projectId, key) {
23
+ return (db
24
+ .prepare("SELECT 1 FROM prompt_softening WHERE project_id=? AND work_key=?")
25
+ .get(projectId, key) !== undefined);
26
+ }
27
+ export function clearSoftening(db, projectId, keys) {
28
+ for (const key of keys)
29
+ db.prepare("DELETE FROM prompt_softening WHERE project_id=? AND work_key=?").run(projectId, key);
30
+ }
31
+ export function softenMessages(prompt) {
32
+ return [
33
+ {
34
+ role: "user",
35
+ content: `An image generator's content filter refused the image prompt below. Rewrite it so it keeps the same scene, subject, composition and style but leaves out whatever a filter could flag: graphic violence, gore, nudity or sexual content, real people's names, weapons aimed at people, self-harm and hateful symbols. Describe such things indirectly or leave them out. Answer with the rewritten prompt only, no preamble and no quotes.
36
+
37
+ Prompt:
38
+ ${prompt}`,
39
+ },
40
+ ];
41
+ }
42
+ // The model's answer, trimmed of the quotes and labels models add anyway.
43
+ export function softenedPrompt(answer) {
44
+ return answer
45
+ .trim()
46
+ .replace(/^(?:rewritten prompt|prompt)\s*:\s*/i, "")
47
+ .replace(/^["“](.*)["”]$/s, "$1")
48
+ .trim();
49
+ }
@@ -130,18 +130,25 @@ function existingWork(db, projectId, key, fingerprint) {
130
130
  : { workId: z.string().parse(row.work_id), pieceId: z.string().parse(row.piece_id) };
131
131
  }
132
132
  function stageForKey(key) {
133
+ // A review runs in its item's stage; the narration's review in Audio.
134
+ if (key === "review:narration")
135
+ return "audio";
136
+ if (key.startsWith("review:shorts:"))
137
+ return "video";
138
+ if (key.startsWith("review:"))
139
+ return stageForKey(key.slice("review:".length));
133
140
  const prefix = key.split(":")[0];
134
141
  if (prefix === "research")
135
142
  return "research";
136
- if (prefix === "article" || prefix === "entry")
143
+ if (prefix === "article" || prefix === "entry" || prefix === "script")
137
144
  return "article";
138
145
  if (prefix === "audio" || prefix === "narration")
139
146
  return "audio";
140
- if (prefix === "image")
147
+ if (prefix === "image" || prefix === "reference")
141
148
  return "images";
142
149
  if (prefix === "thumbnail")
143
150
  return "thumbnail";
144
- if (prefix === "export" || prefix === "subtitles" || prefix === "video")
151
+ if (prefix === "export" || prefix === "subtitles" || prefix === "video" || prefix === "voices")
145
152
  return "video";
146
153
  if (prefix === "document")
147
154
  return "document";
@@ -10,7 +10,7 @@ import { chaptersFrom, plannerMessages, subAgentMessages } from "./planner.js";
10
10
  import { sourcedAnswer, synthesisMessages } from "./synthesis.js";
11
11
  // A model that cannot ground on the web fails the stage immediately, with no fallback to
12
12
  // what the model already knows.
13
- export const webResearchUnsupported = "The chosen AI model can't search the web, which research needs. Choose a model with web search in Edit project → Providers, or turn research off, then Retry stage.";
13
+ export const webResearchUnsupported = "The chosen AI model can't search the web, which research needs. Choose a model with web search in Edit project → Providers, or turn research off, then Try again.";
14
14
  // What a chapter piece carries between runs: its title, and its notes once a sub-agent
15
15
  // has answered. That is the whole of the resume.
16
16
  const chapterPayload = z.object({ title: z.string(), notes: z.string().optional() });
@@ -25,7 +25,7 @@ export async function runResearch(deps, context, providers) {
25
25
  if (choice === undefined || articlePrompt === undefined) {
26
26
  // Admission refuses a run whose research is Generate without both, so
27
27
  // reaching here is a bug in admission rather than something the user did.
28
- throw new Error("No AI model or article prompt is set for research. Choose a model in Edit project → Providers and check Edit project → Prompts, then Retry stage.");
28
+ throw new Error("No AI model or article prompt is set for research. Choose a model in Edit project → Providers and check Edit project → Prompts, then Try again.");
29
29
  }
30
30
  const brief = { articlePrompt, values: project.config.values };
31
31
  try {
@@ -111,7 +111,7 @@ async function plan(deps, context, providers, choice, brief, add) {
111
111
  previewLabel: "Planning research",
112
112
  // An empty answer, or one with no chapter in it, is a failed attempt.
113
113
  check: (given) => chaptersFrom(given.text).length === 0
114
- ? "The AI model didn't return any research topics. Retry stage; if it keeps happening, choose a different model in Edit project → Providers."
114
+ ? "The AI model didn't return any research topics. Try again; if it keeps happening, choose a different model in Edit project → Providers."
115
115
  : undefined,
116
116
  });
117
117
  if (!answer.ok)
@@ -217,7 +217,7 @@ function section(label, messages) {
217
217
  }
218
218
  function payloadOf(piece) {
219
219
  if (piece.payload === null) {
220
- throw new Error("Slopify hit an internal error (a saved research topic is empty). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
220
+ throw new Error("Slopify hit an internal error (a saved research topic is empty). Try again; if it happens again, use Download diagnostics in Settings and report it.");
221
221
  }
222
222
  return chapterPayload.parse(JSON.parse(piece.payload));
223
223
  }
@@ -41,9 +41,9 @@ export function endsWithSources(text) {
41
41
  // but cannot be used is a failed attempt, so the wrapper retries it.
42
42
  export function sourcedAnswer(who, text) {
43
43
  if (text.trim() === "") {
44
- return `The AI model returned an empty answer for ${who}. Retry stage; if it keeps happening, choose a different model in Edit project → Providers.`;
44
+ return `The AI model returned an empty answer for ${who}. Try again; if it keeps happening, choose a different model in Edit project → Providers.`;
45
45
  }
46
46
  return endsWithSources(text)
47
47
  ? undefined
48
- : `The AI model's answer for ${who} had no sources. Research needs web sources: Retry stage, or choose a model with web search in Edit project → Providers.`;
48
+ : `The AI model's answer for ${who} had no sources. Research needs web sources: Try again, or choose a model with web search in Edit project → Providers.`;
49
49
  }
@@ -0,0 +1,105 @@
1
+ // Automatic reviews: a reviewer model looks at each finished item of a stage (the article,
2
+ // every image, the narration, the thumbnail, every short) before the run moves on, and says
3
+ // pass or fail with its reasons. A failed item is flagged, or made again while its retries
4
+ // last. Browser-safe: Play, Edit project and the project page read these names and limits too.
5
+ export const reviewStages = ["article", "images", "narration", "thumbnail", "shorts"];
6
+ // Off is the same as the stage having no entry, which is what every project saved before
7
+ // reviews existed has.
8
+ export const reviewModes = ["off", "flag", "redo"];
9
+ // ceiling: the owner's numbers. Two redos is what a person would try before looking at it
10
+ // themselves; five is already six paid images for one slot.
11
+ export const defaultReviewRetries = 2;
12
+ export const reviewRetriesMin = 0;
13
+ export const reviewRetriesMax = 5;
14
+ export const reviewStageLabels = {
15
+ article: "Article",
16
+ images: "Images",
17
+ narration: "Narration",
18
+ thumbnail: "Thumbnail",
19
+ shorts: "Shorts",
20
+ };
21
+ export const reviewModeLabels = {
22
+ off: "Off",
23
+ flag: "Flag only",
24
+ redo: "Flag and redo",
25
+ };
26
+ // The stages whose items are pictures, so the reviewer has to be shown them.
27
+ export const visionStages = ["images", "thumbnail", "shorts"];
28
+ // The text providers whose adapter hands the model image files (`capabilities.images` in
29
+ // `adapters/llm`): Claude Code reads them with its Read tool in a private folder, Codex
30
+ // attaches them with `--image`. The Gemini CLI and OpenRouter adapters send text only.
31
+ export const imageReviewers = ["claude-code", "codex"];
32
+ export function reviewModeOf(settings, stage) {
33
+ return settings?.stages[stage]?.mode ?? "off";
34
+ }
35
+ export function reviewRetriesOf(settings) {
36
+ return settings?.retries ?? defaultReviewRetries;
37
+ }
38
+ export function reviewedStages(settings) {
39
+ return reviewStages.filter((stage) => reviewModeOf(settings, stage) !== "off");
40
+ }
41
+ // The key a stage's picked Review prompt renders under in `config.rendered`.
42
+ export function reviewPromptKey(stage) {
43
+ return `review.${stage}`;
44
+ }
45
+ // A review's step is `review:` and the key of the item it reviews. The narration is
46
+ // reviewed as a whole, so its item key names no step.
47
+ export const narrationItemKey = "narration";
48
+ export function reviewKey(itemKey) {
49
+ return `review:${itemKey}`;
50
+ }
51
+ export function reviewedItemKey(key) {
52
+ return key.startsWith("review:") ? key.slice("review:".length) : undefined;
53
+ }
54
+ export function reviewSettingsForm(settings) {
55
+ return {
56
+ provider: settings.provider,
57
+ model: settings.model,
58
+ ...(settings.thinking === undefined ? {} : { thinking: settings.thinking }),
59
+ retries: settings.retries === undefined ? "" : String(settings.retries),
60
+ stages: Object.fromEntries(reviewStages.flatMap((stage) => {
61
+ const picked = settings.stages[stage];
62
+ return picked === undefined
63
+ ? []
64
+ : [[stage, { mode: picked.mode, prompt: picked.prompt ?? "" }]];
65
+ })),
66
+ };
67
+ }
68
+ // The form back to settings: only the reviews of stages that make something (`makes`), and
69
+ // none left is no reviews at all, which is what every project saved before them has. Retries
70
+ // that are not a number are left out and reported, so the form marks the field.
71
+ export function reviewSettingsFromForm(form, makes) {
72
+ const stages = Object.fromEntries(reviewStages.flatMap((stage) => {
73
+ const picked = form.stages[stage];
74
+ if (picked === undefined || picked.mode === "off" || !makes(stage))
75
+ return [];
76
+ return [
77
+ [stage, { mode: picked.mode, ...(picked.prompt.trim() ? { prompt: picked.prompt } : {}) }],
78
+ ];
79
+ }));
80
+ if (Object.keys(stages).length === 0)
81
+ return { settings: undefined };
82
+ const retries = form.retries.trim() === "" ? undefined : Number(form.retries);
83
+ const valid = retries === undefined || Number.isFinite(retries);
84
+ return {
85
+ settings: {
86
+ provider: form.provider,
87
+ model: form.model,
88
+ ...(form.thinking === undefined ? {} : { thinking: form.thinking }),
89
+ ...(retries === undefined || !valid ? {} : { retries }),
90
+ stages,
91
+ },
92
+ ...(valid
93
+ ? {}
94
+ : {
95
+ retriesProblem: `Enter a number of redos between ${String(reviewRetriesMin)} and ${String(reviewRetriesMax)} in the Reviews section.`,
96
+ }),
97
+ };
98
+ }
99
+ export const defaultReviewPrompts = {
100
+ article: "Review this article against the article prompt it was written from. Fail it when it breaks the prompt's rules on length, structure or tone; when it states names, places, events or facts that neither the prompt nor the research notes support (invented lore); or when a section repeats another section's content. Small style preferences are not a reason to fail it.",
101
+ images: "Review this image against the brief it was drawn from. Fail it when a hand or body is malformed (extra or missing fingers, fused limbs, broken anatomy); when it shows stray text, letters, captions or watermarks the brief did not ask for; when it does not show what the brief asks for; or, when an establishing image is attached, when it does not match it (a different character, palette or style).",
102
+ narration: "Review this narration against the text it was read from. The transcript was heard back from the audio; the gaps list stretches of text the timing could not hear. Fail it when words, sentences or passages are missing or garbled in a way a listener would notice. Ignore punctuation, numbers written out, and small differences in how words were transcribed.",
103
+ thumbnail: "Review this thumbnail as it would be seen on a phone, about 320 pixels wide. Fail it when its text (if any) cannot be read at that size, when the subject is unclear or cluttered, when a hand or body is malformed, or when it shows stray letters or watermarks.",
104
+ shorts: "Review these images from one vertical short, as they would be seen full screen on a phone. Fail them when the subject is unclear, when a hand or body is malformed, when they show stray text, letters or watermarks, or when they do not fit the short's title.",
105
+ };