@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,19 +1,26 @@
1
- import { referenceKey } from "../admission/model.js";
1
+ import { basename } from "node:path";
2
+ import { referenceKey, thumbnailVariant } from "../admission/model.js";
2
3
  import { plainText } from "../article/plain.js";
3
4
  import { splitEndMatter } from "../article/split.js";
5
+ import { spokenPassage } from "../narration/describe.js";
4
6
  import { observeNarration } from "../narration/live.js";
5
7
  import { validatePreparation } from "../narration/preparation.js";
6
8
  import { prepareRequests } from "../narration/steering.js";
7
9
  import { chaptersFrom } from "../research/planner.js";
8
10
  import { sourcedAnswer } from "../research/synthesis.js";
11
+ import { imageReviewers } from "../reviews/model.js";
9
12
  import { discardPreparedAssets, writeAsset } from "../storage/assets.js";
10
13
  import { outputPath } from "../storage/layout.js";
11
14
  import { probeDurationMs } from "../video/ffmpeg.js";
15
+ import { parseAttribution } from "../voices/attribution.js";
16
+ import { parseScript } from "../voices/script.js";
12
17
  import { executeArticleRequests } from "./runtime-article.js";
13
18
  import { imageCall } from "./runtime-image.js";
14
19
  import { frozenInstructions } from "./runtime-instructions.js";
20
+ import { articlePicture } from "./runtime-pictures.js";
15
21
  import { executionPlan, executionView, savedCatalogue } from "./runtime-plan.js";
16
22
  import { preparedResult, preparedTexts, publishResult } from "./runtime-publication.js";
23
+ import { clearSoftening, softenedPrompt, softeningRequested, softenMessages } from "./soften.js";
17
24
  export async function executeProviderRecipe(deps, context, providers, piece) {
18
25
  if (deps.db
19
26
  .prepare("SELECT state FROM revision_work_pieces WHERE id=? AND work_id=?")
@@ -22,6 +29,8 @@ export async function executeProviderRecipe(deps, context, providers, piece) {
22
29
  const input = piece.input;
23
30
  const wrapped = providers.forPiece(piece.id);
24
31
  if (input.kind === "llm") {
32
+ // A described figure's own picture, when the project has it and the model can look.
33
+ const picture = describedPicture(deps, context, input);
25
34
  const answer = piece.key === "article:body"
26
35
  ? await executeArticleRequests(deps, context, providers, piece)
27
36
  : await wrapped.llm({
@@ -29,7 +38,8 @@ export async function executeProviderRecipe(deps, context, providers, piece) {
29
38
  model: input.model,
30
39
  ...(input.thinking === null ? {} : { thinking: input.thinking }),
31
40
  thinkingConfig: input.thinkingConfig,
32
- messages: input.messages,
41
+ messages: picture === undefined ? input.messages : withPicture(input.messages),
42
+ ...(picture === undefined ? {} : { images: [picture] }),
33
43
  documents: input.documents,
34
44
  webSearch: input.webSearch,
35
45
  previewLabel: piece.key,
@@ -54,7 +64,17 @@ export async function executeProviderRecipe(deps, context, providers, piece) {
54
64
  return "done";
55
65
  }
56
66
  if (input.kind === "tts") {
57
- const spoken = await wrapped.tts({ provider: input.provider, model: input.model, voiceId: input.voice, text: input.text }, observeNarration(deps.audioPreviews, context.work.projectId, piece.id, narrationLabel(deps, context, piece), { revisionId: context.work.revisionId, workId: context.work.workId, workPieceId: piece.id }));
67
+ const spoken = await wrapped.tts({
68
+ provider: input.provider,
69
+ model: input.model,
70
+ voiceId: input.voice,
71
+ text: input.text,
72
+ ...(input.dialogue === undefined
73
+ ? {}
74
+ : {
75
+ dialogue: input.dialogue.map((line) => ({ voiceId: line.voice, text: line.text })),
76
+ }),
77
+ }, observeNarration(deps.audioPreviews, context.work.projectId, piece.id, narrationLabel(deps, context, piece), { revisionId: context.work.revisionId, workId: context.work.workId, workPieceId: piece.id }));
58
78
  if (!spoken.ok)
59
79
  return "held";
60
80
  const asset = writeAsset(deps, context.work.projectId, "narration.mp3", spoken.value.bytes);
@@ -77,6 +97,15 @@ export async function executeProviderRecipe(deps, context, providers, piece) {
77
97
  voice: input.voice,
78
98
  ...(durationMs === undefined ? {} : { durationMs }),
79
99
  }, asset);
100
+ context.emit({
101
+ type: "narration.piece",
102
+ projectId: context.work.projectId,
103
+ key: piece.key,
104
+ durationMs: durationMs ?? null,
105
+ revisionId: context.work.revisionId,
106
+ workId: context.work.workId,
107
+ workPieceId: piece.id,
108
+ });
80
109
  deps.count?.("stage.completed", {
81
110
  stage: "audio",
82
111
  segment: input.segment,
@@ -95,48 +124,91 @@ export async function executeProviderRecipe(deps, context, providers, piece) {
95
124
  const index = piece.key.startsWith("image:")
96
125
  ? (view?.revision.content.imageOrder.indexOf(piece.key.slice(6)) ?? 0) + 1
97
126
  : undefined;
98
- const image = await wrapped.image(imageCall(deps, context.work.projectId, input, piece.key === "thumbnail:image"
99
- ? "Thumbnail"
127
+ // Which thumbnail this is (1-3), or undefined for any other image.
128
+ const variant = thumbnailVariant(piece.key);
129
+ const label = variant !== undefined
130
+ ? variant === 1
131
+ ? "Thumbnail"
132
+ : `Thumbnail ${String(variant)}`
100
133
  : piece.key === referenceKey
101
134
  ? "Establishing image"
102
135
  : index === undefined
103
136
  ? "Image"
104
- : `Image ${String(index)}`));
137
+ : `Image ${String(index)}`;
138
+ const softened = await softenIfAsked(deps, context, wrapped, piece, input.prompt, label);
139
+ if (softened === "held")
140
+ return "held";
141
+ const prompt = softened ?? input.prompt;
142
+ const image = await wrapped.image(imageCall(deps, context.work.projectId, { ...input, prompt }, label));
105
143
  if (!image.ok)
106
144
  return "held";
107
145
  const asset = writeAsset(deps, context.work.projectId, image.value.mime === "image/jpeg" ? "image.jpg" : "image.png", image.value.bytes);
108
- const output = preparedResult(deps, context, piece, piece.key === "thumbnail:image"
109
- ? "thumbnail"
110
- : piece.key === referenceKey
111
- ? "reference"
112
- : "image", asset, null, {
113
- prompt: input.prompt,
146
+ const output = preparedResult(deps, context, piece, variant !== undefined ? "thumbnail" : piece.key === referenceKey ? "reference" : "image", asset, null, {
147
+ prompt,
114
148
  provider: input.provider,
115
149
  model: input.model,
116
150
  ...(index === undefined ? {} : { index }),
151
+ // The second and third thumbnails carry their number; the first keeps the meta a
152
+ // thumbnail always had.
153
+ ...(variant === undefined || variant === 1 ? {} : { index: variant }),
117
154
  });
118
- await publishResult(deps, context, piece, [output], { prompt: input.prompt }, asset);
155
+ await publishResult(deps, context, piece, [output], { prompt }, asset);
156
+ if (softened !== undefined)
157
+ clearSoftening(deps.db, context.work.projectId, [piece.key]);
119
158
  deps.count?.("stage.completed", {
120
159
  stage: context.work.kind,
121
160
  provider: input.provider,
122
161
  model: input.model,
123
- ...(piece.key === "thumbnail:image" ? { thumbnails: 1 } : { images: 1 }),
162
+ ...(variant !== undefined ? { thumbnails: 1 } : { images: 1 }),
124
163
  });
125
164
  return "done";
126
165
  }
127
- throw new Error("Slopify hit an internal error (this step has no provider request to run). Use Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
166
+ throw new Error("Slopify hit an internal error (this step has no provider request to run). Use Try again; if it happens again, use Download diagnostics in Settings and report it.");
167
+ }
168
+ // Soften and retry (`soften.ts`): the project's AI model rewords a refused image prompt
169
+ // before the image is drawn again. Undefined when nobody asked, or the project has no model.
170
+ async function softenIfAsked(deps, context, providers, piece, prompt, label) {
171
+ if (!softeningRequested(deps.db, context.work.projectId, piece.key))
172
+ return undefined;
173
+ const llm = executionView(deps, context.work.projectId, context.work.revisionId)?.revision.config
174
+ .llm;
175
+ if (llm === undefined)
176
+ return undefined;
177
+ const answer = await providers.llm({
178
+ provider: llm.provider,
179
+ model: llm.model,
180
+ ...(llm.thinking === undefined ? {} : { thinking: llm.thinking }),
181
+ messages: softenMessages(prompt),
182
+ previewLabel: `${label}: softened prompt`,
183
+ check: (value) => softenedPrompt(value.text) === ""
184
+ ? "The AI model sent back an empty prompt while softening it. Use Soften and retry again, or reword the prompt yourself in Edit project → Images."
185
+ : undefined,
186
+ });
187
+ return answer.ok ? softenedPrompt(answer.value.text) : "held";
128
188
  }
129
189
  function checkAnswer(piece, answer) {
130
190
  if (answer.text.trim() === "")
131
- return "The AI model sent back an empty answer. Use Retry stage; if it keeps happening, choose another model in the Providers section of Edit project.";
191
+ return "The AI model sent back an empty answer. Use Try again; if it keeps happening, choose another model in the Providers section of Edit project.";
192
+ if (piece.input.kind === "llm" && piece.input.describe !== undefined)
193
+ return spokenPassage(answer.text) === ""
194
+ ? "The AI model's description had no words a narrator could say. Use Try again; if it keeps happening, choose another model in the Providers section of Edit project, or turn off Describe tables and figures there."
195
+ : undefined;
132
196
  if (piece.key === "research:planner")
133
197
  return chaptersFrom(answer.text).length === 0
134
- ? "The AI model's research plan listed no chapters, so research could not go on. Use Retry stage; if it keeps happening, choose another model in the Providers section of Edit project."
198
+ ? "The AI model's research plan listed no chapters, so research could not go on. Use Try again; if it keeps happening, choose another model in the Providers section of Edit project."
135
199
  : undefined;
136
200
  if (piece.key === "research:notes" || piece.key.startsWith("research:chapter:"))
137
201
  return sourcedAnswer(piece.key, answer.text);
202
+ // A speaker split that loses or adds words is asked for again rather than narrated.
203
+ if (piece.input.kind === "llm" && piece.input.script?.attribute === true) {
204
+ const checked = parseAttribution(answer.text, attributionSource(piece.input.messages), piece.input.script.speakers);
205
+ return checked.ok ? undefined : checked.reason;
206
+ }
138
207
  return undefined;
139
208
  }
209
+ function attributionSource(messages) {
210
+ return messages.at(-1)?.content ?? "";
211
+ }
140
212
  async function publishText(deps, context, piece, answer) {
141
213
  if (piece.input.kind === "llm" && piece.input.preparation !== undefined) {
142
214
  const checked = validatePreparation(answer.text, piece.input.preparation.source);
@@ -150,8 +222,20 @@ async function publishText(deps, context, piece, answer) {
150
222
  });
151
223
  return;
152
224
  }
225
+ if (piece.input.kind === "llm" && piece.input.describe !== undefined) {
226
+ // The passage the narration says, as it says it; nothing to download.
227
+ await publishResult(deps, context, piece, [], { text: spokenPassage(answer.text) });
228
+ return;
229
+ }
153
230
  if (piece.key === "article:body") {
154
231
  const parts = splitEndMatter(answer.text);
232
+ // A script is read before it is kept: one the narration could not speak fails here, with
233
+ // the line to fix, rather than at the narration.
234
+ if (piece.input.kind === "llm" && piece.input.script !== undefined) {
235
+ const checked = parseScript(parts.body, piece.input.script.speakers);
236
+ if (!checked.ok)
237
+ throw new Error(`The text model's script can't be read: ${checked.reason} Try again to have it written again, or fix it in Edit project → Article.`);
238
+ }
155
239
  await publishResult(deps, context, piece, preparedTexts(deps, context, piece, [
156
240
  ["instructions", "instructions.md", frozenInstructions(deps, context, piece)],
157
241
  ["article_md", "article.md", answer.text],
@@ -161,6 +245,10 @@ async function publishText(deps, context, piece, answer) {
161
245
  ]), { text: answer.text });
162
246
  return;
163
247
  }
248
+ if (piece.key === "script:attribute") {
249
+ await publishResult(deps, context, piece, preparedTexts(deps, context, piece, [["script_md", "script.md", answer.text]]), { text: answer.text });
250
+ return;
251
+ }
164
252
  if (piece.key === "research:notes") {
165
253
  await publishResult(deps, context, piece, preparedTexts(deps, context, piece, [
166
254
  ["notes", "notes.md", answer.text],
@@ -184,7 +272,7 @@ async function publishText(deps, context, piece, answer) {
184
272
  : [];
185
273
  const title = titles[Number(piece.key.split(":").at(-1)) - 1];
186
274
  if (typeof title !== "string")
187
- throw new Error("Slopify hit an internal error (a research chapter has no title in the research plan). Use Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
275
+ throw new Error("Slopify hit an internal error (a research chapter has no title in the research plan). Use Try again; if it happens again, use Download diagnostics in Settings and report it.");
188
276
  const asset = writeAsset(deps, context.work.projectId, `research-${piece.key.split(":").at(-1)}.md`, Buffer.from(answer.text));
189
277
  await publishResult(deps, context, piece, [], { title, notes: answer.text }, asset);
190
278
  return;
@@ -222,3 +310,23 @@ function checkPreparation(deps, context, piece, answer) {
222
310
  const prepared = prepareRequests(source, checked.cues, model?.tts.maxCharacters ?? 4000);
223
311
  return prepared.ok ? undefined : prepared.reason;
224
312
  }
313
+ // A described figure's own picture (`runtime-pictures.ts`), sent only to a text provider that
314
+ // looks at pictures (`reviews/model.ts`). Otherwise the figure is described from its caption
315
+ // and alt text alone.
316
+ function describedPicture(deps, context, input) {
317
+ const named = input.describe?.image;
318
+ if (named === undefined || !imageReviewers.includes(input.provider))
319
+ return undefined;
320
+ const view = executionView(deps, context.work.projectId, context.work.revisionId);
321
+ const path = view === undefined ? undefined : articlePicture(deps, view, named);
322
+ return path === undefined ? undefined : { path, name: basename(path) };
323
+ }
324
+ function withPicture(messages) {
325
+ const last = messages.findLastIndex((message) => message.role === "user");
326
+ return messages.map((message, index) => index === last
327
+ ? {
328
+ ...message,
329
+ content: `${message.content}\n\nThe picture itself is attached: look at it before you answer.`,
330
+ }
331
+ : message);
332
+ }
@@ -12,7 +12,11 @@ export function preparedResult(deps, context, piece, role, asset, durationMs = n
12
12
  slot: role === "image" ||
13
13
  role === "short_image" ||
14
14
  role === "short_video" ||
15
- role === "animated_image"
15
+ role === "animated_image" ||
16
+ // Level the volume: one levelled narration per segment.
17
+ role === "audio_levelled" ||
18
+ // The second and third thumbnails; the first keeps the slot it always had.
19
+ (role === "thumbnail" && piece.key !== "thumbnail:image")
16
20
  ? piece.key
17
21
  : `${context.work.kind}:${role}`,
18
22
  workKey: piece.key,
@@ -35,7 +39,7 @@ export function preparedResult(deps, context, piece, role, asset, durationMs = n
35
39
  export function preparedText(deps, context, piece, role, filename, text) {
36
40
  const output = preparedTexts(deps, context, piece, [[role, filename, text]])[0];
37
41
  if (output === undefined)
38
- throw new Error("Slopify hit an internal error (a text file could not be prepared). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
42
+ throw new Error("Slopify hit an internal error (a text file could not be prepared). Try again; if it happens again, use Download diagnostics in Settings and report it.");
39
43
  return output;
40
44
  }
41
45
  export function preparedTexts(deps, context, piece, texts) {
@@ -93,13 +97,17 @@ export async function publishResult(deps, context, piece, outputs, payload, asse
93
97
  function pieceKind(piece) {
94
98
  if (piece.input.kind === "llm" && piece.input.preparation !== undefined)
95
99
  return "prompt_written";
100
+ if (piece.input.kind === "llm" && piece.input.describe !== undefined)
101
+ return "prompt_written";
96
102
  if (piece.input.kind === "tts")
97
103
  return piece.input.segment === "body" ? "chunk" : "segment";
98
104
  if (piece.input.kind === "provided" && /^audio:body:.+:\d+$/.test(piece.key))
99
105
  return "chunk";
100
106
  if (piece.input.kind === "provided" && /^audio:(intro|outro):\d+$/.test(piece.key))
101
107
  return "segment";
102
- if (piece.key.startsWith("image:") || piece.key.startsWith("animate:"))
108
+ if (piece.key.startsWith("image:") ||
109
+ piece.key.startsWith("animate:") ||
110
+ piece.key.startsWith("figure:card:"))
103
111
  return "image";
104
112
  if (piece.key.startsWith("research:chapter:"))
105
113
  return "chapter";
@@ -123,6 +131,13 @@ function pieceIndex(deps, context, piece) {
123
131
  return 1;
124
132
  if (piece.key === "entry:outro:text")
125
133
  return 2;
134
+ // The cards take places of their own, past the animated images'.
135
+ const card = /^figure:card:(\d+)$/.exec(piece.key);
136
+ if (card !== null)
137
+ return 800000 + Number(card[1]);
138
+ // The narration's descriptions take places of their own, past the preparation steps'.
139
+ if (piece.input.kind === "llm" && piece.input.describe !== undefined)
140
+ return 500000 + piece.input.describe.index;
126
141
  // The shorts' steps take places of their own, past every other step of the Video stage:
127
142
  // the pick, then per short its prompts, its images and its render.
128
143
  const short = /^shorts:(?:(\d+):(prompts|image:(\d+)|render)|pick)$/.exec(piece.key);
@@ -152,6 +167,7 @@ function pieceIndex(deps, context, piece) {
152
167
  "narration:files:body",
153
168
  "narration:files:intro",
154
169
  "narration:files:outro",
170
+ "voices:files",
155
171
  ];
156
172
  const localIndex = localOrder.indexOf(piece.key);
157
173
  if (localIndex >= 0)
@@ -190,10 +206,10 @@ export function retainPartialArticle(deps, context, piece, text) {
190
206
  if (authority === undefined)
191
207
  return;
192
208
  if (authority.key !== piece.key || authority.fingerprint !== piece.fingerprint)
193
- throw new Error("Slopify hit an internal error (a partly written article doesn't match its saved step). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
209
+ throw new Error("Slopify hit an internal error (a partly written article doesn't match its saved step). Try again; if it happens again, use Download diagnostics in Settings and report it.");
194
210
  const revision = revisionById(deps.db, context.work.projectId, context.work.revisionId);
195
211
  if (revision === undefined)
196
- throw new Error("Slopify hit an internal error (the project version for a partly written article is missing). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
212
+ throw new Error("Slopify hit an internal error (the project version for a partly written article is missing). Try again; if it happens again, use Download diagnostics in Settings and report it.");
197
213
  insertAsset(deps.db, result.asset);
198
214
  insertManifestOutput(deps.db, revision, {
199
215
  slot: `article:partial:${piece.id}`,
@@ -0,0 +1,267 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { z } from "zod";
3
+ import { thinkingModes } from "../../kernel/ports/llm.js";
4
+ import { plainText } from "../article/plain.js";
5
+ import { defaultReviewPrompts, narrationItemKey, reviewStages, } from "../reviews/model.js";
6
+ import { nextAttempt, reviewOutcome } from "../reviews/outcome.js";
7
+ import { latestVerdict, saveVerdict } from "../reviews/repo.js";
8
+ import { parseVerdict, reviewMessages } from "../reviews/verdict.js";
9
+ import { pickedShortsOf } from "../shorts/clips.js";
10
+ import { outputPath } from "../storage/layout.js";
11
+ import { exportSnapshot, revisionTranscript, } from "./runtime-export-inputs.js";
12
+ import { savedCatalogue } from "./runtime-plan.js";
13
+ import { publishResult } from "./runtime-publication.js";
14
+ import { wordsSchema } from "./runtime-subtitles.js";
15
+ // What `recipe-reviews.ts` saves in the step's values, by position.
16
+ const valuesSchema = z.tuple([
17
+ z.enum(reviewStages),
18
+ z.string(),
19
+ z.array(z.tuple([z.string(), z.unknown()])),
20
+ z.string(),
21
+ z.string(),
22
+ z.string().nullable(),
23
+ z.string(),
24
+ z.enum(["flag", "redo"]),
25
+ z.number().int().nonnegative(),
26
+ ]);
27
+ // ceiling: enough of a long article or research for a reviewer to judge it, and well inside
28
+ // every supported model's context.
29
+ const textMax = 60_000;
30
+ // Asks the reviewer about one finished item and saves its verdict. A malformed answer is a
31
+ // failed attempt the provider wrapper asks again; a verdict that sends the item back is
32
+ // saved as a pending redo, which `review-redo.ts` starts once this step has finished, and
33
+ // until then the item's dependents are held (`reviewHold`).
34
+ export async function executeReviewRecipe(deps, context, providers, piece) {
35
+ if (deps.db
36
+ .prepare("SELECT state FROM revision_work_pieces WHERE id=? AND work_id=?")
37
+ .get(piece.id, context.work.workId)?.state === "done")
38
+ return "done";
39
+ if (!context.maySubmit(piece.id))
40
+ return "held";
41
+ context.signal.throwIfAborted();
42
+ const input = piece.input;
43
+ const values = input.kind === "local" && input.operation === "review-v1"
44
+ ? valuesSchema.safeParse(input.values)
45
+ : undefined;
46
+ if (values === undefined || !values.success)
47
+ throw new Error("Slopify hit an internal error (a review step was set up wrongly). Try again; if it happens again, use Download diagnostics in Settings and report it.");
48
+ const [stage, itemKey, , provider, model, thinking, prompt, mode, retries] = values.data;
49
+ const snapshot = exportSnapshot(deps, context, piece);
50
+ const material = reviewMaterial(deps, context, snapshot, stage, itemKey, prompt);
51
+ const previous = latestVerdict(deps.db, context.work.projectId, itemKey);
52
+ const attempt = nextAttempt(previous, material.itemFingerprint);
53
+ const row = deps.db
54
+ .prepare("SELECT recipe_context FROM revision_work WHERE id=?")
55
+ .get(context.work.workId);
56
+ const catalogued = savedCatalogue(row?.recipe_context).llm.find((one) => one.provider === provider && one.id === model);
57
+ const level = thinking === null ? undefined : z.enum(thinkingModes).parse(thinking);
58
+ if (!context.maySubmit(piece.id))
59
+ return "held";
60
+ const answer = await providers.forPiece(piece.id).llm({
61
+ provider,
62
+ model,
63
+ ...(level === undefined ? {} : { thinking: level }),
64
+ thinkingConfig: level === undefined ? null : (catalogued?.llm.thinking?.[level] ?? null),
65
+ messages: reviewMessages(material.request),
66
+ webSearch: false,
67
+ ...(material.images.length === 0 ? {} : { images: material.images }),
68
+ previewLabel: piece.key,
69
+ check: (value) => {
70
+ const parsed = parseVerdict(value.text);
71
+ return parsed.ok ? undefined : parsed.reason;
72
+ },
73
+ });
74
+ if (!answer.ok)
75
+ return "held";
76
+ const verdict = parseVerdict(answer.value.text);
77
+ if (!verdict.ok)
78
+ throw new Error(verdict.reason);
79
+ const outcome = reviewOutcome({ passed: verdict.value.passed, mode, attempt, retries });
80
+ const saved = saveVerdict(deps.db, {
81
+ id: deps.ids.next(),
82
+ projectId: context.work.projectId,
83
+ revisionId: context.work.revisionId,
84
+ itemKey,
85
+ stage,
86
+ itemFingerprint: material.itemFingerprint,
87
+ reviewFingerprint: piece.fingerprint,
88
+ passed: verdict.value.passed,
89
+ reasons: verdict.value.reasons,
90
+ outcome,
91
+ attempt,
92
+ action: null,
93
+ actionAt: null,
94
+ redoState: outcome === "redo" ? "pending" : null,
95
+ redoError: null,
96
+ createdAt: deps.clock.now().toISOString(),
97
+ });
98
+ await publishResult(deps, context, piece, [], {
99
+ verdictId: saved.id,
100
+ passed: saved.passed,
101
+ reasons: saved.reasons,
102
+ outcome: saved.outcome,
103
+ attempt: saved.attempt,
104
+ });
105
+ // Kept and flagged: nothing more happens to the item until someone decides.
106
+ if (saved.outcome === "flagged")
107
+ context.emit({
108
+ type: "review.flagged",
109
+ projectId: context.work.projectId,
110
+ verdictId: saved.id,
111
+ stage: saved.stage,
112
+ itemKey: saved.itemKey,
113
+ ...(saved.reasons[0] === undefined ? {} : { reason: saved.reasons[0] }),
114
+ });
115
+ deps.count?.("stage.completed", {
116
+ provider,
117
+ model,
118
+ tokensIn: answer.value.usage?.inputTokens ?? 0,
119
+ tokensOut: answer.value.usage?.outputTokens ?? 0,
120
+ });
121
+ return "done";
122
+ }
123
+ // What the voice was asked to say, segment by segment, as the word timing matched it: the intro
124
+ // and outro, and the prepared or hand-edited spoken text rather than the article as written.
125
+ // Falls back to the article when those texts can't be read.
126
+ function spokenText(deps, snapshot) {
127
+ const present = ["intro", "body", "outro"].filter((kind) => snapshot.view.outputs.some((one) => one.selected &&
128
+ one.available &&
129
+ one.state === "ready" &&
130
+ one.output.role === `audio_${kind}`));
131
+ try {
132
+ const text = present.map((kind) => revisionTranscript(deps, snapshot, kind)).join("\n\n");
133
+ if (text.trim() !== "")
134
+ return text;
135
+ }
136
+ catch {
137
+ // The article below is still a fair reference.
138
+ }
139
+ return plainText(snapshot.view.articleMarkdown ?? "");
140
+ }
141
+ function reviewMaterial(deps, context, snapshot, stage, itemKey, prompt) {
142
+ const { view } = snapshot;
143
+ const recipes = snapshot.plan.recipes;
144
+ const config = view.revision.config;
145
+ const output = (workKey, role) => view.outputs.find((row) => row.workKey === workKey &&
146
+ row.selected &&
147
+ row.available &&
148
+ row.state === "ready" &&
149
+ (role === undefined || row.output.role === role));
150
+ const path = (row) => outputPath(deps.paths, context.work.projectId, row.output.path);
151
+ const text = (row) => clip(readFileSync(path(row), "utf8"));
152
+ const missing = (what, fix) => new Error(`The ${what} this review looks at is missing, so it could not be reviewed. ${fix}, then use Try again.`);
153
+ const request = (sections, images = []) => ({
154
+ stage,
155
+ instruction: prompt.trim() === "" ? defaultReviewPrompts[stage] : prompt,
156
+ sections,
157
+ images,
158
+ });
159
+ switch (stage) {
160
+ case "article": {
161
+ const article = output(itemKey, "article_md");
162
+ if (article === undefined)
163
+ throw missing("article", "Use More → Write the article again in the Article section");
164
+ const notes = output("research:notes", "notes");
165
+ const research = notes !== undefined ? text(notes) : clip(config.provided.research?.trim() ?? "");
166
+ return {
167
+ request: request([
168
+ {
169
+ label: "Article prompt the article was written from",
170
+ text: clip(config.rendered.article ?? ""),
171
+ },
172
+ ...(research === "" ? [] : [{ label: "Research notes", text: research }]),
173
+ { label: "Article", text: text(article) },
174
+ ]),
175
+ images: [],
176
+ itemFingerprint: article.fingerprint,
177
+ };
178
+ }
179
+ case "images":
180
+ case "thumbnail": {
181
+ const image = output(itemKey);
182
+ if (image === undefined)
183
+ throw missing(stage === "images" ? "image" : "thumbnail", stage === "images"
184
+ ? "Use More → make it again in its section on Images"
185
+ : "Use More → make it again in its section on Thumbnail");
186
+ const recipe = recipes.find((one) => one.key === itemKey);
187
+ const brief = recipe?.input.kind === "image" ? recipe.input.prompt : "";
188
+ const reference = stage === "images" ? output("reference:image") : undefined;
189
+ return {
190
+ request: request([
191
+ ...(stage === "thumbnail" ? [{ label: "Video title", text: config.title }] : []),
192
+ ...(brief === "" ? [] : [{ label: "Brief the image was drawn from", text: brief }]),
193
+ ], [
194
+ stage === "images" ? "the image to review" : "the thumbnail to review",
195
+ ...(reference === undefined ? [] : ["the establishing image it must match"]),
196
+ ]),
197
+ images: [
198
+ {
199
+ path: path(image),
200
+ name: stage === "images" ? "the image to review" : "the thumbnail to review",
201
+ },
202
+ ...(reference === undefined
203
+ ? []
204
+ : [{ path: path(reference), name: "the establishing image it must match" }]),
205
+ ],
206
+ itemFingerprint: image.fingerprint,
207
+ };
208
+ }
209
+ case "narration": {
210
+ const timing = output("subtitles:timing", "subtitle_words");
211
+ if (timing === undefined || itemKey !== narrationItemKey)
212
+ throw missing("narration's word timing", "Use More → Render the video again in the Video section");
213
+ const { words, omissions } = wordsSchema.parse(JSON.parse(readFileSync(path(timing), "utf8")));
214
+ const unsure = words.filter((word) => (word.confidence ?? 1) < 0.3).length;
215
+ return {
216
+ request: request([
217
+ {
218
+ label: "Text the narration was read from",
219
+ text: clip(spokenText(deps, snapshot)),
220
+ },
221
+ { label: "What the timing heard", text: clip(words.map((word) => word.text).join(" ")) },
222
+ {
223
+ label: "Stretches of text the timing could not hear",
224
+ text: omissions.length === 0
225
+ ? "None."
226
+ : omissions.map((gap) => `At ${gap.start.toFixed(1)} s: ${gap.text}`).join("\n"),
227
+ },
228
+ {
229
+ label: "Words heard with low confidence",
230
+ text: `${String(unsure)} of ${String(words.length)}.`,
231
+ },
232
+ ]),
233
+ images: [],
234
+ itemFingerprint: timing.fingerprint,
235
+ };
236
+ }
237
+ case "shorts": {
238
+ const render = output(`${itemKey}:render`);
239
+ if (render === undefined)
240
+ throw missing("short", "Use Make this short again in Edit project → Shorts");
241
+ const number = Number(itemKey.slice("shorts:".length));
242
+ const pick = view.pieces.find((row) => row.key === "shorts:pick" && row.selected && row.piece.state === "done");
243
+ const title = pickedShortsOf(pick?.piece.payload)?.shorts.find((clip) => clip.number === number)?.title ??
244
+ "";
245
+ const stills = view.outputs
246
+ .filter((row) => row.workKey.startsWith(`${itemKey}:image:`) &&
247
+ row.selected &&
248
+ row.available &&
249
+ row.state === "ready")
250
+ .slice(0, 6);
251
+ if (stills.length === 0)
252
+ throw missing("short's images", "Use Make this short again in Edit project → Shorts");
253
+ const names = stills.map((_, index) => `image ${String(index + 1)} of the short`);
254
+ return {
255
+ request: request([{ label: "Title of the short", text: title }], names),
256
+ images: stills.map((row, index) => ({
257
+ path: path(row),
258
+ name: names[index] ?? "an image of the short",
259
+ })),
260
+ itemFingerprint: render.fingerprint,
261
+ };
262
+ }
263
+ }
264
+ }
265
+ function clip(value) {
266
+ return value.length <= textMax ? value : `${value.slice(0, textMax)}\n[... cut for length]`;
267
+ }
@@ -4,8 +4,10 @@ import { executeExportRecipe } from "./runtime-export.js";
4
4
  import { executeLocalRecipe } from "./runtime-local.js";
5
5
  import { pieceLabel } from "./runtime-piece-label.js";
6
6
  import { executeProviderRecipe } from "./runtime-provider.js";
7
+ import { executeReviewRecipe } from "./runtime-review.js";
7
8
  import { executeShortsRecipe } from "./runtime-shorts.js";
8
9
  import { executeSubtitleRecipe } from "./runtime-subtitles.js";
10
+ import { executeVoicesRecipe } from "./runtime-voices.js";
9
11
  import { executeYoutubeRecipe } from "./runtime-youtube.js";
10
12
  import { workPieces } from "./work-records.js";
11
13
  export async function runRevisionInvocation(deps, context, providers) {
@@ -22,17 +24,21 @@ export async function runRevisionInvocation(deps, context, providers) {
22
24
  ? await executeExportRecipe(deps, context, piece)
23
25
  : piece.key.startsWith("document:")
24
26
  ? await executeDocumentRecipe(deps, context, piece)
25
- : piece.key.startsWith("youtube:")
26
- ? await executeYoutubeRecipe(deps, context, providers, piece)
27
- : piece.key.startsWith("shorts:")
28
- ? await executeShortsRecipe(deps, context, providers, piece)
29
- : piece.key.startsWith("animate:")
30
- ? await executeAnimateRecipe(deps, context, providers, piece)
31
- : piece.input.kind === "llm" ||
32
- piece.input.kind === "tts" ||
33
- piece.input.kind === "image"
34
- ? await executeProviderRecipe(deps, context, providers, piece)
35
- : await executeLocalRecipe(deps, context, piece);
27
+ : piece.key.startsWith("review:")
28
+ ? await executeReviewRecipe(deps, context, providers, piece)
29
+ : piece.key.startsWith("voices:")
30
+ ? await executeVoicesRecipe(deps, context, piece)
31
+ : piece.key.startsWith("youtube:")
32
+ ? await executeYoutubeRecipe(deps, context, providers, piece)
33
+ : piece.key.startsWith("shorts:")
34
+ ? await executeShortsRecipe(deps, context, providers, piece)
35
+ : piece.key.startsWith("animate:")
36
+ ? await executeAnimateRecipe(deps, context, providers, piece)
37
+ : piece.input.kind === "llm" ||
38
+ piece.input.kind === "tts" ||
39
+ piece.input.kind === "image"
40
+ ? await executeProviderRecipe(deps, context, providers, piece)
41
+ : await executeLocalRecipe(deps, context, piece);
36
42
  if (outcome === "held")
37
43
  return outcome;
38
44
  }