@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,4 +1,6 @@
1
1
  import { z } from "zod";
2
+ import { apiEquivalentOf, tokenCost } from "../run-cost/pricing.js";
3
+ import { isLocalCliProvider, systemVoiceProvider } from "../settings/model.js";
2
4
  const quantity = z.number().finite().nonnegative();
3
5
  const common = z.object({
4
6
  stage: z.string(),
@@ -6,7 +8,12 @@ const common = z.object({
6
8
  variable: z.boolean().optional(),
7
9
  expectedWords: quantity.optional(),
8
10
  });
9
- const provider = common.extend({ provider: z.string(), model: z.string() });
11
+ const provider = common.extend({
12
+ provider: z.string(),
13
+ model: z.string(),
14
+ // A CLI request whose API figure cannot be told in advance.
15
+ apiUnknown: z.literal(true).optional(),
16
+ });
10
17
  const requestSchema = z.discriminatedUnion("kind", [
11
18
  provider
12
19
  .extend({ kind: z.literal("llm"), inputCharacters: quantity, outputCharacters: quantity })
@@ -26,6 +33,7 @@ export function estimateRequests(requests, catalogue) {
26
33
  low: rows.reduce((total, row) => total + (row.low ?? 0), 0),
27
34
  high: rows.reduce((total, row) => total + (row.high ?? 0), 0),
28
35
  unknown: rows.filter((row) => row.low === null).length,
36
+ ...apiTotals(rows),
29
37
  expectedWords: parsed.reduce((total, request) => total + (request.expectedWords ?? 0), 0),
30
38
  catalogueDate: catalogue.updatedAt,
31
39
  assumptions: [
@@ -39,16 +47,64 @@ export function groupEstimateRows(estimate) {
39
47
  const grouped = new Map();
40
48
  for (const row of estimate.rows) {
41
49
  const previous = grouped.get(row.stage);
50
+ const plan = previous?.onPlan === true || row.onPlan === true;
51
+ const sum = (left, right) => (left === null || right === null ? null : (left ?? 0) + (right ?? 0));
42
52
  grouped.set(row.stage, previous === undefined
43
53
  ? row
44
54
  : {
45
55
  ...previous,
46
56
  low: previous.low === null || row.low === null ? null : previous.low + row.low,
47
57
  high: previous.high === null || row.high === null ? null : previous.high + row.high,
58
+ ...(plan
59
+ ? {
60
+ onPlan: true,
61
+ apiLow: sum(previous.apiLow, row.apiLow),
62
+ apiHigh: sum(previous.apiHigh, row.apiHigh),
63
+ }
64
+ : {}),
48
65
  });
49
66
  }
50
67
  const rows = [...grouped.values()];
51
- return { ...estimate, rows, unknown: rows.filter((row) => row.low === null).length };
68
+ return {
69
+ ...estimate,
70
+ rows,
71
+ unknown: rows.filter((row) => row.low === null).length,
72
+ ...apiTotals(rows),
73
+ };
74
+ }
75
+ function apiTotals(rows) {
76
+ const plan = rows.filter((row) => row.onPlan === true);
77
+ if (plan.length === 0)
78
+ return {};
79
+ return {
80
+ apiLow: plan.reduce((total, row) => total + (row.apiLow ?? 0), 0),
81
+ apiHigh: plan.reduce((total, row) => total + (row.apiHigh ?? 0), 0),
82
+ apiUnknown: plan.filter((row) => row.apiLow === null || row.apiLow === undefined).length,
83
+ };
84
+ }
85
+ // A CLI call adds nothing to the bill - it runs on the user's plan - so it is known at $0,
86
+ // with what the same tokens would cost through the API beside it, priced by the same
87
+ // catalogue and the same token assumptions as a keyed model. A Codex image has no API
88
+ // figure: how many drafts the agent draws is not known in advance.
89
+ function onPlan(request, catalogue) {
90
+ const api = request.kind === "llm"
91
+ ? apiEquivalentOf(request.provider, request.model, catalogue)
92
+ : undefined;
93
+ const amount = api === undefined || request.kind !== "llm" || request.apiUnknown === true
94
+ ? null
95
+ : tokenCost(api.pricing, request.inputCharacters / 4, request.outputCharacters / 4);
96
+ return {
97
+ stage: request.stage,
98
+ low: 0,
99
+ high: 0,
100
+ detail: request.detail ??
101
+ (amount === null
102
+ ? "Runs on your CLI plan, so it adds no charge; no API price is listed for this model."
103
+ : `Runs on your CLI plan, so it adds no charge; priced as ${api?.name ?? "the API model"} through the API.`),
104
+ onPlan: true,
105
+ apiLow: amount === null ? null : amount * 0.5,
106
+ apiHigh: amount === null ? null : amount * 1.5,
107
+ };
52
108
  }
53
109
  function price(request, catalogue) {
54
110
  if (request.kind === "unknown")
@@ -65,6 +121,16 @@ function price(request, catalogue) {
65
121
  high: 0,
66
122
  detail: request.detail ?? "Local processing or retained output; no API fee.",
67
123
  };
124
+ if ((request.kind === "llm" || request.kind === "image") && isLocalCliProvider(request.provider))
125
+ return onPlan(request, catalogue);
126
+ if ((request.kind === "tts" || request.kind === "tts-estimate") &&
127
+ request.provider === systemVoiceProvider)
128
+ return {
129
+ stage: request.stage,
130
+ low: 0,
131
+ high: 0,
132
+ detail: "Spoken by your computer's built-in voice, so it costs nothing.",
133
+ };
68
134
  const family = request.kind === "tts-estimate" ? "tts" : request.kind;
69
135
  const model = catalogue[family].find((entry) => entry.provider === request.provider &&
70
136
  entry.id === request.model &&
@@ -0,0 +1,56 @@
1
+ import { modelKeyOf } from "./model.js";
2
+ // How long steps took in finished runs, read from the stage rows' own start and finish times.
3
+ // ceiling: the most recent finished steps are what the machine and the providers are like now;
4
+ // a few hundred rows is a fraction of a millisecond to read and plenty for a median.
5
+ const sampleLimit = 400;
6
+ // A model's own history is used once it has this many finished steps; below that, every
7
+ // step of the same kind is a fairer guess than one lucky or unlucky run.
8
+ const ownModelMinimum = 2;
9
+ export function readStageHistory(db) {
10
+ const samples = db
11
+ .prepare(`SELECT s.kind, s.started_at, s.finished_at, s.progress_total, p.config FROM stages s
12
+ JOIN projects p ON p.id=s.project_id
13
+ WHERE s.state='done' AND s.started_at IS NOT NULL AND s.finished_at IS NOT NULL
14
+ ORDER BY s.finished_at DESC LIMIT ?`)
15
+ .all(sampleLimit)
16
+ .flatMap((row) => {
17
+ const seconds = (Date.parse(String(row.finished_at)) - Date.parse(String(row.started_at))) / 1000;
18
+ if (!Number.isFinite(seconds) || seconds <= 0)
19
+ return [];
20
+ const kind = String(row.kind);
21
+ const units = typeof row.progress_total === "number" && row.progress_total > 0
22
+ ? row.progress_total
23
+ : undefined;
24
+ return [{ kind, model: modelKeyOf(kind, configOf(row.config)), seconds, units }];
25
+ });
26
+ return {
27
+ typicalSeconds: (kind, config, units) => {
28
+ const model = modelKeyOf(kind, config);
29
+ const own = samples.filter((one) => one.kind === kind && one.model === model);
30
+ const pool = own.length >= ownModelMinimum ? own : samples.filter((one) => one.kind === kind);
31
+ if (pool.length === 0)
32
+ return undefined;
33
+ // A step with a count scales with it: eight images take longer than two.
34
+ const counted = pool.filter((one) => one.units !== undefined);
35
+ if (units !== null && units > 0 && counted.length > 0)
36
+ return median(counted.map((one) => one.seconds / (one.units ?? 1))) * units;
37
+ return median(pool.map((one) => one.seconds));
38
+ },
39
+ };
40
+ }
41
+ function configOf(value) {
42
+ try {
43
+ const parsed = JSON.parse(String(value));
44
+ return typeof parsed === "object" && parsed !== null ? parsed : {};
45
+ }
46
+ catch {
47
+ return {};
48
+ }
49
+ }
50
+ export function median(values) {
51
+ const sorted = values.toSorted((left, right) => left - right);
52
+ const middle = Math.floor(sorted.length / 2);
53
+ return sorted.length % 2 === 1
54
+ ? (sorted[middle] ?? 0)
55
+ : ((sorted[middle - 1] ?? 0) + (sorted[middle] ?? 0)) / 2;
56
+ }
@@ -0,0 +1,63 @@
1
+ // Time left on a running step, said honestly. Two bases, in this order:
2
+ // - progress: the step has counted some of its chapters, chunks or images, so the rest is
3
+ // estimated from the rate so far;
4
+ // - history: nothing counted yet (a CLI image job drawing its first picture for minutes), so the
5
+ // estimate is how long the same kind of step, on the same provider and model when known,
6
+ // took in finished runs (`history.ts`), minus the time spent already.
7
+ // Neither: "unknown", never a made-up number. A step past its usual time is "overdue".
8
+ // Browser-safe: the project page and Home recompute it every second from the same inputs.
9
+ export const etaBases = ["progress", "history", "overdue", "unknown"];
10
+ // Undefined for a step that isn't running: a finished one has no time left, and a waiting one
11
+ // hasn't started the clock.
12
+ export function stageEta(stage, now) {
13
+ if (stage.state !== "running")
14
+ return undefined;
15
+ const started = stage.startedAt === null ? Number.NaN : Date.parse(stage.startedAt);
16
+ const spent = Number.isFinite(started) ? Math.max(0, (now - started) / 1000) : undefined;
17
+ const current = stage.progressCurrent ?? 0;
18
+ const total = stage.progressTotal ?? 0;
19
+ if (spent !== undefined && total > 0 && current > 0) {
20
+ if (current >= total)
21
+ return { basis: "progress", seconds: 0 };
22
+ return { basis: "progress", seconds: Math.round((spent / current) * (total - current)) };
23
+ }
24
+ const typical = stage.typicalSeconds;
25
+ if (typical === undefined || !Number.isFinite(typical) || typical <= 0)
26
+ return { basis: "unknown" };
27
+ const left = typical - (spent ?? 0);
28
+ return left > 0 ? { basis: "history", seconds: Math.round(left) } : { basis: "overdue" };
29
+ }
30
+ // "about 4 min left", "under a minute left", "about 1 h 10 min left", "finishing up",
31
+ // "taking longer than usual", "time left unknown".
32
+ export function etaLabel(eta) {
33
+ if (eta.basis === "unknown")
34
+ return "time left unknown";
35
+ if (eta.basis === "overdue")
36
+ return "taking longer than usual";
37
+ const seconds = eta.seconds ?? 0;
38
+ if (seconds <= 0)
39
+ return "finishing up";
40
+ if (seconds < 60)
41
+ return "under a minute left";
42
+ const minutes = Math.round(seconds / 60);
43
+ if (minutes < 60)
44
+ return `about ${String(minutes)} min left`;
45
+ const hours = Math.floor(minutes / 60);
46
+ const rest = minutes % 60;
47
+ return rest === 0
48
+ ? `about ${String(hours)} h left`
49
+ : `about ${String(hours)} h ${String(rest)} min left`;
50
+ }
51
+ export function modelKeyOf(kind, config) {
52
+ const choice = kind === "research" || kind === "article"
53
+ ? config.llm
54
+ : kind === "audio"
55
+ ? config.audio
56
+ : kind === "images" || kind === "thumbnail"
57
+ ? config.images
58
+ : undefined;
59
+ // A hand-edited config may hold anything here.
60
+ if (typeof choice?.provider !== "string" || typeof choice.model !== "string")
61
+ return undefined;
62
+ return `${choice.provider}/${choice.model}`;
63
+ }
@@ -0,0 +1,25 @@
1
+ import { readStageHistory } from "./history.js";
2
+ import { stageEta } from "./model.js";
3
+ // The project's stage rows with each running step's time left. The history is read only when
4
+ // something is running, so a finished project's page costs no extra query.
5
+ export function stagesWithEta(db, stages, config, now) {
6
+ if (!stages.some((stage) => stage.state === "running"))
7
+ return [...stages];
8
+ const history = readStageHistory(db);
9
+ return stages.map((stage) => {
10
+ if (stage.state !== "running")
11
+ return stage;
12
+ const typical = history.typicalSeconds(stage.kind, config, stage.progressTotal);
13
+ const eta = stageEta({ ...stage, ...(typical === undefined ? {} : { typicalSeconds: typical }) }, now.valueOf());
14
+ return {
15
+ ...stage,
16
+ ...(typical === undefined ? {} : { typicalSeconds: Math.round(typical) }),
17
+ ...(eta === undefined
18
+ ? {}
19
+ : {
20
+ etaBasis: eta.basis,
21
+ ...(eta.seconds === undefined ? {} : { etaSeconds: eta.seconds }),
22
+ }),
23
+ };
24
+ });
25
+ }
@@ -0,0 +1,73 @@
1
+ import { providers } from "../settings/model.js";
2
+ const signIn = {
3
+ "claude-code": { name: "Claude Code", command: "claude auth login" },
4
+ codex: { name: "Codex", command: "codex login" },
5
+ gemini: { name: "Gemini", command: "gemini" },
6
+ };
7
+ const signedOut = /not signed in|sign-in has expired|not logged in|run\s+"?(?:codex login|claude auth login)|authentication required/i;
8
+ const diskFull = /ENOSPC|no space left on device|disk (?:is )?full|not enough (?:free )?(?:disk )?space/i;
9
+ const refusedWords = /refused to make|content (?:rules|policy|filter)|safety (?:system|filter)|flagged|moderation/i;
10
+ const retiredModel = /\bunknown model\b|\bmodel\b[^\n]{0,80}?\b(?:not found|does not exist|retired|deprecated|decommissioned|no longer (?:available|supported)|is not available)/i;
11
+ const keyTrouble = /did not accept the API key|API key is saved|invalid api key|key may be wrong/i;
12
+ // Stages whose failed call draws an image from a prompt, which Soften and retry can reword.
13
+ const drawn = ["images", "thumbnail"];
14
+ export function fixFor(step) {
15
+ const reason = step.reason ?? "";
16
+ const cli = cliOf(step.provider, reason);
17
+ if (cli !== undefined && (signedOut.test(reason) || step.kind === "auth"))
18
+ return {
19
+ kind: "sign-in",
20
+ label: `Sign in to ${signIn[cli].name}`,
21
+ cli,
22
+ command: signIn[cli].command,
23
+ };
24
+ if (diskFull.test(reason))
25
+ return { kind: "free-space", label: "Free space" };
26
+ if (step.kind === "refusal" || refusedWords.test(reason))
27
+ return drawn.includes(step.stage)
28
+ ? { kind: "refused", label: "Soften and retry", soften: true }
29
+ : { kind: "refused", label: "Edit the prompt", soften: false };
30
+ if (retiredModel.test(reason))
31
+ return { kind: "switch-model", label: "Switch model" };
32
+ if (step.kind === "auth" || step.kind === "missing_key" || keyTrouble.test(reason)) {
33
+ const name = nameOf(step.provider);
34
+ return {
35
+ kind: "provider-settings",
36
+ label: name === undefined ? "Open Settings → Providers" : `Open Settings → Providers → ${name}`,
37
+ provider: step.provider ?? "",
38
+ };
39
+ }
40
+ return undefined;
41
+ }
42
+ // The CLI a failure names: the project's own choice, or the one its sentence mentions.
43
+ function cliOf(provider, reason) {
44
+ if (provider === "claude-code" || provider === "gemini")
45
+ return provider;
46
+ if (provider === "codex" || provider === "codex-image")
47
+ return "codex";
48
+ if (/Codex CLI/.test(reason))
49
+ return "codex";
50
+ if (/Claude Code CLI/.test(reason))
51
+ return "claude-code";
52
+ if (/Gemini CLI/.test(reason))
53
+ return "gemini";
54
+ return undefined;
55
+ }
56
+ function nameOf(provider) {
57
+ return providers.find((one) => one.id === provider)?.displayName;
58
+ }
59
+ // Which provider a stage's calls go to, from the project's choices.
60
+ export function stageProvider(config, stage) {
61
+ switch (stage) {
62
+ case "audio":
63
+ return config.audio?.provider;
64
+ case "images":
65
+ case "thumbnail":
66
+ return config.images?.provider;
67
+ case "research":
68
+ case "article":
69
+ return config.llm?.provider;
70
+ default:
71
+ return undefined;
72
+ }
73
+ }
@@ -0,0 +1,82 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import { languageInfo, languageLetters } from "../../kernel/ports/languages.js";
3
+ import { resolveBoldFont, resolveFont } from "./catalog.js";
4
+ import { discoverSystemFonts } from "./discovery.js";
5
+ import { readFontFile } from "./files.js";
6
+ import { mappedCodepoints, readFontMetadata } from "./sfnt.js";
7
+ // Captions in a language the chosen font can't draw would come out as empty boxes. For every
8
+ // language but English the chosen font is checked against that language's letters
9
+ // (`languageLetters`); when it misses any, the captions use a bundled Noto font for the script,
10
+ // else a font on this computer that has them all, else the render stops and says how to fix
11
+ // it. English is never checked, so an English project renders with exactly the font it chose.
12
+ // Small enough to ship: Latin, Greek and Cyrillic in one family, and the other alphabets'
13
+ // own. Chinese, Japanese and Korean fonts run to 10-20 MB each, so they are not bundled.
14
+ const bundled = {
15
+ latin: "NotoSans-Regular.ttf",
16
+ greek: "NotoSans-Regular.ttf",
17
+ cyrillic: "NotoSans-Regular.ttf",
18
+ arabic: "NotoSansArabic-Regular.ttf",
19
+ hebrew: "NotoSansHebrew-Regular.ttf",
20
+ devanagari: "NotoSansDevanagari-Regular.ttf",
21
+ thai: "NotoSansThai-Regular.ttf",
22
+ };
23
+ export function captionFontDeps(paths) {
24
+ return {
25
+ resolve: (id) => resolveFont(paths, id),
26
+ system: discoverSystemFonts,
27
+ bundledDir: fileURLToPath(new URL("../../assets/fonts/noto/", import.meta.url)),
28
+ };
29
+ }
30
+ // The font captions are drawn in, for the chosen font and the project language.
31
+ export async function captionFont(deps, fontId, language) {
32
+ const chosen = await deps.resolve(fontId);
33
+ if (language === undefined || language === "en")
34
+ return chosen;
35
+ const letters = languageLetters(language);
36
+ if (await covers(chosen, letters))
37
+ return chosen;
38
+ const info = languageInfo(language);
39
+ const file = bundled[info.script];
40
+ if (file !== undefined) {
41
+ const font = await fontFile(`${deps.bundledDir}${file}`);
42
+ if (font !== undefined && (await covers(font, letters)))
43
+ return font;
44
+ }
45
+ for (const font of await deps.system())
46
+ if (await covers(font, letters))
47
+ return font;
48
+ throw new Error(`The caption font ${chosen.name} has no ${info.name} letters, and Slopify doesn't ship a font for ${info.name} (they are 10-20 MB). In Edit project → Subtitles, use Upload font in the caption font picker to add a font that has them (for example Noto Sans ${info.code === "ja" ? "JP" : info.code === "ko" ? "KR" : "SC"} from fonts.google.com), choose it there, then Try again.`);
49
+ }
50
+ // The same for text drawn bold (the shorts' captions): the chosen font's bold face when it has
51
+ // the letters, else the fallback, which the renderer emboldens.
52
+ export async function captionBoldFont(paths, fontId, language) {
53
+ if (language === undefined || language === "en")
54
+ return resolveBoldFont(paths, fontId);
55
+ const chosen = await resolveFont(paths, fontId);
56
+ const font = await captionFont(captionFontDeps(paths), fontId, language);
57
+ return font.path === chosen.path ? resolveBoldFont(paths, fontId) : font;
58
+ }
59
+ async function covers(font, letters) {
60
+ const bytes = await readFontFile(font.path);
61
+ if (bytes === undefined)
62
+ return false;
63
+ const found = mappedCodepoints(bytes, font.faceIndex, letters);
64
+ // A font whose cmap can't be read is taken at its word: it was chosen for this.
65
+ return found === undefined || found.size === letters.length;
66
+ }
67
+ async function fontFile(path) {
68
+ const bytes = await readFontFile(path);
69
+ const face = bytes === undefined ? undefined : readFontMetadata(bytes)?.[0];
70
+ if (face === undefined)
71
+ return undefined;
72
+ return {
73
+ id: "default",
74
+ name: face.name,
75
+ family: face.family,
76
+ source: "bundled",
77
+ path,
78
+ extension: ".ttf",
79
+ assName: face.assName,
80
+ faceIndex: 0,
81
+ };
82
+ }
@@ -260,3 +260,77 @@ function checksum(bytes) {
260
260
  sum = (sum + bytes.readUInt32BE(at)) >>> 0;
261
261
  return sum;
262
262
  }
263
+ // Which of `codepoints` the face maps to a glyph, read from its Unicode cmap (format 4 or 12,
264
+ // the two every modern font carries). Undefined when the font has neither; the caller then
265
+ // cannot tell, and treats the font as covering what it was chosen for.
266
+ export function mappedCodepoints(content, index, codepoints) {
267
+ const bytes = Buffer.from(content.buffer, content.byteOffset, content.byteLength);
268
+ const face = directories(bytes)?.[index];
269
+ const table = face?.tables.get("cmap");
270
+ if (face === undefined || table === undefined || !validCmap(bytes, table))
271
+ return undefined;
272
+ const count = bytes.readUInt16BE(table.offset + 2);
273
+ let best;
274
+ for (let entry = 0; entry < count; entry += 1) {
275
+ const record = table.offset + 4 + entry * 8;
276
+ const platform = bytes.readUInt16BE(record);
277
+ const encoding = bytes.readUInt16BE(record + 2);
278
+ const at = table.offset + bytes.readUInt32BE(record + 4);
279
+ const format = bytes.readUInt16BE(at);
280
+ const unicode = platform === 0 || (platform === 3 && (encoding === 1 || encoding === 10));
281
+ if (!unicode || (format !== 4 && format !== 12))
282
+ continue;
283
+ if (best === undefined || format > best.format)
284
+ best = { at, format };
285
+ }
286
+ if (best === undefined)
287
+ return undefined;
288
+ const end = table.offset + table.length;
289
+ const found = new Set();
290
+ for (const codepoint of codepoints)
291
+ if (glyphOf(bytes, best.at, best.format, codepoint, end) !== 0)
292
+ found.add(codepoint);
293
+ return found;
294
+ }
295
+ function glyphOf(bytes, at, format, codepoint, end) {
296
+ if (format === 12) {
297
+ const groups = bytes.readUInt32BE(at + 12);
298
+ if (at + 16 + groups * 12 > end)
299
+ return 0;
300
+ for (let group = 0; group < groups; group += 1) {
301
+ const row = at + 16 + group * 12;
302
+ const first = bytes.readUInt32BE(row);
303
+ const last = bytes.readUInt32BE(row + 4);
304
+ if (codepoint >= first && codepoint <= last)
305
+ return bytes.readUInt32BE(row + 8) + (codepoint - first);
306
+ }
307
+ return 0;
308
+ }
309
+ if (codepoint > 0xffff)
310
+ return 0;
311
+ const segments = bytes.readUInt16BE(at + 6) / 2;
312
+ const ends = at + 14;
313
+ const starts = ends + segments * 2 + 2;
314
+ const deltas = starts + segments * 2;
315
+ const ranges = deltas + segments * 2;
316
+ if (ranges + segments * 2 > end)
317
+ return 0;
318
+ for (let segment = 0; segment < segments; segment += 1) {
319
+ const last = bytes.readUInt16BE(ends + segment * 2);
320
+ if (codepoint > last)
321
+ continue;
322
+ const first = bytes.readUInt16BE(starts + segment * 2);
323
+ if (codepoint < first)
324
+ return 0;
325
+ const delta = bytes.readInt16BE(deltas + segment * 2);
326
+ const range = bytes.readUInt16BE(ranges + segment * 2);
327
+ if (range === 0)
328
+ return (codepoint + delta) & 0xffff;
329
+ const glyphAt = ranges + segment * 2 + range + (codepoint - first) * 2;
330
+ if (glyphAt + 2 > end)
331
+ return 0;
332
+ const glyph = bytes.readUInt16BE(glyphAt);
333
+ return glyph === 0 ? 0 : (glyph + delta) & 0xffff;
334
+ }
335
+ return 0;
336
+ }
@@ -6,6 +6,7 @@ import { insertPiece, piecesOf, setPiece } from "../../kernel/runner/piece-repo.
6
6
  import { projectById, setStageProgress } from "../admission/repo.js";
7
7
  import { outputFileName, outputPath } from "../storage/layout.js";
8
8
  import { insertOutput, outputsOf } from "../storage/repo.js";
9
+ import { imageCountsOf } from "./scale.js";
9
10
  // What an image piece carries between runs: what was asked for, and the file it came back
10
11
  // as once the provider answered. That is the whole of the resume.
11
12
  const imagePayload = z.object({
@@ -28,13 +29,13 @@ export async function runImages(deps, context, providers) {
28
29
  if (choice === undefined) {
29
30
  // Admission refuses a run whose images are Generate without a provider and a model, so
30
31
  // reaching here is a bug in admission rather than the user's.
31
- throw new Error("No image model is set for this project. Choose one in Edit project → Providers, then Retry stage.");
32
+ throw new Error("No image model is set for this project. Choose one in Edit project → Providers, then Try again.");
32
33
  }
33
34
  const pieces = plan(deps, context, project.config);
34
35
  if (pieces.length === 0) {
35
36
  // Admission makes an image source mandatory and puts the Number at one or more, so an
36
37
  // empty plan is a bug upstream rather than a run with no pictures.
37
- throw new Error("Slopify hit an internal error (no image prompt was selected). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
38
+ throw new Error("Slopify hit an internal error (no image prompt was selected). Try again; if it happens again, use Download diagnostics in Settings and report it.");
38
39
  }
39
40
  // The run's own frame. The adapter turns it into whatever its provider spells the
40
41
  // closest supported size, and the render crops whatever is left over.
@@ -73,14 +74,16 @@ function plan(deps, context, config) {
73
74
  return existing;
74
75
  }
75
76
  const planned = [];
77
+ // Each prompt's Number, or more when the run scales its images (`scale.ts`).
78
+ const counts = imageCountsOf(config);
76
79
  for (const [at, picked] of config.imagePrompts.entries()) {
77
80
  // `slices/library/slots.ts` names the rendered body after the draft field that picked
78
81
  // it, so the run carries the substituted text under this key.
79
82
  const prompt = config.rendered[`imagePrompts.${String(at)}`];
80
83
  if (prompt === undefined) {
81
- throw new Error(`Slopify hit an internal error (the image prompt ${picked.name} was never filled in). Retry stage; if it happens again, use Download diagnostics in Settings and report it.`);
84
+ throw new Error(`Slopify hit an internal error (the image prompt ${picked.name} was never filled in). Try again; if it happens again, use Download diagnostics in Settings and report it.`);
82
85
  }
83
- for (let send = 1; send <= picked.number; send += 1) {
86
+ for (let send = 1; send <= (counts[at] ?? picked.number); send += 1) {
84
87
  planned.push({
85
88
  id: deps.ids.next(),
86
89
  stageId: context.stage.id,
@@ -224,7 +227,7 @@ function report(deps, context, done, total) {
224
227
  }
225
228
  function payloadOf(piece) {
226
229
  if (piece.payload === null) {
227
- throw new Error("Slopify hit an internal error (a saved image step is empty). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
230
+ throw new Error("Slopify hit an internal error (a saved image step is empty). Try again; if it happens again, use Download diagnostics in Settings and report it.");
228
231
  }
229
232
  return imagePayload.parse(JSON.parse(piece.payload));
230
233
  }
@@ -0,0 +1,84 @@
1
+ import { imagesPerRunMax } from "../admission/rules.js";
2
+ // ceiling: one image an hour is the least that still scales; one every 15 seconds is the
3
+ // most, which is already as fast as the default Seconds per image shows them.
4
+ export const imagesPerHourMin = 1;
5
+ export const imagesPerHourMax = 240;
6
+ // "Every N minutes" spells the same range.
7
+ export const everyMinutesMin = 60 / imagesPerHourMax;
8
+ export const everyMinutesMax = 60 / imagesPerHourMin;
9
+ // ceiling: a hand-set run stops at `imagesPerRunMax` (60), which is one image every two
10
+ // minutes of a two-hour video. A scaled run may plan up to 240, four hours at one a minute:
11
+ // the images are drawn a few at a time under the image provider's own rate limits and the
12
+ // render joins its clips one at a time, so the count costs time and money, both of which the
13
+ // estimate shows before the run starts, rather than anything breaking. Past 240 the count is
14
+ // capped and Play says so.
15
+ export const scaledImagesMax = 240;
16
+ export const expectedWordsMax = 100000;
17
+ // Play's starting expected words (Review).
18
+ export const defaultExpectedWords = 1500;
19
+ // The estimate's own assumption (`estimate/requests.ts`).
20
+ export const spokenWordsPerMinute = 150;
21
+ // How many images a project's revision may hold: a scaled run's cap, else the hand-set one.
22
+ export function imagesPerVideoMax(config) {
23
+ return config.imageScale === undefined ? imagesPerRunMax : scaledImagesMax;
24
+ }
25
+ export function wordCount(text) {
26
+ return text.split(/\s+/).filter((word) => word !== "").length;
27
+ }
28
+ export function narrationMinutes(words) {
29
+ return words / spokenWordsPerMinute;
30
+ }
31
+ // Images per hour, rounded to four places so "every 7 minutes" is stored as a plain number
32
+ // that reads back as 7.
33
+ export function perHourFromMinutes(minutes) {
34
+ return Math.round((60 / minutes) * 10000) / 10000;
35
+ }
36
+ // The setting as Play's control spells it: "every N minutes" when N is a whole or a quarter
37
+ // minute, else "N per hour".
38
+ export function imageScaleForm(scale) {
39
+ const minutes = 60 / scale.perHour;
40
+ const quarters = Math.round(minutes * 4);
41
+ return Math.abs(minutes * 4 - quarters) < 0.001
42
+ ? { every: "minutes", value: String(quarters / 4) }
43
+ : { every: "hour", value: String(scale.perHour) };
44
+ }
45
+ // What the length asks for, before the prompts' own Numbers are counted: one image per
46
+ // started stretch, capped at `scaledImagesMax`.
47
+ export function scaledTarget(scale) {
48
+ // The small epsilon keeps a length that is exactly a whole number of stretches (a rounded
49
+ // 60 / 7) from asking for one more.
50
+ const exact = (narrationMinutes(scale.words) * scale.perHour) / 60;
51
+ return Math.min(Math.max(Math.ceil(exact - 1e-9), 0), scaledImagesMax);
52
+ }
53
+ // Whether the run scales its images: set, and the images are generated from prompts.
54
+ export function scalesImages(draft) {
55
+ return draft.imageScale !== undefined && draft.sources.images === "generate";
56
+ }
57
+ // How many images each ticked prompt makes, in prompt order. Without the setting it is each
58
+ // prompt's Number, exactly as before it existed.
59
+ export function imageCountsOf(draft) {
60
+ const counts = draft.imagePrompts.map((prompt) => prompt.number);
61
+ if (!scalesImages(draft) || counts.length === 0)
62
+ return counts;
63
+ const floor = counts.reduce((sum, count) => sum + count, 0);
64
+ const extra = scaledTarget(draft.imageScale) - floor;
65
+ for (let at = 0; at < extra; at += 1) {
66
+ const index = at % counts.length;
67
+ counts[index] = (counts[index] ?? 0) + 1;
68
+ }
69
+ return counts;
70
+ }
71
+ // The images the run makes from its prompts.
72
+ export function plannedImageCount(draft) {
73
+ return imageCountsOf(draft).reduce((sum, count) => sum + count, 0);
74
+ }
75
+ // Admission's sentence for a stored setting out of range; Play's control says the same.
76
+ export function imageScaleProblem(scale) {
77
+ if (!Number.isFinite(scale.perHour) ||
78
+ scale.perHour < imagesPerHourMin ||
79
+ scale.perHour > imagesPerHourMax)
80
+ return `Enter between ${String(imagesPerHourMin)} and ${String(imagesPerHourMax)} images per hour (one every ${String(everyMinutesMin)} to ${String(everyMinutesMax)} minutes) under Images → More images for long videos.`;
81
+ if (!Number.isInteger(scale.words) || scale.words < 1 || scale.words > expectedWordsMax)
82
+ return `Enter the expected words (Review) as a whole number between 1 and ${String(expectedWordsMax)}; the image count for long videos is planned from it.`;
83
+ return undefined;
84
+ }