@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
@@ -4,6 +4,10 @@ import remarkGfm from "remark-gfm";
4
4
  // https://docs.inworld.ai/tts/capabilities/custom-pronunciation
5
5
  const ipaAtom = "[ˈˌ]?[abdefghijklmnoprstuvwxzæðŋθɑɒɔəɚɛɜɝɡɪɹʃʊʌʒʔɫɾ][\\u0303\\u031a\\u0325\\u0329\\u032a\\u032c\\u032f\\u035c\\u0361ʰʲʷ]*[ːˑ]?";
6
6
  const ipaSymbols = new RegExp(`^(?:${ipaAtom})+(?:\\.(?:${ipaAtom})+)*$`, "u");
7
+ // Another language's glossary needs that language's sounds: the full IPA consonant and vowel
8
+ // charts (ç, ʁ, ø, y, ɲ, ʎ, x, β, …) and its diacritics, still never ARPAbet or tags.
9
+ const worldAtom = "[ˈˌ]?[a-zæçðøħŋœɐɑɒɓɔɕɖɗɘəɚɛɜɝɞɟɠɡɢɣɤɥɦɧɨɪɫɬɭɮɯɰɱɲɳɴɵɶɸɹɺɻɽɾʀʁʂʃʄʈʉʊʋʌʍʎʏʐʑʒʔʕʙʛʜʝʟʡʢβθχ][\\u0300-\\u036fʰʱʲʷʼˠˤ˞]*[ːˑ]?";
10
+ const worldSymbols = new RegExp(`^(?:${worldAtom})+(?:\\.(?:${worldAtom})+)*$`, "u");
7
11
  function textOf(node) {
8
12
  if (node.type === "break")
9
13
  return "\n";
@@ -18,7 +22,12 @@ function rowsOf(node) {
18
22
  const rows = node.children ?? [];
19
23
  return rows.slice(1).map((row) => {
20
24
  const cells = row.children ?? [];
21
- return cells.length >= 2 ? cells.slice(0, 2).map(textOf).join(": ") : textOf(row);
25
+ if (cells.length < 2)
26
+ return textOf(row);
27
+ const [term, ipa] = cells.slice(0, 2).map((cell) => textOf(cell).trim());
28
+ // A table's IPA column is often written without the slashes ("| Tiamat | ˈtiːəmɑːt |");
29
+ // the column already says what the cell is, so a bare cell reads as one /…/ pronunciation.
30
+ return `${term}: ${ipa !== undefined && ipa !== "" && !ipa.includes("/") ? `/${ipa}/` : ipa}`;
22
31
  });
23
32
  }
24
33
  if (node.type === "paragraph")
@@ -27,16 +36,6 @@ function rowsOf(node) {
27
36
  return (node.children ?? []).flatMap(rowsOf);
28
37
  return ["(unsupported glossary block)"];
29
38
  }
30
- function refused(row, reason) {
31
- return {
32
- ok: false,
33
- reason: "Pronunciation Glossary entry " +
34
- row +
35
- ": " +
36
- reason +
37
- ". Edit the glossary or turn off Use Pronunciation Glossary.",
38
- };
39
- }
40
39
  function termIdentity(term) {
41
40
  return Array.from(term, (character) => {
42
41
  const lower = character.toLowerCase();
@@ -48,10 +47,19 @@ function termIdentity(term) {
48
47
  return match.test(folded) ? folded : match.test(lower) ? lower : character;
49
48
  }).join("");
50
49
  }
51
- export function parsePronunciationGlossary(markdown) {
50
+ // `language` is the project's (`kernel/ports/languages.ts`): absent or English accepts the
51
+ // standard-English IPA Inworld asks for, as it always did; any other accepts full IPA.
52
+ export function parsePronunciationGlossary(markdown, language) {
53
+ const english = language === undefined || language === "en";
54
+ const symbols = english ? ipaSymbols : worldSymbols;
55
+ const kind = english ? "standard-English IPA" : "IPA";
52
56
  const rows = rowsOf(remark().use(remarkGfm).parse(markdown));
53
57
  const entries = new Map();
58
+ const skipped = [];
54
59
  for (const [index, row] of rows.entries()) {
60
+ const skip = (reason) => {
61
+ skipped.push({ row: index + 1, reason });
62
+ };
55
63
  const trimmed = row.trim();
56
64
  if (trimmed === "" || /^pronunciation glossary:?\s*$/iu.test(trimmed))
57
65
  continue;
@@ -61,24 +69,63 @@ export function parsePronunciationGlossary(markdown) {
61
69
  if (term === undefined ||
62
70
  pronunciation === undefined ||
63
71
  !/[\p{L}\p{N}]/u.test(term) ||
64
- /[/[\]<>\p{Cc}]/u.test(term))
65
- return refused(index + 1, "use Term: /IPA/ or a Term | IPA table");
72
+ /[/[\]<>\p{Cc}]/u.test(term)) {
73
+ skip("use Term: /IPA/ or a Term | IPA table");
74
+ continue;
75
+ }
66
76
  const notation = /^(\/[^/]+\/(?:\s+\/[^/]+\/)*)(?:\s+[^/]+)?$/u.exec(pronunciation);
67
- if (notation?.[1] === undefined)
68
- return refused(index + 1, "use slash-delimited standard-English IPA");
77
+ if (notation?.[1] === undefined) {
78
+ skip(`use slash-delimited ${kind}`);
79
+ continue;
80
+ }
69
81
  const ipa = Array.from(notation[1].matchAll(/\/([^/]+)\//gu)).flatMap((match) => (match[1] ?? "").trim().split(/\s+/u));
70
- if (ipa.length === 0 || ipa.some((word) => !ipaSymbols.test(word)))
71
- return refused(index + 1, "use standard-English IPA, not ARPAbet or delivery tags");
72
- if (term.split(" ").length !== ipa.length)
73
- return refused(index + 1, "supply one IPA word for each written word");
82
+ if (ipa.length === 0 || ipa.some((word) => !symbols.test(word))) {
83
+ skip(english
84
+ ? "use standard-English IPA only, not ARPAbet, delivery tags or non-English sounds; give a foreign name an English approximation"
85
+ : "use IPA, not ARPAbet or delivery tags");
86
+ continue;
87
+ }
88
+ if (term.split(" ").length !== ipa.length) {
89
+ skip("supply one IPA word for each written word");
90
+ continue;
91
+ }
74
92
  const key = termIdentity(term);
75
93
  const previous = entries.get(key);
76
- if (previous !== undefined && previous.ipa.join(" ") !== ipa.join(" "))
77
- return refused(index + 1, "conflicting pronunciations were supplied for the same term");
94
+ if (previous !== undefined && previous.ipa.join(" ") !== ipa.join(" ")) {
95
+ skip("an earlier entry already gives this term a different pronunciation");
96
+ continue;
97
+ }
78
98
  if (previous === undefined)
79
99
  entries.set(key, { term, ipa });
80
100
  }
81
- return { ok: true, entries: [...entries.values()] };
101
+ return skipped.length === 0
102
+ ? { ok: true, entries: [...entries.values()] }
103
+ : { ok: true, entries: [...entries.values()], skipped };
104
+ }
105
+ // What a person reads about the skipped rows: where they are and why, never their text.
106
+ export function skippedGlossaryNotice(skipped) {
107
+ if (skipped.length === 0)
108
+ return null;
109
+ const rows = skipped.map((one) => `entry ${one.row}: ${one.reason}`).join("; ");
110
+ return (`Pronunciation Glossary: ${skipped.length === 1 ? "1 entry is" : `${skipped.length} entries are`} ` +
111
+ `skipped and read as ordinary text (${rows}). The rest of the glossary is used. ` +
112
+ "To use them, fix those entries in the Pronunciation Glossary at the end of the article in Edit project → Article.");
113
+ }
114
+ export function skippedSpeakerPronunciations(speakers, language) {
115
+ return speakers.flatMap((speaker) => {
116
+ if (!speaker.pronunciations?.trim())
117
+ return [];
118
+ const parsed = parsePronunciationGlossary(speaker.pronunciations, language);
119
+ const skipped = parsed.ok ? (parsed.skipped ?? []) : [];
120
+ return skipped.length === 0 ? [] : [{ speaker: speaker.name.trim() || "A speaker", skipped }];
121
+ });
122
+ }
123
+ export function skippedSpeakerPronunciationsNotice(rows) {
124
+ if (rows.length === 0)
125
+ return null;
126
+ const parts = rows.map((row) => `${row.speaker}: ${row.skipped.map((one) => `entry ${one.row}: ${one.reason}`).join("; ")}`);
127
+ return (`Speaker pronunciations: some entries are skipped and read as ordinary text (${parts.join(". ")}). ` +
128
+ "The rest are used. To use them, fix those entries under Speakers → Pronunciations for that speaker (Play → Audio, or Edit project → Providers).");
82
129
  }
83
130
  export function pronunciationMatches(source, entries) {
84
131
  if (entries.length === 0)
@@ -105,7 +152,7 @@ export function pronunciationMatches(source, entries) {
105
152
  continue;
106
153
  const entry = ordered.find((_, index) => match[index + 1] !== undefined);
107
154
  if (entry === undefined)
108
- throw new Error("Slopify hit an internal error (a pronunciation glossary term has no pronunciation). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
155
+ throw new Error("Slopify hit an internal error (a pronunciation glossary term has no pronunciation). Try again; if it happens again, use Download diagnostics in Settings and report it.");
109
156
  matches.push({ start: match.index, end, entry });
110
157
  }
111
158
  return matches;
@@ -117,7 +164,7 @@ export function pronunciationSpans(source, entries) {
117
164
  for (const [index, word] of Array.from(words).entries()) {
118
165
  const ipa = match.entry.ipa[index];
119
166
  if (ipa === undefined)
120
- throw new Error("Slopify hit an internal error (a pronunciation glossary term is missing a word). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
167
+ throw new Error("Slopify hit an internal error (a pronunciation glossary term is missing a word). Try again; if it happens again, use Download diagnostics in Settings and report it.");
121
168
  const start = match.start + word.index;
122
169
  spans.push({ start, end: start + word[0].length, text: `/${ipa}/` });
123
170
  }
@@ -14,7 +14,7 @@ import { planLegacyNarration } from "./legacy-plan.js";
14
14
  import { observeNarration } from "./live.js";
15
15
  // An empty narration source fails the stage immediately with no retries. Thrown from the
16
16
  // slice rather than from a provider call, so the attempt wrapper never sees it.
17
- export const nothingToNarrate = "There is nothing to narrate because the article is empty. Add article text in Edit project → Article, then Retry stage.";
17
+ export const nothingToNarrate = "There is nothing to narrate because the article is empty. Add article text in Edit project → Article, then Try again.";
18
18
  // What a chunk piece carries between runs: the text that was sent, and the file it came
19
19
  // back as once the provider answered. That is the whole of the resume.
20
20
  const chunkPayload = z.object({ text: z.string(), file: z.string().optional() });
@@ -41,7 +41,7 @@ export async function runNarration(deps, context, providers) {
41
41
  if (choice === undefined) {
42
42
  // Admission refuses a run whose audio is Generate without a provider and a voice, so
43
43
  // reaching here is a bug in admission rather than something the user did.
44
- throw new Error("No voice is set for the narration. Choose a voice in Edit project → Providers, then Retry stage.");
44
+ throw new Error("No voice is set for the narration. Choose a voice in Edit project → Providers, then Try again.");
45
45
  }
46
46
  // Read once, before anything is written: the segment steps below ask it what a previous
47
47
  // run already stored, and the body step is the only thing that adds to it.
@@ -168,7 +168,7 @@ async function speakSegments(deps, context, providers, choice, outputs) {
168
168
  if (text === "") {
169
169
  // The same rule, applied to a segment: there is nothing to say, and silently
170
170
  // dropping an intro the user picked would lose it without telling anyone.
171
- throw new Error(`The ${segment.category} is empty, so there is nothing to narrate for it. Write the ${segment.category} or remove it in Edit project, then Retry stage.`);
171
+ throw new Error(`The ${segment.category} is empty, so there is nothing to narrate for it. Write the ${segment.category} or remove it in Edit project, then Try again.`);
172
172
  }
173
173
  const spoken = await providers.forPiece(piece.id).tts({
174
174
  provider: choice.provider,
@@ -200,7 +200,7 @@ function segmentPieces(deps, projectId) {
200
200
  if (article === undefined) {
201
201
  // Admission writes all six stage rows with the project, so a project without an article
202
202
  // stage is a bug rather than a run the user configured.
203
- throw new Error("Slopify hit an internal error (the project has no article stage). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
203
+ throw new Error("Slopify hit an internal error (the project has no article stage). Try again; if it happens again, use Download diagnostics in Settings and report it.");
204
204
  }
205
205
  return piecesOf(deps.db, article.id, "segment");
206
206
  }
@@ -257,13 +257,13 @@ function store(deps, projectId, role, path, durationMs, choice) {
257
257
  }
258
258
  function chunkOf(piece) {
259
259
  if (piece.payload === null) {
260
- throw new Error("Slopify hit an internal error (a saved narration chunk is empty). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
260
+ throw new Error("Slopify hit an internal error (a saved narration chunk is empty). Try again; if it happens again, use Download diagnostics in Settings and report it.");
261
261
  }
262
262
  return chunkPayload.parse(JSON.parse(piece.payload));
263
263
  }
264
264
  function segmentOf(piece) {
265
265
  if (piece.payload === null) {
266
- throw new Error("Slopify hit an internal error (a saved intro or outro is empty). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
266
+ throw new Error("Slopify hit an internal error (a saved intro or outro is empty). Try again; if it happens again, use Download diagnostics in Settings and report it.");
267
267
  }
268
268
  return segmentPayload.parse(JSON.parse(piece.payload));
269
269
  }
@@ -22,7 +22,7 @@ export function collectSharedGlossary(deps, exceptProjectId) {
22
22
  continue;
23
23
  }
24
24
  const parsed = parsePronunciationGlossary(text);
25
- // A glossary that doesn't parse can't be trusted in someone else's narration either.
25
+ // Rows it had to skip are left out; a glossary that does not parse at all shares nothing.
26
26
  if (!parsed.ok)
27
27
  continue;
28
28
  for (const entry of parsed.entries) {
@@ -9,21 +9,32 @@ function atoms(source, from, to, spans) {
9
9
  const span = spans.get(at);
10
10
  if (span !== undefined) {
11
11
  if (span.end > to || span.end <= at)
12
- throw new Error("Slopify hit an internal error (a pronunciation fix spans more than one word). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
12
+ throw new Error("Slopify hit an internal error (a pronunciation fix spans more than one word). Try again; if it happens again, use Download diagnostics in Settings and report it.");
13
13
  result.push({ text: span.text, spokenText: source.slice(at, span.end) });
14
14
  at = span.end;
15
15
  }
16
16
  else {
17
17
  const point = source.codePointAt(at);
18
18
  if (point === undefined)
19
- throw new Error("Slopify hit an internal error (a narration position is outside the article text). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
19
+ throw new Error("Slopify hit an internal error (a narration position is outside the article text). Try again; if it happens again, use Download diagnostics in Settings and report it.");
20
20
  const text = String.fromCodePoint(point);
21
- result.push({ text, spokenText: text });
21
+ // An empty span is a later word of a multi-word alias (its spoken form sits on the first
22
+ // word), so the whitespace in front of it would leave two spaces where one word was.
23
+ // It stays in the spoken (transcript) text, which the captions are aligned against.
24
+ result.push({ text: silencedBefore(source, at, spans) ? "" : text, spokenText: text });
22
25
  at += text.length;
23
26
  }
24
27
  }
25
28
  return result;
26
29
  }
30
+ function silencedBefore(source, at, spans) {
31
+ if (!/\s/u.test(source[at] ?? ""))
32
+ return false;
33
+ let next = at;
34
+ while (next < source.length && /\s/u.test(source[next] ?? ""))
35
+ next += 1;
36
+ return spans.get(next)?.text === "";
37
+ }
27
38
  function checkedSpans(source, pronunciation) {
28
39
  const spans = new Map();
29
40
  let end = 0;
@@ -34,7 +45,7 @@ function checkedSpans(source, pronunciation) {
34
45
  span.end <= span.start ||
35
46
  span.end > source.length ||
36
47
  !/^[^\s\uD800-\uDFFF]+$/u.test(source.slice(span.start, span.end)))
37
- throw new Error("Slopify hit an internal error (pronunciation fixes overlap or split a word). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
48
+ throw new Error("Slopify hit an internal error (pronunciation fixes overlap or split a word). Try again; if it happens again, use Download diagnostics in Settings and report it.");
38
49
  spans.set(span.start, span);
39
50
  end = span.end;
40
51
  }
@@ -0,0 +1,94 @@
1
+ import { redact } from "../../kernel/log.js";
2
+ import { noticeOf, noticeText, topicsNoticeText, webhookBody } from "./rules.js";
3
+ import { sendFailureText } from "./send.js";
4
+ export function createRunNotifier(deps) {
5
+ // ceiling: one entry per project seen since boot, a few dozen bytes each.
6
+ const states = new Map();
7
+ const reasons = new Map();
8
+ const inflight = new Set();
9
+ let closed = false;
10
+ const notify = (projectId, kind) => {
11
+ const url = deps.url();
12
+ if (url === null)
13
+ return;
14
+ const subject = deps.subject(projectId);
15
+ if (subject === undefined)
16
+ return;
17
+ const text = noticeText(kind, { ...subject, reason: reasons.get(projectId) });
18
+ post(url, text, kind, { projectId });
19
+ };
20
+ const post = (url, text, kind, about) => {
21
+ const body = redact(webhookBody(text));
22
+ const sending = deps
23
+ .send(url, body)
24
+ .then((result) => {
25
+ if (!result.ok)
26
+ deps.log.write("warn", "notification.failed", {
27
+ ...about,
28
+ detail: `${kind} notification not delivered: ${sendFailureText(result)}`,
29
+ });
30
+ })
31
+ .catch((error) => {
32
+ deps.log.write("warn", "notification.failed", {
33
+ ...about,
34
+ detail: `${kind} notification not delivered: ${error instanceof Error ? error.message : String(error)}`,
35
+ });
36
+ })
37
+ .finally(() => {
38
+ inflight.delete(sending);
39
+ });
40
+ inflight.add(sending);
41
+ };
42
+ return {
43
+ observe: (event) => {
44
+ if (closed)
45
+ return;
46
+ try {
47
+ if (event.type === "stage.state") {
48
+ if (event.state === "failed" && event.failureReason !== undefined)
49
+ reasons.set(event.projectId, event.failureReason);
50
+ else if (event.state === "running")
51
+ reasons.delete(event.projectId);
52
+ return;
53
+ }
54
+ if (event.type !== "project.state")
55
+ return;
56
+ const previous = states.get(event.projectId);
57
+ if (previous === event.state)
58
+ return;
59
+ states.set(event.projectId, event.state);
60
+ const kind = noticeOf(previous, event.state);
61
+ if (kind !== undefined)
62
+ notify(event.projectId, kind);
63
+ }
64
+ catch (error) {
65
+ deps.log.write("warn", "notification.failed", {
66
+ projectId: event.projectId,
67
+ detail: `notification skipped: ${error instanceof Error ? error.message : String(error)}`,
68
+ });
69
+ }
70
+ },
71
+ observeTopics: (event) => {
72
+ if (closed || event.added === 0)
73
+ return;
74
+ try {
75
+ const url = deps.url();
76
+ if (url === null)
77
+ return;
78
+ post(url, topicsNoticeText(event), "topics", {});
79
+ }
80
+ catch (error) {
81
+ deps.log.write("warn", "notification.failed", {
82
+ detail: `topics notification skipped: ${error instanceof Error ? error.message : String(error)}`,
83
+ });
84
+ }
85
+ },
86
+ close: () => {
87
+ closed = true;
88
+ },
89
+ settled: async () => {
90
+ while (inflight.size > 0)
91
+ await Promise.all([...inflight]);
92
+ },
93
+ };
94
+ }
@@ -0,0 +1,102 @@
1
+ // Only a run seen running can finish, fail or stop to wait: a project first seen already done
2
+ // was done before anyone was watching. Paused and canceled are what the user just pressed, so
3
+ // they say nothing. `pending` after `running` is the runner finding nothing it may start: a
4
+ // review checkpoint holding the next step, or held work waiting for Resume.
5
+ export function noticeOf(previous, next) {
6
+ if (previous !== "running")
7
+ return undefined;
8
+ switch (next) {
9
+ case "done":
10
+ return "ready";
11
+ case "partial":
12
+ return "partial";
13
+ case "failed":
14
+ return "failed";
15
+ case "pending":
16
+ return "waiting";
17
+ default:
18
+ return undefined;
19
+ }
20
+ }
21
+ const reasonMax = 140;
22
+ // A provider's failure can run to paragraphs; a notification has a line.
23
+ export function shortReason(reason) {
24
+ const line = reason
25
+ ?.split(/\r?\n/)
26
+ .map((part) => part.trim())
27
+ .find((part) => part !== "");
28
+ if (line === undefined)
29
+ return undefined;
30
+ return line.length <= reasonMax ? line : `${line.slice(0, reasonMax - 1).trimEnd()}…`;
31
+ }
32
+ export function noticeText(kind, subject) {
33
+ const title = subject.title.trim() === "" ? "Untitled project" : subject.title.trim();
34
+ switch (kind) {
35
+ case "ready":
36
+ return subject.makesVideo
37
+ ? { headline: `Video ready: ${title}`, detail: "Open the project to watch it." }
38
+ : { headline: `Run finished: ${title}`, detail: "Open the project to see its files." };
39
+ case "partial": {
40
+ const reason = shortReason(subject.reason);
41
+ const made = subject.makesVideo
42
+ ? `Video ready with problems: ${title}`
43
+ : `Run finished with problems: ${title}`;
44
+ return {
45
+ headline: reason === undefined ? made : `${made} — ${reason}`,
46
+ detail: "Open the project to see which step failed and the button that fixes it.",
47
+ };
48
+ }
49
+ case "failed": {
50
+ const reason = shortReason(subject.reason);
51
+ return {
52
+ headline: reason === undefined ? `Run failed: ${title}` : `Run failed: ${title} — ${reason}`,
53
+ detail: "Open the project to see what stopped it and press Retry.",
54
+ };
55
+ }
56
+ case "waiting":
57
+ return {
58
+ headline: `Waiting for you: ${title}`,
59
+ detail: "Open the project to review the held step or press Continue the run.",
60
+ };
61
+ }
62
+ }
63
+ export function topicsNoticeText(subject) {
64
+ const name = subject.scheduleName.trim() === "" ? "Untitled schedule" : subject.scheduleName.trim();
65
+ const count = subject.added;
66
+ const total = subject.waiting > count ? ` ${String(subject.waiting)} are waiting in all.` : "";
67
+ return {
68
+ headline: `${String(count)} new ${count === 1 ? "topic is" : "topics are"} waiting for you`,
69
+ detail: `Open Calendar → Suggested topics → ${name} to queue or reject ${count === 1 ? "it" : "them"}.${total}`,
70
+ };
71
+ }
72
+ export const testNotice = {
73
+ headline: "Slopify test notification",
74
+ detail: "Notifications work. You'll get one when a run finishes, fails or waits for you.",
75
+ };
76
+ // The body a Notification URL receives: plain text, which ntfy shows as the message. Nothing
77
+ // but the project's title, what happened and the provider's own short reason goes in it.
78
+ export function webhookBody(text) {
79
+ return `${text.headline}\n${text.detail}\n`;
80
+ }
81
+ export const notificationUrlMax = 2048;
82
+ // Empty clears the setting. Otherwise one absolute http(s) URL with no user name or password:
83
+ // fetch refuses those, and a password in a URL would sit in the database in plain sight.
84
+ export function notificationUrlProblem(value) {
85
+ const trimmed = value.trim();
86
+ if (trimmed === "")
87
+ return undefined;
88
+ if (trimmed.length > notificationUrlMax)
89
+ return `Keep the Notification URL to ${String(notificationUrlMax)} characters or fewer.`;
90
+ let parsed;
91
+ try {
92
+ parsed = new URL(trimmed);
93
+ }
94
+ catch {
95
+ return "Enter a full address starting with https:// or http://, for example https://ntfy.sh/your-topic.";
96
+ }
97
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:")
98
+ return "The Notification URL must start with https:// or http://.";
99
+ if (parsed.username !== "" || parsed.password !== "")
100
+ return "Remove the user name and password from the Notification URL; Slopify can't send them.";
101
+ return undefined;
102
+ }
@@ -0,0 +1,40 @@
1
+ // One POST to the user's Notification URL. Short, single-shot and never retried: a
2
+ // notification that did not arrive is logged, and a run never waits on one.
3
+ export const notificationTimeoutMs = 5_000;
4
+ export function createNotificationSender(fetchImpl, timeoutMs = notificationTimeoutMs) {
5
+ return async (url, body) => {
6
+ try {
7
+ const response = await fetchImpl(url, {
8
+ method: "POST",
9
+ headers: { "content-type": "text/plain; charset=utf-8" },
10
+ body,
11
+ redirect: "manual",
12
+ signal: AbortSignal.timeout(timeoutMs),
13
+ });
14
+ // The answer's body is never read; cancelling it frees the socket.
15
+ await response.body?.cancel().catch(() => { });
16
+ return response.ok ? { ok: true } : { ok: false, reason: "refused", status: response.status };
17
+ }
18
+ catch (error) {
19
+ return {
20
+ ok: false,
21
+ reason: error instanceof DOMException &&
22
+ (error.name === "TimeoutError" || error.name === "AbortError")
23
+ ? "timeout"
24
+ : "unreachable",
25
+ };
26
+ }
27
+ };
28
+ }
29
+ // What went wrong, in words fit for both the log and the Settings screen. The URL itself is
30
+ // never repeated: an ntfy topic in it is as good as a password.
31
+ export function sendFailureText(result) {
32
+ switch (result.reason) {
33
+ case "timeout":
34
+ return `the Notification URL didn't answer within ${String(notificationTimeoutMs / 1000)} seconds`;
35
+ case "unreachable":
36
+ return "the Notification URL couldn't be reached (wrong address, no network, or the server is down)";
37
+ case "refused":
38
+ return `the Notification URL answered HTTP ${String(result.status)}`;
39
+ }
40
+ }
@@ -0,0 +1,34 @@
1
+ import { readSetting, writeSetting } from "../settings/repo.js";
2
+ import { notificationUrlProblem } from "./rules.js";
3
+ // Its own row in the key/value `settings` table. Backups leave it out
4
+ // (`slices/storage/portable.ts`): an ntfy topic in the URL is as good as a password.
5
+ export const notificationUrlKey = "notificationUrl";
6
+ export function readNotificationUrl(db) {
7
+ const stored = readSetting(db, notificationUrlKey);
8
+ if (stored === undefined)
9
+ return null;
10
+ let value;
11
+ try {
12
+ value = JSON.parse(stored);
13
+ }
14
+ catch {
15
+ return null;
16
+ }
17
+ // A hand-edited row that no longer passes the rules is treated as not set rather than
18
+ // POSTed to.
19
+ return typeof value === "string" && value !== "" && notificationUrlProblem(value) === undefined
20
+ ? value
21
+ : null;
22
+ }
23
+ export function saveNotificationUrl(db, raw) {
24
+ const problem = notificationUrlProblem(raw);
25
+ if (problem !== undefined)
26
+ return { ok: false, message: problem };
27
+ const url = raw.trim();
28
+ if (url === "") {
29
+ db.prepare("DELETE FROM settings WHERE key = ?").run(notificationUrlKey);
30
+ return { ok: true, url: null };
31
+ }
32
+ writeSetting(db, notificationUrlKey, JSON.stringify(url));
33
+ return { ok: true, url };
34
+ }