@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,5 +1,8 @@
1
1
  import { copyFileSync, mkdirSync, mkdtempSync, rmSync } from "node:fs";
2
2
  import { extname, join, relative, sep } from "node:path";
3
+ import { usesShortMode } from "../admission/short-mode.js";
4
+ import { masterFile, masterReport } from "../loudness/loudnorm.js";
5
+ import { masterGoal } from "../loudness/model.js";
3
6
  import { allocateAsset, discardPreparedAssets, sealAsset } from "../storage/assets.js";
4
7
  import { outputPath, projectDir } from "../storage/layout.js";
5
8
  import { audioExportArgs } from "../video/audio-export.js";
@@ -7,8 +10,11 @@ import { withPaths } from "../video/edit-list.js";
7
10
  import { runFfmpeg } from "../video/ffmpeg.js";
8
11
  import { planRender } from "../video/plan.js";
9
12
  import { renderSlideshow } from "../video/slideshow.js";
13
+ import { writePortraits } from "../voices/portraits.js";
14
+ import { exportBed } from "./runtime-export-bed.js";
10
15
  import { exportEdit } from "./runtime-export-edit.js";
11
16
  import { exportSnapshot, retainedOutput, revisionAudio, } from "./runtime-export-inputs.js";
17
+ import { executeShortExport } from "./runtime-export-short.js";
12
18
  import { preparedResult, preparedText, publishResult } from "./runtime-publication.js";
13
19
  export async function executeExportRecipe(deps, context, piece) {
14
20
  if (deps.db
@@ -20,12 +26,14 @@ export async function executeExportRecipe(deps, context, piece) {
20
26
  context.signal.throwIfAborted();
21
27
  const snapshot = exportSnapshot(deps, context, piece);
22
28
  const { view } = snapshot;
23
- const audio = await revisionAudio(deps, context, view);
29
+ const audio = await revisionAudio(deps, context, view, { levelled: true });
24
30
  const wav = piece.key === "export:wav";
25
31
  if (!wav && piece.key !== "export:video")
26
- throw new Error("Slopify hit an internal error (unknown kind of export). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
32
+ throw new Error("Slopify hit an internal error (unknown kind of export). Try again; if it happens again, use Download diagnostics in Settings and report it.");
27
33
  if (wav && audio.length === 0)
28
- throw new Error("The audio export has nothing to export because narration is turned off for this project. Turn narration on in Edit project, then Retry stage.");
34
+ throw new Error("The audio export has nothing to export because narration is turned off for this project. Turn narration on in Edit project, then Try again.");
35
+ if (!wav && usesShortMode(view.revision.config))
36
+ return executeShortExport(deps, context, piece, snapshot, audio);
29
37
  const filename = wav ? "audio.wav" : "video.mp4";
30
38
  const pending = allocateAsset(deps, context.work.projectId, filename);
31
39
  const prepared = [];
@@ -35,6 +43,9 @@ export async function executeExportRecipe(deps, context, piece) {
35
43
  const burn = !wav && view.revision.config.subtitles?.mode === "burn-in";
36
44
  if (burn)
37
45
  directory = captionDirectory(deps, context, snapshot);
46
+ const portraits = directory === undefined
47
+ ? undefined
48
+ : writePortraits(deps.db, view.revision.config, directory);
38
49
  const mode = burn
39
50
  ? "burn-in"
40
51
  : captions.some((row) => row.output.role === "subtitles_srt") &&
@@ -48,13 +59,9 @@ export async function executeExportRecipe(deps, context, piece) {
48
59
  ? undefined
49
60
  : { path: row.path, seconds: row.seconds };
50
61
  };
51
- const images = (wav ? [] : view.revision.content.imageOrder).map((key) => {
52
- const row = view.outputs.find((one) => one.workKey === `image:${key}` && one.selected && one.available && one.state === "ready");
53
- if (row === undefined)
54
- throw new Error("The video export can't find one of the slideshow images. Use Re-run section on Images (or regenerate that image in Edit project → Images), then Retry stage.");
55
- return outputPath(deps.paths, context.work.projectId, row.output.path);
56
- });
62
+ const images = wav ? [] : slideshowImages(deps, context, view);
57
63
  const edited = wav ? undefined : await exportEdit(deps, context, view, images);
64
+ const bed = wav ? undefined : await exportBed(deps, context, view);
58
65
  const plan = wav
59
66
  ? undefined
60
67
  : planRender({
@@ -70,6 +77,7 @@ export async function executeExportRecipe(deps, context, piece) {
70
77
  images,
71
78
  output: pending.absolutePath,
72
79
  edit: edited?.edit,
80
+ bed,
73
81
  });
74
82
  const totalSeconds = plan?.totalSeconds ?? audio.reduce((sum, row) => sum + row.seconds, 0);
75
83
  const projectDirectory = projectDir(deps.paths, context.work.projectId);
@@ -93,6 +101,7 @@ export async function executeExportRecipe(deps, context, piece) {
93
101
  output: pending.path,
94
102
  editList: withPaths(plan.editList, relativePath),
95
103
  subtitles: config.subtitles,
104
+ ...(portraits === undefined ? {} : { speakerPortraits: portraits.recorded }),
96
105
  ...(edited?.settings === undefined ? {} : { videoEdit: edited.settings }),
97
106
  ...(edited === undefined || edited.warnings.length === 0
98
107
  ? {}
@@ -107,25 +116,71 @@ export async function executeExportRecipe(deps, context, piece) {
107
116
  current: Math.min(100, Math.round(elapsedMs / (totalSeconds * 10))),
108
117
  total: 100,
109
118
  });
110
- if (plan === undefined)
111
- await runFfmpeg({
112
- bin: deps.ffmpeg,
113
- args: audioExportArgs(audio, pending.absolutePath),
114
- signal: context.signal,
115
- log: deps.log,
116
- onProgress,
117
- });
119
+ // Level the volume: the audio-only file is mastered to the audio files' target, the video to
120
+ // the video's (`loudness/model.ts`).
121
+ const goal = masterGoal(config, plan === undefined ? "audioFiles" : "video");
122
+ let master;
123
+ if (plan === undefined) {
124
+ const mixed = goal === undefined ? pending.absolutePath : `${pending.absolutePath}.mix.wav`;
125
+ try {
126
+ await runFfmpeg({
127
+ bin: deps.ffmpeg,
128
+ args: audioExportArgs(audio, mixed),
129
+ signal: context.signal,
130
+ log: deps.log,
131
+ onProgress,
132
+ });
133
+ if (goal !== undefined) {
134
+ const run = { bin: deps.ffmpeg, log: deps.log, signal: context.signal };
135
+ await masterFile(run, mixed, `${pending.absolutePath}.master.wav`, goal, {
136
+ sampleRate: 48000,
137
+ channels: 2,
138
+ });
139
+ // The export is 16-bit PCM, as it always was.
140
+ await runFfmpeg({
141
+ bin: deps.ffmpeg,
142
+ args: [
143
+ "-hide_banner",
144
+ "-nostdin",
145
+ "-loglevel",
146
+ "error",
147
+ "-nostats",
148
+ "-y",
149
+ "-i",
150
+ `${pending.absolutePath}.master.wav`,
151
+ "-c:a",
152
+ "pcm_s16le",
153
+ "-f",
154
+ "wav",
155
+ pending.absolutePath,
156
+ ],
157
+ signal: context.signal,
158
+ log: deps.log,
159
+ onProgress: () => { },
160
+ });
161
+ master = await masterReport(run, pending.absolutePath, goal);
162
+ }
163
+ }
164
+ finally {
165
+ if (goal !== undefined) {
166
+ rmSync(mixed, { force: true });
167
+ rmSync(`${pending.absolutePath}.master.wav`, { force: true });
168
+ }
169
+ }
170
+ }
118
171
  else
119
- await renderSlideshow({
172
+ master = await renderSlideshow({
120
173
  bin: deps.ffmpeg,
121
174
  edit: plan.editList,
122
175
  output: plan.output,
123
176
  burnSubtitles: burn,
124
177
  cwd: directory,
178
+ ...(portraits === undefined ? {} : { portraits: portraits.overlays }),
125
179
  scratch: projectDirectory,
126
180
  signal: context.signal,
127
181
  log: deps.log,
128
182
  onProgress,
183
+ ...(goal === undefined ? {} : { master: goal }),
129
184
  });
130
185
  context.signal.throwIfAborted();
131
186
  const asset = sealAsset(deps, pending);
@@ -134,6 +189,7 @@ export async function executeExportRecipe(deps, context, piece) {
134
189
  ...(edited === undefined || edited.warnings.length === 0
135
190
  ? {}
136
191
  : { warnings: edited.warnings }),
192
+ ...(master === undefined ? {} : { master }),
137
193
  }));
138
194
  prepared.push(preparedText(deps, context, piece, "render_params", "render.json", JSON.stringify(record)));
139
195
  prepared.push(...captions.map((row) => retainedOutput(deps, view, row)));
@@ -147,12 +203,21 @@ export async function executeExportRecipe(deps, context, piece) {
147
203
  rmSync(directory, { recursive: true, force: true });
148
204
  }
149
205
  }
206
+ // The project's images in slideshow order, as files.
207
+ export function slideshowImages(deps, context, view) {
208
+ return view.revision.content.imageOrder.map((key) => {
209
+ const row = view.outputs.find((one) => one.workKey === `image:${key}` && one.selected && one.available && one.state === "ready");
210
+ if (row === undefined)
211
+ throw new Error("The video export can't find one of the slideshow images. Use More → make it again in its section on Images (or regenerate that image in Edit project → Images), then Try again.");
212
+ return outputPath(deps.paths, context.work.projectId, row.output.path);
213
+ });
214
+ }
150
215
  function captionDirectory(deps, context, snapshot) {
151
216
  const find = (role) => snapshot.view.outputs.find((row) => row.selected && row.available && row.state === "ready" && row.output.role === role);
152
217
  const ass = find("subtitle_ass");
153
218
  const font = find("subtitle_font");
154
219
  if (ass === undefined || font === undefined)
155
- throw new Error("Burned-in captions are missing their caption file or font. Use Re-run section on Video (or choose the caption font again in Edit project → Subtitles), then Retry stage.");
220
+ throw new Error("Burned-in captions are missing their caption file or font. Use More → Render the video again in the Video section (or choose the caption font again in Edit project → Subtitles), then Try again.");
156
221
  const directory = mkdtempSync(join(projectDir(deps.paths, context.work.projectId), "render-"));
157
222
  try {
158
223
  mkdirSync(join(directory, "fonts"), { mode: 0o700 });
@@ -0,0 +1,87 @@
1
+ import { copyFileSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { z } from "zod";
4
+ import { captionFont, captionFontDeps } from "../fonts/coverage.js";
5
+ import { describedKinds } from "../narration/blocks.js";
6
+ import { allocateAsset, discardPreparedAssets, sealAsset } from "../storage/assets.js";
7
+ import { projectDir } from "../storage/layout.js";
8
+ import { defaultCardStyle, renderCard } from "../video/figure-card.js";
9
+ import { articlePicture } from "./runtime-pictures.js";
10
+ import { executionView } from "./runtime-plan.js";
11
+ import { preparedResult, publishResult } from "./runtime-publication.js";
12
+ // A `figure:card:<n>` step: the block drawn as a card in each frame the project needs (the
13
+ // video's, and upright for the Shorts of a 16:9 video), with the ffmpeg Slopify ships.
14
+ const values = z.object({
15
+ kind: z.enum(describedKinds),
16
+ index: z.number().int().positive(),
17
+ source: z.string(),
18
+ section: z.string().nullable(),
19
+ image: z.string().nullable(),
20
+ formats: z.array(z.enum(["16:9", "9:16"])).min(1),
21
+ fontId: z.string(),
22
+ color: z.string().nullable(),
23
+ language: z.string(),
24
+ });
25
+ const frames = { "16:9": [1920, 1080], "9:16": [1080, 1920] };
26
+ export async function executeFigureCard(deps, context, piece) {
27
+ if (piece.input.kind !== "local")
28
+ throw new Error("Slopify hit an internal error (a figure card step was set up wrongly). Try again; if it happens again, use Download diagnostics in Settings and report it.");
29
+ const card = values.parse(piece.input.values);
30
+ const view = executionView(deps, context.work.projectId, context.work.revisionId);
31
+ // The article's own picture for a figure, when the project has it as an upload.
32
+ const picture = card.kind === "figure" && view !== undefined
33
+ ? articlePicture(deps, view, card.image ?? undefined)
34
+ : undefined;
35
+ const font = await captionFont(captionFontDeps(deps.paths), card.fontId, card.language === "en" ? undefined : card.language);
36
+ const workspace = mkdtempSync(join(projectDir(deps.paths, context.work.projectId), "card-"));
37
+ const pending = card.formats.map(() => allocateAsset(deps, context.work.projectId, `card-${String(card.index)}.png`));
38
+ const prepared = [];
39
+ try {
40
+ mkdirSync(join(workspace, "fonts"), { mode: 0o700 });
41
+ copyFileSync(font.path, join(workspace, "fonts", `card${font.extension}`));
42
+ for (const [at, format] of card.formats.entries()) {
43
+ const target = pending[at];
44
+ if (target === undefined)
45
+ continue;
46
+ const [width, height] = frames[format];
47
+ await renderCard({
48
+ bin: deps.ffmpeg,
49
+ input: {
50
+ kind: card.kind,
51
+ source: card.source,
52
+ section: card.section,
53
+ width,
54
+ height,
55
+ style: {
56
+ ...defaultCardStyle,
57
+ font: font.assName,
58
+ ...(card.color === null ? {} : { accent: card.color }),
59
+ },
60
+ picture: picture !== undefined,
61
+ },
62
+ cwd: workspace,
63
+ output: target.absolutePath,
64
+ picture,
65
+ signal: context.signal,
66
+ log: deps.log,
67
+ }, (ass) => writeFileSync(join(workspace, "card.ass"), ass, { mode: 0o600 }));
68
+ context.signal.throwIfAborted();
69
+ const asset = sealAsset(deps, target);
70
+ const output = preparedResult(deps, context, piece, "figure_card", asset, null, {
71
+ index: card.index,
72
+ format,
73
+ });
74
+ prepared.push({ ...output, slot: `${piece.key}:${format}` });
75
+ }
76
+ await publishResult(deps, context, piece, prepared, {
77
+ index: card.index,
78
+ kind: card.kind,
79
+ picture: picture !== undefined,
80
+ });
81
+ return "done";
82
+ }
83
+ finally {
84
+ rmSync(workspace, { recursive: true, force: true });
85
+ discardPreparedAssets(deps, [...pending, ...prepared.map((one) => one.asset)]);
86
+ }
87
+ }
@@ -0,0 +1,69 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { z } from "zod";
3
+ import { imageBlob } from "../channels/repo.js";
4
+ import { outputPath } from "../storage/layout.js";
5
+ // The provider call an image step makes: its request, the effort when one is chosen and, while
6
+ // the project's Establishing image is on, that image's bytes as the visual reference.
7
+ export function imageCall(deps, projectId, input, previewLabel) {
8
+ const reference = referenceImage(deps, projectId, input);
9
+ const cast = castImages(deps, input);
10
+ return {
11
+ provider: input.provider,
12
+ model: input.model,
13
+ prompt: input.prompt,
14
+ aspect: input.aspect,
15
+ ...(input.thinking === undefined ? {} : { thinking: input.thinking }),
16
+ ...(reference === undefined ? {} : { reference }),
17
+ ...(cast === undefined ? {} : { cast }),
18
+ ...(previewLabel === undefined ? {} : { previewLabel }),
19
+ };
20
+ }
21
+ function referenceImage(deps, projectId, input) {
22
+ if (input.reference === undefined)
23
+ return undefined;
24
+ // The step waits for the establishing image, so a missing one went away since.
25
+ const missing = "The establishing image this image is drawn from is missing. Use Regenerate on the establishing image in the Images section (or upload it again in Edit project → Images), then use Try again.";
26
+ if (input.reference.assetId === null)
27
+ throw new Error(missing);
28
+ const row = z
29
+ .object({ path: z.string() })
30
+ .optional()
31
+ .parse(deps.db
32
+ .prepare("SELECT path FROM project_assets WHERE id=? AND project_id=?")
33
+ .get(input.reference.assetId, projectId));
34
+ if (row === undefined)
35
+ throw new Error(missing);
36
+ let bytes;
37
+ try {
38
+ bytes = new Uint8Array(readFileSync(outputPath(deps.paths, projectId, row.path)));
39
+ }
40
+ catch {
41
+ throw new Error(missing);
42
+ }
43
+ const mime = imageMime(bytes);
44
+ if (mime === undefined)
45
+ throw new Error("The establishing image is not a PNG or JPEG file, so it can't be sent as a reference. Upload a PNG or JPEG in Edit project → Images → Establishing image, then use Try again.");
46
+ return { bytes, mime };
47
+ }
48
+ // The cast pictures the request names, read by hash from the database. They are never removed
49
+ // while a project may name them, so a missing one came from a backup of another install.
50
+ function castImages(deps, input) {
51
+ if (input.cast === undefined)
52
+ return undefined;
53
+ return input.cast.map((member) => ({
54
+ name: member.name,
55
+ description: member.description,
56
+ images: member.images.map((sha256) => {
57
+ const blob = imageBlob(deps.db, sha256);
58
+ if (blob === undefined)
59
+ throw new Error(`A picture of ${member.name} that this project was started with is not in this Slopify's database, so it can't be sent as a reference. Add the picture again in Channels → Cast, then start the video again from Play.`);
60
+ return blob;
61
+ }),
62
+ }));
63
+ }
64
+ function imageMime(bytes) {
65
+ const png = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
66
+ if (png.every((value, index) => bytes[index] === value))
67
+ return "image/png";
68
+ return bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff ? "image/jpeg" : undefined;
69
+ }
@@ -1,11 +1,18 @@
1
- import { statSync } from "node:fs";
1
+ import { rmSync, statSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
3
  import { z } from "zod";
4
+ import { referenceKey } from "../admission/model.js";
4
5
  import { plainText } from "../article/plain.js";
5
6
  import { splitEndMatter } from "../article/split.js";
7
+ import { levelPieces } from "../loudness/level-pieces.js";
6
8
  import { joinNarration } from "../narration/concat.js";
9
+ import { applyPauses, planPieces } from "../narration/pauses.js";
7
10
  import { allocateAsset, discardPreparedAssets, sealAsset } from "../storage/assets.js";
8
11
  import { outputPath } from "../storage/layout.js";
12
+ import { probeDurationMs, runFfmpeg } from "../video/ffmpeg.js";
13
+ import { turnJoinArgs } from "../voices/join.js";
14
+ import { levelOperation } from "./recipe-loudness.js";
15
+ import { executeFigureCard } from "./runtime-figure-card.js";
9
16
  import { publishNarrationText } from "./runtime-narration-text.js";
10
17
  import { executionPlan, executionView, savedCatalogue } from "./runtime-plan.js";
11
18
  import { preparedResult, preparedText, preparedTexts, publishResult, } from "./runtime-publication.js";
@@ -19,7 +26,7 @@ export async function executeLocalRecipe(deps, context, piece) {
19
26
  const input = piece.input;
20
27
  if (input.kind === "provided") {
21
28
  if (input.assetId === null)
22
- throw new Error("A file you uploaded for this project is missing. Upload it again in Edit project, then Retry stage.");
29
+ throw new Error("A file you uploaded for this project is missing. Upload it again in Edit project, then Try again.");
23
30
  const row = z
24
31
  .object({ id: z.string(), project_id: z.string(), path: z.string(), created_at: z.string() })
25
32
  .parse(deps.db
@@ -36,19 +43,25 @@ export async function executeLocalRecipe(deps, context, piece) {
36
43
  ? "audio_body"
37
44
  : piece.key === "thumbnail:image"
38
45
  ? "thumbnail"
39
- : piece.key.startsWith("image:")
40
- ? "image"
41
- : undefined;
46
+ : piece.key === referenceKey
47
+ ? "reference"
48
+ : piece.key.startsWith("image:")
49
+ ? "image"
50
+ : undefined;
42
51
  await publishResult(deps, context, piece, role === undefined ? [] : [preparedResult(deps, context, piece, role, asset)], {}, asset);
43
52
  return "done";
44
53
  }
45
54
  if (input.kind !== "local")
46
- throw new Error("Slopify hit an internal error (a local step was set up wrongly). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
55
+ throw new Error("Slopify hit an internal error (a local step was set up wrongly). Try again; if it happens again, use Download diagnostics in Settings and report it.");
56
+ if (input.operation === "figure-card-v1")
57
+ return executeFigureCard(deps, context, piece);
47
58
  if (input.operation === "narration-files-v1") {
48
59
  await publishNarrationText(deps, context, piece);
49
60
  return "done";
50
61
  }
51
- if (input.operation === "concat-narration") {
62
+ if (input.operation === "concat-narration" ||
63
+ input.operation === "concat-turns-v1" ||
64
+ input.operation === levelOperation) {
52
65
  await concatenate(deps, context, piece);
53
66
  return "done";
54
67
  }
@@ -75,19 +88,19 @@ export async function executeLocalRecipe(deps, context, piece) {
75
88
  });
76
89
  return "done";
77
90
  }
78
- throw new Error(`Slopify hit an internal error (no handler for the step "${input.operation}"). Retry stage; if it happens again, use Download diagnostics in Settings and report it.`);
91
+ throw new Error(`Slopify hit an internal error (no handler for the step "${input.operation}"). Try again; if it happens again, use Download diagnostics in Settings and report it.`);
79
92
  }
80
93
  async function concatenate(deps, context, piece) {
81
94
  const view = executionView(deps, context.work.projectId, context.work.revisionId);
82
95
  if (view === undefined)
83
- throw new Error("Slopify hit an internal error (the project version being built is missing). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
96
+ throw new Error("Slopify hit an internal error (the project version being built is missing). Try again; if it happens again, use Download diagnostics in Settings and report it.");
84
97
  const row = deps.db
85
98
  .prepare("SELECT recipe_context FROM revision_work WHERE id=?")
86
99
  .get(context.work.workId);
87
100
  const plan = executionPlan(deps, view, savedCatalogue(row?.recipe_context));
88
101
  const recipe = plan.recipes.find((candidate) => candidate.key === piece.key && candidate.fingerprint === piece.fingerprint);
89
102
  if (recipe === undefined)
90
- throw new Error("Slopify hit an internal error (the narration chunks to join no longer match the project's saved inputs). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
103
+ throw new Error("Slopify hit an internal error (the narration chunks to join no longer match the project's saved inputs). Try again; if it happens again, use Download diagnostics in Settings and report it.");
91
104
  const files = recipe.dependsOn.map((key) => {
92
105
  const input = view.pieces.find((candidate) => candidate.key === key &&
93
106
  candidate.selected &&
@@ -95,36 +108,73 @@ async function concatenate(deps, context, piece) {
95
108
  candidate.piece.state === "done" &&
96
109
  candidate.fingerprint === plan.recipes.find((value) => value.key === key)?.fingerprint);
97
110
  if (input?.assetId === null || input === undefined)
98
- throw new Error("One narration chunk has no saved audio, so the narration can't be joined together. Regenerate the missing chunk in Edit project → Narration, then Retry stage.");
111
+ throw new Error("One narration chunk has no saved audio, so the narration can't be joined together. Regenerate the missing chunk in Edit project → Narration, then Try again.");
99
112
  const asset = deps.db
100
113
  .prepare("SELECT path FROM project_assets WHERE id=? AND project_id=?")
101
114
  .get(input.assetId, context.work.projectId);
102
115
  return outputPath(deps.paths, context.work.projectId, z.string().parse(asset?.path));
103
116
  });
117
+ // Level the volume repeats the join its recipe names, from the same pieces, each levelled
118
+ // first (`recipe-loudness.ts`).
119
+ const level = recipe.input.kind === "local" && recipe.input.operation === levelOperation;
120
+ const [operation, values] = recipe.input.kind !== "local"
121
+ ? ["", undefined]
122
+ : level
123
+ ? z.tuple([z.string(), z.unknown(), z.unknown()]).parse(recipe.input.values)
124
+ : [recipe.input.operation, recipe.input.values];
104
125
  const pending = allocateAsset(deps, context.work.projectId, "narration.mp3");
126
+ const piecesDirectory = join(dirname(pending.absolutePath), "levelled");
105
127
  try {
106
- const durationMs = await joinNarration({ bin: deps.ffmpeg, log: deps.log }, {
107
- files,
108
- output: pending.absolutePath,
109
- listPath: join(dirname(pending.absolutePath), "parts.txt"),
110
- signal: context.signal,
111
- });
128
+ const run = { bin: deps.ffmpeg, log: deps.log, signal: context.signal };
129
+ const levelled = level ? await levelPieces(run, files, piecesDirectory) : undefined;
130
+ // Pauses between sentences: planned from the pieces as spoken, made in what is joined.
131
+ const pauses = pausesIn(values);
132
+ const paced = pauses === undefined
133
+ ? undefined
134
+ : await applyPauses(run, levelled?.files ?? files, paceScaled(await planPieces(run, files.map((file, at) => ({
135
+ file,
136
+ text: pieceText(plan.recipes.find((one) => one.key === recipe.dependsOn[at])),
137
+ next: pauses.next[at] ?? "end",
138
+ })), pauses.settings), operation === "concat-turns-v1" ? values : undefined), join(piecesDirectory, "paced"));
139
+ const joined = paced?.files ?? levelled?.files ?? files;
140
+ const durationMs = operation === "concat-turns-v1"
141
+ ? await joinTurns(run, joined, values, pending.absolutePath)
142
+ : await joinNarration({ bin: deps.ffmpeg, log: deps.log }, {
143
+ files: joined,
144
+ output: pending.absolutePath,
145
+ listPath: join(dirname(pending.absolutePath), "parts.txt"),
146
+ signal: context.signal,
147
+ reencode: level || paced !== undefined,
148
+ });
149
+ rmSync(piecesDirectory, { recursive: true, force: true });
112
150
  context.signal.throwIfAborted();
113
151
  const asset = sealAsset(deps, pending);
114
- const role = piece.key === "audio:intro"
115
- ? "audio_intro"
116
- : piece.key === "audio:outro"
117
- ? "audio_outro"
118
- : "audio_body";
152
+ const segment = /^(?:audio|level):(intro|outro)$/.exec(piece.key)?.[1] ?? "body";
153
+ const role = level
154
+ ? "audio_levelled"
155
+ : piece.key === "audio:intro"
156
+ ? "audio_intro"
157
+ : piece.key === "audio:outro"
158
+ ? "audio_outro"
159
+ : "audio_body";
160
+ const voice = view.revision.config.audio === undefined
161
+ ? {}
162
+ : {
163
+ provider: view.revision.config.audio.provider,
164
+ model: view.revision.config.audio.model,
165
+ voice: view.revision.config.audio.voice,
166
+ };
119
167
  await publishResult(deps, context, piece, [
120
- preparedResult(deps, context, piece, role, asset, durationMs, view.revision.config.audio === undefined
121
- ? {}
168
+ preparedResult(deps, context, piece, role, asset, durationMs, levelled === undefined
169
+ ? voice
122
170
  : {
123
- provider: view.revision.config.audio.provider,
124
- model: view.revision.config.audio.model,
125
- voice: view.revision.config.audio.voice,
171
+ ...voice,
172
+ segment: segment,
173
+ loudness: levelled.report,
126
174
  }),
127
175
  ], { durationMs }, asset);
176
+ if (level)
177
+ return;
128
178
  for (const key of recipe.dependsOn) {
129
179
  const retained = view.pieces.find((candidate) => candidate.key === key && candidate.selected && candidate.available);
130
180
  if (retained?.publicationId === null || retained === undefined)
@@ -137,6 +187,83 @@ async function concatenate(deps, context, piece) {
137
187
  }
138
188
  }
139
189
  finally {
190
+ rmSync(piecesDirectory, { recursive: true, force: true });
140
191
  discardPreparedAssets(deps, [pending]);
141
192
  }
142
193
  }
194
+ // The pauses a join's values ask for (`recipe-pauses.ts`): its third value, when there is one.
195
+ function pausesIn(values) {
196
+ const value = Array.isArray(values) ? values[2] : undefined;
197
+ const parsed = z
198
+ .tuple([
199
+ z.literal("pauses-v1"),
200
+ z.number(),
201
+ z.number(),
202
+ z.array(z.enum(["end", "turn", "sentence", "paragraph"])),
203
+ ])
204
+ .safeParse(value);
205
+ if (!parsed.success)
206
+ return undefined;
207
+ const [, sentenceSeconds, paragraphSeconds, next] = parsed.data;
208
+ return { settings: { sentenceSeconds, paragraphSeconds }, next };
209
+ }
210
+ // A speaker's pace plays their turn faster or slower, pauses included, so a turn's added
211
+ // silence is scaled by it to come out at the minimum.
212
+ function paceScaled(plans, turnValues) {
213
+ const layout = Array.isArray(turnValues) ? turnValues[1] : undefined;
214
+ if (!Array.isArray(layout))
215
+ return plans;
216
+ return plans.map((plan, at) => {
217
+ const pace = z.tuple([z.number().positive(), z.number()]).safeParse(layout[at]);
218
+ const factor = pace.success ? pace.data[0] : 1;
219
+ if (factor === 1)
220
+ return plan;
221
+ return {
222
+ ...plan,
223
+ padEnd: plan.padEnd * factor,
224
+ pauses: {
225
+ ...plan.pauses,
226
+ inserts: plan.pauses.inserts.map((one) => ({ ...one, seconds: one.seconds * factor })),
227
+ },
228
+ };
229
+ });
230
+ }
231
+ // The words a narration piece speaks, where its sentences end: the clean words of a request, or
232
+ // of a piece supplied as a file.
233
+ function pieceText(recipe) {
234
+ if (recipe === undefined)
235
+ return "";
236
+ const input = recipe.input;
237
+ // The words as said: a prepared request's delivery tags take no time of their own.
238
+ if (input.kind === "tts")
239
+ return (input.spokenText ?? input.text).replace(/\[[^\]]*\]/g, "");
240
+ if (input.kind === "provided" &&
241
+ Array.isArray(input.semantic) &&
242
+ typeof input.semantic[0] === "string")
243
+ return input.semantic[0];
244
+ return "";
245
+ }
246
+ // A multi-voice narration: every turn at its speaker's pace with the gap between turns, as the
247
+ // recipe laid it out (`recipe-voices.ts`), measured off the file that was written. `values` is
248
+ // the concat-turns-v1 recipe's own values.
249
+ export async function joinTurns(run, files, values, output) {
250
+ const layout = z
251
+ .tuple([z.unknown(), z.array(z.tuple([z.number().positive(), z.number().nonnegative()]))])
252
+ // The pauses between sentences may follow (`recipe-pauses.ts`); they were made already.
253
+ .rest(z.unknown())
254
+ .parse(values)[1];
255
+ if (layout.length !== files.length || files.length === 0)
256
+ throw new Error("Slopify hit an internal error (the speaker turns to join don't match their audio). Try again; if it happens again, use Download diagnostics in Settings and report it.");
257
+ await runFfmpeg({
258
+ bin: run.bin,
259
+ args: turnJoinArgs(files.map((path, index) => ({
260
+ path,
261
+ pace: layout[index]?.[0] ?? 1,
262
+ gapAfter: layout[index]?.[1] ?? 0,
263
+ })), output),
264
+ signal: run.signal,
265
+ log: run.log,
266
+ onProgress: () => { },
267
+ });
268
+ return probeDurationMs(run.bin, output, run.signal, run.log);
269
+ }
@@ -136,6 +136,10 @@ function futureKey(recipe) {
136
136
  return `narration:prepare:${recipe.input.preparation.segment}:future`;
137
137
  if (recipe.input.kind === "tts")
138
138
  return `audio:${recipe.input.segment}:future`;
139
+ // The descriptions unfold from the one step that stood in for them while the text was
140
+ // unwritten.
141
+ if (recipe.key.startsWith("narration:describe:") || recipe.key.startsWith("figure:card:"))
142
+ return "narration:describe:future";
139
143
  if (recipe.key.startsWith("research:chapter:"))
140
144
  return "research:planner";
141
145
  if (recipe.key === "article:continuation")
@@ -143,6 +147,10 @@ function futureKey(recipe) {
143
147
  // A short's images and render unfold from its image prompts, admitted with them: under the
144
148
  // pick on a first run, or on their own when one short is made again and the pick is kept.
145
149
  // The prompts unfold from the pick's answer.
150
+ // A short's review unfolds with the short it reviews.
151
+ const reviewed = /^review:(shorts:\d+)$/.exec(recipe.key);
152
+ if (reviewed?.[1] !== undefined)
153
+ return `${reviewed[1]}:prompts`;
146
154
  const clip = /^(shorts:\d+):(?:image:\d+|render)$/.exec(recipe.key);
147
155
  if (clip?.[1] !== undefined)
148
156
  return `${clip[1]}:prompts`;
@@ -3,7 +3,8 @@ import { insertManifestOutput, insertManifestPiece, selectOutputRecord, selectPi
3
3
  import { retainedNarrationPieces } from "./narration-history.js";
4
4
  import { matchingNarrationPiece } from "./narration-reuse.js";
5
5
  export function bindNarrationReuse(deps, view, recipe, ordinal) {
6
- if (recipe.input.kind === "local" && recipe.input.operation === "concat-narration") {
6
+ if (recipe.input.kind === "local" &&
7
+ (recipe.input.operation === "concat-narration" || recipe.input.operation === "concat-turns-v1")) {
7
8
  const completed = view.pieces.find((one) => one.available &&
8
9
  one.piece.state === "done" &&
9
10
  one.key === recipe.key &&