@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
@@ -31,6 +31,8 @@ const stageRow = z.object({
31
31
  progress_total: z.number().nullable(),
32
32
  started_at: z.string().nullable(),
33
33
  finished_at: z.string().nullable(),
34
+ failure_kind: z.string().nullable().optional(),
35
+ retry_at: z.string().nullable().optional(),
34
36
  });
35
37
  export function insertProject(db, project) {
36
38
  db.prepare("INSERT INTO projects (id, title, format, config, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)").run(project.id, project.title, project.format, JSON.stringify(project.config), project.createdAt, project.updatedAt);
@@ -40,7 +42,21 @@ export function insertProject(db, project) {
40
42
  export function insertStage(db, stage) {
41
43
  db.prepare("INSERT INTO stages (id, project_id, kind, source, state, attempt_count) VALUES (?, ?, ?, ?, ?, ?)").run(stage.id, stage.projectId, stage.kind, stage.source, stage.state, stage.attemptCount);
42
44
  }
45
+ // A project in the trash (`project_trash`, Settings → Trash) is not found by any of these
46
+ // reads: it is listed nowhere, opens nowhere and takes no action until it is restored.
47
+ // `slices/trash` and `slices/storage/delete-project.ts` read the table directly.
48
+ export function liveProject(alias = "projects", column = "id") {
49
+ return `NOT EXISTS (SELECT 1 FROM project_trash WHERE project_trash.project_id = ${alias}.${column})`;
50
+ }
51
+ export function projectTrashed(db, id) {
52
+ return db.prepare("SELECT 1 FROM project_trash WHERE project_id = ?").get(id) !== undefined;
53
+ }
43
54
  export function projectById(db, id) {
55
+ const row = db.prepare(`${projectSelect} WHERE projects.id = ? AND ${liveProject()}`).get(id);
56
+ return row === undefined ? undefined : toProject(projectRow.parse(row));
57
+ }
58
+ // The same, trash or not: a backup carries a project in the trash and checks it can be read.
59
+ export function projectByIdIncludingTrash(db, id) {
44
60
  const row = db.prepare(`${projectSelect} WHERE projects.id = ?`).get(id);
45
61
  return row === undefined ? undefined : toProject(projectRow.parse(row));
46
62
  }
@@ -48,12 +64,12 @@ export function projectById(db, id) {
48
64
  // asks whether the id names a project, and parsing the config to answer that would tie
49
65
  // cancelling to a schema it never reads.
50
66
  export function projectExists(db, id) {
51
- return db.prepare("SELECT 1 FROM projects WHERE id = ?").get(id) !== undefined;
67
+ return (db.prepare(`SELECT 1 FROM projects WHERE id = ? AND ${liveProject()}`).get(id) !== undefined);
52
68
  }
53
69
  // Newest first.
54
70
  export function listProjects(db) {
55
71
  return db
56
- .prepare(`${projectSelect} ORDER BY created_at DESC, id DESC`)
72
+ .prepare(`${projectSelect} WHERE ${liveProject()} ORDER BY created_at DESC, id DESC`)
57
73
  .all()
58
74
  .map((row) => toProject(projectRow.parse(row)));
59
75
  }
@@ -98,12 +114,12 @@ export function claimStage(db, stageId, at) {
98
114
  return Number(result.changes) === 1;
99
115
  }
100
116
  export function finishStage(db, stageId, state, failureReason, at) {
101
- db.prepare("UPDATE stages SET state = ?, failure_reason = ?, finished_at = ? WHERE id = ?").run(state, failureReason, state === "pending" ? null : at, stageId);
117
+ db.prepare("UPDATE stages SET state = ?, failure_reason = ?, retry_at = NULL, finished_at = ? WHERE id = ?").run(state, failureReason, state === "pending" ? null : at, stageId);
102
118
  }
103
119
  // A stage put back to `pending` by a re-run, a cascade or a retry starts again from a clean row
104
120
  // - no error text, no progress from the run before it, and a fresh attempt budget.
105
121
  export function resetStage(db, stageId) {
106
- db.prepare("UPDATE stages SET state = 'pending', failure_reason = NULL, attempt_count = 0, progress_current = NULL, progress_total = NULL, started_at = NULL, finished_at = NULL WHERE id = ?").run(stageId);
122
+ db.prepare("UPDATE stages SET state = 'pending', failure_reason = NULL, failure_kind = NULL, retry_at = NULL, attempt_count = 0, progress_current = NULL, progress_total = NULL, started_at = NULL, finished_at = NULL WHERE id = ?").run(stageId);
107
123
  }
108
124
  export function setStageProgress(db, stageId, current, total) {
109
125
  db.prepare("UPDATE stages SET progress_current = ?, progress_total = ? WHERE id = ?").run(current, total, stageId);
@@ -151,5 +167,9 @@ function toStage(row) {
151
167
  progressTotal: row.progress_total,
152
168
  startedAt: row.started_at,
153
169
  finishedAt: row.finished_at,
170
+ ...(row.failure_kind === null || row.failure_kind === undefined
171
+ ? {}
172
+ : { failureKind: row.failure_kind }),
173
+ ...(row.retry_at === null || row.retry_at === undefined ? {} : { retryAt: row.retry_at }),
154
174
  };
155
175
  }
@@ -1,7 +1,14 @@
1
1
  import { stageKinds } from "../../kernel/pipeline.js";
2
+ import { imageScaleProblem } from "../images/scale.js";
3
+ import { loudnessFields } from "../loudness/model.js";
4
+ import { pauseFields } from "../narration/pauses-model.js";
5
+ import { reviewFields } from "../reviews/rules.js";
2
6
  import { shortsSettingsProblems } from "../shorts/model.js";
7
+ import { ambientBedProblems, usesAmbientBed } from "../video/ambient-bed.js";
3
8
  import { usesAnimation, videoEditProblems } from "../video/edit-settings.js";
9
+ import { usesVoices, voicesProblems } from "../voices/model.js";
4
10
  import { sourceOf } from "./model.js";
11
+ import { shortModeFields } from "./short-mode.js";
5
12
  export const titleMax = 200;
6
13
  export const valueMax = 200;
7
14
  export const numberPerPromptMax = 20;
@@ -84,7 +91,8 @@ export function admit(input) {
84
91
  draft.outro?.mode === "llm" ||
85
92
  usesNarrationPreparation(draft) ||
86
93
  usesYoutubeDescription(draft) ||
87
- usesShorts(draft);
94
+ usesShorts(draft) ||
95
+ (usesVoices(draft) && draft.voices?.source === "attribute");
88
96
  if (needsLlm && !chosen(draft.llm)) {
89
97
  fields.push({ field: "llm", message: "Choose a text (LLM) provider and model." });
90
98
  }
@@ -93,6 +101,7 @@ export function admit(input) {
93
101
  }
94
102
  const voiced = draft.audio;
95
103
  fields.push(...narrationPreparationFields(draft));
104
+ fields.push(...voicesFields(draft));
96
105
  if (sources.audio === "generate") {
97
106
  if (voiced === undefined || !chosen(voiced)) {
98
107
  fields.push({ field: "audio", message: "Choose a narration provider and model." });
@@ -124,8 +133,13 @@ export function admit(input) {
124
133
  }
125
134
  fields.push(...youtubeDescriptionFields(draft));
126
135
  fields.push(...shortsFields(draft));
136
+ fields.push(...shortModeFields(draft));
127
137
  fields.push(...videoEditFields(draft));
138
+ fields.push(...ambientBedFields(draft, input.staged));
139
+ fields.push(...loudnessFields(draft.loudness));
140
+ fields.push(...pauseFields(draft));
128
141
  fields.push(...referenceFields(draft));
142
+ fields.push(...reviewFields(draft));
129
143
  if (usesReference(draft) && draft.reference?.source === "provide")
130
144
  checkFile(input.staged, draft.provided.reference, "images", "provided.reference", "Upload the establishing image, or set Establishing image to Off in the Images section.", fields);
131
145
  checkProvided(draft, input.staged, fields);
@@ -210,6 +224,9 @@ function checkImagePrompts(draft, fields) {
210
224
  message: `A video can have at most ${imagesPerRunMax} images; these prompts ask for ${total}. Lower the numbers.`,
211
225
  });
212
226
  }
227
+ const scaleProblem = draft.imageScale === undefined ? undefined : imageScaleProblem(draft.imageScale);
228
+ if (scaleProblem !== undefined)
229
+ fields.push({ field: "imageScale", message: scaleProblem });
213
230
  }
214
231
  function checkProvided(draft, staged, fields) {
215
232
  const { sources, provided } = draft;
@@ -254,7 +271,7 @@ function checkShortsMusic(draft, staged, fields) {
254
271
  const id = draft.provided.shortsMusic;
255
272
  if (draft.shorts?.enabled !== true || id === undefined)
256
273
  return;
257
- const where = "under Outputs → Export → More shorts options → Background music";
274
+ const where = "under Outputs → More shorts options → Background music";
258
275
  const file = staged.find((candidate) => candidate.id === id);
259
276
  if (file === undefined || file.stageKind !== "audio")
260
277
  fields.push({
@@ -320,6 +337,21 @@ export function narrationPreparationFields(draft) {
320
337
  ]
321
338
  : [];
322
339
  }
340
+ // Multiple voices, checked only while narration is generated: a format left set with
341
+ // narration Off or uploaded asks for nothing.
342
+ export function voicesFields(draft) {
343
+ if (draft.voices === undefined || !usesVoices(draft))
344
+ return [];
345
+ const fields = [...voicesProblems(draft.voices)];
346
+ // Preparation adds delivery tags to the turns of speakers on Inworld TTS-2, so it needs one.
347
+ if (!blank(draft.narrationPrompt) &&
348
+ !draft.voices.speakers.some((speaker) => speaker.voice.provider === "inworld" && speaker.voice.model === "inworld-tts-2"))
349
+ fields.push({
350
+ field: "narrationPrompt",
351
+ message: "Narration preparation adds delivery cues only to speakers on Inworld's TTS-2 model, and no speaker uses it. Choose that model for a speaker under Speakers, or clear Narration Preparation under Narration → Audio Advanced.",
352
+ });
353
+ return fields;
354
+ }
323
355
  // The YouTube description is written from the narration's word timings, so it needs
324
356
  // narration; it runs whether the video renders or only the WAV is exported.
325
357
  export function usesYoutubeDescription(draft) {
@@ -394,9 +426,55 @@ export function videoEditFields(draft) {
394
426
  message: problem.message,
395
427
  }));
396
428
  }
429
+ // The ambient bed, checked only while the long video renders with narration; its own file only
430
+ // while the source is "upload". Named by where the control sits: Play's Export rail.
431
+ export function ambientBedFields(draft, staged) {
432
+ const bed = draft.ambientBed;
433
+ if (bed === undefined || !usesAmbientBed(draft))
434
+ return [];
435
+ const where = "under Video and style → Ambient sound";
436
+ const fields = ambientBedProblems(bed).map((problem) => ({
437
+ field: `ambientBed.${problem.field}`,
438
+ message: `${problem.message} Change it ${where}.`,
439
+ }));
440
+ if (bed.source !== "upload")
441
+ return fields;
442
+ const id = draft.provided.ambientBed;
443
+ const file = id === undefined ? undefined : staged.find((candidate) => candidate.id === id);
444
+ if (file === undefined || file.stageKind !== "audio")
445
+ fields.push({
446
+ field: "ambientBed.file",
447
+ message: `The ambient sound's audio file is missing, so it can't be added to the project. Choose the file ${where}, or pick Rain, Fireplace or Wind instead.`,
448
+ });
449
+ else if (file.state !== "staged")
450
+ fields.push({
451
+ field: "ambientBed.file",
452
+ message: `The ambient sound's audio file is still uploading, so the run can't start yet. Wait for it to finish ${where}.`,
453
+ });
454
+ return fields;
455
+ }
397
456
  function chosen(choice) {
398
457
  return choice !== undefined && choice.provider.trim() !== "" && choice.model.trim() !== "";
399
458
  }
459
+ // The aliases a run narrates with: its copied Library → Aliases while Use narration aliases is
460
+ // on for generated audio, otherwise none. Any voice provider: an alias is plain text.
461
+ export function narrationAliasesOf(draft) {
462
+ return draft.sources.audio === "generate" && draft.audio?.useNarrationAliases === true
463
+ ? (draft.narrationAliases ?? [])
464
+ : [];
465
+ }
466
+ // Describe tables and figures in the narration: generated narration, the setting on, and a
467
+ // text model to write the descriptions. Without a text model the narration flattens the
468
+ // article as it always did, so the default being on never asks for one.
469
+ export function usesDescribedNarration(draft) {
470
+ return (draft.sources.audio === "generate" && draft.audio?.describeFigures === true && chosen(draft.llm));
471
+ }
472
+ // "Show tables and figures on screen": a video, and the narration describing its blocks.
473
+ export function usesFigureCards(draft) {
474
+ return (usesDescribedNarration(draft) &&
475
+ draft.sources.video === "generate" &&
476
+ draft.showFigures === true);
477
+ }
400
478
  export function usesPronunciationGlossary(draft) {
401
479
  return (draft.sources.audio === "generate" &&
402
480
  draft.audio?.usePronunciationGlossary === true &&
@@ -1,12 +1,19 @@
1
1
  import { z } from "zod";
2
+ import { languageSchema } from "../../kernel/ports/languages.js";
2
3
  import { thinkingModes } from "../../kernel/ports/llm.js";
4
+ import { castSnapshotSchema } from "../channels/schema.js";
3
5
  import { checkpointStageSchema } from "../checkpoints/schema.js";
4
6
  import { documentSettingsSchema } from "../document/theme-schema.js";
7
+ import { narrationAliasesSchema } from "../narration/aliases-schema.js";
5
8
  import { chunkModes } from "../narration/chunk.js";
9
+ import { reviewModes, reviewStages } from "../reviews/model.js";
6
10
  import { subtitleConfigSchema } from "../subtitles/model.js";
11
+ import { ambientBedSchema } from "../video/ambient-bed-schema.js";
7
12
  import { animateModes, atmospheres, colorGrades, cutModes, lookLevels, transitionKinds, } from "../video/edit-settings.js";
8
- import { entryModes, formats, motionStyles, referenceSources, stageSources } from "./model.js";
13
+ import { voicesSettingsSchema } from "../voices/model.js";
14
+ import { entryModes, formats, motionStyles, referenceSources, runModes, stageSources, } from "./model.js";
9
15
  import { defaultEdgeSilenceSeconds, defaultImageSeconds, defaultMotionStyle, defaultZoomPercent, } from "./rules.js";
16
+ const hexColour = z.string().regex(/^#[0-9A-Fa-f]{6}$/);
10
17
  const providerChoice = z.object({
11
18
  provider: z.string(),
12
19
  model: z.string(),
@@ -29,6 +36,8 @@ export const videoEditSchema = z.object({
29
36
  // The shape Play posts and the shape `projects.config` holds, in one place: the second
30
37
  // is the first plus the rendered prompt texts.
31
38
  export const runDraftSchema = z.object({
39
+ // Absent is the long video.
40
+ mode: z.enum(runModes).optional(),
32
41
  checkpoints: z
33
42
  .array(checkpointStageSchema)
34
43
  .max(3)
@@ -55,8 +64,12 @@ export const runDraftSchema = z.object({
55
64
  voice: z.string(),
56
65
  usePronunciationGlossary: z.boolean().optional(),
57
66
  shareGlossary: z.boolean().optional(),
67
+ useNarrationAliases: z.boolean().optional(),
68
+ describeFigures: z.boolean().optional(),
69
+ skipCode: z.boolean().optional(),
58
70
  })
59
71
  .optional(),
72
+ narrationAliases: narrationAliasesSchema.optional(),
60
73
  sharedGlossary: z
61
74
  .array(z.object({
62
75
  term: z.string().min(1).max(200),
@@ -76,7 +89,10 @@ export const runDraftSchema = z.object({
76
89
  articlePrompt: z.string().optional(),
77
90
  narrationPrompt: z.string().optional(),
78
91
  imagePrompts: z.array(z.object({ name: z.string(), number: z.number() })),
92
+ // The ranges are `slices/images/scale.ts`'s, checked by admission, not the schema's.
93
+ imageScale: z.object({ perHour: z.number(), words: z.number() }).optional(),
79
94
  thumbnailPrompt: z.string().optional(),
95
+ thumbnailCount: z.union([z.literal(1), z.literal(3)]).optional(),
80
96
  intro: entryChoice.optional(),
81
97
  outro: entryChoice.optional(),
82
98
  values: z.record(z.string(), z.string()),
@@ -89,6 +105,8 @@ export const runDraftSchema = z.object({
89
105
  reference: z.string().optional(),
90
106
  // The staged file of the shorts' background music; used only while Shorts is on.
91
107
  shortsMusic: z.string().optional(),
108
+ // The staged file of the ambient bed; used only while its source is "upload".
109
+ ambientBed: z.string().optional(),
92
110
  }),
93
111
  // Optional until Play carries the control; unknown keys are stripped by this schema, so
94
112
  // a mode not listed here would never reach the audio stage.
@@ -109,6 +127,7 @@ export const runDraftSchema = z.object({
109
127
  subtitles: subtitleConfigSchema.optional(),
110
128
  document: documentSettingsSchema.optional(),
111
129
  youtubeDescription: z.boolean().optional(),
130
+ showFigures: z.boolean().optional(),
112
131
  descriptionPrompt: z.string().optional(),
113
132
  // The ranges are `slices/shorts/model.ts`'s, checked by admission, not the schema's.
114
133
  shorts: z
@@ -127,6 +146,44 @@ export const runDraftSchema = z.object({
127
146
  .optional(),
128
147
  // The ranges are `slices/video/edit-settings.ts`'s, checked by admission, not the schema's.
129
148
  videoEdit: videoEditSchema.optional(),
149
+ channelId: z.string().optional(),
150
+ useBrandKit: z.literal(false).optional(),
151
+ cast: castSnapshotSchema.optional(),
152
+ earlierEpisodes: z
153
+ .array(z.object({ title: z.string(), summary: z.string() }))
154
+ .readonly()
155
+ .optional(),
156
+ titleStyle: z
157
+ .object({
158
+ fontId: z
159
+ .string()
160
+ .max(160)
161
+ .regex(/^[A-Za-z0-9_-]+$/)
162
+ .optional(),
163
+ color: hexColour.optional(),
164
+ })
165
+ .optional(),
166
+ endScreen: z.object({ text: z.string().max(200) }).optional(),
167
+ // The retry range is `slices/reviews/model.ts`'s, checked by admission, not the schema's.
168
+ reviews: z
169
+ .object({
170
+ provider: z.string(),
171
+ model: z.string(),
172
+ thinking: z.enum(thinkingModes).optional(),
173
+ retries: z.number().optional(),
174
+ stages: z.partialRecord(z.enum(reviewStages), z.object({ mode: z.enum(reviewModes), prompt: z.string().optional() })),
175
+ })
176
+ .optional(),
177
+ // The rules are `slices/voices/model.ts`'s, checked by admission, not the schema's.
178
+ voices: voicesSettingsSchema.optional(),
179
+ ambientBed: ambientBedSchema.optional(),
180
+ // Level the volume (`loudness/model.ts`); the range is checked by admission.
181
+ loudness: z.object({ videoLufs: z.number(), audioFilesLufs: z.number() }).strict().optional(),
182
+ // Pauses between sentences and paragraphs (`narration/pauses-model.ts`); checked by admission.
183
+ sentencePauseSeconds: z.number().optional(),
184
+ paragraphPauseSeconds: z.number().optional(),
185
+ // Absent is English (`kernel/ports/languages.ts`).
186
+ language: languageSchema.optional(),
130
187
  });
131
188
  export const runConfigSchema = runDraftSchema.extend({
132
189
  rendered: z.record(z.string(), z.string()),
@@ -0,0 +1,47 @@
1
+ import { sourceOf } from "./model.js";
2
+ // The short-only run mode: the same stages as a long video, narrowed to one vertical clip of
3
+ // about a minute. The article is written for a short, the narration and images are made as
4
+ // usual, and the Video stage renders the whole narration through the Shorts renderer
5
+ // (`shorts/render.ts`) with its word-by-word captions instead of the slideshow. A short has
6
+ // no thumbnail, PDF or shorts of its own.
7
+ export function usesShortMode(draft) {
8
+ return draft.mode === "short";
9
+ }
10
+ // Said where each control sits, so a draft or a Save that breaks the mode names the fix.
11
+ export function shortModeFields(draft) {
12
+ if (!usesShortMode(draft))
13
+ return [];
14
+ const fields = [];
15
+ if (draft.format !== "9:16")
16
+ fields.push({
17
+ field: "format",
18
+ message: "A short is vertical. Choose the 9:16 format, or make a long video instead.",
19
+ });
20
+ if (sourceOf(draft.sources, "video") !== "generate")
21
+ fields.push({
22
+ field: "sources.video",
23
+ message: "A short is a video. Set Video to Generate.",
24
+ });
25
+ if (sourceOf(draft.sources, "audio") === "off")
26
+ fields.push({
27
+ field: "sources.audio",
28
+ message: "A short's length and captions come from its narration. Set Narration to Generate or Provide.",
29
+ });
30
+ if (sourceOf(draft.sources, "images") === "off")
31
+ fields.push({
32
+ field: "sources.images",
33
+ message: "A short shows images behind its captions. Set Images to Generate or Provide.",
34
+ });
35
+ for (const kind of ["thumbnail", "document"])
36
+ if (sourceOf(draft.sources, kind) !== "off")
37
+ fields.push({
38
+ field: `sources.${kind}`,
39
+ message: `A short has no ${kind === "thumbnail" ? "thumbnail" : "PDF"}. Set ${kind === "thumbnail" ? "Thumbnail" : "Document"} to Off.`,
40
+ });
41
+ if (draft.shorts?.enabled === true)
42
+ fields.push({
43
+ field: "shorts.enabled",
44
+ message: "A short isn't cut into more shorts. Turn Shorts off.",
45
+ });
46
+ return fields;
47
+ }
@@ -1,7 +1,10 @@
1
1
  import { transact } from "../../kernel/db/tx.js";
2
2
  import { stageKinds } from "../../kernel/pipeline.js";
3
3
  import { storeArticleText } from "../article/store.js";
4
+ import { resolveChannelId, setProjectChannel } from "../channels/repo.js";
4
5
  import { defaultDocumentTheme } from "../document/model.js";
6
+ import { earlierEpisodesFor } from "../episodes/related.js";
7
+ import { listNarrationAliases } from "../narration/aliases-library.js";
5
8
  import { collectSharedGlossary } from "../narration/shared-glossary.js";
6
9
  import { admitInitialRevision } from "../rebuild/runtime-admission.js";
7
10
  import { adoptBaseline } from "../revisions/adopt.js";
@@ -10,6 +13,7 @@ import { discardPreparedAssets } from "../storage/assets.js";
10
13
  import { prepareStagedFile } from "../storage/prepare.js";
11
14
  import { attachStagedFile, storeText } from "../storage/staging.js";
12
15
  import { releaseStagedFile } from "../storage/staging-refs.js";
16
+ import { usesAmbientBed } from "../video/ambient-bed.js";
13
17
  import { sourceOf, } from "./model.js";
14
18
  import { insertProject, insertStage } from "./repo.js";
15
19
  import { usesPronunciationGlossary, usesReference } from "./rules.js";
@@ -30,11 +34,19 @@ export function startRun(deps, draft, rendered, retainStaged = false, templates)
30
34
  const shared = usesPronunciationGlossary(draft) && draft.audio?.shareGlossary === true
31
35
  ? collectSharedGlossary(deps).entries
32
36
  : [];
33
- // The title may name keywords too ("D&D Lore: {{Topic}}"), filled like a prompt's, so a
37
+ // Library → Aliases as they are now: later edits there never change this project.
38
+ const aliases = draft.sources.audio === "generate" && draft.audio?.useNarrationAliases === true
39
+ ? listNarrationAliases(deps.db)
40
+ : [];
41
+ // The title may name keywords too ("History: {{Topic}}"), filled like a prompt's, so a
34
42
  // template or Play keeps the pattern and each project gets its own title.
35
43
  const title = render(draft.title, draft.values).trim() || draft.title;
44
+ // The channel's related earlier episodes as they are now (`slices/episodes`): later edits
45
+ // there never change this project. A draft never brings its own.
46
+ const { earlierEpisodes: _carried, ...fresh } = draft;
47
+ const earlier = earlierEpisodesFor(deps.db, draft, title);
36
48
  const config = {
37
- ...draft,
49
+ ...fresh,
38
50
  title,
39
51
  // A new project always names its document theme: a config without one reads as the
40
52
  // DiceMaster of older projects (see documentThemeOf), which no new project should get.
@@ -43,6 +55,8 @@ export function startRun(deps, draft, rendered, retainStaged = false, templates)
43
55
  : {}),
44
56
  rendered,
45
57
  ...(shared.length === 0 ? {} : { sharedGlossary: shared }),
58
+ ...(aliases.length === 0 ? {} : { narrationAliases: aliases }),
59
+ ...(earlier === undefined ? {} : { earlierEpisodes: earlier }),
46
60
  };
47
61
  const project = {
48
62
  id,
@@ -72,18 +86,25 @@ export function startRun(deps, draft, rendered, retainStaged = false, templates)
72
86
  // as the form left them and the same Play can simply be pressed again.
73
87
  const moved = [];
74
88
  let music;
89
+ let bed;
75
90
  try {
76
91
  transact(deps.db, () => {
77
92
  insertProject(deps.db, project);
93
+ setProjectChannel(deps.db, id, resolveChannelId(deps.db, draft.channelId));
78
94
  for (const stage of stages) {
79
95
  insertStage(deps.db, stage);
80
96
  }
81
97
  attachProvided(deps, id, draft, moved, retainStaged);
82
98
  music = attachShortsMusic(deps, id, draft, moved);
99
+ bed = attachAmbientBed(deps, id, draft, moved);
83
100
  // The music is named by the first revision, as Edit project → Shorts names it, so the
84
101
  // project gets its revision now even where it would otherwise wait for its first view.
85
- if (deps.catalogue !== undefined || music !== undefined) {
86
- const baseline = adoptBaseline(deps, id, templates, music === undefined ? {} : { shortsMusic: music.id });
102
+ // The ambient bed's own file is named the same way.
103
+ if (deps.catalogue !== undefined || music !== undefined || bed !== undefined) {
104
+ const baseline = adoptBaseline(deps, id, templates, {
105
+ ...(music === undefined ? {} : { shortsMusic: music.id }),
106
+ ...(bed === undefined ? {} : { ambientBed: bed.id }),
107
+ });
87
108
  if (!baseline.ok)
88
109
  throw new Error("The new project has no revision.");
89
110
  if (deps.catalogue !== undefined)
@@ -93,8 +114,9 @@ export function startRun(deps, draft, rendered, retainStaged = false, templates)
93
114
  }
94
115
  catch (error) {
95
116
  // The copy was written outside the transaction; nothing names it once it rolled back.
96
- if (music !== undefined)
97
- discardPreparedAssets(deps, [music]);
117
+ const written = [music, bed].flatMap((asset) => (asset === undefined ? [] : [asset]));
118
+ if (written.length > 0)
119
+ discardPreparedAssets(deps, written);
98
120
  throw error;
99
121
  }
100
122
  for (const source of retainStaged ? [] : moved) {
@@ -148,6 +170,20 @@ function attachShortsMusic(deps, projectId, draft, collected) {
148
170
  collected.push(stagedFileId);
149
171
  return result.asset;
150
172
  }
173
+ // The ambient bed's own file, copied into the project as an asset of its own like the shorts'
174
+ // music. Only while the long video gets the bed and its source is "upload".
175
+ function attachAmbientBed(deps, projectId, draft, collected) {
176
+ const stagedFileId = draft.provided.ambientBed;
177
+ if (draft.ambientBed?.source !== "upload" || !usesAmbientBed(draft) || stagedFileId === undefined)
178
+ return undefined;
179
+ const result = prepareStagedFile(deps, { projectId, stagedFileId, role: "audio_body" });
180
+ if (!result.ok)
181
+ // admit() already refused a missing or still-copying file, as for the stages' own files.
182
+ throw new Error(`the ambient sound's audio file could not be attached: ${result.reason}`);
183
+ insertAsset(deps.db, result.asset);
184
+ collected.push(stagedFileId);
185
+ return result.asset;
186
+ }
151
187
  function attach(deps, projectId, kind, stagedFileId, role, collected, retainStaged, index) {
152
188
  if (stagedFileId === undefined) {
153
189
  throw new Error(`the ${kind} stage is provided but carries no staged file`);
@@ -82,7 +82,7 @@ function truncated(answer) {
82
82
  // Empty response → failed attempt.
83
83
  function written(answer) {
84
84
  return answer.text.trim() === ""
85
- ? "The AI model returned an empty article. Retry stage; if it keeps happening, choose a different model in Edit project → Providers."
85
+ ? "The AI model returned an empty article. Try again; if it keeps happening, choose a different model in Edit project → Providers."
86
86
  : undefined;
87
87
  }
88
88
  function finished(answer) {
@@ -91,6 +91,6 @@ function finished(answer) {
91
91
  return empty;
92
92
  }
93
93
  return truncated(answer)
94
- ? `The article was still unfinished after ${String(continuationLimit)} continuations because the AI model kept stopping at its length limit. Ask for a shorter article in Edit project → Prompts, or choose a model with a larger output limit in Edit project → Providers, then Retry stage.`
94
+ ? `The article was still unfinished after ${String(continuationLimit)} continuations because the AI model kept stopping at its length limit. Ask for a shorter article in Edit project → Prompts, or choose a model with a larger output limit in Edit project → Providers, then Try again.`
95
95
  : undefined;
96
96
  }
@@ -20,11 +20,11 @@ export async function prepareProvidedArticleSegments(deps, audioContext, provide
20
20
  audioContext.signal.throwIfAborted();
21
21
  const article = stagesOf(deps.db, projectId).find((stage) => stage.kind === "article");
22
22
  if (!article)
23
- throw new Error("Slopify hit an internal error (the project has no article stage). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
23
+ throw new Error("Slopify hit an internal error (the project has no article stage). Try again; if it happens again, use Download diagnostics in Settings and report it.");
24
24
  const outputs = outputsOf(deps.db, projectId);
25
25
  const plain = outputs.find((output) => output.role === "article_txt");
26
26
  if (!plain)
27
- throw new Error("The article text you provided is missing, so the intro and outro can't be written from it. Paste the article again in Edit project → Article, then Retry stage.");
27
+ throw new Error("The article text you provided is missing, so the intro and outro can't be written from it. Paste the article again in Edit project → Article, then Try again.");
28
28
  const narration = readFileSync(outputPath(deps.paths, projectId, plain.path), "utf8");
29
29
  const saved = outputs.find((output) => output.stageKind === "article" && output.role === "instructions");
30
30
  let instructions = saved
@@ -3,6 +3,7 @@ import { z } from "zod";
3
3
  import { messageRoles } from "../../kernel/ports/llm.js";
4
4
  import { insertPiece, piecesOf, setPiece } from "../../kernel/runner/piece-repo.js";
5
5
  import { projectById } from "../admission/repo.js";
6
+ import { withEarlierEpisodes } from "../episodes/related.js";
6
7
  import { outputPath } from "../storage/layout.js";
7
8
  import { outputsOf } from "../storage/repo.js";
8
9
  import { storeText } from "../storage/staging.js";
@@ -20,10 +21,13 @@ export async function runArticle(deps, context, providers) {
20
21
  if (choice === undefined || articlePrompt === undefined) {
21
22
  // Admission refuses a run whose article is Generate without both, so
22
23
  // reaching here is a bug in admission rather than something the user did.
23
- throw new Error("No AI model or article prompt is set for writing the article. Choose a model in Edit project → Providers and check Edit project → Prompts, then Retry stage.");
24
+ throw new Error("No AI model or article prompt is set for writing the article. Choose a model in Edit project → Providers and check Edit project → Prompts, then Try again.");
24
25
  }
25
26
  const notes = researchNotes(deps, projectId);
26
- const brief = { articlePrompt, ...(notes === undefined ? {} : { notes }) };
27
+ const brief = {
28
+ articlePrompt: withEarlierEpisodes(articlePrompt, project.config.earlierEpisodes),
29
+ ...(notes === undefined ? {} : { notes }),
30
+ };
27
31
  const result = await articleBody(deps, context, providers, choice, brief);
28
32
  if (!result.ok)
29
33
  return "held";
@@ -7,12 +7,12 @@ export async function writeSegment(providers, choice, config, category, article,
7
7
  return { ok: true, value: undefined };
8
8
  const body = config.rendered[category];
9
9
  if (body === undefined)
10
- throw new Error(`Slopify hit an internal error (the ${category} text was never filled in). Retry stage; if it happens again, use Download diagnostics in Settings and report it.`);
10
+ throw new Error(`Slopify hit an internal error (the ${category} text was never filled in). Try again; if it happens again, use Download diagnostics in Settings and report it.`);
11
11
  const common = { category, name: picked.name, mode: picked.mode };
12
12
  if (picked.mode === "text")
13
13
  return { ok: true, value: { ...common, text: body, tokens: noTokens } };
14
14
  if (choice === undefined)
15
- throw new Error(`No AI model is set for writing the ${category}. Choose one in Edit project → Providers, then Retry stage.`);
15
+ throw new Error(`No AI model is set for writing the ${category}. Choose one in Edit project → Providers, then Try again.`);
16
16
  const messages = segmentMessages(body, config, article);
17
17
  sent.push({ label: category === "intro" ? "Intro" : "Outro", messages });
18
18
  const answer = await providers.llm({
@@ -21,7 +21,7 @@ export async function writeSegment(providers, choice, config, category, article,
21
21
  ...(choice.thinking === undefined ? {} : { thinking: choice.thinking }),
22
22
  messages,
23
23
  check: (given) => given.text.trim() === ""
24
- ? `The AI model returned an empty ${category}. Retry stage; if it keeps happening, choose a different model in Edit project → Providers.`
24
+ ? `The AI model returned an empty ${category}. Try again; if it keeps happening, choose a different model in Edit project → Providers.`
25
25
  : undefined,
26
26
  });
27
27
  if (!answer.ok)
@@ -23,13 +23,21 @@ export function folderProblem(paths, folder) {
23
23
  return "This folder is one Slopify uses for its own temporary files. Pick another folder, or leave Folder empty to use the default.";
24
24
  return undefined;
25
25
  }
26
- // Where the folder is on the user's computer. In Docker only the projects folder is shared
27
- // with the host, so a folder elsewhere lives in the container's private volume.
26
+ // Where the folder is on the user's computer. In Docker only the projects folder (and, from
27
+ // 3.0, the Backups folder) is shared with the host, so a folder elsewhere lives in the
28
+ // container's private volume.
28
29
  export function hostFolder(paths, location, folder) {
29
30
  if (!location.container)
30
31
  return folder;
31
- if (location.hostProjects === null || !within(paths.projects, folder))
32
- return null;
33
- const inside = relative(paths.projects, folder);
34
- return inside === "" ? location.hostProjects : join(location.hostProjects, inside);
32
+ const shared = [
33
+ [paths.projects, location.hostProjects],
34
+ [paths.backups, location.hostBackups],
35
+ ];
36
+ for (const [inContainer, onHost] of shared.toSorted((a, b) => b[0].length - a[0].length)) {
37
+ if (!onHost || !within(inContainer, folder))
38
+ continue;
39
+ const inside = relative(inContainer, folder);
40
+ return inside === "" ? onHost : join(onHost, inside);
41
+ }
42
+ return null;
35
43
  }
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { transact } from "../../kernel/db/tx.js";
3
3
  import { derive } from "../../kernel/runner/graph.js";
4
- import { projectPaused, stagesOf } from "../admission/repo.js";
4
+ import { liveProject, projectPaused, stagesOf } from "../admission/repo.js";
5
5
  import { startRun } from "../admission/start.js";
6
6
  import { deleteStagedFile } from "../storage/repo.js";
7
7
  import { releaseStagedFile } from "../storage/staging-refs.js";
@@ -11,10 +11,13 @@ const queueRow = z.object({
11
11
  position: z.number(),
12
12
  state: z.enum(["queued", "active", "finished"]),
13
13
  });
14
+ // A project in the trash leaves the queue's view, so the queue moves on past it; restored,
15
+ // it is back in its place.
14
16
  export function queueEntries(db, batchId) {
15
17
  return db
16
18
  .prepare(`SELECT project_id AS projectId, batch_id AS batchId, position, state
17
19
  FROM project_queue ${batchId === undefined ? "WHERE state != 'finished'" : "WHERE batch_id = ?"}
20
+ AND ${liveProject("project_queue", "project_id")}
18
21
  ORDER BY position`)
19
22
  .all(...(batchId === undefined ? [] : [batchId]))
20
23
  .map((row) => queueRow.parse(row));
@@ -50,6 +53,8 @@ export function enqueueBatch(deps, batchId, runs, retainStaged = false) {
50
53
  used.add(id);
51
54
  if (draft.shorts?.enabled === true && draft.provided.shortsMusic)
52
55
  used.add(draft.provided.shortsMusic);
56
+ if (draft.ambientBed?.source === "upload" && draft.provided.ambientBed)
57
+ used.add(draft.provided.ambientBed);
53
58
  }
54
59
  for (const id of retainStaged ? [] : used) {
55
60
  sources.add(id);
@@ -68,7 +73,7 @@ export function pumpQueue(db, runner) {
68
73
  const status = derive(stages, projectPaused(db, entry.projectId));
69
74
  if (runner.hasInflight?.(entry.projectId))
70
75
  return;
71
- if (["done", "failed", "canceled"].includes(status)) {
76
+ if (["done", "partial", "failed", "canceled"].includes(status)) {
72
77
  db.prepare("UPDATE project_queue SET state = 'finished' WHERE project_id = ?").run(entry.projectId);
73
78
  continue;
74
79
  }
@@ -53,7 +53,7 @@ async function cancel(deps, projectId) {
53
53
  if (paused)
54
54
  setProjectPaused(deps.db, projectId, false, at);
55
55
  deps.db
56
- .prepare("UPDATE revision_work SET state='canceled',dispatch_state='held',failure_reason=? WHERE project_id=? AND state!='done'")
56
+ .prepare("UPDATE revision_work SET state='canceled',dispatch_state='held',failure_reason=?,retry_at=NULL WHERE project_id=? AND state!='done'")
57
57
  .run(canceledByUser, projectId);
58
58
  deps.db
59
59
  .prepare("UPDATE revision_work_pieces SET state='held',dispatch_state='held' WHERE work_id IN (SELECT id FROM revision_work WHERE project_id=?) AND state!='done'")