@gentbajko/slopify 2.4.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 (469) 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 +11 -1
  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 +55 -5
  20. package/dist/adapters/host-cli/transport.js +13 -6
  21. package/dist/adapters/image/bytes.js +2 -2
  22. package/dist/adapters/image/codex-output.js +60 -18
  23. package/dist/adapters/image/codex.js +166 -25
  24. package/dist/adapters/image/fal.js +31 -3
  25. package/dist/adapters/image/google.js +17 -2
  26. package/dist/adapters/image/openai.js +45 -16
  27. package/dist/adapters/image/reference.js +41 -0
  28. package/dist/adapters/image/replicate.js +14 -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-models.js +2 -1
  35. package/dist/adapters/llm/codex-server-models.js +2 -1
  36. package/dist/adapters/llm/codex.js +99 -12
  37. package/dist/adapters/llm/document-workspace.js +2 -2
  38. package/dist/adapters/llm/gemini-workspace.js +1 -1
  39. package/dist/adapters/llm/gemini.js +16 -3
  40. package/dist/adapters/llm/image-workspace.js +58 -0
  41. package/dist/adapters/llm/openrouter.js +11 -3
  42. package/dist/adapters/llm/run-cli.js +26 -9
  43. package/dist/adapters/tts/cartesia.js +10 -0
  44. package/dist/adapters/tts/elevenlabs.js +31 -9
  45. package/dist/adapters/tts/inworld-async.js +9 -5
  46. package/dist/adapters/tts/inworld.js +17 -3
  47. package/dist/adapters/tts/openai.js +3 -0
  48. package/dist/adapters/tts/voice-languages.js +80 -0
  49. package/dist/assets/fonts/noto/NotoSans-Regular.ttf +0 -0
  50. package/dist/assets/fonts/noto/NotoSansArabic-Regular.ttf +0 -0
  51. package/dist/assets/fonts/noto/NotoSansDevanagari-Regular.ttf +0 -0
  52. package/dist/assets/fonts/noto/NotoSansHebrew-Regular.ttf +0 -0
  53. package/dist/assets/fonts/noto/NotoSansThai-Regular.ttf +0 -0
  54. package/dist/assets/fonts/noto/OFL.txt +93 -0
  55. package/dist/assets/fonts/noto/SOURCE.txt +18 -0
  56. package/dist/assets/models.yaml +7 -0
  57. package/dist/assets/sample/sample-audiobook.tar +0 -0
  58. package/dist/assets/sample/sample-podcast.tar +0 -0
  59. package/dist/assets/sample/sample-project.tar +0 -0
  60. package/dist/catalog/merge.js +132 -0
  61. package/dist/catalog/registry.js +33 -12
  62. package/dist/catalog/runtime-models.js +1 -1
  63. package/dist/catalog/schema.js +12 -1
  64. package/dist/catalog/store.js +147 -8
  65. package/dist/catalog/validate.js +17 -2
  66. package/dist/compose.yaml +71 -0
  67. package/dist/edge/autostart/docker-record.js +32 -0
  68. package/dist/edge/autostart/docker.js +50 -0
  69. package/dist/edge/autostart/index.js +78 -0
  70. package/dist/edge/autostart/launcher.js +176 -0
  71. package/dist/edge/autostart/model.js +3 -0
  72. package/dist/edge/autostart/native.js +210 -0
  73. package/dist/edge/autostart/prompt.js +66 -0
  74. package/dist/edge/autostart/service.js +81 -0
  75. package/dist/edge/cli.js +57 -51
  76. package/dist/edge/docker-install/activation.js +56 -0
  77. package/dist/edge/docker-install/apply.js +458 -0
  78. package/dist/edge/docker-install/engine.js +362 -0
  79. package/dist/edge/docker-install/run.js +154 -0
  80. package/dist/edge/docker-install/state.js +146 -0
  81. package/dist/edge/{docker-projects → docker-install}/volume.js +1 -5
  82. package/dist/edge/docker.js +14 -22
  83. package/dist/edge/events/hub.js +11 -0
  84. package/dist/edge/host-cli.js +1 -1
  85. package/dist/edge/http/actions.js +32 -10
  86. package/dist/edge/http/app.js +60 -2
  87. package/dist/edge/http/auditions.js +70 -0
  88. package/dist/edge/http/autostart.js +37 -0
  89. package/dist/edge/http/backups.js +40 -0
  90. package/dist/edge/http/channel-memory.js +92 -0
  91. package/dist/edge/http/channels.js +128 -0
  92. package/dist/edge/http/entries.js +22 -3
  93. package/dist/edge/http/files.js +1 -1
  94. package/dist/edge/http/folder-location.js +1 -1
  95. package/dist/edge/http/home.js +39 -0
  96. package/dist/edge/http/host-cli.js +45 -6
  97. package/dist/edge/http/narration-peaks.js +76 -0
  98. package/dist/edge/http/onboarding.js +152 -0
  99. package/dist/edge/http/open-folder.js +1 -1
  100. package/dist/edge/http/patch-notes.js +45 -0
  101. package/dist/edge/http/planning.js +4 -2
  102. package/dist/edge/http/problem.js +1 -0
  103. package/dist/edge/http/project-create.js +87 -0
  104. package/dist/edge/http/projects.js +31 -99
  105. package/dist/edge/http/prompts.js +40 -3
  106. package/dist/edge/http/pronunciations.js +23 -4
  107. package/dist/edge/http/providers.js +86 -0
  108. package/dist/edge/http/reviews.js +121 -0
  109. package/dist/edge/http/revision-files.js +1 -1
  110. package/dist/edge/http/revisions.js +1 -1
  111. package/dist/edge/http/run-cost.js +25 -0
  112. package/dist/edge/http/schedules.js +111 -6
  113. package/dist/edge/http/settings.js +128 -6
  114. package/dist/edge/http/storage-files.js +50 -0
  115. package/dist/edge/http/storage.js +29 -2
  116. package/dist/edge/http/studio.js +207 -0
  117. package/dist/edge/http/style-preview.js +100 -0
  118. package/dist/edge/http/subtitles.js +1 -1
  119. package/dist/edge/http/trash.js +69 -0
  120. package/dist/edge/http/update.js +24 -3
  121. package/dist/edge/http/whats-new.js +15 -0
  122. package/dist/edge/http/youtube-edits.js +83 -0
  123. package/dist/edge/native-update.js +62 -0
  124. package/dist/host-cli/open-folder.js +10 -10
  125. package/dist/host-cli/runtime.js +12 -3
  126. package/dist/kernel/db/migrations/0022-drop-duplicate-held-work.sql +56 -0
  127. package/dist/kernel/db/migrations/0024-reference-attachments.sql +18 -0
  128. package/dist/kernel/db/migrations/0025-automatic-reviews.sql +40 -0
  129. package/dist/kernel/db/migrations/0026-schedule-topic-generation.sql +23 -0
  130. package/dist/kernel/db/migrations/0027-run-cost.sql +58 -0
  131. package/dist/kernel/db/migrations/0030-automatic-retries.sql +18 -0
  132. package/dist/kernel/db/migrations/0031-prompt-history-and-description-edits.sql +33 -0
  133. package/dist/kernel/db/migrations/0032-channels.sql +65 -0
  134. package/dist/kernel/db/migrations/0034-script-prompts.sql +23 -0
  135. package/dist/kernel/db/migrations/0035-narration-aliases.sql +13 -0
  136. package/dist/kernel/db/migrations/0037-project-uploads.sql +8 -0
  137. package/dist/kernel/db/migrations/0038-voice-languages.sql +7 -0
  138. package/dist/kernel/db/migrations/0039-channel-essentials.sql +64 -0
  139. package/dist/kernel/db/migrations/0040-standalone-usage.sql +35 -0
  140. package/dist/kernel/paths.js +27 -6
  141. package/dist/kernel/pipeline.js +4 -1
  142. package/dist/kernel/ports/host-cli.js +139 -16
  143. package/dist/kernel/ports/image.js +3 -1
  144. package/dist/kernel/ports/key-probe.js +1 -0
  145. package/dist/kernel/ports/languages.js +157 -0
  146. package/dist/kernel/ports/llm.js +3 -1
  147. package/dist/kernel/ports/model.js +5 -2
  148. package/dist/kernel/ports/narration-aliases.js +60 -0
  149. package/dist/kernel/ports/plan-limits.js +36 -0
  150. package/dist/kernel/runner/attempt.js +18 -9
  151. package/dist/kernel/runner/audition.js +36 -0
  152. package/dist/kernel/runner/graph.js +24 -5
  153. package/dist/kernel/runner/index.js +35 -5
  154. package/dist/kernel/runner/meter.js +1 -0
  155. package/dist/kernel/runner/providers.js +164 -9
  156. package/dist/kernel/runner/retry-policy.js +43 -0
  157. package/dist/kernel/runner/standalone.js +134 -0
  158. package/dist/kernel/runner/work-authority.js +28 -2
  159. package/dist/main.js +321 -20
  160. package/dist/patch-notes/0.4.2-to-1.0.md +93 -0
  161. package/dist/patch-notes/1.0-to-2.0.md +74 -0
  162. package/dist/patch-notes/2.0-to-3.0.md +257 -0
  163. package/dist/patch-notes/3.0.0.md +70 -0
  164. package/dist/patch-notes/index.json +26 -0
  165. package/dist/slices/admission/model.js +35 -0
  166. package/dist/slices/admission/repo.js +24 -4
  167. package/dist/slices/admission/rules.js +126 -1
  168. package/dist/slices/admission/schema.js +68 -1
  169. package/dist/slices/admission/short-mode.js +47 -0
  170. package/dist/slices/admission/start.js +90 -16
  171. package/dist/slices/article/continuation.js +2 -2
  172. package/dist/slices/article/provided-entries.js +2 -2
  173. package/dist/slices/article/run.js +6 -2
  174. package/dist/slices/article/segments.js +3 -3
  175. package/dist/slices/backups/files.js +135 -0
  176. package/dist/slices/backups/folder.js +43 -0
  177. package/dist/slices/backups/model.js +65 -0
  178. package/dist/slices/backups/repo.js +28 -0
  179. package/dist/slices/backups/schedule.js +60 -0
  180. package/dist/slices/backups/service.js +311 -0
  181. package/dist/slices/batch/index.js +9 -2
  182. package/dist/slices/cancel/index.js +1 -1
  183. package/dist/slices/channels/cast-images.js +110 -0
  184. package/dist/slices/channels/cast-match.js +32 -0
  185. package/dist/slices/channels/images.js +19 -0
  186. package/dist/slices/channels/model.js +5 -0
  187. package/dist/slices/channels/rebrand.js +100 -0
  188. package/dist/slices/channels/repo.js +218 -0
  189. package/dist/slices/channels/runs.js +129 -0
  190. package/dist/slices/channels/schema.js +129 -0
  191. package/dist/slices/channels/service.js +180 -0
  192. package/dist/slices/channels/studio-csv.js +110 -0
  193. package/dist/slices/channels/videos.js +120 -0
  194. package/dist/slices/checkpoints/recovery.js +10 -0
  195. package/dist/slices/control/index.js +2 -1
  196. package/dist/slices/document/fonts.js +1 -1
  197. package/dist/slices/document/render.js +1 -1
  198. package/dist/slices/episodes/related.js +90 -0
  199. package/dist/slices/episodes/repo.js +60 -0
  200. package/dist/slices/episodes/service.js +58 -0
  201. package/dist/slices/episodes/summarize.js +170 -0
  202. package/dist/slices/estimate/index.js +113 -24
  203. package/dist/slices/estimate/requests.js +60 -2
  204. package/dist/slices/fixes/rules.js +73 -0
  205. package/dist/slices/fonts/coverage.js +82 -0
  206. package/dist/slices/fonts/sfnt.js +74 -0
  207. package/dist/slices/images/run.js +8 -5
  208. package/dist/slices/images/scale.js +84 -0
  209. package/dist/slices/library/diff.js +122 -0
  210. package/dist/slices/library/history.js +100 -0
  211. package/dist/slices/library/model.js +3 -0
  212. package/dist/slices/library/photorealistic.js +49 -0
  213. package/dist/slices/library/repo.js +19 -14
  214. package/dist/slices/library/save.js +62 -12
  215. package/dist/slices/library/slots.js +15 -3
  216. package/dist/slices/library/snapshot.js +8 -2
  217. package/dist/slices/library/used-by.js +153 -0
  218. package/dist/slices/loudness/level-pieces.js +41 -0
  219. package/dist/slices/loudness/loudnorm.js +164 -0
  220. package/dist/slices/loudness/model.js +153 -0
  221. package/dist/slices/model-upkeep/model.js +10 -0
  222. package/dist/slices/model-upkeep/switch.js +217 -0
  223. package/dist/slices/model-upkeep/usage.js +183 -0
  224. package/dist/slices/narration/aliases-library.js +31 -0
  225. package/dist/slices/narration/aliases-schema.js +53 -0
  226. package/dist/slices/narration/aliases.js +49 -0
  227. package/dist/slices/narration/blocks.js +352 -0
  228. package/dist/slices/narration/chunk.js +19 -7
  229. package/dist/slices/narration/concat.js +2 -2
  230. package/dist/slices/narration/describe.js +45 -0
  231. package/dist/slices/narration/pauses-model.js +63 -0
  232. package/dist/slices/narration/pauses.js +269 -0
  233. package/dist/slices/narration/peaks-model.js +1 -0
  234. package/dist/slices/narration/peaks.js +94 -0
  235. package/dist/slices/narration/preparation.js +21 -3
  236. package/dist/slices/narration/pronunciation-chunks.js +3 -3
  237. package/dist/slices/narration/pronunciation.js +72 -25
  238. package/dist/slices/narration/run.js +6 -6
  239. package/dist/slices/narration/shared-glossary.js +1 -1
  240. package/dist/slices/narration/steering.js +15 -4
  241. package/dist/slices/notifications/notifier.js +94 -0
  242. package/dist/slices/notifications/rules.js +102 -0
  243. package/dist/slices/notifications/send.js +40 -0
  244. package/dist/slices/notifications/settings.js +34 -0
  245. package/dist/slices/onboarding/copy.js +143 -0
  246. package/dist/slices/onboarding/first-run.js +46 -0
  247. package/dist/slices/onboarding/install.js +119 -0
  248. package/dist/slices/onboarding/model.js +64 -0
  249. package/dist/slices/onboarding/packs.js +491 -0
  250. package/dist/slices/onboarding/quick-short.js +129 -0
  251. package/dist/slices/onboarding/sample.js +132 -0
  252. package/dist/slices/onboarding/state.js +68 -0
  253. package/dist/slices/onboarding/template.js +73 -0
  254. package/dist/slices/patch-notes/library.js +73 -0
  255. package/dist/slices/patch-notes/seen.js +77 -0
  256. package/dist/slices/play-drafts/convert.js +160 -9
  257. package/dist/slices/play-drafts/readiness.js +3 -0
  258. package/dist/slices/play-drafts/repo.js +9 -1
  259. package/dist/slices/play-drafts/review-inputs.js +22 -3
  260. package/dist/slices/play-drafts/schema.js +109 -2
  261. package/dist/slices/play-drafts/service.js +10 -1
  262. package/dist/slices/play-drafts/start.js +43 -18
  263. package/dist/slices/play-drafts/uploads.js +2 -1
  264. package/dist/slices/project-templates/from-project.js +70 -4
  265. package/dist/slices/project-templates/one-off.js +26 -0
  266. package/dist/slices/project-templates/repo.js +2 -1
  267. package/dist/slices/project-templates/service.js +15 -4
  268. package/dist/slices/project-templates/setup.js +29 -2
  269. package/dist/slices/rebuild/admission-repo.js +1 -1
  270. package/dist/slices/rebuild/model.js +6 -0
  271. package/dist/slices/rebuild/preview-details.js +67 -2
  272. package/dist/slices/rebuild/preview-plan.js +13 -6
  273. package/dist/slices/rebuild/recipe-audio-parts.js +40 -7
  274. package/dist/slices/rebuild/recipe-audio.js +66 -12
  275. package/dist/slices/rebuild/recipe-build.js +15 -5
  276. package/dist/slices/rebuild/recipe-cast.js +18 -0
  277. package/dist/slices/rebuild/recipe-checkpoints.js +11 -9
  278. package/dist/slices/rebuild/recipe-describe.js +93 -0
  279. package/dist/slices/rebuild/recipe-edit.js +13 -1
  280. package/dist/slices/rebuild/recipe-exports.js +62 -8
  281. package/dist/slices/rebuild/recipe-input-schema.js +43 -1
  282. package/dist/slices/rebuild/recipe-legacy.js +12 -0
  283. package/dist/slices/rebuild/recipe-loudness.js +45 -0
  284. package/dist/slices/rebuild/recipe-model.js +25 -6
  285. package/dist/slices/rebuild/recipe-pauses.js +7 -0
  286. package/dist/slices/rebuild/recipe-preparation.js +3 -3
  287. package/dist/slices/rebuild/recipe-provider-choice.js +11 -1
  288. package/dist/slices/rebuild/recipe-reference.js +58 -0
  289. package/dist/slices/rebuild/recipe-reviews.js +117 -0
  290. package/dist/slices/rebuild/recipe-save.js +4 -1
  291. package/dist/slices/rebuild/recipe-shorts.js +32 -5
  292. package/dist/slices/rebuild/recipe-text.js +114 -19
  293. package/dist/slices/rebuild/recipe-validation.js +29 -3
  294. package/dist/slices/rebuild/recipe-visual.js +104 -29
  295. package/dist/slices/rebuild/recipe-voices.js +286 -0
  296. package/dist/slices/rebuild/recipe-work.js +77 -6
  297. package/dist/slices/rebuild/recipe-youtube.js +2 -0
  298. package/dist/slices/rebuild/recovery-conflict.js +7 -3
  299. package/dist/slices/rebuild/recovery-model.js +3 -0
  300. package/dist/slices/rebuild/recovery-selection.js +28 -2
  301. package/dist/slices/rebuild/recovery.js +35 -17
  302. package/dist/slices/rebuild/repo.js +3 -3
  303. package/dist/slices/rebuild/retry.js +18 -0
  304. package/dist/slices/rebuild/review-redo.js +110 -0
  305. package/dist/slices/rebuild/runtime-actions.js +14 -2
  306. package/dist/slices/rebuild/runtime-admission.js +4 -1
  307. package/dist/slices/rebuild/runtime-animate.js +4 -4
  308. package/dist/slices/rebuild/runtime-article.js +5 -5
  309. package/dist/slices/rebuild/runtime-document.js +4 -4
  310. package/dist/slices/rebuild/runtime-export-bed.js +43 -0
  311. package/dist/slices/rebuild/runtime-export-edit.js +70 -11
  312. package/dist/slices/rebuild/runtime-export-inputs.js +52 -10
  313. package/dist/slices/rebuild/runtime-export-short.js +85 -0
  314. package/dist/slices/rebuild/runtime-export.js +84 -19
  315. package/dist/slices/rebuild/runtime-figure-card.js +87 -0
  316. package/dist/slices/rebuild/runtime-image.js +69 -0
  317. package/dist/slices/rebuild/runtime-local.js +154 -27
  318. package/dist/slices/rebuild/runtime-materialize.js +8 -0
  319. package/dist/slices/rebuild/runtime-narration-reuse.js +2 -1
  320. package/dist/slices/rebuild/runtime-narration-text.js +7 -7
  321. package/dist/slices/rebuild/runtime-pictures.js +28 -0
  322. package/dist/slices/rebuild/runtime-piece-label.js +29 -0
  323. package/dist/slices/rebuild/runtime-provider.js +134 -19
  324. package/dist/slices/rebuild/runtime-publication.js +21 -5
  325. package/dist/slices/rebuild/runtime-review.js +237 -0
  326. package/dist/slices/rebuild/runtime-run.js +17 -11
  327. package/dist/slices/rebuild/runtime-shorts.js +48 -23
  328. package/dist/slices/rebuild/runtime-store.js +39 -10
  329. package/dist/slices/rebuild/runtime-subtitles.js +72 -19
  330. package/dist/slices/rebuild/runtime-voices.js +103 -0
  331. package/dist/slices/rebuild/runtime-youtube.js +7 -6
  332. package/dist/slices/rebuild/service-readiness.js +15 -1
  333. package/dist/slices/rebuild/soften.js +49 -0
  334. package/dist/slices/rebuild/transition-repo.js +10 -3
  335. package/dist/slices/research/run.js +4 -4
  336. package/dist/slices/research/synthesis.js +2 -2
  337. package/dist/slices/reviews/model.js +105 -0
  338. package/dist/slices/reviews/outcome.js +22 -0
  339. package/dist/slices/reviews/repo.js +106 -0
  340. package/dist/slices/reviews/rules.js +63 -0
  341. package/dist/slices/reviews/verdict.js +71 -0
  342. package/dist/slices/reviews/view.js +30 -0
  343. package/dist/slices/revisions/adopt-content.js +12 -2
  344. package/dist/slices/revisions/adopt.js +9 -2
  345. package/dist/slices/revisions/downloads.js +7 -1
  346. package/dist/slices/revisions/image-plan.js +183 -0
  347. package/dist/slices/revisions/mutation-assets.js +43 -9
  348. package/dist/slices/revisions/mutation-cues.js +71 -0
  349. package/dist/slices/revisions/mutation-prepare.js +8 -10
  350. package/dist/slices/revisions/mutation-request.js +2 -1
  351. package/dist/slices/revisions/mutations.js +97 -3
  352. package/dist/slices/revisions/schema.js +16 -2
  353. package/dist/slices/run-cost/limits.js +128 -0
  354. package/dist/slices/run-cost/meter.js +47 -0
  355. package/dist/slices/run-cost/panel.js +234 -0
  356. package/dist/slices/run-cost/pricing.js +92 -0
  357. package/dist/slices/run-cost/week.js +108 -0
  358. package/dist/slices/schedules/agenda.js +158 -0
  359. package/dist/slices/schedules/repo.js +58 -18
  360. package/dist/slices/schedules/scheduler.js +67 -16
  361. package/dist/slices/schedules/schema.js +138 -1
  362. package/dist/slices/schedules/service.js +48 -4
  363. package/dist/slices/schedules/similar.js +90 -0
  364. package/dist/slices/schedules/topic-list.js +163 -0
  365. package/dist/slices/schedules/topics.js +422 -0
  366. package/dist/slices/settings/first-run.js +102 -0
  367. package/dist/slices/settings/health.js +190 -0
  368. package/dist/slices/settings/key-guides.js +128 -0
  369. package/dist/slices/settings/key-test.js +85 -0
  370. package/dist/slices/settings/model.js +11 -1
  371. package/dist/slices/settings/playback.js +15 -0
  372. package/dist/slices/settings/repo.js +17 -3
  373. package/dist/slices/settings/voices.js +64 -4
  374. package/dist/slices/settings/whats-new.js +64 -0
  375. package/dist/slices/shorts/captions.js +9 -0
  376. package/dist/slices/shorts/pick.js +3 -3
  377. package/dist/slices/shorts/prompts.js +1 -1
  378. package/dist/slices/shorts/render.js +54 -5
  379. package/dist/slices/storage/asset-name.js +3 -0
  380. package/dist/slices/storage/backup-export.js +48 -9
  381. package/dist/slices/storage/backup-format.js +38 -1
  382. package/dist/slices/storage/backup-import.js +191 -19
  383. package/dist/slices/storage/delete-project.js +7 -4
  384. package/dist/slices/storage/documents.js +79 -0
  385. package/dist/slices/storage/files-location.js +531 -0
  386. package/dist/slices/storage/layout.js +20 -0
  387. package/dist/slices/storage/model.js +14 -0
  388. package/dist/slices/storage/portable.js +95 -12
  389. package/dist/slices/storage/reconcile.js +19 -2
  390. package/dist/slices/storage/repo.js +4 -1
  391. package/dist/slices/storage/schema.js +11 -0
  392. package/dist/slices/storage/trim.js +146 -0
  393. package/dist/slices/studio/disclosure.js +74 -0
  394. package/dist/slices/studio/model.js +28 -0
  395. package/dist/slices/studio/pack.js +209 -0
  396. package/dist/slices/studio/settings.js +133 -0
  397. package/dist/slices/style-preview/images.js +61 -0
  398. package/dist/slices/style-preview/render.js +161 -0
  399. package/dist/slices/style-preview/schema.js +45 -0
  400. package/dist/slices/style-preview/service.js +93 -0
  401. package/dist/slices/style-preview/settings.js +89 -0
  402. package/dist/slices/subtitles/captions.js +42 -9
  403. package/dist/slices/subtitles/model.js +22 -0
  404. package/dist/slices/subtitles/prepare.js +27 -16
  405. package/dist/slices/subtitles/transcript.js +1 -1
  406. package/dist/slices/thumbnail/run.js +7 -7
  407. package/dist/slices/trash/model.js +25 -0
  408. package/dist/slices/trash/service.js +273 -0
  409. package/dist/slices/uploads/repo.js +20 -0
  410. package/dist/slices/video/ambient-bed-schema.js +15 -0
  411. package/dist/slices/video/ambient-bed.js +95 -0
  412. package/dist/slices/video/ambient-mix.js +72 -0
  413. package/dist/slices/video/audio-inputs.js +2 -2
  414. package/dist/slices/video/cards.js +5 -2
  415. package/dist/slices/video/edit-list.js +23 -2
  416. package/dist/slices/video/edit-settings.js +13 -3
  417. package/dist/slices/video/ffmpeg.js +63 -9
  418. package/dist/slices/video/figure-card.js +492 -0
  419. package/dist/slices/video/figure-spans.js +51 -0
  420. package/dist/slices/video/plan.js +125 -13
  421. package/dist/slices/video/run.js +14 -12
  422. package/dist/slices/video/slideshow.js +39 -4
  423. package/dist/slices/voices/attribution.js +69 -0
  424. package/dist/slices/voices/audio-files.js +82 -0
  425. package/dist/slices/voices/audition.js +25 -0
  426. package/dist/slices/voices/cast.js +56 -0
  427. package/dist/slices/voices/grouping.js +49 -0
  428. package/dist/slices/voices/join.js +35 -0
  429. package/dist/slices/voices/languages.js +25 -0
  430. package/dist/slices/voices/model.js +179 -0
  431. package/dist/slices/voices/palette.js +26 -0
  432. package/dist/slices/voices/panel.js +91 -0
  433. package/dist/slices/voices/portraits.js +55 -0
  434. package/dist/slices/voices/script.js +136 -0
  435. package/dist/slices/voices/timing.js +58 -0
  436. package/dist/slices/youtube/answer.js +1 -1
  437. package/dist/slices/youtube/chapters.js +126 -0
  438. package/dist/slices/youtube/edits-repo.js +69 -0
  439. package/dist/slices/youtube/edits.js +89 -0
  440. package/dist/slices/youtube/placeholders.js +98 -0
  441. package/dist/slices/youtube/transcript.js +11 -4
  442. package/dist/updater/service.js +66 -21
  443. package/dist/web/assets/index-CUA2sUxU.css +1 -0
  444. package/dist/web/assets/index-CX590kE-.js +303 -0
  445. package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  446. package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  447. package/dist/web/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  448. package/dist/web/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  449. package/dist/web/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  450. package/dist/web/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  451. package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  452. package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  453. package/dist/web/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  454. package/dist/web/assets/{pdf-CAJNT254.js → pdf-BG8n_uc5.js} +1 -1
  455. package/dist/web/index.html +2 -2
  456. package/package.json +4 -4
  457. package/dist/edge/docker-launch.js +0 -81
  458. package/dist/edge/docker-projects/activation.js +0 -35
  459. package/dist/edge/docker-projects/claims.js +0 -85
  460. package/dist/edge/docker-projects/committed.js +0 -40
  461. package/dist/edge/docker-projects/engine.js +0 -461
  462. package/dist/edge/docker-projects/install.js +0 -254
  463. package/dist/edge/docker-projects/prune.js +0 -66
  464. package/dist/edge/docker-projects/recover.js +0 -100
  465. package/dist/edge/docker-projects/state.js +0 -225
  466. package/dist/web/assets/index-BGgYMm2u.css +0 -1
  467. package/dist/web/assets/index-BVVgtntL.js +0 -171
  468. package/scripts/docker-run.sh +0 -3
  469. /package/dist/edge/{docker-projects → docker-install}/tree.js +0 -0
@@ -1,5 +1,6 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { isAbsolute, resolve as resolvePath } from "node:path";
3
+ import { bedChains, bedInputs } from "./ambient-mix.js";
3
4
  import { gradeFilter, lookChain, lookEncoding, placed } from "./look.js";
4
5
  import { decimal, zoomRange } from "./plan.js";
5
6
  import { segments, transitionHalves } from "./transitions.js";
@@ -43,7 +44,7 @@ export function slideshowClips(edit) {
43
44
  named.set(key, name);
44
45
  const shot = edit.shots[segment.kind === "shot" ? segment.shot : segment.to];
45
46
  if (shot === undefined)
46
- throw new Error("Slopify hit an internal error (the video's edit list lost a shot). Use Re-run section on Video; if it happens again, use Download diagnostics in Settings and report it.");
47
+ throw new Error("Slopify hit an internal error (the video's edit list lost a shot). Use More → Render the video again in the Video section; if it happens again, use Download diagnostics in Settings and report it.");
47
48
  clips.push({ name, segment, shot, frames: segment.count });
48
49
  }
49
50
  return name;
@@ -92,7 +93,7 @@ cardsFile = "cards.ass") {
92
93
  const chains = [];
93
94
  const picture = (shot, from, label) => {
94
95
  if (shot === undefined)
95
- throw new Error("Slopify hit an internal error (the video's edit list lost a shot). Use Re-run section on Video; if it happens again, use Download diagnostics in Settings and report it.");
96
+ throw new Error("Slopify hit an internal error (the video's edit list lost a shot). Use More → Render the video again in the Video section; if it happens again, use Download diagnostics in Settings and report it.");
96
97
  const at = inputs.filter((one) => one === "-i").length;
97
98
  const built = pictureChain(edit, shot, from, segment.count, at, label, gradeFilter(edit.look));
98
99
  inputs.push(...built.inputs);
@@ -207,11 +208,13 @@ function seconds6(value) {
207
208
  export function concatList(order) {
208
209
  return `ffconcat version 1.0\n${order.map((name) => `file ${name}`).join("\n")}\n`;
209
210
  }
210
- export function joinArgs(edit, output, list, burnSubtitles = false) {
211
- const inputs = ["-f", "concat", "-i", list];
211
+ // The video's sound: the narration's segments and silences joined, with the ambient bed under
212
+ // them when there is one, as the inputs from `first` on and the chains ending in `[a]`.
213
+ function audioMix(edit, first) {
214
+ const inputs = [];
212
215
  const audioAt = [];
213
216
  for (const segment of edit.audio) {
214
- audioAt.push(1 + audioAt.length);
217
+ audioAt.push(first + audioAt.length);
215
218
  if (segment.path === null) {
216
219
  inputs.push("-f", "lavfi", "-t", seconds(segment.seconds), "-i", `anullsrc=r=${sampleRate}:cl=${channelLayout}`);
217
220
  continue;
@@ -219,15 +222,65 @@ export function joinArgs(edit, output, list, burnSubtitles = false) {
219
222
  inputs.push("-i", segment.path);
220
223
  }
221
224
  const chains = [];
222
- if (burnSubtitles)
223
- chains.push("[0:v]ass=filename=subtitles.ass:fontsdir=fonts[v]");
224
225
  audioAt.forEach((input, at) => {
225
226
  // The segments come from different files and the silence from lavfi, so they are
226
227
  // brought to one format before concat, which refuses to join mismatched streams.
227
228
  chains.push(`[${input}:a]aformat=sample_fmts=fltp:sample_rates=${sampleRate}:channel_layouts=${channelLayout}[a${at}]`);
228
229
  });
230
+ // The ambient bed lies under the joined narration, ducked by it (`ambient-mix.ts`); without
231
+ // one the arguments are the ones every video was joined with.
232
+ const bed = audioAt.length > 0 ? edit.bed : undefined;
229
233
  if (audioAt.length > 0) {
230
- chains.push(`${audioAt.map((_input, at) => `[a${at}]`).join("")}concat=n=${audioAt.length}:v=0:a=1[a]`);
234
+ chains.push(`${audioAt.map((_input, at) => `[a${at}]`).join("")}concat=n=${audioAt.length}:v=0:a=1${bed === undefined ? "[a]" : "[narration]"}`);
235
+ }
236
+ if (bed !== undefined) {
237
+ const total = edit.audio.reduce((sum, segment) => sum + segment.seconds, 0);
238
+ inputs.push(...bedInputs(bed, total, sampleRate));
239
+ chains.push(...bedChains(bed, first + audioAt.length, total, `aformat=sample_fmts=fltp:sample_rates=${sampleRate}:channel_layouts=${channelLayout}`, "[narration]", "[a]"));
240
+ }
241
+ return { inputs, chains };
242
+ }
243
+ // The video's sound alone, as a WAV: what Level the volume masters before the join plays it
244
+ // (`slideshow.ts`).
245
+ export function audioMixArgs(edit, output) {
246
+ const mix = audioMix(edit, 0);
247
+ return [
248
+ ...progressArgs,
249
+ ...mix.inputs,
250
+ "-filter_complex",
251
+ mix.chains.join(";"),
252
+ "-map",
253
+ "[a]",
254
+ "-c:a",
255
+ "pcm_s16le",
256
+ "-f",
257
+ "wav",
258
+ output,
259
+ ];
260
+ }
261
+ export function joinArgs(edit, output, list, burnSubtitles = false, portraits = []) {
262
+ const mix = audioMix(edit, 1);
263
+ const inputs = ["-f", "concat", "-i", list, ...mix.inputs];
264
+ const chains = [];
265
+ const overlaid = burnSubtitles && portraits.length > 0;
266
+ if (burnSubtitles && !overlaid)
267
+ chains.push("[0:v]ass=filename=subtitles.ass:fontsdir=fonts[v]");
268
+ chains.push(...mix.chains);
269
+ // The portraits go in after every other input and under the captions, so the panel's lit
270
+ // outline is drawn over them; a video without any is joined exactly as before. A still
271
+ // image is one frame, which overlay holds to the end (`eof_action=repeat`).
272
+ if (overlaid) {
273
+ let first = inputs.filter((value) => value === "-i").length;
274
+ let source = "[0:v]";
275
+ const video = [];
276
+ portraits.forEach((portrait, at) => {
277
+ inputs.push("-i", portrait.path);
278
+ const size = String(portrait.size);
279
+ video.push(`[${String(first)}:v]scale=${size}:${size}:force_original_aspect_ratio=increase,crop=${size}:${size},setsar=1[pic${String(at)}]`, `${source}[pic${String(at)}]overlay=x=${String(portrait.x)}:y=${String(portrait.y)}:eof_action=repeat[panel${String(at)}]`);
280
+ source = `[panel${String(at)}]`;
281
+ first += 1;
282
+ });
283
+ chains.unshift(...video, `${source}ass=filename=subtitles.ass:fontsdir=fonts[v]`);
231
284
  }
232
285
  return [
233
286
  ...progressArgs,
@@ -359,6 +412,7 @@ export function runFfmpeg(run) {
359
412
  child.stderr.setEncoding("utf8");
360
413
  child.stderr.on("data", (chunk) => {
361
414
  guarded("video.stderr.read", () => {
415
+ run.onStderr?.(chunk);
362
416
  for (const line of chunk.split("\n")) {
363
417
  if (line.trim() !== "") {
364
418
  errors.push(line.trim());
@@ -436,5 +490,5 @@ function failure(code, signal, errors) {
436
490
  const said = errors.length === 0
437
491
  ? `ffmpeg ${ended} and wrote nothing to its error stream`
438
492
  : `ffmpeg ${ended}: ${errors.join(" / ")}`;
439
- return `The audio/video export failed (${said}). Retry stage; if it fails again, make sure your disk has free space (see Settings → Backup & storage), then use Download diagnostics in Settings and report it.`;
493
+ return `The audio/video export failed (${said}). Try again; if it fails again, make sure your disk has free space (see Settings → Backup & storage), then use Download diagnostics in Settings and report it.`;
440
494
  }
@@ -0,0 +1,492 @@
1
+ import { remark } from "remark";
2
+ import remarkGfm from "remark-gfm";
3
+ import { runFfmpeg } from "./ffmpeg.js";
4
+ // The 3.0 design tokens (`packages/web/src/styles/index.css`): the surface graphite, ink,
5
+ // the second ink, Slopify lime and the line colour, in the bundled Barlow.
6
+ export const defaultCardStyle = {
7
+ ground: "#19191c",
8
+ ink: "#ece9e2",
9
+ muted: "#a9a69e",
10
+ accent: "#a6d45c",
11
+ line: "#2a2a30",
12
+ font: "Barlow",
13
+ mono: "DejaVu Sans Mono",
14
+ };
15
+ // ceiling: the owner's eye at 1080p. Text never smaller than 2.4% of the short side (26 px on
16
+ // a 1080-line frame), so a card stays readable on a phone; a table with more rows than fit at
17
+ // that size says how many it left out.
18
+ const minShare = 0.024;
19
+ const rowSpacing = 1.55;
20
+ const lineSpacing = 1.35;
21
+ // Barlow's average advance is about half its size; a monospace face's is 0.6.
22
+ const textAdvance = 0.52;
23
+ const monoAdvance = 0.6;
24
+ export function cardLayout(input) {
25
+ const { width, height } = input;
26
+ const short = Math.min(width, height);
27
+ const margin = Math.round(short * 0.07);
28
+ const events = [];
29
+ let top = margin;
30
+ const label = labelOf(input);
31
+ if (label !== "") {
32
+ const size = Math.round(short * 0.03);
33
+ events.push(text(margin, top, 7, `{\\fs${String(size)}\\c${colour(input.style.accent)}}${assText(label)}`));
34
+ top += Math.round(size * 1.9);
35
+ }
36
+ const area = { x: margin, y: top, width: width - margin * 2, height: height - top - margin };
37
+ let box;
38
+ switch (input.kind) {
39
+ case "table":
40
+ events.push(...tableEvents(input, area));
41
+ break;
42
+ case "figure": {
43
+ const caption = figureCaption(input.source);
44
+ const size = Math.round(short * 0.032);
45
+ const lines = wrap(caption, Math.floor(area.width / (size * textAdvance)), 3);
46
+ const captionHeight = lines.length === 0 ? 0 : Math.round(lines.length * size * lineSpacing);
47
+ if (input.picture) {
48
+ box = {
49
+ x: area.x,
50
+ y: area.y,
51
+ width: area.width,
52
+ height: Math.max(1, area.height - captionHeight - (lines.length === 0 ? 0 : size)),
53
+ };
54
+ for (const [at, line] of lines.entries())
55
+ events.push(text(width / 2, area.y + area.height - captionHeight + at * size * lineSpacing, 8, `{\\fs${String(size)}\\c${colour(input.style.muted)}}${assText(line)}`));
56
+ }
57
+ else {
58
+ // No picture to show: the caption itself, large, as the figure's card.
59
+ const big = Math.round(short * 0.05);
60
+ const said = wrap(caption || "Figure", Math.floor(area.width / (big * textAdvance)), 6);
61
+ const start = area.y + (area.height - said.length * big * lineSpacing) / 2;
62
+ for (const [at, line] of said.entries())
63
+ events.push(text(width / 2, start + at * big * lineSpacing, 8, `{\\fs${String(big)}\\c${colour(input.style.ink)}}${assText(line)}`));
64
+ }
65
+ break;
66
+ }
67
+ case "math":
68
+ events.push(...mathEvents(input, area));
69
+ break;
70
+ case "code":
71
+ case "diagram":
72
+ events.push(...codeEvents(input, area));
73
+ break;
74
+ }
75
+ return { ass: script(input, events), ...(box === undefined ? {} : { box }) };
76
+ }
77
+ // The ffmpeg arguments that draw the card to `output` (a PNG) in `cwd`, which holds
78
+ // `card.ass` and a `fonts` folder.
79
+ export function cardArgs(input, layout, output, picture) {
80
+ const ground = `color=c=0x${input.style.ground.slice(1)}:s=${String(input.width)}x${String(input.height)}:d=1`;
81
+ const box = layout.box;
82
+ if (picture === undefined || box === undefined)
83
+ return [
84
+ ...["-hide_banner", "-nostdin", "-loglevel", "error", "-y", "-f", "lavfi", "-i", ground],
85
+ ...["-vf", "ass=filename=card.ass:fontsdir=fonts", "-frames:v", "1", output],
86
+ ];
87
+ return [
88
+ ...[
89
+ "-hide_banner",
90
+ "-nostdin",
91
+ "-loglevel",
92
+ "error",
93
+ "-y",
94
+ "-f",
95
+ "lavfi",
96
+ "-i",
97
+ ground,
98
+ "-i",
99
+ picture,
100
+ ],
101
+ "-filter_complex",
102
+ [
103
+ `[1:v]scale=${String(box.width)}:${String(box.height)}:force_original_aspect_ratio=decrease:flags=lanczos,format=rgba[pic]`,
104
+ `[0:v][pic]overlay=x=${String(box.x)}+(${String(box.width)}-overlay_w)/2:y=${String(box.y)}+(${String(box.height)}-overlay_h)/2:format=auto,ass=filename=card.ass:fontsdir=fonts[v]`,
105
+ ].join(";"),
106
+ ...["-map", "[v]", "-frames:v", "1", output],
107
+ ];
108
+ }
109
+ export async function renderCard(run, write) {
110
+ const layout = cardLayout(run.input);
111
+ write(layout.ass);
112
+ await runFfmpeg({
113
+ bin: run.bin,
114
+ args: cardArgs(run.input, layout, run.output, run.picture),
115
+ cwd: run.cwd,
116
+ signal: run.signal,
117
+ log: run.log,
118
+ onProgress: () => undefined,
119
+ });
120
+ }
121
+ function labelOf(input) {
122
+ const section = input.section?.trim() ?? "";
123
+ if (input.kind === "code") {
124
+ const lang = /^```\s*([\w+#.-]+)/.exec(input.source)?.[1];
125
+ return [section, lang].filter((part) => part !== undefined && part !== "").join(" · ");
126
+ }
127
+ return section;
128
+ }
129
+ // A table as a clean grid: the header in the accent colour over a rule, a hairline between
130
+ // rows, numbers right-aligned. Sized to fit; past the smallest readable size the rows that do
131
+ // not fit are left out and counted.
132
+ function tableEvents(input, area) {
133
+ const rows = tableRows(input.source);
134
+ const header = rows[0] ?? [];
135
+ const body = rows.slice(1);
136
+ const columns = Math.max(1, ...rows.map((row) => row.length));
137
+ const short = Math.min(input.width, input.height);
138
+ const smallest = Math.round(short * minShare);
139
+ const largest = Math.round(short * 0.045);
140
+ // Every row but the header, and a line for what was left out when some are.
141
+ let shown = body.length;
142
+ let size = Math.floor(area.height / ((shown + 1) * rowSpacing));
143
+ if (size < smallest) {
144
+ size = smallest;
145
+ shown = Math.max(1, Math.floor(area.height / (size * rowSpacing)) - 2);
146
+ }
147
+ size = Math.min(size, largest);
148
+ const cells = (row) => Array.from({ length: columns }, (_value, at) => row[at] ?? "");
149
+ const kept = [cells(header), ...body.slice(0, shown).map(cells)];
150
+ // Each column as wide as its widest cell (at most 28 letters); cut to fit if need be.
151
+ const letters = Array.from({ length: columns }, (_value, at) => Math.min(28, Math.max(3, ...kept.map((row) => (row[at] ?? "").length))));
152
+ const gap = 1.6;
153
+ const wanted = (letters.reduce((sum, n) => sum + n, 0) + gap * columns) * size * textAdvance;
154
+ if (wanted > area.width)
155
+ size = Math.max(smallest, Math.floor((size * area.width) / wanted));
156
+ const scale = area.width /
157
+ Math.max(area.width, (letters.reduce((sum, n) => sum + n, 0) + gap * columns) * size * textAdvance);
158
+ const widths = letters.map((n) => (n + gap) * size * textAdvance * scale);
159
+ const room = (at) => Math.max(3, Math.floor((widths[at] ?? 0) / (size * textAdvance)) - 1);
160
+ const rowHeight = Math.round(size * rowSpacing);
161
+ const events = [];
162
+ // Centred across the card; a narrow table is not pushed into a corner.
163
+ const tableWidth = widths.reduce((sum, n) => sum + n, 0);
164
+ const left = area.x + Math.max(0, (area.width - tableWidth) / 2);
165
+ const right = left + tableWidth;
166
+ kept.forEach((row, index) => {
167
+ const y = area.y + index * rowHeight;
168
+ let x = left;
169
+ row.forEach((cell, at) => {
170
+ const value = clip(cell, room(at));
171
+ const numeric = index > 0 && /^[-+−]?[\d.,]+\s*%?$|^[-+−]?[\d.,]+\s*[a-zA-Zµ%/]{0,4}$/.test(cell.trim());
172
+ const cellWidth = widths[at] ?? 0;
173
+ const colourTag = index === 0 ? colour(input.style.accent) : colour(input.style.ink);
174
+ events.push(numeric
175
+ ? text(x + cellWidth - size * 0.8, y + rowHeight / 2, 6, `{\\fs${String(size)}\\c${colourTag}}${assText(value)}`)
176
+ : text(x, y + rowHeight / 2, 4, `{\\fs${String(size)}\\c${colourTag}${index === 0 ? "\\b1" : ""}}${assText(value)}`));
177
+ x += cellWidth;
178
+ });
179
+ // The rule under the header is the accent's; the rest are hairlines.
180
+ const under = y + rowHeight;
181
+ const thick = index === 0 ? Math.max(2, Math.round(size / 10)) : 1;
182
+ events.push(rule(left, under, right - left, thick, index === 0 ? input.style.accent : input.style.line));
183
+ });
184
+ const leftOut = body.length - Math.min(shown, body.length);
185
+ if (leftOut > 0)
186
+ events.push(text(left, area.y + kept.length * rowHeight + rowHeight / 2, 4, `{\\fs${String(size)}\\c${colour(input.style.muted)}\\i1}${assText(`…and ${String(leftOut)} more ${leftOut === 1 ? "row" : "rows"}`)}`));
187
+ return events;
188
+ }
189
+ function tableRows(markdown) {
190
+ const tree = remark().use(remarkGfm).parse(markdown);
191
+ const table = tree.children.find((node) => node.type === "table");
192
+ if (table === undefined)
193
+ return [];
194
+ return table.children.map((row) => row.children.map((cell) => plain(cell).trim()));
195
+ }
196
+ function plain(node) {
197
+ if ("value" in node && typeof node.value === "string")
198
+ return node.value;
199
+ if (node.type === "image")
200
+ return node.alt ?? "";
201
+ if ("children" in node)
202
+ return node.children.map((child) => plain(child)).join("");
203
+ return "";
204
+ }
205
+ // Code and drawings in the monospace face, one line per line, with simple colouring: comments
206
+ // muted, strings and numbers in the ink's warm second tone, keywords in the accent.
207
+ function codeEvents(input, area) {
208
+ const lines = fenced(input.source).split("\n");
209
+ const longest = Math.max(12, ...lines.map((line) => line.length));
210
+ const short = Math.min(input.width, input.height);
211
+ const smallest = Math.round(short * minShare * 0.9);
212
+ let size = Math.min(Math.round(short * 0.04), Math.floor(area.width / (longest * monoAdvance)), Math.floor(area.height / (lines.length * lineSpacing)));
213
+ let shown = lines.length;
214
+ if (size < smallest) {
215
+ size = smallest;
216
+ shown = Math.max(1, Math.floor(area.height / (size * lineSpacing)) - 1);
217
+ }
218
+ const room = Math.floor(area.width / (size * monoAdvance));
219
+ // A short program sits in the middle of the card rather than at its top.
220
+ const top = area.y + Math.max(0, (area.height - shown * size * lineSpacing) / 2);
221
+ const events = lines
222
+ .slice(0, shown)
223
+ .map((line, at) => text(area.x, top + at * size * lineSpacing, 7, `{\\fn${input.style.mono}\\fs${String(size)}\\c${colour(input.style.ink)}}${input.kind === "code"
224
+ ? coloured(clip(line, room), input.style)
225
+ : assText(clip(line, room), true)}`));
226
+ if (shown < lines.length)
227
+ events.push(text(area.x, top + shown * size * lineSpacing, 7, `{\\fs${String(size)}\\c${colour(input.style.muted)}\\i1}${assText(`…and ${String(lines.length - shown)} more lines`)}`));
228
+ return events;
229
+ }
230
+ const keywords = new Set("fn let mut pub use mod impl struct enum trait match if else for while loop return break continue in as where const static async await function var class extends new this def import from export type interface yield lambda try catch except finally raise throw public private protected void int float double bool boolean char string true false null None nil self Self package func go defer select case switch default".split(" "));
231
+ function coloured(line, style) {
232
+ const comment = /(\/\/|#(?!\[)|--\s).*$/.exec(line);
233
+ const code = comment === null ? line : line.slice(0, comment.index);
234
+ const out = code.replace(/("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|\b\d[\d._]*\b|\b[A-Za-z_]\w*\b|[^"'\w]+)/g, (token) => {
235
+ if (/^["']/.test(token))
236
+ return `{\\c${colour(style.muted)}}${assText(token, true)}{\\c${colour(style.ink)}}`;
237
+ if (/^\d/.test(token))
238
+ return `{\\c${colour(style.muted)}}${assText(token, true)}{\\c${colour(style.ink)}}`;
239
+ if (keywords.has(token))
240
+ return `{\\c${colour(style.accent)}}${token}{\\c${colour(style.ink)}}`;
241
+ return assText(token, true);
242
+ });
243
+ return comment === null
244
+ ? out
245
+ : `${out}{\\c${colour(style.muted)}\\i1}${assText(comment[0], true)}`;
246
+ }
247
+ // An equation set in readable text: TeX's commands as the symbols they stand for, powers
248
+ // and indices as superscripts and subscripts where Unicode has them, centred and large.
249
+ function mathEvents(input, area) {
250
+ const tex = input.source
251
+ .replace(/^\$\$|\$\$$/g, "")
252
+ .replace(/^\\\[|\\\]$/g, "")
253
+ .replace(/^```\s*\w*\n?|```$/g, "")
254
+ .trim();
255
+ // A paragraph with formulas in it shows the paragraph with its formulas set.
256
+ const said = /\$[^$]+\$/.test(tex)
257
+ ? tex.replace(/\$\$?([^$]+)\$\$?/g, (_all, inner) => typeset(inner))
258
+ : typeset(tex);
259
+ const short = Math.min(input.width, input.height);
260
+ const size = Math.round(short * (said.length > 60 ? 0.04 : 0.06));
261
+ const lines = wrap(said, Math.floor(area.width / (size * textAdvance)), 6);
262
+ const start = area.y + (area.height - lines.length * size * lineSpacing) / 2;
263
+ return lines.map((line, at) => text(input.width / 2, start + at * size * lineSpacing, 8, `{\\fs${String(size)}\\c${colour(input.style.ink)}}${assText(line)}`));
264
+ }
265
+ const symbols = {
266
+ alpha: "α",
267
+ beta: "β",
268
+ gamma: "γ",
269
+ delta: "δ",
270
+ epsilon: "ε",
271
+ varepsilon: "ε",
272
+ zeta: "ζ",
273
+ eta: "η",
274
+ theta: "θ",
275
+ iota: "ι",
276
+ kappa: "κ",
277
+ lambda: "λ",
278
+ mu: "μ",
279
+ nu: "ν",
280
+ xi: "ξ",
281
+ pi: "π",
282
+ rho: "ρ",
283
+ sigma: "σ",
284
+ tau: "τ",
285
+ upsilon: "υ",
286
+ phi: "φ",
287
+ varphi: "φ",
288
+ chi: "χ",
289
+ psi: "ψ",
290
+ omega: "ω",
291
+ Gamma: "Γ",
292
+ Delta: "Δ",
293
+ Theta: "Θ",
294
+ Lambda: "Λ",
295
+ Xi: "Ξ",
296
+ Pi: "Π",
297
+ Sigma: "Σ",
298
+ Phi: "Φ",
299
+ Psi: "Ψ",
300
+ Omega: "Ω",
301
+ cdot: "·",
302
+ times: "×",
303
+ div: "÷",
304
+ pm: "±",
305
+ mp: "∓",
306
+ le: "≤",
307
+ leq: "≤",
308
+ ge: "≥",
309
+ geq: "≥",
310
+ neq: "≠",
311
+ ne: "≠",
312
+ approx: "≈",
313
+ sim: "∼",
314
+ equiv: "≡",
315
+ propto: "∝",
316
+ infty: "∞",
317
+ partial: "∂",
318
+ nabla: "∇",
319
+ sum: "Σ",
320
+ prod: "Π",
321
+ int: "∫",
322
+ to: "→",
323
+ rightarrow: "→",
324
+ leftarrow: "←",
325
+ Rightarrow: "⇒",
326
+ in: "∈",
327
+ forall: "∀",
328
+ exists: "∃",
329
+ ldots: "…",
330
+ cdots: "⋯",
331
+ degree: "°",
332
+ circ: "°",
333
+ };
334
+ const superscripts = {
335
+ "0": "⁰",
336
+ "1": "¹",
337
+ "2": "²",
338
+ "3": "³",
339
+ "4": "⁴",
340
+ "5": "⁵",
341
+ "6": "⁶",
342
+ "7": "⁷",
343
+ "8": "⁸",
344
+ "9": "⁹",
345
+ "+": "⁺",
346
+ "-": "⁻",
347
+ "=": "⁼",
348
+ "(": "⁽",
349
+ ")": "⁾",
350
+ n: "ⁿ",
351
+ i: "ⁱ",
352
+ };
353
+ const subscripts = {
354
+ "0": "₀",
355
+ "1": "₁",
356
+ "2": "₂",
357
+ "3": "₃",
358
+ "4": "₄",
359
+ "5": "₅",
360
+ "6": "₆",
361
+ "7": "₇",
362
+ "8": "₈",
363
+ "9": "₉",
364
+ "+": "₊",
365
+ "-": "₋",
366
+ "=": "₌",
367
+ "(": "₍",
368
+ ")": "₎",
369
+ a: "ₐ",
370
+ e: "ₑ",
371
+ o: "ₒ",
372
+ x: "ₓ",
373
+ i: "ᵢ",
374
+ j: "ⱼ",
375
+ k: "ₖ",
376
+ n: "ₙ",
377
+ t: "ₜ",
378
+ s: "ₛ",
379
+ r: "ᵣ",
380
+ m: "ₘ",
381
+ p: "ₚ",
382
+ };
383
+ export function typeset(tex) {
384
+ let out = tex
385
+ .replace(/\\(left|right|big|Big|bigg|Bigg|displaystyle|,|;|!|quad|qquad)\b/g, " ")
386
+ .replace(/\\(mathrm|mathbf|mathit|text|operatorname)\{([^}]*)\}/g, "$2");
387
+ // Innermost first, so a fraction inside a root comes out whole.
388
+ for (let pass = 0; pass < 4; pass++)
389
+ out = out
390
+ .replace(/\\frac\{([^{}]*)\}\{([^{}]*)\}/g, (_all, a, b) => `${group(a)}/${group(b)}`)
391
+ .replace(/\\sqrt\{([^{}]*)\}/g, (_all, a) => `√${group(a)}`);
392
+ out = out
393
+ .replace(/\\([A-Za-z]+)/g, (_all, name) => symbols[name] ?? name)
394
+ .replace(/\^\{([^{}]*)\}|\^(\S)/g, (_all, a, b) => scripted(a ?? b ?? "", superscripts, "^"))
395
+ .replace(/_\{([^{}]*)\}|_(\S)/g, (_all, a, b) => scripted(a ?? b ?? "", subscripts, "_"))
396
+ .replace(/[{}]/g, "")
397
+ .replace(/\s*([=<>≤≥≈≠+×·−])\s*/g, " $1 ")
398
+ .replace(/\s+/g, " ")
399
+ .trim();
400
+ return out;
401
+ }
402
+ function group(text) {
403
+ const clean = text.trim();
404
+ return /^[\w.]+$/.test(clean) ? clean : `(${clean})`;
405
+ }
406
+ function scripted(text, map, mark) {
407
+ const letters = [...text];
408
+ return letters.every((letter) => map[letter] !== undefined)
409
+ ? letters.map((letter) => map[letter]).join("")
410
+ : `${mark}(${text})`;
411
+ }
412
+ function figureCaption(source) {
413
+ const lines = source.split("\n");
414
+ const captions = lines
415
+ .filter((line) => line.startsWith("Caption: "))
416
+ .map((line) => line.slice(9));
417
+ const alt = lines.find((line) => line.startsWith("Alt text: "))?.slice(10);
418
+ return (captions.length > 0 ? captions.join(" ") : (alt ?? "")).trim();
419
+ }
420
+ function fenced(source) {
421
+ return source
422
+ .replace(/^\s*(```|~~~)[^\n]*\n?/, "")
423
+ .replace(/\n?(```|~~~)\s*$/, "")
424
+ .replace(/\t/g, " ")
425
+ .replace(/\s+$/, "");
426
+ }
427
+ function wrap(textValue, width, most) {
428
+ const words = textValue.split(/\s+/).filter(Boolean);
429
+ const lines = [];
430
+ let line = "";
431
+ for (const word of words) {
432
+ if (line === "")
433
+ line = word;
434
+ else if (line.length + 1 + word.length <= width)
435
+ line += ` ${word}`;
436
+ else {
437
+ lines.push(line);
438
+ line = word;
439
+ }
440
+ }
441
+ if (line !== "")
442
+ lines.push(line);
443
+ if (lines.length <= most)
444
+ return lines;
445
+ const kept = lines.slice(0, most);
446
+ kept[most - 1] = clip(`${kept[most - 1] ?? ""} …`, width);
447
+ return kept;
448
+ }
449
+ function clip(value, room) {
450
+ return value.length <= room ? value : `${value.slice(0, Math.max(1, room - 1))}…`;
451
+ }
452
+ // Text as libass draws it literally: braces and backslashes escaped rather than read as
453
+ // override tags; code keeps its spaces, prose has them collapsed.
454
+ function assText(value, keepSpaces = false) {
455
+ const escaped = value.replace(/\\/g, "\\\\").replace(/\{/g, "\\{").replace(/\}/g, "\\}");
456
+ return keepSpaces
457
+ ? escaped.replace(/ {2,}/g, (run) => "\\h".repeat(run.length))
458
+ : escaped.replace(/\s+/g, " ");
459
+ }
460
+ function colour(hex) {
461
+ const match = /^#([0-9A-Fa-f]{2})([0-9A-Fa-f]{2})([0-9A-Fa-f]{2})$/.exec(hex);
462
+ if (match === null)
463
+ return "&HFFFFFF&";
464
+ const [, r, g, b] = match;
465
+ return `&H${(b ?? "").toUpperCase()}${(g ?? "").toUpperCase()}${(r ?? "").toUpperCase()}&`;
466
+ }
467
+ function text(x, y, align, body) {
468
+ return `Dialogue: 0,0:00:00.00,0:00:10.00,Card,,0,0,0,,{\\an${String(align)}\\pos(${String(Math.round(x))},${String(Math.round(y))})}${body}`;
469
+ }
470
+ function rule(x, y, width, thick, hex) {
471
+ const [x0, y0, x1, y1] = [x, y, x + width, y + thick].map((n) => String(Math.round(n)));
472
+ return `Dialogue: 0,0:00:00.00,0:00:10.00,Card,,0,0,0,,{\\an7\\pos(0,0)\\p1\\bord0\\shad0\\c${colour(hex)}}m ${x0} ${y0} l ${x1} ${y0} l ${x1} ${y1} l ${x0} ${y1}{\\p0}`;
473
+ }
474
+ function script(input, events) {
475
+ const { width, height, style } = input;
476
+ return `${[
477
+ "[Script Info]",
478
+ "ScriptType: v4.00+",
479
+ `PlayResX: ${String(width)}`,
480
+ `PlayResY: ${String(height)}`,
481
+ "WrapStyle: 2",
482
+ "ScaledBorderAndShadow: yes",
483
+ "",
484
+ "[V4+ Styles]",
485
+ "Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding",
486
+ `Style: Card,${style.font.replace(/,/g, " ")},${String(Math.round(Math.min(width, height) * 0.03))},&H00FFFFFF,&H00FFFFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,0,0,7,0,0,0,1`,
487
+ "",
488
+ "[Events]",
489
+ "Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text",
490
+ ...events,
491
+ ].join("\n")}\n`;
492
+ }
@@ -0,0 +1,51 @@
1
+ // ceiling: the words compared at each end of a passage, and how many of them must match.
2
+ const edgeWords = 4;
3
+ const matchShare = 0.75;
4
+ export function passageSpans(words, passages) {
5
+ const flat = [];
6
+ for (const [index, word] of words.entries())
7
+ for (const token of tokens(word.text))
8
+ flat.push({ token, word: index });
9
+ let cursor = 0;
10
+ return passages.map((passage) => {
11
+ const wanted = tokens(passage);
12
+ if (wanted.length === 0)
13
+ return undefined;
14
+ const k = Math.min(edgeWords, wanted.length);
15
+ const head = wanted.slice(0, k);
16
+ const start = find(flat, head, cursor, flat.length);
17
+ if (start === undefined)
18
+ return undefined;
19
+ const tail = wanted.slice(-k);
20
+ const from = start + Math.max(0, Math.floor(wanted.length / 2) - k);
21
+ const to = Math.min(flat.length, start + Math.ceil(wanted.length * 1.5) + k);
22
+ const last = find(flat, tail, from, to);
23
+ const endToken = Math.min(flat.length - 1, last === undefined ? start + wanted.length - 1 : last + k - 1);
24
+ cursor = endToken + 1;
25
+ const first = words[flat[start]?.word ?? 0];
26
+ const final = words[flat[endToken]?.word ?? 0];
27
+ return first === undefined || final === undefined
28
+ ? undefined
29
+ : { start: first.start, end: Math.max(first.start, final.end) };
30
+ });
31
+ }
32
+ function find(flat, wanted, from, to) {
33
+ const need = Math.ceil(wanted.length * matchShare);
34
+ for (let at = from; at + wanted.length <= Math.max(to, from + wanted.length) && at + wanted.length <= flat.length; at++) {
35
+ let hits = 0;
36
+ for (const [offset, token] of wanted.entries())
37
+ if (flat[at + offset]?.token === token)
38
+ hits += 1;
39
+ if (hits >= need)
40
+ return at;
41
+ }
42
+ return undefined;
43
+ }
44
+ function tokens(text) {
45
+ return text
46
+ .toLowerCase()
47
+ .normalize("NFKD")
48
+ .replace(/\p{M}/gu, "")
49
+ .split(/[^\p{L}\p{N}]+/u)
50
+ .filter(Boolean);
51
+ }