@gentbajko/slopify 2.5.0 → 3.0.1

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 (542) hide show
  1. package/LICENSE +201 -21
  2. package/NOTICE +12 -0
  3. package/README.md +24 -41
  4. package/SUBTITLES.md +1 -1
  5. package/dist/adapter-registry.js +31 -0
  6. package/dist/adapters/alignment/cache.js +4 -1
  7. package/dist/adapters/alignment/ctc.js +25 -23
  8. package/dist/adapters/alignment/index.js +37 -6
  9. package/dist/adapters/alignment/multilingual.js +267 -0
  10. package/dist/adapters/alignment/protocol.js +11 -0
  11. package/dist/adapters/alignment/quality.js +4 -3
  12. package/dist/adapters/alignment/sentences.js +95 -0
  13. package/dist/adapters/alignment/spec.js +24 -0
  14. package/dist/adapters/alignment/spell.js +769 -0
  15. package/dist/adapters/alignment/text.js +56 -9
  16. package/dist/adapters/alignment/window.js +34 -30
  17. package/dist/adapters/alignment/worker.js +23 -21
  18. package/dist/adapters/explain.js +11 -11
  19. package/dist/adapters/host-cli/index.js +44 -5
  20. package/dist/adapters/host-cli/transport.js +7 -5
  21. package/dist/adapters/image/bytes.js +2 -2
  22. package/dist/adapters/image/codex-output.js +1 -1
  23. package/dist/adapters/image/codex.js +87 -22
  24. package/dist/adapters/image/fal.js +13 -5
  25. package/dist/adapters/image/google.js +11 -8
  26. package/dist/adapters/image/openai.js +14 -7
  27. package/dist/adapters/image/reference.js +34 -0
  28. package/dist/adapters/image/replicate.js +7 -2
  29. package/dist/adapters/image/video.js +2 -2
  30. package/dist/adapters/key-probes.js +85 -0
  31. package/dist/adapters/llm/claude-code.js +168 -32
  32. package/dist/adapters/llm/cli-login-error.js +1 -1
  33. package/dist/adapters/llm/codex-limits.js +157 -0
  34. package/dist/adapters/llm/codex.js +99 -12
  35. package/dist/adapters/llm/document-workspace.js +2 -2
  36. package/dist/adapters/llm/gemini-workspace.js +1 -1
  37. package/dist/adapters/llm/gemini.js +16 -3
  38. package/dist/adapters/llm/image-workspace.js +58 -0
  39. package/dist/adapters/llm/openrouter.js +11 -3
  40. package/dist/adapters/llm/run-cli.js +12 -7
  41. package/dist/adapters/tts/cartesia.js +10 -0
  42. package/dist/adapters/tts/elevenlabs.js +31 -9
  43. package/dist/adapters/tts/gemini.js +206 -0
  44. package/dist/adapters/tts/inworld-async.js +9 -5
  45. package/dist/adapters/tts/inworld.js +17 -3
  46. package/dist/adapters/tts/openai.js +3 -0
  47. package/dist/adapters/tts/pcm-mp3.js +62 -0
  48. package/dist/adapters/tts/system.js +147 -0
  49. package/dist/adapters/tts/voice-languages.js +80 -0
  50. package/dist/assets/fonts/noto/NotoSans-Regular.ttf +0 -0
  51. package/dist/assets/fonts/noto/NotoSansArabic-Regular.ttf +0 -0
  52. package/dist/assets/fonts/noto/NotoSansDevanagari-Regular.ttf +0 -0
  53. package/dist/assets/fonts/noto/NotoSansHebrew-Regular.ttf +0 -0
  54. package/dist/assets/fonts/noto/NotoSansThai-Regular.ttf +0 -0
  55. package/dist/assets/fonts/noto/OFL.txt +93 -0
  56. package/dist/assets/fonts/noto/SOURCE.txt +18 -0
  57. package/dist/assets/models.yaml +38 -0
  58. package/dist/assets/sample/sample-audiobook.tar +0 -0
  59. package/dist/assets/sample/sample-podcast.tar +0 -0
  60. package/dist/assets/sample/sample-project.tar +0 -0
  61. package/dist/assets/style-preview/narration.mp3 +0 -0
  62. package/dist/assets/style-preview/tall-1.jpg +0 -0
  63. package/dist/assets/style-preview/tall-2.jpg +0 -0
  64. package/dist/assets/style-preview/tall-3.jpg +0 -0
  65. package/dist/assets/style-preview/wide-1.jpg +0 -0
  66. package/dist/assets/style-preview/wide-2.jpg +0 -0
  67. package/dist/assets/style-preview/wide-3.jpg +0 -0
  68. package/dist/catalog/merge.js +132 -0
  69. package/dist/catalog/registry.js +17 -14
  70. package/dist/catalog/schema.js +2 -0
  71. package/dist/catalog/store.js +147 -8
  72. package/dist/catalog/validate.js +2 -2
  73. package/dist/compose.yaml +71 -0
  74. package/dist/edge/autostart/docker-record.js +32 -0
  75. package/dist/edge/autostart/docker.js +118 -0
  76. package/dist/edge/autostart/index.js +78 -0
  77. package/dist/edge/autostart/launcher.js +176 -0
  78. package/dist/edge/autostart/model.js +3 -0
  79. package/dist/edge/autostart/native.js +210 -0
  80. package/dist/edge/autostart/prompt.js +72 -0
  81. package/dist/edge/autostart/service.js +90 -0
  82. package/dist/edge/cli-args.js +71 -0
  83. package/dist/edge/cli.js +64 -63
  84. package/dist/edge/docker-install/activation.js +56 -0
  85. package/dist/edge/docker-install/apply.js +458 -0
  86. package/dist/edge/docker-install/engine.js +362 -0
  87. package/dist/edge/docker-install/run.js +160 -0
  88. package/dist/edge/docker-install/state.js +146 -0
  89. package/dist/edge/{docker-projects → docker-install}/volume.js +1 -5
  90. package/dist/edge/docker.js +14 -22
  91. package/dist/edge/events/hub.js +3 -1
  92. package/dist/edge/host-cli.js +1 -1
  93. package/dist/edge/http/actions.js +38 -11
  94. package/dist/edge/http/app.js +60 -2
  95. package/dist/edge/http/auditions.js +70 -0
  96. package/dist/edge/http/autostart.js +37 -0
  97. package/dist/edge/http/byte-range.js +42 -0
  98. package/dist/edge/http/channel-memory.js +92 -0
  99. package/dist/edge/http/channels.js +128 -0
  100. package/dist/edge/http/diagnostics.js +1 -0
  101. package/dist/edge/http/draft-files.js +2 -4
  102. package/dist/edge/http/entries.js +22 -3
  103. package/dist/edge/http/files.js +11 -7
  104. package/dist/edge/http/folder-location.js +1 -1
  105. package/dist/edge/http/home.js +39 -0
  106. package/dist/edge/http/host-cli.js +29 -5
  107. package/dist/edge/http/narration-peaks.js +76 -0
  108. package/dist/edge/http/onboarding.js +212 -0
  109. package/dist/edge/http/open-folder.js +1 -1
  110. package/dist/edge/http/patch-notes.js +45 -0
  111. package/dist/edge/http/planning.js +4 -2
  112. package/dist/edge/http/project-create.js +87 -0
  113. package/dist/edge/http/project-templates.js +19 -2
  114. package/dist/edge/http/projects.js +37 -100
  115. package/dist/edge/http/prompts.js +40 -3
  116. package/dist/edge/http/pronunciations.js +23 -4
  117. package/dist/edge/http/providers.js +119 -3
  118. package/dist/edge/http/reviews.js +131 -0
  119. package/dist/edge/http/revision-files.js +3 -5
  120. package/dist/edge/http/revisions.js +1 -1
  121. package/dist/edge/http/run-cost.js +25 -0
  122. package/dist/edge/http/schedules.js +115 -6
  123. package/dist/edge/http/settings.js +84 -6
  124. package/dist/edge/http/storage-files.js +50 -0
  125. package/dist/edge/http/storage.js +43 -2
  126. package/dist/edge/http/studio.js +287 -0
  127. package/dist/edge/http/style-preview.js +61 -0
  128. package/dist/edge/http/subtitles.js +1 -1
  129. package/dist/edge/http/trash.js +69 -0
  130. package/dist/edge/http/tutorials.js +59 -0
  131. package/dist/edge/http/update.js +24 -3
  132. package/dist/edge/http/whats-new.js +15 -0
  133. package/dist/edge/http/youtube-edits.js +92 -0
  134. package/dist/edge/native-update.js +62 -0
  135. package/dist/extension/slopify-studio-chrome.zip +0 -0
  136. package/dist/extension/slopify-studio-firefox.zip +0 -0
  137. package/dist/host-cli/gemini-login.js +83 -0
  138. package/dist/host-cli/open-folder.js +10 -10
  139. package/dist/host-cli/runtime.js +9 -1
  140. package/dist/host-cli/status.js +4 -2
  141. package/dist/kernel/db/migrations/0025-automatic-reviews.sql +40 -0
  142. package/dist/kernel/db/migrations/0026-schedule-topic-generation.sql +23 -0
  143. package/dist/kernel/db/migrations/0027-run-cost.sql +58 -0
  144. package/dist/kernel/db/migrations/0030-automatic-retries.sql +18 -0
  145. package/dist/kernel/db/migrations/0031-prompt-history-and-description-edits.sql +33 -0
  146. package/dist/kernel/db/migrations/0032-channels.sql +65 -0
  147. package/dist/kernel/db/migrations/0034-script-prompts.sql +23 -0
  148. package/dist/kernel/db/migrations/0035-narration-aliases.sql +13 -0
  149. package/dist/kernel/db/migrations/0037-project-uploads.sql +8 -0
  150. package/dist/kernel/db/migrations/0038-voice-languages.sql +7 -0
  151. package/dist/kernel/db/migrations/0039-channel-essentials.sql +64 -0
  152. package/dist/kernel/db/migrations/0040-standalone-usage.sql +35 -0
  153. package/dist/kernel/db/migrations/0041-cast-hosts.sql +3 -0
  154. package/dist/kernel/paths.js +27 -6
  155. package/dist/kernel/pipeline.js +4 -1
  156. package/dist/kernel/ports/host-cli.js +122 -14
  157. package/dist/kernel/ports/key-probe.js +1 -0
  158. package/dist/kernel/ports/languages.js +157 -0
  159. package/dist/kernel/ports/model.js +10 -5
  160. package/dist/kernel/ports/narration-aliases.js +60 -0
  161. package/dist/kernel/ports/plan-limits.js +36 -0
  162. package/dist/kernel/ports/system-speech.js +203 -0
  163. package/dist/kernel/runner/attempt.js +17 -8
  164. package/dist/kernel/runner/audition.js +36 -0
  165. package/dist/kernel/runner/graph.js +40 -5
  166. package/dist/kernel/runner/index.js +39 -9
  167. package/dist/kernel/runner/meter.js +1 -0
  168. package/dist/kernel/runner/providers.js +142 -7
  169. package/dist/kernel/runner/retry-policy.js +43 -0
  170. package/dist/kernel/runner/standalone.js +134 -0
  171. package/dist/kernel/runner/work-authority.js +28 -2
  172. package/dist/main.js +277 -16
  173. package/dist/model-catalog.js +2 -0
  174. package/dist/patch-notes/0.4.2-to-1.0.md +93 -0
  175. package/dist/patch-notes/1.0-to-2.0.md +74 -0
  176. package/dist/patch-notes/2.0-to-3.0.md +257 -0
  177. package/dist/patch-notes/3.0.0.md +70 -0
  178. package/dist/patch-notes/3.0.1.md +105 -0
  179. package/dist/patch-notes/index.json +32 -0
  180. package/dist/slices/admission/model.js +27 -0
  181. package/dist/slices/admission/repo.js +24 -4
  182. package/dist/slices/admission/rules.js +80 -2
  183. package/dist/slices/admission/schema.js +58 -1
  184. package/dist/slices/admission/short-mode.js +47 -0
  185. package/dist/slices/admission/start.js +42 -6
  186. package/dist/slices/article/continuation.js +2 -2
  187. package/dist/slices/article/provided-entries.js +2 -2
  188. package/dist/slices/article/run.js +6 -2
  189. package/dist/slices/article/segments.js +3 -3
  190. package/dist/slices/backups/folder.js +14 -6
  191. package/dist/slices/batch/index.js +7 -2
  192. package/dist/slices/cancel/index.js +1 -1
  193. package/dist/slices/channels/cast-images.js +110 -0
  194. package/dist/slices/channels/cast-match.js +32 -0
  195. package/dist/slices/channels/images.js +19 -0
  196. package/dist/slices/channels/model.js +5 -0
  197. package/dist/slices/channels/rebrand.js +100 -0
  198. package/dist/slices/channels/repo.js +221 -0
  199. package/dist/slices/channels/runs.js +129 -0
  200. package/dist/slices/channels/schema.js +152 -0
  201. package/dist/slices/channels/service.js +180 -0
  202. package/dist/slices/channels/studio-csv.js +110 -0
  203. package/dist/slices/channels/videos.js +120 -0
  204. package/dist/slices/checkpoints/recovery.js +10 -0
  205. package/dist/slices/control/index.js +5 -2
  206. package/dist/slices/control/providers.js +4 -1
  207. package/dist/slices/document/fonts.js +1 -1
  208. package/dist/slices/document/render.js +1 -1
  209. package/dist/slices/episodes/related.js +90 -0
  210. package/dist/slices/episodes/repo.js +60 -0
  211. package/dist/slices/episodes/service.js +58 -0
  212. package/dist/slices/episodes/summarize.js +170 -0
  213. package/dist/slices/estimate/index.js +109 -28
  214. package/dist/slices/estimate/requests.js +68 -2
  215. package/dist/slices/eta/history.js +56 -0
  216. package/dist/slices/eta/model.js +63 -0
  217. package/dist/slices/eta/view.js +25 -0
  218. package/dist/slices/fixes/rules.js +73 -0
  219. package/dist/slices/fonts/coverage.js +82 -0
  220. package/dist/slices/fonts/sfnt.js +74 -0
  221. package/dist/slices/images/run.js +8 -5
  222. package/dist/slices/images/scale.js +84 -0
  223. package/dist/slices/library/diff.js +122 -0
  224. package/dist/slices/library/history.js +100 -0
  225. package/dist/slices/library/model.js +3 -0
  226. package/dist/slices/library/photorealistic.js +49 -0
  227. package/dist/slices/library/repo.js +19 -14
  228. package/dist/slices/library/save.js +62 -12
  229. package/dist/slices/library/slots.js +7 -1
  230. package/dist/slices/library/used-by.js +153 -0
  231. package/dist/slices/loudness/level-pieces.js +41 -0
  232. package/dist/slices/loudness/loudnorm.js +164 -0
  233. package/dist/slices/loudness/model.js +153 -0
  234. package/dist/slices/model-upkeep/model.js +10 -0
  235. package/dist/slices/model-upkeep/switch.js +217 -0
  236. package/dist/slices/model-upkeep/usage.js +185 -0
  237. package/dist/slices/narration/aliases-library.js +31 -0
  238. package/dist/slices/narration/aliases-schema.js +53 -0
  239. package/dist/slices/narration/aliases.js +49 -0
  240. package/dist/slices/narration/blocks.js +352 -0
  241. package/dist/slices/narration/chunk.js +19 -7
  242. package/dist/slices/narration/concat.js +2 -2
  243. package/dist/slices/narration/describe.js +45 -0
  244. package/dist/slices/narration/pauses-model.js +63 -0
  245. package/dist/slices/narration/pauses.js +269 -0
  246. package/dist/slices/narration/peaks-model.js +1 -0
  247. package/dist/slices/narration/peaks.js +94 -0
  248. package/dist/slices/narration/preparation.js +21 -3
  249. package/dist/slices/narration/pronunciation-chunks.js +3 -3
  250. package/dist/slices/narration/pronunciation.js +72 -25
  251. package/dist/slices/narration/run.js +6 -6
  252. package/dist/slices/narration/shared-glossary.js +1 -1
  253. package/dist/slices/narration/steering.js +15 -4
  254. package/dist/slices/notifications/notifier.js +47 -3
  255. package/dist/slices/notifications/rules.js +48 -3
  256. package/dist/slices/onboarding/copy.js +143 -0
  257. package/dist/slices/onboarding/first-run.js +72 -0
  258. package/dist/slices/onboarding/full-video.js +81 -0
  259. package/dist/slices/onboarding/install.js +119 -0
  260. package/dist/slices/onboarding/model.js +85 -0
  261. package/dist/slices/onboarding/packs.js +491 -0
  262. package/dist/slices/onboarding/quick-short.js +144 -0
  263. package/dist/slices/onboarding/sample.js +132 -0
  264. package/dist/slices/onboarding/state.js +68 -0
  265. package/dist/slices/onboarding/template.js +73 -0
  266. package/dist/slices/patch-notes/library.js +73 -0
  267. package/dist/slices/patch-notes/seen.js +77 -0
  268. package/dist/slices/play-drafts/convert.js +127 -8
  269. package/dist/slices/play-drafts/readiness.js +4 -1
  270. package/dist/slices/play-drafts/repo.js +2 -0
  271. package/dist/slices/play-drafts/review-inputs.js +20 -3
  272. package/dist/slices/play-drafts/schema.js +89 -0
  273. package/dist/slices/play-drafts/service.js +4 -1
  274. package/dist/slices/play-drafts/start.js +43 -18
  275. package/dist/slices/project-templates/from-project.js +59 -6
  276. package/dist/slices/project-templates/next-chapter.js +94 -0
  277. package/dist/slices/project-templates/one-off.js +26 -0
  278. package/dist/slices/project-templates/repo.js +2 -1
  279. package/dist/slices/project-templates/schema.js +7 -1
  280. package/dist/slices/project-templates/service.js +15 -4
  281. package/dist/slices/project-templates/setup.js +18 -2
  282. package/dist/slices/rebuild/admission-repo.js +1 -1
  283. package/dist/slices/rebuild/model.js +6 -0
  284. package/dist/slices/rebuild/preview-details.js +46 -3
  285. package/dist/slices/rebuild/preview-plan.js +13 -6
  286. package/dist/slices/rebuild/recipe-audio-parts.js +40 -7
  287. package/dist/slices/rebuild/recipe-audio.js +66 -12
  288. package/dist/slices/rebuild/recipe-build.js +8 -4
  289. package/dist/slices/rebuild/recipe-cast.js +18 -0
  290. package/dist/slices/rebuild/recipe-checkpoints.js +11 -9
  291. package/dist/slices/rebuild/recipe-describe.js +93 -0
  292. package/dist/slices/rebuild/recipe-edit.js +13 -1
  293. package/dist/slices/rebuild/recipe-exports.js +67 -8
  294. package/dist/slices/rebuild/recipe-input-schema.js +35 -0
  295. package/dist/slices/rebuild/recipe-legacy.js +9 -0
  296. package/dist/slices/rebuild/recipe-loudness.js +45 -0
  297. package/dist/slices/rebuild/recipe-model.js +25 -6
  298. package/dist/slices/rebuild/recipe-pauses.js +7 -0
  299. package/dist/slices/rebuild/recipe-preparation.js +3 -3
  300. package/dist/slices/rebuild/recipe-provider-choice.js +11 -1
  301. package/dist/slices/rebuild/recipe-reference.js +15 -1
  302. package/dist/slices/rebuild/recipe-reviews.js +117 -0
  303. package/dist/slices/rebuild/recipe-save.js +4 -1
  304. package/dist/slices/rebuild/recipe-shorts.js +23 -3
  305. package/dist/slices/rebuild/recipe-text.js +114 -19
  306. package/dist/slices/rebuild/recipe-validation.js +14 -2
  307. package/dist/slices/rebuild/recipe-visual.js +96 -33
  308. package/dist/slices/rebuild/recipe-voices.js +286 -0
  309. package/dist/slices/rebuild/recipe-work.js +77 -6
  310. package/dist/slices/rebuild/recipe-youtube.js +2 -0
  311. package/dist/slices/rebuild/recovery-conflict.js +7 -3
  312. package/dist/slices/rebuild/recovery-model.js +3 -0
  313. package/dist/slices/rebuild/recovery-selection.js +28 -2
  314. package/dist/slices/rebuild/recovery.js +35 -17
  315. package/dist/slices/rebuild/repo.js +3 -3
  316. package/dist/slices/rebuild/retry.js +18 -0
  317. package/dist/slices/rebuild/review-redo.js +125 -0
  318. package/dist/slices/rebuild/runtime-actions.js +5 -0
  319. package/dist/slices/rebuild/runtime-admission.js +4 -1
  320. package/dist/slices/rebuild/runtime-animate.js +4 -4
  321. package/dist/slices/rebuild/runtime-article.js +5 -5
  322. package/dist/slices/rebuild/runtime-document.js +4 -4
  323. package/dist/slices/rebuild/runtime-export-bed.js +43 -0
  324. package/dist/slices/rebuild/runtime-export-edit.js +70 -11
  325. package/dist/slices/rebuild/runtime-export-inputs.js +52 -10
  326. package/dist/slices/rebuild/runtime-export-short.js +85 -0
  327. package/dist/slices/rebuild/runtime-export.js +84 -19
  328. package/dist/slices/rebuild/runtime-figure-card.js +87 -0
  329. package/dist/slices/rebuild/runtime-image.js +21 -2
  330. package/dist/slices/rebuild/runtime-local.js +148 -24
  331. package/dist/slices/rebuild/runtime-materialize.js +8 -0
  332. package/dist/slices/rebuild/runtime-narration-reuse.js +2 -1
  333. package/dist/slices/rebuild/runtime-narration-text.js +7 -7
  334. package/dist/slices/rebuild/runtime-pictures.js +28 -0
  335. package/dist/slices/rebuild/runtime-piece-label.js +27 -0
  336. package/dist/slices/rebuild/runtime-provider.js +126 -18
  337. package/dist/slices/rebuild/runtime-publication.js +21 -5
  338. package/dist/slices/rebuild/runtime-review.js +267 -0
  339. package/dist/slices/rebuild/runtime-run.js +17 -11
  340. package/dist/slices/rebuild/runtime-shorts.js +44 -17
  341. package/dist/slices/rebuild/runtime-store.js +62 -11
  342. package/dist/slices/rebuild/runtime-subtitles.js +72 -19
  343. package/dist/slices/rebuild/runtime-voices.js +103 -0
  344. package/dist/slices/rebuild/runtime-youtube.js +7 -6
  345. package/dist/slices/rebuild/service-readiness.js +3 -3
  346. package/dist/slices/rebuild/soften.js +49 -0
  347. package/dist/slices/rebuild/transition-repo.js +9 -2
  348. package/dist/slices/research/run.js +4 -4
  349. package/dist/slices/research/synthesis.js +2 -2
  350. package/dist/slices/reviews/model.js +105 -0
  351. package/dist/slices/reviews/outcome.js +22 -0
  352. package/dist/slices/reviews/repo.js +118 -0
  353. package/dist/slices/reviews/rules.js +63 -0
  354. package/dist/slices/reviews/verdict.js +71 -0
  355. package/dist/slices/reviews/view.js +30 -0
  356. package/dist/slices/revisions/adopt-content.js +5 -2
  357. package/dist/slices/revisions/adopt.js +3 -1
  358. package/dist/slices/revisions/downloads.js +7 -1
  359. package/dist/slices/revisions/image-plan.js +183 -0
  360. package/dist/slices/revisions/mutation-assets.js +10 -0
  361. package/dist/slices/revisions/mutation-cues.js +71 -0
  362. package/dist/slices/revisions/mutation-request.js +2 -1
  363. package/dist/slices/revisions/mutations.js +97 -3
  364. package/dist/slices/revisions/schema.js +11 -1
  365. package/dist/slices/run-cost/limits.js +128 -0
  366. package/dist/slices/run-cost/meter.js +47 -0
  367. package/dist/slices/run-cost/panel.js +256 -0
  368. package/dist/slices/run-cost/pricing.js +95 -0
  369. package/dist/slices/run-cost/week.js +108 -0
  370. package/dist/slices/schedules/agenda.js +222 -0
  371. package/dist/slices/schedules/repo.js +58 -18
  372. package/dist/slices/schedules/scheduler.js +68 -17
  373. package/dist/slices/schedules/schema.js +179 -1
  374. package/dist/slices/schedules/service.js +43 -4
  375. package/dist/slices/schedules/similar.js +128 -0
  376. package/dist/slices/schedules/topic-list.js +163 -0
  377. package/dist/slices/schedules/topics.js +523 -0
  378. package/dist/slices/settings/first-run.js +106 -0
  379. package/dist/slices/settings/health.js +247 -0
  380. package/dist/slices/settings/key-guides.js +138 -0
  381. package/dist/slices/settings/key-test.js +93 -0
  382. package/dist/slices/settings/keys.js +10 -5
  383. package/dist/slices/settings/model-reach.js +52 -0
  384. package/dist/slices/settings/model.js +55 -1
  385. package/dist/slices/settings/models.js +5 -1
  386. package/dist/slices/settings/playback.js +15 -0
  387. package/dist/slices/settings/readiness.js +14 -2
  388. package/dist/slices/settings/repo.js +17 -3
  389. package/dist/slices/settings/tutorial-schema.js +5 -0
  390. package/dist/slices/settings/voices.js +64 -4
  391. package/dist/slices/settings/whats-new.js +64 -0
  392. package/dist/slices/shorts/captions.js +9 -0
  393. package/dist/slices/shorts/pick.js +3 -3
  394. package/dist/slices/shorts/prompts.js +1 -1
  395. package/dist/slices/shorts/render.js +63 -13
  396. package/dist/slices/storage/asset-name.js +3 -0
  397. package/dist/slices/storage/backup-export.js +48 -9
  398. package/dist/slices/storage/backup-format.js +38 -1
  399. package/dist/slices/storage/backup-import.js +191 -19
  400. package/dist/slices/storage/delete-project.js +7 -4
  401. package/dist/slices/storage/documents.js +87 -0
  402. package/dist/slices/storage/downloads.js +33 -1
  403. package/dist/slices/storage/files-location.js +531 -0
  404. package/dist/slices/storage/layout.js +14 -4
  405. package/dist/slices/storage/model.js +11 -0
  406. package/dist/slices/storage/portable.js +94 -12
  407. package/dist/slices/storage/reconcile.js +14 -1
  408. package/dist/slices/storage/repo.js +4 -1
  409. package/dist/slices/storage/schema.js +11 -0
  410. package/dist/slices/storage/trim.js +146 -0
  411. package/dist/slices/studio/disclosure.js +74 -0
  412. package/dist/slices/studio/model.js +30 -0
  413. package/dist/slices/studio/pack.js +211 -0
  414. package/dist/slices/studio/queue.js +69 -0
  415. package/dist/slices/studio/settings.js +170 -0
  416. package/dist/slices/style-preview/images.js +61 -0
  417. package/dist/slices/style-preview/narration.js +33 -0
  418. package/dist/slices/style-preview/render.js +148 -0
  419. package/dist/slices/style-preview/schema.js +59 -0
  420. package/dist/slices/style-preview/service.js +92 -0
  421. package/dist/slices/style-preview/settings.js +139 -0
  422. package/dist/slices/subtitles/captions.js +42 -9
  423. package/dist/slices/subtitles/model.js +22 -0
  424. package/dist/slices/subtitles/prepare.js +27 -16
  425. package/dist/slices/subtitles/transcript.js +1 -1
  426. package/dist/slices/thumbnail/run.js +7 -7
  427. package/dist/slices/trash/model.js +25 -0
  428. package/dist/slices/trash/service.js +273 -0
  429. package/dist/slices/tutorials/anchors.js +57 -0
  430. package/dist/slices/tutorials/library.js +185 -0
  431. package/dist/slices/uploads/repo.js +20 -0
  432. package/dist/slices/video/ambient-bed-schema.js +15 -0
  433. package/dist/slices/video/ambient-bed.js +95 -0
  434. package/dist/slices/video/ambient-mix.js +72 -0
  435. package/dist/slices/video/audio-inputs.js +2 -2
  436. package/dist/slices/video/cards.js +5 -2
  437. package/dist/slices/video/edit-list.js +23 -2
  438. package/dist/slices/video/edit-settings.js +13 -3
  439. package/dist/slices/video/ffmpeg.js +63 -9
  440. package/dist/slices/video/figure-card.js +492 -0
  441. package/dist/slices/video/figure-spans.js +51 -0
  442. package/dist/slices/video/plan.js +125 -13
  443. package/dist/slices/video/run.js +14 -12
  444. package/dist/slices/video/slideshow.js +39 -4
  445. package/dist/slices/voices/attribution.js +69 -0
  446. package/dist/slices/voices/audio-files.js +87 -0
  447. package/dist/slices/voices/audition.js +25 -0
  448. package/dist/slices/voices/cast.js +74 -0
  449. package/dist/slices/voices/grouping.js +59 -0
  450. package/dist/slices/voices/join.js +35 -0
  451. package/dist/slices/voices/languages.js +25 -0
  452. package/dist/slices/voices/model.js +218 -0
  453. package/dist/slices/voices/palette.js +26 -0
  454. package/dist/slices/voices/panel.js +91 -0
  455. package/dist/slices/voices/portraits.js +55 -0
  456. package/dist/slices/voices/script.js +136 -0
  457. package/dist/slices/voices/timing.js +58 -0
  458. package/dist/slices/youtube/answer.js +1 -1
  459. package/dist/slices/youtube/chapters.js +126 -0
  460. package/dist/slices/youtube/edits-repo.js +84 -0
  461. package/dist/slices/youtube/edits.js +89 -0
  462. package/dist/slices/youtube/placeholders.js +98 -0
  463. package/dist/slices/youtube/transcript.js +11 -4
  464. package/dist/tutorials/AI-CLIs.md +146 -0
  465. package/dist/tutorials/Backups.md +121 -0
  466. package/dist/tutorials/Calendar.md +103 -0
  467. package/dist/tutorials/Cast-Library.md +112 -0
  468. package/dist/tutorials/Channels.md +148 -0
  469. package/dist/tutorials/Costs-and-Run-Cost.md +136 -0
  470. package/dist/tutorials/Docker.md +171 -0
  471. package/dist/tutorials/Document-Themes.md +207 -0
  472. package/dist/tutorials/Editing-a-Project.md +204 -0
  473. package/dist/tutorials/FAQ.md +134 -0
  474. package/dist/tutorials/First-Launch-and-Welcome.md +153 -0
  475. package/dist/tutorials/Home-and-Projects.md +174 -0
  476. package/dist/tutorials/Home.md +119 -0
  477. package/dist/tutorials/Install.md +154 -0
  478. package/dist/tutorials/Intros-and-Outros.md +86 -0
  479. package/dist/tutorials/Keyboard-Shortcuts-and-Command-Palette.md +215 -0
  480. package/dist/tutorials/Library-Overview.md +75 -0
  481. package/dist/tutorials/Models.md +133 -0
  482. package/dist/tutorials/Multiple-Voices.md +161 -0
  483. package/dist/tutorials/Narration-Aliases-and-Glossary.md +129 -0
  484. package/dist/tutorials/Notifications.md +86 -0
  485. package/dist/tutorials/Other-Languages.md +110 -0
  486. package/dist/tutorials/PDF-Documents.md +100 -0
  487. package/dist/tutorials/Play-Images.md +135 -0
  488. package/dist/tutorials/Play-Narration.md +167 -0
  489. package/dist/tutorials/Play-Outputs.md +109 -0
  490. package/dist/tutorials/Play-Overview.md +216 -0
  491. package/dist/tutorials/Play-Title-and-Article.md +135 -0
  492. package/dist/tutorials/Play-Video-and-Style.md +143 -0
  493. package/dist/tutorials/Project-Page.md +228 -0
  494. package/dist/tutorials/Prompts.md +136 -0
  495. package/dist/tutorials/Providers-and-Keys.md +290 -0
  496. package/dist/tutorials/Publishing-to-YouTube.md +135 -0
  497. package/dist/tutorials/Recovery-and-Retries.md +141 -0
  498. package/dist/tutorials/Reviews-and-Checkpoints.md +143 -0
  499. package/dist/tutorials/Schedules.md +154 -0
  500. package/dist/tutorials/Settings-Reference.md +258 -0
  501. package/dist/tutorials/Shorts.md +116 -0
  502. package/dist/tutorials/Start-at-Login.md +126 -0
  503. package/dist/tutorials/Studio-Extension.md +146 -0
  504. package/dist/tutorials/Templates.md +146 -0
  505. package/dist/tutorials/Trash.md +87 -0
  506. package/dist/tutorials/Troubleshooting.md +149 -0
  507. package/dist/tutorials/Uninstalling-and-Moving.md +113 -0
  508. package/dist/tutorials/Updating-and-Patch-Notes.md +120 -0
  509. package/dist/tutorials/Video-Editing.md +135 -0
  510. package/dist/tutorials/Where-Your-Files-Live.md +149 -0
  511. package/dist/tutorials/YouTube-Description.md +132 -0
  512. package/dist/tutorials/Your-First-Short.md +125 -0
  513. package/dist/tutorials/_Footer.md +1 -0
  514. package/dist/tutorials/_Sidebar.md +69 -0
  515. package/dist/updater/service.js +66 -21
  516. package/dist/web/assets/index-DBLSfqA-.css +1 -0
  517. package/dist/web/assets/index-DFdIhmm0.js +307 -0
  518. package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  519. package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  520. package/dist/web/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  521. package/dist/web/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  522. package/dist/web/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  523. package/dist/web/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  524. package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  525. package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  526. package/dist/web/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  527. package/dist/web/assets/{pdf-BLF2i8c2.js → pdf-B9OY1ioX.js} +1 -1
  528. package/dist/web/index.html +2 -2
  529. package/package.json +5 -5
  530. package/dist/edge/docker-launch.js +0 -81
  531. package/dist/edge/docker-projects/activation.js +0 -35
  532. package/dist/edge/docker-projects/claims.js +0 -85
  533. package/dist/edge/docker-projects/committed.js +0 -40
  534. package/dist/edge/docker-projects/engine.js +0 -461
  535. package/dist/edge/docker-projects/install.js +0 -254
  536. package/dist/edge/docker-projects/prune.js +0 -66
  537. package/dist/edge/docker-projects/recover.js +0 -100
  538. package/dist/edge/docker-projects/state.js +0 -225
  539. package/dist/web/assets/index-CktpxlJd.js +0 -171
  540. package/dist/web/assets/index-DB-vOnGU.css +0 -1
  541. package/scripts/docker-run.sh +0 -3
  542. /package/dist/edge/{docker-projects → docker-install}/tree.js +0 -0
@@ -1,7 +1,10 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { z } from "zod";
3
- import { usesShorts } from "../admission/rules.js";
3
+ import { withLanguage, wordTimingUnavailable } from "../../kernel/ports/languages.js";
4
+ import { usesFigureCards, usesShorts } from "../admission/rules.js";
5
+ import { captionBoldFont } from "../fonts/coverage.js";
4
6
  import { resolveBoldFont } from "../fonts/index.js";
7
+ import { masterGoal } from "../loudness/model.js";
5
8
  import { clipWords } from "../shorts/captions.js";
6
9
  import { effectiveClips, pickedShortsOf } from "../shorts/clips.js";
7
10
  import { defaultShortsPrompt, musicVolumeOf, shortImageCount, shortsSpeedOf, } from "../shorts/model.js";
@@ -11,8 +14,10 @@ import { renderShort } from "../shorts/render.js";
11
14
  import { allocateAsset, discardPreparedAssets, sealAsset, writeAsset } from "../storage/assets.js";
12
15
  import { outputPath, projectDir } from "../storage/layout.js";
13
16
  import { probeDurationMs } from "../video/ffmpeg.js";
17
+ import { usesVoices } from "../voices/model.js";
14
18
  import { sentencesText, transcriptSentences } from "../youtube/transcript.js";
15
19
  import { shortsPickKey } from "./recipe-shorts.js";
20
+ import { figureShots } from "./runtime-export-edit.js";
16
21
  import { exportSnapshot, revisionAudio } from "./runtime-export-inputs.js";
17
22
  import { imageCall } from "./runtime-image.js";
18
23
  import { savedCatalogue } from "./runtime-plan.js";
@@ -20,7 +25,7 @@ import { preparedResult, preparedTexts, publishResult } from "./runtime-publicat
20
25
  import { wordsSchema } from "./runtime-subtitles.js";
21
26
  // The Shorts step's four kinds of work (`recipe-shorts.ts`): the pick, each clip's image
22
27
  // prompts, each image, and each clip's render. Each is one piece of its own, so a failure
23
- // names the clip it belongs to and Retry stage redoes only what did not finish.
28
+ // names the clip it belongs to and Try again redoes only what did not finish.
24
29
  export async function executeShortsRecipe(deps, context, providers, piece) {
25
30
  if (deps.db
26
31
  .prepare("SELECT state FROM revision_work_pieces WHERE id=? AND work_id=?")
@@ -40,7 +45,7 @@ export async function executeShortsRecipe(deps, context, providers, piece) {
40
45
  throw setupError();
41
46
  const clip = savedClips(snapshot).find((one) => one.number === number);
42
47
  if (clip === undefined)
43
- throw new Error("The clip this short was cut from is no longer among the picked shorts. Use Pick different moments in Edit project → Shorts, then Retry stage.");
48
+ throw new Error("The clip this short was cut from is no longer among the picked shorts. Use Pick different moments in Edit project → Shorts, then Try again.");
44
49
  if (matched[2] === "prompts")
45
50
  return prompts(deps, context, providers, piece, snapshot, clip);
46
51
  if (matched[2] === "render")
@@ -58,10 +63,10 @@ async function pick(deps, context, providers, piece, snapshot) {
58
63
  throw setupError();
59
64
  const llm = config.llm;
60
65
  if (llm === undefined || llm.provider.trim() === "" || llm.model.trim() === "")
61
- throw new Error("Shorts need an AI text model to pick the clips, and none is chosen. Choose one in Edit project → Providers, then Retry stage.");
66
+ throw new Error("Shorts need an AI text model to pick the clips, and none is chosen. Choose one in Edit project → Providers, then Try again.");
62
67
  const sentences = transcriptSentences(timingWords(deps, context, view));
63
68
  if (sentences.length === 0)
64
- throw new Error("The narration's word timing holds no words, so there is nothing to cut shorts from. Use Re-run section on Video, then Retry stage.");
69
+ throw new Error("The narration's word timing holds no words, so there is nothing to cut shorts from. Use More → Render the video again in the Video section, then Try again.");
65
70
  const audio = await revisionAudio(deps, context, view);
66
71
  const durationSeconds = audio.reduce((sum, segment) => sum + segment.seconds, 0);
67
72
  const limits = {
@@ -81,13 +86,17 @@ async function pick(deps, context, providers, piece, snapshot) {
81
86
  count: shorts.count,
82
87
  minSeconds: shorts.minSeconds,
83
88
  maxSeconds: shorts.maxSeconds,
84
- sentences: sentencesText(sentences),
89
+ sentences: sentencesText(sentences, usesVoices(config) && config.voices !== undefined
90
+ ? Object.fromEntries(config.voices.speakers.map((speaker) => [speaker.id, speaker.name.trim()]))
91
+ : undefined),
85
92
  };
86
93
  const ask = llmCall(deps, context, piece, view, (text) => {
87
94
  const checked = checkPicks(text, sentences, limits);
88
95
  return checked.ok ? undefined : checked.reason;
89
96
  });
90
- const first = await providers.forPiece(piece.id).llm(ask(pickMessages(brief)));
97
+ const first = await providers
98
+ .forPiece(piece.id)
99
+ .llm(ask(withLanguage(pickMessages(brief), config.language)));
91
100
  if (!first.ok)
92
101
  return "held";
93
102
  let checked = checkPicks(first.value.text, sentences, limits);
@@ -100,7 +109,7 @@ async function pick(deps, context, providers, piece, snapshot) {
100
109
  return "held";
101
110
  const second = await providers
102
111
  .forPiece(piece.id)
103
- .llm(ask(pickRetryMessages(brief, first.value.text, checked.problems)));
112
+ .llm(ask(withLanguage(pickRetryMessages(brief, first.value.text, checked.problems), config.language)));
104
113
  if (!second.ok)
105
114
  return "held";
106
115
  const again = checkPicks(second.value.text, sentences, limits);
@@ -191,13 +200,27 @@ async function render(deps, context, piece, snapshot, clip) {
191
200
  .map((recipe) => {
192
201
  const row = view.outputs.find((one) => one.workKey === recipe.key && one.selected && one.available && one.state === "ready");
193
202
  if (row === undefined)
194
- throw new Error(`Short ${String(clip.number)} is missing one of its images. Retry stage to make it again; if it keeps happening, use Make this short again in Edit project → Shorts.`);
203
+ throw new Error(`Short ${String(clip.number)} is missing one of its images. Try again to make it again; if it keeps happening, use Make this short again in Edit project → Shorts.`);
195
204
  return outputPath(deps.paths, context.work.projectId, row.output.path);
196
205
  });
197
- const words = clipWords(timingWords(deps, context, view), clip.start, clip.end);
198
- const timeline = await revisionAudio(deps, context, view);
206
+ const allWords = timingWords(deps, context, view);
207
+ const words = clipWords(allWords, clip.start, clip.end);
208
+ // The upright cards whose descriptions fall in the clip, on the clip's own timeline.
209
+ const figures = usesFigureCards(config)
210
+ ? figureShots(deps, context, view, "9:16", allWords).flatMap((figure) => figure.end <= clip.start || figure.start >= clip.end
211
+ ? []
212
+ : [
213
+ {
214
+ path: figure.path,
215
+ start: Math.max(0, figure.start - clip.start),
216
+ end: Math.min(clip.end, figure.end) - clip.start,
217
+ },
218
+ ])
219
+ : [];
220
+ const timeline = await revisionAudio(deps, context, view, { levelled: true });
199
221
  // The bundled Barlow is drawn in its own Bold face; any other font is emboldened.
200
- const font = await resolveBoldFont(deps.paths, config.subtitles?.fontId ?? "default");
222
+ // In another language, a font that has its letters (`fonts/coverage.ts`).
223
+ const font = await captionBoldFont(deps.paths, config.subtitles?.fontId ?? "default", config.language);
201
224
  const shorts = config.shorts;
202
225
  const speed = shorts === undefined ? 1 : shortsSpeedOf(shorts);
203
226
  const music = await backgroundMusic(deps, context, view);
@@ -207,8 +230,9 @@ async function render(deps, context, piece, snapshot, clip) {
207
230
  const prepared = [];
208
231
  const seconds = (clip.end - clip.start) / speed;
209
232
  try {
210
- await renderShort({
233
+ const master = await renderShort({
211
234
  bin: deps.ffmpeg,
235
+ master: masterGoal(config, "video"),
212
236
  timeline,
213
237
  start: clip.start,
214
238
  end: clip.end,
@@ -217,8 +241,10 @@ async function render(deps, context, piece, snapshot, clip) {
217
241
  motionStyle: config.motionStyle,
218
242
  zoomPercent: config.zoomPercent,
219
243
  words,
244
+ ...(wordTimingUnavailable(config.language) === undefined ? {} : { wordByWord: false }),
220
245
  font,
221
246
  ...(shorts?.titleOnScreen === true ? { title: clip.title } : {}),
247
+ ...(figures.length === 0 ? {} : { figures }),
222
248
  speed,
223
249
  music,
224
250
  output: pending.absolutePath,
@@ -239,6 +265,7 @@ async function render(deps, context, piece, snapshot, clip) {
239
265
  prepared.push(preparedResult(deps, context, piece, "short_video", asset, durationMs, {
240
266
  short: clip.number,
241
267
  sentences: [clip.first, clip.last],
268
+ ...(master === undefined ? {} : { master }),
242
269
  }));
243
270
  await publishResult(deps, context, piece, prepared, { short: clip.number, start: clip.start, end: clip.end, durationMs }, asset);
244
271
  // One per rendered short, with no stage: stage "video" would read as a finished video.
@@ -271,7 +298,7 @@ async function backgroundMusic(deps, context, view) {
271
298
  const shorts = view.revision.config.shorts;
272
299
  if (assetId === undefined || shorts === undefined)
273
300
  return undefined;
274
- const fix = "Choose another file under Background music in Edit project → Shorts, or remove the music, then Retry stage.";
301
+ const fix = "Choose another file under Background music in Edit project → Shorts, or remove the music, then Try again.";
275
302
  const row = z
276
303
  .object({ path: z.string() })
277
304
  .safeParse(deps.db
@@ -292,14 +319,14 @@ async function backgroundMusic(deps, context, view) {
292
319
  throw new Error(`The shorts' background music file holds no sound. ${fix}`);
293
320
  return { path, volume: musicVolumeOf(shorts) };
294
321
  }
295
- function timingWords(deps, context, view) {
322
+ export function timingWords(deps, context, view) {
296
323
  const timing = view.outputs.find((row) => row.selected &&
297
324
  row.available &&
298
325
  row.state === "ready" &&
299
326
  row.workKey === "subtitles:timing" &&
300
327
  row.output.role === "subtitle_words");
301
328
  if (timing === undefined)
302
- throw new Error("The narration's word timing, which the shorts are cut and captioned from, is missing. Use Re-run section on Video, then Retry stage.");
329
+ throw new Error("The narration's word timing, which the shorts are cut and captioned from, is missing. Use More → Render the video again in the Video section, then Try again.");
303
330
  return wordsSchema.parse(JSON.parse(readFileSync(outputPath(deps.paths, context.work.projectId, timing.output.path), "utf8"))).words;
304
331
  }
305
332
  // The project's text model, with the thinking settings saved for this step's catalogue.
@@ -324,5 +351,5 @@ function imageIndex(key) {
324
351
  return Number(key.split(":").at(-1));
325
352
  }
326
353
  function setupError() {
327
- return new Error("Slopify hit an internal error (a Shorts step was set up wrongly). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
354
+ return new Error("Slopify hit an internal error (a Shorts step was set up wrongly). Try again; if it happens again, use Download diagnostics in Settings and report it.");
328
355
  }
@@ -1,7 +1,9 @@
1
1
  import { z } from "zod";
2
2
  import { stageKinds, stageStates } from "../../kernel/pipeline.js";
3
+ import { optionalDeps } from "../../kernel/runner/graph.js";
3
4
  import { sourceOf } from "../admission/model.js";
4
5
  import { usesShorts, usesYoutubeDescription } from "../admission/rules.js";
6
+ import { reviewsNarration } from "../reviews/rules.js";
5
7
  import { currentRevisionId } from "../revisions/repo.js";
6
8
  import { getRevisionView } from "../revisions/view.js";
7
9
  import { settleScheduleRunsForProject } from "../schedules/repo.js";
@@ -40,7 +42,8 @@ export function executionStages(deps, projectId) {
40
42
  view?.revision.content.subtitleCues !== undefined &&
41
43
  !usesYoutubeDescription(view.revision.config) &&
42
44
  !usesShorts(view.revision.config) &&
43
- !editNeedsTiming(view.revision.config))
45
+ !editNeedsTiming(view.revision.config) &&
46
+ !reviewsNarration(view.revision.config))
44
47
  return false;
45
48
  if (piece.input.kind === "deferred" && piece.key.endsWith(":future")) {
46
49
  const cacheKey = String(row.id);
@@ -78,10 +81,13 @@ export function executionStages(deps, projectId) {
78
81
  }
79
82
  export function invocationReady(deps, work) {
80
83
  const row = deps.db
81
- .prepare("SELECT recipe_context FROM revision_work WHERE id=? AND project_id=? AND revision_id=?")
84
+ .prepare("SELECT recipe_context,retry_at FROM revision_work WHERE id=? AND project_id=? AND revision_id=?")
82
85
  .get(work.workId, work.projectId, work.revisionId);
83
86
  if (row?.recipe_context === null || row === undefined)
84
87
  return false;
88
+ // Waiting out a failure time can fix: `dueRetries` clears the wait when it runs out.
89
+ if (row.retry_at !== null)
90
+ return false;
85
91
  const view = executionView(deps, work.projectId, work.revisionId);
86
92
  if (view === undefined)
87
93
  return false;
@@ -99,10 +105,31 @@ export function invocationReady(deps, work) {
99
105
  return false;
100
106
  return recipe.dependsOn.every((key) => {
101
107
  const dependency = plan.work.find((value) => value.key === key);
102
- return dependency?.disposition === "reuse" && !dependency.inflight;
108
+ if (dependency?.disposition === "reuse" && !dependency.inflight)
109
+ return true;
110
+ // A dependency the step can do without (the document's cover) releases it once it has
111
+ // failed for good; the step then runs without it. The fingerprint is unchanged: it
112
+ // already names the missing cover, so a thumbnail made later marks the document
113
+ // outdated rather than current.
114
+ return (dependency !== undefined &&
115
+ (optionalDeps[recipe.stage]?.includes(dependency.stage) ?? false) &&
116
+ gaveUp(deps, work.projectId, key));
103
117
  });
104
118
  });
105
119
  }
120
+ // Whether the current run's step for `key` failed or was canceled and isn't waiting to try
121
+ // again by itself.
122
+ function gaveUp(deps, projectId, key) {
123
+ const row = deps.db
124
+ .prepare(`SELECT w.state,w.retry_at FROM revision_work_reservations r
125
+ JOIN revision_work w ON w.id=r.work_id
126
+ JOIN project_heads h ON h.project_id=r.project_id AND h.revision_id=r.revision_id
127
+ WHERE r.project_id=? AND r.work_key=?`)
128
+ .get(projectId, key);
129
+ return (row !== undefined &&
130
+ row.retry_at === null &&
131
+ (row.state === "failed" || row.state === "canceled"));
132
+ }
106
133
  export function executionStandings(deps, projectId, derived) {
107
134
  const revisionId = currentRevisionId(deps.db, projectId);
108
135
  const view = revisionId === undefined ? undefined : getRevisionView(deps, projectId, revisionId);
@@ -129,6 +156,9 @@ export function executionStandings(deps, projectId, derived) {
129
156
  return retained ? { ...entry, state: "done" } : entry;
130
157
  });
131
158
  let state = source === "off" ? "skipped" : source === "provide" ? "provided" : "pending";
159
+ const retryAt = group.some((entry) => entry.state === "failed")
160
+ ? null
161
+ : latestRetry(deps, group);
132
162
  if (group.some((entry) => entry.state === "running"))
133
163
  state = "running";
134
164
  else if (group.some((entry) => entry.state === "failed"))
@@ -141,9 +171,23 @@ export function executionStandings(deps, projectId, derived) {
141
171
  ? "provided"
142
172
  : "done"
143
173
  : "pending";
144
- return { kind, state };
174
+ return state === "pending" && retryAt !== null ? { kind, state, retryAt } : { kind, state };
145
175
  });
146
176
  }
177
+ // The latest wait among a stage's steps that went back to wait after a failure time can fix.
178
+ function latestRetry(deps, group) {
179
+ let latest = null;
180
+ for (const entry of group) {
181
+ if (entry.state !== "pending")
182
+ continue;
183
+ const at = deps.db
184
+ .prepare("SELECT retry_at FROM revision_work WHERE id=?")
185
+ .get(entry.work.workId)?.retry_at;
186
+ if (typeof at === "string" && (latest === null || at > latest))
187
+ latest = at;
188
+ }
189
+ return latest;
190
+ }
147
191
  export function projectStandings(deps, projectId) {
148
192
  // One derivation serves every stage: each builds the whole execution plan.
149
193
  const work = executionStages(deps, projectId);
@@ -159,15 +203,19 @@ export function projectStandings(deps, projectId) {
159
203
  .get(entry.work.workId);
160
204
  return total + fraction(progress?.progress_current, progress?.progress_total);
161
205
  }, 0);
162
- const failed = all.find((work) => work.state === "failed");
206
+ // A failed step, or else one waiting to run again, says why on the stage row.
207
+ const failed = all.find((work) => work.state === "failed") ??
208
+ (stage.retryAt === undefined || stage.retryAt === null
209
+ ? undefined
210
+ : all.find((work) => work.state === "pending"));
163
211
  const failure = failed === undefined
164
- ? null
165
- : (deps.db
166
- .prepare("SELECT failure_reason FROM revision_work WHERE id=?")
167
- .get(failed.work.workId)?.failure_reason ?? null);
212
+ ? undefined
213
+ : deps.db
214
+ .prepare("SELECT failure_reason,failure_kind FROM revision_work WHERE id=?")
215
+ .get(failed.work.workId);
168
216
  deps.db
169
- .prepare("UPDATE stages SET state=?,failure_reason=?,progress_current=?,progress_total=?,finished_at=CASE WHEN ?='done' THEN ? ELSE NULL END WHERE project_id=? AND kind=?")
170
- .run(stage.state, failure, invocations.length === 0 ? null : current, invocations.length === 0 ? null : invocations.length, stage.state, deps.clock.now().toISOString(), projectId, stage.kind);
217
+ .prepare("UPDATE stages SET state=?,failure_reason=?,failure_kind=?,retry_at=?,progress_current=?,progress_total=?,finished_at=CASE WHEN ?='done' THEN ? ELSE NULL END WHERE project_id=? AND kind=?")
218
+ .run(stage.state, textOf(failure?.failure_reason), textOf(failure?.failure_kind), stage.retryAt ?? null, invocations.length === 0 ? null : current, invocations.length === 0 ? null : invocations.length, stage.state, deps.clock.now().toISOString(), projectId, stage.kind);
171
219
  }
172
220
  settleScheduleRunsForProject(deps.db, projectId, deps.clock.now().toISOString());
173
221
  }
@@ -225,6 +273,9 @@ const spokenKey = /^audio:(?:intro|body|outro)(?::(?!concat$|future$).+)?$/;
225
273
  function spoken(deps, workId) {
226
274
  return workPieces(deps.db, workId).some((piece) => spokenKey.test(piece.key));
227
275
  }
276
+ function textOf(value) {
277
+ return typeof value === "string" ? value : null;
278
+ }
228
279
  function fraction(current, total) {
229
280
  if (typeof current !== "number" || typeof total !== "number" || total <= 0)
230
281
  return 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 → Narration, 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, view.revision.config.voices?.book), { 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
+ }