@gentbajko/slopify 2.5.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (446) hide show
  1. package/LICENSE +201 -21
  2. package/NOTICE +12 -0
  3. package/README.md +21 -40
  4. package/SUBTITLES.md +1 -1
  5. package/dist/adapter-registry.js +3 -0
  6. package/dist/adapters/alignment/cache.js +4 -1
  7. package/dist/adapters/alignment/ctc.js +25 -23
  8. package/dist/adapters/alignment/index.js +37 -6
  9. package/dist/adapters/alignment/multilingual.js +267 -0
  10. package/dist/adapters/alignment/protocol.js +11 -0
  11. package/dist/adapters/alignment/quality.js +4 -3
  12. package/dist/adapters/alignment/sentences.js +95 -0
  13. package/dist/adapters/alignment/spec.js +24 -0
  14. package/dist/adapters/alignment/spell.js +769 -0
  15. package/dist/adapters/alignment/text.js +56 -9
  16. package/dist/adapters/alignment/window.js +34 -30
  17. package/dist/adapters/alignment/worker.js +23 -21
  18. package/dist/adapters/explain.js +11 -11
  19. package/dist/adapters/host-cli/index.js +44 -5
  20. package/dist/adapters/host-cli/transport.js +7 -5
  21. package/dist/adapters/image/bytes.js +2 -2
  22. package/dist/adapters/image/codex-output.js +1 -1
  23. package/dist/adapters/image/codex.js +87 -22
  24. package/dist/adapters/image/fal.js +13 -5
  25. package/dist/adapters/image/google.js +11 -8
  26. package/dist/adapters/image/openai.js +14 -7
  27. package/dist/adapters/image/reference.js +34 -0
  28. package/dist/adapters/image/replicate.js +7 -2
  29. package/dist/adapters/image/video.js +2 -2
  30. package/dist/adapters/key-probes.js +36 -0
  31. package/dist/adapters/llm/claude-code.js +168 -32
  32. package/dist/adapters/llm/cli-login-error.js +1 -1
  33. package/dist/adapters/llm/codex-limits.js +157 -0
  34. package/dist/adapters/llm/codex.js +99 -12
  35. package/dist/adapters/llm/document-workspace.js +2 -2
  36. package/dist/adapters/llm/gemini-workspace.js +1 -1
  37. package/dist/adapters/llm/gemini.js +16 -3
  38. package/dist/adapters/llm/image-workspace.js +58 -0
  39. package/dist/adapters/llm/openrouter.js +11 -3
  40. package/dist/adapters/llm/run-cli.js +12 -7
  41. package/dist/adapters/tts/cartesia.js +10 -0
  42. package/dist/adapters/tts/elevenlabs.js +31 -9
  43. package/dist/adapters/tts/inworld-async.js +9 -5
  44. package/dist/adapters/tts/inworld.js +17 -3
  45. package/dist/adapters/tts/openai.js +3 -0
  46. package/dist/adapters/tts/voice-languages.js +80 -0
  47. package/dist/assets/fonts/noto/NotoSans-Regular.ttf +0 -0
  48. package/dist/assets/fonts/noto/NotoSansArabic-Regular.ttf +0 -0
  49. package/dist/assets/fonts/noto/NotoSansDevanagari-Regular.ttf +0 -0
  50. package/dist/assets/fonts/noto/NotoSansHebrew-Regular.ttf +0 -0
  51. package/dist/assets/fonts/noto/NotoSansThai-Regular.ttf +0 -0
  52. package/dist/assets/fonts/noto/OFL.txt +93 -0
  53. package/dist/assets/fonts/noto/SOURCE.txt +18 -0
  54. package/dist/assets/sample/sample-audiobook.tar +0 -0
  55. package/dist/assets/sample/sample-podcast.tar +0 -0
  56. package/dist/assets/sample/sample-project.tar +0 -0
  57. package/dist/catalog/merge.js +132 -0
  58. package/dist/catalog/registry.js +13 -13
  59. package/dist/catalog/schema.js +2 -0
  60. package/dist/catalog/store.js +147 -8
  61. package/dist/compose.yaml +71 -0
  62. package/dist/edge/autostart/docker-record.js +32 -0
  63. package/dist/edge/autostart/docker.js +50 -0
  64. package/dist/edge/autostart/index.js +78 -0
  65. package/dist/edge/autostart/launcher.js +176 -0
  66. package/dist/edge/autostart/model.js +3 -0
  67. package/dist/edge/autostart/native.js +210 -0
  68. package/dist/edge/autostart/prompt.js +66 -0
  69. package/dist/edge/autostart/service.js +81 -0
  70. package/dist/edge/cli.js +57 -51
  71. package/dist/edge/docker-install/activation.js +56 -0
  72. package/dist/edge/docker-install/apply.js +458 -0
  73. package/dist/edge/docker-install/engine.js +362 -0
  74. package/dist/edge/docker-install/run.js +154 -0
  75. package/dist/edge/docker-install/state.js +146 -0
  76. package/dist/edge/{docker-projects → docker-install}/volume.js +1 -5
  77. package/dist/edge/docker.js +14 -22
  78. package/dist/edge/host-cli.js +1 -1
  79. package/dist/edge/http/actions.js +32 -10
  80. package/dist/edge/http/app.js +58 -2
  81. package/dist/edge/http/auditions.js +70 -0
  82. package/dist/edge/http/autostart.js +37 -0
  83. package/dist/edge/http/channel-memory.js +92 -0
  84. package/dist/edge/http/channels.js +128 -0
  85. package/dist/edge/http/entries.js +22 -3
  86. package/dist/edge/http/files.js +1 -1
  87. package/dist/edge/http/folder-location.js +1 -1
  88. package/dist/edge/http/home.js +39 -0
  89. package/dist/edge/http/host-cli.js +29 -5
  90. package/dist/edge/http/narration-peaks.js +76 -0
  91. package/dist/edge/http/onboarding.js +152 -0
  92. package/dist/edge/http/open-folder.js +1 -1
  93. package/dist/edge/http/patch-notes.js +45 -0
  94. package/dist/edge/http/planning.js +4 -2
  95. package/dist/edge/http/project-create.js +87 -0
  96. package/dist/edge/http/projects.js +31 -99
  97. package/dist/edge/http/prompts.js +40 -3
  98. package/dist/edge/http/pronunciations.js +23 -4
  99. package/dist/edge/http/providers.js +86 -0
  100. package/dist/edge/http/reviews.js +121 -0
  101. package/dist/edge/http/revision-files.js +1 -1
  102. package/dist/edge/http/revisions.js +1 -1
  103. package/dist/edge/http/run-cost.js +25 -0
  104. package/dist/edge/http/schedules.js +111 -6
  105. package/dist/edge/http/settings.js +84 -6
  106. package/dist/edge/http/storage-files.js +50 -0
  107. package/dist/edge/http/storage.js +29 -2
  108. package/dist/edge/http/studio.js +207 -0
  109. package/dist/edge/http/style-preview.js +100 -0
  110. package/dist/edge/http/subtitles.js +1 -1
  111. package/dist/edge/http/trash.js +69 -0
  112. package/dist/edge/http/update.js +24 -3
  113. package/dist/edge/http/whats-new.js +15 -0
  114. package/dist/edge/http/youtube-edits.js +83 -0
  115. package/dist/edge/native-update.js +62 -0
  116. package/dist/host-cli/open-folder.js +10 -10
  117. package/dist/host-cli/runtime.js +9 -1
  118. package/dist/kernel/db/migrations/0025-automatic-reviews.sql +40 -0
  119. package/dist/kernel/db/migrations/0026-schedule-topic-generation.sql +23 -0
  120. package/dist/kernel/db/migrations/0027-run-cost.sql +58 -0
  121. package/dist/kernel/db/migrations/0030-automatic-retries.sql +18 -0
  122. package/dist/kernel/db/migrations/0031-prompt-history-and-description-edits.sql +33 -0
  123. package/dist/kernel/db/migrations/0032-channels.sql +65 -0
  124. package/dist/kernel/db/migrations/0034-script-prompts.sql +23 -0
  125. package/dist/kernel/db/migrations/0035-narration-aliases.sql +13 -0
  126. package/dist/kernel/db/migrations/0037-project-uploads.sql +8 -0
  127. package/dist/kernel/db/migrations/0038-voice-languages.sql +7 -0
  128. package/dist/kernel/db/migrations/0039-channel-essentials.sql +64 -0
  129. package/dist/kernel/db/migrations/0040-standalone-usage.sql +35 -0
  130. package/dist/kernel/paths.js +27 -6
  131. package/dist/kernel/pipeline.js +4 -1
  132. package/dist/kernel/ports/host-cli.js +122 -14
  133. package/dist/kernel/ports/key-probe.js +1 -0
  134. package/dist/kernel/ports/languages.js +157 -0
  135. package/dist/kernel/ports/model.js +5 -2
  136. package/dist/kernel/ports/narration-aliases.js +60 -0
  137. package/dist/kernel/ports/plan-limits.js +36 -0
  138. package/dist/kernel/runner/attempt.js +17 -8
  139. package/dist/kernel/runner/audition.js +36 -0
  140. package/dist/kernel/runner/graph.js +24 -5
  141. package/dist/kernel/runner/index.js +35 -5
  142. package/dist/kernel/runner/meter.js +1 -0
  143. package/dist/kernel/runner/providers.js +142 -7
  144. package/dist/kernel/runner/retry-policy.js +43 -0
  145. package/dist/kernel/runner/standalone.js +134 -0
  146. package/dist/kernel/runner/work-authority.js +28 -2
  147. package/dist/main.js +272 -16
  148. package/dist/patch-notes/0.4.2-to-1.0.md +93 -0
  149. package/dist/patch-notes/1.0-to-2.0.md +74 -0
  150. package/dist/patch-notes/2.0-to-3.0.md +257 -0
  151. package/dist/patch-notes/3.0.0.md +70 -0
  152. package/dist/patch-notes/index.json +26 -0
  153. package/dist/slices/admission/model.js +27 -0
  154. package/dist/slices/admission/repo.js +24 -4
  155. package/dist/slices/admission/rules.js +79 -1
  156. package/dist/slices/admission/schema.js +58 -1
  157. package/dist/slices/admission/short-mode.js +47 -0
  158. package/dist/slices/admission/start.js +41 -5
  159. package/dist/slices/article/continuation.js +2 -2
  160. package/dist/slices/article/provided-entries.js +2 -2
  161. package/dist/slices/article/run.js +6 -2
  162. package/dist/slices/article/segments.js +3 -3
  163. package/dist/slices/backups/folder.js +14 -6
  164. package/dist/slices/batch/index.js +7 -2
  165. package/dist/slices/cancel/index.js +1 -1
  166. package/dist/slices/channels/cast-images.js +110 -0
  167. package/dist/slices/channels/cast-match.js +32 -0
  168. package/dist/slices/channels/images.js +19 -0
  169. package/dist/slices/channels/model.js +5 -0
  170. package/dist/slices/channels/rebrand.js +100 -0
  171. package/dist/slices/channels/repo.js +218 -0
  172. package/dist/slices/channels/runs.js +129 -0
  173. package/dist/slices/channels/schema.js +129 -0
  174. package/dist/slices/channels/service.js +180 -0
  175. package/dist/slices/channels/studio-csv.js +110 -0
  176. package/dist/slices/channels/videos.js +120 -0
  177. package/dist/slices/checkpoints/recovery.js +10 -0
  178. package/dist/slices/control/index.js +2 -1
  179. package/dist/slices/document/fonts.js +1 -1
  180. package/dist/slices/document/render.js +1 -1
  181. package/dist/slices/episodes/related.js +90 -0
  182. package/dist/slices/episodes/repo.js +60 -0
  183. package/dist/slices/episodes/service.js +58 -0
  184. package/dist/slices/episodes/summarize.js +170 -0
  185. package/dist/slices/estimate/index.js +109 -28
  186. package/dist/slices/estimate/requests.js +60 -2
  187. package/dist/slices/fixes/rules.js +73 -0
  188. package/dist/slices/fonts/coverage.js +82 -0
  189. package/dist/slices/fonts/sfnt.js +74 -0
  190. package/dist/slices/images/run.js +8 -5
  191. package/dist/slices/images/scale.js +84 -0
  192. package/dist/slices/library/diff.js +122 -0
  193. package/dist/slices/library/history.js +100 -0
  194. package/dist/slices/library/model.js +3 -0
  195. package/dist/slices/library/photorealistic.js +49 -0
  196. package/dist/slices/library/repo.js +19 -14
  197. package/dist/slices/library/save.js +62 -12
  198. package/dist/slices/library/slots.js +7 -1
  199. package/dist/slices/library/used-by.js +153 -0
  200. package/dist/slices/loudness/level-pieces.js +41 -0
  201. package/dist/slices/loudness/loudnorm.js +164 -0
  202. package/dist/slices/loudness/model.js +153 -0
  203. package/dist/slices/model-upkeep/model.js +10 -0
  204. package/dist/slices/model-upkeep/switch.js +217 -0
  205. package/dist/slices/model-upkeep/usage.js +183 -0
  206. package/dist/slices/narration/aliases-library.js +31 -0
  207. package/dist/slices/narration/aliases-schema.js +53 -0
  208. package/dist/slices/narration/aliases.js +49 -0
  209. package/dist/slices/narration/blocks.js +352 -0
  210. package/dist/slices/narration/chunk.js +19 -7
  211. package/dist/slices/narration/concat.js +2 -2
  212. package/dist/slices/narration/describe.js +45 -0
  213. package/dist/slices/narration/pauses-model.js +63 -0
  214. package/dist/slices/narration/pauses.js +269 -0
  215. package/dist/slices/narration/peaks-model.js +1 -0
  216. package/dist/slices/narration/peaks.js +94 -0
  217. package/dist/slices/narration/preparation.js +21 -3
  218. package/dist/slices/narration/pronunciation-chunks.js +3 -3
  219. package/dist/slices/narration/pronunciation.js +72 -25
  220. package/dist/slices/narration/run.js +6 -6
  221. package/dist/slices/narration/shared-glossary.js +1 -1
  222. package/dist/slices/narration/steering.js +15 -4
  223. package/dist/slices/notifications/notifier.js +21 -3
  224. package/dist/slices/notifications/rules.js +22 -1
  225. package/dist/slices/onboarding/copy.js +143 -0
  226. package/dist/slices/onboarding/first-run.js +46 -0
  227. package/dist/slices/onboarding/install.js +119 -0
  228. package/dist/slices/onboarding/model.js +64 -0
  229. package/dist/slices/onboarding/packs.js +491 -0
  230. package/dist/slices/onboarding/quick-short.js +129 -0
  231. package/dist/slices/onboarding/sample.js +132 -0
  232. package/dist/slices/onboarding/state.js +68 -0
  233. package/dist/slices/onboarding/template.js +73 -0
  234. package/dist/slices/patch-notes/library.js +73 -0
  235. package/dist/slices/patch-notes/seen.js +77 -0
  236. package/dist/slices/play-drafts/convert.js +125 -6
  237. package/dist/slices/play-drafts/readiness.js +3 -0
  238. package/dist/slices/play-drafts/repo.js +2 -0
  239. package/dist/slices/play-drafts/review-inputs.js +20 -3
  240. package/dist/slices/play-drafts/schema.js +89 -0
  241. package/dist/slices/play-drafts/service.js +4 -1
  242. package/dist/slices/play-drafts/start.js +43 -18
  243. package/dist/slices/project-templates/from-project.js +50 -4
  244. package/dist/slices/project-templates/one-off.js +26 -0
  245. package/dist/slices/project-templates/repo.js +2 -1
  246. package/dist/slices/project-templates/service.js +15 -4
  247. package/dist/slices/project-templates/setup.js +18 -2
  248. package/dist/slices/rebuild/admission-repo.js +1 -1
  249. package/dist/slices/rebuild/model.js +6 -0
  250. package/dist/slices/rebuild/preview-details.js +46 -3
  251. package/dist/slices/rebuild/preview-plan.js +13 -6
  252. package/dist/slices/rebuild/recipe-audio-parts.js +40 -7
  253. package/dist/slices/rebuild/recipe-audio.js +66 -12
  254. package/dist/slices/rebuild/recipe-build.js +8 -4
  255. package/dist/slices/rebuild/recipe-cast.js +18 -0
  256. package/dist/slices/rebuild/recipe-checkpoints.js +11 -9
  257. package/dist/slices/rebuild/recipe-describe.js +93 -0
  258. package/dist/slices/rebuild/recipe-edit.js +13 -1
  259. package/dist/slices/rebuild/recipe-exports.js +62 -8
  260. package/dist/slices/rebuild/recipe-input-schema.js +35 -0
  261. package/dist/slices/rebuild/recipe-legacy.js +9 -0
  262. package/dist/slices/rebuild/recipe-loudness.js +45 -0
  263. package/dist/slices/rebuild/recipe-model.js +25 -6
  264. package/dist/slices/rebuild/recipe-pauses.js +7 -0
  265. package/dist/slices/rebuild/recipe-preparation.js +3 -3
  266. package/dist/slices/rebuild/recipe-provider-choice.js +11 -1
  267. package/dist/slices/rebuild/recipe-reference.js +15 -1
  268. package/dist/slices/rebuild/recipe-reviews.js +117 -0
  269. package/dist/slices/rebuild/recipe-save.js +4 -1
  270. package/dist/slices/rebuild/recipe-shorts.js +23 -3
  271. package/dist/slices/rebuild/recipe-text.js +114 -19
  272. package/dist/slices/rebuild/recipe-validation.js +14 -2
  273. package/dist/slices/rebuild/recipe-visual.js +96 -33
  274. package/dist/slices/rebuild/recipe-voices.js +286 -0
  275. package/dist/slices/rebuild/recipe-work.js +77 -6
  276. package/dist/slices/rebuild/recipe-youtube.js +2 -0
  277. package/dist/slices/rebuild/recovery-conflict.js +7 -3
  278. package/dist/slices/rebuild/recovery-model.js +3 -0
  279. package/dist/slices/rebuild/recovery-selection.js +28 -2
  280. package/dist/slices/rebuild/recovery.js +35 -17
  281. package/dist/slices/rebuild/repo.js +3 -3
  282. package/dist/slices/rebuild/retry.js +18 -0
  283. package/dist/slices/rebuild/review-redo.js +110 -0
  284. package/dist/slices/rebuild/runtime-actions.js +5 -0
  285. package/dist/slices/rebuild/runtime-admission.js +4 -1
  286. package/dist/slices/rebuild/runtime-animate.js +4 -4
  287. package/dist/slices/rebuild/runtime-article.js +5 -5
  288. package/dist/slices/rebuild/runtime-document.js +4 -4
  289. package/dist/slices/rebuild/runtime-export-bed.js +43 -0
  290. package/dist/slices/rebuild/runtime-export-edit.js +70 -11
  291. package/dist/slices/rebuild/runtime-export-inputs.js +52 -10
  292. package/dist/slices/rebuild/runtime-export-short.js +85 -0
  293. package/dist/slices/rebuild/runtime-export.js +84 -19
  294. package/dist/slices/rebuild/runtime-figure-card.js +87 -0
  295. package/dist/slices/rebuild/runtime-image.js +21 -2
  296. package/dist/slices/rebuild/runtime-local.js +148 -24
  297. package/dist/slices/rebuild/runtime-materialize.js +8 -0
  298. package/dist/slices/rebuild/runtime-narration-reuse.js +2 -1
  299. package/dist/slices/rebuild/runtime-narration-text.js +7 -7
  300. package/dist/slices/rebuild/runtime-pictures.js +28 -0
  301. package/dist/slices/rebuild/runtime-piece-label.js +27 -0
  302. package/dist/slices/rebuild/runtime-provider.js +126 -18
  303. package/dist/slices/rebuild/runtime-publication.js +21 -5
  304. package/dist/slices/rebuild/runtime-review.js +237 -0
  305. package/dist/slices/rebuild/runtime-run.js +17 -11
  306. package/dist/slices/rebuild/runtime-shorts.js +44 -17
  307. package/dist/slices/rebuild/runtime-store.js +39 -10
  308. package/dist/slices/rebuild/runtime-subtitles.js +72 -19
  309. package/dist/slices/rebuild/runtime-voices.js +103 -0
  310. package/dist/slices/rebuild/runtime-youtube.js +7 -6
  311. package/dist/slices/rebuild/soften.js +49 -0
  312. package/dist/slices/rebuild/transition-repo.js +9 -2
  313. package/dist/slices/research/run.js +4 -4
  314. package/dist/slices/research/synthesis.js +2 -2
  315. package/dist/slices/reviews/model.js +105 -0
  316. package/dist/slices/reviews/outcome.js +22 -0
  317. package/dist/slices/reviews/repo.js +106 -0
  318. package/dist/slices/reviews/rules.js +63 -0
  319. package/dist/slices/reviews/verdict.js +71 -0
  320. package/dist/slices/reviews/view.js +30 -0
  321. package/dist/slices/revisions/adopt-content.js +5 -2
  322. package/dist/slices/revisions/adopt.js +3 -1
  323. package/dist/slices/revisions/downloads.js +7 -1
  324. package/dist/slices/revisions/image-plan.js +183 -0
  325. package/dist/slices/revisions/mutation-assets.js +10 -0
  326. package/dist/slices/revisions/mutation-cues.js +71 -0
  327. package/dist/slices/revisions/mutation-request.js +2 -1
  328. package/dist/slices/revisions/mutations.js +97 -3
  329. package/dist/slices/revisions/schema.js +11 -1
  330. package/dist/slices/run-cost/limits.js +128 -0
  331. package/dist/slices/run-cost/meter.js +47 -0
  332. package/dist/slices/run-cost/panel.js +234 -0
  333. package/dist/slices/run-cost/pricing.js +92 -0
  334. package/dist/slices/run-cost/week.js +108 -0
  335. package/dist/slices/schedules/agenda.js +158 -0
  336. package/dist/slices/schedules/repo.js +58 -18
  337. package/dist/slices/schedules/scheduler.js +67 -16
  338. package/dist/slices/schedules/schema.js +138 -1
  339. package/dist/slices/schedules/service.js +43 -4
  340. package/dist/slices/schedules/similar.js +90 -0
  341. package/dist/slices/schedules/topic-list.js +163 -0
  342. package/dist/slices/schedules/topics.js +422 -0
  343. package/dist/slices/settings/first-run.js +102 -0
  344. package/dist/slices/settings/health.js +190 -0
  345. package/dist/slices/settings/key-guides.js +128 -0
  346. package/dist/slices/settings/key-test.js +85 -0
  347. package/dist/slices/settings/model.js +6 -1
  348. package/dist/slices/settings/playback.js +15 -0
  349. package/dist/slices/settings/repo.js +17 -3
  350. package/dist/slices/settings/voices.js +64 -4
  351. package/dist/slices/settings/whats-new.js +64 -0
  352. package/dist/slices/shorts/captions.js +9 -0
  353. package/dist/slices/shorts/pick.js +3 -3
  354. package/dist/slices/shorts/prompts.js +1 -1
  355. package/dist/slices/shorts/render.js +54 -5
  356. package/dist/slices/storage/asset-name.js +3 -0
  357. package/dist/slices/storage/backup-export.js +48 -9
  358. package/dist/slices/storage/backup-format.js +38 -1
  359. package/dist/slices/storage/backup-import.js +191 -19
  360. package/dist/slices/storage/delete-project.js +7 -4
  361. package/dist/slices/storage/documents.js +79 -0
  362. package/dist/slices/storage/files-location.js +531 -0
  363. package/dist/slices/storage/layout.js +14 -4
  364. package/dist/slices/storage/model.js +11 -0
  365. package/dist/slices/storage/portable.js +92 -12
  366. package/dist/slices/storage/reconcile.js +14 -1
  367. package/dist/slices/storage/repo.js +4 -1
  368. package/dist/slices/storage/schema.js +11 -0
  369. package/dist/slices/storage/trim.js +146 -0
  370. package/dist/slices/studio/disclosure.js +74 -0
  371. package/dist/slices/studio/model.js +28 -0
  372. package/dist/slices/studio/pack.js +209 -0
  373. package/dist/slices/studio/settings.js +133 -0
  374. package/dist/slices/style-preview/images.js +61 -0
  375. package/dist/slices/style-preview/render.js +161 -0
  376. package/dist/slices/style-preview/schema.js +45 -0
  377. package/dist/slices/style-preview/service.js +93 -0
  378. package/dist/slices/style-preview/settings.js +89 -0
  379. package/dist/slices/subtitles/captions.js +42 -9
  380. package/dist/slices/subtitles/model.js +22 -0
  381. package/dist/slices/subtitles/prepare.js +27 -16
  382. package/dist/slices/subtitles/transcript.js +1 -1
  383. package/dist/slices/thumbnail/run.js +7 -7
  384. package/dist/slices/trash/model.js +25 -0
  385. package/dist/slices/trash/service.js +273 -0
  386. package/dist/slices/uploads/repo.js +20 -0
  387. package/dist/slices/video/ambient-bed-schema.js +15 -0
  388. package/dist/slices/video/ambient-bed.js +95 -0
  389. package/dist/slices/video/ambient-mix.js +72 -0
  390. package/dist/slices/video/audio-inputs.js +2 -2
  391. package/dist/slices/video/cards.js +5 -2
  392. package/dist/slices/video/edit-list.js +23 -2
  393. package/dist/slices/video/edit-settings.js +13 -3
  394. package/dist/slices/video/ffmpeg.js +63 -9
  395. package/dist/slices/video/figure-card.js +492 -0
  396. package/dist/slices/video/figure-spans.js +51 -0
  397. package/dist/slices/video/plan.js +125 -13
  398. package/dist/slices/video/run.js +14 -12
  399. package/dist/slices/video/slideshow.js +39 -4
  400. package/dist/slices/voices/attribution.js +69 -0
  401. package/dist/slices/voices/audio-files.js +82 -0
  402. package/dist/slices/voices/audition.js +25 -0
  403. package/dist/slices/voices/cast.js +56 -0
  404. package/dist/slices/voices/grouping.js +49 -0
  405. package/dist/slices/voices/join.js +35 -0
  406. package/dist/slices/voices/languages.js +25 -0
  407. package/dist/slices/voices/model.js +179 -0
  408. package/dist/slices/voices/palette.js +26 -0
  409. package/dist/slices/voices/panel.js +91 -0
  410. package/dist/slices/voices/portraits.js +55 -0
  411. package/dist/slices/voices/script.js +136 -0
  412. package/dist/slices/voices/timing.js +58 -0
  413. package/dist/slices/youtube/answer.js +1 -1
  414. package/dist/slices/youtube/chapters.js +126 -0
  415. package/dist/slices/youtube/edits-repo.js +69 -0
  416. package/dist/slices/youtube/edits.js +89 -0
  417. package/dist/slices/youtube/placeholders.js +98 -0
  418. package/dist/slices/youtube/transcript.js +11 -4
  419. package/dist/updater/service.js +66 -21
  420. package/dist/web/assets/index-CUA2sUxU.css +1 -0
  421. package/dist/web/assets/index-CX590kE-.js +303 -0
  422. package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  423. package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  424. package/dist/web/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  425. package/dist/web/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  426. package/dist/web/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  427. package/dist/web/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  428. package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  429. package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  430. package/dist/web/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  431. package/dist/web/assets/{pdf-BLF2i8c2.js → pdf-BG8n_uc5.js} +1 -1
  432. package/dist/web/index.html +2 -2
  433. package/package.json +4 -4
  434. package/dist/edge/docker-launch.js +0 -81
  435. package/dist/edge/docker-projects/activation.js +0 -35
  436. package/dist/edge/docker-projects/claims.js +0 -85
  437. package/dist/edge/docker-projects/committed.js +0 -40
  438. package/dist/edge/docker-projects/engine.js +0 -461
  439. package/dist/edge/docker-projects/install.js +0 -254
  440. package/dist/edge/docker-projects/prune.js +0 -66
  441. package/dist/edge/docker-projects/recover.js +0 -100
  442. package/dist/edge/docker-projects/state.js +0 -225
  443. package/dist/web/assets/index-CktpxlJd.js +0 -171
  444. package/dist/web/assets/index-DB-vOnGU.css +0 -1
  445. package/scripts/docker-run.sh +0 -3
  446. /package/dist/edge/{docker-projects → docker-install}/tree.js +0 -0
@@ -0,0 +1,183 @@
1
+ import { z } from "zod";
2
+ import { isVideoModel } from "../../catalog/schema.js";
3
+ import { listProjects, stagesOf } from "../admission/repo.js";
4
+ import { draftRows } from "../play-drafts/repo.js";
5
+ import { documentOf } from "../play-drafts/service.js";
6
+ import { templateById } from "../project-templates/repo.js";
7
+ import { currentRevisionId, revisionById } from "../revisions/repo.js";
8
+ import { isLocalCliProvider } from "../settings/model.js";
9
+ import { usageSlots } from "./model.js";
10
+ export { slotLabels, usageKinds, usageSlots } from "./model.js";
11
+ function familyOf(slot) {
12
+ return slot === "llm" ? "llm" : slot === "audio" ? "tts" : "image";
13
+ }
14
+ // The model choices a Play form (a draft or a template) makes.
15
+ export function formChoices(form) {
16
+ const edit = form.videoEdit;
17
+ return {
18
+ llm: form.llm,
19
+ audio: form.audio,
20
+ images: form.images,
21
+ ...(edit !== undefined && edit.animate !== "off"
22
+ ? { animate: { provider: form.images.provider, model: edit.animateModel } }
23
+ : {}),
24
+ };
25
+ }
26
+ // The same for a project's run configuration.
27
+ export function configChoices(config) {
28
+ const edit = config.videoEdit;
29
+ return {
30
+ ...(config.llm === undefined ? {} : { llm: config.llm }),
31
+ ...(config.audio === undefined ? {} : { audio: config.audio }),
32
+ ...(config.images === undefined ? {} : { images: config.images }),
33
+ ...(edit !== undefined && edit.animate !== "off" && config.images !== undefined
34
+ ? { animate: { provider: config.images.provider, model: edit.animateModel } }
35
+ : {}),
36
+ };
37
+ }
38
+ // Whether a choice names a catalogued model that is no longer offered. Command-line tools
39
+ // list their own models and are checked by the health check instead.
40
+ export function retiredStatus(catalogue, slot, choice) {
41
+ if (choice.provider === "" || choice.model.trim() === "" || isLocalCliProvider(choice.provider))
42
+ return null;
43
+ const row = catalogue[familyOf(slot)].find((model) => model.provider === choice.provider &&
44
+ model.id === choice.model &&
45
+ isVideoModel(model) === (slot === "animate"));
46
+ if (row === undefined)
47
+ return "unlisted";
48
+ if (row.deprecated)
49
+ return "retired";
50
+ return row.enabled ? null : "unlisted";
51
+ }
52
+ // The closest active model from the same provider and list: the one sharing the longest
53
+ // start of its ID (a newer version of the same line), the catalogue's order breaking ties.
54
+ export function suggestReplacement(catalogue, slot, choice) {
55
+ let best = null;
56
+ for (const row of catalogue[familyOf(slot)]) {
57
+ if (row.provider !== choice.provider ||
58
+ row.id === choice.model ||
59
+ !row.enabled ||
60
+ row.deprecated ||
61
+ isVideoModel(row) !== (slot === "animate"))
62
+ continue;
63
+ const score = sharedStart(row.id.toLowerCase(), choice.model.toLowerCase());
64
+ if (best === null || score > best.score)
65
+ best = { id: row.id, name: row.name, score };
66
+ }
67
+ return best === null ? null : { id: best.id, name: best.name };
68
+ }
69
+ function sharedStart(a, b) {
70
+ let index = 0;
71
+ while (index < a.length && index < b.length && a[index] === b[index])
72
+ index++;
73
+ return index;
74
+ }
75
+ function usagesOf(catalogue, base, choices, blocked) {
76
+ const found = [];
77
+ for (const slot of usageSlots) {
78
+ const choice = choices[slot];
79
+ if (choice === undefined)
80
+ continue;
81
+ const why = retiredStatus(catalogue, slot, choice);
82
+ if (why === null)
83
+ continue;
84
+ const replacement = suggestReplacement(catalogue, slot, choice);
85
+ found.push({
86
+ ...base,
87
+ key: `${base.kind}:${base.id}:${slot}`,
88
+ slot,
89
+ provider: choice.provider,
90
+ model: choice.model,
91
+ why,
92
+ replacement,
93
+ blocked: blocked ??
94
+ (replacement === null
95
+ ? "This provider has no other model to switch to. Choose another provider for this step and save."
96
+ : null),
97
+ });
98
+ }
99
+ return found;
100
+ }
101
+ const scheduleRow = z.object({
102
+ id: z.string(),
103
+ name: z.string(),
104
+ template_id: z.string(),
105
+ template_version: z.number(),
106
+ });
107
+ const headRow = z.object({ head_version: z.number() });
108
+ export function choiceSites(db) {
109
+ const sites = [];
110
+ const heads = new Map();
111
+ for (const row of db.prepare("SELECT id FROM project_templates ORDER BY id").all()) {
112
+ const id = z.object({ id: z.string() }).parse(row).id;
113
+ const template = templateById(db, id);
114
+ if (template === undefined)
115
+ continue;
116
+ heads.set(id, template.version);
117
+ sites.push({
118
+ kind: "template",
119
+ id,
120
+ name: template.name,
121
+ choices: formChoices(template.document.form),
122
+ blocked: null,
123
+ });
124
+ }
125
+ for (const raw of db
126
+ .prepare("SELECT id,name,template_id,template_version FROM schedules WHERE deleted_at IS NULL AND status IN ('active','paused') ORDER BY lower(name),id")
127
+ .all()) {
128
+ const row = scheduleRow.parse(raw);
129
+ const template = templateById(db, row.template_id, row.template_version);
130
+ if (template === undefined)
131
+ continue;
132
+ const head = heads.get(row.template_id) ??
133
+ headRow.parse(db.prepare("SELECT head_version FROM project_templates WHERE id=?").get(row.template_id)).head_version;
134
+ sites.push({
135
+ kind: "schedule",
136
+ id: row.id,
137
+ name: row.name,
138
+ choices: formChoices(template.document.form),
139
+ blocked: head === row.template_version
140
+ ? null
141
+ : `This schedule runs an older version of the template "${template.name}". In Library → Schedules, choose Edit, pick the template again so it runs the latest version, then save.`,
142
+ });
143
+ }
144
+ for (const row of draftRows(db)) {
145
+ if (row.state !== "active")
146
+ continue;
147
+ const document = documentOf(row);
148
+ if (document === null)
149
+ continue;
150
+ sites.push({
151
+ kind: "draft",
152
+ id: row.id,
153
+ name: row.title.trim() === "" ? "Untitled draft" : row.title,
154
+ choices: formChoices(document.form),
155
+ blocked: null,
156
+ });
157
+ }
158
+ for (const project of listProjects(db)) {
159
+ const stages = stagesOf(db, project.id);
160
+ // Only a project with steps still to run will use its models again.
161
+ if (!stages.some((stage) => ["pending", "running", "failed", "canceled"].includes(stage.state)))
162
+ continue;
163
+ const revisionId = currentRevisionId(db, project.id);
164
+ const config = revisionId === undefined
165
+ ? project.config
166
+ : (revisionById(db, project.id, revisionId)?.config ?? project.config);
167
+ sites.push({
168
+ kind: "project",
169
+ id: project.id,
170
+ name: project.title,
171
+ choices: configChoices(config),
172
+ blocked: stages.some((stage) => stage.state === "running")
173
+ ? "This project is running. Wait for it to finish or pause it, then switch."
174
+ : null,
175
+ });
176
+ }
177
+ return sites;
178
+ }
179
+ // Every template, schedule, draft and unfinished project that still picks a retired or
180
+ // unlisted model, with the suggested replacement. Nothing is changed here.
181
+ export function retiredModelUsage(db, catalogue) {
182
+ return choiceSites(db).flatMap((site) => usagesOf(catalogue, { kind: site.kind, id: site.id, name: site.name }, site.choices, site.blocked));
183
+ }
@@ -0,0 +1,31 @@
1
+ import { transact } from "../../kernel/db/tx.js";
2
+ import { aliasProblems, narrationAliasSchema } from "./aliases-schema.js";
3
+ export function listNarrationAliases(db) {
4
+ return db
5
+ .prepare("SELECT written,spoken,whole_word,case_sensitive FROM narration_aliases ORDER BY position,id")
6
+ .all()
7
+ .flatMap((row) => {
8
+ // A row edited by hand into something the editor can't save is left out, not fatal.
9
+ const parsed = narrationAliasSchema.safeParse({
10
+ written: row.written,
11
+ spoken: row.spoken,
12
+ wholeWord: row.whole_word === 1,
13
+ caseSensitive: row.case_sensitive === 1,
14
+ });
15
+ return parsed.success ? [parsed.data] : [];
16
+ });
17
+ }
18
+ export function saveNarrationAliases(deps, rows) {
19
+ const fields = aliasProblems(rows);
20
+ if (fields.length > 0)
21
+ return { ok: false, fields };
22
+ const aliases = rows.map((row) => narrationAliasSchema.parse(row));
23
+ const now = deps.clock.now().toISOString();
24
+ transact(deps.db, () => {
25
+ deps.db.prepare("DELETE FROM narration_aliases").run();
26
+ const insert = deps.db.prepare("INSERT INTO narration_aliases(id,position,written,spoken,whole_word,case_sensitive,updated_at) VALUES (?,?,?,?,?,?,?)");
27
+ for (const [position, alias] of aliases.entries())
28
+ insert.run(deps.ids.next(), position, alias.written, alias.spoken, alias.wholeWord ? 1 : 0, alias.caseSensitive ? 1 : 0, now);
29
+ });
30
+ return { ok: true, aliases };
31
+ }
@@ -0,0 +1,53 @@
1
+ import { z } from "zod";
2
+ import { aliasCountMax, aliasSpokenMax, aliasWrittenMax, } from "../../kernel/ports/narration-aliases.js";
3
+ // One alias as a project config and Library → Aliases store it.
4
+ export const narrationAliasSchema = z
5
+ .object({
6
+ written: z.string().trim().min(1).max(aliasWrittenMax),
7
+ spoken: z.string().trim().min(1).max(aliasSpokenMax),
8
+ wholeWord: z.boolean(),
9
+ caseSensitive: z.boolean(),
10
+ })
11
+ .strict()
12
+ .readonly();
13
+ export const narrationAliasesSchema = z.array(narrationAliasSchema).max(aliasCountMax).readonly();
14
+ export function aliasProblems(rows) {
15
+ const problems = [];
16
+ if (rows.length > aliasCountMax)
17
+ problems.push({
18
+ field: "aliases",
19
+ message: `Keep at most ${aliasCountMax} aliases; remove some, then Save.`,
20
+ });
21
+ const seen = new Map();
22
+ for (const [index, raw] of rows.entries()) {
23
+ const row = index + 1;
24
+ const parsed = narrationAliasSchema.safeParse(raw);
25
+ if (!parsed.success) {
26
+ const written = z.object({ written: z.string() }).safeParse(raw);
27
+ const spoken = z.object({ spoken: z.string() }).safeParse(raw);
28
+ problems.push({
29
+ field: `aliases.${index}`,
30
+ message: !written.success || written.data.written.trim() === ""
31
+ ? `Alias ${row}: write the word or phrase as it appears in the article.`
32
+ : !spoken.success || spoken.data.spoken.trim() === ""
33
+ ? `Alias ${row}: write how the narrator should say it.`
34
+ : `Alias ${row}: keep the written form under ${aliasWrittenMax} characters and the spoken form under ${aliasSpokenMax}.`,
35
+ });
36
+ continue;
37
+ }
38
+ const key = aliasKey(parsed.data);
39
+ const earlier = seen.get(key);
40
+ if (earlier !== undefined)
41
+ problems.push({
42
+ field: `aliases.${index}`,
43
+ message: `Alias ${row}: alias ${earlier} already covers the same written form; keep one of them.`,
44
+ });
45
+ else
46
+ seen.set(key, row);
47
+ }
48
+ return problems;
49
+ }
50
+ function aliasKey(alias) {
51
+ const written = alias.written.trim().replace(/\s+/gu, " ");
52
+ return alias.caseSensitive ? `=${written}` : `~${written.toLocaleLowerCase("en")}`;
53
+ }
@@ -0,0 +1,49 @@
1
+ import { aliasMatches, } from "../../kernel/ports/narration-aliases.js";
2
+ // Narration aliases ride the same rails as the Pronunciation Glossary: a span of the clean
3
+ // narration text with what the voice gets instead. The clean text (what the transcript and
4
+ // captions show) is untouched. A span never holds whitespace, so an alias whose written form
5
+ // has several words puts all of the spoken form on its first word and nothing on the rest.
6
+ export function aliasSpans(source, aliases) {
7
+ const spans = [];
8
+ for (const match of aliasMatches(source, aliases))
9
+ for (const [index, word] of Array.from(source.slice(match.start, match.end).matchAll(/\S+/gu)).entries()) {
10
+ const start = match.start + word.index;
11
+ spans.push({ start, end: start + word[0].length, text: index === 0 ? match.spoken : "" });
12
+ }
13
+ return spans;
14
+ }
15
+ // An alias wins over a glossary pronunciation of the same words: it says what to read, and the
16
+ // glossary only how to read what is written.
17
+ export function withAliasSpans(glossary, aliases) {
18
+ if (aliases.length === 0)
19
+ return glossary;
20
+ const kept = glossary.filter((span) => !aliases.some((alias) => alias.start < span.end && span.start < alias.end));
21
+ return [...kept, ...aliases].sort((a, b) => a.start - b.start);
22
+ }
23
+ // The sentences Narration Preparation reads, with the aliases already applied, so its
24
+ // delivery cues fit what will actually be said. The sentences are cut from the clean text
25
+ // first, so their numbers stay the ones the cues are checked and placed against.
26
+ export function aliasedSentences(sentences, matches) {
27
+ if (matches.length === 0)
28
+ return sentences;
29
+ let offset = 0;
30
+ return sentences.map((sentence) => {
31
+ const start = offset;
32
+ const end = start + sentence.text.length;
33
+ offset = end;
34
+ let text = "";
35
+ let at = start;
36
+ for (const match of matches) {
37
+ if (match.end <= start || match.start >= end)
38
+ continue;
39
+ if (match.start >= at)
40
+ text += sentence.text.slice(at - start, match.start - start);
41
+ // The spoken form goes with the sentence the written form starts in.
42
+ if (match.start >= start)
43
+ text += match.spoken;
44
+ at = Math.max(at, Math.min(match.end, end));
45
+ }
46
+ text += sentence.text.slice(at - start);
47
+ return { sentence: sentence.sentence, text };
48
+ });
49
+ }
@@ -0,0 +1,352 @@
1
+ import { remark } from "remark";
2
+ import remarkGfm from "remark-gfm";
3
+ // The article as it will be spoken, block by block, for "Describe tables and figures in the
4
+ // narration". Flattening the Markdown (`article/plain.ts`) keeps the words of prose but has
5
+ // nothing to say about a table, a picture, an equation or a program: it drops them or reads
6
+ // their bare cells. Here the document is walked instead, and every block a listener cannot
7
+ // follow read aloud is kept whole for the text model to describe in a sentence or four
8
+ // (`narration/describe.ts`); everything else becomes plain spoken text.
9
+ //
10
+ // Deterministic: the same Markdown always gives the same blocks, so a description is cached by
11
+ // its block and an unchanged article never asks for one again. The article's end matter (the
12
+ // sources and the pronunciation glossary) is cut off before this runs (`article/split.ts`), as
13
+ // it is for the flattened text, so neither is ever narrated.
14
+ export const describedKinds = ["table", "figure", "diagram", "math", "code"];
15
+ // Private-use characters around an equation's number while the document is parsed, so an
16
+ // underscore or asterisk inside an equation is never read as emphasis.
17
+ const open = "\uE000";
18
+ const close = "\uE001";
19
+ const placeholder = /\uE000(\d+)\uE001/g;
20
+ export function narrationBlocks(markdown, options = {}) {
21
+ const { text: protectedText, math } = protectMath(markdown);
22
+ const tree = remark().use(remarkGfm).parse(protectedText);
23
+ const walker = new Walker(protectedText, math, options);
24
+ walker.blocks(tree.children);
25
+ return walker.out;
26
+ }
27
+ export function describedBlocks(blocks) {
28
+ return blocks.filter((block) => block.kind !== "text");
29
+ }
30
+ // The narration: the text blocks as they are and each described block as its spoken passage,
31
+ // one paragraph each, in the flattened text's own shape. Null while any passage is unknown.
32
+ export function spokenNarration(blocks, passage) {
33
+ const paragraphs = [];
34
+ for (const block of blocks) {
35
+ const text = block.kind === "text" ? block.text : passage(block);
36
+ if (text === null)
37
+ return null;
38
+ const clean = text.trim();
39
+ if (clean !== "")
40
+ paragraphs.push(clean);
41
+ }
42
+ return paragraphs.length === 0 ? "" : `${paragraphs.join("\n\n")}\n`;
43
+ }
44
+ function protectMath(markdown) {
45
+ // Code keeps its dollars: a shell variable is not an equation.
46
+ const code = [];
47
+ const visit = (node) => {
48
+ if ((node.type === "code" || node.type === "inlineCode") && node.position) {
49
+ code.push([node.position.start.offset ?? 0, node.position.end.offset ?? 0]);
50
+ return;
51
+ }
52
+ if ("children" in node)
53
+ for (const child of node.children)
54
+ visit(child);
55
+ };
56
+ visit(remark().use(remarkGfm).parse(markdown));
57
+ const math = [];
58
+ const pattern = /\$\$([\s\S]+?)\$\$|\\\[([\s\S]+?)\\\]|\\\(([\s\S]+?)\\\)|(?<![\\$\w])\$(?![\s$])([^$\n]+?)(?<![\s\\])\$(?![\w$])/g;
59
+ const text = markdown.replace(pattern, (written, a, b, c, d, at) => {
60
+ if (code.some(([from, to]) => at < to && at + written.length > from))
61
+ return written;
62
+ const tex = String(a ?? b ?? c ?? d ?? "").trim();
63
+ math.push({ tex, written, display: a !== undefined || b !== undefined });
64
+ return `${open}${String(math.length - 1)}${close}`;
65
+ });
66
+ return { text, math };
67
+ }
68
+ class Walker {
69
+ markdown;
70
+ math;
71
+ options;
72
+ out = [];
73
+ section = null;
74
+ described = 0;
75
+ constructor(markdown, math, options) {
76
+ this.markdown = markdown;
77
+ this.math = math;
78
+ this.options = options;
79
+ }
80
+ blocks(nodes) {
81
+ for (let at = 0; at < nodes.length; at++) {
82
+ const node = nodes[at];
83
+ if (node === undefined)
84
+ continue;
85
+ switch (node.type) {
86
+ case "heading": {
87
+ const text = this.spoken(node.children).replace(placeholder, (_all, n) => {
88
+ const tex = this.math[Number(n)]?.tex ?? "";
89
+ return readable(tex) ?? tex;
90
+ });
91
+ this.section = text.trim() || this.section;
92
+ this.text(text);
93
+ break;
94
+ }
95
+ case "paragraph": {
96
+ if (node.children.some((child) => child.type === "image")) {
97
+ // A caption or legend written under the picture belongs to it.
98
+ const captions = [];
99
+ while (captions.length < 2) {
100
+ const next = nodes[at + captions.length + 1];
101
+ if (next?.type !== "paragraph" || !isCaption(next, this.markdown))
102
+ break;
103
+ captions.push(next);
104
+ }
105
+ at += captions.length;
106
+ this.figure(node, captions);
107
+ break;
108
+ }
109
+ this.paragraph(node);
110
+ break;
111
+ }
112
+ case "list":
113
+ this.list(node);
114
+ break;
115
+ case "blockquote":
116
+ this.quote(node);
117
+ break;
118
+ case "table":
119
+ this.describe("table", this.source(node));
120
+ break;
121
+ case "code":
122
+ this.code(node);
123
+ break;
124
+ case "html":
125
+ this.html(node);
126
+ break;
127
+ // A footnote's text, a link definition, a rule and front matter are never spoken.
128
+ default:
129
+ break;
130
+ }
131
+ }
132
+ }
133
+ text(text) {
134
+ const clean = tidy(text);
135
+ if (clean !== "")
136
+ this.out.push({ kind: "text", text: clean });
137
+ }
138
+ describe(kind, source, extra = {}) {
139
+ this.described += 1;
140
+ this.out.push({
141
+ kind,
142
+ index: this.described,
143
+ source: source.trim(),
144
+ section: this.section,
145
+ ...(extra.image === undefined ? {} : { image: extra.image }),
146
+ ...(extra.lang === undefined ? {} : { lang: extra.lang }),
147
+ ...(extra.inline === undefined ? {} : { inline: extra.inline }),
148
+ });
149
+ }
150
+ paragraph(node) {
151
+ const raw = this.spoken(node.children);
152
+ const only = /^\s*\uE000(\d+)\uE001\s*$/.exec(raw);
153
+ const alone = only === null ? undefined : this.math[Number(only[1])];
154
+ if (alone !== undefined) {
155
+ this.describe("math", alone.written);
156
+ return;
157
+ }
158
+ const spans = [...raw.matchAll(placeholder)].map((match) => this.math[Number(match[1])]);
159
+ if (spans.some((span) => span === undefined || span.display || readable(span.tex) === null)) {
160
+ // Said in words by the model, the rest of the paragraph with it.
161
+ this.describe("math", this.source(node), { inline: true });
162
+ return;
163
+ }
164
+ this.text(raw.replace(placeholder, (_all, n) => readable(this.math[Number(n)]?.tex ?? "") ?? ""));
165
+ }
166
+ figure(node, captions) {
167
+ const image = node.children.find((child) => child.type === "image");
168
+ const lines = [];
169
+ for (const child of node.children)
170
+ if (child.type === "image" && child.alt?.trim())
171
+ lines.push(`Alt text: ${child.alt.trim()}`);
172
+ const beside = tidy(this.restore(this.spoken(node.children.filter((child) => child.type !== "image"))));
173
+ if (beside !== "")
174
+ lines.push(`Caption: ${beside}`);
175
+ for (const caption of captions) {
176
+ const text = tidy(this.restore(this.spoken(caption.children)));
177
+ if (text !== "")
178
+ lines.push(`Caption: ${text}`);
179
+ }
180
+ this.describe("figure", lines.join("\n") || "A picture with no caption.", {
181
+ ...(image?.url ? { image: image.url } : {}),
182
+ });
183
+ }
184
+ html(node) {
185
+ // An article pasted from the web may carry its figures as HTML.
186
+ const img = /<img\b[^>]*>/i.exec(node.value);
187
+ if (img === null)
188
+ return;
189
+ const attribute = (name) => new RegExp(`\\b${name}\\s*=\\s*("([^"]*)"|'([^']*)')`, "i")
190
+ .exec(img[0])
191
+ ?.slice(2)
192
+ .find(Boolean);
193
+ const caption = /<figcaption[^>]*>([\s\S]*?)<\/figcaption>/i.exec(node.value)?.[1];
194
+ const lines = [
195
+ ...(attribute("alt")?.trim() ? [`Alt text: ${attribute("alt")?.trim() ?? ""}`] : []),
196
+ ...(caption?.trim() ? [`Caption: ${tidy(caption.replace(/<[^>]+>/g, " "))}`] : []),
197
+ ];
198
+ const src = attribute("src");
199
+ this.describe("figure", lines.join("\n") || "A picture with no caption.", {
200
+ ...(src ? { image: src } : {}),
201
+ });
202
+ }
203
+ code(node) {
204
+ const lang = node.lang?.trim().toLowerCase() ?? "";
205
+ const source = this.restore(this.source(node));
206
+ const named = lang === "" ? {} : { lang };
207
+ if (lang === "mermaid" ||
208
+ (["", "text", "txt", "ascii", "plain"].includes(lang) && isDrawing(node.value)))
209
+ this.describe("diagram", source, named);
210
+ else if (["math", "latex", "tex", "katex"].includes(lang))
211
+ this.describe("math", source);
212
+ else if (this.options.code !== "skip")
213
+ this.describe("code", source, named);
214
+ }
215
+ quote(node) {
216
+ const start = this.out.length;
217
+ this.blocks(node.children);
218
+ // The quoted words are spoken in quotation marks, which a voice reads as a quote.
219
+ for (let at = start; at < this.out.length; at++) {
220
+ const block = this.out[at];
221
+ if (block?.kind === "text")
222
+ this.out[at] = { kind: "text", text: `“${block.text}”` };
223
+ }
224
+ }
225
+ list(node) {
226
+ const items = [];
227
+ const flush = () => {
228
+ if (items.length > 0)
229
+ this.text(this.sentences(items.splice(0), node.ordered === true));
230
+ };
231
+ for (const item of node.children) {
232
+ const words = [];
233
+ for (const child of item.children) {
234
+ if (child.type === "paragraph" && !child.children.some((one) => one.type === "image")) {
235
+ const raw = this.spoken(child.children);
236
+ if (!raw.includes(open)) {
237
+ words.push(raw);
238
+ continue;
239
+ }
240
+ }
241
+ // A table, a picture or a nested list inside an item is its own block, after the
242
+ // item's words.
243
+ if (words.length > 0)
244
+ items.push(tidy(words.splice(0).join(" ")));
245
+ flush();
246
+ this.blocks([child]);
247
+ }
248
+ if (words.length > 0)
249
+ items.push(tidy(words.join(" ")));
250
+ }
251
+ flush();
252
+ }
253
+ sentences(items, ordered) {
254
+ const english = this.options.language === undefined || this.options.language === "en";
255
+ const kept = items.filter((item) => item !== "");
256
+ if (!english || kept.length < 2)
257
+ return kept.map(sentence).join(" ");
258
+ if (ordered)
259
+ return kept
260
+ .map((item, at) => sentence(`${at === 0 ? "First" : at === kept.length - 1 ? "Finally" : "Then"}, ${item}`))
261
+ .join(" ");
262
+ // Short items read as one sentence, "a, b and c"; longer ones one sentence each.
263
+ if (kept.every((item) => item.split(/\s+/).length <= 5 && !/[.!?;:,—–]/.test(item))) {
264
+ const joined = `${kept.slice(0, -1).join(", ")} and ${kept.at(-1) ?? ""}`;
265
+ return sentence(joined.charAt(0).toUpperCase() + joined.slice(1));
266
+ }
267
+ return kept.map(sentence).join(" ");
268
+ }
269
+ // The words of inline Markdown: link text without its address, no footnote markers, no
270
+ // bare URLs. Equations stay as placeholders for the caller to decide on.
271
+ spoken(nodes) {
272
+ return nodes.map((node) => this.inline(node)).join("");
273
+ }
274
+ inline(node) {
275
+ switch (node.type) {
276
+ case "text":
277
+ case "inlineCode":
278
+ return node.value;
279
+ case "emphasis":
280
+ case "strong":
281
+ case "delete":
282
+ return this.spoken(node.children);
283
+ case "link": {
284
+ const text = this.spoken(node.children);
285
+ return isAddress(text, node.url) ? "" : text;
286
+ }
287
+ case "linkReference": {
288
+ const text = this.spoken(node.children);
289
+ return /^\s*\^?\d+\s*$/.test(text) ? "" : text;
290
+ }
291
+ case "image":
292
+ case "imageReference":
293
+ return node.alt ?? "";
294
+ case "break":
295
+ return "\n";
296
+ default:
297
+ return "";
298
+ }
299
+ }
300
+ source(node) {
301
+ const from = node.position?.start.offset;
302
+ const to = node.position?.end.offset;
303
+ return from === undefined || to === undefined
304
+ ? ""
305
+ : this.restore(this.markdown.slice(from, to));
306
+ }
307
+ restore(text) {
308
+ return text.replace(placeholder, (_all, n) => this.math[Number(n)]?.written ?? "");
309
+ }
310
+ }
311
+ function isCaption(node, markdown) {
312
+ if (node.children.length === 1 && node.children[0]?.type === "emphasis")
313
+ return true;
314
+ const from = node.position?.start.offset ?? 0;
315
+ const head = markdown.slice(from, from + 40).replace(/^[*_\s]+/, "");
316
+ return /^(figure|fig\.|chart|graph|diagram|image|legend|caption|source)\b/i.test(head);
317
+ }
318
+ // An ASCII or box-drawing chart in a plain fence: box characters, or lines that are mostly
319
+ // rules and bars.
320
+ function isDrawing(value) {
321
+ if (/[─│┌┐└┘├┤┬┴┼═║╔╗╚╝▲▼►◄█░▒▓]/.test(value))
322
+ return true;
323
+ const lines = value.split("\n").filter((line) => line.trim() !== "");
324
+ return (lines.filter((line) => /[-+|*]{3,}|[|+].*[|+]/.test(line)).length >=
325
+ Math.max(3, lines.length / 2));
326
+ }
327
+ // A link whose text is its own address, or any bare URL: nothing a listener can use.
328
+ function isAddress(text, url) {
329
+ const clean = text.trim();
330
+ return clean === "" || clean === url || /^(https?:\/\/|www\.|mailto:)/i.test(clean);
331
+ }
332
+ const greek = /\\(alpha|beta|gamma|delta|epsilon|zeta|eta|theta|iota|kappa|lambda|mu|nu|xi|pi|rho|sigma|tau|upsilon|phi|chi|psi|omega|Gamma|Delta|Theta|Lambda|Xi|Pi|Sigma|Phi|Psi|Omega)\b/g;
333
+ // A short inline symbol read as it is written ("$T$" is "T", "$\alpha$" is "alpha"), or null
334
+ // when it is a formula to be said in words.
335
+ function readable(tex) {
336
+ const plain = tex.replace(greek, (_all, name) => name.toLowerCase()).trim();
337
+ return plain.length <= 20 && /^[\p{L}\p{N}\s.,%]*$/u.test(plain) ? plain : null;
338
+ }
339
+ function sentence(text) {
340
+ const clean = text.trim();
341
+ return clean === "" || /[.!?…:;]["”’)]?$/.test(clean) ? clean : `${clean}.`;
342
+ }
343
+ // Footnote markers and bare addresses out, then the spacing they leave behind.
344
+ function tidy(text) {
345
+ return text
346
+ .replace(/\s?\[(?:\^?[\w-]+)(?:\s*[,–-]\s*\^?\d+)*\](?!\()/g, (marker) => /\[\^|\[\d/.test(marker) ? "" : marker)
347
+ .replace(/\s?\b(?:https?:\/\/|www\.)[^\s)]+/g, "")
348
+ .replace(/[ \t]+([.,;:!?])/g, "$1")
349
+ .replace(/[ \t]{2,}/g, " ")
350
+ .replace(/\(\s*\)/g, "")
351
+ .trim();
352
+ }