@gentbajko/slopify 2.5.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (446) hide show
  1. package/LICENSE +201 -21
  2. package/NOTICE +12 -0
  3. package/README.md +21 -40
  4. package/SUBTITLES.md +1 -1
  5. package/dist/adapter-registry.js +3 -0
  6. package/dist/adapters/alignment/cache.js +4 -1
  7. package/dist/adapters/alignment/ctc.js +25 -23
  8. package/dist/adapters/alignment/index.js +37 -6
  9. package/dist/adapters/alignment/multilingual.js +267 -0
  10. package/dist/adapters/alignment/protocol.js +11 -0
  11. package/dist/adapters/alignment/quality.js +4 -3
  12. package/dist/adapters/alignment/sentences.js +95 -0
  13. package/dist/adapters/alignment/spec.js +24 -0
  14. package/dist/adapters/alignment/spell.js +769 -0
  15. package/dist/adapters/alignment/text.js +56 -9
  16. package/dist/adapters/alignment/window.js +34 -30
  17. package/dist/adapters/alignment/worker.js +23 -21
  18. package/dist/adapters/explain.js +11 -11
  19. package/dist/adapters/host-cli/index.js +44 -5
  20. package/dist/adapters/host-cli/transport.js +7 -5
  21. package/dist/adapters/image/bytes.js +2 -2
  22. package/dist/adapters/image/codex-output.js +1 -1
  23. package/dist/adapters/image/codex.js +87 -22
  24. package/dist/adapters/image/fal.js +13 -5
  25. package/dist/adapters/image/google.js +11 -8
  26. package/dist/adapters/image/openai.js +14 -7
  27. package/dist/adapters/image/reference.js +34 -0
  28. package/dist/adapters/image/replicate.js +7 -2
  29. package/dist/adapters/image/video.js +2 -2
  30. package/dist/adapters/key-probes.js +36 -0
  31. package/dist/adapters/llm/claude-code.js +168 -32
  32. package/dist/adapters/llm/cli-login-error.js +1 -1
  33. package/dist/adapters/llm/codex-limits.js +157 -0
  34. package/dist/adapters/llm/codex.js +99 -12
  35. package/dist/adapters/llm/document-workspace.js +2 -2
  36. package/dist/adapters/llm/gemini-workspace.js +1 -1
  37. package/dist/adapters/llm/gemini.js +16 -3
  38. package/dist/adapters/llm/image-workspace.js +58 -0
  39. package/dist/adapters/llm/openrouter.js +11 -3
  40. package/dist/adapters/llm/run-cli.js +12 -7
  41. package/dist/adapters/tts/cartesia.js +10 -0
  42. package/dist/adapters/tts/elevenlabs.js +31 -9
  43. package/dist/adapters/tts/inworld-async.js +9 -5
  44. package/dist/adapters/tts/inworld.js +17 -3
  45. package/dist/adapters/tts/openai.js +3 -0
  46. package/dist/adapters/tts/voice-languages.js +80 -0
  47. package/dist/assets/fonts/noto/NotoSans-Regular.ttf +0 -0
  48. package/dist/assets/fonts/noto/NotoSansArabic-Regular.ttf +0 -0
  49. package/dist/assets/fonts/noto/NotoSansDevanagari-Regular.ttf +0 -0
  50. package/dist/assets/fonts/noto/NotoSansHebrew-Regular.ttf +0 -0
  51. package/dist/assets/fonts/noto/NotoSansThai-Regular.ttf +0 -0
  52. package/dist/assets/fonts/noto/OFL.txt +93 -0
  53. package/dist/assets/fonts/noto/SOURCE.txt +18 -0
  54. package/dist/assets/sample/sample-audiobook.tar +0 -0
  55. package/dist/assets/sample/sample-podcast.tar +0 -0
  56. package/dist/assets/sample/sample-project.tar +0 -0
  57. package/dist/catalog/merge.js +132 -0
  58. package/dist/catalog/registry.js +13 -13
  59. package/dist/catalog/schema.js +2 -0
  60. package/dist/catalog/store.js +147 -8
  61. package/dist/compose.yaml +71 -0
  62. package/dist/edge/autostart/docker-record.js +32 -0
  63. package/dist/edge/autostart/docker.js +50 -0
  64. package/dist/edge/autostart/index.js +78 -0
  65. package/dist/edge/autostart/launcher.js +176 -0
  66. package/dist/edge/autostart/model.js +3 -0
  67. package/dist/edge/autostart/native.js +210 -0
  68. package/dist/edge/autostart/prompt.js +66 -0
  69. package/dist/edge/autostart/service.js +81 -0
  70. package/dist/edge/cli.js +57 -51
  71. package/dist/edge/docker-install/activation.js +56 -0
  72. package/dist/edge/docker-install/apply.js +458 -0
  73. package/dist/edge/docker-install/engine.js +362 -0
  74. package/dist/edge/docker-install/run.js +154 -0
  75. package/dist/edge/docker-install/state.js +146 -0
  76. package/dist/edge/{docker-projects → docker-install}/volume.js +1 -5
  77. package/dist/edge/docker.js +14 -22
  78. package/dist/edge/host-cli.js +1 -1
  79. package/dist/edge/http/actions.js +32 -10
  80. package/dist/edge/http/app.js +58 -2
  81. package/dist/edge/http/auditions.js +70 -0
  82. package/dist/edge/http/autostart.js +37 -0
  83. package/dist/edge/http/channel-memory.js +92 -0
  84. package/dist/edge/http/channels.js +128 -0
  85. package/dist/edge/http/entries.js +22 -3
  86. package/dist/edge/http/files.js +1 -1
  87. package/dist/edge/http/folder-location.js +1 -1
  88. package/dist/edge/http/home.js +39 -0
  89. package/dist/edge/http/host-cli.js +29 -5
  90. package/dist/edge/http/narration-peaks.js +76 -0
  91. package/dist/edge/http/onboarding.js +152 -0
  92. package/dist/edge/http/open-folder.js +1 -1
  93. package/dist/edge/http/patch-notes.js +45 -0
  94. package/dist/edge/http/planning.js +4 -2
  95. package/dist/edge/http/project-create.js +87 -0
  96. package/dist/edge/http/projects.js +31 -99
  97. package/dist/edge/http/prompts.js +40 -3
  98. package/dist/edge/http/pronunciations.js +23 -4
  99. package/dist/edge/http/providers.js +86 -0
  100. package/dist/edge/http/reviews.js +121 -0
  101. package/dist/edge/http/revision-files.js +1 -1
  102. package/dist/edge/http/revisions.js +1 -1
  103. package/dist/edge/http/run-cost.js +25 -0
  104. package/dist/edge/http/schedules.js +111 -6
  105. package/dist/edge/http/settings.js +84 -6
  106. package/dist/edge/http/storage-files.js +50 -0
  107. package/dist/edge/http/storage.js +29 -2
  108. package/dist/edge/http/studio.js +207 -0
  109. package/dist/edge/http/style-preview.js +100 -0
  110. package/dist/edge/http/subtitles.js +1 -1
  111. package/dist/edge/http/trash.js +69 -0
  112. package/dist/edge/http/update.js +24 -3
  113. package/dist/edge/http/whats-new.js +15 -0
  114. package/dist/edge/http/youtube-edits.js +83 -0
  115. package/dist/edge/native-update.js +62 -0
  116. package/dist/host-cli/open-folder.js +10 -10
  117. package/dist/host-cli/runtime.js +9 -1
  118. package/dist/kernel/db/migrations/0025-automatic-reviews.sql +40 -0
  119. package/dist/kernel/db/migrations/0026-schedule-topic-generation.sql +23 -0
  120. package/dist/kernel/db/migrations/0027-run-cost.sql +58 -0
  121. package/dist/kernel/db/migrations/0030-automatic-retries.sql +18 -0
  122. package/dist/kernel/db/migrations/0031-prompt-history-and-description-edits.sql +33 -0
  123. package/dist/kernel/db/migrations/0032-channels.sql +65 -0
  124. package/dist/kernel/db/migrations/0034-script-prompts.sql +23 -0
  125. package/dist/kernel/db/migrations/0035-narration-aliases.sql +13 -0
  126. package/dist/kernel/db/migrations/0037-project-uploads.sql +8 -0
  127. package/dist/kernel/db/migrations/0038-voice-languages.sql +7 -0
  128. package/dist/kernel/db/migrations/0039-channel-essentials.sql +64 -0
  129. package/dist/kernel/db/migrations/0040-standalone-usage.sql +35 -0
  130. package/dist/kernel/paths.js +27 -6
  131. package/dist/kernel/pipeline.js +4 -1
  132. package/dist/kernel/ports/host-cli.js +122 -14
  133. package/dist/kernel/ports/key-probe.js +1 -0
  134. package/dist/kernel/ports/languages.js +157 -0
  135. package/dist/kernel/ports/model.js +5 -2
  136. package/dist/kernel/ports/narration-aliases.js +60 -0
  137. package/dist/kernel/ports/plan-limits.js +36 -0
  138. package/dist/kernel/runner/attempt.js +17 -8
  139. package/dist/kernel/runner/audition.js +36 -0
  140. package/dist/kernel/runner/graph.js +24 -5
  141. package/dist/kernel/runner/index.js +35 -5
  142. package/dist/kernel/runner/meter.js +1 -0
  143. package/dist/kernel/runner/providers.js +142 -7
  144. package/dist/kernel/runner/retry-policy.js +43 -0
  145. package/dist/kernel/runner/standalone.js +134 -0
  146. package/dist/kernel/runner/work-authority.js +28 -2
  147. package/dist/main.js +272 -16
  148. package/dist/patch-notes/0.4.2-to-1.0.md +93 -0
  149. package/dist/patch-notes/1.0-to-2.0.md +74 -0
  150. package/dist/patch-notes/2.0-to-3.0.md +257 -0
  151. package/dist/patch-notes/3.0.0.md +70 -0
  152. package/dist/patch-notes/index.json +26 -0
  153. package/dist/slices/admission/model.js +27 -0
  154. package/dist/slices/admission/repo.js +24 -4
  155. package/dist/slices/admission/rules.js +79 -1
  156. package/dist/slices/admission/schema.js +58 -1
  157. package/dist/slices/admission/short-mode.js +47 -0
  158. package/dist/slices/admission/start.js +41 -5
  159. package/dist/slices/article/continuation.js +2 -2
  160. package/dist/slices/article/provided-entries.js +2 -2
  161. package/dist/slices/article/run.js +6 -2
  162. package/dist/slices/article/segments.js +3 -3
  163. package/dist/slices/backups/folder.js +14 -6
  164. package/dist/slices/batch/index.js +7 -2
  165. package/dist/slices/cancel/index.js +1 -1
  166. package/dist/slices/channels/cast-images.js +110 -0
  167. package/dist/slices/channels/cast-match.js +32 -0
  168. package/dist/slices/channels/images.js +19 -0
  169. package/dist/slices/channels/model.js +5 -0
  170. package/dist/slices/channels/rebrand.js +100 -0
  171. package/dist/slices/channels/repo.js +218 -0
  172. package/dist/slices/channels/runs.js +129 -0
  173. package/dist/slices/channels/schema.js +129 -0
  174. package/dist/slices/channels/service.js +180 -0
  175. package/dist/slices/channels/studio-csv.js +110 -0
  176. package/dist/slices/channels/videos.js +120 -0
  177. package/dist/slices/checkpoints/recovery.js +10 -0
  178. package/dist/slices/control/index.js +2 -1
  179. package/dist/slices/document/fonts.js +1 -1
  180. package/dist/slices/document/render.js +1 -1
  181. package/dist/slices/episodes/related.js +90 -0
  182. package/dist/slices/episodes/repo.js +60 -0
  183. package/dist/slices/episodes/service.js +58 -0
  184. package/dist/slices/episodes/summarize.js +170 -0
  185. package/dist/slices/estimate/index.js +109 -28
  186. package/dist/slices/estimate/requests.js +60 -2
  187. package/dist/slices/fixes/rules.js +73 -0
  188. package/dist/slices/fonts/coverage.js +82 -0
  189. package/dist/slices/fonts/sfnt.js +74 -0
  190. package/dist/slices/images/run.js +8 -5
  191. package/dist/slices/images/scale.js +84 -0
  192. package/dist/slices/library/diff.js +122 -0
  193. package/dist/slices/library/history.js +100 -0
  194. package/dist/slices/library/model.js +3 -0
  195. package/dist/slices/library/photorealistic.js +49 -0
  196. package/dist/slices/library/repo.js +19 -14
  197. package/dist/slices/library/save.js +62 -12
  198. package/dist/slices/library/slots.js +7 -1
  199. package/dist/slices/library/used-by.js +153 -0
  200. package/dist/slices/loudness/level-pieces.js +41 -0
  201. package/dist/slices/loudness/loudnorm.js +164 -0
  202. package/dist/slices/loudness/model.js +153 -0
  203. package/dist/slices/model-upkeep/model.js +10 -0
  204. package/dist/slices/model-upkeep/switch.js +217 -0
  205. package/dist/slices/model-upkeep/usage.js +183 -0
  206. package/dist/slices/narration/aliases-library.js +31 -0
  207. package/dist/slices/narration/aliases-schema.js +53 -0
  208. package/dist/slices/narration/aliases.js +49 -0
  209. package/dist/slices/narration/blocks.js +352 -0
  210. package/dist/slices/narration/chunk.js +19 -7
  211. package/dist/slices/narration/concat.js +2 -2
  212. package/dist/slices/narration/describe.js +45 -0
  213. package/dist/slices/narration/pauses-model.js +63 -0
  214. package/dist/slices/narration/pauses.js +269 -0
  215. package/dist/slices/narration/peaks-model.js +1 -0
  216. package/dist/slices/narration/peaks.js +94 -0
  217. package/dist/slices/narration/preparation.js +21 -3
  218. package/dist/slices/narration/pronunciation-chunks.js +3 -3
  219. package/dist/slices/narration/pronunciation.js +72 -25
  220. package/dist/slices/narration/run.js +6 -6
  221. package/dist/slices/narration/shared-glossary.js +1 -1
  222. package/dist/slices/narration/steering.js +15 -4
  223. package/dist/slices/notifications/notifier.js +21 -3
  224. package/dist/slices/notifications/rules.js +22 -1
  225. package/dist/slices/onboarding/copy.js +143 -0
  226. package/dist/slices/onboarding/first-run.js +46 -0
  227. package/dist/slices/onboarding/install.js +119 -0
  228. package/dist/slices/onboarding/model.js +64 -0
  229. package/dist/slices/onboarding/packs.js +491 -0
  230. package/dist/slices/onboarding/quick-short.js +129 -0
  231. package/dist/slices/onboarding/sample.js +132 -0
  232. package/dist/slices/onboarding/state.js +68 -0
  233. package/dist/slices/onboarding/template.js +73 -0
  234. package/dist/slices/patch-notes/library.js +73 -0
  235. package/dist/slices/patch-notes/seen.js +77 -0
  236. package/dist/slices/play-drafts/convert.js +125 -6
  237. package/dist/slices/play-drafts/readiness.js +3 -0
  238. package/dist/slices/play-drafts/repo.js +2 -0
  239. package/dist/slices/play-drafts/review-inputs.js +20 -3
  240. package/dist/slices/play-drafts/schema.js +89 -0
  241. package/dist/slices/play-drafts/service.js +4 -1
  242. package/dist/slices/play-drafts/start.js +43 -18
  243. package/dist/slices/project-templates/from-project.js +50 -4
  244. package/dist/slices/project-templates/one-off.js +26 -0
  245. package/dist/slices/project-templates/repo.js +2 -1
  246. package/dist/slices/project-templates/service.js +15 -4
  247. package/dist/slices/project-templates/setup.js +18 -2
  248. package/dist/slices/rebuild/admission-repo.js +1 -1
  249. package/dist/slices/rebuild/model.js +6 -0
  250. package/dist/slices/rebuild/preview-details.js +46 -3
  251. package/dist/slices/rebuild/preview-plan.js +13 -6
  252. package/dist/slices/rebuild/recipe-audio-parts.js +40 -7
  253. package/dist/slices/rebuild/recipe-audio.js +66 -12
  254. package/dist/slices/rebuild/recipe-build.js +8 -4
  255. package/dist/slices/rebuild/recipe-cast.js +18 -0
  256. package/dist/slices/rebuild/recipe-checkpoints.js +11 -9
  257. package/dist/slices/rebuild/recipe-describe.js +93 -0
  258. package/dist/slices/rebuild/recipe-edit.js +13 -1
  259. package/dist/slices/rebuild/recipe-exports.js +62 -8
  260. package/dist/slices/rebuild/recipe-input-schema.js +35 -0
  261. package/dist/slices/rebuild/recipe-legacy.js +9 -0
  262. package/dist/slices/rebuild/recipe-loudness.js +45 -0
  263. package/dist/slices/rebuild/recipe-model.js +25 -6
  264. package/dist/slices/rebuild/recipe-pauses.js +7 -0
  265. package/dist/slices/rebuild/recipe-preparation.js +3 -3
  266. package/dist/slices/rebuild/recipe-provider-choice.js +11 -1
  267. package/dist/slices/rebuild/recipe-reference.js +15 -1
  268. package/dist/slices/rebuild/recipe-reviews.js +117 -0
  269. package/dist/slices/rebuild/recipe-save.js +4 -1
  270. package/dist/slices/rebuild/recipe-shorts.js +23 -3
  271. package/dist/slices/rebuild/recipe-text.js +114 -19
  272. package/dist/slices/rebuild/recipe-validation.js +14 -2
  273. package/dist/slices/rebuild/recipe-visual.js +96 -33
  274. package/dist/slices/rebuild/recipe-voices.js +286 -0
  275. package/dist/slices/rebuild/recipe-work.js +77 -6
  276. package/dist/slices/rebuild/recipe-youtube.js +2 -0
  277. package/dist/slices/rebuild/recovery-conflict.js +7 -3
  278. package/dist/slices/rebuild/recovery-model.js +3 -0
  279. package/dist/slices/rebuild/recovery-selection.js +28 -2
  280. package/dist/slices/rebuild/recovery.js +35 -17
  281. package/dist/slices/rebuild/repo.js +3 -3
  282. package/dist/slices/rebuild/retry.js +18 -0
  283. package/dist/slices/rebuild/review-redo.js +110 -0
  284. package/dist/slices/rebuild/runtime-actions.js +5 -0
  285. package/dist/slices/rebuild/runtime-admission.js +4 -1
  286. package/dist/slices/rebuild/runtime-animate.js +4 -4
  287. package/dist/slices/rebuild/runtime-article.js +5 -5
  288. package/dist/slices/rebuild/runtime-document.js +4 -4
  289. package/dist/slices/rebuild/runtime-export-bed.js +43 -0
  290. package/dist/slices/rebuild/runtime-export-edit.js +70 -11
  291. package/dist/slices/rebuild/runtime-export-inputs.js +52 -10
  292. package/dist/slices/rebuild/runtime-export-short.js +85 -0
  293. package/dist/slices/rebuild/runtime-export.js +84 -19
  294. package/dist/slices/rebuild/runtime-figure-card.js +87 -0
  295. package/dist/slices/rebuild/runtime-image.js +21 -2
  296. package/dist/slices/rebuild/runtime-local.js +148 -24
  297. package/dist/slices/rebuild/runtime-materialize.js +8 -0
  298. package/dist/slices/rebuild/runtime-narration-reuse.js +2 -1
  299. package/dist/slices/rebuild/runtime-narration-text.js +7 -7
  300. package/dist/slices/rebuild/runtime-pictures.js +28 -0
  301. package/dist/slices/rebuild/runtime-piece-label.js +27 -0
  302. package/dist/slices/rebuild/runtime-provider.js +126 -18
  303. package/dist/slices/rebuild/runtime-publication.js +21 -5
  304. package/dist/slices/rebuild/runtime-review.js +237 -0
  305. package/dist/slices/rebuild/runtime-run.js +17 -11
  306. package/dist/slices/rebuild/runtime-shorts.js +44 -17
  307. package/dist/slices/rebuild/runtime-store.js +39 -10
  308. package/dist/slices/rebuild/runtime-subtitles.js +72 -19
  309. package/dist/slices/rebuild/runtime-voices.js +103 -0
  310. package/dist/slices/rebuild/runtime-youtube.js +7 -6
  311. package/dist/slices/rebuild/soften.js +49 -0
  312. package/dist/slices/rebuild/transition-repo.js +9 -2
  313. package/dist/slices/research/run.js +4 -4
  314. package/dist/slices/research/synthesis.js +2 -2
  315. package/dist/slices/reviews/model.js +105 -0
  316. package/dist/slices/reviews/outcome.js +22 -0
  317. package/dist/slices/reviews/repo.js +106 -0
  318. package/dist/slices/reviews/rules.js +63 -0
  319. package/dist/slices/reviews/verdict.js +71 -0
  320. package/dist/slices/reviews/view.js +30 -0
  321. package/dist/slices/revisions/adopt-content.js +5 -2
  322. package/dist/slices/revisions/adopt.js +3 -1
  323. package/dist/slices/revisions/downloads.js +7 -1
  324. package/dist/slices/revisions/image-plan.js +183 -0
  325. package/dist/slices/revisions/mutation-assets.js +10 -0
  326. package/dist/slices/revisions/mutation-cues.js +71 -0
  327. package/dist/slices/revisions/mutation-request.js +2 -1
  328. package/dist/slices/revisions/mutations.js +97 -3
  329. package/dist/slices/revisions/schema.js +11 -1
  330. package/dist/slices/run-cost/limits.js +128 -0
  331. package/dist/slices/run-cost/meter.js +47 -0
  332. package/dist/slices/run-cost/panel.js +234 -0
  333. package/dist/slices/run-cost/pricing.js +92 -0
  334. package/dist/slices/run-cost/week.js +108 -0
  335. package/dist/slices/schedules/agenda.js +158 -0
  336. package/dist/slices/schedules/repo.js +58 -18
  337. package/dist/slices/schedules/scheduler.js +67 -16
  338. package/dist/slices/schedules/schema.js +138 -1
  339. package/dist/slices/schedules/service.js +43 -4
  340. package/dist/slices/schedules/similar.js +90 -0
  341. package/dist/slices/schedules/topic-list.js +163 -0
  342. package/dist/slices/schedules/topics.js +422 -0
  343. package/dist/slices/settings/first-run.js +102 -0
  344. package/dist/slices/settings/health.js +190 -0
  345. package/dist/slices/settings/key-guides.js +128 -0
  346. package/dist/slices/settings/key-test.js +85 -0
  347. package/dist/slices/settings/model.js +6 -1
  348. package/dist/slices/settings/playback.js +15 -0
  349. package/dist/slices/settings/repo.js +17 -3
  350. package/dist/slices/settings/voices.js +64 -4
  351. package/dist/slices/settings/whats-new.js +64 -0
  352. package/dist/slices/shorts/captions.js +9 -0
  353. package/dist/slices/shorts/pick.js +3 -3
  354. package/dist/slices/shorts/prompts.js +1 -1
  355. package/dist/slices/shorts/render.js +54 -5
  356. package/dist/slices/storage/asset-name.js +3 -0
  357. package/dist/slices/storage/backup-export.js +48 -9
  358. package/dist/slices/storage/backup-format.js +38 -1
  359. package/dist/slices/storage/backup-import.js +191 -19
  360. package/dist/slices/storage/delete-project.js +7 -4
  361. package/dist/slices/storage/documents.js +79 -0
  362. package/dist/slices/storage/files-location.js +531 -0
  363. package/dist/slices/storage/layout.js +14 -4
  364. package/dist/slices/storage/model.js +11 -0
  365. package/dist/slices/storage/portable.js +92 -12
  366. package/dist/slices/storage/reconcile.js +14 -1
  367. package/dist/slices/storage/repo.js +4 -1
  368. package/dist/slices/storage/schema.js +11 -0
  369. package/dist/slices/storage/trim.js +146 -0
  370. package/dist/slices/studio/disclosure.js +74 -0
  371. package/dist/slices/studio/model.js +28 -0
  372. package/dist/slices/studio/pack.js +209 -0
  373. package/dist/slices/studio/settings.js +133 -0
  374. package/dist/slices/style-preview/images.js +61 -0
  375. package/dist/slices/style-preview/render.js +161 -0
  376. package/dist/slices/style-preview/schema.js +45 -0
  377. package/dist/slices/style-preview/service.js +93 -0
  378. package/dist/slices/style-preview/settings.js +89 -0
  379. package/dist/slices/subtitles/captions.js +42 -9
  380. package/dist/slices/subtitles/model.js +22 -0
  381. package/dist/slices/subtitles/prepare.js +27 -16
  382. package/dist/slices/subtitles/transcript.js +1 -1
  383. package/dist/slices/thumbnail/run.js +7 -7
  384. package/dist/slices/trash/model.js +25 -0
  385. package/dist/slices/trash/service.js +273 -0
  386. package/dist/slices/uploads/repo.js +20 -0
  387. package/dist/slices/video/ambient-bed-schema.js +15 -0
  388. package/dist/slices/video/ambient-bed.js +95 -0
  389. package/dist/slices/video/ambient-mix.js +72 -0
  390. package/dist/slices/video/audio-inputs.js +2 -2
  391. package/dist/slices/video/cards.js +5 -2
  392. package/dist/slices/video/edit-list.js +23 -2
  393. package/dist/slices/video/edit-settings.js +13 -3
  394. package/dist/slices/video/ffmpeg.js +63 -9
  395. package/dist/slices/video/figure-card.js +492 -0
  396. package/dist/slices/video/figure-spans.js +51 -0
  397. package/dist/slices/video/plan.js +125 -13
  398. package/dist/slices/video/run.js +14 -12
  399. package/dist/slices/video/slideshow.js +39 -4
  400. package/dist/slices/voices/attribution.js +69 -0
  401. package/dist/slices/voices/audio-files.js +82 -0
  402. package/dist/slices/voices/audition.js +25 -0
  403. package/dist/slices/voices/cast.js +56 -0
  404. package/dist/slices/voices/grouping.js +49 -0
  405. package/dist/slices/voices/join.js +35 -0
  406. package/dist/slices/voices/languages.js +25 -0
  407. package/dist/slices/voices/model.js +179 -0
  408. package/dist/slices/voices/palette.js +26 -0
  409. package/dist/slices/voices/panel.js +91 -0
  410. package/dist/slices/voices/portraits.js +55 -0
  411. package/dist/slices/voices/script.js +136 -0
  412. package/dist/slices/voices/timing.js +58 -0
  413. package/dist/slices/youtube/answer.js +1 -1
  414. package/dist/slices/youtube/chapters.js +126 -0
  415. package/dist/slices/youtube/edits-repo.js +69 -0
  416. package/dist/slices/youtube/edits.js +89 -0
  417. package/dist/slices/youtube/placeholders.js +98 -0
  418. package/dist/slices/youtube/transcript.js +11 -4
  419. package/dist/updater/service.js +66 -21
  420. package/dist/web/assets/index-CUA2sUxU.css +1 -0
  421. package/dist/web/assets/index-CX590kE-.js +303 -0
  422. package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  423. package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  424. package/dist/web/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  425. package/dist/web/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  426. package/dist/web/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  427. package/dist/web/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  428. package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  429. package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  430. package/dist/web/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  431. package/dist/web/assets/{pdf-BLF2i8c2.js → pdf-BG8n_uc5.js} +1 -1
  432. package/dist/web/index.html +2 -2
  433. package/package.json +4 -4
  434. package/dist/edge/docker-launch.js +0 -81
  435. package/dist/edge/docker-projects/activation.js +0 -35
  436. package/dist/edge/docker-projects/claims.js +0 -85
  437. package/dist/edge/docker-projects/committed.js +0 -40
  438. package/dist/edge/docker-projects/engine.js +0 -461
  439. package/dist/edge/docker-projects/install.js +0 -254
  440. package/dist/edge/docker-projects/prune.js +0 -66
  441. package/dist/edge/docker-projects/recover.js +0 -100
  442. package/dist/edge/docker-projects/state.js +0 -225
  443. package/dist/web/assets/index-CktpxlJd.js +0 -171
  444. package/dist/web/assets/index-DB-vOnGU.css +0 -1
  445. package/scripts/docker-run.sh +0 -3
  446. /package/dist/edge/{docker-projects → docker-install}/tree.js +0 -0
@@ -1,21 +1,68 @@
1
+ import { aliasMatches } from "../../kernel/ports/narration-aliases.js";
1
2
  import { cardinal, numberForms } from "./numbers.js";
2
3
  // Original display words remain intact; only acoustic matching uses normalized English.
3
- export function speechWords(text, observed = "") {
4
+ // Narration aliases ("Dr." said as "Doctor") make the aliased form the default for the
5
+ // written words they cover, with the written form kept as an alternative for audio that
6
+ // was not aliased (an uploaded chunk, say).
7
+ // `formsOf` is the language's normaliser (`multilingual.ts` passes its own); the default is
8
+ // English, and `empty` is the sentence for a text with nothing to say.
9
+ export function speechWords(text, observed = "", aliases = [], formsOf = wordForms, empty = "Local subtitles currently require an English transcript with spoken words.") {
10
+ const matches = aliases.length === 0 ? [] : aliasMatches(text, aliases);
4
11
  const words = [];
5
- for (const textWord of text.trim().split(/\s+/)) {
6
- const forms = wordForms(textWord);
7
- const spoken = forms.find((form) => form !== "" && ` ${observed} `.includes(` ${form} `)) ?? forms[0] ?? "";
12
+ for (const token of text.matchAll(/\S+/g)) {
13
+ const textWord = token[0];
14
+ const start = token.index;
15
+ const end = start + textWord.length;
16
+ const plain = formsOf(textWord);
17
+ const touching = matches.filter((match) => match.start < end && start < match.end);
18
+ let forms = [...plain];
19
+ if (touching.length > 0) {
20
+ let said = "";
21
+ let at = start;
22
+ for (const match of touching) {
23
+ if (match.start >= at)
24
+ said += textWord.slice(at - start, match.start - start);
25
+ // The spoken form belongs to the word the written form starts in.
26
+ if (match.start >= start)
27
+ said += ` ${match.spoken} `;
28
+ at = Math.max(at, Math.min(match.end, end));
29
+ }
30
+ said += textWord.slice(at - start);
31
+ const aliased = said
32
+ .trim()
33
+ .split(/\s+/)
34
+ .map((piece) => (piece === "" ? "" : (formsOf(piece)[0] ?? "")))
35
+ .filter((form) => form !== "")
36
+ .join(" ");
37
+ forms = [aliased, ...plain.filter((form) => form !== aliased)];
38
+ }
39
+ const spoken = pick(forms, observed);
40
+ const previous = words.at(-1);
8
41
  if (spoken === "") {
9
- const previous = words.at(-1);
10
- if (previous !== undefined)
42
+ if (previous !== undefined) {
11
43
  previous.text += ` ${textWord}`;
44
+ // A word an alias swallowed ("al." of "et al.") still counts when the written
45
+ // form was read out.
46
+ const next = plain[0];
47
+ if (touching.length > 0 && next !== undefined)
48
+ previous.forms = previous.forms.map((form, index) => index === 0 && previous.aliased ? form : `${form} ${next}`);
49
+ }
12
50
  continue;
13
51
  }
14
- words.push({ text: textWord, spoken });
52
+ words.push({ text: textWord, spoken, forms, aliased: touching.length > 0 });
15
53
  }
16
54
  if (words.length === 0)
17
- throw new Error("Local subtitles currently require an English transcript with spoken words.");
18
- return words;
55
+ throw new Error(empty);
56
+ return words.map(({ text: shown, spoken, forms }) => forms.length > 1 ? { text: shown, spoken, forms } : { text: shown, spoken });
57
+ }
58
+ // The form of a word the recording seems to say, else its default.
59
+ export function respoken(word, observed, formsOf = wordForms) {
60
+ if (word.forms === undefined)
61
+ return speechWords(word.text, observed, [], formsOf)[0] ?? word;
62
+ return { ...word, spoken: pick(word.forms, observed) };
63
+ }
64
+ function pick(forms, observed) {
65
+ return (forms.find((form) => form !== "" && ` ${observed} `.includes(` ${form} `)) ?? forms[0] ?? "");
19
66
  }
20
67
  function wordForms(raw) {
21
68
  const plain = raw.normalize("NFKD").replace(/\p{M}/gu, "").replace(/[‘’]/g, "'");
@@ -1,11 +1,13 @@
1
- import { alignWindow, frameSeconds, mismatch } from "./ctc.js";
1
+ import { alignWindow, frameSeconds } from "./ctc.js";
2
2
  import { agreesWithSpeech } from "./quality.js";
3
- import { letters } from "./vocabulary.js";
3
+ import { englishSpec } from "./spec.js";
4
4
  // Recovery omits bounded transcript spans only after confirming surrounding speech.
5
- export function alignSpeechWindow(logits, frames, candidate, complete, cutoff, skipBudget) {
5
+ export function alignSpeechWindow(logits, frames, candidate, complete, cutoff, skipBudget, spec = englishSpec) {
6
+ const { gates, mismatch } = spec;
7
+ const agrees = (expected, observed, maximumError) => agreesWithSpeech(expected, observed, maximumError, spec.comparable);
6
8
  try {
7
9
  return {
8
- words: accepted(logits, frames, candidate, complete, cutoff),
10
+ words: accepted(logits, frames, candidate, complete, cutoff, spec),
9
11
  skipped: 0,
10
12
  omissionStart: 0,
11
13
  };
@@ -16,19 +18,20 @@ export function alignSpeechWindow(logits, frames, candidate, complete, cutoff, s
16
18
  }
17
19
  if (skipBudget <= 0)
18
20
  throw new Error(mismatch);
19
- const heard = greedy(logits, frames).split(/\s+/);
21
+ const heard = greedy(logits, frames, spec).split(/\s+/);
20
22
  for (let skipped = 1; skipped <= Math.min(40, skipBudget, candidate.length - 4); skipped += 1) {
21
23
  const remaining = candidate.slice(skipped);
22
24
  const anchor = remaining
23
25
  .slice(0, 4)
24
26
  .map((word) => word.spoken)
25
27
  .join(" ");
26
- if (anchor.replace(/[^A-Z]/g, "").length < 20 ||
27
- !agreesWithSpeech(anchor, heard.slice(0, anchor.split(/\s+/).length).join(" "), 0.2))
28
+ if (spec.lettersOnly(anchor).length < gates.anchorLetters ||
29
+ !agrees(anchor, heard.slice(0, anchor.split(/\s+/).length).join(" "), 0.2))
28
30
  continue;
29
31
  try {
30
- const words = accepted(logits, frames, remaining, complete, cutoff);
31
- if (words.length < 4 || words.slice(0, 4).some((word) => (word.confidence ?? 0) < 0.75))
32
+ const words = accepted(logits, frames, remaining, complete, cutoff, spec);
33
+ if (words.length < 4 ||
34
+ words.slice(0, 4).some((word) => (word.confidence ?? 0) < gates.anchorConfidence))
32
35
  continue;
33
36
  return { words, skipped, omissionStart: 0 };
34
37
  }
@@ -37,32 +40,32 @@ export function alignSpeechWindow(logits, frames, candidate, complete, cutoff, s
37
40
  throw error;
38
41
  }
39
42
  }
40
- const observed = greedy(logits, frames).replace(/[^A-Z']/g, "");
43
+ const observed = spec.comparable(greedy(logits, frames, spec));
41
44
  for (let start = candidate.length - 4; start >= 4; start -= 1) {
42
- const prefix = candidate
45
+ const prefix = spec.comparable(candidate
43
46
  .slice(0, start)
44
47
  .map((word) => word.spoken)
45
- .join("")
46
- .replace(/[^A-Z']/g, "");
48
+ .join(""));
47
49
  if (prefix.length > observed.length)
48
50
  continue;
49
- if (!agreesWithSpeech(prefix, observed.slice(0, prefix.length), 0.2))
51
+ if (!agrees(prefix, observed.slice(0, prefix.length), 0.2))
50
52
  continue;
51
53
  for (let skipped = 1; skipped <= Math.min(40, skipBudget, candidate.length - start - 4); skipped += 1) {
52
54
  const remaining = [...candidate.slice(0, start), ...candidate.slice(start + skipped)];
53
- const anchor = remaining
55
+ const anchor = spec.comparable(remaining
54
56
  .slice(start, start + 4)
55
57
  .map((word) => word.spoken)
56
- .join("")
57
- .replace(/[^A-Z']/g, "");
58
- if (anchor.length < 20 ||
59
- !agreesWithSpeech(anchor, observed.slice(prefix.length, prefix.length + anchor.length), 0.2) ||
60
- !agreesWithSpeech(prefix + anchor, observed.slice(0, prefix.length + anchor.length), 0.15))
58
+ .join(""));
59
+ if (anchor.length < gates.anchorLetters ||
60
+ !agrees(anchor, observed.slice(prefix.length, prefix.length + anchor.length), 0.2) ||
61
+ !agrees(prefix + anchor, observed.slice(0, prefix.length + anchor.length), 0.15))
61
62
  continue;
62
63
  try {
63
- const words = accepted(logits, frames, remaining, complete, cutoff, 0.2);
64
+ const words = accepted(logits, frames, remaining, complete, cutoff, spec, 0.2);
64
65
  if (words.length < start + 4 ||
65
- words.slice(start - 4, start + 4).some((word) => (word.confidence ?? 0) < 0.75))
66
+ words
67
+ .slice(start - 4, start + 4)
68
+ .some((word) => (word.confidence ?? 0) < gates.anchorConfidence))
66
69
  continue;
67
70
  return { words, skipped, omissionStart: start };
68
71
  }
@@ -74,27 +77,28 @@ export function alignSpeechWindow(logits, frames, candidate, complete, cutoff, s
74
77
  }
75
78
  throw new Error(mismatch);
76
79
  }
77
- function accepted(logits, frames, candidate, complete, cutoff, maximumError = 0.42) {
78
- const words = alignWindow(logits, frames, candidate, complete).words.filter((word) => word.end <= cutoff);
80
+ function accepted(logits, frames, candidate, complete, cutoff, spec, maximumError = spec.gates.maximumError) {
81
+ const words = alignWindow(logits, frames, candidate, complete, spec).words.filter((word) => word.end <= cutoff);
79
82
  const last = words.at(-1);
80
83
  if (last === undefined ||
81
84
  !agreesWithSpeech(candidate
82
85
  .slice(0, words.length)
83
86
  .map((word) => word.spoken)
84
- .join(" "), greedy(logits, Math.min(frames, Math.ceil(last.end / frameSeconds))), maximumError))
85
- throw new Error(mismatch);
87
+ .join(" "), greedy(logits, Math.min(frames, Math.ceil(last.end / frameSeconds)), spec), maximumError, spec.comparable))
88
+ throw new Error(spec.mismatch);
86
89
  return words;
87
90
  }
88
- export function greedy(logits, frames) {
91
+ export function greedy(logits, frames, spec = englishSpec) {
92
+ const labels = spec.labels;
89
93
  let previous = -1;
90
94
  let text = "";
91
95
  for (let frame = 0; frame < frames; frame += 1) {
92
96
  let best = 0;
93
- for (let label = 1; label < 32; label += 1)
94
- if ((logits[frame * 32 + label] ?? -Infinity) > (logits[frame * 32 + best] ?? -Infinity))
97
+ for (let label = 1; label < labels; label += 1)
98
+ if ((logits[frame * labels + label] ?? -Infinity) > (logits[frame * labels + best] ?? -Infinity))
95
99
  best = label;
96
100
  if (best !== previous && best !== 0)
97
- text += letters[best] ?? "";
101
+ text += spec.letters[best] ?? "";
98
102
  previous = best;
99
103
  }
100
104
  return text.trim().replace(/\s+/g, " ");
@@ -1,8 +1,7 @@
1
1
  import { open, readFile, stat } from "node:fs/promises";
2
2
  import { SubtitleMismatch } from "../../kernel/ports/subtitles.js";
3
- import { mismatch } from "./ctc.js";
3
+ import { alignmentSpecFor } from "./multilingual.js";
4
4
  import { workerInput } from "./protocol.js";
5
- import { speechWords } from "./text.js";
6
5
  import { subtitleThreads } from "./threads.js";
7
6
  import { alignSpeechWindow, greedy } from "./window.js";
8
7
  const sampleRate = 16000;
@@ -26,13 +25,15 @@ function send(message) {
26
25
  process.send?.(message);
27
26
  }
28
27
  async function run(input) {
28
+ const spec = alignmentSpecFor(input.language);
29
+ const { mismatch } = spec;
29
30
  const { ort, session } = await openSession(input.modelPath);
30
31
  const file = await open(input.pcmPath, "r");
31
32
  try {
32
33
  const totalSamples = (await stat(input.pcmPath)).size / 4;
33
34
  if (!Number.isInteger(totalSamples) || totalSamples < sampleRate / 10)
34
35
  throw new Error("The narration is too short to align subtitles.");
35
- const source = speechWords(input.text);
36
+ const source = spec.words(input.text, "", input.aliases ?? []);
36
37
  const output = [];
37
38
  let cursor = 0;
38
39
  let omitted = 0;
@@ -51,28 +52,29 @@ async function run(input) {
51
52
  const result = await session.run({
52
53
  input_values: new ort.Tensor("float32", normalized, [1, count]),
53
54
  });
54
- const logits = result.logits;
55
- if (logits === undefined ||
56
- !(logits.data instanceof Float32Array) ||
57
- logits.dims.length !== 3 ||
58
- logits.dims[2] !== 32)
55
+ const raw = result.logits;
56
+ if (raw === undefined ||
57
+ !(raw.data instanceof Float32Array) ||
58
+ raw.dims.length !== 3 ||
59
+ raw.dims[2] !== spec.modelLabels)
59
60
  throw new Error("The local subtitle model returned an unsupported result.");
60
- const frames = logits.dims[1] ?? 0;
61
- const observed = greedy(logits.data, frames);
62
- if (observed.replace(/[^A-Z]/g, "").length === 0) {
61
+ const frames = raw.dims[1] ?? 0;
62
+ const logits = { data: spec.compact?.(raw.data, frames) ?? raw.data };
63
+ const observed = greedy(logits.data, frames, spec);
64
+ if (spec.lettersOnly(observed).length === 0) {
63
65
  sampleAt += count;
64
66
  for (const tensor of Object.values(result))
65
67
  tensor.dispose();
66
68
  continue;
67
69
  }
68
- const candidate = candidates(source, cursor, observed);
70
+ const candidate = candidates(source, cursor, observed, spec);
69
71
  const finalWindow = sampleAt + count >= totalSamples;
70
72
  const complete = finalWindow && cursor + candidate.length === source.length;
71
73
  const cutoff = finalWindow ? count / sampleRate : count / sampleRate - overlapSeconds;
72
74
  const stuck = () => new SubtitleMismatch(mismatch, sampleAt / sampleRate, snippet(candidate.map((word) => word.text)), snippet(observed.split(/\s+/)));
73
75
  let recovered;
74
76
  try {
75
- recovered = alignSpeechWindow(logits.data, frames, candidate, complete, cutoff, cursor === 0 ? 0 : omissionBudget - omitted);
77
+ recovered = alignSpeechWindow(logits.data, frames, candidate, complete, cutoff, cursor === 0 ? 0 : omissionBudget - omitted, spec);
76
78
  }
77
79
  catch (error) {
78
80
  if (error instanceof Error && error.message === mismatch)
@@ -121,10 +123,12 @@ async function run(input) {
121
123
  input_values: new ort.Tensor("float32", audio, [1, count]),
122
124
  });
123
125
  const logits = result.logits;
124
- if (logits !== undefined &&
125
- logits.data instanceof Float32Array &&
126
- greedy(logits.data, logits.dims[1] ?? 0).replace(/[^A-Z]/g, "").length > 8)
127
- throw new SubtitleMismatch(mismatch, sampleAt / sampleRate, "", snippet(greedy(logits.data, logits.dims[1] ?? 0).split(/\s+/)));
126
+ if (logits !== undefined && logits.data instanceof Float32Array) {
127
+ const frames = logits.dims[1] ?? 0;
128
+ const heard = greedy(spec.compact?.(logits.data, frames) ?? logits.data, frames, spec);
129
+ if (spec.lettersOnly(heard).length > 8)
130
+ throw new SubtitleMismatch(mismatch, sampleAt / sampleRate, "", snippet(heard.split(/\s+/)));
131
+ }
128
132
  for (const tensor of Object.values(result))
129
133
  tensor.dispose();
130
134
  }
@@ -173,16 +177,14 @@ async function openSession(modelPath) {
173
177
  });
174
178
  return { ort: native, session };
175
179
  }
176
- function candidates(source, cursor, observed) {
180
+ function candidates(source, cursor, observed, spec) {
177
181
  const selected = [];
178
182
  let length = 0;
179
183
  for (let index = cursor; index < source.length; index += 1) {
180
184
  const word = source[index];
181
185
  if (word === undefined)
182
186
  break;
183
- const normalized = speechWords(word.text, observed)[0];
184
- if (normalized === undefined)
185
- continue;
187
+ const normalized = spec.respoken(word, observed);
186
188
  if (length + normalized.spoken.length + 1 > 900)
187
189
  break;
188
190
  selected.push(normalized);
@@ -6,14 +6,14 @@
6
6
  // A provider's error body can be a whole HTML page; the first few hundred characters say why.
7
7
  const detailMax = 300;
8
8
  export function missingKey(provider) {
9
- return `No ${provider} API key is saved. Add one in Settings → Providers, then use Retry stage.`;
9
+ return `No ${provider} API key is saved. Add one in Settings → Providers, then use Try again.`;
10
10
  }
11
11
  export const refreshList = "refresh the list";
12
- export const checkChoice = "Check the provider and model chosen in the Providers section of Edit project, then use Retry stage.";
12
+ export const checkChoice = "Check the provider and model chosen in the Providers section of Edit project, then use Try again.";
13
13
  export function httpFailure(failure) {
14
14
  const { provider, status } = failure;
15
15
  const said = quoted(failure.label ?? `error ${String(status)}`, failure.detail);
16
- const next = failure.next ?? "use Retry stage";
16
+ const next = failure.next ?? "use Try again";
17
17
  if (status === 401) {
18
18
  return `${provider} did not accept the API key${said}. The key may be wrong, expired or revoked: paste a current key in Settings → Providers, then ${next}.`;
19
19
  }
@@ -32,23 +32,23 @@ export function httpFailure(failure) {
32
32
  if (status >= 500) {
33
33
  return `${provider} had a problem on its side${said}. This is usually temporary: wait a few minutes, then ${next}.`;
34
34
  }
35
- return `${provider} answered with an unexpected error${said}. ${failure.next === undefined ? "Use Retry stage" : "Try again"}; if it keeps failing, use Download diagnostics in Settings and report it.`;
35
+ return `${provider} answered with an unexpected error${said}. ${failure.next === undefined ? "Use Try again" : "Try again"}; if it keeps failing, use Download diagnostics in Settings and report it.`;
36
36
  }
37
37
  // A failure the provider reported inside an otherwise successful answer, with no status.
38
38
  export function providerSaid(provider, what, detail, next) {
39
39
  return `${provider} ${what}${quoted("", detail)}. ${next}`;
40
40
  }
41
41
  export function unreadable(provider) {
42
- return `${provider} sent back an answer Slopify could not read. This is usually temporary: use Retry stage; if it keeps happening, use Download diagnostics in Settings and report it.`;
42
+ return `${provider} sent back an answer Slopify could not read. This is usually temporary: use Try again; if it keeps happening, use Download diagnostics in Settings and report it.`;
43
43
  }
44
44
  export function droppedStream(provider) {
45
- return `The connection to ${provider} dropped before the answer was complete. Check your internet connection, then use Retry stage.`;
45
+ return `The connection to ${provider} dropped before the answer was complete. Check your internet connection, then use Try again.`;
46
46
  }
47
47
  export function noAudio(provider) {
48
- return `${provider} finished without sending any audio. Use Retry stage; if it keeps happening, check the voice in Settings → Voices.`;
48
+ return `${provider} finished without sending any audio. Use Try again; if it keeps happening, check the voice in Settings → Voices.`;
49
49
  }
50
50
  export function noImage(provider) {
51
- return `${provider} finished without sending an image. Use Retry stage; if it keeps happening, reword the image prompt in the Images section of Edit project or choose another image model in its Providers section.`;
51
+ return `${provider} finished without sending an image. Use Try again; if it keeps happening, reword the image prompt in the Images section of Edit project or choose another image model in its Providers section.`;
52
52
  }
53
53
  export function refusedImage(provider, detail) {
54
54
  return `${provider} refused to make this image under its content rules${quoted("", detail)}. Reword the image prompt in the Images section of Edit project, or choose another image provider in its Providers section.`;
@@ -56,10 +56,10 @@ export function refusedImage(provider, detail) {
56
56
  // A narration request the provider turned down is most often a voice that no longer exists
57
57
  // or a part longer than the model takes.
58
58
  export function voiceFix(voiceId) {
59
- return `Check that voice "${voiceId}" still exists in Settings → Voices and that each narration part fits the model's length limit (Chunking, in the Providers section of Edit project), then use Retry stage.`;
59
+ return `Check that voice "${voiceId}" still exists in Settings → Voices and that each narration part fits the model's length limit (Chunking, in the Providers section of Edit project), then use Try again.`;
60
60
  }
61
61
  export function internalError(what) {
62
- return `Slopify hit an internal error (${what}). Use Retry stage; if it happens again, use Download diagnostics in Settings and report it.`;
62
+ return `Slopify hit an internal error (${what}). Use Try again; if it happens again, use Download diagnostics in Settings and report it.`;
63
63
  }
64
64
  const cliNames = {
65
65
  claude: "Claude Code CLI",
@@ -81,7 +81,7 @@ export function cliReported(binary, detail, next) {
81
81
  return `The ${cliName(binary)} reported an error${quoted("", detail)}. ${next}`;
82
82
  }
83
83
  export function cliCheck(binary) {
84
- return `Use Retry stage; if it keeps failing, run ${commandOf(binary)} in a terminal to check it works and is signed in, or choose another model in the Providers section of Edit project.`;
84
+ return `Use Try again; if it keeps failing, run ${commandOf(binary)} in a terminal to check it works and is signed in, or choose another model in the Providers section of Edit project.`;
85
85
  }
86
86
  // ` (error 401: "Unauthorized")`, ` (error 401)`, ` ("Unauthorized")`, or nothing.
87
87
  export function quoted(label, detail) {
@@ -1,10 +1,12 @@
1
1
  import { redact } from "../../kernel/log.js";
2
- import { bridgeLimits, hostFaultSchema, hostFrameSchema, hostHealthSchema, hostImageSchema, hostLlmSchema, hostModelsSchema, hostOpenedSchema, hostOpenFolderSchema, hostStatusSchema, } from "../../kernel/ports/host-cli.js";
2
+ import { bridgeLimits, decodeHostImageReport, hostFaultSchema, hostFrameSchema, hostHealthSchema, hostImageReportHeader, hostImageSchema, hostLlmSchema, hostModelsSchema, hostOpenedSchema, hostOpenFolderSchema, hostStatusSchema, } from "../../kernel/ports/host-cli.js";
3
3
  import { agentImageTimeoutMs } from "../../kernel/ports/image.js";
4
4
  import { isProviderError, providerError } from "../../kernel/ports/model.js";
5
5
  import { sniffImage } from "../image/bytes.js";
6
+ import { claudePlanLimit } from "../llm/claude-code.js";
7
+ import { codexPlanLimit } from "../llm/codex-limits.js";
6
8
  import { hostRequest, hostUnavailable, readHostBytes, } from "./transport.js";
7
- const tooBigForHost = "Slopify could not send this request to the host helper because it is larger than, or shaped differently from, what the helper accepts. Make the inputs shorter in Edit project, then use Retry stage; if it happens again, use Download diagnostics in Settings and report it.";
9
+ const tooBigForHost = "Slopify could not send this request to the host helper because it is larger than, or shaped differently from, what the helper accepts. Make the inputs shorter in Edit project, then use Try again; if it happens again, use Download diagnostics in Settings and report it.";
8
10
  export function createHostCliClient(options) {
9
11
  async function request(input) {
10
12
  if (!options.directory)
@@ -125,6 +127,8 @@ export function createHostCliClient(options) {
125
127
  kind: "llm",
126
128
  body: Buffer.from(JSON.stringify(parsed.data)),
127
129
  signal: req.signal,
130
+ }).catch((error) => {
131
+ throw isProviderError(error) ? withPlanLimit(error, id) : error;
128
132
  });
129
133
  try {
130
134
  if (response.headers["content-type"]?.split(";")[0] !== "application/x-ndjson")
@@ -133,7 +137,7 @@ export function createHostCliClient(options) {
133
137
  }
134
138
  catch (error) {
135
139
  if (isProviderError(error))
136
- throw error;
140
+ throw withPlanLimit(error, id);
137
141
  throw hostUnavailable(true);
138
142
  }
139
143
  finally {
@@ -159,6 +163,18 @@ export function createHostCliClient(options) {
159
163
  base64: Buffer.from(req.reference.bytes).toString("base64"),
160
164
  },
161
165
  }),
166
+ ...(req.cast === undefined
167
+ ? {}
168
+ : {
169
+ cast: req.cast.map((member) => ({
170
+ name: member.name,
171
+ description: member.description,
172
+ images: member.images.map((image) => ({
173
+ mime: image.mime,
174
+ base64: Buffer.from(image.bytes).toString("base64"),
175
+ })),
176
+ })),
177
+ }),
162
178
  });
163
179
  if (!parsed.success)
164
180
  throw providerError({
@@ -172,6 +188,8 @@ export function createHostCliClient(options) {
172
188
  kind: "image",
173
189
  body: Buffer.from(JSON.stringify(parsed.data)),
174
190
  signal: req.signal,
191
+ }).catch((error) => {
192
+ throw isProviderError(error) ? withPlanLimit(error, "codex-image") : error;
175
193
  });
176
194
  try {
177
195
  const mime = response.headers["content-type"];
@@ -180,11 +198,15 @@ export function createHostCliClient(options) {
180
198
  const bytes = await readHostBytes(response, bridgeLimits.image);
181
199
  if (sniffImage(bytes) !== mime)
182
200
  throw hostUnavailable(true);
183
- return { bytes, mime };
201
+ return {
202
+ bytes,
203
+ mime,
204
+ ...decodeHostImageReport(response.headers[hostImageReportHeader]),
205
+ };
184
206
  }
185
207
  catch (error) {
186
208
  if (isProviderError(error))
187
- throw error;
209
+ throw withPlanLimit(error, "codex-image");
188
210
  throw hostUnavailable(true);
189
211
  }
190
212
  finally {
@@ -194,6 +216,23 @@ export function createHostCliClient(options) {
194
216
  },
195
217
  };
196
218
  }
219
+ // A used-up plan crosses the bridge as a rate-limit fault carrying the CLI's own words, which
220
+ // the host-side adapter already quoted; they are read again here so a Docker run waits for the
221
+ // reset like a local one instead of failing.
222
+ export function withPlanLimit(error, id) {
223
+ if (error.fault.kind !== "rate_limit" || error.fault.planLimit !== undefined)
224
+ return error;
225
+ const planLimit = id === "claude-code"
226
+ ? claudePlanLimit(error.message, 429, undefined)
227
+ : id === "gemini"
228
+ ? /daily quota/i.test(error.message)
229
+ ? { account: "gemini", resetsAt: null }
230
+ : undefined
231
+ : codexPlanLimit(error.message, new Date());
232
+ return planLimit === undefined
233
+ ? error
234
+ : providerError({ kind: "rate_limit", message: error.message, planLimit });
235
+ }
197
236
  async function* readEvents(response) {
198
237
  const decoder = new TextDecoder("utf-8", { fatal: true });
199
238
  let pending = "";
@@ -2,15 +2,15 @@ import { constants } from "node:fs";
2
2
  import { open } from "node:fs/promises";
3
3
  import { request } from "node:http";
4
4
  import { join } from "node:path";
5
- import { bridgeLimits } from "../../kernel/ports/host-cli.js";
5
+ import { bridgeLimits, hostFramesHeader, hostFramesVersion } from "../../kernel/ports/host-cli.js";
6
6
  import { agentImageTimeoutMs } from "../../kernel/ports/image.js";
7
7
  import { providerError } from "../../kernel/ports/model.js";
8
8
  export function hostUnavailable(submitted = false) {
9
9
  return providerError({
10
10
  kind: "unavailable",
11
11
  message: submitted
12
- ? "The connection to the host helper dropped before the CLI's answer arrived, so Slopify cannot tell whether it finished (it may already have used your quota). Check the host helper is still running, then use Retry stage."
13
- : "Slopify cannot reach its host helper, which runs the Claude Code, Codex and Gemini CLIs outside Docker. On the host computer, run npx @gentbajko/slopify@latest --docker to set it up or restart it, then use Retry stage.",
12
+ ? "The connection to the host helper dropped before the CLI's answer arrived, so Slopify cannot tell whether it finished (it may already have used your quota). Check the host helper is still running, then use Try again."
13
+ : "Slopify cannot reach its host helper, which runs the Claude Code, Codex and Gemini CLIs outside Docker. On the host computer, run npx @gentbajko/slopify@latest --docker to set it up or restart it, then use Try again.",
14
14
  });
15
15
  }
16
16
  async function tokenAt(directory) {
@@ -34,7 +34,7 @@ export async function hostRequest(options) {
34
34
  if (options.body && options.body.byteLength > bridgeLimits.request)
35
35
  throw providerError({
36
36
  kind: "unsupported",
37
- message: "This request is larger than the host helper accepts (16 MB), so nothing was sent. Make the inputs shorter in Edit project, or choose an API provider such as OpenRouter in its Providers section, then use Retry stage.",
37
+ message: "This request is larger than the host helper accepts (16 MB), so nothing was sent. Make the inputs shorter in Edit project, or choose an API provider such as OpenRouter in its Providers section, then use Try again.",
38
38
  });
39
39
  let token;
40
40
  try {
@@ -55,6 +55,8 @@ export async function hostRequest(options) {
55
55
  headers: {
56
56
  authorization: `Bearer ${token}`,
57
57
  "content-type": "application/json",
58
+ // The stream frames this app reads; an older helper ignores the header.
59
+ ...(options.kind === "llm" ? { [hostFramesHeader]: String(hostFramesVersion) } : {}),
58
60
  ...(options.body ? { "content-length": options.body.byteLength } : {}),
59
61
  },
60
62
  }, (incoming) => {
@@ -126,7 +128,7 @@ export async function readHostBytes(response, maximum) {
126
128
  if (length > maximum)
127
129
  throw providerError({
128
130
  kind: "unavailable",
129
- message: "The host helper sent back more data than Slopify accepts, so the answer was dropped. Use Retry stage; if it happens again, use Download diagnostics in Settings and report it.",
131
+ message: "The host helper sent back more data than Slopify accepts, so the answer was dropped. Use Try again; if it happens again, use Download diagnostics in Settings and report it.",
130
132
  });
131
133
  chunks.push(value);
132
134
  }
@@ -40,7 +40,7 @@ export async function downloadImage(download) {
40
40
  // quoted back - a fal or Replicate delivery link carries its own signature.
41
41
  throw providerError({
42
42
  kind: "other",
43
- message: `${download.provider} made the image, but Slopify could not download it (error ${String(response.status)}). Check your internet connection, then use Retry stage.`,
43
+ message: `${download.provider} made the image, but Slopify could not download it (error ${String(response.status)}). Check your internet connection, then use Try again.`,
44
44
  });
45
45
  }
46
46
  const bytes = new Uint8Array(await response.arrayBuffer());
@@ -48,7 +48,7 @@ export async function downloadImage(download) {
48
48
  if (mime === undefined) {
49
49
  throw providerError({
50
50
  kind: "other",
51
- message: `${download.provider} sent back something that is not a PNG or JPEG image (${describeBytes(bytes)}). Use Retry stage; if it keeps happening, choose another image model in the Providers section of Edit project.`,
51
+ message: `${download.provider} sent back something that is not a PNG or JPEG image (${describeBytes(bytes)}). Use Try again; if it keeps happening, choose another image model in the Providers section of Edit project.`,
52
52
  });
53
53
  }
54
54
  return { bytes, mime };
@@ -8,7 +8,7 @@ const maxOutputBytes = 32 * 1024 * 1024;
8
8
  function unavailable(detail) {
9
9
  return providerError({
10
10
  kind: "unavailable",
11
- message: `The Codex CLI finished, but Slopify could not collect the image: ${detail}. Use Retry stage to make it again (this uses your Codex quota again).`,
11
+ message: `The Codex CLI finished, but Slopify could not collect the image: ${detail}. Use Try again to make it again (this uses your Codex quota again).`,
12
12
  });
13
13
  }
14
14
  // A thread that drew more than this is not an image job any more.