@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.
- package/LICENSE +201 -21
- package/NOTICE +12 -0
- package/README.md +24 -41
- package/SUBTITLES.md +1 -1
- package/dist/adapter-registry.js +31 -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 +85 -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/gemini.js +206 -0
- 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/pcm-mp3.js +62 -0
- package/dist/adapters/tts/system.js +147 -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/models.yaml +38 -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/assets/style-preview/narration.mp3 +0 -0
- package/dist/assets/style-preview/tall-1.jpg +0 -0
- package/dist/assets/style-preview/tall-2.jpg +0 -0
- package/dist/assets/style-preview/tall-3.jpg +0 -0
- package/dist/assets/style-preview/wide-1.jpg +0 -0
- package/dist/assets/style-preview/wide-2.jpg +0 -0
- package/dist/assets/style-preview/wide-3.jpg +0 -0
- package/dist/catalog/merge.js +132 -0
- package/dist/catalog/registry.js +17 -14
- package/dist/catalog/schema.js +2 -0
- package/dist/catalog/store.js +147 -8
- package/dist/catalog/validate.js +2 -2
- package/dist/compose.yaml +71 -0
- package/dist/edge/autostart/docker-record.js +32 -0
- package/dist/edge/autostart/docker.js +118 -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 +72 -0
- package/dist/edge/autostart/service.js +90 -0
- package/dist/edge/cli-args.js +71 -0
- package/dist/edge/cli.js +64 -63
- 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 +160 -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/events/hub.js +3 -1
- package/dist/edge/host-cli.js +1 -1
- package/dist/edge/http/actions.js +38 -11
- package/dist/edge/http/app.js +60 -2
- package/dist/edge/http/auditions.js +70 -0
- package/dist/edge/http/autostart.js +37 -0
- package/dist/edge/http/byte-range.js +42 -0
- package/dist/edge/http/channel-memory.js +92 -0
- package/dist/edge/http/channels.js +128 -0
- package/dist/edge/http/diagnostics.js +1 -0
- package/dist/edge/http/draft-files.js +2 -4
- package/dist/edge/http/entries.js +22 -3
- package/dist/edge/http/files.js +11 -7
- 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 +212 -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/project-templates.js +19 -2
- package/dist/edge/http/projects.js +37 -100
- package/dist/edge/http/prompts.js +40 -3
- package/dist/edge/http/pronunciations.js +23 -4
- package/dist/edge/http/providers.js +119 -3
- package/dist/edge/http/reviews.js +131 -0
- package/dist/edge/http/revision-files.js +3 -5
- package/dist/edge/http/revisions.js +1 -1
- package/dist/edge/http/run-cost.js +25 -0
- package/dist/edge/http/schedules.js +115 -6
- package/dist/edge/http/settings.js +84 -6
- package/dist/edge/http/storage-files.js +50 -0
- package/dist/edge/http/storage.js +43 -2
- package/dist/edge/http/studio.js +287 -0
- package/dist/edge/http/style-preview.js +61 -0
- package/dist/edge/http/subtitles.js +1 -1
- package/dist/edge/http/trash.js +69 -0
- package/dist/edge/http/tutorials.js +59 -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 +92 -0
- package/dist/edge/native-update.js +62 -0
- package/dist/extension/slopify-studio-chrome.zip +0 -0
- package/dist/extension/slopify-studio-firefox.zip +0 -0
- package/dist/host-cli/gemini-login.js +83 -0
- package/dist/host-cli/open-folder.js +10 -10
- package/dist/host-cli/runtime.js +9 -1
- package/dist/host-cli/status.js +4 -2
- 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/db/migrations/0041-cast-hosts.sql +3 -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 +10 -5
- package/dist/kernel/ports/narration-aliases.js +60 -0
- package/dist/kernel/ports/plan-limits.js +36 -0
- package/dist/kernel/ports/system-speech.js +203 -0
- package/dist/kernel/runner/attempt.js +17 -8
- package/dist/kernel/runner/audition.js +36 -0
- package/dist/kernel/runner/graph.js +40 -5
- package/dist/kernel/runner/index.js +39 -9
- 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 +277 -16
- package/dist/model-catalog.js +2 -0
- 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/3.0.1.md +105 -0
- package/dist/patch-notes/index.json +32 -0
- package/dist/slices/admission/model.js +27 -0
- package/dist/slices/admission/repo.js +24 -4
- package/dist/slices/admission/rules.js +80 -2
- package/dist/slices/admission/schema.js +58 -1
- package/dist/slices/admission/short-mode.js +47 -0
- package/dist/slices/admission/start.js +42 -6
- 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 +221 -0
- package/dist/slices/channels/runs.js +129 -0
- package/dist/slices/channels/schema.js +152 -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 +5 -2
- package/dist/slices/control/providers.js +4 -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 +68 -2
- package/dist/slices/eta/history.js +56 -0
- package/dist/slices/eta/model.js +63 -0
- package/dist/slices/eta/view.js +25 -0
- 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 +185 -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 +47 -3
- package/dist/slices/notifications/rules.js +48 -3
- package/dist/slices/onboarding/copy.js +143 -0
- package/dist/slices/onboarding/first-run.js +72 -0
- package/dist/slices/onboarding/full-video.js +81 -0
- package/dist/slices/onboarding/install.js +119 -0
- package/dist/slices/onboarding/model.js +85 -0
- package/dist/slices/onboarding/packs.js +491 -0
- package/dist/slices/onboarding/quick-short.js +144 -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 +127 -8
- package/dist/slices/play-drafts/readiness.js +4 -1
- 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 +59 -6
- package/dist/slices/project-templates/next-chapter.js +94 -0
- package/dist/slices/project-templates/one-off.js +26 -0
- package/dist/slices/project-templates/repo.js +2 -1
- package/dist/slices/project-templates/schema.js +7 -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 +67 -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 +125 -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 +267 -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 +62 -11
- 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/service-readiness.js +3 -3
- 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 +118 -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 +256 -0
- package/dist/slices/run-cost/pricing.js +95 -0
- package/dist/slices/run-cost/week.js +108 -0
- package/dist/slices/schedules/agenda.js +222 -0
- package/dist/slices/schedules/repo.js +58 -18
- package/dist/slices/schedules/scheduler.js +68 -17
- package/dist/slices/schedules/schema.js +179 -1
- package/dist/slices/schedules/service.js +43 -4
- package/dist/slices/schedules/similar.js +128 -0
- package/dist/slices/schedules/topic-list.js +163 -0
- package/dist/slices/schedules/topics.js +523 -0
- package/dist/slices/settings/first-run.js +106 -0
- package/dist/slices/settings/health.js +247 -0
- package/dist/slices/settings/key-guides.js +138 -0
- package/dist/slices/settings/key-test.js +93 -0
- package/dist/slices/settings/keys.js +10 -5
- package/dist/slices/settings/model-reach.js +52 -0
- package/dist/slices/settings/model.js +55 -1
- package/dist/slices/settings/models.js +5 -1
- package/dist/slices/settings/playback.js +15 -0
- package/dist/slices/settings/readiness.js +14 -2
- package/dist/slices/settings/repo.js +17 -3
- package/dist/slices/settings/tutorial-schema.js +5 -0
- 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 +63 -13
- 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 +87 -0
- package/dist/slices/storage/downloads.js +33 -1
- 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 +94 -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 +30 -0
- package/dist/slices/studio/pack.js +211 -0
- package/dist/slices/studio/queue.js +69 -0
- package/dist/slices/studio/settings.js +170 -0
- package/dist/slices/style-preview/images.js +61 -0
- package/dist/slices/style-preview/narration.js +33 -0
- package/dist/slices/style-preview/render.js +148 -0
- package/dist/slices/style-preview/schema.js +59 -0
- package/dist/slices/style-preview/service.js +92 -0
- package/dist/slices/style-preview/settings.js +139 -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/tutorials/anchors.js +57 -0
- package/dist/slices/tutorials/library.js +185 -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 +87 -0
- package/dist/slices/voices/audition.js +25 -0
- package/dist/slices/voices/cast.js +74 -0
- package/dist/slices/voices/grouping.js +59 -0
- package/dist/slices/voices/join.js +35 -0
- package/dist/slices/voices/languages.js +25 -0
- package/dist/slices/voices/model.js +218 -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 +84 -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/tutorials/AI-CLIs.md +146 -0
- package/dist/tutorials/Backups.md +121 -0
- package/dist/tutorials/Calendar.md +103 -0
- package/dist/tutorials/Cast-Library.md +112 -0
- package/dist/tutorials/Channels.md +148 -0
- package/dist/tutorials/Costs-and-Run-Cost.md +136 -0
- package/dist/tutorials/Docker.md +171 -0
- package/dist/tutorials/Document-Themes.md +207 -0
- package/dist/tutorials/Editing-a-Project.md +204 -0
- package/dist/tutorials/FAQ.md +134 -0
- package/dist/tutorials/First-Launch-and-Welcome.md +153 -0
- package/dist/tutorials/Home-and-Projects.md +174 -0
- package/dist/tutorials/Home.md +119 -0
- package/dist/tutorials/Install.md +154 -0
- package/dist/tutorials/Intros-and-Outros.md +86 -0
- package/dist/tutorials/Keyboard-Shortcuts-and-Command-Palette.md +215 -0
- package/dist/tutorials/Library-Overview.md +75 -0
- package/dist/tutorials/Models.md +133 -0
- package/dist/tutorials/Multiple-Voices.md +161 -0
- package/dist/tutorials/Narration-Aliases-and-Glossary.md +129 -0
- package/dist/tutorials/Notifications.md +86 -0
- package/dist/tutorials/Other-Languages.md +110 -0
- package/dist/tutorials/PDF-Documents.md +100 -0
- package/dist/tutorials/Play-Images.md +135 -0
- package/dist/tutorials/Play-Narration.md +167 -0
- package/dist/tutorials/Play-Outputs.md +109 -0
- package/dist/tutorials/Play-Overview.md +216 -0
- package/dist/tutorials/Play-Title-and-Article.md +135 -0
- package/dist/tutorials/Play-Video-and-Style.md +143 -0
- package/dist/tutorials/Project-Page.md +228 -0
- package/dist/tutorials/Prompts.md +136 -0
- package/dist/tutorials/Providers-and-Keys.md +290 -0
- package/dist/tutorials/Publishing-to-YouTube.md +135 -0
- package/dist/tutorials/Recovery-and-Retries.md +141 -0
- package/dist/tutorials/Reviews-and-Checkpoints.md +143 -0
- package/dist/tutorials/Schedules.md +154 -0
- package/dist/tutorials/Settings-Reference.md +258 -0
- package/dist/tutorials/Shorts.md +116 -0
- package/dist/tutorials/Start-at-Login.md +126 -0
- package/dist/tutorials/Studio-Extension.md +146 -0
- package/dist/tutorials/Templates.md +146 -0
- package/dist/tutorials/Trash.md +87 -0
- package/dist/tutorials/Troubleshooting.md +149 -0
- package/dist/tutorials/Uninstalling-and-Moving.md +113 -0
- package/dist/tutorials/Updating-and-Patch-Notes.md +120 -0
- package/dist/tutorials/Video-Editing.md +135 -0
- package/dist/tutorials/Where-Your-Files-Live.md +149 -0
- package/dist/tutorials/YouTube-Description.md +132 -0
- package/dist/tutorials/Your-First-Short.md +125 -0
- package/dist/tutorials/_Footer.md +1 -0
- package/dist/tutorials/_Sidebar.md +69 -0
- package/dist/updater/service.js +66 -21
- package/dist/web/assets/index-DBLSfqA-.css +1 -0
- package/dist/web/assets/index-DFdIhmm0.js +307 -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-B9OY1ioX.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +5 -5
- 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
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { transact } from "../../kernel/db/tx.js";
|
|
3
|
+
import { liveProject } from "../admission/repo.js";
|
|
4
|
+
import { defaultChannelId } from "../channels/model.js";
|
|
5
|
+
import { projectChannels, scheduleChannel } from "../channels/repo.js";
|
|
6
|
+
import { channelVideoTitles } from "../channels/videos.js";
|
|
7
|
+
import { readSetting, writeSetting } from "../settings/repo.js";
|
|
8
|
+
import { scheduleById } from "./repo.js";
|
|
9
|
+
import { heldTopicEditSchema, heldTopicSchema, queueMax, topicMoveSchema, topicQueueSchema, topicTitleSchema, topicTransferSchema, } from "./schema.js";
|
|
10
|
+
import { newTopics } from "./similar.js";
|
|
11
|
+
import { templateKeywords, topicRowProblems } from "./topic-list.js";
|
|
12
|
+
// A schedule that finds its own topics asks an LLM whenever its queue holds fewer than
|
|
13
|
+
// `keepAtLeast`. Only one generation per schedule runs at a time: a lease on the schedule row
|
|
14
|
+
// (`topics_generating_at`) is taken before the LLM is asked and cleared after, so a tick and a
|
|
15
|
+
// "Generate topics now" press cannot both ask. A lease older than `leaseMs` belongs to a
|
|
16
|
+
// generation that died with the process and is taken over.
|
|
17
|
+
export const generationTimeoutMs = 10 * 60_000;
|
|
18
|
+
const leaseMs = generationTimeoutMs + 5 * 60_000;
|
|
19
|
+
// A failure is recorded on the schedule and the next tick after this long tries again, so a
|
|
20
|
+
// signed-out CLI or a bad answer is not asked again every 15 seconds.
|
|
21
|
+
export const generationRetryMs = 5 * 60_000;
|
|
22
|
+
// Asked for on top of what is missing, so the duplicates dropped still leave enough.
|
|
23
|
+
const spare = 5;
|
|
24
|
+
const askMax = 50;
|
|
25
|
+
// The prompt lists every title; this only bounds a pathological library.
|
|
26
|
+
const knownMax = 5000;
|
|
27
|
+
export function generationWanted(schedule) {
|
|
28
|
+
const generation = schedule.topicGeneration;
|
|
29
|
+
if (generation.mode === "off")
|
|
30
|
+
return 0;
|
|
31
|
+
const waiting = schedule.items.length + (generation.mode === "hold" ? schedule.topics.held : 0);
|
|
32
|
+
return Math.max(0, Math.min(generation.keepAtLeast, queueMax) - waiting);
|
|
33
|
+
}
|
|
34
|
+
// Whether a tick should start a generation for this schedule now.
|
|
35
|
+
export function generationDue(schedule, now) {
|
|
36
|
+
if (schedule.deletedAt !== null || schedule.status !== "active")
|
|
37
|
+
return false;
|
|
38
|
+
if (generationWanted(schedule) === 0)
|
|
39
|
+
return false;
|
|
40
|
+
if (schedule.topics.generatingSince !== null && !stale(schedule.topics.generatingSince, now))
|
|
41
|
+
return false;
|
|
42
|
+
return (schedule.topics.failedAt === null ||
|
|
43
|
+
now.valueOf() - new Date(schedule.topics.failedAt).valueOf() >= generationRetryMs);
|
|
44
|
+
}
|
|
45
|
+
function stale(since, now) {
|
|
46
|
+
return now.valueOf() - new Date(since).valueOf() >= leaseMs;
|
|
47
|
+
}
|
|
48
|
+
// `force` is the "Generate topics now" button: it ignores the retry wait and asks for a few
|
|
49
|
+
// even when the queue is full.
|
|
50
|
+
export async function generateTopics(deps, scheduleId, options = {}) {
|
|
51
|
+
const now = deps.clock.now();
|
|
52
|
+
const claimed = transact(deps.db, () => {
|
|
53
|
+
const schedule = scheduleById(deps.db, scheduleId);
|
|
54
|
+
if (schedule === undefined || schedule.deletedAt !== null)
|
|
55
|
+
return { ok: false, reason: "not-found", message: "This schedule no longer exists." };
|
|
56
|
+
if (schedule.topicGeneration.mode === "off")
|
|
57
|
+
return {
|
|
58
|
+
ok: false,
|
|
59
|
+
reason: "off",
|
|
60
|
+
message: "Topic generation is off for this schedule. Turn it on under Edit → Topic generation.",
|
|
61
|
+
};
|
|
62
|
+
if (schedule.status === "canceled" || schedule.status === "completed")
|
|
63
|
+
return {
|
|
64
|
+
ok: false,
|
|
65
|
+
reason: "off",
|
|
66
|
+
message: `This schedule is ${schedule.status}, so it needs no more topics.`,
|
|
67
|
+
};
|
|
68
|
+
if (!options.force && generationWanted(schedule) === 0)
|
|
69
|
+
return { ok: false, reason: "full", message: "The queue already has enough topics." };
|
|
70
|
+
const taken = deps.db
|
|
71
|
+
.prepare(`UPDATE schedules SET topics_generating_at=?
|
|
72
|
+
WHERE id=? AND (topics_generating_at IS NULL OR topics_generating_at<=?)`)
|
|
73
|
+
.run(now.toISOString(), scheduleId, new Date(now.valueOf() - leaseMs).toISOString());
|
|
74
|
+
if (Number(taken.changes) !== 1)
|
|
75
|
+
return {
|
|
76
|
+
ok: false,
|
|
77
|
+
reason: "busy",
|
|
78
|
+
message: "Slopify is already generating topics for this schedule. Wait for it to finish.",
|
|
79
|
+
};
|
|
80
|
+
return schedule;
|
|
81
|
+
});
|
|
82
|
+
if ("ok" in claimed)
|
|
83
|
+
return claimed;
|
|
84
|
+
const schedule = claimed;
|
|
85
|
+
try {
|
|
86
|
+
return await ask(deps, schedule, options);
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
deps.db
|
|
90
|
+
.prepare("UPDATE schedules SET topics_generating_at=NULL WHERE id=? AND topics_generating_at=?")
|
|
91
|
+
.run(scheduleId, now.toISOString());
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
async function ask(deps, schedule, options) {
|
|
95
|
+
const fail = (message) => {
|
|
96
|
+
const failedAt = deps.clock.now();
|
|
97
|
+
const retry = "Slopify tries again in 5 minutes, or press Generate topics now on the Schedules screen.";
|
|
98
|
+
const text = `Couldn't generate topics: ${message} ${retry}`;
|
|
99
|
+
deps.db
|
|
100
|
+
.prepare("UPDATE schedules SET topics_failed_at=?,topics_error=? WHERE id=?")
|
|
101
|
+
.run(failedAt.toISOString(), text, schedule.id);
|
|
102
|
+
return { ok: false, reason: "failed", message: text };
|
|
103
|
+
};
|
|
104
|
+
const template = deps.template(schedule.templateId, schedule.templateVersion);
|
|
105
|
+
const llm = schedule.topicGeneration.llm ?? template?.document.form.llm;
|
|
106
|
+
if (llm === undefined)
|
|
107
|
+
return fail("the template this schedule uses was deleted, so there is no LLM to ask. Edit the schedule and choose a template, or pick an LLM under Topic generation.");
|
|
108
|
+
if (deps.topicLlm === undefined)
|
|
109
|
+
return fail("no LLM is available in this Slopify. Restart Slopify and try again.");
|
|
110
|
+
const wanted = Math.max(generationWanted(schedule), options.force ? spare : 0);
|
|
111
|
+
const known = knownTitles(deps, schedule.id);
|
|
112
|
+
const signal = AbortSignal.any([
|
|
113
|
+
AbortSignal.timeout(generationTimeoutMs),
|
|
114
|
+
...(options.signal === undefined ? [] : [options.signal]),
|
|
115
|
+
]);
|
|
116
|
+
let answer;
|
|
117
|
+
try {
|
|
118
|
+
const answered = await deps.topicLlm({
|
|
119
|
+
owner: { kind: "schedule", id: schedule.id },
|
|
120
|
+
purpose: "topics",
|
|
121
|
+
provider: llm.provider,
|
|
122
|
+
model: llm.model,
|
|
123
|
+
...(llm.thinking === undefined ? {} : { thinking: llm.thinking }),
|
|
124
|
+
messages: topicMessages(withChannelBrief(deps, schedule), template?.document.form.title, known, Math.min(wanted + spare, askMax)),
|
|
125
|
+
signal,
|
|
126
|
+
});
|
|
127
|
+
answer = answered.text;
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
if (options.signal?.aborted === true)
|
|
131
|
+
return { ok: false, reason: "failed", message: "Slopify was closing." };
|
|
132
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
133
|
+
return fail(`${llm.provider} (${llm.model}) did not answer: ${detail.trim().slice(0, 300)}. Check it in Settings → Providers, or pick another LLM under Edit → Topic generation.`);
|
|
134
|
+
}
|
|
135
|
+
const suggested = parseTopics(answer);
|
|
136
|
+
if (suggested.length === 0)
|
|
137
|
+
return fail(`${llm.provider} (${llm.model}) answered without a list of topics. Try again, or pick another LLM under Edit → Topic generation.`);
|
|
138
|
+
return transact(deps.db, () => {
|
|
139
|
+
const current = scheduleById(deps.db, schedule.id);
|
|
140
|
+
if (current === undefined || current.deletedAt !== null)
|
|
141
|
+
return { ok: false, reason: "not-found", message: "This schedule no longer exists." };
|
|
142
|
+
// Checked again against what is there now: a topic added while the LLM was thinking counts.
|
|
143
|
+
const fresh = newTopics(suggested, knownTitles(deps, schedule.id)).slice(0, Math.max(0, Math.min(wanted, queueMax - current.items.length)));
|
|
144
|
+
if (fresh.length === 0)
|
|
145
|
+
return fail("every topic the LLM suggested was already made, queued or turned down for this schedule. Add more detail to the series brief under Edit, so it has new ground to cover.");
|
|
146
|
+
const at = deps.clock.now().toISOString();
|
|
147
|
+
const mode = current.topicGeneration.mode === "hold" ? "hold" : "queue";
|
|
148
|
+
if (mode === "queue") {
|
|
149
|
+
deps.db
|
|
150
|
+
.prepare("UPDATE schedules SET items_json=?,version=version+1,updated_at=? WHERE id=? AND deleted_at IS NULL")
|
|
151
|
+
.run(JSON.stringify([...current.items, ...fresh.map((title) => ({ title, values: {} }))]), at, schedule.id);
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
const start = nextRank(deps, schedule.id);
|
|
155
|
+
fresh.forEach((title, index) => {
|
|
156
|
+
deps.db
|
|
157
|
+
.prepare(`INSERT INTO schedule_topics (id,schedule_id,title,state,rank,created_at)
|
|
158
|
+
VALUES (?,?,?,'held',?,?)`)
|
|
159
|
+
.run(deps.uuid(), schedule.id, title, start + index, at);
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
deps.db
|
|
163
|
+
.prepare("UPDATE schedules SET topics_generated_at=?,topics_failed_at=NULL,topics_error=NULL WHERE id=?")
|
|
164
|
+
.run(at, schedule.id);
|
|
165
|
+
if (mode === "hold") {
|
|
166
|
+
const waiting = heldTopics(deps, schedule.id).length;
|
|
167
|
+
try {
|
|
168
|
+
deps.topicsWaiting?.({
|
|
169
|
+
type: "schedule.topics",
|
|
170
|
+
scheduleId: schedule.id,
|
|
171
|
+
scheduleName: current.name,
|
|
172
|
+
added: fresh.length,
|
|
173
|
+
waiting,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
// A notification is never a reason to lose the topics.
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return { ok: true, added: fresh.length, mode };
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
function nextRank(deps, scheduleId) {
|
|
184
|
+
const row = deps.db
|
|
185
|
+
.prepare("SELECT max(rank) AS rank FROM schedule_topics WHERE schedule_id=? AND state='held'")
|
|
186
|
+
.get(scheduleId);
|
|
187
|
+
const rank = z.object({ rank: z.number().nullable() }).parse(row).rank;
|
|
188
|
+
return rank === null ? 0 : rank + 1;
|
|
189
|
+
}
|
|
190
|
+
// Everything this schedule has queued, held, turned down or used, the titles of the projects in
|
|
191
|
+
// its channel, and the titles of the videos its channel made before Slopify (Channel page →
|
|
192
|
+
// Existing videos). Another channel's videos are its own business: a sleep channel may cover a
|
|
193
|
+
// subject a lore channel already did.
|
|
194
|
+
export function knownTitles(deps, scheduleId) {
|
|
195
|
+
const schedule = scheduleById(deps.db, scheduleId);
|
|
196
|
+
const logged = deps.db
|
|
197
|
+
.prepare("SELECT title FROM schedule_topics WHERE schedule_id=? ORDER BY created_at DESC")
|
|
198
|
+
.all(scheduleId)
|
|
199
|
+
.map((row) => z.object({ title: z.string() }).parse(row).title);
|
|
200
|
+
const channel = scheduleChannel(deps.db, scheduleId);
|
|
201
|
+
const channels = projectChannels(deps.db);
|
|
202
|
+
const projects = deps.db
|
|
203
|
+
.prepare(`SELECT id,title FROM projects WHERE ${liveProject()} ORDER BY created_at DESC`)
|
|
204
|
+
.all()
|
|
205
|
+
.map((row) => z.object({ id: z.string(), title: z.string() }).parse(row))
|
|
206
|
+
.filter((row) => channel === undefined || (channels.get(row.id) ?? defaultChannelId) === channel.id)
|
|
207
|
+
.map((row) => row.title);
|
|
208
|
+
const existing = channel === undefined ? [] : channelVideoTitles(deps.db, channel.id);
|
|
209
|
+
return {
|
|
210
|
+
topics: [...new Set([...(schedule?.items.map((item) => item.title) ?? []), ...logged])],
|
|
211
|
+
projects: [...new Set([...projects, ...existing])],
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
// A schedule without a brief of its own reads its channel's series brief (the channel of the
|
|
215
|
+
// template it runs); one it sets itself wins.
|
|
216
|
+
export function withChannelBrief(deps, schedule) {
|
|
217
|
+
if (schedule.brief?.trim())
|
|
218
|
+
return schedule;
|
|
219
|
+
const brief = scheduleChannel(deps.db, schedule.id)?.seriesBrief.trim() ?? "";
|
|
220
|
+
return brief === "" ? schedule : { ...schedule, brief };
|
|
221
|
+
}
|
|
222
|
+
export function topicMessages(schedule, templateTitle, known, count) {
|
|
223
|
+
const titles = [...new Set([...known.topics, ...known.projects])].slice(0, knownMax);
|
|
224
|
+
const keyword = schedule.topicKeyword;
|
|
225
|
+
const fills = keyword !== null && templateTitle?.includes(`{{${keyword}}}`) === true
|
|
226
|
+
? `Each topic fills {{${keyword}}} in the video title "${templateTitle}", so write only the topic itself, not a whole title.`
|
|
227
|
+
: "Each topic becomes the video's subject; write it as a short title.";
|
|
228
|
+
const brief = schedule.brief?.trim();
|
|
229
|
+
return [
|
|
230
|
+
{
|
|
231
|
+
role: "system",
|
|
232
|
+
content: "You plan the next videos for a YouTube channel. Answer with a JSON array of strings and nothing else.",
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
role: "user",
|
|
236
|
+
content: [
|
|
237
|
+
`Suggest ${String(count)} new topics for the series "${schedule.name}".`,
|
|
238
|
+
"",
|
|
239
|
+
"Series brief:",
|
|
240
|
+
brief === undefined || brief === ""
|
|
241
|
+
? "(none given; work out what the channel covers from the titles below)"
|
|
242
|
+
: brief,
|
|
243
|
+
"",
|
|
244
|
+
`${fills} Keep each under 120 characters.`,
|
|
245
|
+
"Rank them most view-worthy first: the topics most likely to be watched, as the brief defines it.",
|
|
246
|
+
"",
|
|
247
|
+
titles.length === 0
|
|
248
|
+
? "Nothing has been made for this channel yet."
|
|
249
|
+
: "Never repeat or rephrase any of these, which were already made, queued or turned down:",
|
|
250
|
+
...titles.map((title) => `- ${title}`),
|
|
251
|
+
].join("\n"),
|
|
252
|
+
},
|
|
253
|
+
];
|
|
254
|
+
}
|
|
255
|
+
// A JSON array of strings, or failing that one topic per line with bullets and numbers dropped.
|
|
256
|
+
export function parseTopics(answer) {
|
|
257
|
+
const start = answer.indexOf("[");
|
|
258
|
+
const end = answer.lastIndexOf("]");
|
|
259
|
+
let lines;
|
|
260
|
+
if (start !== -1 && end > start) {
|
|
261
|
+
try {
|
|
262
|
+
const parsed = JSON.parse(answer.slice(start, end + 1));
|
|
263
|
+
if (Array.isArray(parsed))
|
|
264
|
+
lines = parsed;
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
lines = undefined;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
lines ??= answer.split(/\r?\n/);
|
|
271
|
+
return lines
|
|
272
|
+
.map((line) => typeof line === "string"
|
|
273
|
+
? line
|
|
274
|
+
: typeof line === "object" && line !== null && "title" in line
|
|
275
|
+
? String(line.title)
|
|
276
|
+
: "")
|
|
277
|
+
.map((line) => line
|
|
278
|
+
.replace(/^\s*(?:[-*•]|\d+[.)])\s*/, "")
|
|
279
|
+
.replace(/^["'“”]+|["'“”,]+$/g, "")
|
|
280
|
+
.replace(/\s+/g, " ")
|
|
281
|
+
.trim())
|
|
282
|
+
.filter((line) => line !== "" && line !== "[" && line !== "]" && line.length <= 200);
|
|
283
|
+
}
|
|
284
|
+
// The keywords set on held topics, by topic id. Kept in a settings row, since a held topic's row
|
|
285
|
+
// has only its title; an entry leaves when its topic is approved or turned down.
|
|
286
|
+
export const heldValuesKey = "schedules.held-topic-values";
|
|
287
|
+
const heldValuesSchema = z.record(z.string(), z.record(z.string(), z.string()));
|
|
288
|
+
function readHeldValues(db) {
|
|
289
|
+
const raw = readSetting(db, heldValuesKey);
|
|
290
|
+
if (raw === undefined)
|
|
291
|
+
return {};
|
|
292
|
+
try {
|
|
293
|
+
const parsed = heldValuesSchema.safeParse(JSON.parse(raw));
|
|
294
|
+
return parsed.success ? parsed.data : {};
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
return {};
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
// Entries of topics that are no longer held (approved, turned down, or gone with their
|
|
301
|
+
// schedule) are dropped on every write.
|
|
302
|
+
function writeHeldValues(db, change = (values) => values) {
|
|
303
|
+
const held = new Set(db
|
|
304
|
+
.prepare("SELECT id FROM schedule_topics WHERE state='held'")
|
|
305
|
+
.all()
|
|
306
|
+
.map((row) => String(row.id)));
|
|
307
|
+
const next = Object.fromEntries(Object.entries(change(readHeldValues(db))).filter(([id, values]) => held.has(id) && Object.keys(values).length > 0));
|
|
308
|
+
writeSetting(db, heldValuesKey, JSON.stringify(next));
|
|
309
|
+
}
|
|
310
|
+
export function heldTopics(deps, scheduleId) {
|
|
311
|
+
const stored = readHeldValues(deps.db);
|
|
312
|
+
return deps.db
|
|
313
|
+
.prepare(`SELECT id,title,rank,created_at FROM schedule_topics
|
|
314
|
+
WHERE schedule_id=? AND state='held' ORDER BY rank,created_at,id`)
|
|
315
|
+
.all(scheduleId)
|
|
316
|
+
.map((row) => {
|
|
317
|
+
const value = z
|
|
318
|
+
.object({ id: z.string(), title: z.string(), rank: z.number(), created_at: z.string() })
|
|
319
|
+
.parse(row);
|
|
320
|
+
return heldTopicSchema.parse({
|
|
321
|
+
id: value.id,
|
|
322
|
+
title: value.title,
|
|
323
|
+
values: stored[value.id] ?? {},
|
|
324
|
+
rank: value.rank,
|
|
325
|
+
createdAt: value.created_at,
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
function openSchedule(deps, scheduleId) {
|
|
330
|
+
const schedule = scheduleById(deps.db, scheduleId);
|
|
331
|
+
if (schedule === undefined || schedule.deletedAt !== null)
|
|
332
|
+
return { ok: false, reason: "not-found" };
|
|
333
|
+
if (schedule.status === "canceled" || schedule.status === "completed")
|
|
334
|
+
return { ok: false, reason: "conflict" };
|
|
335
|
+
return { ok: true, value: schedule };
|
|
336
|
+
}
|
|
337
|
+
// Approved topics join the end of the queue in their ranked order. `edits` renames a topic
|
|
338
|
+
// as it is approved.
|
|
339
|
+
export function approveHeldTopics(deps, scheduleId, ids, edits = {}) {
|
|
340
|
+
return transact(deps.db, () => {
|
|
341
|
+
const open = openSchedule(deps, scheduleId);
|
|
342
|
+
if (!open.ok)
|
|
343
|
+
return open;
|
|
344
|
+
const held = heldTopics(deps, scheduleId);
|
|
345
|
+
const chosen = ids === "all" ? held : held.filter((topic) => ids.includes(topic.id));
|
|
346
|
+
if (ids !== "all" && chosen.length !== ids.length)
|
|
347
|
+
return { ok: false, reason: "topic-not-found" };
|
|
348
|
+
const items = [];
|
|
349
|
+
for (const topic of chosen) {
|
|
350
|
+
const title = topicTitleSchema.safeParse(edits[topic.id] ?? topic.title);
|
|
351
|
+
if (!title.success)
|
|
352
|
+
return { ok: false, reason: "invalid-input" };
|
|
353
|
+
items.push({ title: title.data, values: topic.values });
|
|
354
|
+
}
|
|
355
|
+
if (open.value.items.length + items.length > queueMax)
|
|
356
|
+
return { ok: false, reason: "queue-full" };
|
|
357
|
+
const at = deps.clock.now().toISOString();
|
|
358
|
+
for (const topic of chosen)
|
|
359
|
+
deps.db.prepare("DELETE FROM schedule_topics WHERE id=?").run(topic.id);
|
|
360
|
+
deps.db
|
|
361
|
+
.prepare("UPDATE schedules SET items_json=?,version=version+1,updated_at=? WHERE id=?")
|
|
362
|
+
.run(JSON.stringify([...open.value.items, ...items]), at, scheduleId);
|
|
363
|
+
writeHeldValues(deps.db);
|
|
364
|
+
return { ok: true, value: scheduleById(deps.db, scheduleId) };
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
// Turned down topics stay recorded so no later generation suggests them again.
|
|
368
|
+
export function rejectHeldTopic(deps, scheduleId, topicId) {
|
|
369
|
+
const changed = deps.db
|
|
370
|
+
.prepare(`UPDATE schedule_topics SET state='rejected',decided_at=?
|
|
371
|
+
WHERE id=? AND schedule_id=? AND state='held'`)
|
|
372
|
+
.run(deps.clock.now().toISOString(), topicId, scheduleId);
|
|
373
|
+
if (Number(changed.changes) !== 1)
|
|
374
|
+
return { ok: false, reason: "topic-not-found" };
|
|
375
|
+
writeHeldValues(deps.db);
|
|
376
|
+
return { ok: true, value: scheduleById(deps.db, scheduleId) };
|
|
377
|
+
}
|
|
378
|
+
// Edit on a held topic: its title, and the keywords it sets for its run, checked against the
|
|
379
|
+
// template the way the queue's are. Without `values` the ones it had are kept; an empty value
|
|
380
|
+
// is dropped, so the every-run value applies.
|
|
381
|
+
export function editHeldTopic(deps, scheduleId, topicId, input) {
|
|
382
|
+
const edit = heldTopicEditSchema.safeParse(input);
|
|
383
|
+
if (!edit.success)
|
|
384
|
+
return { ok: false, reason: "invalid-input" };
|
|
385
|
+
const parsed = topicTitleSchema.safeParse(edit.data.title);
|
|
386
|
+
if (!parsed.success)
|
|
387
|
+
return { ok: false, reason: "invalid-input" };
|
|
388
|
+
const values = edit.data.values === undefined
|
|
389
|
+
? undefined
|
|
390
|
+
: Object.fromEntries(Object.entries(edit.data.values)
|
|
391
|
+
.map(([name, value]) => [name, value.trim()])
|
|
392
|
+
.filter(([, value]) => value !== ""));
|
|
393
|
+
if (values !== undefined && Object.keys(values).length > 0) {
|
|
394
|
+
const schedule = scheduleById(deps.db, scheduleId);
|
|
395
|
+
if (schedule === undefined)
|
|
396
|
+
return { ok: false, reason: "not-found" };
|
|
397
|
+
const template = deps.template(schedule.templateId, schedule.templateVersion);
|
|
398
|
+
if (template === undefined)
|
|
399
|
+
return { ok: false, reason: "missing-template" };
|
|
400
|
+
const problems = topicRowProblems([{ title: parsed.data, values }], {
|
|
401
|
+
keywords: templateKeywords(template.document.form).filter((name) => name !== schedule.topicKeyword),
|
|
402
|
+
topicKeyword: schedule.topicKeyword,
|
|
403
|
+
});
|
|
404
|
+
if (problems.length > 0)
|
|
405
|
+
return {
|
|
406
|
+
ok: false,
|
|
407
|
+
reason: "invalid-topics",
|
|
408
|
+
message: `The topic wasn't saved. ${problems.join(" ")}`,
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
const changed = transact(deps.db, () => {
|
|
412
|
+
const row = deps.db
|
|
413
|
+
.prepare("UPDATE schedule_topics SET title=? WHERE id=? AND schedule_id=? AND state='held'")
|
|
414
|
+
.run(parsed.data, topicId, scheduleId);
|
|
415
|
+
if (Number(row.changes) !== 1)
|
|
416
|
+
return false;
|
|
417
|
+
if (values !== undefined)
|
|
418
|
+
writeHeldValues(deps.db, (stored) => ({ ...stored, [topicId]: values }));
|
|
419
|
+
return true;
|
|
420
|
+
});
|
|
421
|
+
if (!changed)
|
|
422
|
+
return { ok: false, reason: "topic-not-found" };
|
|
423
|
+
const topic = heldTopics(deps, scheduleId).find((row) => row.id === topicId);
|
|
424
|
+
return topic === undefined
|
|
425
|
+
? { ok: false, reason: "topic-not-found" }
|
|
426
|
+
: { ok: true, value: topic };
|
|
427
|
+
}
|
|
428
|
+
// Moves one queued topic to another position. Each run takes the first topic, so this is
|
|
429
|
+
// also what reorders the calendar.
|
|
430
|
+
export function moveTopic(deps, scheduleId, input) {
|
|
431
|
+
const parsed = topicMoveSchema.safeParse(input);
|
|
432
|
+
if (!parsed.success)
|
|
433
|
+
return { ok: false, reason: "invalid-input" };
|
|
434
|
+
return transact(deps.db, () => {
|
|
435
|
+
const open = openSchedule(deps, scheduleId);
|
|
436
|
+
if (!open.ok)
|
|
437
|
+
return open;
|
|
438
|
+
const schedule = open.value;
|
|
439
|
+
if (schedule.version !== parsed.data.baseVersion)
|
|
440
|
+
return { ok: false, reason: "conflict" };
|
|
441
|
+
const { from } = parsed.data;
|
|
442
|
+
if (from >= schedule.items.length)
|
|
443
|
+
return { ok: false, reason: "invalid-input" };
|
|
444
|
+
const items = [...schedule.items];
|
|
445
|
+
const [topic] = items.splice(from, 1);
|
|
446
|
+
if (topic === undefined)
|
|
447
|
+
return { ok: false, reason: "invalid-input" };
|
|
448
|
+
items.splice(Math.min(parsed.data.to, items.length), 0, topic);
|
|
449
|
+
writeItems(deps, scheduleId, schedule.version, items);
|
|
450
|
+
return { ok: true, value: scheduleById(deps.db, scheduleId) };
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
// Saves the queue as sent, without touching the schedule's other settings or its next run.
|
|
454
|
+
// Topics already queued are not checked again; a new or renamed one must suit the template.
|
|
455
|
+
export function replaceTopics(deps, scheduleId, input) {
|
|
456
|
+
const parsed = topicQueueSchema.safeParse(input);
|
|
457
|
+
if (!parsed.success)
|
|
458
|
+
return { ok: false, reason: "invalid-input" };
|
|
459
|
+
return transact(deps.db, () => {
|
|
460
|
+
const open = openSchedule(deps, scheduleId);
|
|
461
|
+
if (!open.ok)
|
|
462
|
+
return open;
|
|
463
|
+
const schedule = open.value;
|
|
464
|
+
if (schedule.version !== parsed.data.baseVersion)
|
|
465
|
+
return { ok: false, reason: "conflict" };
|
|
466
|
+
const template = deps.template(schedule.templateId, schedule.templateVersion);
|
|
467
|
+
if (!template)
|
|
468
|
+
return { ok: false, reason: "missing-template" };
|
|
469
|
+
const problems = topicRowProblems(parsed.data.items, {
|
|
470
|
+
keywords: templateKeywords(template.document.form),
|
|
471
|
+
topicKeyword: schedule.topicKeyword,
|
|
472
|
+
kept: schedule.items,
|
|
473
|
+
});
|
|
474
|
+
if (problems.length > 0)
|
|
475
|
+
return { ok: false, reason: "invalid-topics", message: problems.slice(0, 5).join(" ") };
|
|
476
|
+
writeItems(deps, scheduleId, schedule.version, parsed.data.items);
|
|
477
|
+
return { ok: true, value: scheduleById(deps.db, scheduleId) };
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
// Takes one queued topic off this schedule and puts it on another's queue.
|
|
481
|
+
export function transferTopic(deps, scheduleId, input) {
|
|
482
|
+
const parsed = topicTransferSchema.safeParse(input);
|
|
483
|
+
if (!parsed.success)
|
|
484
|
+
return { ok: false, reason: "invalid-input" };
|
|
485
|
+
if (parsed.data.targetId === scheduleId)
|
|
486
|
+
return { ok: false, reason: "invalid-input" };
|
|
487
|
+
return transact(deps.db, () => {
|
|
488
|
+
const source = openSchedule(deps, scheduleId);
|
|
489
|
+
if (!source.ok)
|
|
490
|
+
return source;
|
|
491
|
+
const target = openSchedule(deps, parsed.data.targetId);
|
|
492
|
+
if (!target.ok)
|
|
493
|
+
return target;
|
|
494
|
+
if (source.value.version !== parsed.data.baseVersion)
|
|
495
|
+
return { ok: false, reason: "conflict" };
|
|
496
|
+
const items = [...source.value.items];
|
|
497
|
+
const [topic] = items.splice(parsed.data.index, 1);
|
|
498
|
+
if (topic === undefined)
|
|
499
|
+
return { ok: false, reason: "invalid-input" };
|
|
500
|
+
if (target.value.items.length + 1 > queueMax)
|
|
501
|
+
return { ok: false, reason: "queue-full" };
|
|
502
|
+
const landing = [...target.value.items];
|
|
503
|
+
landing.splice(Math.min(parsed.data.position ?? landing.length, landing.length), 0, topic);
|
|
504
|
+
writeItems(deps, scheduleId, source.value.version, items);
|
|
505
|
+
writeItems(deps, target.value.id, target.value.version, landing);
|
|
506
|
+
return {
|
|
507
|
+
ok: true,
|
|
508
|
+
value: {
|
|
509
|
+
source: scheduleById(deps.db, scheduleId),
|
|
510
|
+
target: scheduleById(deps.db, target.value.id),
|
|
511
|
+
},
|
|
512
|
+
};
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
function writeItems(deps, scheduleId, version, items) {
|
|
516
|
+
deps.db
|
|
517
|
+
.prepare("UPDATE schedules SET items_json=?,version=version+1,updated_at=? WHERE id=? AND version=?")
|
|
518
|
+
.run(JSON.stringify(items), deps.clock.now().toISOString(), scheduleId, version);
|
|
519
|
+
}
|
|
520
|
+
// Boot: a generation cannot outlive the process that asked.
|
|
521
|
+
export function releaseTopicLeases(deps) {
|
|
522
|
+
deps.db.prepare("UPDATE schedules SET topics_generating_at=NULL").run();
|
|
523
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { readinessIsUsable } from "../../kernel/ports/model.js";
|
|
3
|
+
import { keyedProviders, readSetting, writeSetting } from "./repo.js";
|
|
4
|
+
// What a fresh Play form picks before the user picks anything. Written once, on the first
|
|
5
|
+
// launch that finds a command-line tool, and read by Play for every new draft.
|
|
6
|
+
const choiceSchema = z.object({ provider: z.string().min(1), model: z.string() }).strict();
|
|
7
|
+
export const providerDefaultsSchema = z
|
|
8
|
+
.object({ llm: choiceSchema.optional(), images: choiceSchema.optional() })
|
|
9
|
+
.strict();
|
|
10
|
+
export const providerDefaultsKey = "provider.defaults";
|
|
11
|
+
const defaultsKey = providerDefaultsKey;
|
|
12
|
+
const doneKey = "first-run.done";
|
|
13
|
+
// The order a text tool is preferred in when several are found.
|
|
14
|
+
const textPreference = ["claude-code", "codex", "gemini"];
|
|
15
|
+
export function readProviderDefaults(db) {
|
|
16
|
+
const raw = readSetting(db, defaultsKey);
|
|
17
|
+
if (raw === undefined)
|
|
18
|
+
return {};
|
|
19
|
+
try {
|
|
20
|
+
const parsed = providerDefaultsSchema.safeParse(JSON.parse(raw));
|
|
21
|
+
return parsed.success ? parsed.data : {};
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return {};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function dismissFirstRun(db) {
|
|
28
|
+
writeSetting(db, doneKey, "1");
|
|
29
|
+
}
|
|
30
|
+
// First launch: find Claude Code, Codex and Gemini, pick the found ones as Play's defaults, and
|
|
31
|
+
// say a video can be made without any API key. Later launches only report what was found.
|
|
32
|
+
export async function firstRunStatus(deps, statuses) {
|
|
33
|
+
const detected = statuses
|
|
34
|
+
.filter((status) => status.readiness.kind === "cli")
|
|
35
|
+
.map((status) => {
|
|
36
|
+
const readiness = status.readiness;
|
|
37
|
+
if (readiness.kind !== "cli")
|
|
38
|
+
throw new Error("Expected a command-line provider.");
|
|
39
|
+
return {
|
|
40
|
+
id: status.id,
|
|
41
|
+
displayName: status.id === "codex-image" ? "Codex CLI (images)" : status.displayName,
|
|
42
|
+
installed: readiness.installed,
|
|
43
|
+
usable: readinessIsUsable(readiness),
|
|
44
|
+
...(readiness.version === undefined ? {} : { version: readiness.version }),
|
|
45
|
+
...(readiness.issue === undefined ? {} : { issue: readiness.issue }),
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
const firstRun = keyedProviders(deps.db).size === 0 && readSetting(deps.db, doneKey) === undefined;
|
|
49
|
+
let defaults = readProviderDefaults(deps.db);
|
|
50
|
+
const usable = new Set(detected.filter((cli) => cli.usable).map((cli) => cli.id));
|
|
51
|
+
const text = textPreference.find((id) => usable.has(id));
|
|
52
|
+
if (firstRun &&
|
|
53
|
+
readSetting(deps.db, defaultsKey) === undefined &&
|
|
54
|
+
(text !== undefined || usable.has("codex-image"))) {
|
|
55
|
+
defaults = {
|
|
56
|
+
...(text === undefined
|
|
57
|
+
? {}
|
|
58
|
+
: { llm: { provider: text, model: await firstModel(deps, text, "llm") } }),
|
|
59
|
+
...(usable.has("codex-image")
|
|
60
|
+
? {
|
|
61
|
+
images: {
|
|
62
|
+
provider: "codex-image",
|
|
63
|
+
model: await firstModel(deps, "codex-image", "image"),
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
: {}),
|
|
67
|
+
};
|
|
68
|
+
writeSetting(deps.db, defaultsKey, JSON.stringify(defaults));
|
|
69
|
+
}
|
|
70
|
+
const names = detected
|
|
71
|
+
.filter((cli) => cli.usable && cli.id !== "codex-image")
|
|
72
|
+
.map((cli) => cli.displayName);
|
|
73
|
+
const systemVoice = statuses.some((status) => status.readiness.kind === "local" && status.readiness.available);
|
|
74
|
+
const narration = systemVoice
|
|
75
|
+
? "Narration can use your computer's built-in voice (System voice in Settings → Voices); add an ElevenLabs or OpenAI key later for a better one."
|
|
76
|
+
: "Narration still needs a voice provider key, or turn Audio off or upload your own.";
|
|
77
|
+
return {
|
|
78
|
+
firstRun,
|
|
79
|
+
detected,
|
|
80
|
+
defaults,
|
|
81
|
+
message: !firstRun
|
|
82
|
+
? null
|
|
83
|
+
: names.length > 0
|
|
84
|
+
? "You can make a video now, no API keys needed."
|
|
85
|
+
: "No AI command-line tool was found on this computer.",
|
|
86
|
+
detail: !firstRun
|
|
87
|
+
? null
|
|
88
|
+
: names.length > 0
|
|
89
|
+
? `Slopify found ${list(names)} and picked ${names.length === 1 ? "it" : "them"} on Play. ${narration} Keys are optional extras in Settings → Providers.`
|
|
90
|
+
: "Install Claude Code, Codex or Gemini CLI and sign in once in a terminal, then reload this page. Or paste an API key in Settings → Providers.",
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
async function firstModel(deps, provider, family) {
|
|
94
|
+
try {
|
|
95
|
+
return (await deps.modelsFor?.(provider, family))?.[0]?.id ?? "";
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// The picker shows the tool's list once it answers; the provider is still chosen.
|
|
99
|
+
return "";
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function list(names) {
|
|
103
|
+
return names.length <= 1
|
|
104
|
+
? (names[0] ?? "")
|
|
105
|
+
: `${names.slice(0, -1).join(", ")} and ${names[names.length - 1]}`;
|
|
106
|
+
}
|