@gentbajko/slopify 2.4.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (469) hide show
  1. package/LICENSE +201 -21
  2. package/NOTICE +12 -0
  3. package/README.md +21 -40
  4. package/SUBTITLES.md +1 -1
  5. package/dist/adapter-registry.js +11 -1
  6. package/dist/adapters/alignment/cache.js +4 -1
  7. package/dist/adapters/alignment/ctc.js +25 -23
  8. package/dist/adapters/alignment/index.js +37 -6
  9. package/dist/adapters/alignment/multilingual.js +267 -0
  10. package/dist/adapters/alignment/protocol.js +11 -0
  11. package/dist/adapters/alignment/quality.js +4 -3
  12. package/dist/adapters/alignment/sentences.js +95 -0
  13. package/dist/adapters/alignment/spec.js +24 -0
  14. package/dist/adapters/alignment/spell.js +769 -0
  15. package/dist/adapters/alignment/text.js +56 -9
  16. package/dist/adapters/alignment/window.js +34 -30
  17. package/dist/adapters/alignment/worker.js +23 -21
  18. package/dist/adapters/explain.js +11 -11
  19. package/dist/adapters/host-cli/index.js +55 -5
  20. package/dist/adapters/host-cli/transport.js +13 -6
  21. package/dist/adapters/image/bytes.js +2 -2
  22. package/dist/adapters/image/codex-output.js +60 -18
  23. package/dist/adapters/image/codex.js +166 -25
  24. package/dist/adapters/image/fal.js +31 -3
  25. package/dist/adapters/image/google.js +17 -2
  26. package/dist/adapters/image/openai.js +45 -16
  27. package/dist/adapters/image/reference.js +41 -0
  28. package/dist/adapters/image/replicate.js +14 -2
  29. package/dist/adapters/image/video.js +2 -2
  30. package/dist/adapters/key-probes.js +36 -0
  31. package/dist/adapters/llm/claude-code.js +168 -32
  32. package/dist/adapters/llm/cli-login-error.js +1 -1
  33. package/dist/adapters/llm/codex-limits.js +157 -0
  34. package/dist/adapters/llm/codex-models.js +2 -1
  35. package/dist/adapters/llm/codex-server-models.js +2 -1
  36. package/dist/adapters/llm/codex.js +99 -12
  37. package/dist/adapters/llm/document-workspace.js +2 -2
  38. package/dist/adapters/llm/gemini-workspace.js +1 -1
  39. package/dist/adapters/llm/gemini.js +16 -3
  40. package/dist/adapters/llm/image-workspace.js +58 -0
  41. package/dist/adapters/llm/openrouter.js +11 -3
  42. package/dist/adapters/llm/run-cli.js +26 -9
  43. package/dist/adapters/tts/cartesia.js +10 -0
  44. package/dist/adapters/tts/elevenlabs.js +31 -9
  45. package/dist/adapters/tts/inworld-async.js +9 -5
  46. package/dist/adapters/tts/inworld.js +17 -3
  47. package/dist/adapters/tts/openai.js +3 -0
  48. package/dist/adapters/tts/voice-languages.js +80 -0
  49. package/dist/assets/fonts/noto/NotoSans-Regular.ttf +0 -0
  50. package/dist/assets/fonts/noto/NotoSansArabic-Regular.ttf +0 -0
  51. package/dist/assets/fonts/noto/NotoSansDevanagari-Regular.ttf +0 -0
  52. package/dist/assets/fonts/noto/NotoSansHebrew-Regular.ttf +0 -0
  53. package/dist/assets/fonts/noto/NotoSansThai-Regular.ttf +0 -0
  54. package/dist/assets/fonts/noto/OFL.txt +93 -0
  55. package/dist/assets/fonts/noto/SOURCE.txt +18 -0
  56. package/dist/assets/models.yaml +7 -0
  57. package/dist/assets/sample/sample-audiobook.tar +0 -0
  58. package/dist/assets/sample/sample-podcast.tar +0 -0
  59. package/dist/assets/sample/sample-project.tar +0 -0
  60. package/dist/catalog/merge.js +132 -0
  61. package/dist/catalog/registry.js +33 -12
  62. package/dist/catalog/runtime-models.js +1 -1
  63. package/dist/catalog/schema.js +12 -1
  64. package/dist/catalog/store.js +147 -8
  65. package/dist/catalog/validate.js +17 -2
  66. package/dist/compose.yaml +71 -0
  67. package/dist/edge/autostart/docker-record.js +32 -0
  68. package/dist/edge/autostart/docker.js +50 -0
  69. package/dist/edge/autostart/index.js +78 -0
  70. package/dist/edge/autostart/launcher.js +176 -0
  71. package/dist/edge/autostart/model.js +3 -0
  72. package/dist/edge/autostart/native.js +210 -0
  73. package/dist/edge/autostart/prompt.js +66 -0
  74. package/dist/edge/autostart/service.js +81 -0
  75. package/dist/edge/cli.js +57 -51
  76. package/dist/edge/docker-install/activation.js +56 -0
  77. package/dist/edge/docker-install/apply.js +458 -0
  78. package/dist/edge/docker-install/engine.js +362 -0
  79. package/dist/edge/docker-install/run.js +154 -0
  80. package/dist/edge/docker-install/state.js +146 -0
  81. package/dist/edge/{docker-projects → docker-install}/volume.js +1 -5
  82. package/dist/edge/docker.js +14 -22
  83. package/dist/edge/events/hub.js +11 -0
  84. package/dist/edge/host-cli.js +1 -1
  85. package/dist/edge/http/actions.js +32 -10
  86. package/dist/edge/http/app.js +60 -2
  87. package/dist/edge/http/auditions.js +70 -0
  88. package/dist/edge/http/autostart.js +37 -0
  89. package/dist/edge/http/backups.js +40 -0
  90. package/dist/edge/http/channel-memory.js +92 -0
  91. package/dist/edge/http/channels.js +128 -0
  92. package/dist/edge/http/entries.js +22 -3
  93. package/dist/edge/http/files.js +1 -1
  94. package/dist/edge/http/folder-location.js +1 -1
  95. package/dist/edge/http/home.js +39 -0
  96. package/dist/edge/http/host-cli.js +45 -6
  97. package/dist/edge/http/narration-peaks.js +76 -0
  98. package/dist/edge/http/onboarding.js +152 -0
  99. package/dist/edge/http/open-folder.js +1 -1
  100. package/dist/edge/http/patch-notes.js +45 -0
  101. package/dist/edge/http/planning.js +4 -2
  102. package/dist/edge/http/problem.js +1 -0
  103. package/dist/edge/http/project-create.js +87 -0
  104. package/dist/edge/http/projects.js +31 -99
  105. package/dist/edge/http/prompts.js +40 -3
  106. package/dist/edge/http/pronunciations.js +23 -4
  107. package/dist/edge/http/providers.js +86 -0
  108. package/dist/edge/http/reviews.js +121 -0
  109. package/dist/edge/http/revision-files.js +1 -1
  110. package/dist/edge/http/revisions.js +1 -1
  111. package/dist/edge/http/run-cost.js +25 -0
  112. package/dist/edge/http/schedules.js +111 -6
  113. package/dist/edge/http/settings.js +128 -6
  114. package/dist/edge/http/storage-files.js +50 -0
  115. package/dist/edge/http/storage.js +29 -2
  116. package/dist/edge/http/studio.js +207 -0
  117. package/dist/edge/http/style-preview.js +100 -0
  118. package/dist/edge/http/subtitles.js +1 -1
  119. package/dist/edge/http/trash.js +69 -0
  120. package/dist/edge/http/update.js +24 -3
  121. package/dist/edge/http/whats-new.js +15 -0
  122. package/dist/edge/http/youtube-edits.js +83 -0
  123. package/dist/edge/native-update.js +62 -0
  124. package/dist/host-cli/open-folder.js +10 -10
  125. package/dist/host-cli/runtime.js +12 -3
  126. package/dist/kernel/db/migrations/0022-drop-duplicate-held-work.sql +56 -0
  127. package/dist/kernel/db/migrations/0024-reference-attachments.sql +18 -0
  128. package/dist/kernel/db/migrations/0025-automatic-reviews.sql +40 -0
  129. package/dist/kernel/db/migrations/0026-schedule-topic-generation.sql +23 -0
  130. package/dist/kernel/db/migrations/0027-run-cost.sql +58 -0
  131. package/dist/kernel/db/migrations/0030-automatic-retries.sql +18 -0
  132. package/dist/kernel/db/migrations/0031-prompt-history-and-description-edits.sql +33 -0
  133. package/dist/kernel/db/migrations/0032-channels.sql +65 -0
  134. package/dist/kernel/db/migrations/0034-script-prompts.sql +23 -0
  135. package/dist/kernel/db/migrations/0035-narration-aliases.sql +13 -0
  136. package/dist/kernel/db/migrations/0037-project-uploads.sql +8 -0
  137. package/dist/kernel/db/migrations/0038-voice-languages.sql +7 -0
  138. package/dist/kernel/db/migrations/0039-channel-essentials.sql +64 -0
  139. package/dist/kernel/db/migrations/0040-standalone-usage.sql +35 -0
  140. package/dist/kernel/paths.js +27 -6
  141. package/dist/kernel/pipeline.js +4 -1
  142. package/dist/kernel/ports/host-cli.js +139 -16
  143. package/dist/kernel/ports/image.js +3 -1
  144. package/dist/kernel/ports/key-probe.js +1 -0
  145. package/dist/kernel/ports/languages.js +157 -0
  146. package/dist/kernel/ports/llm.js +3 -1
  147. package/dist/kernel/ports/model.js +5 -2
  148. package/dist/kernel/ports/narration-aliases.js +60 -0
  149. package/dist/kernel/ports/plan-limits.js +36 -0
  150. package/dist/kernel/runner/attempt.js +18 -9
  151. package/dist/kernel/runner/audition.js +36 -0
  152. package/dist/kernel/runner/graph.js +24 -5
  153. package/dist/kernel/runner/index.js +35 -5
  154. package/dist/kernel/runner/meter.js +1 -0
  155. package/dist/kernel/runner/providers.js +164 -9
  156. package/dist/kernel/runner/retry-policy.js +43 -0
  157. package/dist/kernel/runner/standalone.js +134 -0
  158. package/dist/kernel/runner/work-authority.js +28 -2
  159. package/dist/main.js +321 -20
  160. package/dist/patch-notes/0.4.2-to-1.0.md +93 -0
  161. package/dist/patch-notes/1.0-to-2.0.md +74 -0
  162. package/dist/patch-notes/2.0-to-3.0.md +257 -0
  163. package/dist/patch-notes/3.0.0.md +70 -0
  164. package/dist/patch-notes/index.json +26 -0
  165. package/dist/slices/admission/model.js +35 -0
  166. package/dist/slices/admission/repo.js +24 -4
  167. package/dist/slices/admission/rules.js +126 -1
  168. package/dist/slices/admission/schema.js +68 -1
  169. package/dist/slices/admission/short-mode.js +47 -0
  170. package/dist/slices/admission/start.js +90 -16
  171. package/dist/slices/article/continuation.js +2 -2
  172. package/dist/slices/article/provided-entries.js +2 -2
  173. package/dist/slices/article/run.js +6 -2
  174. package/dist/slices/article/segments.js +3 -3
  175. package/dist/slices/backups/files.js +135 -0
  176. package/dist/slices/backups/folder.js +43 -0
  177. package/dist/slices/backups/model.js +65 -0
  178. package/dist/slices/backups/repo.js +28 -0
  179. package/dist/slices/backups/schedule.js +60 -0
  180. package/dist/slices/backups/service.js +311 -0
  181. package/dist/slices/batch/index.js +9 -2
  182. package/dist/slices/cancel/index.js +1 -1
  183. package/dist/slices/channels/cast-images.js +110 -0
  184. package/dist/slices/channels/cast-match.js +32 -0
  185. package/dist/slices/channels/images.js +19 -0
  186. package/dist/slices/channels/model.js +5 -0
  187. package/dist/slices/channels/rebrand.js +100 -0
  188. package/dist/slices/channels/repo.js +218 -0
  189. package/dist/slices/channels/runs.js +129 -0
  190. package/dist/slices/channels/schema.js +129 -0
  191. package/dist/slices/channels/service.js +180 -0
  192. package/dist/slices/channels/studio-csv.js +110 -0
  193. package/dist/slices/channels/videos.js +120 -0
  194. package/dist/slices/checkpoints/recovery.js +10 -0
  195. package/dist/slices/control/index.js +2 -1
  196. package/dist/slices/document/fonts.js +1 -1
  197. package/dist/slices/document/render.js +1 -1
  198. package/dist/slices/episodes/related.js +90 -0
  199. package/dist/slices/episodes/repo.js +60 -0
  200. package/dist/slices/episodes/service.js +58 -0
  201. package/dist/slices/episodes/summarize.js +170 -0
  202. package/dist/slices/estimate/index.js +113 -24
  203. package/dist/slices/estimate/requests.js +60 -2
  204. package/dist/slices/fixes/rules.js +73 -0
  205. package/dist/slices/fonts/coverage.js +82 -0
  206. package/dist/slices/fonts/sfnt.js +74 -0
  207. package/dist/slices/images/run.js +8 -5
  208. package/dist/slices/images/scale.js +84 -0
  209. package/dist/slices/library/diff.js +122 -0
  210. package/dist/slices/library/history.js +100 -0
  211. package/dist/slices/library/model.js +3 -0
  212. package/dist/slices/library/photorealistic.js +49 -0
  213. package/dist/slices/library/repo.js +19 -14
  214. package/dist/slices/library/save.js +62 -12
  215. package/dist/slices/library/slots.js +15 -3
  216. package/dist/slices/library/snapshot.js +8 -2
  217. package/dist/slices/library/used-by.js +153 -0
  218. package/dist/slices/loudness/level-pieces.js +41 -0
  219. package/dist/slices/loudness/loudnorm.js +164 -0
  220. package/dist/slices/loudness/model.js +153 -0
  221. package/dist/slices/model-upkeep/model.js +10 -0
  222. package/dist/slices/model-upkeep/switch.js +217 -0
  223. package/dist/slices/model-upkeep/usage.js +183 -0
  224. package/dist/slices/narration/aliases-library.js +31 -0
  225. package/dist/slices/narration/aliases-schema.js +53 -0
  226. package/dist/slices/narration/aliases.js +49 -0
  227. package/dist/slices/narration/blocks.js +352 -0
  228. package/dist/slices/narration/chunk.js +19 -7
  229. package/dist/slices/narration/concat.js +2 -2
  230. package/dist/slices/narration/describe.js +45 -0
  231. package/dist/slices/narration/pauses-model.js +63 -0
  232. package/dist/slices/narration/pauses.js +269 -0
  233. package/dist/slices/narration/peaks-model.js +1 -0
  234. package/dist/slices/narration/peaks.js +94 -0
  235. package/dist/slices/narration/preparation.js +21 -3
  236. package/dist/slices/narration/pronunciation-chunks.js +3 -3
  237. package/dist/slices/narration/pronunciation.js +72 -25
  238. package/dist/slices/narration/run.js +6 -6
  239. package/dist/slices/narration/shared-glossary.js +1 -1
  240. package/dist/slices/narration/steering.js +15 -4
  241. package/dist/slices/notifications/notifier.js +94 -0
  242. package/dist/slices/notifications/rules.js +102 -0
  243. package/dist/slices/notifications/send.js +40 -0
  244. package/dist/slices/notifications/settings.js +34 -0
  245. package/dist/slices/onboarding/copy.js +143 -0
  246. package/dist/slices/onboarding/first-run.js +46 -0
  247. package/dist/slices/onboarding/install.js +119 -0
  248. package/dist/slices/onboarding/model.js +64 -0
  249. package/dist/slices/onboarding/packs.js +491 -0
  250. package/dist/slices/onboarding/quick-short.js +129 -0
  251. package/dist/slices/onboarding/sample.js +132 -0
  252. package/dist/slices/onboarding/state.js +68 -0
  253. package/dist/slices/onboarding/template.js +73 -0
  254. package/dist/slices/patch-notes/library.js +73 -0
  255. package/dist/slices/patch-notes/seen.js +77 -0
  256. package/dist/slices/play-drafts/convert.js +160 -9
  257. package/dist/slices/play-drafts/readiness.js +3 -0
  258. package/dist/slices/play-drafts/repo.js +9 -1
  259. package/dist/slices/play-drafts/review-inputs.js +22 -3
  260. package/dist/slices/play-drafts/schema.js +109 -2
  261. package/dist/slices/play-drafts/service.js +10 -1
  262. package/dist/slices/play-drafts/start.js +43 -18
  263. package/dist/slices/play-drafts/uploads.js +2 -1
  264. package/dist/slices/project-templates/from-project.js +70 -4
  265. package/dist/slices/project-templates/one-off.js +26 -0
  266. package/dist/slices/project-templates/repo.js +2 -1
  267. package/dist/slices/project-templates/service.js +15 -4
  268. package/dist/slices/project-templates/setup.js +29 -2
  269. package/dist/slices/rebuild/admission-repo.js +1 -1
  270. package/dist/slices/rebuild/model.js +6 -0
  271. package/dist/slices/rebuild/preview-details.js +67 -2
  272. package/dist/slices/rebuild/preview-plan.js +13 -6
  273. package/dist/slices/rebuild/recipe-audio-parts.js +40 -7
  274. package/dist/slices/rebuild/recipe-audio.js +66 -12
  275. package/dist/slices/rebuild/recipe-build.js +15 -5
  276. package/dist/slices/rebuild/recipe-cast.js +18 -0
  277. package/dist/slices/rebuild/recipe-checkpoints.js +11 -9
  278. package/dist/slices/rebuild/recipe-describe.js +93 -0
  279. package/dist/slices/rebuild/recipe-edit.js +13 -1
  280. package/dist/slices/rebuild/recipe-exports.js +62 -8
  281. package/dist/slices/rebuild/recipe-input-schema.js +43 -1
  282. package/dist/slices/rebuild/recipe-legacy.js +12 -0
  283. package/dist/slices/rebuild/recipe-loudness.js +45 -0
  284. package/dist/slices/rebuild/recipe-model.js +25 -6
  285. package/dist/slices/rebuild/recipe-pauses.js +7 -0
  286. package/dist/slices/rebuild/recipe-preparation.js +3 -3
  287. package/dist/slices/rebuild/recipe-provider-choice.js +11 -1
  288. package/dist/slices/rebuild/recipe-reference.js +58 -0
  289. package/dist/slices/rebuild/recipe-reviews.js +117 -0
  290. package/dist/slices/rebuild/recipe-save.js +4 -1
  291. package/dist/slices/rebuild/recipe-shorts.js +32 -5
  292. package/dist/slices/rebuild/recipe-text.js +114 -19
  293. package/dist/slices/rebuild/recipe-validation.js +29 -3
  294. package/dist/slices/rebuild/recipe-visual.js +104 -29
  295. package/dist/slices/rebuild/recipe-voices.js +286 -0
  296. package/dist/slices/rebuild/recipe-work.js +77 -6
  297. package/dist/slices/rebuild/recipe-youtube.js +2 -0
  298. package/dist/slices/rebuild/recovery-conflict.js +7 -3
  299. package/dist/slices/rebuild/recovery-model.js +3 -0
  300. package/dist/slices/rebuild/recovery-selection.js +28 -2
  301. package/dist/slices/rebuild/recovery.js +35 -17
  302. package/dist/slices/rebuild/repo.js +3 -3
  303. package/dist/slices/rebuild/retry.js +18 -0
  304. package/dist/slices/rebuild/review-redo.js +110 -0
  305. package/dist/slices/rebuild/runtime-actions.js +14 -2
  306. package/dist/slices/rebuild/runtime-admission.js +4 -1
  307. package/dist/slices/rebuild/runtime-animate.js +4 -4
  308. package/dist/slices/rebuild/runtime-article.js +5 -5
  309. package/dist/slices/rebuild/runtime-document.js +4 -4
  310. package/dist/slices/rebuild/runtime-export-bed.js +43 -0
  311. package/dist/slices/rebuild/runtime-export-edit.js +70 -11
  312. package/dist/slices/rebuild/runtime-export-inputs.js +52 -10
  313. package/dist/slices/rebuild/runtime-export-short.js +85 -0
  314. package/dist/slices/rebuild/runtime-export.js +84 -19
  315. package/dist/slices/rebuild/runtime-figure-card.js +87 -0
  316. package/dist/slices/rebuild/runtime-image.js +69 -0
  317. package/dist/slices/rebuild/runtime-local.js +154 -27
  318. package/dist/slices/rebuild/runtime-materialize.js +8 -0
  319. package/dist/slices/rebuild/runtime-narration-reuse.js +2 -1
  320. package/dist/slices/rebuild/runtime-narration-text.js +7 -7
  321. package/dist/slices/rebuild/runtime-pictures.js +28 -0
  322. package/dist/slices/rebuild/runtime-piece-label.js +29 -0
  323. package/dist/slices/rebuild/runtime-provider.js +134 -19
  324. package/dist/slices/rebuild/runtime-publication.js +21 -5
  325. package/dist/slices/rebuild/runtime-review.js +237 -0
  326. package/dist/slices/rebuild/runtime-run.js +17 -11
  327. package/dist/slices/rebuild/runtime-shorts.js +48 -23
  328. package/dist/slices/rebuild/runtime-store.js +39 -10
  329. package/dist/slices/rebuild/runtime-subtitles.js +72 -19
  330. package/dist/slices/rebuild/runtime-voices.js +103 -0
  331. package/dist/slices/rebuild/runtime-youtube.js +7 -6
  332. package/dist/slices/rebuild/service-readiness.js +15 -1
  333. package/dist/slices/rebuild/soften.js +49 -0
  334. package/dist/slices/rebuild/transition-repo.js +10 -3
  335. package/dist/slices/research/run.js +4 -4
  336. package/dist/slices/research/synthesis.js +2 -2
  337. package/dist/slices/reviews/model.js +105 -0
  338. package/dist/slices/reviews/outcome.js +22 -0
  339. package/dist/slices/reviews/repo.js +106 -0
  340. package/dist/slices/reviews/rules.js +63 -0
  341. package/dist/slices/reviews/verdict.js +71 -0
  342. package/dist/slices/reviews/view.js +30 -0
  343. package/dist/slices/revisions/adopt-content.js +12 -2
  344. package/dist/slices/revisions/adopt.js +9 -2
  345. package/dist/slices/revisions/downloads.js +7 -1
  346. package/dist/slices/revisions/image-plan.js +183 -0
  347. package/dist/slices/revisions/mutation-assets.js +43 -9
  348. package/dist/slices/revisions/mutation-cues.js +71 -0
  349. package/dist/slices/revisions/mutation-prepare.js +8 -10
  350. package/dist/slices/revisions/mutation-request.js +2 -1
  351. package/dist/slices/revisions/mutations.js +97 -3
  352. package/dist/slices/revisions/schema.js +16 -2
  353. package/dist/slices/run-cost/limits.js +128 -0
  354. package/dist/slices/run-cost/meter.js +47 -0
  355. package/dist/slices/run-cost/panel.js +234 -0
  356. package/dist/slices/run-cost/pricing.js +92 -0
  357. package/dist/slices/run-cost/week.js +108 -0
  358. package/dist/slices/schedules/agenda.js +158 -0
  359. package/dist/slices/schedules/repo.js +58 -18
  360. package/dist/slices/schedules/scheduler.js +67 -16
  361. package/dist/slices/schedules/schema.js +138 -1
  362. package/dist/slices/schedules/service.js +48 -4
  363. package/dist/slices/schedules/similar.js +90 -0
  364. package/dist/slices/schedules/topic-list.js +163 -0
  365. package/dist/slices/schedules/topics.js +422 -0
  366. package/dist/slices/settings/first-run.js +102 -0
  367. package/dist/slices/settings/health.js +190 -0
  368. package/dist/slices/settings/key-guides.js +128 -0
  369. package/dist/slices/settings/key-test.js +85 -0
  370. package/dist/slices/settings/model.js +11 -1
  371. package/dist/slices/settings/playback.js +15 -0
  372. package/dist/slices/settings/repo.js +17 -3
  373. package/dist/slices/settings/voices.js +64 -4
  374. package/dist/slices/settings/whats-new.js +64 -0
  375. package/dist/slices/shorts/captions.js +9 -0
  376. package/dist/slices/shorts/pick.js +3 -3
  377. package/dist/slices/shorts/prompts.js +1 -1
  378. package/dist/slices/shorts/render.js +54 -5
  379. package/dist/slices/storage/asset-name.js +3 -0
  380. package/dist/slices/storage/backup-export.js +48 -9
  381. package/dist/slices/storage/backup-format.js +38 -1
  382. package/dist/slices/storage/backup-import.js +191 -19
  383. package/dist/slices/storage/delete-project.js +7 -4
  384. package/dist/slices/storage/documents.js +79 -0
  385. package/dist/slices/storage/files-location.js +531 -0
  386. package/dist/slices/storage/layout.js +20 -0
  387. package/dist/slices/storage/model.js +14 -0
  388. package/dist/slices/storage/portable.js +95 -12
  389. package/dist/slices/storage/reconcile.js +19 -2
  390. package/dist/slices/storage/repo.js +4 -1
  391. package/dist/slices/storage/schema.js +11 -0
  392. package/dist/slices/storage/trim.js +146 -0
  393. package/dist/slices/studio/disclosure.js +74 -0
  394. package/dist/slices/studio/model.js +28 -0
  395. package/dist/slices/studio/pack.js +209 -0
  396. package/dist/slices/studio/settings.js +133 -0
  397. package/dist/slices/style-preview/images.js +61 -0
  398. package/dist/slices/style-preview/render.js +161 -0
  399. package/dist/slices/style-preview/schema.js +45 -0
  400. package/dist/slices/style-preview/service.js +93 -0
  401. package/dist/slices/style-preview/settings.js +89 -0
  402. package/dist/slices/subtitles/captions.js +42 -9
  403. package/dist/slices/subtitles/model.js +22 -0
  404. package/dist/slices/subtitles/prepare.js +27 -16
  405. package/dist/slices/subtitles/transcript.js +1 -1
  406. package/dist/slices/thumbnail/run.js +7 -7
  407. package/dist/slices/trash/model.js +25 -0
  408. package/dist/slices/trash/service.js +273 -0
  409. package/dist/slices/uploads/repo.js +20 -0
  410. package/dist/slices/video/ambient-bed-schema.js +15 -0
  411. package/dist/slices/video/ambient-bed.js +95 -0
  412. package/dist/slices/video/ambient-mix.js +72 -0
  413. package/dist/slices/video/audio-inputs.js +2 -2
  414. package/dist/slices/video/cards.js +5 -2
  415. package/dist/slices/video/edit-list.js +23 -2
  416. package/dist/slices/video/edit-settings.js +13 -3
  417. package/dist/slices/video/ffmpeg.js +63 -9
  418. package/dist/slices/video/figure-card.js +492 -0
  419. package/dist/slices/video/figure-spans.js +51 -0
  420. package/dist/slices/video/plan.js +125 -13
  421. package/dist/slices/video/run.js +14 -12
  422. package/dist/slices/video/slideshow.js +39 -4
  423. package/dist/slices/voices/attribution.js +69 -0
  424. package/dist/slices/voices/audio-files.js +82 -0
  425. package/dist/slices/voices/audition.js +25 -0
  426. package/dist/slices/voices/cast.js +56 -0
  427. package/dist/slices/voices/grouping.js +49 -0
  428. package/dist/slices/voices/join.js +35 -0
  429. package/dist/slices/voices/languages.js +25 -0
  430. package/dist/slices/voices/model.js +179 -0
  431. package/dist/slices/voices/palette.js +26 -0
  432. package/dist/slices/voices/panel.js +91 -0
  433. package/dist/slices/voices/portraits.js +55 -0
  434. package/dist/slices/voices/script.js +136 -0
  435. package/dist/slices/voices/timing.js +58 -0
  436. package/dist/slices/youtube/answer.js +1 -1
  437. package/dist/slices/youtube/chapters.js +126 -0
  438. package/dist/slices/youtube/edits-repo.js +69 -0
  439. package/dist/slices/youtube/edits.js +89 -0
  440. package/dist/slices/youtube/placeholders.js +98 -0
  441. package/dist/slices/youtube/transcript.js +11 -4
  442. package/dist/updater/service.js +66 -21
  443. package/dist/web/assets/index-CUA2sUxU.css +1 -0
  444. package/dist/web/assets/index-CX590kE-.js +303 -0
  445. package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  446. package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  447. package/dist/web/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  448. package/dist/web/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  449. package/dist/web/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  450. package/dist/web/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  451. package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  452. package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  453. package/dist/web/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  454. package/dist/web/assets/{pdf-CAJNT254.js → pdf-BG8n_uc5.js} +1 -1
  455. package/dist/web/index.html +2 -2
  456. package/package.json +4 -4
  457. package/dist/edge/docker-launch.js +0 -81
  458. package/dist/edge/docker-projects/activation.js +0 -35
  459. package/dist/edge/docker-projects/claims.js +0 -85
  460. package/dist/edge/docker-projects/committed.js +0 -40
  461. package/dist/edge/docker-projects/engine.js +0 -461
  462. package/dist/edge/docker-projects/install.js +0 -254
  463. package/dist/edge/docker-projects/prune.js +0 -66
  464. package/dist/edge/docker-projects/recover.js +0 -100
  465. package/dist/edge/docker-projects/state.js +0 -225
  466. package/dist/web/assets/index-BGgYMm2u.css +0 -1
  467. package/dist/web/assets/index-BVVgtntL.js +0 -171
  468. package/scripts/docker-run.sh +0 -3
  469. /package/dist/edge/{docker-projects → docker-install}/tree.js +0 -0
@@ -1,5 +1,9 @@
1
- import { narrationPreparationFields, shortsFields, usesNarrationPreparation, usesShorts, usesYoutubeDescription, videoEditFields, youtubeDescriptionFields, } from "../admission/rules.js";
1
+ import { narrationPreparationFields, shortsFields, usesNarrationPreparation, usesReference, usesShorts, usesYoutubeDescription, videoEditFields, youtubeDescriptionFields, } from "../admission/rules.js";
2
2
  import { detectSlots, render } from "../admission/substitute.js";
3
+ import { imagesPerVideoMax } from "../images/scale.js";
4
+ import { loudnessFields } from "../loudness/model.js";
5
+ import { pauseFields } from "../narration/pauses-model.js";
6
+ import { reviewFields } from "../reviews/rules.js";
3
7
  import { usesAnimation } from "../video/edit-settings.js";
4
8
  export function validateRecipeInputs(config, content) {
5
9
  const fields = [
@@ -7,6 +11,9 @@ export function validateRecipeInputs(config, content) {
7
11
  ...youtubeDescriptionFields(config),
8
12
  ...shortsFields(config),
9
13
  ...videoEditFields(config),
14
+ ...reviewFields(config),
15
+ ...loudnessFields(config.loudness),
16
+ ...pauseFields(config),
10
17
  ];
11
18
  const llm = (config.sources.research === "generate" && config.sources.article !== "provide") ||
12
19
  (config.sources.article === "generate" && !content.articleEdited) ||
@@ -33,8 +40,13 @@ export function validateRecipeInputs(config, content) {
33
40
  usesAnimation(config)) &&
34
41
  (!config.images?.provider.trim() || !config.images.model.trim()))
35
42
  fields.push({ field: "images", message: "Pick an image provider and model." });
36
- if (content.imageOrder.length > 60)
37
- fields.push({ field: "content.imageOrder", message: "Keep at most 60 images." });
43
+ // 60, or more for a project that scales its images with the narration (`images/scale.ts`).
44
+ const imagesMax = imagesPerVideoMax(config);
45
+ if (content.imageOrder.length > imagesMax)
46
+ fields.push({
47
+ field: "content.imageOrder",
48
+ message: `Keep at most ${String(imagesMax)} images.`,
49
+ });
38
50
  const chunking = config.chunking;
39
51
  const size = chunking?.mode === "words"
40
52
  ? chunking.words
@@ -93,6 +105,20 @@ export function validateRecipeInputs(config, content) {
93
105
  raw: content.promptTemplates.thumbnailPrompt ?? null,
94
106
  literal: config.rendered.thumbnailPrompt,
95
107
  });
108
+ // The establishing image: its prompt is checked like the thumbnail's, an upload must be there.
109
+ if (usesReference(config) && config.reference?.source === "prompt")
110
+ prompts.push({
111
+ field: "rendered.referencePrompt",
112
+ raw: content.promptTemplates.referencePrompt ?? null,
113
+ literal: config.rendered.referencePrompt,
114
+ });
115
+ if (usesReference(config) &&
116
+ config.reference?.source === "provide" &&
117
+ content.provided.reference === undefined)
118
+ fields.push({
119
+ field: "content.provided.reference",
120
+ message: "Upload the establishing image, choose From a prompt instead, or set Establishing image to Off.",
121
+ });
96
122
  if (config.sources.images !== "off")
97
123
  for (const key of content.imageOrder) {
98
124
  const image = content.imageDefinitions[key];
@@ -1,9 +1,21 @@
1
+ import { thumbnailCountOf, thumbnailKey } from "../admission/model.js";
2
+ import { usesShortMode } from "../admission/short-mode.js";
1
3
  import { render } from "../admission/substitute.js";
4
+ import { usesAmbientBed } from "../video/ambient-bed.js";
5
+ import { castFor } from "./recipe-cast.js";
6
+ import { noMaster } from "./recipe-loudness.js";
2
7
  import { recipe, resourceIdentity, } from "./recipe-model.js";
8
+ import { castField, imageChoice } from "./recipe-reference.js";
3
9
  import { matchingText, renderedPrompt } from "./recipe-text.js";
4
10
  export function visualRecipes(config, content, audioFingerprint, captionFingerprint,
5
11
  // What the Video stage's edit settings add (`recipe-edit.ts`), from the images planned here.
6
- edit) {
12
+ edit,
13
+ // The establishing image the generated images are drawn from, when it is on.
14
+ reference,
15
+ // The word timing's identity, which a short's captions are drawn from.
16
+ timing = null,
17
+ // Level the volume: the levelled narration and the master (`recipe-loudness.ts`).
18
+ master = noMaster) {
7
19
  const recipes = [];
8
20
  const imageKeys = config.sources.images === "off" ? [] : content.imageOrder;
9
21
  for (const key of imageKeys) {
@@ -19,14 +31,23 @@ edit) {
19
31
  : {
20
32
  kind: "image",
21
33
  version: 1,
22
- provider: config.images?.provider ?? "",
23
- model: config.images?.model ?? "",
34
+ ...imageChoice(config),
24
35
  aspect: config.format,
25
36
  prompt: prompt ?? "",
26
- }, [], { unresolved: image.source === "provide" ? image.assetId === null : !prompt?.trim() }));
37
+ ...(reference === undefined ? {} : { reference: reference.input }),
38
+ ...castField(castFor(config, prompt)),
39
+ }, image.source === "provide" || reference === undefined ? [] : [reference.key], { unresolved: image.source === "provide" ? image.assetId === null : !prompt?.trim() }));
27
40
  }
28
41
  if (config.sources.video !== "off") {
29
- const edited = edit?.([...recipes]) ?? { recipes: [], values: [], dependsOn: [] };
42
+ const short = usesShortMode(config);
43
+ // A short is rendered by the Shorts renderer, which has no cuts, transitions or Look.
44
+ const edited = (short ? undefined : edit?.([...recipes])) ?? {
45
+ recipes: [],
46
+ values: [],
47
+ dependsOn: [],
48
+ };
49
+ // A short (rendered by the Shorts renderer) never has the bed.
50
+ const bed = short ? undefined : ambientBedValues(config, content);
30
51
  const audioKeys = config.sources.audio === "off"
31
52
  ? []
32
53
  : [
@@ -61,19 +82,51 @@ edit) {
61
82
  // Only what the edit settings change; nothing at all for today's slideshow, so
62
83
  // its fingerprint is the one it always had.
63
84
  ...(edited.values.length === 0 ? [] : [["video-edit", ...edited.values]]),
85
+ // Only for a short, so every long video keeps its fingerprint: the word-by-word
86
+ // captions are drawn from the word timing in the caption font, under the title.
87
+ ...(short
88
+ ? [["short-v1", config.subtitles?.fontId ?? "default", config.title, timing]]
89
+ : []),
90
+ // Only while the video has an ambient bed, so every video without one keeps the
91
+ // fingerprint it always had.
92
+ ...(bed === undefined ? [] : [bed]),
93
+ // Only while the volume is levelled, so every video without it keeps its fingerprint.
94
+ ...master.values,
64
95
  ],
65
96
  }, [
66
97
  ...recipes.map((value) => value.key),
67
98
  ...audioKeys,
99
+ ...master.keys,
68
100
  ...(config.subtitles?.mode === "burn-in" ? ["subtitles:files"] : []),
101
+ ...(short ? ["subtitles:timing"] : []),
69
102
  ...edited.dependsOn,
70
- ], { unresolved: imageKeys.length === 0 }));
103
+ ], {
104
+ unresolved: imageKeys.length === 0 ||
105
+ (config.ambientBed?.source === "upload" && content.ambientBed === undefined),
106
+ }));
71
107
  // After the export, so the images' recipes above stay the only ones its first values list.
72
108
  recipes.splice(recipes.length - 1, 0, ...edited.recipes);
73
109
  }
74
110
  return recipes;
75
111
  }
76
- export function thumbnailRecipes(context, textRecipes) {
112
+ // What the ambient bed adds to the render's fingerprint: its settings and, for the user's own
113
+ // file, the project asset it plays. Undefined without a bed.
114
+ export function ambientBedValues(config, content) {
115
+ const bed = config.ambientBed;
116
+ if (bed === undefined || !usesAmbientBed(config))
117
+ return undefined;
118
+ return [
119
+ "ambient-bed-v1",
120
+ bed.source,
121
+ bed.levelDb,
122
+ bed.fadeInSeconds,
123
+ bed.tailSeconds,
124
+ bed.source === "upload" ? (content.ambientBed ?? null) : null,
125
+ ];
126
+ }
127
+ export function thumbnailRecipes(context, textRecipes,
128
+ // The establishing image, when it is on and the thumbnail is drawn from it too.
129
+ reference) {
77
130
  const { config, content } = context;
78
131
  if (config.sources.thumbnail === "off")
79
132
  return [];
@@ -90,28 +143,50 @@ export function thumbnailRecipes(context, textRecipes) {
90
143
  const prompt = config.sources.thumbnail === "prompt_by_llm" && promptRecipe !== undefined
91
144
  ? matchingText(context, promptRecipe, "prompt")
92
145
  : renderedPrompt(context, "thumbnailPrompt");
93
- return [
94
- recipe(context, "thumbnail:image", "thumbnail", prompt === null
95
- ? {
96
- kind: "deferred",
97
- version: 1,
98
- operation: "thumbnail-image",
99
- template: [
100
- promptRecipe?.fingerprint ?? null,
101
- config.images?.provider ?? null,
102
- config.images?.model ?? null,
103
- config.format,
104
- ],
105
- }
106
- : {
107
- kind: "image",
108
- version: 1,
109
- provider: config.images?.provider ?? "",
110
- model: config.images?.model ?? "",
111
- aspect: config.format,
112
- prompt,
113
- }, promptRecipe === undefined ? [] : [promptRecipe.key]),
114
- ];
146
+ const variants = Array.from({ length: thumbnailCountOf(config) }, (_, index) => index + 1);
147
+ return variants.map((variant) => recipe(context, thumbnailKey(variant), "thumbnail", prompt === null
148
+ ? {
149
+ kind: "deferred",
150
+ version: 1,
151
+ operation: "thumbnail-image",
152
+ template: [
153
+ promptRecipe?.fingerprint ?? null,
154
+ config.images?.provider ?? null,
155
+ config.images?.model ?? null,
156
+ config.format,
157
+ // Only what is in use, so a thumbnail made before these existed keeps its
158
+ // fingerprint.
159
+ ...(config.images?.thinking === undefined ? [] : [config.images.thinking]),
160
+ ...(reference === undefined ? [] : [reference.input.fingerprint]),
161
+ ...(variant === 1 ? [] : [["thumbnail-variant", variant]]),
162
+ ],
163
+ }
164
+ : {
165
+ kind: "image",
166
+ version: 1,
167
+ ...imageChoice(config),
168
+ aspect: config.format,
169
+ prompt: thumbnailVariantPrompt(prompt, variant),
170
+ ...(reference === undefined ? {} : { reference: reference.input }),
171
+ // The thumbnail stands for the whole video, like the establishing image.
172
+ ...castField(castFor(config, config.title, prompt)),
173
+ }, [
174
+ ...(promptRecipe === undefined ? [] : [promptRecipe.key]),
175
+ ...(reference === undefined ? [] : [reference.key]),
176
+ ]));
177
+ }
178
+ // The second and third thumbnails are the same prompt asked for another composition, so the
179
+ // three test one idea against itself rather than three ideas. The first is the prompt as it
180
+ // is, which keeps the thumbnail every project already has.
181
+ const variantFraming = {
182
+ 2: "a tight close-up where the main subject fills most of the frame, seen from a different angle",
183
+ 3: "a wider shot that shows the main subject in its setting, placed off-centre with room around it",
184
+ };
185
+ export function thumbnailVariantPrompt(prompt, variant) {
186
+ const framing = variantFraming[variant];
187
+ if (framing === undefined)
188
+ return prompt;
189
+ return `${prompt}\n\nThis is thumbnail ${String(variant)} of 3 for a YouTube thumbnail test. Keep the subject, style, colours and any text asked for above, but use a clearly different composition: ${framing}.`;
115
190
  }
116
191
  export function visualAssets(context, recipes) {
117
192
  const images = recipes.filter((value) => value.stage === "images");
@@ -0,0 +1,286 @@
1
+ import { aliasMatches, applyAliases } from "../../kernel/ports/narration-aliases.js";
2
+ import { splitText } from "../../kernel/ports/text.js";
3
+ import { narrationAliasesOf, usesNarrationPreparation } from "../admission/rules.js";
4
+ import { aliasSpans, withAliasSpans } from "../narration/aliases.js";
5
+ import { normalizeNarrationText } from "../narration/plan.js";
6
+ import { parsePronunciationGlossary, pronunciationSpans, } from "../narration/pronunciation.js";
7
+ import { withSharedGlossary } from "../narration/shared-glossary.js";
8
+ import { prepareRequests } from "../narration/steering.js";
9
+ import { groupTurns } from "../voices/grouping.js";
10
+ import { speakerPace } from "../voices/model.js";
11
+ import { parseScript } from "../voices/script.js";
12
+ import { describedText, describeFuture, describing } from "./recipe-describe.js";
13
+ import { recipe, resourceIdentity, } from "./recipe-model.js";
14
+ import { pauseValues } from "./recipe-pauses.js";
15
+ import { preparationForGroup, preparationFuture, preparationTemplate, } from "./recipe-preparation.js";
16
+ export function voiceBodyRecipes(context, voices, script, glossary) {
17
+ const parts = [];
18
+ const preparations = [];
19
+ const layout = [];
20
+ // Which parts end a turn, for the pauses between sentences (a turn gap of 0 is still a turn).
21
+ const turnEnds = new Set();
22
+ let transcript = script.fingerprint ?? script.text ?? null;
23
+ const describe = describing(context);
24
+ const parsed = script.text === null ? undefined : parseScript(script.text, voices.speakers, describe);
25
+ const prepare = voices.speakers.some((speaker) => preparesTurns(context.config, speaker));
26
+ // The narration still to be worked out: the whole script's while it is being written, or
27
+ // every turn's while a turn's delivery cues are (as a single voice waits for all of its own).
28
+ const future = () => recipe(context, "audio:body:future", "audio", {
29
+ kind: "deferred",
30
+ version: 1,
31
+ operation: "body-narration",
32
+ template: [
33
+ script.fingerprint ?? script.text ?? null,
34
+ speakerValues(voices),
35
+ ...(prepare ? [preparationTemplate(context)] : []),
36
+ ...(described.length === 0
37
+ ? []
38
+ : [["described-v1", described.map((row) => row.fingerprint)]]),
39
+ ],
40
+ }, [
41
+ ...new Set([
42
+ ...script.dependsOn,
43
+ ...described.map((row) => row.key),
44
+ ...preparations.map((row) => row.key),
45
+ ]),
46
+ ]);
47
+ let pending = false;
48
+ // A table, figure, equation or code block inside a turn is described within that turn: the
49
+ // turn says the description where the block was, in the same voice.
50
+ const described = [];
51
+ const cards = [];
52
+ let turns = parsed?.ok === true ? parsed.script.turns : [];
53
+ if (describe && script.text === null) {
54
+ const from = script.dependsOn[0];
55
+ if (from !== undefined)
56
+ described.push(describeFuture(context, { key: from, fingerprint: script.fingerprint ?? from }));
57
+ }
58
+ else if (describe && parsed?.ok === true) {
59
+ let offset = 0;
60
+ turns = turns.map((turn) => {
61
+ if (turn.markdown === undefined)
62
+ return turn;
63
+ const said = describedText(context, turn.markdown, script.dependsOn, offset);
64
+ if (said.recipes.length === 0)
65
+ return turn;
66
+ offset += said.recipes.length;
67
+ described.push(...said.recipes);
68
+ cards.push(...said.cards);
69
+ if (said.spoken === null) {
70
+ pending = true;
71
+ return turn;
72
+ }
73
+ return { ...turn, text: said.spoken.trim().replace(/\s*\n\s*/g, " ") };
74
+ });
75
+ }
76
+ preparations.push(...described);
77
+ if (script.text === null) {
78
+ // Turns not yet known are prepared behind one future step, as a single voice's text is.
79
+ const from = script.dependsOn[0];
80
+ if (prepare && from !== undefined)
81
+ preparations.push(preparationFuture(context, "body", { key: from, fingerprint: script.fingerprint ?? from }));
82
+ parts.push(future());
83
+ }
84
+ else if (parsed !== undefined && !parsed.ok) {
85
+ parts.push(refused(context, "audio:body:turn:1", script.dependsOn, `The script can't be narrated: ${parsed.reason} Fix it in Edit project → Article, then Try again.`));
86
+ }
87
+ else if (pending) {
88
+ parts.push(future());
89
+ }
90
+ else if (parsed !== undefined) {
91
+ transcript = ["voices-transcript-v1", turns.map((turn) => [turn.speaker, turn.text])];
92
+ const aliases = narrationAliasesOf(context.config);
93
+ for (const group of groupTurns(turns, voices.speakers, voices.nativeDialogue, (text) => applyAliases(text, aliases).length)) {
94
+ const first = group.turns[0];
95
+ const speaker = voices.speakers.find((one) => one.id === first?.speaker);
96
+ if (first === undefined || speaker === undefined)
97
+ continue;
98
+ const made = groupParts(context, voices, group.native, group.turns, speaker, glossary, script, preparations);
99
+ if (made.length === 0)
100
+ pending = true;
101
+ made.forEach((part, index) => {
102
+ parts.push(part);
103
+ if (index === made.length - 1)
104
+ turnEnds.add(layout.length);
105
+ layout.push([speakerPace(speaker), index === made.length - 1 ? voices.turnGapSeconds : 0]);
106
+ });
107
+ }
108
+ // No gap after the last turn: the timeline's own gap follows it.
109
+ const last = layout.at(-1);
110
+ if (last !== undefined)
111
+ last[1] = 0;
112
+ if (pending) {
113
+ parts.length = 0;
114
+ layout.length = 0;
115
+ turnEnds.clear();
116
+ parts.push(future());
117
+ }
118
+ }
119
+ const body = recipe(context, "audio:body:concat", "audio", {
120
+ kind: "local",
121
+ version: 1,
122
+ operation: "concat-turns-v1",
123
+ values: [
124
+ parts.map((part) => resourceIdentity(context, part)),
125
+ layout,
126
+ // Pauses between sentences inside a turn; the turn gap stays the layout's.
127
+ ...pauseValues(context.config, layout.map(([, gap], at) => at === layout.length - 1 ? "end" : gap > 0 || turnEnds.has(at) ? "turn" : "sentence")),
128
+ ],
129
+ }, parts.map((part) => part.key), { unresolved: parsed?.ok !== true || pending });
130
+ return {
131
+ preparations,
132
+ cards,
133
+ parts,
134
+ body,
135
+ transcript,
136
+ ...(parsed?.ok === true ? { sections: parsed.script.sections } : {}),
137
+ };
138
+ }
139
+ function groupParts(context, voices, native, turns, speaker, glossary, script, preparations) {
140
+ const first = turns[0];
141
+ if (first === undefined)
142
+ return [];
143
+ const logicalKey = `audio:body:turn:${String(first.index)}`;
144
+ const override = context.content.narrationOverrides[logicalKey];
145
+ if (override?.kind === "asset")
146
+ return [
147
+ recipe(context, `${logicalKey}:1`, "audio", {
148
+ kind: "provided",
149
+ version: 1,
150
+ assetId: override.assetId,
151
+ semantic: [turns.map((turn) => turn.text).join("\n"), speaker.id],
152
+ }, script.dependsOn, { tokenKey: logicalKey }),
153
+ ];
154
+ if (native) {
155
+ const byId = new Map(voices.speakers.map((one) => [one.id, one]));
156
+ // Narration aliases reach every line of the one request, each turn on its own so an alias
157
+ // never joins the end of one turn to the start of the next. The clean turns stay the
158
+ // transcript, as they do for a voice that gets one request per turn.
159
+ const aliases = narrationAliasesOf(context.config);
160
+ const spoken = turns.map((turn) => applyAliases(turn.text, aliases));
161
+ const logicalText = turns.map((turn) => turn.text).join("\n");
162
+ const text = spoken.join("\n");
163
+ return [
164
+ recipe(context, `${logicalKey}:1`, "audio", {
165
+ kind: "tts",
166
+ version: 1,
167
+ provider: speaker.voice.provider,
168
+ model: speaker.voice.model,
169
+ voice: speaker.voice.voice,
170
+ text,
171
+ ...(text === logicalText ? {} : { spokenText: logicalText }),
172
+ logicalKey,
173
+ logicalText,
174
+ segment: "body",
175
+ pronunciation: null,
176
+ dialogue: turns.map((turn, index) => ({
177
+ speaker: turn.speaker,
178
+ turn: turn.index,
179
+ voice: byId.get(turn.speaker)?.voice.voice ?? "",
180
+ text: spoken[index] ?? turn.text,
181
+ })),
182
+ }, script.dependsOn, { tokenKey: logicalKey }),
183
+ ];
184
+ }
185
+ const logicalText = normalizeNarrationText(override?.kind === "text" ? override.text : first.text);
186
+ const entries = speakerGlossary(context, speaker, glossary);
187
+ if (!entries.ok)
188
+ return [refused(context, logicalKey, script.dependsOn, entries.reason)];
189
+ const maxCharacters = context.catalogue?.tts.find((row) => row.provider === speaker.voice.provider &&
190
+ row.id === speaker.voice.model &&
191
+ row.enabled &&
192
+ !row.deprecated)?.tts.maxCharacters ?? Math.max(2, logicalText.length * 4);
193
+ // Narration aliases apply to every voice, the speaker's pronunciations to IPA voices only.
194
+ const spans = withAliasSpans(pronunciationSpans(logicalText, entries.entries), aliasSpans(logicalText, narrationAliasesOf(context.config)));
195
+ if (preparesTurns(context.config, speaker)) {
196
+ const prepared = preparationForGroup(context, logicalKey, logicalText, "body", script.dependsOn, maxCharacters, spans, aliasMatches(logicalText, narrationAliasesOf(context.config)), { name: speaker.name.trim(), role: speaker.role });
197
+ preparations.push(prepared.preparation);
198
+ if (prepared.refusal !== null)
199
+ return [refused(context, logicalKey, [prepared.preparation.key], prepared.refusal)];
200
+ return (prepared.requests ?? []).map((request, index) => recipe(context, `${logicalKey}:${String(index + 1)}`, "audio", {
201
+ kind: "tts",
202
+ version: 1,
203
+ provider: speaker.voice.provider,
204
+ model: speaker.voice.model,
205
+ voice: speaker.voice.voice,
206
+ text: request.text,
207
+ spokenText: request.spokenText,
208
+ logicalKey,
209
+ logicalText,
210
+ segment: "body",
211
+ pronunciation: null,
212
+ speaker: speaker.id,
213
+ turn: first.index,
214
+ }, [prepared.preparation.key], { tokenKey: logicalKey, unresolved: logicalText.length === 0 }));
215
+ }
216
+ let requests;
217
+ if (spans.length > 0) {
218
+ const prepared = prepareRequests(logicalText, [], maxCharacters, spans);
219
+ if (!prepared.ok)
220
+ return [refused(context, logicalKey, script.dependsOn, prepared.reason)];
221
+ requests = prepared.requests;
222
+ }
223
+ else
224
+ requests = splitText(logicalText, maxCharacters).map((text) => ({ text }));
225
+ return requests.map((request, index) => recipe(context, `${logicalKey}:${String(index + 1)}`, "audio", {
226
+ kind: "tts",
227
+ version: 1,
228
+ provider: speaker.voice.provider,
229
+ model: speaker.voice.model,
230
+ voice: speaker.voice.voice,
231
+ text: request.text,
232
+ ...(request.spokenText === undefined ? {} : { spokenText: request.spokenText }),
233
+ logicalKey,
234
+ logicalText,
235
+ segment: "body",
236
+ pronunciation: null,
237
+ speaker: speaker.id,
238
+ turn: first.index,
239
+ }, script.dependsOn, { tokenKey: logicalKey, unresolved: logicalText.length === 0 }));
240
+ }
241
+ // IPA pronunciations reach only the voices that read IPA: the speaker's own first, then the
242
+ // run's glossary where Use Pronunciation Glossary is on.
243
+ function speakerGlossary(context, speaker, glossary) {
244
+ if (!readsIpa(speaker))
245
+ return { ok: true, entries: [] };
246
+ const own = speaker.pronunciations?.trim()
247
+ ? parsePronunciationGlossary(speaker.pronunciations, context.config.language)
248
+ : { ok: true, entries: [] };
249
+ if (!own.ok)
250
+ return {
251
+ ok: false,
252
+ reason: `${speaker.name.trim()}'s pronunciations can't be used: ${own.reason.replace("Edit the glossary or turn off Use Pronunciation Glossary.", "Fix them under Speakers (Play → Audio, or Edit project → Providers).")}`,
253
+ };
254
+ const run = context.config.audio?.usePronunciationGlossary === true && glossary?.ok === true
255
+ ? glossary.entries
256
+ : [];
257
+ return { ok: true, entries: withSharedGlossary(own.entries, run) };
258
+ }
259
+ // A turn is prepared when the run has a Narration Preparation prompt and its speaker's voice
260
+ // takes Inworld TTS-2's delivery tags; every other speaker's turns are spoken as written.
261
+ export function preparesTurns(config, speaker) {
262
+ return (usesNarrationPreparation(config) &&
263
+ speaker.voice.provider === "inworld" &&
264
+ speaker.voice.model === "inworld-tts-2");
265
+ }
266
+ export function readsIpa(speaker) {
267
+ return (speaker.voice.provider === "inworld" &&
268
+ (speaker.voice.model === "inworld-tts-2" || speaker.voice.model === "inworld-tts-2-flash"));
269
+ }
270
+ function refused(context, logicalKey, dependsOn, reason) {
271
+ return recipe(context, `${logicalKey}:1`, "audio", { kind: "deferred", version: 1, operation: "resolve-revision-recipe", template: reason }, dependsOn, { unresolved: true, refusal: reason });
272
+ }
273
+ // What a pending script will be spoken with, so a voice change before the script lands is
274
+ // still a change.
275
+ export function speakerValues(voices) {
276
+ return [
277
+ voices.nativeDialogue,
278
+ voices.speakers.map((speaker) => [
279
+ speaker.id,
280
+ speaker.voice.provider,
281
+ speaker.voice.model,
282
+ speaker.voice.voice,
283
+ speaker.pronunciations ?? null,
284
+ ]),
285
+ ];
286
+ }