@gentbajko/slopify 2.5.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -21
- package/NOTICE +12 -0
- package/README.md +21 -40
- package/SUBTITLES.md +1 -1
- package/dist/adapter-registry.js +3 -0
- package/dist/adapters/alignment/cache.js +4 -1
- package/dist/adapters/alignment/ctc.js +25 -23
- package/dist/adapters/alignment/index.js +37 -6
- package/dist/adapters/alignment/multilingual.js +267 -0
- package/dist/adapters/alignment/protocol.js +11 -0
- package/dist/adapters/alignment/quality.js +4 -3
- package/dist/adapters/alignment/sentences.js +95 -0
- package/dist/adapters/alignment/spec.js +24 -0
- package/dist/adapters/alignment/spell.js +769 -0
- package/dist/adapters/alignment/text.js +56 -9
- package/dist/adapters/alignment/window.js +34 -30
- package/dist/adapters/alignment/worker.js +23 -21
- package/dist/adapters/explain.js +11 -11
- package/dist/adapters/host-cli/index.js +44 -5
- package/dist/adapters/host-cli/transport.js +7 -5
- package/dist/adapters/image/bytes.js +2 -2
- package/dist/adapters/image/codex-output.js +1 -1
- package/dist/adapters/image/codex.js +87 -22
- package/dist/adapters/image/fal.js +13 -5
- package/dist/adapters/image/google.js +11 -8
- package/dist/adapters/image/openai.js +14 -7
- package/dist/adapters/image/reference.js +34 -0
- package/dist/adapters/image/replicate.js +7 -2
- package/dist/adapters/image/video.js +2 -2
- package/dist/adapters/key-probes.js +36 -0
- package/dist/adapters/llm/claude-code.js +168 -32
- package/dist/adapters/llm/cli-login-error.js +1 -1
- package/dist/adapters/llm/codex-limits.js +157 -0
- package/dist/adapters/llm/codex.js +99 -12
- package/dist/adapters/llm/document-workspace.js +2 -2
- package/dist/adapters/llm/gemini-workspace.js +1 -1
- package/dist/adapters/llm/gemini.js +16 -3
- package/dist/adapters/llm/image-workspace.js +58 -0
- package/dist/adapters/llm/openrouter.js +11 -3
- package/dist/adapters/llm/run-cli.js +12 -7
- package/dist/adapters/tts/cartesia.js +10 -0
- package/dist/adapters/tts/elevenlabs.js +31 -9
- package/dist/adapters/tts/inworld-async.js +9 -5
- package/dist/adapters/tts/inworld.js +17 -3
- package/dist/adapters/tts/openai.js +3 -0
- package/dist/adapters/tts/voice-languages.js +80 -0
- package/dist/assets/fonts/noto/NotoSans-Regular.ttf +0 -0
- package/dist/assets/fonts/noto/NotoSansArabic-Regular.ttf +0 -0
- package/dist/assets/fonts/noto/NotoSansDevanagari-Regular.ttf +0 -0
- package/dist/assets/fonts/noto/NotoSansHebrew-Regular.ttf +0 -0
- package/dist/assets/fonts/noto/NotoSansThai-Regular.ttf +0 -0
- package/dist/assets/fonts/noto/OFL.txt +93 -0
- package/dist/assets/fonts/noto/SOURCE.txt +18 -0
- package/dist/assets/sample/sample-audiobook.tar +0 -0
- package/dist/assets/sample/sample-podcast.tar +0 -0
- package/dist/assets/sample/sample-project.tar +0 -0
- package/dist/catalog/merge.js +132 -0
- package/dist/catalog/registry.js +13 -13
- package/dist/catalog/schema.js +2 -0
- package/dist/catalog/store.js +147 -8
- package/dist/compose.yaml +71 -0
- package/dist/edge/autostart/docker-record.js +32 -0
- package/dist/edge/autostart/docker.js +50 -0
- package/dist/edge/autostart/index.js +78 -0
- package/dist/edge/autostart/launcher.js +176 -0
- package/dist/edge/autostart/model.js +3 -0
- package/dist/edge/autostart/native.js +210 -0
- package/dist/edge/autostart/prompt.js +66 -0
- package/dist/edge/autostart/service.js +81 -0
- package/dist/edge/cli.js +57 -51
- package/dist/edge/docker-install/activation.js +56 -0
- package/dist/edge/docker-install/apply.js +458 -0
- package/dist/edge/docker-install/engine.js +362 -0
- package/dist/edge/docker-install/run.js +154 -0
- package/dist/edge/docker-install/state.js +146 -0
- package/dist/edge/{docker-projects → docker-install}/volume.js +1 -5
- package/dist/edge/docker.js +14 -22
- package/dist/edge/host-cli.js +1 -1
- package/dist/edge/http/actions.js +32 -10
- package/dist/edge/http/app.js +58 -2
- package/dist/edge/http/auditions.js +70 -0
- package/dist/edge/http/autostart.js +37 -0
- package/dist/edge/http/channel-memory.js +92 -0
- package/dist/edge/http/channels.js +128 -0
- package/dist/edge/http/entries.js +22 -3
- package/dist/edge/http/files.js +1 -1
- package/dist/edge/http/folder-location.js +1 -1
- package/dist/edge/http/home.js +39 -0
- package/dist/edge/http/host-cli.js +29 -5
- package/dist/edge/http/narration-peaks.js +76 -0
- package/dist/edge/http/onboarding.js +152 -0
- package/dist/edge/http/open-folder.js +1 -1
- package/dist/edge/http/patch-notes.js +45 -0
- package/dist/edge/http/planning.js +4 -2
- package/dist/edge/http/project-create.js +87 -0
- package/dist/edge/http/projects.js +31 -99
- package/dist/edge/http/prompts.js +40 -3
- package/dist/edge/http/pronunciations.js +23 -4
- package/dist/edge/http/providers.js +86 -0
- package/dist/edge/http/reviews.js +121 -0
- package/dist/edge/http/revision-files.js +1 -1
- package/dist/edge/http/revisions.js +1 -1
- package/dist/edge/http/run-cost.js +25 -0
- package/dist/edge/http/schedules.js +111 -6
- package/dist/edge/http/settings.js +84 -6
- package/dist/edge/http/storage-files.js +50 -0
- package/dist/edge/http/storage.js +29 -2
- package/dist/edge/http/studio.js +207 -0
- package/dist/edge/http/style-preview.js +100 -0
- package/dist/edge/http/subtitles.js +1 -1
- package/dist/edge/http/trash.js +69 -0
- package/dist/edge/http/update.js +24 -3
- package/dist/edge/http/whats-new.js +15 -0
- package/dist/edge/http/youtube-edits.js +83 -0
- package/dist/edge/native-update.js +62 -0
- package/dist/host-cli/open-folder.js +10 -10
- package/dist/host-cli/runtime.js +9 -1
- package/dist/kernel/db/migrations/0025-automatic-reviews.sql +40 -0
- package/dist/kernel/db/migrations/0026-schedule-topic-generation.sql +23 -0
- package/dist/kernel/db/migrations/0027-run-cost.sql +58 -0
- package/dist/kernel/db/migrations/0030-automatic-retries.sql +18 -0
- package/dist/kernel/db/migrations/0031-prompt-history-and-description-edits.sql +33 -0
- package/dist/kernel/db/migrations/0032-channels.sql +65 -0
- package/dist/kernel/db/migrations/0034-script-prompts.sql +23 -0
- package/dist/kernel/db/migrations/0035-narration-aliases.sql +13 -0
- package/dist/kernel/db/migrations/0037-project-uploads.sql +8 -0
- package/dist/kernel/db/migrations/0038-voice-languages.sql +7 -0
- package/dist/kernel/db/migrations/0039-channel-essentials.sql +64 -0
- package/dist/kernel/db/migrations/0040-standalone-usage.sql +35 -0
- package/dist/kernel/paths.js +27 -6
- package/dist/kernel/pipeline.js +4 -1
- package/dist/kernel/ports/host-cli.js +122 -14
- package/dist/kernel/ports/key-probe.js +1 -0
- package/dist/kernel/ports/languages.js +157 -0
- package/dist/kernel/ports/model.js +5 -2
- package/dist/kernel/ports/narration-aliases.js +60 -0
- package/dist/kernel/ports/plan-limits.js +36 -0
- package/dist/kernel/runner/attempt.js +17 -8
- package/dist/kernel/runner/audition.js +36 -0
- package/dist/kernel/runner/graph.js +24 -5
- package/dist/kernel/runner/index.js +35 -5
- package/dist/kernel/runner/meter.js +1 -0
- package/dist/kernel/runner/providers.js +142 -7
- package/dist/kernel/runner/retry-policy.js +43 -0
- package/dist/kernel/runner/standalone.js +134 -0
- package/dist/kernel/runner/work-authority.js +28 -2
- package/dist/main.js +272 -16
- package/dist/patch-notes/0.4.2-to-1.0.md +93 -0
- package/dist/patch-notes/1.0-to-2.0.md +74 -0
- package/dist/patch-notes/2.0-to-3.0.md +257 -0
- package/dist/patch-notes/3.0.0.md +70 -0
- package/dist/patch-notes/index.json +26 -0
- package/dist/slices/admission/model.js +27 -0
- package/dist/slices/admission/repo.js +24 -4
- package/dist/slices/admission/rules.js +79 -1
- package/dist/slices/admission/schema.js +58 -1
- package/dist/slices/admission/short-mode.js +47 -0
- package/dist/slices/admission/start.js +41 -5
- package/dist/slices/article/continuation.js +2 -2
- package/dist/slices/article/provided-entries.js +2 -2
- package/dist/slices/article/run.js +6 -2
- package/dist/slices/article/segments.js +3 -3
- package/dist/slices/backups/folder.js +14 -6
- package/dist/slices/batch/index.js +7 -2
- package/dist/slices/cancel/index.js +1 -1
- package/dist/slices/channels/cast-images.js +110 -0
- package/dist/slices/channels/cast-match.js +32 -0
- package/dist/slices/channels/images.js +19 -0
- package/dist/slices/channels/model.js +5 -0
- package/dist/slices/channels/rebrand.js +100 -0
- package/dist/slices/channels/repo.js +218 -0
- package/dist/slices/channels/runs.js +129 -0
- package/dist/slices/channels/schema.js +129 -0
- package/dist/slices/channels/service.js +180 -0
- package/dist/slices/channels/studio-csv.js +110 -0
- package/dist/slices/channels/videos.js +120 -0
- package/dist/slices/checkpoints/recovery.js +10 -0
- package/dist/slices/control/index.js +2 -1
- package/dist/slices/document/fonts.js +1 -1
- package/dist/slices/document/render.js +1 -1
- package/dist/slices/episodes/related.js +90 -0
- package/dist/slices/episodes/repo.js +60 -0
- package/dist/slices/episodes/service.js +58 -0
- package/dist/slices/episodes/summarize.js +170 -0
- package/dist/slices/estimate/index.js +109 -28
- package/dist/slices/estimate/requests.js +60 -2
- package/dist/slices/fixes/rules.js +73 -0
- package/dist/slices/fonts/coverage.js +82 -0
- package/dist/slices/fonts/sfnt.js +74 -0
- package/dist/slices/images/run.js +8 -5
- package/dist/slices/images/scale.js +84 -0
- package/dist/slices/library/diff.js +122 -0
- package/dist/slices/library/history.js +100 -0
- package/dist/slices/library/model.js +3 -0
- package/dist/slices/library/photorealistic.js +49 -0
- package/dist/slices/library/repo.js +19 -14
- package/dist/slices/library/save.js +62 -12
- package/dist/slices/library/slots.js +7 -1
- package/dist/slices/library/used-by.js +153 -0
- package/dist/slices/loudness/level-pieces.js +41 -0
- package/dist/slices/loudness/loudnorm.js +164 -0
- package/dist/slices/loudness/model.js +153 -0
- package/dist/slices/model-upkeep/model.js +10 -0
- package/dist/slices/model-upkeep/switch.js +217 -0
- package/dist/slices/model-upkeep/usage.js +183 -0
- package/dist/slices/narration/aliases-library.js +31 -0
- package/dist/slices/narration/aliases-schema.js +53 -0
- package/dist/slices/narration/aliases.js +49 -0
- package/dist/slices/narration/blocks.js +352 -0
- package/dist/slices/narration/chunk.js +19 -7
- package/dist/slices/narration/concat.js +2 -2
- package/dist/slices/narration/describe.js +45 -0
- package/dist/slices/narration/pauses-model.js +63 -0
- package/dist/slices/narration/pauses.js +269 -0
- package/dist/slices/narration/peaks-model.js +1 -0
- package/dist/slices/narration/peaks.js +94 -0
- package/dist/slices/narration/preparation.js +21 -3
- package/dist/slices/narration/pronunciation-chunks.js +3 -3
- package/dist/slices/narration/pronunciation.js +72 -25
- package/dist/slices/narration/run.js +6 -6
- package/dist/slices/narration/shared-glossary.js +1 -1
- package/dist/slices/narration/steering.js +15 -4
- package/dist/slices/notifications/notifier.js +21 -3
- package/dist/slices/notifications/rules.js +22 -1
- package/dist/slices/onboarding/copy.js +143 -0
- package/dist/slices/onboarding/first-run.js +46 -0
- package/dist/slices/onboarding/install.js +119 -0
- package/dist/slices/onboarding/model.js +64 -0
- package/dist/slices/onboarding/packs.js +491 -0
- package/dist/slices/onboarding/quick-short.js +129 -0
- package/dist/slices/onboarding/sample.js +132 -0
- package/dist/slices/onboarding/state.js +68 -0
- package/dist/slices/onboarding/template.js +73 -0
- package/dist/slices/patch-notes/library.js +73 -0
- package/dist/slices/patch-notes/seen.js +77 -0
- package/dist/slices/play-drafts/convert.js +125 -6
- package/dist/slices/play-drafts/readiness.js +3 -0
- package/dist/slices/play-drafts/repo.js +2 -0
- package/dist/slices/play-drafts/review-inputs.js +20 -3
- package/dist/slices/play-drafts/schema.js +89 -0
- package/dist/slices/play-drafts/service.js +4 -1
- package/dist/slices/play-drafts/start.js +43 -18
- package/dist/slices/project-templates/from-project.js +50 -4
- package/dist/slices/project-templates/one-off.js +26 -0
- package/dist/slices/project-templates/repo.js +2 -1
- package/dist/slices/project-templates/service.js +15 -4
- package/dist/slices/project-templates/setup.js +18 -2
- package/dist/slices/rebuild/admission-repo.js +1 -1
- package/dist/slices/rebuild/model.js +6 -0
- package/dist/slices/rebuild/preview-details.js +46 -3
- package/dist/slices/rebuild/preview-plan.js +13 -6
- package/dist/slices/rebuild/recipe-audio-parts.js +40 -7
- package/dist/slices/rebuild/recipe-audio.js +66 -12
- package/dist/slices/rebuild/recipe-build.js +8 -4
- package/dist/slices/rebuild/recipe-cast.js +18 -0
- package/dist/slices/rebuild/recipe-checkpoints.js +11 -9
- package/dist/slices/rebuild/recipe-describe.js +93 -0
- package/dist/slices/rebuild/recipe-edit.js +13 -1
- package/dist/slices/rebuild/recipe-exports.js +62 -8
- package/dist/slices/rebuild/recipe-input-schema.js +35 -0
- package/dist/slices/rebuild/recipe-legacy.js +9 -0
- package/dist/slices/rebuild/recipe-loudness.js +45 -0
- package/dist/slices/rebuild/recipe-model.js +25 -6
- package/dist/slices/rebuild/recipe-pauses.js +7 -0
- package/dist/slices/rebuild/recipe-preparation.js +3 -3
- package/dist/slices/rebuild/recipe-provider-choice.js +11 -1
- package/dist/slices/rebuild/recipe-reference.js +15 -1
- package/dist/slices/rebuild/recipe-reviews.js +117 -0
- package/dist/slices/rebuild/recipe-save.js +4 -1
- package/dist/slices/rebuild/recipe-shorts.js +23 -3
- package/dist/slices/rebuild/recipe-text.js +114 -19
- package/dist/slices/rebuild/recipe-validation.js +14 -2
- package/dist/slices/rebuild/recipe-visual.js +96 -33
- package/dist/slices/rebuild/recipe-voices.js +286 -0
- package/dist/slices/rebuild/recipe-work.js +77 -6
- package/dist/slices/rebuild/recipe-youtube.js +2 -0
- package/dist/slices/rebuild/recovery-conflict.js +7 -3
- package/dist/slices/rebuild/recovery-model.js +3 -0
- package/dist/slices/rebuild/recovery-selection.js +28 -2
- package/dist/slices/rebuild/recovery.js +35 -17
- package/dist/slices/rebuild/repo.js +3 -3
- package/dist/slices/rebuild/retry.js +18 -0
- package/dist/slices/rebuild/review-redo.js +110 -0
- package/dist/slices/rebuild/runtime-actions.js +5 -0
- package/dist/slices/rebuild/runtime-admission.js +4 -1
- package/dist/slices/rebuild/runtime-animate.js +4 -4
- package/dist/slices/rebuild/runtime-article.js +5 -5
- package/dist/slices/rebuild/runtime-document.js +4 -4
- package/dist/slices/rebuild/runtime-export-bed.js +43 -0
- package/dist/slices/rebuild/runtime-export-edit.js +70 -11
- package/dist/slices/rebuild/runtime-export-inputs.js +52 -10
- package/dist/slices/rebuild/runtime-export-short.js +85 -0
- package/dist/slices/rebuild/runtime-export.js +84 -19
- package/dist/slices/rebuild/runtime-figure-card.js +87 -0
- package/dist/slices/rebuild/runtime-image.js +21 -2
- package/dist/slices/rebuild/runtime-local.js +148 -24
- package/dist/slices/rebuild/runtime-materialize.js +8 -0
- package/dist/slices/rebuild/runtime-narration-reuse.js +2 -1
- package/dist/slices/rebuild/runtime-narration-text.js +7 -7
- package/dist/slices/rebuild/runtime-pictures.js +28 -0
- package/dist/slices/rebuild/runtime-piece-label.js +27 -0
- package/dist/slices/rebuild/runtime-provider.js +126 -18
- package/dist/slices/rebuild/runtime-publication.js +21 -5
- package/dist/slices/rebuild/runtime-review.js +237 -0
- package/dist/slices/rebuild/runtime-run.js +17 -11
- package/dist/slices/rebuild/runtime-shorts.js +44 -17
- package/dist/slices/rebuild/runtime-store.js +39 -10
- package/dist/slices/rebuild/runtime-subtitles.js +72 -19
- package/dist/slices/rebuild/runtime-voices.js +103 -0
- package/dist/slices/rebuild/runtime-youtube.js +7 -6
- package/dist/slices/rebuild/soften.js +49 -0
- package/dist/slices/rebuild/transition-repo.js +9 -2
- package/dist/slices/research/run.js +4 -4
- package/dist/slices/research/synthesis.js +2 -2
- package/dist/slices/reviews/model.js +105 -0
- package/dist/slices/reviews/outcome.js +22 -0
- package/dist/slices/reviews/repo.js +106 -0
- package/dist/slices/reviews/rules.js +63 -0
- package/dist/slices/reviews/verdict.js +71 -0
- package/dist/slices/reviews/view.js +30 -0
- package/dist/slices/revisions/adopt-content.js +5 -2
- package/dist/slices/revisions/adopt.js +3 -1
- package/dist/slices/revisions/downloads.js +7 -1
- package/dist/slices/revisions/image-plan.js +183 -0
- package/dist/slices/revisions/mutation-assets.js +10 -0
- package/dist/slices/revisions/mutation-cues.js +71 -0
- package/dist/slices/revisions/mutation-request.js +2 -1
- package/dist/slices/revisions/mutations.js +97 -3
- package/dist/slices/revisions/schema.js +11 -1
- package/dist/slices/run-cost/limits.js +128 -0
- package/dist/slices/run-cost/meter.js +47 -0
- package/dist/slices/run-cost/panel.js +234 -0
- package/dist/slices/run-cost/pricing.js +92 -0
- package/dist/slices/run-cost/week.js +108 -0
- package/dist/slices/schedules/agenda.js +158 -0
- package/dist/slices/schedules/repo.js +58 -18
- package/dist/slices/schedules/scheduler.js +67 -16
- package/dist/slices/schedules/schema.js +138 -1
- package/dist/slices/schedules/service.js +43 -4
- package/dist/slices/schedules/similar.js +90 -0
- package/dist/slices/schedules/topic-list.js +163 -0
- package/dist/slices/schedules/topics.js +422 -0
- package/dist/slices/settings/first-run.js +102 -0
- package/dist/slices/settings/health.js +190 -0
- package/dist/slices/settings/key-guides.js +128 -0
- package/dist/slices/settings/key-test.js +85 -0
- package/dist/slices/settings/model.js +6 -1
- package/dist/slices/settings/playback.js +15 -0
- package/dist/slices/settings/repo.js +17 -3
- package/dist/slices/settings/voices.js +64 -4
- package/dist/slices/settings/whats-new.js +64 -0
- package/dist/slices/shorts/captions.js +9 -0
- package/dist/slices/shorts/pick.js +3 -3
- package/dist/slices/shorts/prompts.js +1 -1
- package/dist/slices/shorts/render.js +54 -5
- package/dist/slices/storage/asset-name.js +3 -0
- package/dist/slices/storage/backup-export.js +48 -9
- package/dist/slices/storage/backup-format.js +38 -1
- package/dist/slices/storage/backup-import.js +191 -19
- package/dist/slices/storage/delete-project.js +7 -4
- package/dist/slices/storage/documents.js +79 -0
- package/dist/slices/storage/files-location.js +531 -0
- package/dist/slices/storage/layout.js +14 -4
- package/dist/slices/storage/model.js +11 -0
- package/dist/slices/storage/portable.js +92 -12
- package/dist/slices/storage/reconcile.js +14 -1
- package/dist/slices/storage/repo.js +4 -1
- package/dist/slices/storage/schema.js +11 -0
- package/dist/slices/storage/trim.js +146 -0
- package/dist/slices/studio/disclosure.js +74 -0
- package/dist/slices/studio/model.js +28 -0
- package/dist/slices/studio/pack.js +209 -0
- package/dist/slices/studio/settings.js +133 -0
- package/dist/slices/style-preview/images.js +61 -0
- package/dist/slices/style-preview/render.js +161 -0
- package/dist/slices/style-preview/schema.js +45 -0
- package/dist/slices/style-preview/service.js +93 -0
- package/dist/slices/style-preview/settings.js +89 -0
- package/dist/slices/subtitles/captions.js +42 -9
- package/dist/slices/subtitles/model.js +22 -0
- package/dist/slices/subtitles/prepare.js +27 -16
- package/dist/slices/subtitles/transcript.js +1 -1
- package/dist/slices/thumbnail/run.js +7 -7
- package/dist/slices/trash/model.js +25 -0
- package/dist/slices/trash/service.js +273 -0
- package/dist/slices/uploads/repo.js +20 -0
- package/dist/slices/video/ambient-bed-schema.js +15 -0
- package/dist/slices/video/ambient-bed.js +95 -0
- package/dist/slices/video/ambient-mix.js +72 -0
- package/dist/slices/video/audio-inputs.js +2 -2
- package/dist/slices/video/cards.js +5 -2
- package/dist/slices/video/edit-list.js +23 -2
- package/dist/slices/video/edit-settings.js +13 -3
- package/dist/slices/video/ffmpeg.js +63 -9
- package/dist/slices/video/figure-card.js +492 -0
- package/dist/slices/video/figure-spans.js +51 -0
- package/dist/slices/video/plan.js +125 -13
- package/dist/slices/video/run.js +14 -12
- package/dist/slices/video/slideshow.js +39 -4
- package/dist/slices/voices/attribution.js +69 -0
- package/dist/slices/voices/audio-files.js +82 -0
- package/dist/slices/voices/audition.js +25 -0
- package/dist/slices/voices/cast.js +56 -0
- package/dist/slices/voices/grouping.js +49 -0
- package/dist/slices/voices/join.js +35 -0
- package/dist/slices/voices/languages.js +25 -0
- package/dist/slices/voices/model.js +179 -0
- package/dist/slices/voices/palette.js +26 -0
- package/dist/slices/voices/panel.js +91 -0
- package/dist/slices/voices/portraits.js +55 -0
- package/dist/slices/voices/script.js +136 -0
- package/dist/slices/voices/timing.js +58 -0
- package/dist/slices/youtube/answer.js +1 -1
- package/dist/slices/youtube/chapters.js +126 -0
- package/dist/slices/youtube/edits-repo.js +69 -0
- package/dist/slices/youtube/edits.js +89 -0
- package/dist/slices/youtube/placeholders.js +98 -0
- package/dist/slices/youtube/transcript.js +11 -4
- package/dist/updater/service.js +66 -21
- package/dist/web/assets/index-CUA2sUxU.css +1 -0
- package/dist/web/assets/index-CX590kE-.js +303 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
- package/dist/web/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
- package/dist/web/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
- package/dist/web/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
- package/dist/web/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
- package/dist/web/assets/{pdf-BLF2i8c2.js → pdf-BG8n_uc5.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +4 -4
- package/dist/edge/docker-launch.js +0 -81
- package/dist/edge/docker-projects/activation.js +0 -35
- package/dist/edge/docker-projects/claims.js +0 -85
- package/dist/edge/docker-projects/committed.js +0 -40
- package/dist/edge/docker-projects/engine.js +0 -461
- package/dist/edge/docker-projects/install.js +0 -254
- package/dist/edge/docker-projects/prune.js +0 -66
- package/dist/edge/docker-projects/recover.js +0 -100
- package/dist/edge/docker-projects/state.js +0 -225
- package/dist/web/assets/index-CktpxlJd.js +0 -171
- package/dist/web/assets/index-DB-vOnGU.css +0 -1
- package/scripts/docker-run.sh +0 -3
- /package/dist/edge/{docker-projects → docker-install}/tree.js +0 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
import { transact } from "../../kernel/db/tx.js";
|
|
3
4
|
import { derive } from "../../kernel/runner/graph.js";
|
|
4
|
-
import { projectPaused, stagesOf } from "../admission/repo.js";
|
|
5
|
+
import { projectExists, projectPaused, stagesOf } from "../admission/repo.js";
|
|
5
6
|
import { readStartReceipt } from "../play-drafts/start-repo.js";
|
|
6
7
|
import { scheduleRunSchema, scheduleSummarySchema, } from "./schema.js";
|
|
7
8
|
const rowSchema = z.object({
|
|
@@ -23,30 +24,39 @@ const rowSchema = z.object({
|
|
|
23
24
|
created_at: z.string(),
|
|
24
25
|
updated_at: z.string(),
|
|
25
26
|
deleted_at: z.string().nullable(),
|
|
27
|
+
brief: z.string().nullable(),
|
|
28
|
+
topic_mode: z.string(),
|
|
29
|
+
topic_min: z.number(),
|
|
30
|
+
topic_llm_json: z.string().nullable(),
|
|
31
|
+
topics_generating_at: z.string().nullable(),
|
|
32
|
+
topics_generated_at: z.string().nullable(),
|
|
33
|
+
topics_failed_at: z.string().nullable(),
|
|
34
|
+
topics_error: z.string().nullable(),
|
|
35
|
+
held_topics: z.number(),
|
|
26
36
|
});
|
|
37
|
+
const scheduleColumns = `id,name,template_id,template_version,cadence_json,timezone,missed_policy,
|
|
38
|
+
overlap_policy,spend_limit_cents,items_json,topic_keyword,values_json,status,version,next_run_at,
|
|
39
|
+
created_at,updated_at,deleted_at,brief,topic_mode,topic_min,topic_llm_json,topics_generating_at,
|
|
40
|
+
topics_generated_at,topics_failed_at,topics_error,
|
|
41
|
+
(SELECT count(*) FROM schedule_topics AS held
|
|
42
|
+
WHERE held.schedule_id=schedules.id AND held.state='held') AS held_topics`;
|
|
27
43
|
export function scheduleById(db, id) {
|
|
28
44
|
const row = db
|
|
29
|
-
.prepare(`SELECT
|
|
30
|
-
overlap_policy,spend_limit_cents,items_json,topic_keyword,values_json,status,version,next_run_at,created_at,updated_at,
|
|
31
|
-
deleted_at
|
|
45
|
+
.prepare(`SELECT ${scheduleColumns}
|
|
32
46
|
FROM schedules WHERE id=?`)
|
|
33
47
|
.get(id);
|
|
34
48
|
return row === undefined ? undefined : parseSchedule(row);
|
|
35
49
|
}
|
|
36
50
|
export function scheduleRows(db) {
|
|
37
51
|
return db
|
|
38
|
-
.prepare(`SELECT
|
|
39
|
-
overlap_policy,spend_limit_cents,items_json,topic_keyword,values_json,status,version,next_run_at,created_at,updated_at,
|
|
40
|
-
deleted_at
|
|
52
|
+
.prepare(`SELECT ${scheduleColumns}
|
|
41
53
|
FROM schedules ORDER BY created_at DESC,id`)
|
|
42
54
|
.all()
|
|
43
55
|
.map(parseSchedule);
|
|
44
56
|
}
|
|
45
57
|
export function dueSchedules(db, now) {
|
|
46
58
|
return db
|
|
47
|
-
.prepare(`SELECT
|
|
48
|
-
overlap_policy,spend_limit_cents,items_json,topic_keyword,values_json,status,version,next_run_at,created_at,updated_at,
|
|
49
|
-
deleted_at
|
|
59
|
+
.prepare(`SELECT ${scheduleColumns}
|
|
50
60
|
FROM schedules WHERE deleted_at IS NULL AND status='active' AND next_run_at IS NOT NULL
|
|
51
61
|
AND next_run_at <= ?
|
|
52
62
|
ORDER BY next_run_at,id`)
|
|
@@ -57,17 +67,27 @@ export function insertSchedule(db, schedule, creationHash) {
|
|
|
57
67
|
db.prepare(`INSERT INTO schedules
|
|
58
68
|
(id,name,template_id,template_version,cadence_json,timezone,missed_policy,overlap_policy,
|
|
59
69
|
spend_limit_cents,items_json,topic_keyword,values_json,status,version,creation_hash,
|
|
60
|
-
next_run_at,created_at,updated_at,deleted_at)
|
|
61
|
-
VALUES (
|
|
70
|
+
next_run_at,created_at,updated_at,deleted_at,brief,topic_mode,topic_min,topic_llm_json)
|
|
71
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`).run(schedule.id, schedule.name, schedule.templateId, schedule.templateVersion, JSON.stringify(schedule.cadence), schedule.timezone, schedule.missedPolicy, schedule.overlapPolicy, schedule.spendLimitCents, JSON.stringify(schedule.items), schedule.topicKeyword, JSON.stringify(schedule.values), schedule.status, schedule.version, creationHash, schedule.nextRunAt, schedule.createdAt, schedule.updatedAt, schedule.deletedAt, ...generationColumns(schedule));
|
|
72
|
+
}
|
|
73
|
+
function generationColumns(schedule) {
|
|
74
|
+
const generation = schedule.topicGeneration;
|
|
75
|
+
return [
|
|
76
|
+
schedule.brief,
|
|
77
|
+
generation.mode,
|
|
78
|
+
generation.keepAtLeast,
|
|
79
|
+
generation.llm === null ? null : JSON.stringify(generation.llm),
|
|
80
|
+
];
|
|
62
81
|
}
|
|
63
82
|
export function updateScheduleRow(db, schedule, mutationId, mutationHash, baseVersion) {
|
|
64
83
|
const changed = db
|
|
65
84
|
.prepare(`UPDATE schedules SET name=?,template_id=?,template_version=?,cadence_json=?,timezone=?,
|
|
66
85
|
missed_policy=?,overlap_policy=?,spend_limit_cents=?,items_json=?,topic_keyword=?,
|
|
67
86
|
values_json=?,status=?,version=?,
|
|
68
|
-
next_run_at=?,updated_at=?,mutation_id=?,mutation_hash
|
|
87
|
+
next_run_at=?,updated_at=?,mutation_id=?,mutation_hash=?,
|
|
88
|
+
brief=?,topic_mode=?,topic_min=?,topic_llm_json=?,topics_failed_at=NULL,topics_error=NULL
|
|
69
89
|
WHERE id=? AND version=? AND deleted_at IS NULL`)
|
|
70
|
-
.run(schedule.name, schedule.templateId, schedule.templateVersion, JSON.stringify(schedule.cadence), schedule.timezone, schedule.missedPolicy, schedule.overlapPolicy, schedule.spendLimitCents, JSON.stringify(schedule.items), schedule.topicKeyword, JSON.stringify(schedule.values), schedule.status, schedule.version, schedule.nextRunAt, schedule.updatedAt, mutationId, mutationHash, schedule.id, baseVersion);
|
|
90
|
+
.run(schedule.name, schedule.templateId, schedule.templateVersion, JSON.stringify(schedule.cadence), schedule.timezone, schedule.missedPolicy, schedule.overlapPolicy, schedule.spendLimitCents, JSON.stringify(schedule.items), schedule.topicKeyword, JSON.stringify(schedule.values), schedule.status, schedule.version, schedule.nextRunAt, schedule.updatedAt, mutationId, mutationHash, ...generationColumns(schedule), schedule.id, baseVersion);
|
|
71
91
|
return Number(changed.changes) === 1;
|
|
72
92
|
}
|
|
73
93
|
export function tombstoneScheduleRow(db, id, version, deletedAt) {
|
|
@@ -138,7 +158,13 @@ function consumeTopic(db, scheduleId, used, updatedAt) {
|
|
|
138
158
|
if (at === -1)
|
|
139
159
|
return;
|
|
140
160
|
const items = schedule.items.filter((_item, index) => index !== at);
|
|
141
|
-
|
|
161
|
+
// A schedule that finds its own topics keeps going when its queue empties.
|
|
162
|
+
const done = items.length === 0 &&
|
|
163
|
+
schedule.topicGeneration.mode === "off" &&
|
|
164
|
+
(schedule.status === "active" || schedule.status === "paused");
|
|
165
|
+
// Remembered so topic generation never suggests it again.
|
|
166
|
+
db.prepare(`INSERT INTO schedule_topics (id,schedule_id,title,state,rank,created_at,decided_at)
|
|
167
|
+
VALUES (?,?,?,'used',0,?,?)`).run(randomUUID(), scheduleId, used.title, updatedAt, updatedAt);
|
|
142
168
|
db.prepare(`UPDATE schedules SET items_json=?,status=?,next_run_at=?,version=version+1,updated_at=?
|
|
143
169
|
WHERE id=? AND deleted_at IS NULL`).run(JSON.stringify(items), done ? "completed" : schedule.status, done ? null : schedule.nextRunAt, updatedAt, scheduleId);
|
|
144
170
|
}
|
|
@@ -217,10 +243,11 @@ export function settleTerminalScheduleRuns(db, settledAt) {
|
|
|
217
243
|
});
|
|
218
244
|
}
|
|
219
245
|
function projectIsActive(db, projectId) {
|
|
220
|
-
|
|
246
|
+
// A project in the trash takes no part in a schedule's overlap: it cannot run there.
|
|
247
|
+
if (!projectExists(db, projectId))
|
|
221
248
|
return false;
|
|
222
249
|
const state = derive(stagesOf(db, projectId), projectPaused(db, projectId));
|
|
223
|
-
return state !== "done" && state !== "failed" && state !== "canceled";
|
|
250
|
+
return state !== "done" && state !== "partial" && state !== "failed" && state !== "canceled";
|
|
224
251
|
}
|
|
225
252
|
export function recoverRunningRuns(db, endedAt, error = "Slopify was closed while this run was in progress. Check its video in Projects; the next run will go ahead as planned.") {
|
|
226
253
|
return transact(db, () => {
|
|
@@ -267,6 +294,19 @@ function parseSchedule(row) {
|
|
|
267
294
|
items: JSON.parse(value.items_json),
|
|
268
295
|
topicKeyword: value.topic_keyword,
|
|
269
296
|
values: JSON.parse(value.values_json),
|
|
297
|
+
brief: value.brief,
|
|
298
|
+
topicGeneration: {
|
|
299
|
+
mode: value.topic_mode,
|
|
300
|
+
keepAtLeast: value.topic_min,
|
|
301
|
+
llm: value.topic_llm_json === null ? null : JSON.parse(value.topic_llm_json),
|
|
302
|
+
},
|
|
303
|
+
topics: {
|
|
304
|
+
held: value.held_topics,
|
|
305
|
+
generatingSince: value.topics_generating_at,
|
|
306
|
+
generatedAt: value.topics_generated_at,
|
|
307
|
+
failedAt: value.topics_failed_at,
|
|
308
|
+
error: value.topics_error,
|
|
309
|
+
},
|
|
270
310
|
status: value.status,
|
|
271
311
|
version: value.version,
|
|
272
312
|
nextRunAt: value.next_run_at,
|
|
@@ -308,7 +348,7 @@ function parseRun(row) {
|
|
|
308
348
|
const internalRunError = "Slopify hit an internal error while starting this run. The next run will try again; if it keeps happening, use Download diagnostics in Settings and report it.";
|
|
309
349
|
const runErrors = {
|
|
310
350
|
"missing-template": "The template this schedule uses was deleted or changed. Edit the schedule and choose a template again.",
|
|
311
|
-
"unsupported-media": "This template uses audio, images, a thumbnail
|
|
351
|
+
"unsupported-media": "This template uses audio, images, a thumbnail, shorts background music or an ambient sound file you supplied, which scheduled runs cannot use. Pick a template that generates these instead, or on Play remove the music under Outputs → Export → More shorts options (or pick Rain, Fireplace or Wind under Outputs → Export → Ambient sound) and save the template again.",
|
|
312
352
|
"spend-limit": "Not started: the estimated cost was above this schedule's spend limit, or some prices were unknown. Raise the spend limit or choose models with known prices.",
|
|
313
353
|
readiness: "Not started: a provider was not ready, for example a missing API key, model or voice. Check Settings → Providers; the next run will try again.",
|
|
314
354
|
"not-found": "The template's settings are no longer valid, for example a prompt or voice it uses was deleted. Fix the template in Library > Templates and save it.",
|
|
@@ -1,21 +1,62 @@
|
|
|
1
1
|
import { transact } from "../../kernel/db/tx.js";
|
|
2
|
-
import { render } from "../admission/substitute.js";
|
|
3
2
|
import { reviewDraft } from "../play-drafts/review.js";
|
|
4
3
|
import { createDraft } from "../play-drafts/service.js";
|
|
5
4
|
import { startPlayDraft } from "../play-drafts/start.js";
|
|
6
5
|
import { freshTemplateDraft } from "../project-templates/setup.js";
|
|
7
6
|
import { nextOccurrence } from "./calendar.js";
|
|
8
|
-
import { activeRun, dueSchedules, insertRun, recordRunDispatch, recordRunStartIdentity, recoverRunningRuns, scheduleById, updateRun, } from "./repo.js";
|
|
7
|
+
import { activeRun, dueSchedules, insertRun, recordRunDispatch, recordRunStartIdentity, recoverRunningRuns, scheduleById, scheduleRows, updateRun, } from "./repo.js";
|
|
8
|
+
import { renderedTitle, scheduledValues } from "./topic-list.js";
|
|
9
|
+
import { generateTopics, generationDue, releaseTopicLeases } from "./topics.js";
|
|
9
10
|
const missedGraceMs = 60_000;
|
|
10
11
|
export function createScheduleRunner(deps) {
|
|
11
12
|
let ticking = false;
|
|
13
|
+
// ceiling: one entry per schedule generating now.
|
|
14
|
+
const generations = new Set();
|
|
15
|
+
const stopping = new AbortController();
|
|
16
|
+
const generated = async () => {
|
|
17
|
+
while (generations.size > 0)
|
|
18
|
+
await Promise.allSettled([...generations]);
|
|
19
|
+
};
|
|
20
|
+
// Topic generation can take minutes, so a tick starts it and moves on: another schedule's
|
|
21
|
+
// run is never late because of it. The lease inside `generateTopics` is what keeps it to
|
|
22
|
+
// one at a time per schedule.
|
|
23
|
+
const start = (scheduleId, force) => {
|
|
24
|
+
if (stopping.signal.aborted)
|
|
25
|
+
return;
|
|
26
|
+
const running = generateTopics(deps, scheduleId, { signal: stopping.signal, force })
|
|
27
|
+
.then((result) => {
|
|
28
|
+
if (!result.ok && result.reason === "failed")
|
|
29
|
+
deps.log.write("warn", "schedule.topics", { detail: result.message });
|
|
30
|
+
})
|
|
31
|
+
.catch(() => {
|
|
32
|
+
deps.log.write("error", "schedule.topics", {
|
|
33
|
+
detail: "Topic generation could not record its result.",
|
|
34
|
+
});
|
|
35
|
+
})
|
|
36
|
+
.finally(() => {
|
|
37
|
+
generations.delete(running);
|
|
38
|
+
});
|
|
39
|
+
generations.add(running);
|
|
40
|
+
};
|
|
41
|
+
const startGenerations = (now) => {
|
|
42
|
+
for (const schedule of scheduleRows(deps.db))
|
|
43
|
+
if (generationDue(schedule, now))
|
|
44
|
+
start(schedule.id, false);
|
|
45
|
+
};
|
|
12
46
|
return {
|
|
47
|
+
generated,
|
|
48
|
+
requestTopics: (scheduleId) => start(scheduleId, true),
|
|
49
|
+
stop: async () => {
|
|
50
|
+
stopping.abort();
|
|
51
|
+
await generated();
|
|
52
|
+
},
|
|
13
53
|
tick: async (now = deps.clock.now()) => {
|
|
14
54
|
if (ticking)
|
|
15
55
|
return;
|
|
16
56
|
ticking = true;
|
|
17
57
|
try {
|
|
18
58
|
recoverRunningRuns(deps.db, now.toISOString(), "The previous schedule tick could not record this run's result.");
|
|
59
|
+
startGenerations(now);
|
|
19
60
|
const due = dueSchedules(deps.db, now.toISOString());
|
|
20
61
|
const claimed = due.flatMap((schedule) => claim(deps, schedule, now));
|
|
21
62
|
const settled = await Promise.allSettled(claimed.map((entry) => execute(deps, entry)));
|
|
@@ -27,7 +68,10 @@ export function createScheduleRunner(deps) {
|
|
|
27
68
|
ticking = false;
|
|
28
69
|
}
|
|
29
70
|
},
|
|
30
|
-
recover: (now = deps.clock.now()) =>
|
|
71
|
+
recover: (now = deps.clock.now()) => {
|
|
72
|
+
releaseTopicLeases(deps);
|
|
73
|
+
return recoverRunningRuns(deps.db, now.toISOString());
|
|
74
|
+
},
|
|
31
75
|
};
|
|
32
76
|
}
|
|
33
77
|
function claim(deps, schedule, now) {
|
|
@@ -47,7 +91,11 @@ function claim(deps, schedule, now) {
|
|
|
47
91
|
current.status !== "active")
|
|
48
92
|
return [];
|
|
49
93
|
const overlapping = activeRun(deps.db, schedule.id, now.toISOString());
|
|
50
|
-
|
|
94
|
+
// A schedule that finds its own topics never runs the template without one.
|
|
95
|
+
const noTopic = current.topicGeneration.mode !== "off" && current.items.length === 0
|
|
96
|
+
? noTopicReason(current)
|
|
97
|
+
: null;
|
|
98
|
+
const skip = occurrence || overlapping || noTopic !== null;
|
|
51
99
|
const run = {
|
|
52
100
|
id: deps.uuid(),
|
|
53
101
|
scheduleId: schedule.id,
|
|
@@ -61,7 +109,9 @@ function claim(deps, schedule, now) {
|
|
|
61
109
|
error: skip
|
|
62
110
|
? overlapping
|
|
63
111
|
? "Skipped because the previous run of this schedule was still going."
|
|
64
|
-
:
|
|
112
|
+
: occurrence
|
|
113
|
+
? "Skipped because Slopify was not running at the scheduled time."
|
|
114
|
+
: noTopic
|
|
65
115
|
: null,
|
|
66
116
|
};
|
|
67
117
|
if (!insertRun(deps.db, run))
|
|
@@ -72,6 +122,14 @@ function claim(deps, schedule, now) {
|
|
|
72
122
|
return skip ? [] : [{ run, schedule }];
|
|
73
123
|
});
|
|
74
124
|
}
|
|
125
|
+
function noTopicReason(schedule) {
|
|
126
|
+
const { held, error } = schedule.topics;
|
|
127
|
+
if (held > 0)
|
|
128
|
+
return `Skipped because no topic was approved: ${String(held)} ${held === 1 ? "topic is" : "topics are"} waiting for you. Approve them under Schedules → ${schedule.name} → Topics waiting.`;
|
|
129
|
+
if (error !== null)
|
|
130
|
+
return `Skipped because the topic queue was empty. ${error}`;
|
|
131
|
+
return "Skipped because the topic queue was empty and new topics were still being generated. The next run uses the first one.";
|
|
132
|
+
}
|
|
75
133
|
async function execute(deps, claimed) {
|
|
76
134
|
let run = claimed.run;
|
|
77
135
|
try {
|
|
@@ -139,18 +197,11 @@ async function execute(deps, claimed) {
|
|
|
139
197
|
// same keywords, so "D&D Lore: {{Topic}}" names the project after the topic. A topic saved
|
|
140
198
|
// before topics had a keyword brings its own title and values instead.
|
|
141
199
|
function runForm(form, schedule, topic) {
|
|
142
|
-
|
|
143
|
-
...form
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
...(topic !== undefined && schedule.topicKeyword !== null
|
|
147
|
-
? { [schedule.topicKeyword]: topic.title }
|
|
148
|
-
: {}),
|
|
200
|
+
return {
|
|
201
|
+
...form,
|
|
202
|
+
title: renderedTitle(form, schedule, topic),
|
|
203
|
+
values: scheduledValues(form, schedule, topic),
|
|
149
204
|
};
|
|
150
|
-
const title = topic !== undefined && schedule.topicKeyword === null
|
|
151
|
-
? topic.title
|
|
152
|
-
: render(form.title, values);
|
|
153
|
-
return { ...form, title, values };
|
|
154
205
|
}
|
|
155
206
|
class ScheduleDispatchError extends Error {
|
|
156
207
|
reason;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { projectStates } from "../../kernel/pipeline.js";
|
|
3
|
+
import { thinkingModes } from "../../kernel/ports/llm.js";
|
|
2
4
|
import { cadenceSchema, validTimeZone } from "./calendar.js";
|
|
3
5
|
const id = z.uuid();
|
|
4
6
|
const keywordName = z
|
|
@@ -9,14 +11,58 @@ const keywordName = z
|
|
|
9
11
|
const values = z.record(keywordName, z.string().max(10000)).readonly();
|
|
10
12
|
// A queued topic. Each run takes the first one and removes it once its project starts.
|
|
11
13
|
// `values` is only filled on schedules saved before topics had one keyword.
|
|
14
|
+
export const topicTitleSchema = z.string().trim().min(1).max(200);
|
|
12
15
|
const item = z
|
|
13
16
|
.object({
|
|
14
|
-
title:
|
|
17
|
+
title: topicTitleSchema,
|
|
15
18
|
values,
|
|
16
19
|
})
|
|
17
20
|
.strict()
|
|
18
21
|
.readonly();
|
|
19
22
|
const policy = z.enum(["skip", "run-once"]);
|
|
23
|
+
export const topicModes = ["off", "queue", "hold"];
|
|
24
|
+
export const topicMinDefault = 10;
|
|
25
|
+
export const briefMax = 4000;
|
|
26
|
+
// Whether a schedule asks an LLM for topics when its queue runs low, and whether they join the
|
|
27
|
+
// queue at once or wait for approval. `llm` is null for the template's own LLM.
|
|
28
|
+
export const topicGenerationSchema = z
|
|
29
|
+
.object({
|
|
30
|
+
mode: z.enum(topicModes),
|
|
31
|
+
keepAtLeast: z.number().int().min(1).max(100),
|
|
32
|
+
llm: z
|
|
33
|
+
.object({
|
|
34
|
+
provider: z.string().min(1).max(100),
|
|
35
|
+
model: z.string().min(1).max(200),
|
|
36
|
+
thinking: z.enum(thinkingModes).optional(),
|
|
37
|
+
})
|
|
38
|
+
.strict()
|
|
39
|
+
.readonly()
|
|
40
|
+
.nullable(),
|
|
41
|
+
})
|
|
42
|
+
.strict()
|
|
43
|
+
.readonly();
|
|
44
|
+
export const topicGenerationOff = {
|
|
45
|
+
mode: "off",
|
|
46
|
+
keepAtLeast: topicMinDefault,
|
|
47
|
+
llm: null,
|
|
48
|
+
};
|
|
49
|
+
const topicState = z
|
|
50
|
+
.object({
|
|
51
|
+
held: z.number().int().nonnegative(),
|
|
52
|
+
generatingSince: z.string().nullable(),
|
|
53
|
+
generatedAt: z.string().nullable(),
|
|
54
|
+
failedAt: z.string().nullable(),
|
|
55
|
+
error: z.string().nullable(),
|
|
56
|
+
})
|
|
57
|
+
.strict()
|
|
58
|
+
.readonly();
|
|
59
|
+
export const topicStateIdle = {
|
|
60
|
+
held: 0,
|
|
61
|
+
generatingSince: null,
|
|
62
|
+
generatedAt: null,
|
|
63
|
+
failedAt: null,
|
|
64
|
+
error: null,
|
|
65
|
+
};
|
|
20
66
|
// Enough for a year of daily runs, pasted as one list.
|
|
21
67
|
export const queueMax = 500;
|
|
22
68
|
export const scheduleCreateSchema = z
|
|
@@ -39,6 +85,10 @@ export const scheduleCreateSchema = z
|
|
|
39
85
|
topicKeyword: keywordName.nullable().default(null),
|
|
40
86
|
// Keywords every run uses as they are, such as the word counts.
|
|
41
87
|
values: values.default({}),
|
|
88
|
+
// What the channel covers, its style and what makes a topic worth watching. Only topic
|
|
89
|
+
// generation reads it.
|
|
90
|
+
brief: z.string().trim().max(briefMax).nullable().default(null),
|
|
91
|
+
topicGeneration: topicGenerationSchema.default(topicGenerationOff),
|
|
42
92
|
})
|
|
43
93
|
.strict()
|
|
44
94
|
.readonly();
|
|
@@ -69,6 +119,11 @@ export const scheduleSummarySchema = z
|
|
|
69
119
|
items: z.array(item).readonly(),
|
|
70
120
|
topicKeyword: z.string().nullable().default(null),
|
|
71
121
|
values: values.default({}),
|
|
122
|
+
brief: z.string().nullable().default(null),
|
|
123
|
+
topicGeneration: topicGenerationSchema.default(topicGenerationOff),
|
|
124
|
+
// The generation's own state: how many topics wait for approval, whether an LLM is being
|
|
125
|
+
// asked now, and the last failure's plain reason (cleared by the next success).
|
|
126
|
+
topics: topicState.default(topicStateIdle),
|
|
72
127
|
status: z.enum(["active", "paused", "completed", "canceled"]),
|
|
73
128
|
version: z.number().int().positive(),
|
|
74
129
|
nextRunAt: z.string().datetime({ offset: true }).nullable(),
|
|
@@ -93,3 +148,85 @@ export const scheduleRunSchema = z
|
|
|
93
148
|
})
|
|
94
149
|
.strict()
|
|
95
150
|
.readonly();
|
|
151
|
+
export const heldTopicSchema = z
|
|
152
|
+
.object({ id, title: z.string(), rank: z.number().int(), createdAt: z.string() })
|
|
153
|
+
.strict()
|
|
154
|
+
.readonly();
|
|
155
|
+
// Reordering is by position against the version the person saw, so a run taking the first
|
|
156
|
+
// topic meanwhile makes the move refuse instead of moving the wrong one.
|
|
157
|
+
export const topicMoveSchema = z
|
|
158
|
+
.object({
|
|
159
|
+
baseVersion: z.number().int().positive(),
|
|
160
|
+
from: z.number().int().nonnegative(),
|
|
161
|
+
to: z.number().int().nonnegative(),
|
|
162
|
+
})
|
|
163
|
+
.strict()
|
|
164
|
+
.readonly();
|
|
165
|
+
// The calendar: what will run, what is running or finished, and what waits in the batch queue.
|
|
166
|
+
// `topicSource` says where a run's topic comes from: the queue (`topic` names it), topics held
|
|
167
|
+
// for approval, a generation still to come, or none (the template as saved).
|
|
168
|
+
export const calendarRunSchema = z
|
|
169
|
+
.object({
|
|
170
|
+
at: z.string(),
|
|
171
|
+
scheduleId: id,
|
|
172
|
+
scheduleName: z.string(),
|
|
173
|
+
scheduleVersion: z.number().int().positive(),
|
|
174
|
+
paused: z.boolean(),
|
|
175
|
+
templateId: id,
|
|
176
|
+
templateVersion: z.number().int().positive(),
|
|
177
|
+
templateName: z.string().nullable(),
|
|
178
|
+
// The topic's position in the schedule's queue, for reordering; null without one.
|
|
179
|
+
index: z.number().int().nonnegative().nullable(),
|
|
180
|
+
topic: z.string().nullable(),
|
|
181
|
+
topicSource: z.enum(["queued", "held", "generated", "template"]),
|
|
182
|
+
// The project title this run will get, the way the scheduler builds it; null when it can't
|
|
183
|
+
// be known yet (a topic still to be approved or generated, or a deleted template). Older
|
|
184
|
+
// servers leave it out.
|
|
185
|
+
renderedTitle: z.string().nullable().default(null),
|
|
186
|
+
})
|
|
187
|
+
.strict()
|
|
188
|
+
.readonly();
|
|
189
|
+
export const calendarProjectSchema = z
|
|
190
|
+
.object({
|
|
191
|
+
id: z.string(),
|
|
192
|
+
title: z.string(),
|
|
193
|
+
state: z.enum(projectStates),
|
|
194
|
+
createdAt: z.string(),
|
|
195
|
+
finishedAt: z.string().nullable(),
|
|
196
|
+
scheduleId: z.string().nullable(),
|
|
197
|
+
})
|
|
198
|
+
.strict()
|
|
199
|
+
.readonly();
|
|
200
|
+
export const calendarQueuedSchema = z
|
|
201
|
+
.object({
|
|
202
|
+
projectId: z.string(),
|
|
203
|
+
title: z.string(),
|
|
204
|
+
batchId: z.string(),
|
|
205
|
+
position: z.number().int(),
|
|
206
|
+
state: z.enum(["queued", "active"]),
|
|
207
|
+
queuedAt: z.string(),
|
|
208
|
+
})
|
|
209
|
+
.strict()
|
|
210
|
+
.readonly();
|
|
211
|
+
export const calendarSchema = z
|
|
212
|
+
.object({
|
|
213
|
+
from: z.string(),
|
|
214
|
+
to: z.string(),
|
|
215
|
+
runs: z.array(calendarRunSchema).readonly(),
|
|
216
|
+
projects: z.array(calendarProjectSchema).readonly(),
|
|
217
|
+
queued: z.array(calendarQueuedSchema).readonly(),
|
|
218
|
+
})
|
|
219
|
+
.strict()
|
|
220
|
+
.readonly();
|
|
221
|
+
// A calendar covers at most this many days per request.
|
|
222
|
+
export const calendarMaxDays = 92;
|
|
223
|
+
export const topicTransferSchema = z
|
|
224
|
+
.object({
|
|
225
|
+
baseVersion: z.number().int().positive(),
|
|
226
|
+
index: z.number().int().nonnegative(),
|
|
227
|
+
targetId: id,
|
|
228
|
+
// Where it lands in the other schedule's queue; the end when left out.
|
|
229
|
+
position: z.number().int().nonnegative().optional(),
|
|
230
|
+
})
|
|
231
|
+
.strict()
|
|
232
|
+
.readonly();
|
|
@@ -3,7 +3,8 @@ import { transact } from "../../kernel/db/tx.js";
|
|
|
3
3
|
import { requestHash } from "../play-drafts/repo.js";
|
|
4
4
|
import { nextOccurrence } from "./calendar.js";
|
|
5
5
|
import { insertSchedule, runsForSchedule, scheduleById, scheduleRows, setScheduleStatus, tombstoneScheduleRow, updateScheduleRow, } from "./repo.js";
|
|
6
|
-
import { scheduleControlSchema, scheduleCreateSchema, scheduleDeleteSchema, scheduleUpdateSchema, } from "./schema.js";
|
|
6
|
+
import { scheduleControlSchema, scheduleCreateSchema, scheduleDeleteSchema, scheduleUpdateSchema, topicStateIdle, } from "./schema.js";
|
|
7
|
+
import { templateKeywords, topicRowProblems } from "./topic-list.js";
|
|
7
8
|
export function listSchedules(deps) {
|
|
8
9
|
return scheduleRows(deps.db);
|
|
9
10
|
}
|
|
@@ -37,6 +38,9 @@ export function createSchedule(deps, input) {
|
|
|
37
38
|
const checked = checkTemplate(deps, parsed.data.templateId, parsed.data.templateVersion);
|
|
38
39
|
if (!checked.ok)
|
|
39
40
|
return checked;
|
|
41
|
+
const topics = checkTopics(deps, parsed.data, []);
|
|
42
|
+
if (!topics.ok)
|
|
43
|
+
return topics;
|
|
40
44
|
const now = deps.clock.now();
|
|
41
45
|
const next = nextOccurrence(parsed.data.cadence, parsed.data.timezone, now);
|
|
42
46
|
if (next === null)
|
|
@@ -76,13 +80,18 @@ export function updateSchedule(deps, input) {
|
|
|
76
80
|
const checked = checkTemplate(deps, parsed.data.templateId, parsed.data.templateVersion);
|
|
77
81
|
if (!checked.ok)
|
|
78
82
|
return checked;
|
|
83
|
+
const topics = checkTopics(deps, parsed.data, previous.items);
|
|
84
|
+
if (!topics.ok)
|
|
85
|
+
return topics;
|
|
79
86
|
const now = deps.clock.now();
|
|
80
87
|
const next = previous.status === "active"
|
|
81
88
|
? nextOccurrence(parsed.data.cadence, parsed.data.timezone, now)
|
|
82
89
|
: previous.nextRunAt === null
|
|
83
90
|
? null
|
|
84
91
|
: nextOccurrence(parsed.data.cadence, parsed.data.timezone, now);
|
|
85
|
-
const value = summary(parsed.data, next?.toISOString() ?? null, now.toISOString(), previous.status, previous.version + 1
|
|
92
|
+
const value = summary(parsed.data, next?.toISOString() ?? null, now.toISOString(), previous.status, previous.version + 1,
|
|
93
|
+
// Saving clears a recorded generation failure, so the next tick tries again.
|
|
94
|
+
{ ...previous.topics, failedAt: null, error: null });
|
|
86
95
|
if (!updateScheduleRow(deps.db, value, parsed.data.mutationId, hash, parsed.data.baseVersion))
|
|
87
96
|
return { ok: false, reason: "conflict" };
|
|
88
97
|
return { ok: true, value };
|
|
@@ -154,14 +163,41 @@ function checkTemplate(deps, id, version) {
|
|
|
154
163
|
const mediaSources = ["audio", "images", "thumbnail"];
|
|
155
164
|
const { form } = template.document;
|
|
156
165
|
// A supplied file is attached again on Play, which a scheduled run cannot do; the shorts'
|
|
157
|
-
// background music is one too.
|
|
166
|
+
// background music is one too, and so is the ambient bed's own file.
|
|
158
167
|
if (mediaSources.some((kind) => sources[kind] === "provide") ||
|
|
159
168
|
(form.shorts?.enabled === true && form.provided.shortsMusic) ||
|
|
169
|
+
form.ambientBed?.source === "upload" ||
|
|
160
170
|
(sources.images === "generate" && form.reference?.source === "provide"))
|
|
161
171
|
return { ok: false, reason: "unsupported-media" };
|
|
162
172
|
return { ok: true, value: true };
|
|
163
173
|
}
|
|
164
|
-
|
|
174
|
+
// The queue's keywords must be the template's: a topic (or an every-run value) naming a keyword
|
|
175
|
+
// the template doesn't use would silently do nothing. Topics saved before, unchanged, are left
|
|
176
|
+
// alone, so a queue from before this check still saves.
|
|
177
|
+
function checkTopics(deps, input, kept) {
|
|
178
|
+
const template = deps.template(input.templateId, input.templateVersion);
|
|
179
|
+
if (!template)
|
|
180
|
+
return { ok: false, reason: "missing-template" };
|
|
181
|
+
const keywords = templateKeywords(template.document.form);
|
|
182
|
+
const named = keywords.length === 0 ? "none" : keywords.map((one) => `“${one}”`).join(", ");
|
|
183
|
+
const problems = [];
|
|
184
|
+
if (input.topicKeyword !== null && !keywords.includes(input.topicKeyword))
|
|
185
|
+
problems.push(`The topic keyword “${input.topicKeyword}” is not in this template (its keywords are ${named}). Pick another under Each topic fills.`);
|
|
186
|
+
for (const keyword of Object.keys(input.values))
|
|
187
|
+
if (!keywords.includes(keyword))
|
|
188
|
+
problems.push(`“${keyword}” (for every run) is not a keyword of this template (its keywords are ${named}). Remove it, or pick the template that uses it.`);
|
|
189
|
+
problems.push(...topicRowProblems(input.items, { keywords, topicKeyword: input.topicKeyword, kept }));
|
|
190
|
+
if (problems.length === 0)
|
|
191
|
+
return { ok: true, value: true };
|
|
192
|
+
const shown = problems.slice(0, 5);
|
|
193
|
+
const more = problems.length - shown.length;
|
|
194
|
+
return {
|
|
195
|
+
ok: false,
|
|
196
|
+
reason: "invalid-topics",
|
|
197
|
+
message: `The schedule wasn't saved. ${shown.join(" ")}${more > 0 ? ` And ${String(more)} more like these.` : ""}`,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
function summary(input, nextRunAt, now, status = "active", version = 1, topics = topicStateIdle) {
|
|
165
201
|
return {
|
|
166
202
|
id: input.id,
|
|
167
203
|
name: input.name,
|
|
@@ -175,6 +211,9 @@ function summary(input, nextRunAt, now, status = "active", version = 1) {
|
|
|
175
211
|
items: input.items,
|
|
176
212
|
topicKeyword: input.topicKeyword,
|
|
177
213
|
values: input.values,
|
|
214
|
+
brief: input.brief === null || input.brief === "" ? null : input.brief,
|
|
215
|
+
topicGeneration: input.topicGeneration,
|
|
216
|
+
topics,
|
|
178
217
|
status,
|
|
179
218
|
version,
|
|
180
219
|
nextRunAt,
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// Near-duplicate checks for generated topics. Pure, so the web bundle can import it too.
|
|
2
|
+
//
|
|
3
|
+
// Two topics are the same when, after normalising (accents dropped, lower case, punctuation
|
|
4
|
+
// to spaces, "&" read as "and"):
|
|
5
|
+
// - the texts are equal, or
|
|
6
|
+
// - their word sets overlap by at least `jaccardMin` (shared words / all words), or
|
|
7
|
+
// - one's words all appear in the other's and cover at least `containmentMin` of it
|
|
8
|
+
// ("Tiamat" and "Tiamat's Lair" are one video; "Red Dragons" and "Blue Dragons" are two).
|
|
9
|
+
// Words are compared without filler words ("the", "of", …) and without a plural "s".
|
|
10
|
+
// A project title is compared more loosely: every word of the topic appearing in it is enough,
|
|
11
|
+
// because the title wraps the topic in the template's own words ("D&D Lore: Tiamat").
|
|
12
|
+
export const jaccardMin = 0.6;
|
|
13
|
+
export const containmentMin = 0.5;
|
|
14
|
+
const filler = new Set([
|
|
15
|
+
"a",
|
|
16
|
+
"an",
|
|
17
|
+
"the",
|
|
18
|
+
"of",
|
|
19
|
+
"and",
|
|
20
|
+
"or",
|
|
21
|
+
"in",
|
|
22
|
+
"on",
|
|
23
|
+
"to",
|
|
24
|
+
"for",
|
|
25
|
+
"with",
|
|
26
|
+
"vs",
|
|
27
|
+
"s",
|
|
28
|
+
]);
|
|
29
|
+
export function normaliseTopic(text) {
|
|
30
|
+
return text
|
|
31
|
+
.normalize("NFKD")
|
|
32
|
+
.replace(/\p{M}/gu, "")
|
|
33
|
+
.toLowerCase()
|
|
34
|
+
.replace(/&/g, " and ")
|
|
35
|
+
.replace(/[^\p{L}\p{N}]+/gu, " ")
|
|
36
|
+
.trim();
|
|
37
|
+
}
|
|
38
|
+
export function topicWords(text) {
|
|
39
|
+
const words = normaliseTopic(text)
|
|
40
|
+
.split(" ")
|
|
41
|
+
.filter((word) => word !== "" && !filler.has(word))
|
|
42
|
+
.map((word) => word.length > 3 && word.endsWith("s") && !word.endsWith("ss") ? word.slice(0, -1) : word);
|
|
43
|
+
return new Set(words);
|
|
44
|
+
}
|
|
45
|
+
function shared(a, b) {
|
|
46
|
+
let count = 0;
|
|
47
|
+
for (const word of a)
|
|
48
|
+
if (b.has(word))
|
|
49
|
+
count += 1;
|
|
50
|
+
return count;
|
|
51
|
+
}
|
|
52
|
+
export function similarTopics(a, b) {
|
|
53
|
+
const left = normaliseTopic(a);
|
|
54
|
+
const right = normaliseTopic(b);
|
|
55
|
+
if (left === right)
|
|
56
|
+
return left !== "";
|
|
57
|
+
const wa = topicWords(a);
|
|
58
|
+
const wb = topicWords(b);
|
|
59
|
+
if (wa.size === 0 || wb.size === 0)
|
|
60
|
+
return false;
|
|
61
|
+
const common = shared(wa, wb);
|
|
62
|
+
if (common / (wa.size + wb.size - common) >= jaccardMin)
|
|
63
|
+
return true;
|
|
64
|
+
const small = Math.min(wa.size, wb.size);
|
|
65
|
+
const large = Math.max(wa.size, wb.size);
|
|
66
|
+
return common === small && small / large >= containmentMin;
|
|
67
|
+
}
|
|
68
|
+
// A topic already made as a project: all of its words appear in the project's title.
|
|
69
|
+
export function topicInTitle(topic, title) {
|
|
70
|
+
if (similarTopics(topic, title))
|
|
71
|
+
return true;
|
|
72
|
+
const words = topicWords(topic);
|
|
73
|
+
if (words.size === 0)
|
|
74
|
+
return false;
|
|
75
|
+
return shared(words, topicWords(title)) === words.size;
|
|
76
|
+
}
|
|
77
|
+
// Keeps the candidates that are new, in their order, dropping repeats among themselves too.
|
|
78
|
+
export function newTopics(candidates, known) {
|
|
79
|
+
const kept = [];
|
|
80
|
+
for (const candidate of candidates) {
|
|
81
|
+
if (known.topics.some((topic) => similarTopics(candidate, topic)))
|
|
82
|
+
continue;
|
|
83
|
+
if (known.projects.some((title) => topicInTitle(candidate, title)))
|
|
84
|
+
continue;
|
|
85
|
+
if (kept.some((topic) => similarTopics(candidate, topic)))
|
|
86
|
+
continue;
|
|
87
|
+
kept.push(candidate);
|
|
88
|
+
}
|
|
89
|
+
return kept;
|
|
90
|
+
}
|