@gentbajko/slopify 2.5.0 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (542) hide show
  1. package/LICENSE +201 -21
  2. package/NOTICE +12 -0
  3. package/README.md +24 -41
  4. package/SUBTITLES.md +1 -1
  5. package/dist/adapter-registry.js +31 -0
  6. package/dist/adapters/alignment/cache.js +4 -1
  7. package/dist/adapters/alignment/ctc.js +25 -23
  8. package/dist/adapters/alignment/index.js +37 -6
  9. package/dist/adapters/alignment/multilingual.js +267 -0
  10. package/dist/adapters/alignment/protocol.js +11 -0
  11. package/dist/adapters/alignment/quality.js +4 -3
  12. package/dist/adapters/alignment/sentences.js +95 -0
  13. package/dist/adapters/alignment/spec.js +24 -0
  14. package/dist/adapters/alignment/spell.js +769 -0
  15. package/dist/adapters/alignment/text.js +56 -9
  16. package/dist/adapters/alignment/window.js +34 -30
  17. package/dist/adapters/alignment/worker.js +23 -21
  18. package/dist/adapters/explain.js +11 -11
  19. package/dist/adapters/host-cli/index.js +44 -5
  20. package/dist/adapters/host-cli/transport.js +7 -5
  21. package/dist/adapters/image/bytes.js +2 -2
  22. package/dist/adapters/image/codex-output.js +1 -1
  23. package/dist/adapters/image/codex.js +87 -22
  24. package/dist/adapters/image/fal.js +13 -5
  25. package/dist/adapters/image/google.js +11 -8
  26. package/dist/adapters/image/openai.js +14 -7
  27. package/dist/adapters/image/reference.js +34 -0
  28. package/dist/adapters/image/replicate.js +7 -2
  29. package/dist/adapters/image/video.js +2 -2
  30. package/dist/adapters/key-probes.js +85 -0
  31. package/dist/adapters/llm/claude-code.js +168 -32
  32. package/dist/adapters/llm/cli-login-error.js +1 -1
  33. package/dist/adapters/llm/codex-limits.js +157 -0
  34. package/dist/adapters/llm/codex.js +99 -12
  35. package/dist/adapters/llm/document-workspace.js +2 -2
  36. package/dist/adapters/llm/gemini-workspace.js +1 -1
  37. package/dist/adapters/llm/gemini.js +16 -3
  38. package/dist/adapters/llm/image-workspace.js +58 -0
  39. package/dist/adapters/llm/openrouter.js +11 -3
  40. package/dist/adapters/llm/run-cli.js +12 -7
  41. package/dist/adapters/tts/cartesia.js +10 -0
  42. package/dist/adapters/tts/elevenlabs.js +31 -9
  43. package/dist/adapters/tts/gemini.js +206 -0
  44. package/dist/adapters/tts/inworld-async.js +9 -5
  45. package/dist/adapters/tts/inworld.js +17 -3
  46. package/dist/adapters/tts/openai.js +3 -0
  47. package/dist/adapters/tts/pcm-mp3.js +62 -0
  48. package/dist/adapters/tts/system.js +147 -0
  49. package/dist/adapters/tts/voice-languages.js +80 -0
  50. package/dist/assets/fonts/noto/NotoSans-Regular.ttf +0 -0
  51. package/dist/assets/fonts/noto/NotoSansArabic-Regular.ttf +0 -0
  52. package/dist/assets/fonts/noto/NotoSansDevanagari-Regular.ttf +0 -0
  53. package/dist/assets/fonts/noto/NotoSansHebrew-Regular.ttf +0 -0
  54. package/dist/assets/fonts/noto/NotoSansThai-Regular.ttf +0 -0
  55. package/dist/assets/fonts/noto/OFL.txt +93 -0
  56. package/dist/assets/fonts/noto/SOURCE.txt +18 -0
  57. package/dist/assets/models.yaml +38 -0
  58. package/dist/assets/sample/sample-audiobook.tar +0 -0
  59. package/dist/assets/sample/sample-podcast.tar +0 -0
  60. package/dist/assets/sample/sample-project.tar +0 -0
  61. package/dist/assets/style-preview/narration.mp3 +0 -0
  62. package/dist/assets/style-preview/tall-1.jpg +0 -0
  63. package/dist/assets/style-preview/tall-2.jpg +0 -0
  64. package/dist/assets/style-preview/tall-3.jpg +0 -0
  65. package/dist/assets/style-preview/wide-1.jpg +0 -0
  66. package/dist/assets/style-preview/wide-2.jpg +0 -0
  67. package/dist/assets/style-preview/wide-3.jpg +0 -0
  68. package/dist/catalog/merge.js +132 -0
  69. package/dist/catalog/registry.js +17 -14
  70. package/dist/catalog/schema.js +2 -0
  71. package/dist/catalog/store.js +147 -8
  72. package/dist/catalog/validate.js +2 -2
  73. package/dist/compose.yaml +71 -0
  74. package/dist/edge/autostart/docker-record.js +32 -0
  75. package/dist/edge/autostart/docker.js +118 -0
  76. package/dist/edge/autostart/index.js +78 -0
  77. package/dist/edge/autostart/launcher.js +176 -0
  78. package/dist/edge/autostart/model.js +3 -0
  79. package/dist/edge/autostart/native.js +210 -0
  80. package/dist/edge/autostart/prompt.js +72 -0
  81. package/dist/edge/autostart/service.js +90 -0
  82. package/dist/edge/cli-args.js +71 -0
  83. package/dist/edge/cli.js +64 -63
  84. package/dist/edge/docker-install/activation.js +56 -0
  85. package/dist/edge/docker-install/apply.js +458 -0
  86. package/dist/edge/docker-install/engine.js +362 -0
  87. package/dist/edge/docker-install/run.js +160 -0
  88. package/dist/edge/docker-install/state.js +146 -0
  89. package/dist/edge/{docker-projects → docker-install}/volume.js +1 -5
  90. package/dist/edge/docker.js +14 -22
  91. package/dist/edge/events/hub.js +3 -1
  92. package/dist/edge/host-cli.js +1 -1
  93. package/dist/edge/http/actions.js +38 -11
  94. package/dist/edge/http/app.js +60 -2
  95. package/dist/edge/http/auditions.js +70 -0
  96. package/dist/edge/http/autostart.js +37 -0
  97. package/dist/edge/http/byte-range.js +42 -0
  98. package/dist/edge/http/channel-memory.js +92 -0
  99. package/dist/edge/http/channels.js +128 -0
  100. package/dist/edge/http/diagnostics.js +1 -0
  101. package/dist/edge/http/draft-files.js +2 -4
  102. package/dist/edge/http/entries.js +22 -3
  103. package/dist/edge/http/files.js +11 -7
  104. package/dist/edge/http/folder-location.js +1 -1
  105. package/dist/edge/http/home.js +39 -0
  106. package/dist/edge/http/host-cli.js +29 -5
  107. package/dist/edge/http/narration-peaks.js +76 -0
  108. package/dist/edge/http/onboarding.js +212 -0
  109. package/dist/edge/http/open-folder.js +1 -1
  110. package/dist/edge/http/patch-notes.js +45 -0
  111. package/dist/edge/http/planning.js +4 -2
  112. package/dist/edge/http/project-create.js +87 -0
  113. package/dist/edge/http/project-templates.js +19 -2
  114. package/dist/edge/http/projects.js +37 -100
  115. package/dist/edge/http/prompts.js +40 -3
  116. package/dist/edge/http/pronunciations.js +23 -4
  117. package/dist/edge/http/providers.js +119 -3
  118. package/dist/edge/http/reviews.js +131 -0
  119. package/dist/edge/http/revision-files.js +3 -5
  120. package/dist/edge/http/revisions.js +1 -1
  121. package/dist/edge/http/run-cost.js +25 -0
  122. package/dist/edge/http/schedules.js +115 -6
  123. package/dist/edge/http/settings.js +84 -6
  124. package/dist/edge/http/storage-files.js +50 -0
  125. package/dist/edge/http/storage.js +43 -2
  126. package/dist/edge/http/studio.js +287 -0
  127. package/dist/edge/http/style-preview.js +61 -0
  128. package/dist/edge/http/subtitles.js +1 -1
  129. package/dist/edge/http/trash.js +69 -0
  130. package/dist/edge/http/tutorials.js +59 -0
  131. package/dist/edge/http/update.js +24 -3
  132. package/dist/edge/http/whats-new.js +15 -0
  133. package/dist/edge/http/youtube-edits.js +92 -0
  134. package/dist/edge/native-update.js +62 -0
  135. package/dist/extension/slopify-studio-chrome.zip +0 -0
  136. package/dist/extension/slopify-studio-firefox.zip +0 -0
  137. package/dist/host-cli/gemini-login.js +83 -0
  138. package/dist/host-cli/open-folder.js +10 -10
  139. package/dist/host-cli/runtime.js +9 -1
  140. package/dist/host-cli/status.js +4 -2
  141. package/dist/kernel/db/migrations/0025-automatic-reviews.sql +40 -0
  142. package/dist/kernel/db/migrations/0026-schedule-topic-generation.sql +23 -0
  143. package/dist/kernel/db/migrations/0027-run-cost.sql +58 -0
  144. package/dist/kernel/db/migrations/0030-automatic-retries.sql +18 -0
  145. package/dist/kernel/db/migrations/0031-prompt-history-and-description-edits.sql +33 -0
  146. package/dist/kernel/db/migrations/0032-channels.sql +65 -0
  147. package/dist/kernel/db/migrations/0034-script-prompts.sql +23 -0
  148. package/dist/kernel/db/migrations/0035-narration-aliases.sql +13 -0
  149. package/dist/kernel/db/migrations/0037-project-uploads.sql +8 -0
  150. package/dist/kernel/db/migrations/0038-voice-languages.sql +7 -0
  151. package/dist/kernel/db/migrations/0039-channel-essentials.sql +64 -0
  152. package/dist/kernel/db/migrations/0040-standalone-usage.sql +35 -0
  153. package/dist/kernel/db/migrations/0041-cast-hosts.sql +3 -0
  154. package/dist/kernel/paths.js +27 -6
  155. package/dist/kernel/pipeline.js +4 -1
  156. package/dist/kernel/ports/host-cli.js +122 -14
  157. package/dist/kernel/ports/key-probe.js +1 -0
  158. package/dist/kernel/ports/languages.js +157 -0
  159. package/dist/kernel/ports/model.js +10 -5
  160. package/dist/kernel/ports/narration-aliases.js +60 -0
  161. package/dist/kernel/ports/plan-limits.js +36 -0
  162. package/dist/kernel/ports/system-speech.js +203 -0
  163. package/dist/kernel/runner/attempt.js +17 -8
  164. package/dist/kernel/runner/audition.js +36 -0
  165. package/dist/kernel/runner/graph.js +40 -5
  166. package/dist/kernel/runner/index.js +39 -9
  167. package/dist/kernel/runner/meter.js +1 -0
  168. package/dist/kernel/runner/providers.js +142 -7
  169. package/dist/kernel/runner/retry-policy.js +43 -0
  170. package/dist/kernel/runner/standalone.js +134 -0
  171. package/dist/kernel/runner/work-authority.js +28 -2
  172. package/dist/main.js +277 -16
  173. package/dist/model-catalog.js +2 -0
  174. package/dist/patch-notes/0.4.2-to-1.0.md +93 -0
  175. package/dist/patch-notes/1.0-to-2.0.md +74 -0
  176. package/dist/patch-notes/2.0-to-3.0.md +257 -0
  177. package/dist/patch-notes/3.0.0.md +70 -0
  178. package/dist/patch-notes/3.0.1.md +105 -0
  179. package/dist/patch-notes/index.json +32 -0
  180. package/dist/slices/admission/model.js +27 -0
  181. package/dist/slices/admission/repo.js +24 -4
  182. package/dist/slices/admission/rules.js +80 -2
  183. package/dist/slices/admission/schema.js +58 -1
  184. package/dist/slices/admission/short-mode.js +47 -0
  185. package/dist/slices/admission/start.js +42 -6
  186. package/dist/slices/article/continuation.js +2 -2
  187. package/dist/slices/article/provided-entries.js +2 -2
  188. package/dist/slices/article/run.js +6 -2
  189. package/dist/slices/article/segments.js +3 -3
  190. package/dist/slices/backups/folder.js +14 -6
  191. package/dist/slices/batch/index.js +7 -2
  192. package/dist/slices/cancel/index.js +1 -1
  193. package/dist/slices/channels/cast-images.js +110 -0
  194. package/dist/slices/channels/cast-match.js +32 -0
  195. package/dist/slices/channels/images.js +19 -0
  196. package/dist/slices/channels/model.js +5 -0
  197. package/dist/slices/channels/rebrand.js +100 -0
  198. package/dist/slices/channels/repo.js +221 -0
  199. package/dist/slices/channels/runs.js +129 -0
  200. package/dist/slices/channels/schema.js +152 -0
  201. package/dist/slices/channels/service.js +180 -0
  202. package/dist/slices/channels/studio-csv.js +110 -0
  203. package/dist/slices/channels/videos.js +120 -0
  204. package/dist/slices/checkpoints/recovery.js +10 -0
  205. package/dist/slices/control/index.js +5 -2
  206. package/dist/slices/control/providers.js +4 -1
  207. package/dist/slices/document/fonts.js +1 -1
  208. package/dist/slices/document/render.js +1 -1
  209. package/dist/slices/episodes/related.js +90 -0
  210. package/dist/slices/episodes/repo.js +60 -0
  211. package/dist/slices/episodes/service.js +58 -0
  212. package/dist/slices/episodes/summarize.js +170 -0
  213. package/dist/slices/estimate/index.js +109 -28
  214. package/dist/slices/estimate/requests.js +68 -2
  215. package/dist/slices/eta/history.js +56 -0
  216. package/dist/slices/eta/model.js +63 -0
  217. package/dist/slices/eta/view.js +25 -0
  218. package/dist/slices/fixes/rules.js +73 -0
  219. package/dist/slices/fonts/coverage.js +82 -0
  220. package/dist/slices/fonts/sfnt.js +74 -0
  221. package/dist/slices/images/run.js +8 -5
  222. package/dist/slices/images/scale.js +84 -0
  223. package/dist/slices/library/diff.js +122 -0
  224. package/dist/slices/library/history.js +100 -0
  225. package/dist/slices/library/model.js +3 -0
  226. package/dist/slices/library/photorealistic.js +49 -0
  227. package/dist/slices/library/repo.js +19 -14
  228. package/dist/slices/library/save.js +62 -12
  229. package/dist/slices/library/slots.js +7 -1
  230. package/dist/slices/library/used-by.js +153 -0
  231. package/dist/slices/loudness/level-pieces.js +41 -0
  232. package/dist/slices/loudness/loudnorm.js +164 -0
  233. package/dist/slices/loudness/model.js +153 -0
  234. package/dist/slices/model-upkeep/model.js +10 -0
  235. package/dist/slices/model-upkeep/switch.js +217 -0
  236. package/dist/slices/model-upkeep/usage.js +185 -0
  237. package/dist/slices/narration/aliases-library.js +31 -0
  238. package/dist/slices/narration/aliases-schema.js +53 -0
  239. package/dist/slices/narration/aliases.js +49 -0
  240. package/dist/slices/narration/blocks.js +352 -0
  241. package/dist/slices/narration/chunk.js +19 -7
  242. package/dist/slices/narration/concat.js +2 -2
  243. package/dist/slices/narration/describe.js +45 -0
  244. package/dist/slices/narration/pauses-model.js +63 -0
  245. package/dist/slices/narration/pauses.js +269 -0
  246. package/dist/slices/narration/peaks-model.js +1 -0
  247. package/dist/slices/narration/peaks.js +94 -0
  248. package/dist/slices/narration/preparation.js +21 -3
  249. package/dist/slices/narration/pronunciation-chunks.js +3 -3
  250. package/dist/slices/narration/pronunciation.js +72 -25
  251. package/dist/slices/narration/run.js +6 -6
  252. package/dist/slices/narration/shared-glossary.js +1 -1
  253. package/dist/slices/narration/steering.js +15 -4
  254. package/dist/slices/notifications/notifier.js +47 -3
  255. package/dist/slices/notifications/rules.js +48 -3
  256. package/dist/slices/onboarding/copy.js +143 -0
  257. package/dist/slices/onboarding/first-run.js +72 -0
  258. package/dist/slices/onboarding/full-video.js +81 -0
  259. package/dist/slices/onboarding/install.js +119 -0
  260. package/dist/slices/onboarding/model.js +85 -0
  261. package/dist/slices/onboarding/packs.js +491 -0
  262. package/dist/slices/onboarding/quick-short.js +144 -0
  263. package/dist/slices/onboarding/sample.js +132 -0
  264. package/dist/slices/onboarding/state.js +68 -0
  265. package/dist/slices/onboarding/template.js +73 -0
  266. package/dist/slices/patch-notes/library.js +73 -0
  267. package/dist/slices/patch-notes/seen.js +77 -0
  268. package/dist/slices/play-drafts/convert.js +127 -8
  269. package/dist/slices/play-drafts/readiness.js +4 -1
  270. package/dist/slices/play-drafts/repo.js +2 -0
  271. package/dist/slices/play-drafts/review-inputs.js +20 -3
  272. package/dist/slices/play-drafts/schema.js +89 -0
  273. package/dist/slices/play-drafts/service.js +4 -1
  274. package/dist/slices/play-drafts/start.js +43 -18
  275. package/dist/slices/project-templates/from-project.js +59 -6
  276. package/dist/slices/project-templates/next-chapter.js +94 -0
  277. package/dist/slices/project-templates/one-off.js +26 -0
  278. package/dist/slices/project-templates/repo.js +2 -1
  279. package/dist/slices/project-templates/schema.js +7 -1
  280. package/dist/slices/project-templates/service.js +15 -4
  281. package/dist/slices/project-templates/setup.js +18 -2
  282. package/dist/slices/rebuild/admission-repo.js +1 -1
  283. package/dist/slices/rebuild/model.js +6 -0
  284. package/dist/slices/rebuild/preview-details.js +46 -3
  285. package/dist/slices/rebuild/preview-plan.js +13 -6
  286. package/dist/slices/rebuild/recipe-audio-parts.js +40 -7
  287. package/dist/slices/rebuild/recipe-audio.js +66 -12
  288. package/dist/slices/rebuild/recipe-build.js +8 -4
  289. package/dist/slices/rebuild/recipe-cast.js +18 -0
  290. package/dist/slices/rebuild/recipe-checkpoints.js +11 -9
  291. package/dist/slices/rebuild/recipe-describe.js +93 -0
  292. package/dist/slices/rebuild/recipe-edit.js +13 -1
  293. package/dist/slices/rebuild/recipe-exports.js +67 -8
  294. package/dist/slices/rebuild/recipe-input-schema.js +35 -0
  295. package/dist/slices/rebuild/recipe-legacy.js +9 -0
  296. package/dist/slices/rebuild/recipe-loudness.js +45 -0
  297. package/dist/slices/rebuild/recipe-model.js +25 -6
  298. package/dist/slices/rebuild/recipe-pauses.js +7 -0
  299. package/dist/slices/rebuild/recipe-preparation.js +3 -3
  300. package/dist/slices/rebuild/recipe-provider-choice.js +11 -1
  301. package/dist/slices/rebuild/recipe-reference.js +15 -1
  302. package/dist/slices/rebuild/recipe-reviews.js +117 -0
  303. package/dist/slices/rebuild/recipe-save.js +4 -1
  304. package/dist/slices/rebuild/recipe-shorts.js +23 -3
  305. package/dist/slices/rebuild/recipe-text.js +114 -19
  306. package/dist/slices/rebuild/recipe-validation.js +14 -2
  307. package/dist/slices/rebuild/recipe-visual.js +96 -33
  308. package/dist/slices/rebuild/recipe-voices.js +286 -0
  309. package/dist/slices/rebuild/recipe-work.js +77 -6
  310. package/dist/slices/rebuild/recipe-youtube.js +2 -0
  311. package/dist/slices/rebuild/recovery-conflict.js +7 -3
  312. package/dist/slices/rebuild/recovery-model.js +3 -0
  313. package/dist/slices/rebuild/recovery-selection.js +28 -2
  314. package/dist/slices/rebuild/recovery.js +35 -17
  315. package/dist/slices/rebuild/repo.js +3 -3
  316. package/dist/slices/rebuild/retry.js +18 -0
  317. package/dist/slices/rebuild/review-redo.js +125 -0
  318. package/dist/slices/rebuild/runtime-actions.js +5 -0
  319. package/dist/slices/rebuild/runtime-admission.js +4 -1
  320. package/dist/slices/rebuild/runtime-animate.js +4 -4
  321. package/dist/slices/rebuild/runtime-article.js +5 -5
  322. package/dist/slices/rebuild/runtime-document.js +4 -4
  323. package/dist/slices/rebuild/runtime-export-bed.js +43 -0
  324. package/dist/slices/rebuild/runtime-export-edit.js +70 -11
  325. package/dist/slices/rebuild/runtime-export-inputs.js +52 -10
  326. package/dist/slices/rebuild/runtime-export-short.js +85 -0
  327. package/dist/slices/rebuild/runtime-export.js +84 -19
  328. package/dist/slices/rebuild/runtime-figure-card.js +87 -0
  329. package/dist/slices/rebuild/runtime-image.js +21 -2
  330. package/dist/slices/rebuild/runtime-local.js +148 -24
  331. package/dist/slices/rebuild/runtime-materialize.js +8 -0
  332. package/dist/slices/rebuild/runtime-narration-reuse.js +2 -1
  333. package/dist/slices/rebuild/runtime-narration-text.js +7 -7
  334. package/dist/slices/rebuild/runtime-pictures.js +28 -0
  335. package/dist/slices/rebuild/runtime-piece-label.js +27 -0
  336. package/dist/slices/rebuild/runtime-provider.js +126 -18
  337. package/dist/slices/rebuild/runtime-publication.js +21 -5
  338. package/dist/slices/rebuild/runtime-review.js +267 -0
  339. package/dist/slices/rebuild/runtime-run.js +17 -11
  340. package/dist/slices/rebuild/runtime-shorts.js +44 -17
  341. package/dist/slices/rebuild/runtime-store.js +62 -11
  342. package/dist/slices/rebuild/runtime-subtitles.js +72 -19
  343. package/dist/slices/rebuild/runtime-voices.js +103 -0
  344. package/dist/slices/rebuild/runtime-youtube.js +7 -6
  345. package/dist/slices/rebuild/service-readiness.js +3 -3
  346. package/dist/slices/rebuild/soften.js +49 -0
  347. package/dist/slices/rebuild/transition-repo.js +9 -2
  348. package/dist/slices/research/run.js +4 -4
  349. package/dist/slices/research/synthesis.js +2 -2
  350. package/dist/slices/reviews/model.js +105 -0
  351. package/dist/slices/reviews/outcome.js +22 -0
  352. package/dist/slices/reviews/repo.js +118 -0
  353. package/dist/slices/reviews/rules.js +63 -0
  354. package/dist/slices/reviews/verdict.js +71 -0
  355. package/dist/slices/reviews/view.js +30 -0
  356. package/dist/slices/revisions/adopt-content.js +5 -2
  357. package/dist/slices/revisions/adopt.js +3 -1
  358. package/dist/slices/revisions/downloads.js +7 -1
  359. package/dist/slices/revisions/image-plan.js +183 -0
  360. package/dist/slices/revisions/mutation-assets.js +10 -0
  361. package/dist/slices/revisions/mutation-cues.js +71 -0
  362. package/dist/slices/revisions/mutation-request.js +2 -1
  363. package/dist/slices/revisions/mutations.js +97 -3
  364. package/dist/slices/revisions/schema.js +11 -1
  365. package/dist/slices/run-cost/limits.js +128 -0
  366. package/dist/slices/run-cost/meter.js +47 -0
  367. package/dist/slices/run-cost/panel.js +256 -0
  368. package/dist/slices/run-cost/pricing.js +95 -0
  369. package/dist/slices/run-cost/week.js +108 -0
  370. package/dist/slices/schedules/agenda.js +222 -0
  371. package/dist/slices/schedules/repo.js +58 -18
  372. package/dist/slices/schedules/scheduler.js +68 -17
  373. package/dist/slices/schedules/schema.js +179 -1
  374. package/dist/slices/schedules/service.js +43 -4
  375. package/dist/slices/schedules/similar.js +128 -0
  376. package/dist/slices/schedules/topic-list.js +163 -0
  377. package/dist/slices/schedules/topics.js +523 -0
  378. package/dist/slices/settings/first-run.js +106 -0
  379. package/dist/slices/settings/health.js +247 -0
  380. package/dist/slices/settings/key-guides.js +138 -0
  381. package/dist/slices/settings/key-test.js +93 -0
  382. package/dist/slices/settings/keys.js +10 -5
  383. package/dist/slices/settings/model-reach.js +52 -0
  384. package/dist/slices/settings/model.js +55 -1
  385. package/dist/slices/settings/models.js +5 -1
  386. package/dist/slices/settings/playback.js +15 -0
  387. package/dist/slices/settings/readiness.js +14 -2
  388. package/dist/slices/settings/repo.js +17 -3
  389. package/dist/slices/settings/tutorial-schema.js +5 -0
  390. package/dist/slices/settings/voices.js +64 -4
  391. package/dist/slices/settings/whats-new.js +64 -0
  392. package/dist/slices/shorts/captions.js +9 -0
  393. package/dist/slices/shorts/pick.js +3 -3
  394. package/dist/slices/shorts/prompts.js +1 -1
  395. package/dist/slices/shorts/render.js +63 -13
  396. package/dist/slices/storage/asset-name.js +3 -0
  397. package/dist/slices/storage/backup-export.js +48 -9
  398. package/dist/slices/storage/backup-format.js +38 -1
  399. package/dist/slices/storage/backup-import.js +191 -19
  400. package/dist/slices/storage/delete-project.js +7 -4
  401. package/dist/slices/storage/documents.js +87 -0
  402. package/dist/slices/storage/downloads.js +33 -1
  403. package/dist/slices/storage/files-location.js +531 -0
  404. package/dist/slices/storage/layout.js +14 -4
  405. package/dist/slices/storage/model.js +11 -0
  406. package/dist/slices/storage/portable.js +94 -12
  407. package/dist/slices/storage/reconcile.js +14 -1
  408. package/dist/slices/storage/repo.js +4 -1
  409. package/dist/slices/storage/schema.js +11 -0
  410. package/dist/slices/storage/trim.js +146 -0
  411. package/dist/slices/studio/disclosure.js +74 -0
  412. package/dist/slices/studio/model.js +30 -0
  413. package/dist/slices/studio/pack.js +211 -0
  414. package/dist/slices/studio/queue.js +69 -0
  415. package/dist/slices/studio/settings.js +170 -0
  416. package/dist/slices/style-preview/images.js +61 -0
  417. package/dist/slices/style-preview/narration.js +33 -0
  418. package/dist/slices/style-preview/render.js +148 -0
  419. package/dist/slices/style-preview/schema.js +59 -0
  420. package/dist/slices/style-preview/service.js +92 -0
  421. package/dist/slices/style-preview/settings.js +139 -0
  422. package/dist/slices/subtitles/captions.js +42 -9
  423. package/dist/slices/subtitles/model.js +22 -0
  424. package/dist/slices/subtitles/prepare.js +27 -16
  425. package/dist/slices/subtitles/transcript.js +1 -1
  426. package/dist/slices/thumbnail/run.js +7 -7
  427. package/dist/slices/trash/model.js +25 -0
  428. package/dist/slices/trash/service.js +273 -0
  429. package/dist/slices/tutorials/anchors.js +57 -0
  430. package/dist/slices/tutorials/library.js +185 -0
  431. package/dist/slices/uploads/repo.js +20 -0
  432. package/dist/slices/video/ambient-bed-schema.js +15 -0
  433. package/dist/slices/video/ambient-bed.js +95 -0
  434. package/dist/slices/video/ambient-mix.js +72 -0
  435. package/dist/slices/video/audio-inputs.js +2 -2
  436. package/dist/slices/video/cards.js +5 -2
  437. package/dist/slices/video/edit-list.js +23 -2
  438. package/dist/slices/video/edit-settings.js +13 -3
  439. package/dist/slices/video/ffmpeg.js +63 -9
  440. package/dist/slices/video/figure-card.js +492 -0
  441. package/dist/slices/video/figure-spans.js +51 -0
  442. package/dist/slices/video/plan.js +125 -13
  443. package/dist/slices/video/run.js +14 -12
  444. package/dist/slices/video/slideshow.js +39 -4
  445. package/dist/slices/voices/attribution.js +69 -0
  446. package/dist/slices/voices/audio-files.js +87 -0
  447. package/dist/slices/voices/audition.js +25 -0
  448. package/dist/slices/voices/cast.js +74 -0
  449. package/dist/slices/voices/grouping.js +59 -0
  450. package/dist/slices/voices/join.js +35 -0
  451. package/dist/slices/voices/languages.js +25 -0
  452. package/dist/slices/voices/model.js +218 -0
  453. package/dist/slices/voices/palette.js +26 -0
  454. package/dist/slices/voices/panel.js +91 -0
  455. package/dist/slices/voices/portraits.js +55 -0
  456. package/dist/slices/voices/script.js +136 -0
  457. package/dist/slices/voices/timing.js +58 -0
  458. package/dist/slices/youtube/answer.js +1 -1
  459. package/dist/slices/youtube/chapters.js +126 -0
  460. package/dist/slices/youtube/edits-repo.js +84 -0
  461. package/dist/slices/youtube/edits.js +89 -0
  462. package/dist/slices/youtube/placeholders.js +98 -0
  463. package/dist/slices/youtube/transcript.js +11 -4
  464. package/dist/tutorials/AI-CLIs.md +146 -0
  465. package/dist/tutorials/Backups.md +121 -0
  466. package/dist/tutorials/Calendar.md +103 -0
  467. package/dist/tutorials/Cast-Library.md +112 -0
  468. package/dist/tutorials/Channels.md +148 -0
  469. package/dist/tutorials/Costs-and-Run-Cost.md +136 -0
  470. package/dist/tutorials/Docker.md +171 -0
  471. package/dist/tutorials/Document-Themes.md +207 -0
  472. package/dist/tutorials/Editing-a-Project.md +204 -0
  473. package/dist/tutorials/FAQ.md +134 -0
  474. package/dist/tutorials/First-Launch-and-Welcome.md +153 -0
  475. package/dist/tutorials/Home-and-Projects.md +174 -0
  476. package/dist/tutorials/Home.md +119 -0
  477. package/dist/tutorials/Install.md +154 -0
  478. package/dist/tutorials/Intros-and-Outros.md +86 -0
  479. package/dist/tutorials/Keyboard-Shortcuts-and-Command-Palette.md +215 -0
  480. package/dist/tutorials/Library-Overview.md +75 -0
  481. package/dist/tutorials/Models.md +133 -0
  482. package/dist/tutorials/Multiple-Voices.md +161 -0
  483. package/dist/tutorials/Narration-Aliases-and-Glossary.md +129 -0
  484. package/dist/tutorials/Notifications.md +86 -0
  485. package/dist/tutorials/Other-Languages.md +110 -0
  486. package/dist/tutorials/PDF-Documents.md +100 -0
  487. package/dist/tutorials/Play-Images.md +135 -0
  488. package/dist/tutorials/Play-Narration.md +167 -0
  489. package/dist/tutorials/Play-Outputs.md +109 -0
  490. package/dist/tutorials/Play-Overview.md +216 -0
  491. package/dist/tutorials/Play-Title-and-Article.md +135 -0
  492. package/dist/tutorials/Play-Video-and-Style.md +143 -0
  493. package/dist/tutorials/Project-Page.md +228 -0
  494. package/dist/tutorials/Prompts.md +136 -0
  495. package/dist/tutorials/Providers-and-Keys.md +290 -0
  496. package/dist/tutorials/Publishing-to-YouTube.md +135 -0
  497. package/dist/tutorials/Recovery-and-Retries.md +141 -0
  498. package/dist/tutorials/Reviews-and-Checkpoints.md +143 -0
  499. package/dist/tutorials/Schedules.md +154 -0
  500. package/dist/tutorials/Settings-Reference.md +258 -0
  501. package/dist/tutorials/Shorts.md +116 -0
  502. package/dist/tutorials/Start-at-Login.md +126 -0
  503. package/dist/tutorials/Studio-Extension.md +146 -0
  504. package/dist/tutorials/Templates.md +146 -0
  505. package/dist/tutorials/Trash.md +87 -0
  506. package/dist/tutorials/Troubleshooting.md +149 -0
  507. package/dist/tutorials/Uninstalling-and-Moving.md +113 -0
  508. package/dist/tutorials/Updating-and-Patch-Notes.md +120 -0
  509. package/dist/tutorials/Video-Editing.md +135 -0
  510. package/dist/tutorials/Where-Your-Files-Live.md +149 -0
  511. package/dist/tutorials/YouTube-Description.md +132 -0
  512. package/dist/tutorials/Your-First-Short.md +125 -0
  513. package/dist/tutorials/_Footer.md +1 -0
  514. package/dist/tutorials/_Sidebar.md +69 -0
  515. package/dist/updater/service.js +66 -21
  516. package/dist/web/assets/index-DBLSfqA-.css +1 -0
  517. package/dist/web/assets/index-DFdIhmm0.js +307 -0
  518. package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  519. package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  520. package/dist/web/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  521. package/dist/web/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  522. package/dist/web/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  523. package/dist/web/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  524. package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  525. package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  526. package/dist/web/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  527. package/dist/web/assets/{pdf-BLF2i8c2.js → pdf-B9OY1ioX.js} +1 -1
  528. package/dist/web/index.html +2 -2
  529. package/package.json +5 -5
  530. package/dist/edge/docker-launch.js +0 -81
  531. package/dist/edge/docker-projects/activation.js +0 -35
  532. package/dist/edge/docker-projects/claims.js +0 -85
  533. package/dist/edge/docker-projects/committed.js +0 -40
  534. package/dist/edge/docker-projects/engine.js +0 -461
  535. package/dist/edge/docker-projects/install.js +0 -254
  536. package/dist/edge/docker-projects/prune.js +0 -66
  537. package/dist/edge/docker-projects/recover.js +0 -100
  538. package/dist/edge/docker-projects/state.js +0 -225
  539. package/dist/web/assets/index-CktpxlJd.js +0 -171
  540. package/dist/web/assets/index-DB-vOnGU.css +0 -1
  541. package/scripts/docker-run.sh +0 -3
  542. /package/dist/edge/{docker-projects → docker-install}/tree.js +0 -0
@@ -0,0 +1,126 @@
1
+ // Chapters as YouTube takes them. YouTube turns the timestamps of a description into chapters
2
+ // only when the first is at 0:00, there are at least three, each lasts at least ten seconds and
3
+ // they go up in time; otherwise it ignores the whole list. The step's answer is checked against
4
+ // those rules when it is written (`answer.ts`), but a hand edit, or a video cut again to another
5
+ // length after the description was written, can break them. So the chapters are fitted when the
6
+ // description is shown or copied, never stored: the generated file, the user's edit and every
7
+ // fingerprint stay as they were, and the page says what was changed. Pure and browser-safe.
8
+ import { minChapterSeconds, minChapters } from "./model.js";
9
+ import { parseTimestamp, youtubeTimestamp } from "./timestamps.js";
10
+ // "M:SS Title": a time YouTube reads, then a title.
11
+ function chapterLine(line) {
12
+ const trimmed = line.trim();
13
+ const match = /^(\S+)\s+(.+)$/u.exec(trimmed);
14
+ if (match === null)
15
+ return undefined;
16
+ const start = parseTimestamp(match[1] ?? "");
17
+ const title = (match[2] ?? "").trim();
18
+ return start === undefined || title === "" ? undefined : { start, title };
19
+ }
20
+ // Fits one chapters field (one "M:SS Title" per line) to YouTube's rules, in this order:
21
+ // 1. chapters out of time order are put in order, and of two at the same time the one listed
22
+ // first stays;
23
+ // 2. with the video's length known, a chapter starting at or after its end is removed;
24
+ // 3. the first chapter moves to 0:00;
25
+ // 4. the first chapter shorter than ten seconds is merged into its neighbour, again until none
26
+ // is: into the one before it (whose title stays), or, for the first chapter, into the one
27
+ // after it, which then starts at 0:00. The last chapter's length is only known with the
28
+ // video's length;
29
+ // 5. with fewer than three chapters left, the list is dropped, since YouTube would ignore it.
30
+ // Other lines in the field (a note the user typed) stay where they were. When anything changed,
31
+ // the chapter lines are rewritten in YouTube's M:SS form.
32
+ export function fitChapters(text, durationSeconds) {
33
+ const lines = text.replace(/\r\n?/gu, "\n").split("\n");
34
+ const slots = [];
35
+ const found = [];
36
+ for (const [index, line] of lines.entries()) {
37
+ const chapter = chapterLine(line);
38
+ if (chapter === undefined)
39
+ continue;
40
+ slots.push(index);
41
+ found.push(chapter);
42
+ }
43
+ if (found.length === 0)
44
+ return { text, adjustments: [] };
45
+ const adjustments = [];
46
+ const quoted = (chapter) => `"${chapter.title}"`;
47
+ let chapters = [...found].sort((a, b) => a.start - b.start);
48
+ if (chapters.some((chapter, index) => chapter !== found[index]))
49
+ adjustments.push("put the chapters in time order");
50
+ // Two chapters at the same time: the one listed first keeps it.
51
+ const distinct = [];
52
+ for (const chapter of chapters) {
53
+ const previous = distinct.at(-1);
54
+ if (previous !== undefined && previous.start === chapter.start)
55
+ adjustments.push(`merged ${quoted(chapter)} (0 s) into ${quoted(previous)}`);
56
+ else
57
+ distinct.push(chapter);
58
+ }
59
+ chapters = distinct;
60
+ const end = durationSeconds !== undefined && Number.isFinite(durationSeconds) && durationSeconds > 0
61
+ ? durationSeconds
62
+ : undefined;
63
+ if (end !== undefined) {
64
+ const after = chapters.filter((chapter, index) => index > 0 && chapter.start >= end);
65
+ for (const chapter of after)
66
+ adjustments.push(`removed ${quoted(chapter)} (${youtubeTimestamp(chapter.start)}), which starts after the video ends at ${youtubeTimestamp(end)}`);
67
+ chapters = chapters.filter((chapter) => !after.includes(chapter));
68
+ }
69
+ const first = chapters[0];
70
+ if (first !== undefined && first.start !== 0) {
71
+ adjustments.push(`moved the first, ${quoted(first)}, from ${youtubeTimestamp(first.start)} to 0:00`);
72
+ chapters = [{ start: 0, title: first.title }, ...chapters.slice(1)];
73
+ }
74
+ const length = (index) => {
75
+ const next = chapters[index + 1];
76
+ const start = chapters[index]?.start ?? 0;
77
+ if (next !== undefined)
78
+ return next.start - start;
79
+ return end === undefined ? Number.POSITIVE_INFINITY : end - start;
80
+ };
81
+ const seconds = (value) => `${String(Math.max(0, Math.floor(value)))} s`;
82
+ while (chapters.length > 1) {
83
+ const short = chapters.findIndex((_, index) => length(index) < minChapterSeconds);
84
+ if (short === -1)
85
+ break;
86
+ const chapter = chapters[short];
87
+ if (chapter === undefined)
88
+ break;
89
+ const lasted = seconds(length(short));
90
+ if (short === 0) {
91
+ const next = chapters[1];
92
+ if (next === undefined)
93
+ break;
94
+ adjustments.push(`merged ${quoted(chapter)} (${lasted}) into ${quoted(next)}`);
95
+ chapters = [{ start: 0, title: next.title }, ...chapters.slice(2)];
96
+ }
97
+ else {
98
+ const previous = chapters[short - 1];
99
+ if (previous === undefined)
100
+ break;
101
+ adjustments.push(`merged ${quoted(chapter)} (${lasted}) into ${quoted(previous)}`);
102
+ chapters = chapters.filter((_, index) => index !== short);
103
+ }
104
+ }
105
+ if (chapters.length < minChapters) {
106
+ adjustments.push(`left the chapter list out, since YouTube needs at least ${String(minChapters)} chapters of ${String(minChapterSeconds)} s or more and only ${String(chapters.length)} ${chapters.length === 1 ? "is" : "are"} left`);
107
+ chapters = [];
108
+ }
109
+ if (adjustments.length === 0)
110
+ return { text, adjustments };
111
+ // The chapters go back into the first slots in order; the slots left over are removed.
112
+ const bySlot = new Map(slots.map((slot, index) => [slot, chapters[index]]));
113
+ const rewritten = lines.flatMap((line, index) => {
114
+ if (!bySlot.has(index))
115
+ return [line];
116
+ const chapter = bySlot.get(index);
117
+ return chapter === undefined ? [] : [`${youtubeTimestamp(chapter.start)} ${chapter.title}`];
118
+ });
119
+ return { text: rewritten.join("\n").trim(), adjustments };
120
+ }
121
+ // The sentence shown beside the description; undefined when nothing was changed.
122
+ export function chapterNotice(adjustments) {
123
+ return adjustments.length === 0
124
+ ? undefined
125
+ : `Chapters adjusted for YouTube: ${adjustments.join("; ")}.`;
126
+ }
@@ -0,0 +1,84 @@
1
+ // Where the hand edits and the channel links are kept: the edits per project in
2
+ // `youtube_description_edits`, each channel's links in its brand kit, and the older Settings list
3
+ // of links (read as the default channel's until its Brand tab saves its own) as one `settings` row.
4
+ import { z } from "zod";
5
+ import { defaultChannelId } from "../channels/model.js";
6
+ import { channelById, projectChannelId } from "../channels/repo.js";
7
+ import { readSetting, writeSetting } from "../settings/repo.js";
8
+ import { chapterNotice, fitChapters } from "./chapters.js";
9
+ import { composeDescription, descriptionFields, resolveFields, shownFields, splitDescription, } from "./edits.js";
10
+ import { fillPlaceholders, mergeLinks } from "./placeholders.js";
11
+ export const channelLinksKey = "channel_links";
12
+ const fieldEdit = z.object({ base: z.string(), text: z.string() }).strict();
13
+ const editsSchema = z
14
+ .object(Object.fromEntries(descriptionFields.map((field) => [field, fieldEdit.optional()])))
15
+ .strict();
16
+ const linksSchema = z.array(z.object({ name: z.string(), url: z.string() }).strict());
17
+ const none = { fields: {}, links: [] };
18
+ export function readDescriptionEdits(db, projectId) {
19
+ const row = db
20
+ .prepare("SELECT fields_json,links_json FROM youtube_description_edits WHERE project_id=?")
21
+ .get(projectId);
22
+ if (row === undefined)
23
+ return none;
24
+ const parsed = z.object({ fields_json: z.string(), links_json: z.string() }).parse(row);
25
+ return {
26
+ fields: editsSchema.parse(JSON.parse(parsed.fields_json)),
27
+ links: linksSchema.parse(JSON.parse(parsed.links_json)),
28
+ };
29
+ }
30
+ export function writeDescriptionEdits(db, projectId, edits, updatedAt) {
31
+ if (Object.keys(edits.fields).length === 0 && edits.links.length === 0) {
32
+ db.prepare("DELETE FROM youtube_description_edits WHERE project_id=?").run(projectId);
33
+ return;
34
+ }
35
+ db.prepare(`INSERT INTO youtube_description_edits (project_id,fields_json,links_json,updated_at) VALUES (?,?,?,?)
36
+ ON CONFLICT(project_id) DO UPDATE SET fields_json=excluded.fields_json,links_json=excluded.links_json,updated_at=excluded.updated_at`).run(projectId, JSON.stringify(edits.fields), JSON.stringify(edits.links), updatedAt);
37
+ }
38
+ export function readChannelLinks(db) {
39
+ const value = readSetting(db, channelLinksKey);
40
+ if (value === undefined)
41
+ return [];
42
+ // A hand-edited row that no longer parses reads as no links rather than breaking the page;
43
+ // saving the list in Settings writes a good one again.
44
+ try {
45
+ const parsed = linksSchema.safeParse(JSON.parse(value));
46
+ return parsed.success ? parsed.data : [];
47
+ }
48
+ catch {
49
+ return [];
50
+ }
51
+ }
52
+ export function writeChannelLinks(db, links) {
53
+ writeSetting(db, channelLinksKey, JSON.stringify(links.map((link) => ({ name: link.name.trim(), url: link.url.trim() }))));
54
+ }
55
+ // A channel's links: the ones its Brand tab saved. The default channel, until its Brand tab
56
+ // saves a list of its own (even an empty one), reads the older Settings list, so links saved
57
+ // before channels had their own keep filling. A channel that is gone has none.
58
+ export function readChannelLinksFor(db, channelId) {
59
+ const channel = channelById(db, channelId);
60
+ if (channel === undefined)
61
+ return [];
62
+ if (channel.brand.links !== undefined)
63
+ return channel.brand.links;
64
+ return channel.id === defaultChannelId ? readChannelLinks(db) : [];
65
+ }
66
+ // The description and tags as the project page shows and copies them: the user's edits over
67
+ // the generated text, the chapters fitted to YouTube's rules (`chapters.ts`; the last one's
68
+ // length only checked when the video's length is given), placeholders filled from the
69
+ // project's and its channel's links (one with no link stays as typed). For anything that hands
70
+ // the description on, such as Prepare upload and the downloads. `chapterNotice` says what the
71
+ // fitting changed.
72
+ export function effectiveDescription(db, projectId, generated) {
73
+ const edits = readDescriptionEdits(db, projectId);
74
+ const shown = shownFields(resolveFields(splitDescription(generated.description, generated.tags), edits.fields));
75
+ const fitted = fitChapters(shown.chapters, generated.durationSeconds);
76
+ const notice = chapterNotice(fitted.adjustments);
77
+ const links = mergeLinks(readChannelLinksFor(db, projectChannelId(db, projectId)), edits.links);
78
+ return {
79
+ description: fillPlaceholders(composeDescription({ ...shown, chapters: fitted.text }), links)
80
+ .text,
81
+ tags: fillPlaceholders(shown.tags, links).text,
82
+ ...(notice === undefined ? {} : { chapterNotice: notice }),
83
+ };
84
+ }
@@ -0,0 +1,89 @@
1
+ // Hand edits to the YouTube description. The project page lets the user edit the summary,
2
+ // the chapters, the hashtags and the tags in place; each edit is saved with the generated
3
+ // text it was made from (`base`), so when the description is written again the page can tell
4
+ // a field the user changed from one they left alone. Browser-safe: the project page applies
5
+ // these rules to what it shows, and the server only stores the edits.
6
+ import { parseTimestamp } from "./timestamps.js";
7
+ export const descriptionFields = ["summary", "chapters", "hashtags", "tags"];
8
+ // Splits a written description back into its three parts. `assembleDescription` (answer.ts) lays it out as
9
+ // the summary, a blank line, one chapter per line, a blank line and the hashtags; reading it
10
+ // from the end keeps a summary with blank lines of its own whole.
11
+ export function splitDescription(description, tags) {
12
+ const lines = description.replace(/\r\n?/gu, "\n").split("\n");
13
+ while (lines.length > 0 && lines.at(-1)?.trim() === "")
14
+ lines.pop();
15
+ let hashtags = "";
16
+ const last = lines.at(-1)?.trim() ?? "";
17
+ if (last !== "" && last.split(/\s+/u).every((word) => /^#[\p{L}\p{N}_]+$/u.test(word))) {
18
+ hashtags = last;
19
+ lines.pop();
20
+ }
21
+ while (lines.length > 0 && lines.at(-1)?.trim() === "")
22
+ lines.pop();
23
+ const chapters = [];
24
+ while (lines.length > 0 && isChapterLine(lines.at(-1) ?? "")) {
25
+ chapters.unshift((lines.pop() ?? "").trim());
26
+ }
27
+ return {
28
+ summary: lines.join("\n").trim(),
29
+ chapters: chapters.join("\n"),
30
+ hashtags,
31
+ tags: tags.trim(),
32
+ };
33
+ }
34
+ function isChapterLine(line) {
35
+ const [time = "", ...title] = line.trim().split(/\s+/u);
36
+ return parseTimestamp(time) !== undefined && title.length > 0;
37
+ }
38
+ // The description as YouTube takes it, from the fields as shown: the same layout
39
+ // `assembleDescription` writes, leaving out a part the user emptied.
40
+ export function composeDescription(fields) {
41
+ return [fields.summary.trim(), fields.chapters.trim(), fields.hashtags.trim()]
42
+ .filter((part) => part !== "")
43
+ .join("\n\n");
44
+ }
45
+ // The rules for one field:
46
+ // - never edited: the generated text;
47
+ // - edited, and the generated text is still the one the edit started from: the user's text;
48
+ // - edited, and the generated text changed since: the user's text, with the new generated text
49
+ // pending for the user to take or dismiss - unless the two now agree, and then there is
50
+ // nothing to choose;
51
+ // - not written yet: the user's text if there is one, otherwise nothing.
52
+ export function resolveField(generated, edit) {
53
+ if (edit === undefined)
54
+ return { text: generated ?? "", edited: false };
55
+ if (generated === undefined || edit.base === generated)
56
+ return { text: edit.text, edited: true };
57
+ if (edit.text === generated)
58
+ return { text: generated, edited: false };
59
+ return { text: edit.text, edited: true, pending: generated };
60
+ }
61
+ export function resolveFields(generated, edits) {
62
+ const one = (field) => resolveField(generated?.[field], edits[field]);
63
+ return {
64
+ summary: one("summary"),
65
+ chapters: one("chapters"),
66
+ hashtags: one("hashtags"),
67
+ tags: one("tags"),
68
+ };
69
+ }
70
+ export function shownFields(resolved) {
71
+ return {
72
+ summary: resolved.summary.text,
73
+ chapters: resolved.chapters.text,
74
+ hashtags: resolved.hashtags.text,
75
+ tags: resolved.tags.text,
76
+ };
77
+ }
78
+ // Saving an edit: text equal to the generated text is no edit at all, so the field follows the
79
+ // next regeneration again.
80
+ export function withEdit(edits, field, text, generated) {
81
+ if (text === generated)
82
+ return withoutEdit(edits, field);
83
+ return { ...edits, [field]: { base: generated, text } };
84
+ }
85
+ // "Use it": the field follows the generated text again.
86
+ export function withoutEdit(edits, field) {
87
+ const { [field]: _dropped, ...rest } = edits;
88
+ return rest;
89
+ }
@@ -0,0 +1,98 @@
1
+ // Channel links and the placeholders that use them. A description (the generated one, the
2
+ // user's edit, or what a Description prompt told the model to write) can hold `{{Patreon}}` or
3
+ // `{{Previous video}}`; they are filled from the saved links when the description is shown or
4
+ // copied, never when it is written, so changing a link changes every description at once.
5
+ // A placeholder with no link of that name stays in the text as typed and is marked, never
6
+ // silently dropped. Browser-safe.
7
+ // The link YouTube descriptions most often point at, and the one that changes per project:
8
+ // the project page offers it by name, and a project's own value wins over its channel's list.
9
+ export const previousVideoLink = "Previous video";
10
+ export const channelLinkNameMax = 60;
11
+ export const channelLinkUrlMax = 2000;
12
+ export const channelLinksMax = 50;
13
+ const placeholder = /\{\{([^{}\n]{1,80})\}\}/gu;
14
+ // Names match without regard to case or repeated spaces: `{{previous video}}` is the
15
+ // "Previous video" link.
16
+ export function linkKey(name) {
17
+ return name.trim().replace(/\s+/gu, " ").toLowerCase();
18
+ }
19
+ // The project's own links first, so one of the same name replaces the Settings one.
20
+ export function mergeLinks(settings, project) {
21
+ const seen = new Set();
22
+ const merged = [];
23
+ for (const link of [...project, ...settings]) {
24
+ const key = linkKey(link.name);
25
+ if (seen.has(key) || link.url.trim() === "")
26
+ continue;
27
+ seen.add(key);
28
+ merged.push(link);
29
+ }
30
+ return merged;
31
+ }
32
+ export function placeholderParts(text, links) {
33
+ const byName = new Map(links.map((link) => [linkKey(link.name), link.url]));
34
+ const parts = [];
35
+ let at = 0;
36
+ for (const match of text.matchAll(placeholder)) {
37
+ const index = match.index;
38
+ if (index > at)
39
+ parts.push({ kind: "text", text: text.slice(at, index) });
40
+ const name = (match[1] ?? "").trim();
41
+ parts.push({ kind: "placeholder", raw: match[0], name, url: byName.get(linkKey(name)) });
42
+ at = index + match[0].length;
43
+ }
44
+ if (at < text.length)
45
+ parts.push({ kind: "text", text: text.slice(at) });
46
+ return parts;
47
+ }
48
+ export function fillPlaceholders(text, links) {
49
+ const unknown = [];
50
+ const filled = placeholderParts(text, links)
51
+ .map((part) => {
52
+ if (part.kind === "text")
53
+ return part.text;
54
+ if (part.url !== undefined)
55
+ return part.url;
56
+ if (!unknown.some((name) => linkKey(name) === linkKey(part.name)))
57
+ unknown.push(part.name);
58
+ return part.raw;
59
+ })
60
+ .join("");
61
+ return { text: filled, unknown };
62
+ }
63
+ // Why a list of links cannot be saved, in the words Settings shows; undefined when it can.
64
+ export function channelLinksProblem(links) {
65
+ if (links.length > channelLinksMax)
66
+ return `Keep at most ${String(channelLinksMax)} links. Remove some.`;
67
+ const seen = new Set();
68
+ for (const [index, link] of links.entries()) {
69
+ const row = `Link ${String(index + 1)}`;
70
+ const name = link.name.trim();
71
+ if (name === "")
72
+ return `${row} has no name. Give it one, such as Patreon.`;
73
+ if (name.length > channelLinkNameMax)
74
+ return `${row}'s name is over ${String(channelLinkNameMax)} characters. Shorten it.`;
75
+ if (/[{}]/u.test(name))
76
+ return `${row}'s name contains { or }. Use the name alone; the description writes it as {{${name.replace(/[{}]/gu, "")}}}.`;
77
+ if (seen.has(linkKey(name)))
78
+ return `Two links are named "${name}". Rename one, since {{${name}}} can only fill with one.`;
79
+ seen.add(linkKey(name));
80
+ const url = link.url.trim();
81
+ if (url.length > channelLinkUrlMax)
82
+ return `The "${name}" link is over ${String(channelLinkUrlMax)} characters. Use a shorter address.`;
83
+ if (url === "")
84
+ return `The "${name}" link has no address. Paste it, starting with https://.`;
85
+ if (!isWebAddress(url))
86
+ return `The "${name}" link is not a web address. Paste the full address, starting with https://.`;
87
+ }
88
+ return undefined;
89
+ }
90
+ function isWebAddress(url) {
91
+ try {
92
+ const parsed = new URL(url);
93
+ return parsed.protocol === "https:" || parsed.protocol === "http:";
94
+ }
95
+ catch {
96
+ return false;
97
+ }
98
+ }
@@ -51,7 +51,9 @@ const sentenceLongestSeconds = 30;
51
51
  // The narration as numbered sentences, for the Shorts step: the model picks clips by
52
52
  // sentence number and Slopify cuts at the sentences' own word times, so a clip never starts
53
53
  // or ends mid-word. A sentence closes at its full stop, question or exclamation mark, at a
54
- // pause of 1.5 s (the gap between intro, body and outro), or after 30 s.
54
+ // pause of 1.5 s (the gap between intro, body and outro), or after 30 s. On a multi-voice run
55
+ // it also closes where one speaker's turn ends, so a clip always starts and ends on a turn's
56
+ // sentence and never cuts a speaker off mid-line.
55
57
  export function transcriptSentences(words) {
56
58
  const sentences = [];
57
59
  let first = -1;
@@ -73,6 +75,7 @@ export function transcriptSentences(words) {
73
75
  text,
74
76
  firstWord: first,
75
77
  lastWord: last,
78
+ ...(opening.speaker === undefined ? {} : { speaker: opening.speaker }),
76
79
  });
77
80
  first = -1;
78
81
  };
@@ -82,6 +85,7 @@ export function transcriptSentences(words) {
82
85
  if (opening !== undefined &&
83
86
  previous !== undefined &&
84
87
  (word.start - previous.end >= pauseSeconds ||
88
+ word.turn !== previous.turn ||
85
89
  word.end - opening.start > sentenceLongestSeconds))
86
90
  flush(index - 1);
87
91
  if (first < 0)
@@ -93,9 +97,12 @@ export function transcriptSentences(words) {
93
97
  return sentences;
94
98
  }
95
99
  // One sentence per line, led by its number and its span in the video, which is what the
96
- // model picks from.
97
- export function sentencesText(sentences) {
100
+ // model picks from; on a multi-voice run, with the speaker's name before the words.
101
+ export function sentencesText(sentences, names) {
98
102
  return sentences
99
- .map((sentence) => `[${String(sentence.number)}] (${youtubeTimestamp(sentence.start)}-${youtubeTimestamp(sentence.end)}) ${sentence.text}`)
103
+ .map((sentence) => {
104
+ const name = sentence.speaker === undefined ? undefined : names?.[sentence.speaker];
105
+ return `[${String(sentence.number)}] (${youtubeTimestamp(sentence.start)}-${youtubeTimestamp(sentence.end)}) ${name === undefined ? "" : `${name}: `}${sentence.text}`;
106
+ })
100
107
  .join("\n");
101
108
  }
@@ -0,0 +1,146 @@
1
+ # AI CLIs
2
+
3
+ Slopify can write the text with the Claude Code, Codex or Gemini command-line tools, and draw the images with Codex, using the sign-in you already have. You need no API key for those steps, and their calls count toward your plan's limits instead of costing per call.
4
+
5
+ **Where to find it:** Settings → Providers → the **Text** list (Claude Code CLI, Codex CLI, Gemini CLI) and the **Images** list (Codex CLI).
6
+
7
+ ## What each tool does
8
+
9
+ | Tool | Command | Used for | Signs in with | Calls at once |
10
+ |---|---|---|---|---|
11
+ | **Claude Code CLI** | `claude` | Text: research, script, article, titles, descriptions, reviews | Your Claude account | 3 |
12
+ | **Codex CLI** | `codex` | Text | Your ChatGPT account | 3 |
13
+ | **Gemini CLI** | `gemini` | Text | Your Google account | 3 |
14
+ | **Codex CLI** (Images) | `codex` | Images and thumbnails | Your ChatGPT account (the same sign-in and path as Codex text) | 4 |
15
+
16
+ Codex text and Codex images share one sign-in, one executable path and one plan allowance.
17
+
18
+ Codex CLI must be version 0.149.1 or newer. An older Codex shows **Needs attention** and says to update it.
19
+
20
+ ## Set up a CLI
21
+
22
+ 1. Install the tool on the computer that runs Slopify, following the tool's own instructions.
23
+ 2. Sign in once in a terminal:
24
+ - Claude Code: `claude auth login`
25
+ - Codex: `codex login`
26
+ - Gemini: run `gemini` and follow its login steps.
27
+ 3. Open **Settings → Providers** (reload the page if it was already open).
28
+ 4. Pick the tool in the **Text** or **Images** list. Its row should say **Ready**, and its setup shows **Installed, version …** and the **Command** Slopify runs.
29
+ 5. Choose **Check all** to confirm it is signed in.
30
+
31
+ On the very first launch, before any key is saved, Slopify looks for all three tools. If it finds any, it says so and picks one for text on new Play drafts, preferring Claude Code, then Codex, then Gemini, and Codex for images when found. The CLIs can't speak, but the narration needs no key either: without a voice key, the [System voice](Providers-and-Keys#the-system-voice) reads it with your computer's own speech. See [First-Launch-and-Welcome](First-Launch-and-Welcome).
32
+
33
+ ## Row states
34
+
35
+ | State | Meaning |
36
+ |---|---|
37
+ | **Ready** | Found, new enough and not reporting a sign-in problem. |
38
+ | **Needs attention** | Found, but something is wrong (for example an old Codex version). The detail says what. |
39
+ | **Not found** | Not on PATH, or not at the saved path. |
40
+
41
+ ## Set the executable path
42
+
43
+ Slopify finds each tool on PATH, the list of folders your terminal searches. Set a path when the tool is installed somewhere else, or you have more than one copy.
44
+
45
+ 1. Pick the tool in **Settings → Providers**.
46
+ 2. Choose **Change path**.
47
+ 3. In **Executable path**, type the absolute path to the program file, without quotes or arguments. Leave it blank to go back to finding it on PATH.
48
+ 4. Choose **Save path**. Slopify checks the tool answers before saving; a folder or a missing file is refused with the reason.
49
+ 5. Choose **Close path** to fold the field away.
50
+
51
+ | Option | What it does | Default |
52
+ |---|---|---|
53
+ | **Executable path** | Where the tool is on this computer. | Blank (found on PATH) |
54
+
55
+ ## Check the sign-in
56
+
57
+ **Check all** (Settings → Providers) asks each tool:
58
+
59
+ - **Installed**: found, with its version.
60
+ - **Signed in**: Slopify asks Claude Code and Codex directly. If one is signed out, the line tells you the command to run (`claude auth login` or `codex login`). For Gemini CLI, Slopify reads the sign-in from its own files in `~/.gemini`, without starting it: the method saved in `settings.json`, then a Google sign-in in `oauth_creds.json` or a `GEMINI_API_KEY`. When the files don't say, it asks you to run `gemini` in a terminal once to confirm.
61
+ - **Chosen models**: every model your templates, schedules, drafts and unfinished projects pick is still offered by the tool.
62
+
63
+ Each tool's row has **Check again**, to check that tool alone after you fix something.
64
+
65
+ If a run fails because a tool's sign-in expired, the project offers **Copy sign-in command** and **Check again**: run the copied command in a terminal on the computer running the tool and sign in, then press **Check again**. Once the tool answers that it is signed in, the step runs again by itself. See [Recovery and Retries](Recovery-and-Retries#sign-a-cli-in-again).
66
+
67
+ ## Pick a model and thinking level
68
+
69
+ A CLI's model list comes from the tool installed on your computer, not from the model catalogue. On Play:
70
+
71
+ 1. Pick the tool as the text provider (or Codex CLI for images).
72
+ 2. Pick a **Text model** from the list. The refresh button beside it (**Refresh models**) asks the tool for its current list; **Custom ID** lets you type a model the list does not show.
73
+ 3. If the model offers it, pick **Thinking** (text) or **Effort** (images). **Model default** leaves it to the tool. The levels come from the model: for example Low, Medium, High and Xhigh. Higher levels can write or draw more carefully, but take longer and use more of your plan.
74
+
75
+ If a model is missing when a run reaches it, the run stops with "The chosen model is not available in the AI command-line tool on your computer." Update the tool, or pick another model in **Edit project** under Providers, then **Continue the run**. A thinking level the model does not support stops the same way.
76
+
77
+ ## Codex images
78
+
79
+ Codex draws each image as its own job.
80
+
81
+ - **Codex default** runs with the tool's own defaults: no model and no effort on the command line.
82
+ - Any other model is one of Codex's own models (the same list the Codex text provider shows), run with the **Effort** you pick. With a chosen model or effort, or a reference picture to match, the agent may look at its work and redraw it.
83
+ - Each image may take several minutes; the time limit for one image is 30 minutes.
84
+ - Up to 4 images are drawn at once.
85
+
86
+ Codex images count toward your Codex plan. There is no API price for agent-drawn images, so the cost pages show them as $0 on your plan with no API figure.
87
+
88
+ ## Plan limits and waiting them out
89
+
90
+ The CLIs bill your subscription, which gives a share of a 5-hour and a weekly allowance rather than a bill. Slopify shows what the tools report:
91
+
92
+ - **Claude Code** reports its 5-hour and 7-day windows during each call.
93
+ - **Codex** is asked before and after each call. It reports whole percents, so a short run can show "under 1%".
94
+ - **Gemini** reports none.
95
+
96
+ You see these on a project's **Run cost** tab, in the run's **Cost so far** panel, and on Home under **This week**. See [Costs-and-Run-Cost](Costs-and-Run-Cost).
97
+
98
+ When a tool says its allowance is used up, the run does not fail:
99
+
100
+ 1. Slopify stores the reset time the tool gave (per tool, not per project).
101
+ 2. Every call to that tool, from any project, waits until 2 minutes after the reset. Without a stated time, it checks again every 30 minutes.
102
+ 3. The project's status reads **Waiting for limits**, with a line such as "Waiting for Codex limits (resets at 14:00)." The same words show on Home's Running now, the Projects row and the Calendar.
103
+ 4. When the reset passes, the run continues by itself.
104
+
105
+ Other providers are not held up: a project whose narration uses a key carries on while its Codex images wait. The wait survives a restart; a project that was waiting when Slopify stopped resumes on the next start and waits again. A project you paused or cancelled meanwhile is left alone.
106
+
107
+ Gemini's daily quota is handled the same way ("Slopify waits and tries again later by itself").
108
+
109
+ ## In Docker: the host helper
110
+
111
+ A container cannot use the CLIs installed on your computer directly. On Linux, the Docker installer offers a small helper that runs the tools on your computer, outside the container, with the sign-ins they already have.
112
+
113
+ 1. Install and sign in to the CLIs on your computer first.
114
+ 2. Run the Docker installer (`npx @gentbajko/slopify@latest --docker`). If a CLI is found, it asks once whether to set up the helper. `--accept-host-cli` approves without asking; `--host-cli=off` skips it.
115
+ 3. In Settings → Providers the tools show **Managed on host**. Their path cannot be changed there.
116
+
117
+ What to know:
118
+
119
+ - The helper runs as you, as the user service `slopify-cli-bridge.service`, and talks to the container over a private socket. No network port is opened and no sign-in is copied into Docker.
120
+ - After you install, update or remove a CLI, run the Docker installer again so Slopify sees the change.
121
+ - Run cost and plan-limit shares work the same as without Docker. An older helper may not pass them through, and the cost page then says the share is not known.
122
+ - With the helper off, Settings shows the CLIs as unavailable. API-key providers still work.
123
+
124
+ Check or turn off the helper:
125
+
126
+ ```sh
127
+ systemctl --user status slopify-cli-bridge.service
128
+ systemctl --user disable --now slopify-cli-bridge.service
129
+ ```
130
+
131
+ Turning it off leaves Docker, your API providers, your data and your host sign-ins as they are. See [Docker](Docker).
132
+
133
+ ## Tips
134
+
135
+ - A CLI step on Play's estimate reads "$0 on your plan · ~$X via API": what the same tokens would cost with a key.
136
+ - If your plan runs out often, split the work: keep text on a CLI and put images on a keyed provider, or the other way round.
137
+ - Use **Check all** after updating a CLI; a newer tool can drop a model your template still names.
138
+
139
+ ## Related pages
140
+
141
+ - [Providers-and-Keys](Providers-and-Keys)
142
+ - [Models](Models)
143
+ - [Costs-and-Run-Cost](Costs-and-Run-Cost)
144
+ - [Docker](Docker)
145
+ - [Recovery-and-Retries](Recovery-and-Retries)
146
+ - [Troubleshooting](Troubleshooting)