@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,211 @@
1
+ import { readFileSync, statSync } from "node:fs";
2
+ import { z } from "zod";
3
+ import { thumbnailCountOf } from "../admission/model.js";
4
+ import { projectById } from "../admission/repo.js";
5
+ import { channelById, projectChannelId } from "../channels/repo.js";
6
+ import { namesPhotorealisticPrompt } from "../library/photorealistic.js";
7
+ import { isClipPath } from "../rebuild/runtime-export-edit.js";
8
+ import { listVoices } from "../settings/repo.js";
9
+ import { realPersonVoiceIds } from "../settings/voices.js";
10
+ import { fullVideoLine } from "../shorts/model.js";
11
+ import { assetOf } from "../storage/asset-name.js";
12
+ import { contentTypeOf, downloadName, slugOf } from "../storage/downloads.js";
13
+ import { outputPath } from "../storage/layout.js";
14
+ import { outputsOf } from "../storage/repo.js";
15
+ import { videoEditOf } from "../video/edit-settings.js";
16
+ import { usesVoices } from "../voices/model.js";
17
+ import { effectiveDescription } from "../youtube/edits-repo.js";
18
+ import { aiDisclosureOf } from "./disclosure.js";
19
+ import { studioAudience, studioTitleMax, } from "./model.js";
20
+ import { readRealFootage, readStudioPlaylist } from "./settings.js";
21
+ // What `shorts.json` holds for each picked clip; only what the pack uses.
22
+ const clipsSchema = z.object({
23
+ shorts: z.array(z.object({
24
+ number: z.number(),
25
+ first: z.number().optional(),
26
+ last: z.number().optional(),
27
+ title: z.string(),
28
+ description: z.string(),
29
+ hashtags: z.array(z.string()),
30
+ })),
31
+ });
32
+ // Builds the pack from the project's current outputs: the video, the YouTube description and
33
+ // tags, the thumbnails and each short. Nothing is generated here; a part that isn't made yet
34
+ // is left empty and named in `missing` with what to do about it.
35
+ export function uploadPack(deps, projectId) {
36
+ const project = projectById(deps.db, projectId);
37
+ if (project === undefined)
38
+ return { ok: false, reason: "unknown-project" };
39
+ const config = project.config;
40
+ const outputs = outputsOf(deps.db, projectId).filter((output) => available(deps, projectId, output));
41
+ const slug = slugOf(project.title);
42
+ const file = (output) => ({
43
+ url: `/files/${projectId}/${assetOf(output)}`,
44
+ asset: assetOf(output),
45
+ filename: downloadName(slug, output),
46
+ contentType: contentTypeOf(output.path),
47
+ bytes: output.bytes,
48
+ });
49
+ const text = (output) => output === undefined
50
+ ? undefined
51
+ : readFileSync(outputPath(deps.paths, projectId, output.path), "utf8").trim();
52
+ const channelId = projectChannelId(deps.db, projectId);
53
+ // The channel's own playlist, else the default (Settings → YouTube Studio).
54
+ const playlist = readStudioPlaylist(deps.db, channelId);
55
+ const missing = [];
56
+ // The channel's setting, then what the project narrates and shows (`disclosure.ts`).
57
+ const setting = channelById(deps.db, channelId)?.aiDisclosure ?? "auto";
58
+ const realPersonVoices = realPersonVoicesOf(deps, config);
59
+ const uploadedClips = config.sources.images === "provide"
60
+ ? outputs.filter((output) => output.role === "image" && isClipPath(output.path)).length
61
+ : 0;
62
+ const realFootage = uploadedClips > 0 && readRealFootage(deps.db, projectId);
63
+ const atmosphere = videoEditOf(config).atmosphere;
64
+ const disclosure = (kind) => aiDisclosureOf({
65
+ setting,
66
+ kind,
67
+ realPersonVoices,
68
+ // A short's pictures are always drawn anew, never the uploaded clips.
69
+ realFootage: kind === "video" && realFootage,
70
+ footageOverlay: atmosphere === "none" ? undefined : atmosphere,
71
+ photorealistic: kind === "short"
72
+ ? namesPhotorealisticPrompt(deps.db, config.shorts?.imagePrompt)
73
+ : config.sources.images === "generate" &&
74
+ config.imagePrompts.some((prompt) => namesPhotorealisticPrompt(deps.db, prompt.name)),
75
+ });
76
+ const video = outputs.find((output) => output.role === "video");
77
+ if (video === undefined)
78
+ missing.push("The video isn't made yet. Let the Video stage finish (or use Continue the run on the project page), then open Prepare upload again.");
79
+ const written = text(outputs.find((output) => output.role === "youtube_description"));
80
+ const writtenTags = text(outputs.find((output) => output.role === "youtube_tags"));
81
+ // As the project page shows it: the user's edits, and channel links filled in.
82
+ const edited = written === undefined
83
+ ? undefined
84
+ : effectiveDescription(deps.db, projectId, {
85
+ description: written,
86
+ tags: writtenTags ?? "",
87
+ // The last chapter is checked against the video's own length when it is known.
88
+ durationSeconds: video?.durationMs === null || video?.durationMs === undefined
89
+ ? undefined
90
+ : video.durationMs / 1000,
91
+ });
92
+ const tagsFile = edited?.tags ?? writtenTags;
93
+ const description = edited?.description;
94
+ if (description === undefined)
95
+ missing.push(config.youtubeDescription === true
96
+ ? "The YouTube description isn't written yet. It is written after the video's subtitle timing; wait for the Video stage to finish."
97
+ : "No YouTube description is written for this project. Turn on YouTube description in Edit project → Prompts to have one written with chapters and tags.");
98
+ const count = thumbnailCountOf(config);
99
+ // The first thumbnail, then the second and third, and never one left from when the project
100
+ // made three and now makes one.
101
+ const thumbnails = outputs
102
+ .filter((output) => output.role === "thumbnail" && (output.meta.index ?? 1) <= count)
103
+ .sort((a, b) => (a.meta.index ?? 1) - (b.meta.index ?? 1));
104
+ if (thumbnails.length === 0)
105
+ missing.push(config.sources.thumbnail === "off"
106
+ ? "This project makes no thumbnail, so YouTube will pick a frame. Set Thumbnail in Edit project to make or upload one."
107
+ : "The thumbnail isn't made yet. Let the Thumbnail stage finish, then open Prepare upload again.");
108
+ else if (thumbnails.length < count)
109
+ missing.push(`Only ${String(thumbnails.length)} of the ${String(count)} thumbnails are made. Regenerate the missing ones in the project's Images section.`);
110
+ if (playlist === null)
111
+ missing.push("No playlist is set for this project's channel, so the playlist step is left to you. Set one in Settings → YouTube Studio → Playlist.");
112
+ const items = [
113
+ {
114
+ kind: "video",
115
+ video: video === undefined ? null : file(video),
116
+ title: project.title.slice(0, studioTitleMax),
117
+ description: description ?? "",
118
+ tags: tagsOf(tagsFile),
119
+ thumbnails: thumbnails.map(file),
120
+ audience: studioAudience,
121
+ alteredContent: disclosure("video"),
122
+ playlist,
123
+ ...(edited?.chapterNotice === undefined ? {} : { chapterNotice: edited.chapterNotice }),
124
+ },
125
+ ];
126
+ const clips = clipsOf(text(outputs.find((output) => output.role === "shorts")));
127
+ const shortVideos = outputs.filter((output) => output.role === "short_video");
128
+ for (const clip of clips) {
129
+ // The clip's own render: the one cut from the same sentences, else the newest for its
130
+ // number (a list written before the sentences were kept).
131
+ const mine = shortVideos.filter((output) => output.meta.short === clip.number);
132
+ const exact = mine.find((output) => clip.first !== undefined &&
133
+ output.meta.sentences?.[0] === clip.first &&
134
+ output.meta.sentences?.[1] === clip.last);
135
+ const render = exact ?? mine.at(-1);
136
+ if (render === undefined)
137
+ missing.push(`Short ${String(clip.number)} isn't rendered yet. Let the Video stage finish, then open Prepare upload again.`);
138
+ items.push({
139
+ kind: "short",
140
+ short: clip.number,
141
+ video: render === undefined ? null : file(render),
142
+ title: clip.title.slice(0, studioTitleMax),
143
+ description: [
144
+ clip.description,
145
+ fullVideoLine(config.shorts?.fullVideoLink),
146
+ "",
147
+ clip.hashtags.join(" "),
148
+ ].join("\n"),
149
+ // The hashtags without their #, as search tags.
150
+ tags: clip.hashtags.map((tag) => tag.replace(/^#+/, "").trim()).filter((tag) => tag !== ""),
151
+ // Studio shows a frame of a short; a custom thumbnail isn't offered in its upload dialog.
152
+ thumbnails: [],
153
+ audience: studioAudience,
154
+ alteredContent: disclosure("short"),
155
+ playlist,
156
+ });
157
+ }
158
+ return {
159
+ ok: true,
160
+ pack: {
161
+ projectId,
162
+ projectTitle: project.title,
163
+ items,
164
+ missing,
165
+ ...(uploadedClips > 0 ? { footage: { clips: uploadedClips, real: realFootage } } : {}),
166
+ },
167
+ };
168
+ }
169
+ // The saved voices marked "Imitates a real person" that narrate the project: its one narration
170
+ // voice, or every speaker's of a multi-voice run. None when the narration isn't an AI voice.
171
+ function realPersonVoicesOf(deps, config) {
172
+ if (config.sources.audio !== "generate")
173
+ return [];
174
+ const narrating = usesVoices(config)
175
+ ? (config.voices?.speakers ?? []).map((speaker) => speaker.voice)
176
+ : config.audio === undefined
177
+ ? []
178
+ : [config.audio];
179
+ const flagged = realPersonVoiceIds(deps.db);
180
+ const names = listVoices(deps.db)
181
+ .filter((voice) => flagged.has(voice.id) &&
182
+ narrating.some((one) => one.provider === voice.provider && one.voice === voice.voiceId))
183
+ .map((voice) => voice.name);
184
+ return [...new Set(names)];
185
+ }
186
+ // The item the person chose: the long video, or short `short`.
187
+ export function packItem(pack, short) {
188
+ return pack.items.find((item) => short === undefined ? item.kind === "video" : item.kind === "short" && item.short === short);
189
+ }
190
+ function tagsOf(line) {
191
+ return (line ?? "")
192
+ .split(",")
193
+ .map((tag) => tag.trim())
194
+ .filter((tag) => tag !== "");
195
+ }
196
+ function clipsOf(text) {
197
+ if (text === undefined)
198
+ return [];
199
+ try {
200
+ const parsed = clipsSchema.safeParse(JSON.parse(text));
201
+ return parsed.success ? parsed.data.shorts : [];
202
+ }
203
+ catch {
204
+ return [];
205
+ }
206
+ }
207
+ function available(deps, projectId, output) {
208
+ return (statSync(outputPath(deps.paths, projectId, output.path), {
209
+ throwIfNoEntry: false,
210
+ })?.isFile() === true);
211
+ }
@@ -0,0 +1,69 @@
1
+ import { createHash } from "node:crypto";
2
+ import { z } from "zod";
3
+ import { readSetting, writeSetting } from "../settings/repo.js";
4
+ import { studioFillQueuePrefix, studioPairing } from "./settings.js";
5
+ // What Fill in YouTube Studio chose, waiting for the extension: one entry per video or short,
6
+ // oldest first. Each new upload dialog in Studio is filled from the first entry, which then
7
+ // leaves the queue, so preparing several shorts in a row fills them one upload at a time.
8
+ // Kept in the settings table under the current pairing (`studio.fillQueue.<token hash>`), so a
9
+ // restart keeps it and a new pairing token starts with nothing waiting. An entry older than
10
+ // `fillQueueMs` is dropped as stale.
11
+ export const fillQueueMs = 24 * 60 * 60 * 1000;
12
+ // More would be a list nobody works through; the oldest leave first.
13
+ export const fillQueueMax = 50;
14
+ const entrySchema = z.object({
15
+ projectId: z.string().min(1).max(64),
16
+ short: z.number().int().min(1).max(99).nullable(),
17
+ at: z.string(),
18
+ });
19
+ function queueKey(db) {
20
+ const hash = createHash("sha256").update(studioPairing(db).token).digest("hex").slice(0, 16);
21
+ return `${studioFillQueuePrefix}${hash}`;
22
+ }
23
+ function write(db, key, entries) {
24
+ if (entries.length === 0)
25
+ db.prepare("DELETE FROM settings WHERE key = ?").run(key);
26
+ else
27
+ writeSetting(db, key, JSON.stringify(entries));
28
+ }
29
+ // The waiting entries, oldest first, without the stale ones.
30
+ export function readFillQueue(db, now) {
31
+ const stored = readSetting(db, queueKey(db));
32
+ if (stored === undefined)
33
+ return [];
34
+ let raw;
35
+ try {
36
+ raw = JSON.parse(stored);
37
+ }
38
+ catch {
39
+ return [];
40
+ }
41
+ if (!Array.isArray(raw))
42
+ return [];
43
+ return raw.flatMap((one) => {
44
+ const parsed = entrySchema.safeParse(one);
45
+ if (!parsed.success)
46
+ return [];
47
+ const age = now.getTime() - Date.parse(parsed.data.at);
48
+ return Number.isFinite(age) && age <= fillQueueMs ? [parsed.data] : [];
49
+ });
50
+ }
51
+ const same = (entry, projectId, short) => entry.projectId === projectId && entry.short === short;
52
+ // Adds an item at the end; one already waiting keeps its place.
53
+ export function enqueueFill(db, projectId, short, now) {
54
+ const key = queueKey(db);
55
+ const current = readFillQueue(db, now);
56
+ if (current.some((entry) => same(entry, projectId, short))) {
57
+ write(db, key, current);
58
+ return current;
59
+ }
60
+ const next = [...current, { projectId, short, at: now.toISOString() }].slice(-fillQueueMax);
61
+ write(db, key, next);
62
+ return next;
63
+ }
64
+ // Takes an item out: filled by the extension, or removed by the person.
65
+ export function removeFill(db, projectId, short, now) {
66
+ const next = readFillQueue(db, now).filter((entry) => !same(entry, projectId, short));
67
+ write(db, queueKey(db), next);
68
+ return next;
69
+ }
@@ -0,0 +1,170 @@
1
+ import { randomBytes, timingSafeEqual } from "node:crypto";
2
+ import { z } from "zod";
3
+ import { readSetting, writeSetting } from "../settings/repo.js";
4
+ import { studioPlaylistMax } from "./model.js";
5
+ // Two rows of the key/value `settings` table. The playlist travels with a backup; the
6
+ // pairing stays on this machine (`slices/storage/portable.ts`): its token is what lets the
7
+ // browser extension read a project's upload pack.
8
+ export const studioPlaylistKey = "studio.playlist";
9
+ export const studioPairingKey = "studio.pairing";
10
+ const pairingSchema = z.object({
11
+ token: z.string().min(20),
12
+ origin: z.string().nullable(),
13
+ pairedAt: z.string().nullable(),
14
+ });
15
+ // A channel's own playlist is a row of its own, `studio.playlist.<channelId>`; a channel
16
+ // without one uses the default above.
17
+ export const studioChannelPlaylistPrefix = `${studioPlaylistKey}.`;
18
+ function playlistKey(channelId) {
19
+ return channelId === undefined ? studioPlaylistKey : `${studioChannelPlaylistPrefix}${channelId}`;
20
+ }
21
+ // The playlist an upload pack names: the channel's own, else the default. Without a channel,
22
+ // the default alone.
23
+ export function readStudioPlaylist(db, channelId) {
24
+ if (channelId !== undefined) {
25
+ const own = storedPlaylist(db, playlistKey(channelId));
26
+ if (own !== null)
27
+ return own;
28
+ }
29
+ return storedPlaylist(db, studioPlaylistKey);
30
+ }
31
+ // Every channel's own playlist, by channel id.
32
+ export function readChannelPlaylists(db) {
33
+ const prefix = studioChannelPlaylistPrefix;
34
+ const rows = db
35
+ .prepare("SELECT key FROM settings WHERE substr(key, 1, ?) = ? ORDER BY key")
36
+ .all(prefix.length, prefix);
37
+ const out = {};
38
+ for (const { key } of rows) {
39
+ const value = storedPlaylist(db, key);
40
+ if (value !== null)
41
+ out[key.slice(prefix.length)] = value;
42
+ }
43
+ return out;
44
+ }
45
+ function storedPlaylist(db, key) {
46
+ const stored = readSetting(db, key);
47
+ if (stored === undefined)
48
+ return null;
49
+ try {
50
+ const value = JSON.parse(stored);
51
+ return typeof value === "string" && value.trim() !== "" ? value : null;
52
+ }
53
+ catch {
54
+ return null;
55
+ }
56
+ }
57
+ export function studioPlaylistProblem(raw) {
58
+ return raw.trim().length > studioPlaylistMax
59
+ ? `The playlist name is longer than YouTube allows (${String(studioPlaylistMax)} characters). Shorten it in Settings → YouTube Studio.`
60
+ : undefined;
61
+ }
62
+ // Saves the default playlist, or a channel's own; empty clears it (the channel then uses the
63
+ // default again).
64
+ export function saveStudioPlaylist(db, raw, channelId) {
65
+ const value = raw.trim();
66
+ const key = playlistKey(channelId);
67
+ if (value === "") {
68
+ db.prepare("DELETE FROM settings WHERE key = ?").run(key);
69
+ return null;
70
+ }
71
+ writeSetting(db, key, JSON.stringify(value));
72
+ return value;
73
+ }
74
+ // The projects whose uploaded clips are real footage (filmed, not generated): ticked in the
75
+ // project's Prepare upload, under AI use. One row holding their ids; YouTube's second AI use
76
+ // case is footage of a real event or place altered by AI (`disclosure.ts`).
77
+ export const studioRealFootageKey = "studio.realFootage";
78
+ function realFootageIds(db) {
79
+ const stored = readSetting(db, studioRealFootageKey);
80
+ if (stored === undefined)
81
+ return [];
82
+ try {
83
+ const value = JSON.parse(stored);
84
+ return Array.isArray(value)
85
+ ? value.filter((one) => typeof one === "string")
86
+ : [];
87
+ }
88
+ catch {
89
+ return [];
90
+ }
91
+ }
92
+ export function readRealFootage(db, projectId) {
93
+ return realFootageIds(db).includes(projectId);
94
+ }
95
+ export function saveRealFootage(db, projectId, on) {
96
+ const others = realFootageIds(db).filter((one) => one !== projectId);
97
+ const next = on ? [...others, projectId].sort() : others;
98
+ if (next.length === 0)
99
+ db.prepare("DELETE FROM settings WHERE key = ?").run(studioRealFootageKey);
100
+ else
101
+ writeSetting(db, studioRealFootageKey, JSON.stringify(next));
102
+ }
103
+ // The pairing token, made on first read. One per install: a new one (Settings → YouTube
104
+ // Studio → New pairing token) forgets the extension that used the old one.
105
+ export function studioPairing(db) {
106
+ const stored = readSetting(db, studioPairingKey);
107
+ if (stored !== undefined) {
108
+ try {
109
+ const parsed = pairingSchema.safeParse(JSON.parse(stored));
110
+ if (parsed.success)
111
+ return parsed.data;
112
+ }
113
+ catch {
114
+ // A damaged row is replaced below.
115
+ }
116
+ }
117
+ return resetStudioPairing(db);
118
+ }
119
+ // What is waiting to be filled in Studio is kept per pairing (`queue.ts`): one row,
120
+ // `studio.fillQueue.<token hash>`, so a new token starts with nothing waiting.
121
+ export const studioFillQueuePrefix = "studio.fillQueue.";
122
+ export function resetStudioPairing(db) {
123
+ const pairing = {
124
+ token: randomBytes(24).toString("base64url"),
125
+ origin: null,
126
+ pairedAt: null,
127
+ };
128
+ db.prepare("DELETE FROM settings WHERE substr(key, 1, ?) = ?").run(studioFillQueuePrefix.length, studioFillQueuePrefix);
129
+ writeSetting(db, studioPairingKey, JSON.stringify(pairing));
130
+ return pairing;
131
+ }
132
+ // A browser extension's own origin: Chrome's and Firefox's. Web pages never match.
133
+ export function isExtensionOrigin(origin) {
134
+ return (origin !== undefined && /^(chrome-extension|moz-extension):\/\/[a-z0-9-]{1,64}$/.test(origin));
135
+ }
136
+ export function tokenMatches(pairing, given) {
137
+ if (given === undefined)
138
+ return false;
139
+ const a = Buffer.from(pairing.token);
140
+ const b = Buffer.from(given);
141
+ return a.length === b.length && timingSafeEqual(a, b);
142
+ }
143
+ // The token from an `Authorization: Bearer <token>` header.
144
+ export function bearerToken(header) {
145
+ const match = /^Bearer\s+(\S+)$/.exec(header ?? "");
146
+ return match?.[1];
147
+ }
148
+ // The extension pairs by sending the token from its own origin; that origin is then the only
149
+ // one the app answers across origins, until a new token is made.
150
+ export function pairStudioExtension(db, token, origin, now) {
151
+ const pairing = studioPairing(db);
152
+ if (!tokenMatches(pairing, token))
153
+ return { ok: false, reason: "bad-token" };
154
+ if (!isExtensionOrigin(origin))
155
+ return { ok: false, reason: "not-an-extension" };
156
+ const next = { ...pairing, origin, pairedAt: now.toISOString() };
157
+ writeSetting(db, studioPairingKey, JSON.stringify(next));
158
+ return { ok: true, pairing: next };
159
+ }
160
+ // Whether a request may read upload packs: the right token, and, when the browser names its
161
+ // origin, the paired extension's. A request with no Origin (not from a web page at all) is
162
+ // judged on the token alone.
163
+ export function studioRequestAllowed(db, token, origin) {
164
+ const pairing = studioPairing(db);
165
+ if (!tokenMatches(pairing, token))
166
+ return false;
167
+ if (origin === undefined)
168
+ return true;
169
+ return pairing.origin !== null && origin === pairing.origin;
170
+ }
@@ -0,0 +1,61 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFileSync, statSync } from "node:fs";
3
+ import { imageBlob } from "../channels/repo.js";
4
+ import { outputPath, stagingPath } from "../storage/layout.js";
5
+ import { outputById, stagedFileById } from "../storage/repo.js";
6
+ // ceiling: a provider's image or an upload is a few MB; anything this large is not a picture
7
+ // worth reading into memory for a 270-pixel preview.
8
+ const maxBytes = 40 * 1024 * 1024;
9
+ // Roles whose file is a single still picture.
10
+ const pictureRoles = new Set(["reference", "image", "thumbnail", "short_image"]);
11
+ export function previewPictures(deps) {
12
+ return (image) => {
13
+ try {
14
+ const bytes = read(deps, image);
15
+ if (bytes === undefined)
16
+ return undefined;
17
+ const extension = pictureExtension(bytes);
18
+ if (extension === undefined)
19
+ return undefined;
20
+ return { bytes, extension, sha256: createHash("sha256").update(bytes).digest("hex") };
21
+ }
22
+ catch {
23
+ return undefined;
24
+ }
25
+ };
26
+ }
27
+ function read(deps, image) {
28
+ switch (image.kind) {
29
+ case "picture":
30
+ return imageBlob(deps.db, image.sha256)?.bytes;
31
+ case "upload": {
32
+ const staged = stagedFileById(deps.db, image.stagedFileId);
33
+ if (staged === undefined || staged.state !== "staged")
34
+ return undefined;
35
+ return readSmall(stagingPath(deps.paths, staged.id));
36
+ }
37
+ case "output": {
38
+ const output = outputById(deps.db, image.outputId);
39
+ if (output === undefined || !pictureRoles.has(output.role))
40
+ return undefined;
41
+ return readSmall(outputPath(deps.paths, output.projectId, output.path));
42
+ }
43
+ }
44
+ }
45
+ function readSmall(path) {
46
+ const size = statSync(path).size;
47
+ if (size === 0 || size > maxBytes)
48
+ return undefined;
49
+ return new Uint8Array(readFileSync(path));
50
+ }
51
+ export function pictureExtension(bytes) {
52
+ const starts = (...values) => values.every((value, at) => bytes[at] === value);
53
+ if (starts(0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a))
54
+ return ".png";
55
+ if (starts(0xff, 0xd8, 0xff))
56
+ return ".jpg";
57
+ const ascii = (from, to) => String.fromCharCode(...bytes.subarray(from, to));
58
+ if (ascii(0, 4) === "RIFF" && ascii(8, 12) === "WEBP")
59
+ return ".webp";
60
+ return undefined;
61
+ }
@@ -0,0 +1,33 @@
1
+ import { join } from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ // The sample a preview is made of: six seconds of the bundled sample project's narration
4
+ // ("The Library of Alexandria", `assets/sample/sample-project.tar`) with the word timing its
5
+ // alignment found, and three of its images in each shape. Taken once, offline, from the body
6
+ // narration at 3.4 s (the video's 4.4 s, after its one second of edge silence); the words are
7
+ // on the clip's own timeline. Nothing here is generated at preview time.
8
+ export const sampleNarrationSeconds = 6;
9
+ export const sampleNarrationWords = [
10
+ { text: "Around", start: 0.5, end: 0.88 },
11
+ { text: "three", start: 1.14, end: 1.36 },
12
+ { text: "hundred", start: 1.46, end: 1.78 },
13
+ { text: "years", start: 1.88, end: 2.26 },
14
+ { text: "before", start: 2.4, end: 2.78 },
15
+ { text: "our", start: 2.92, end: 3.02 },
16
+ { text: "era,", start: 3.22, end: 3.4 },
17
+ { text: "on", start: 4.18, end: 4.22 },
18
+ { text: "the", start: 4.3, end: 4.36 },
19
+ { text: "coast", start: 4.44, end: 4.72 },
20
+ { text: "of", start: 4.84, end: 4.88 },
21
+ { text: "Egypt,", start: 5.06, end: 5.4 },
22
+ ];
23
+ export const sampleNarrationText = sampleNarrationWords.map((word) => word.text).join(" ");
24
+ // The sample project's title, the headline a Shorts preview shows when none is given.
25
+ export const sampleShortTitle = "The Library of Alexandria";
26
+ export function bundledSampleAssets(dir = fileURLToPath(new URL("../../assets/style-preview/", import.meta.url))) {
27
+ const at = (name) => join(dir, name);
28
+ return {
29
+ narration: at("narration.mp3"),
30
+ wide: [1, 2, 3].map((n) => at(`wide-${String(n)}.jpg`)),
31
+ tall: [1, 2, 3].map((n) => at(`tall-${String(n)}.jpg`)),
32
+ };
33
+ }