@gentbajko/slopify 2.5.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (446) hide show
  1. package/LICENSE +201 -21
  2. package/NOTICE +12 -0
  3. package/README.md +21 -40
  4. package/SUBTITLES.md +1 -1
  5. package/dist/adapter-registry.js +3 -0
  6. package/dist/adapters/alignment/cache.js +4 -1
  7. package/dist/adapters/alignment/ctc.js +25 -23
  8. package/dist/adapters/alignment/index.js +37 -6
  9. package/dist/adapters/alignment/multilingual.js +267 -0
  10. package/dist/adapters/alignment/protocol.js +11 -0
  11. package/dist/adapters/alignment/quality.js +4 -3
  12. package/dist/adapters/alignment/sentences.js +95 -0
  13. package/dist/adapters/alignment/spec.js +24 -0
  14. package/dist/adapters/alignment/spell.js +769 -0
  15. package/dist/adapters/alignment/text.js +56 -9
  16. package/dist/adapters/alignment/window.js +34 -30
  17. package/dist/adapters/alignment/worker.js +23 -21
  18. package/dist/adapters/explain.js +11 -11
  19. package/dist/adapters/host-cli/index.js +44 -5
  20. package/dist/adapters/host-cli/transport.js +7 -5
  21. package/dist/adapters/image/bytes.js +2 -2
  22. package/dist/adapters/image/codex-output.js +1 -1
  23. package/dist/adapters/image/codex.js +87 -22
  24. package/dist/adapters/image/fal.js +13 -5
  25. package/dist/adapters/image/google.js +11 -8
  26. package/dist/adapters/image/openai.js +14 -7
  27. package/dist/adapters/image/reference.js +34 -0
  28. package/dist/adapters/image/replicate.js +7 -2
  29. package/dist/adapters/image/video.js +2 -2
  30. package/dist/adapters/key-probes.js +36 -0
  31. package/dist/adapters/llm/claude-code.js +168 -32
  32. package/dist/adapters/llm/cli-login-error.js +1 -1
  33. package/dist/adapters/llm/codex-limits.js +157 -0
  34. package/dist/adapters/llm/codex.js +99 -12
  35. package/dist/adapters/llm/document-workspace.js +2 -2
  36. package/dist/adapters/llm/gemini-workspace.js +1 -1
  37. package/dist/adapters/llm/gemini.js +16 -3
  38. package/dist/adapters/llm/image-workspace.js +58 -0
  39. package/dist/adapters/llm/openrouter.js +11 -3
  40. package/dist/adapters/llm/run-cli.js +12 -7
  41. package/dist/adapters/tts/cartesia.js +10 -0
  42. package/dist/adapters/tts/elevenlabs.js +31 -9
  43. package/dist/adapters/tts/inworld-async.js +9 -5
  44. package/dist/adapters/tts/inworld.js +17 -3
  45. package/dist/adapters/tts/openai.js +3 -0
  46. package/dist/adapters/tts/voice-languages.js +80 -0
  47. package/dist/assets/fonts/noto/NotoSans-Regular.ttf +0 -0
  48. package/dist/assets/fonts/noto/NotoSansArabic-Regular.ttf +0 -0
  49. package/dist/assets/fonts/noto/NotoSansDevanagari-Regular.ttf +0 -0
  50. package/dist/assets/fonts/noto/NotoSansHebrew-Regular.ttf +0 -0
  51. package/dist/assets/fonts/noto/NotoSansThai-Regular.ttf +0 -0
  52. package/dist/assets/fonts/noto/OFL.txt +93 -0
  53. package/dist/assets/fonts/noto/SOURCE.txt +18 -0
  54. package/dist/assets/sample/sample-audiobook.tar +0 -0
  55. package/dist/assets/sample/sample-podcast.tar +0 -0
  56. package/dist/assets/sample/sample-project.tar +0 -0
  57. package/dist/catalog/merge.js +132 -0
  58. package/dist/catalog/registry.js +13 -13
  59. package/dist/catalog/schema.js +2 -0
  60. package/dist/catalog/store.js +147 -8
  61. package/dist/compose.yaml +71 -0
  62. package/dist/edge/autostart/docker-record.js +32 -0
  63. package/dist/edge/autostart/docker.js +50 -0
  64. package/dist/edge/autostart/index.js +78 -0
  65. package/dist/edge/autostart/launcher.js +176 -0
  66. package/dist/edge/autostart/model.js +3 -0
  67. package/dist/edge/autostart/native.js +210 -0
  68. package/dist/edge/autostart/prompt.js +66 -0
  69. package/dist/edge/autostart/service.js +81 -0
  70. package/dist/edge/cli.js +57 -51
  71. package/dist/edge/docker-install/activation.js +56 -0
  72. package/dist/edge/docker-install/apply.js +458 -0
  73. package/dist/edge/docker-install/engine.js +362 -0
  74. package/dist/edge/docker-install/run.js +154 -0
  75. package/dist/edge/docker-install/state.js +146 -0
  76. package/dist/edge/{docker-projects → docker-install}/volume.js +1 -5
  77. package/dist/edge/docker.js +14 -22
  78. package/dist/edge/host-cli.js +1 -1
  79. package/dist/edge/http/actions.js +32 -10
  80. package/dist/edge/http/app.js +58 -2
  81. package/dist/edge/http/auditions.js +70 -0
  82. package/dist/edge/http/autostart.js +37 -0
  83. package/dist/edge/http/channel-memory.js +92 -0
  84. package/dist/edge/http/channels.js +128 -0
  85. package/dist/edge/http/entries.js +22 -3
  86. package/dist/edge/http/files.js +1 -1
  87. package/dist/edge/http/folder-location.js +1 -1
  88. package/dist/edge/http/home.js +39 -0
  89. package/dist/edge/http/host-cli.js +29 -5
  90. package/dist/edge/http/narration-peaks.js +76 -0
  91. package/dist/edge/http/onboarding.js +152 -0
  92. package/dist/edge/http/open-folder.js +1 -1
  93. package/dist/edge/http/patch-notes.js +45 -0
  94. package/dist/edge/http/planning.js +4 -2
  95. package/dist/edge/http/project-create.js +87 -0
  96. package/dist/edge/http/projects.js +31 -99
  97. package/dist/edge/http/prompts.js +40 -3
  98. package/dist/edge/http/pronunciations.js +23 -4
  99. package/dist/edge/http/providers.js +86 -0
  100. package/dist/edge/http/reviews.js +121 -0
  101. package/dist/edge/http/revision-files.js +1 -1
  102. package/dist/edge/http/revisions.js +1 -1
  103. package/dist/edge/http/run-cost.js +25 -0
  104. package/dist/edge/http/schedules.js +111 -6
  105. package/dist/edge/http/settings.js +84 -6
  106. package/dist/edge/http/storage-files.js +50 -0
  107. package/dist/edge/http/storage.js +29 -2
  108. package/dist/edge/http/studio.js +207 -0
  109. package/dist/edge/http/style-preview.js +100 -0
  110. package/dist/edge/http/subtitles.js +1 -1
  111. package/dist/edge/http/trash.js +69 -0
  112. package/dist/edge/http/update.js +24 -3
  113. package/dist/edge/http/whats-new.js +15 -0
  114. package/dist/edge/http/youtube-edits.js +83 -0
  115. package/dist/edge/native-update.js +62 -0
  116. package/dist/host-cli/open-folder.js +10 -10
  117. package/dist/host-cli/runtime.js +9 -1
  118. package/dist/kernel/db/migrations/0025-automatic-reviews.sql +40 -0
  119. package/dist/kernel/db/migrations/0026-schedule-topic-generation.sql +23 -0
  120. package/dist/kernel/db/migrations/0027-run-cost.sql +58 -0
  121. package/dist/kernel/db/migrations/0030-automatic-retries.sql +18 -0
  122. package/dist/kernel/db/migrations/0031-prompt-history-and-description-edits.sql +33 -0
  123. package/dist/kernel/db/migrations/0032-channels.sql +65 -0
  124. package/dist/kernel/db/migrations/0034-script-prompts.sql +23 -0
  125. package/dist/kernel/db/migrations/0035-narration-aliases.sql +13 -0
  126. package/dist/kernel/db/migrations/0037-project-uploads.sql +8 -0
  127. package/dist/kernel/db/migrations/0038-voice-languages.sql +7 -0
  128. package/dist/kernel/db/migrations/0039-channel-essentials.sql +64 -0
  129. package/dist/kernel/db/migrations/0040-standalone-usage.sql +35 -0
  130. package/dist/kernel/paths.js +27 -6
  131. package/dist/kernel/pipeline.js +4 -1
  132. package/dist/kernel/ports/host-cli.js +122 -14
  133. package/dist/kernel/ports/key-probe.js +1 -0
  134. package/dist/kernel/ports/languages.js +157 -0
  135. package/dist/kernel/ports/model.js +5 -2
  136. package/dist/kernel/ports/narration-aliases.js +60 -0
  137. package/dist/kernel/ports/plan-limits.js +36 -0
  138. package/dist/kernel/runner/attempt.js +17 -8
  139. package/dist/kernel/runner/audition.js +36 -0
  140. package/dist/kernel/runner/graph.js +24 -5
  141. package/dist/kernel/runner/index.js +35 -5
  142. package/dist/kernel/runner/meter.js +1 -0
  143. package/dist/kernel/runner/providers.js +142 -7
  144. package/dist/kernel/runner/retry-policy.js +43 -0
  145. package/dist/kernel/runner/standalone.js +134 -0
  146. package/dist/kernel/runner/work-authority.js +28 -2
  147. package/dist/main.js +272 -16
  148. package/dist/patch-notes/0.4.2-to-1.0.md +93 -0
  149. package/dist/patch-notes/1.0-to-2.0.md +74 -0
  150. package/dist/patch-notes/2.0-to-3.0.md +257 -0
  151. package/dist/patch-notes/3.0.0.md +70 -0
  152. package/dist/patch-notes/index.json +26 -0
  153. package/dist/slices/admission/model.js +27 -0
  154. package/dist/slices/admission/repo.js +24 -4
  155. package/dist/slices/admission/rules.js +79 -1
  156. package/dist/slices/admission/schema.js +58 -1
  157. package/dist/slices/admission/short-mode.js +47 -0
  158. package/dist/slices/admission/start.js +41 -5
  159. package/dist/slices/article/continuation.js +2 -2
  160. package/dist/slices/article/provided-entries.js +2 -2
  161. package/dist/slices/article/run.js +6 -2
  162. package/dist/slices/article/segments.js +3 -3
  163. package/dist/slices/backups/folder.js +14 -6
  164. package/dist/slices/batch/index.js +7 -2
  165. package/dist/slices/cancel/index.js +1 -1
  166. package/dist/slices/channels/cast-images.js +110 -0
  167. package/dist/slices/channels/cast-match.js +32 -0
  168. package/dist/slices/channels/images.js +19 -0
  169. package/dist/slices/channels/model.js +5 -0
  170. package/dist/slices/channels/rebrand.js +100 -0
  171. package/dist/slices/channels/repo.js +218 -0
  172. package/dist/slices/channels/runs.js +129 -0
  173. package/dist/slices/channels/schema.js +129 -0
  174. package/dist/slices/channels/service.js +180 -0
  175. package/dist/slices/channels/studio-csv.js +110 -0
  176. package/dist/slices/channels/videos.js +120 -0
  177. package/dist/slices/checkpoints/recovery.js +10 -0
  178. package/dist/slices/control/index.js +2 -1
  179. package/dist/slices/document/fonts.js +1 -1
  180. package/dist/slices/document/render.js +1 -1
  181. package/dist/slices/episodes/related.js +90 -0
  182. package/dist/slices/episodes/repo.js +60 -0
  183. package/dist/slices/episodes/service.js +58 -0
  184. package/dist/slices/episodes/summarize.js +170 -0
  185. package/dist/slices/estimate/index.js +109 -28
  186. package/dist/slices/estimate/requests.js +60 -2
  187. package/dist/slices/fixes/rules.js +73 -0
  188. package/dist/slices/fonts/coverage.js +82 -0
  189. package/dist/slices/fonts/sfnt.js +74 -0
  190. package/dist/slices/images/run.js +8 -5
  191. package/dist/slices/images/scale.js +84 -0
  192. package/dist/slices/library/diff.js +122 -0
  193. package/dist/slices/library/history.js +100 -0
  194. package/dist/slices/library/model.js +3 -0
  195. package/dist/slices/library/photorealistic.js +49 -0
  196. package/dist/slices/library/repo.js +19 -14
  197. package/dist/slices/library/save.js +62 -12
  198. package/dist/slices/library/slots.js +7 -1
  199. package/dist/slices/library/used-by.js +153 -0
  200. package/dist/slices/loudness/level-pieces.js +41 -0
  201. package/dist/slices/loudness/loudnorm.js +164 -0
  202. package/dist/slices/loudness/model.js +153 -0
  203. package/dist/slices/model-upkeep/model.js +10 -0
  204. package/dist/slices/model-upkeep/switch.js +217 -0
  205. package/dist/slices/model-upkeep/usage.js +183 -0
  206. package/dist/slices/narration/aliases-library.js +31 -0
  207. package/dist/slices/narration/aliases-schema.js +53 -0
  208. package/dist/slices/narration/aliases.js +49 -0
  209. package/dist/slices/narration/blocks.js +352 -0
  210. package/dist/slices/narration/chunk.js +19 -7
  211. package/dist/slices/narration/concat.js +2 -2
  212. package/dist/slices/narration/describe.js +45 -0
  213. package/dist/slices/narration/pauses-model.js +63 -0
  214. package/dist/slices/narration/pauses.js +269 -0
  215. package/dist/slices/narration/peaks-model.js +1 -0
  216. package/dist/slices/narration/peaks.js +94 -0
  217. package/dist/slices/narration/preparation.js +21 -3
  218. package/dist/slices/narration/pronunciation-chunks.js +3 -3
  219. package/dist/slices/narration/pronunciation.js +72 -25
  220. package/dist/slices/narration/run.js +6 -6
  221. package/dist/slices/narration/shared-glossary.js +1 -1
  222. package/dist/slices/narration/steering.js +15 -4
  223. package/dist/slices/notifications/notifier.js +21 -3
  224. package/dist/slices/notifications/rules.js +22 -1
  225. package/dist/slices/onboarding/copy.js +143 -0
  226. package/dist/slices/onboarding/first-run.js +46 -0
  227. package/dist/slices/onboarding/install.js +119 -0
  228. package/dist/slices/onboarding/model.js +64 -0
  229. package/dist/slices/onboarding/packs.js +491 -0
  230. package/dist/slices/onboarding/quick-short.js +129 -0
  231. package/dist/slices/onboarding/sample.js +132 -0
  232. package/dist/slices/onboarding/state.js +68 -0
  233. package/dist/slices/onboarding/template.js +73 -0
  234. package/dist/slices/patch-notes/library.js +73 -0
  235. package/dist/slices/patch-notes/seen.js +77 -0
  236. package/dist/slices/play-drafts/convert.js +125 -6
  237. package/dist/slices/play-drafts/readiness.js +3 -0
  238. package/dist/slices/play-drafts/repo.js +2 -0
  239. package/dist/slices/play-drafts/review-inputs.js +20 -3
  240. package/dist/slices/play-drafts/schema.js +89 -0
  241. package/dist/slices/play-drafts/service.js +4 -1
  242. package/dist/slices/play-drafts/start.js +43 -18
  243. package/dist/slices/project-templates/from-project.js +50 -4
  244. package/dist/slices/project-templates/one-off.js +26 -0
  245. package/dist/slices/project-templates/repo.js +2 -1
  246. package/dist/slices/project-templates/service.js +15 -4
  247. package/dist/slices/project-templates/setup.js +18 -2
  248. package/dist/slices/rebuild/admission-repo.js +1 -1
  249. package/dist/slices/rebuild/model.js +6 -0
  250. package/dist/slices/rebuild/preview-details.js +46 -3
  251. package/dist/slices/rebuild/preview-plan.js +13 -6
  252. package/dist/slices/rebuild/recipe-audio-parts.js +40 -7
  253. package/dist/slices/rebuild/recipe-audio.js +66 -12
  254. package/dist/slices/rebuild/recipe-build.js +8 -4
  255. package/dist/slices/rebuild/recipe-cast.js +18 -0
  256. package/dist/slices/rebuild/recipe-checkpoints.js +11 -9
  257. package/dist/slices/rebuild/recipe-describe.js +93 -0
  258. package/dist/slices/rebuild/recipe-edit.js +13 -1
  259. package/dist/slices/rebuild/recipe-exports.js +62 -8
  260. package/dist/slices/rebuild/recipe-input-schema.js +35 -0
  261. package/dist/slices/rebuild/recipe-legacy.js +9 -0
  262. package/dist/slices/rebuild/recipe-loudness.js +45 -0
  263. package/dist/slices/rebuild/recipe-model.js +25 -6
  264. package/dist/slices/rebuild/recipe-pauses.js +7 -0
  265. package/dist/slices/rebuild/recipe-preparation.js +3 -3
  266. package/dist/slices/rebuild/recipe-provider-choice.js +11 -1
  267. package/dist/slices/rebuild/recipe-reference.js +15 -1
  268. package/dist/slices/rebuild/recipe-reviews.js +117 -0
  269. package/dist/slices/rebuild/recipe-save.js +4 -1
  270. package/dist/slices/rebuild/recipe-shorts.js +23 -3
  271. package/dist/slices/rebuild/recipe-text.js +114 -19
  272. package/dist/slices/rebuild/recipe-validation.js +14 -2
  273. package/dist/slices/rebuild/recipe-visual.js +96 -33
  274. package/dist/slices/rebuild/recipe-voices.js +286 -0
  275. package/dist/slices/rebuild/recipe-work.js +77 -6
  276. package/dist/slices/rebuild/recipe-youtube.js +2 -0
  277. package/dist/slices/rebuild/recovery-conflict.js +7 -3
  278. package/dist/slices/rebuild/recovery-model.js +3 -0
  279. package/dist/slices/rebuild/recovery-selection.js +28 -2
  280. package/dist/slices/rebuild/recovery.js +35 -17
  281. package/dist/slices/rebuild/repo.js +3 -3
  282. package/dist/slices/rebuild/retry.js +18 -0
  283. package/dist/slices/rebuild/review-redo.js +110 -0
  284. package/dist/slices/rebuild/runtime-actions.js +5 -0
  285. package/dist/slices/rebuild/runtime-admission.js +4 -1
  286. package/dist/slices/rebuild/runtime-animate.js +4 -4
  287. package/dist/slices/rebuild/runtime-article.js +5 -5
  288. package/dist/slices/rebuild/runtime-document.js +4 -4
  289. package/dist/slices/rebuild/runtime-export-bed.js +43 -0
  290. package/dist/slices/rebuild/runtime-export-edit.js +70 -11
  291. package/dist/slices/rebuild/runtime-export-inputs.js +52 -10
  292. package/dist/slices/rebuild/runtime-export-short.js +85 -0
  293. package/dist/slices/rebuild/runtime-export.js +84 -19
  294. package/dist/slices/rebuild/runtime-figure-card.js +87 -0
  295. package/dist/slices/rebuild/runtime-image.js +21 -2
  296. package/dist/slices/rebuild/runtime-local.js +148 -24
  297. package/dist/slices/rebuild/runtime-materialize.js +8 -0
  298. package/dist/slices/rebuild/runtime-narration-reuse.js +2 -1
  299. package/dist/slices/rebuild/runtime-narration-text.js +7 -7
  300. package/dist/slices/rebuild/runtime-pictures.js +28 -0
  301. package/dist/slices/rebuild/runtime-piece-label.js +27 -0
  302. package/dist/slices/rebuild/runtime-provider.js +126 -18
  303. package/dist/slices/rebuild/runtime-publication.js +21 -5
  304. package/dist/slices/rebuild/runtime-review.js +237 -0
  305. package/dist/slices/rebuild/runtime-run.js +17 -11
  306. package/dist/slices/rebuild/runtime-shorts.js +44 -17
  307. package/dist/slices/rebuild/runtime-store.js +39 -10
  308. package/dist/slices/rebuild/runtime-subtitles.js +72 -19
  309. package/dist/slices/rebuild/runtime-voices.js +103 -0
  310. package/dist/slices/rebuild/runtime-youtube.js +7 -6
  311. package/dist/slices/rebuild/soften.js +49 -0
  312. package/dist/slices/rebuild/transition-repo.js +9 -2
  313. package/dist/slices/research/run.js +4 -4
  314. package/dist/slices/research/synthesis.js +2 -2
  315. package/dist/slices/reviews/model.js +105 -0
  316. package/dist/slices/reviews/outcome.js +22 -0
  317. package/dist/slices/reviews/repo.js +106 -0
  318. package/dist/slices/reviews/rules.js +63 -0
  319. package/dist/slices/reviews/verdict.js +71 -0
  320. package/dist/slices/reviews/view.js +30 -0
  321. package/dist/slices/revisions/adopt-content.js +5 -2
  322. package/dist/slices/revisions/adopt.js +3 -1
  323. package/dist/slices/revisions/downloads.js +7 -1
  324. package/dist/slices/revisions/image-plan.js +183 -0
  325. package/dist/slices/revisions/mutation-assets.js +10 -0
  326. package/dist/slices/revisions/mutation-cues.js +71 -0
  327. package/dist/slices/revisions/mutation-request.js +2 -1
  328. package/dist/slices/revisions/mutations.js +97 -3
  329. package/dist/slices/revisions/schema.js +11 -1
  330. package/dist/slices/run-cost/limits.js +128 -0
  331. package/dist/slices/run-cost/meter.js +47 -0
  332. package/dist/slices/run-cost/panel.js +234 -0
  333. package/dist/slices/run-cost/pricing.js +92 -0
  334. package/dist/slices/run-cost/week.js +108 -0
  335. package/dist/slices/schedules/agenda.js +158 -0
  336. package/dist/slices/schedules/repo.js +58 -18
  337. package/dist/slices/schedules/scheduler.js +67 -16
  338. package/dist/slices/schedules/schema.js +138 -1
  339. package/dist/slices/schedules/service.js +43 -4
  340. package/dist/slices/schedules/similar.js +90 -0
  341. package/dist/slices/schedules/topic-list.js +163 -0
  342. package/dist/slices/schedules/topics.js +422 -0
  343. package/dist/slices/settings/first-run.js +102 -0
  344. package/dist/slices/settings/health.js +190 -0
  345. package/dist/slices/settings/key-guides.js +128 -0
  346. package/dist/slices/settings/key-test.js +85 -0
  347. package/dist/slices/settings/model.js +6 -1
  348. package/dist/slices/settings/playback.js +15 -0
  349. package/dist/slices/settings/repo.js +17 -3
  350. package/dist/slices/settings/voices.js +64 -4
  351. package/dist/slices/settings/whats-new.js +64 -0
  352. package/dist/slices/shorts/captions.js +9 -0
  353. package/dist/slices/shorts/pick.js +3 -3
  354. package/dist/slices/shorts/prompts.js +1 -1
  355. package/dist/slices/shorts/render.js +54 -5
  356. package/dist/slices/storage/asset-name.js +3 -0
  357. package/dist/slices/storage/backup-export.js +48 -9
  358. package/dist/slices/storage/backup-format.js +38 -1
  359. package/dist/slices/storage/backup-import.js +191 -19
  360. package/dist/slices/storage/delete-project.js +7 -4
  361. package/dist/slices/storage/documents.js +79 -0
  362. package/dist/slices/storage/files-location.js +531 -0
  363. package/dist/slices/storage/layout.js +14 -4
  364. package/dist/slices/storage/model.js +11 -0
  365. package/dist/slices/storage/portable.js +92 -12
  366. package/dist/slices/storage/reconcile.js +14 -1
  367. package/dist/slices/storage/repo.js +4 -1
  368. package/dist/slices/storage/schema.js +11 -0
  369. package/dist/slices/storage/trim.js +146 -0
  370. package/dist/slices/studio/disclosure.js +74 -0
  371. package/dist/slices/studio/model.js +28 -0
  372. package/dist/slices/studio/pack.js +209 -0
  373. package/dist/slices/studio/settings.js +133 -0
  374. package/dist/slices/style-preview/images.js +61 -0
  375. package/dist/slices/style-preview/render.js +161 -0
  376. package/dist/slices/style-preview/schema.js +45 -0
  377. package/dist/slices/style-preview/service.js +93 -0
  378. package/dist/slices/style-preview/settings.js +89 -0
  379. package/dist/slices/subtitles/captions.js +42 -9
  380. package/dist/slices/subtitles/model.js +22 -0
  381. package/dist/slices/subtitles/prepare.js +27 -16
  382. package/dist/slices/subtitles/transcript.js +1 -1
  383. package/dist/slices/thumbnail/run.js +7 -7
  384. package/dist/slices/trash/model.js +25 -0
  385. package/dist/slices/trash/service.js +273 -0
  386. package/dist/slices/uploads/repo.js +20 -0
  387. package/dist/slices/video/ambient-bed-schema.js +15 -0
  388. package/dist/slices/video/ambient-bed.js +95 -0
  389. package/dist/slices/video/ambient-mix.js +72 -0
  390. package/dist/slices/video/audio-inputs.js +2 -2
  391. package/dist/slices/video/cards.js +5 -2
  392. package/dist/slices/video/edit-list.js +23 -2
  393. package/dist/slices/video/edit-settings.js +13 -3
  394. package/dist/slices/video/ffmpeg.js +63 -9
  395. package/dist/slices/video/figure-card.js +492 -0
  396. package/dist/slices/video/figure-spans.js +51 -0
  397. package/dist/slices/video/plan.js +125 -13
  398. package/dist/slices/video/run.js +14 -12
  399. package/dist/slices/video/slideshow.js +39 -4
  400. package/dist/slices/voices/attribution.js +69 -0
  401. package/dist/slices/voices/audio-files.js +82 -0
  402. package/dist/slices/voices/audition.js +25 -0
  403. package/dist/slices/voices/cast.js +56 -0
  404. package/dist/slices/voices/grouping.js +49 -0
  405. package/dist/slices/voices/join.js +35 -0
  406. package/dist/slices/voices/languages.js +25 -0
  407. package/dist/slices/voices/model.js +179 -0
  408. package/dist/slices/voices/palette.js +26 -0
  409. package/dist/slices/voices/panel.js +91 -0
  410. package/dist/slices/voices/portraits.js +55 -0
  411. package/dist/slices/voices/script.js +136 -0
  412. package/dist/slices/voices/timing.js +58 -0
  413. package/dist/slices/youtube/answer.js +1 -1
  414. package/dist/slices/youtube/chapters.js +126 -0
  415. package/dist/slices/youtube/edits-repo.js +69 -0
  416. package/dist/slices/youtube/edits.js +89 -0
  417. package/dist/slices/youtube/placeholders.js +98 -0
  418. package/dist/slices/youtube/transcript.js +11 -4
  419. package/dist/updater/service.js +66 -21
  420. package/dist/web/assets/index-CUA2sUxU.css +1 -0
  421. package/dist/web/assets/index-CX590kE-.js +303 -0
  422. package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  423. package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  424. package/dist/web/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  425. package/dist/web/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  426. package/dist/web/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  427. package/dist/web/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  428. package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  429. package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  430. package/dist/web/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  431. package/dist/web/assets/{pdf-BLF2i8c2.js → pdf-BG8n_uc5.js} +1 -1
  432. package/dist/web/index.html +2 -2
  433. package/package.json +4 -4
  434. package/dist/edge/docker-launch.js +0 -81
  435. package/dist/edge/docker-projects/activation.js +0 -35
  436. package/dist/edge/docker-projects/claims.js +0 -85
  437. package/dist/edge/docker-projects/committed.js +0 -40
  438. package/dist/edge/docker-projects/engine.js +0 -461
  439. package/dist/edge/docker-projects/install.js +0 -254
  440. package/dist/edge/docker-projects/prune.js +0 -66
  441. package/dist/edge/docker-projects/recover.js +0 -100
  442. package/dist/edge/docker-projects/state.js +0 -225
  443. package/dist/web/assets/index-CktpxlJd.js +0 -171
  444. package/dist/web/assets/index-DB-vOnGU.css +0 -1
  445. package/scripts/docker-run.sh +0 -3
  446. /package/dist/edge/{docker-projects → docker-install}/tree.js +0 -0
@@ -0,0 +1,90 @@
1
+ import { castMentions } from "../channels/cast-match.js";
2
+ import { castOfChannel, resolveChannelId } from "../channels/repo.js";
3
+ import { normaliseTopic, topicWords } from "../schedules/similar.js";
4
+ import { episodeMemoryOn, memoriesOfChannel, } from "./repo.js";
5
+ // At most this many earlier episodes go into a new episode's prompt.
6
+ export const earlierEpisodesMax = 5;
7
+ // A shared cast member counts for more than any number of shared title words: the same
8
+ // character is the strongest sign two episodes must agree.
9
+ const castWeight = 100;
10
+ // The earlier episodes of the same channel related to a new one: they share a cast member the
11
+ // new title or keyword values mention, or words of their titles. Most related first, the
12
+ // newest first among equals. An episode with the same title as the new one is a remake of it,
13
+ // not an earlier episode, and is left out.
14
+ export function relatedEpisodes(memories, cast, subject, title) {
15
+ const mentioned = new Set(castMentions(subject, cast).map((member) => member.name.toLowerCase()));
16
+ const titles = memories.map((memory) => titleWords(memory.title));
17
+ // A word most of the channel's titles share is the template's ("D&D Lore: …"), not the
18
+ // topic's, and relates nothing.
19
+ const seen = new Map();
20
+ for (const set of titles)
21
+ for (const word of set)
22
+ seen.set(word, (seen.get(word) ?? 0) + 1);
23
+ const common = (word) => {
24
+ const count = seen.get(word) ?? 0;
25
+ return count >= 2 && count / memories.length > 0.5;
26
+ };
27
+ const words = titleWords(subject);
28
+ const same = normaliseTopic(title);
29
+ return memories
30
+ .flatMap((memory, order) => {
31
+ if (same !== "" && normaliseTopic(memory.title) === same)
32
+ return [];
33
+ const sharedCast = memory.cast.filter((name) => mentioned.has(name.toLowerCase())).length;
34
+ let sharedWords = 0;
35
+ for (const word of titles[order] ?? [])
36
+ if (words.has(word) && !common(word))
37
+ sharedWords += 1;
38
+ const score = sharedCast * castWeight + sharedWords;
39
+ return score === 0 ? [] : [{ memory, score, order }];
40
+ })
41
+ .sort((a, b) => b.score - a.score || a.order - b.order)
42
+ .slice(0, earlierEpisodesMax)
43
+ .map((row) => row.memory);
44
+ }
45
+ // A title's words as the topic checks compare them, less the one- and two-letter ones ("D&D"
46
+ // is "d and d"), which relate nothing.
47
+ function titleWords(text) {
48
+ return new Set([...topicWords(text)].filter((word) => word.length > 2 || /\d/.test(word)));
49
+ }
50
+ // The earlier episodes a new run carries, copied into its config when it starts so a later
51
+ // edit or a new memory never changes a project already made. Only for a run that writes its
52
+ // article (or script) on a channel with episode memory on; undefined otherwise, so the config
53
+ // is exactly what it was before episode memory.
54
+ export function earlierEpisodesFor(db, draft, title) {
55
+ if (draft.sources.article !== "generate")
56
+ return undefined;
57
+ const channelId = resolveChannelId(db, draft.channelId);
58
+ if (!episodeMemoryOn(db, channelId))
59
+ return undefined;
60
+ const memories = memoriesOfChannel(db, channelId);
61
+ if (memories.length === 0)
62
+ return undefined;
63
+ const cast = castOfChannel(db, channelId).map((member) => ({
64
+ name: member.name,
65
+ aliases: member.aliases,
66
+ description: member.description,
67
+ images: [],
68
+ }));
69
+ const subject = [title, ...Object.values(draft.values)].join("\n");
70
+ const related = relatedEpisodes(memories, cast, subject, title);
71
+ return related.length === 0
72
+ ? undefined
73
+ : related.map((memory) => ({ title: memory.title, summary: memory.summary }));
74
+ }
75
+ // The block appended to the article (or script) prompt. The prompt comes back unchanged when
76
+ // the run carries no earlier episodes, so every project made before episode memory, or with it
77
+ // off, sends exactly what it always sent.
78
+ export function withEarlierEpisodes(prompt, episodes) {
79
+ if (episodes === undefined || episodes.length === 0)
80
+ return prompt;
81
+ return [
82
+ prompt,
83
+ "",
84
+ "Earlier episodes",
85
+ "",
86
+ "This channel has already made these related episodes. Stay consistent with them and never contradict them; refer back to one where it fits naturally, but don't retell it.",
87
+ "",
88
+ ...episodes.map((episode) => `- "${episode.title}": ${episode.summary.replace(/\s+/g, " ")}`),
89
+ ].join("\n");
90
+ }
@@ -0,0 +1,60 @@
1
+ import { z } from "zod";
2
+ const memoryRow = z.object({
3
+ id: z.string(),
4
+ channel_id: z.string(),
5
+ project_id: z.string(),
6
+ title: z.string(),
7
+ summary: z.string(),
8
+ cast_json: z.string(),
9
+ source: z.enum(["generated", "edited"]),
10
+ recipe: z.string().nullable(),
11
+ created_at: z.string(),
12
+ updated_at: z.string(),
13
+ });
14
+ function memoryOf(row) {
15
+ const value = memoryRow.parse(row);
16
+ return {
17
+ id: value.id,
18
+ channelId: value.channel_id,
19
+ projectId: value.project_id,
20
+ title: value.title,
21
+ summary: value.summary,
22
+ cast: z.array(z.string()).parse(JSON.parse(value.cast_json)),
23
+ source: value.source,
24
+ recipe: value.recipe,
25
+ createdAt: value.created_at,
26
+ updatedAt: value.updated_at,
27
+ };
28
+ }
29
+ function withoutRecipe(memory) {
30
+ const { recipe: _recipe, ...rest } = memory;
31
+ return rest;
32
+ }
33
+ // Newest first.
34
+ export function memoriesOfChannel(db, channelId) {
35
+ return db
36
+ .prepare("SELECT * FROM episode_memories WHERE channel_id=? ORDER BY created_at DESC, rowid DESC")
37
+ .all(channelId)
38
+ .map((row) => withoutRecipe(memoryOf(row)));
39
+ }
40
+ export function memoryById(db, id) {
41
+ const row = db.prepare("SELECT * FROM episode_memories WHERE id=?").get(id);
42
+ return row === undefined ? undefined : withoutRecipe(memoryOf(row));
43
+ }
44
+ export function memoryOfProject(db, projectId) {
45
+ const row = db.prepare("SELECT * FROM episode_memories WHERE project_id=?").get(projectId);
46
+ return row === undefined ? undefined : memoryOf(row);
47
+ }
48
+ // A project finished again replaces its generated summary in place, keeping its id and when
49
+ // it was first made.
50
+ export function saveGeneratedMemory(db, memory, at) {
51
+ db.prepare(`INSERT INTO episode_memories(id,channel_id,project_id,title,summary,cast_json,source,recipe,created_at,updated_at)
52
+ VALUES (?,?,?,?,?,?,'generated',?,?,?)
53
+ ON CONFLICT(project_id) DO UPDATE SET channel_id=excluded.channel_id,title=excluded.title,
54
+ summary=excluded.summary,cast_json=excluded.cast_json,source='generated',
55
+ recipe=excluded.recipe,updated_at=excluded.updated_at`).run(memory.id, memory.channelId, memory.projectId, memory.title, memory.summary, JSON.stringify(memory.cast), memory.recipe, at, at);
56
+ }
57
+ export function episodeMemoryOn(db, channelId) {
58
+ const row = db.prepare("SELECT episode_memory FROM channels WHERE id=?").get(channelId);
59
+ return row !== undefined && Number(row.episode_memory) === 1;
60
+ }
@@ -0,0 +1,58 @@
1
+ import { z } from "zod";
2
+ import { channelById } from "../channels/repo.js";
3
+ import { episodeMemoryOn, memoriesOfChannel, memoryById } from "./repo.js";
4
+ import { summaryWordsMax } from "./summarize.js";
5
+ const settingSchema = z.object({ enabled: z.boolean() }).strict();
6
+ // Room for a hand-written summary a little longer than a generated one.
7
+ export const summaryCharsMax = 4000;
8
+ const editSchema = z
9
+ .object({
10
+ summary: z
11
+ .string()
12
+ .trim()
13
+ .min(1, "Write the summary, or delete the memory instead.")
14
+ .max(summaryCharsMax, `Keep the summary to ${summaryCharsMax.toLocaleString("en")} characters or fewer; about ${String(summaryWordsMax)} words is plenty.`),
15
+ })
16
+ .strict();
17
+ export function channelEpisodes(deps, channelId) {
18
+ if (channelById(deps.db, channelId) === undefined)
19
+ return { ok: false, reason: "not-found" };
20
+ return {
21
+ ok: true,
22
+ value: {
23
+ enabled: episodeMemoryOn(deps.db, channelId),
24
+ memories: memoriesOfChannel(deps.db, channelId),
25
+ },
26
+ };
27
+ }
28
+ export function setEpisodeMemory(deps, channelId, input) {
29
+ const parsed = settingSchema.safeParse(input);
30
+ if (!parsed.success)
31
+ return { ok: false, reason: "invalid-input" };
32
+ const changed = deps.db
33
+ .prepare("UPDATE channels SET episode_memory=? WHERE id=?")
34
+ .run(parsed.data.enabled ? 1 : 0, channelId);
35
+ if (Number(changed.changes) === 0)
36
+ return { ok: false, reason: "not-found" };
37
+ return channelEpisodes(deps, channelId);
38
+ }
39
+ export function editMemory(deps, channelId, memoryId, input) {
40
+ const parsed = editSchema.safeParse(input);
41
+ if (!parsed.success)
42
+ return { ok: false, reason: "invalid-input", message: parsed.error.issues[0]?.message };
43
+ const changed = deps.db
44
+ .prepare("UPDATE episode_memories SET summary=?,source='edited',updated_at=? WHERE id=? AND channel_id=?")
45
+ .run(parsed.data.summary, deps.clock.now().toISOString(), memoryId, channelId);
46
+ const saved = memoryById(deps.db, memoryId);
47
+ if (Number(changed.changes) === 0 || saved === undefined)
48
+ return { ok: false, reason: "not-found" };
49
+ return { ok: true, value: saved };
50
+ }
51
+ export function deleteMemory(deps, channelId, memoryId) {
52
+ const changed = deps.db
53
+ .prepare("DELETE FROM episode_memories WHERE id=? AND channel_id=?")
54
+ .run(memoryId, channelId);
55
+ return Number(changed.changes) === 0
56
+ ? { ok: false, reason: "not-found" }
57
+ : { ok: true, value: { deleted: true } };
58
+ }
@@ -0,0 +1,170 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { projectById } from "../admission/repo.js";
4
+ import { castMentions } from "../channels/cast-match.js";
5
+ import { castOfChannel, projectChannelId } from "../channels/repo.js";
6
+ import { outputPath } from "../storage/layout.js";
7
+ import { outputsOf } from "../storage/repo.js";
8
+ import { episodeMemoryOn, memoryOfProject, saveGeneratedMemory } from "./repo.js";
9
+ // When a project finishes, a small LLM call writes what the episode covered (at most
10
+ // `summaryWordsMax` words) for its channel's episode memory. It never fails the project: every
11
+ // failure is logged and the project stays done.
12
+ export const summaryWordsMax = 150;
13
+ // Part of the cached recipe: raising it makes every finished article summarised afresh.
14
+ const promptVersion = 1;
15
+ // Only the start of a long article is sent; the summary names what it covers, not every detail.
16
+ const articleCharsMax = 24_000;
17
+ const timeoutMs = 3 * 60_000;
18
+ export function summaryMessages(title, article) {
19
+ return [
20
+ {
21
+ role: "system",
22
+ content: `You keep a YouTube channel's episode log. Answer with the summary alone: plain prose, at most ${String(summaryWordsMax)} words, no heading, no list.`,
23
+ },
24
+ {
25
+ role: "user",
26
+ content: [
27
+ `Summarise the episode "${title}" for the writers of later episodes: what it covered, the key facts and claims it made, the people, characters and places it featured and what happened to them, and how it ended. Later episodes read this so they don't contradict it.`,
28
+ "",
29
+ "The episode's text:",
30
+ "",
31
+ article.length > articleCharsMax ? `${article.slice(0, articleCharsMax)}…` : article,
32
+ ].join("\n"),
33
+ },
34
+ ];
35
+ }
36
+ // Trims a summary that ran long to its first `summaryWordsMax` words.
37
+ export function clampSummary(text) {
38
+ const words = text.replace(/\s+/g, " ").trim().split(" ").filter(Boolean);
39
+ return words.length <= summaryWordsMax
40
+ ? words.join(" ")
41
+ : `${words.slice(0, summaryWordsMax).join(" ")}…`;
42
+ }
43
+ export async function summarizeEpisode(deps, projectId, signal) {
44
+ try {
45
+ return await summarize(deps, projectId, signal);
46
+ }
47
+ catch (error) {
48
+ // Slopify is closing; the next finish of this project writes it.
49
+ if (signal?.aborted === true)
50
+ return "failed";
51
+ deps.log.write("warn", "episode.memory", {
52
+ projectId,
53
+ detail: `The episode summary wasn't saved: ${error instanceof Error ? error.message : String(error)}. The project is unaffected; only later episodes won't be reminded of this one. Check the project's text model in Settings → Providers.`,
54
+ });
55
+ return "failed";
56
+ }
57
+ }
58
+ async function summarize(deps, projectId, signal) {
59
+ const project = projectById(deps.db, projectId);
60
+ if (project === undefined)
61
+ return "gone";
62
+ const channelId = projectChannelId(deps.db, projectId);
63
+ if (!episodeMemoryOn(deps.db, channelId))
64
+ return "off";
65
+ const existing = memoryOfProject(deps.db, projectId);
66
+ if (existing?.source === "edited")
67
+ return "edited";
68
+ const article = articleText(deps, projectId);
69
+ if (article === undefined)
70
+ return "no-article";
71
+ const recipe = createHash("sha256")
72
+ .update(JSON.stringify([promptVersion, project.title, article]))
73
+ .digest("hex");
74
+ if (existing?.recipe === recipe)
75
+ return "unchanged";
76
+ const choice = llmOf(project.config);
77
+ if (choice === undefined) {
78
+ deps.log.write("info", "episode.memory", {
79
+ projectId,
80
+ detail: "No episode summary: this project has no text model chosen, so later episodes won't be reminded of it.",
81
+ });
82
+ return "no-llm";
83
+ }
84
+ const answer = await deps.llm({
85
+ owner: { kind: "channel", id: channelId },
86
+ purpose: "episode-summary",
87
+ provider: choice.provider,
88
+ model: choice.model,
89
+ messages: summaryMessages(project.title, article),
90
+ signal: AbortSignal.any([
91
+ AbortSignal.timeout(timeoutMs),
92
+ ...(signal === undefined ? [] : [signal]),
93
+ ]),
94
+ });
95
+ const summary = clampSummary(answer.text);
96
+ if (summary === "")
97
+ throw new Error(`${choice.provider} (${choice.model}) answered with nothing`);
98
+ // Read again: the project may have been deleted, or the summary edited, while the LLM wrote.
99
+ if (projectById(deps.db, projectId) === undefined)
100
+ return "gone";
101
+ if (memoryOfProject(deps.db, projectId)?.source === "edited")
102
+ return "edited";
103
+ saveGeneratedMemory(deps.db, {
104
+ id: existing?.id ?? deps.uuid(),
105
+ channelId,
106
+ projectId,
107
+ title: project.title,
108
+ summary,
109
+ cast: castMentions(`${project.title}\n${article}`, castOf(deps.db, project.config, channelId)).map((member) => member.name),
110
+ recipe,
111
+ }, deps.clock.now().toISOString());
112
+ return "saved";
113
+ }
114
+ // The project's own text model; failing that, the one its automatic reviews use.
115
+ function llmOf(config) {
116
+ if (config.llm !== undefined)
117
+ return config.llm;
118
+ return config.reviews === undefined
119
+ ? undefined
120
+ : { provider: config.reviews.provider, model: config.reviews.model };
121
+ }
122
+ // The cast the run was started with, else the channel's cast as it is now.
123
+ function castOf(db, config, channelId) {
124
+ if (config.cast !== undefined && config.cast.length > 0)
125
+ return config.cast;
126
+ return castOfChannel(db, channelId).map((member) => ({
127
+ name: member.name,
128
+ aliases: member.aliases,
129
+ description: member.description,
130
+ images: [],
131
+ }));
132
+ }
133
+ function articleText(deps, projectId) {
134
+ const output = outputsOf(deps.db, projectId).find((row) => row.role === "article_txt");
135
+ if (output === undefined)
136
+ return undefined;
137
+ const path = outputPath(deps.paths, projectId, output.path);
138
+ if (!existsSync(path))
139
+ return undefined;
140
+ const text = readFileSync(path, "utf8").trim();
141
+ return text === "" ? undefined : text;
142
+ }
143
+ export function createEpisodeMemoryWatcher(deps) {
144
+ const inflight = new Map();
145
+ const stop = new AbortController();
146
+ return {
147
+ observe: (event) => {
148
+ if (stop.signal.aborted || event.type !== "project.state" || event.state !== "done")
149
+ return;
150
+ const projectId = event.projectId;
151
+ if (inflight.has(projectId))
152
+ return;
153
+ const running = (async () => {
154
+ // Off the emitter's call stack: it may be inside a transaction.
155
+ await Promise.resolve();
156
+ await summarizeEpisode(deps, projectId, stop.signal);
157
+ })().finally(() => {
158
+ inflight.delete(projectId);
159
+ });
160
+ inflight.set(projectId, running);
161
+ },
162
+ settled: async () => {
163
+ while (inflight.size > 0)
164
+ await Promise.all([...inflight.values()]);
165
+ },
166
+ close: () => {
167
+ stop.abort();
168
+ },
169
+ };
170
+ }
@@ -1,13 +1,20 @@
1
1
  import { z } from "zod";
2
- import { sourceOf } from "../admission/model.js";
3
- import { imageSecondsProblem, usesNarrationPreparation, usesReference, usesShorts, usesYoutubeDescription, } from "../admission/rules.js";
2
+ import { sourceOf, thumbnailCountOf } from "../admission/model.js";
3
+ import { imageSecondsProblem, usesDescribedNarration, usesNarrationPreparation, usesReference, usesShorts, usesYoutubeDescription, } from "../admission/rules.js";
4
4
  import { plainText } from "../article/plain.js";
5
5
  import { splitEndMatter } from "../article/split.js";
6
+ import { narrationMinutes, plannedImageCount, scalesImages } from "../images/scale.js";
7
+ import { describedBlocks, narrationBlocks } from "../narration/blocks.js";
6
8
  import { chunkNarration, defaultChunking } from "../narration/chunk.js";
9
+ import { describeMessages } from "../narration/describe.js";
7
10
  import { normalizeNarrationText } from "../narration/plan.js";
8
11
  import { preparationMessages } from "../narration/preparation.js";
12
+ import { defaultReviewPrompts, reviewPromptKey, reviewRetriesOf } from "../reviews/model.js";
13
+ import { activeReviewStages } from "../reviews/rules.js";
14
+ import { isLocalCliProvider } from "../settings/model.js";
9
15
  import { defaultShortsImagePrompt, defaultShortsPrompt, shortsImageUpperBound, shortsSettingsProblems, } from "../shorts/model.js";
10
16
  import { animatedClipSeconds, animatedImageIndexes, usesAnimation, videoEditProblems, } from "../video/edit-settings.js";
17
+ import { usesVoices } from "../voices/model.js";
11
18
  import { defaultDescriptionPrompt } from "../youtube/model.js";
12
19
  // ceiling: the most chapters "Chapter openers" is charged for before the article exists, the
13
20
  // top of the 5-12 chapters the built-in YouTube description prompt asks for.
@@ -28,19 +35,27 @@ export function estimateRun(draft, rendered, expectedWords, catalogue) {
28
35
  const llm = draft.llm ?? { provider: "", model: "" };
29
36
  const tts = draft.audio ?? { provider: "", model: "" };
30
37
  const image = draft.images ?? { provider: "", model: "" };
31
- const textNote = data.llm.find((model) => model.provider === llm.provider &&
32
- model.id === llm.model &&
33
- model.enabled &&
34
- !model.deprecated)?.pricing.note ??
35
- "CLI subscription or model pricing is unavailable; usage may be billed by your account.";
36
- const imageNote = data.image.find((model) => model.provider === image.provider &&
37
- model.id === image.model &&
38
- model.enabled &&
39
- !model.deprecated)?.pricing.note ?? "Model or account pricing is unknown.";
38
+ const textNote = isLocalCliProvider(llm.provider)
39
+ ? "Runs on your CLI plan, so it adds no charge; the API figure is what the same tokens would cost through the API."
40
+ : (data.llm.find((model) => model.provider === llm.provider &&
41
+ model.id === llm.model &&
42
+ model.enabled &&
43
+ !model.deprecated)?.pricing.note ??
44
+ "CLI subscription or model pricing is unavailable; usage may be billed by your account.");
45
+ const imageNote = isLocalCliProvider(image.provider)
46
+ ? "Runs on your Codex plan, so it adds no charge; no API price is given for agent-drawn images."
47
+ : (data.image.find((model) => model.provider === image.provider &&
48
+ model.id === image.model &&
49
+ model.enabled &&
50
+ !model.deprecated)?.pricing.note ?? "Model or account pricing is unknown.");
40
51
  const generatedArticle = draft.sources.article === "generate";
41
52
  const articleChars = generatedArticle ? expectedWords * 6 : (draft.provided.article?.length ?? 0);
42
53
  const promptChars = Object.entries(rendered).reduce((sum, [key, value]) => sum +
43
- (key === "narration" || key === "description" || key === "shorts" || key === "shortsImage"
54
+ (key === "narration" ||
55
+ key === "description" ||
56
+ key === "shorts" ||
57
+ key === "shortsImage" ||
58
+ key.startsWith("review.")
44
59
  ? 0
45
60
  : value.length), 0);
46
61
  const local = (stage, detail) => {
@@ -67,6 +82,25 @@ export function estimateRun(draft, rendered, expectedWords, catalogue) {
67
82
  local("Article", "Provided article; no generation charge.");
68
83
  if (draft.intro?.mode === "llm" || draft.outro?.mode === "llm")
69
84
  text("Intro / outro text", promptChars + articleChars, 2400);
85
+ if (usesDescribedNarration(draft)) {
86
+ // One call per table, figure, equation or code block: counted in a provided article,
87
+ // unknown until a generated one is written.
88
+ const blocks = generatedArticle
89
+ ? undefined
90
+ : describedBlocks(narrationBlocks(splitEndMatter(draft.provided.article ?? "").body, {
91
+ code: draft.audio?.skipCode === true ? "skip" : "describe",
92
+ language: draft.language,
93
+ }));
94
+ if (blocks === undefined)
95
+ requests.push({
96
+ kind: "unknown",
97
+ stage: "Narration descriptions",
98
+ detail: "One LLM call per table, figure, equation or code block in the article; how many is known once the article is written.",
99
+ });
100
+ else
101
+ for (const block of blocks)
102
+ text("Narration descriptions", describeMessages(block, rendered.narration ?? "", draft.language).reduce((n, message) => n + message.content.length, 0), 600, `${String(blocks.length)} LLM ${blocks.length === 1 ? "call" : "calls"}: one per table, figure, equation or code block.`);
103
+ }
70
104
  if (draft.sources.audio === "generate") {
71
105
  if (usesNarrationPreparation(draft)) {
72
106
  const known = generatedArticle
@@ -86,7 +120,7 @@ export function estimateRun(draft, rendered, expectedWords, catalogue) {
86
120
  });
87
121
  else
88
122
  for (const source of known)
89
- text("Narration Preparation", preparationMessages(rendered.narration ?? "", source).reduce((n, message) => n + message.content.length, 0), 2400, `${known.length} LLM calls: one per logical narration chunk or entry.`);
123
+ text("Narration Preparation", preparationMessages(rendered.narration ?? "", source, [], draft.language).reduce((n, message) => n + message.content.length, 0), 2400, `${known.length} LLM calls: one per logical narration chunk or entry.`);
90
124
  requests.push({
91
125
  kind: "unknown",
92
126
  stage: "Delivery cue overhead",
@@ -96,18 +130,38 @@ export function estimateRun(draft, rendered, expectedWords, catalogue) {
96
130
  const extras = ["intro", "outro"].reduce((n, key) => n + (rendered[key]?.length ?? 0), 0);
97
131
  const llmExtras = (draft.intro?.mode === "llm" ? 1200 : 0) + (draft.outro?.mode === "llm" ? 1200 : 0);
98
132
  const choice = { stage: "Narration", provider: tts.provider, model: tts.model };
99
- requests.push(generatedArticle || llmExtras > 0
100
- ? { ...choice, kind: "tts-estimate", characters: articleChars + extras + llmExtras }
101
- : {
102
- ...choice,
103
- kind: "tts",
104
- text: (draft.provided.article ?? "") + (rendered.intro ?? "") + (rendered.outro ?? ""),
105
- });
133
+ const speakers = usesVoices(draft) ? (draft.voices?.speakers ?? []) : [];
134
+ if (speakers.length > 0) {
135
+ // Each speaker's voice is priced on an equal share of the script; the intro and outro
136
+ // keep the narration voice.
137
+ for (const speaker of speakers)
138
+ requests.push({
139
+ kind: "tts-estimate",
140
+ stage: "Narration",
141
+ provider: speaker.voice.provider,
142
+ model: speaker.voice.model,
143
+ characters: Math.round(articleChars / speakers.length),
144
+ detail: `${speaker.name.trim() || "A speaker"}: assumes every speaker says about the same.`,
145
+ });
146
+ if (extras + llmExtras > 0)
147
+ requests.push({ ...choice, kind: "tts-estimate", characters: extras + llmExtras });
148
+ if (draft.voices?.source === "attribute")
149
+ text("Speaker split", articleChars + 1500, Math.round(articleChars * 1.1), "One LLM call hands the text's passages to the speakers.");
150
+ }
151
+ else
152
+ requests.push(generatedArticle || llmExtras > 0
153
+ ? { ...choice, kind: "tts-estimate", characters: articleChars + extras + llmExtras }
154
+ : {
155
+ ...choice,
156
+ kind: "tts",
157
+ text: (draft.provided.article ?? "") + (rendered.intro ?? "") + (rendered.outro ?? ""),
158
+ });
106
159
  }
107
160
  else
108
161
  local("Narration", "Provided or off; no generation charge.");
109
162
  const images = draft.sources.images === "generate"
110
- ? draft.imagePrompts.reduce((n, prompt) => n + prompt.number, 0)
163
+ ? // Each prompt's Number, or more for a long video (`images/scale.ts`).
164
+ plannedImageCount(draft)
111
165
  : 0;
112
166
  z.number().int().nonnegative().parse(images);
113
167
  for (let index = 0; index < images; index++)
@@ -116,7 +170,9 @@ export function estimateRun(draft, rendered, expectedWords, catalogue) {
116
170
  stage: "Images",
117
171
  provider: image.provider,
118
172
  model: image.model,
119
- detail: `${images} images. ${imageNote}`,
173
+ detail: scalesImages(draft)
174
+ ? `${images} images for about ${String(Math.round(narrationMinutes(draft.imageScale.words)))} minutes of narration (${draft.imageScale.words.toLocaleString()} words). ${imageNote}`
175
+ : `${images} images. ${imageNote}`,
120
176
  });
121
177
  if (images === 0)
122
178
  local("Images", "Provided or off.");
@@ -129,12 +185,13 @@ export function estimateRun(draft, rendered, expectedWords, catalogue) {
129
185
  model: image.model,
130
186
  });
131
187
  if (["from_prompt", "prompt_by_llm"].includes(draft.sources.thumbnail))
132
- requests.push({
133
- kind: "image",
134
- stage: "Thumbnail",
135
- provider: image.provider,
136
- model: image.model,
137
- });
188
+ for (let variant = 1; variant <= thumbnailCountOf(draft); variant++)
189
+ requests.push({
190
+ kind: "image",
191
+ stage: "Thumbnail",
192
+ provider: image.provider,
193
+ model: image.model,
194
+ });
138
195
  else
139
196
  local("Thumbnail", "Provided or off.");
140
197
  if (draft.sources.thumbnail === "prompt_by_llm")
@@ -195,6 +252,30 @@ export function estimateRun(draft, rendered, expectedWords, catalogue) {
195
252
  : `Up to ${String(clips)} clips of ${String(animatedClipSeconds)} seconds, one per chapter opening. ${note}`,
196
253
  });
197
254
  }
255
+ // Automatic reviews: one call per reviewed item, on the reviewer's model. A failed item
256
+ // made again is reviewed again, at most the retries allowed, which is not priced here.
257
+ const reviews = draft.reviews;
258
+ if (reviews !== undefined)
259
+ for (const stage of activeReviewStages(draft)) {
260
+ const picked = rendered[reviewPromptKey(stage)]?.trim() ?? "";
261
+ const prompt = picked === "" ? defaultReviewPrompts[stage].length : picked.length;
262
+ const items = stage === "images" ? images : stage === "shorts" ? (draft.shorts?.count ?? 0) : 1;
263
+ const material = stage === "article"
264
+ ? articleChars + promptChars
265
+ : stage === "narration"
266
+ ? Math.round(articleChars * 2.1)
267
+ : 1500;
268
+ for (let index = 0; index < items; index++)
269
+ requests.push({
270
+ kind: "llm",
271
+ stage: "Reviews",
272
+ provider: reviews.provider,
273
+ model: reviews.model,
274
+ inputCharacters: prompt + material + 800,
275
+ outputCharacters: 400,
276
+ detail: `One review per item. A failed item made again is reviewed again, up to ${String(reviewRetriesOf(reviews))} times; those remakes are not in this estimate. ${textNote}`,
277
+ });
278
+ }
198
279
  if (sourceOf(draft.sources, "document") === "generate")
199
280
  local("Document", "Laid out locally from the article; no API fee.");
200
281
  return {