@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
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { castImageMaxBytes, castImagesPerMember, sniffImage, storeImageBlob } from "./images.js";
|
|
2
|
+
import { castMemberById } from "./repo.js";
|
|
3
|
+
import { castGenerateSchema } from "./schema.js";
|
|
4
|
+
export function uploadCastImage(deps, memberId, bytes) {
|
|
5
|
+
const room = roomFor(deps, memberId);
|
|
6
|
+
if (!room.ok)
|
|
7
|
+
return room;
|
|
8
|
+
if (bytes.byteLength > castImageMaxBytes)
|
|
9
|
+
return {
|
|
10
|
+
ok: false,
|
|
11
|
+
reason: "not-an-image",
|
|
12
|
+
message: `This picture is larger than ${String(castImageMaxBytes / 1024 / 1024)} MB. Save it smaller (for example as a JPEG) and upload it again.`,
|
|
13
|
+
};
|
|
14
|
+
const mime = sniffImage(bytes);
|
|
15
|
+
if (mime === undefined)
|
|
16
|
+
return {
|
|
17
|
+
ok: false,
|
|
18
|
+
reason: "not-an-image",
|
|
19
|
+
message: "This file is not a PNG or JPEG picture. Upload a PNG or JPEG file.",
|
|
20
|
+
};
|
|
21
|
+
const at = deps.clock.now().toISOString();
|
|
22
|
+
const sha256 = storeImageBlob(deps.db, bytes, mime, at);
|
|
23
|
+
const id = deps.uuid();
|
|
24
|
+
deps.db
|
|
25
|
+
.prepare("INSERT INTO cast_images(id,member_id,source,prompt,state,error,sha256,created_at) VALUES (?,?,'upload',NULL,'ready',NULL,?,?)")
|
|
26
|
+
.run(id, memberId, sha256, at);
|
|
27
|
+
return { ok: true, value: imageRow(deps, id) };
|
|
28
|
+
}
|
|
29
|
+
// Adds a row in "generating" and returns it at once; the picture lands in the row when the
|
|
30
|
+
// provider answers, or the row turns "failed" with the reason. Characters and creatures are
|
|
31
|
+
// drawn upright, places and objects wide.
|
|
32
|
+
export function generateCastImage(deps, memberId, input) {
|
|
33
|
+
const parsed = castGenerateSchema.safeParse(input);
|
|
34
|
+
if (!parsed.success)
|
|
35
|
+
return { ok: false, reason: "invalid-input" };
|
|
36
|
+
const room = roomFor(deps, memberId);
|
|
37
|
+
if (!room.ok)
|
|
38
|
+
return room;
|
|
39
|
+
const generate = deps.generateImage;
|
|
40
|
+
if (generate === undefined)
|
|
41
|
+
return { ok: false, reason: "no-image-provider" };
|
|
42
|
+
const id = deps.uuid();
|
|
43
|
+
deps.db
|
|
44
|
+
.prepare("INSERT INTO cast_images(id,member_id,source,prompt,state,error,sha256,created_at) VALUES (?,?,'generate',?,'generating',NULL,NULL,?)")
|
|
45
|
+
.run(id, memberId, parsed.data.prompt, deps.clock.now().toISOString());
|
|
46
|
+
void generate({
|
|
47
|
+
...parsed.data,
|
|
48
|
+
channelId: room.value.channelId,
|
|
49
|
+
aspect: aspectOf(room.value.kind),
|
|
50
|
+
}).then((image) => {
|
|
51
|
+
const mime = sniffImage(image.bytes);
|
|
52
|
+
if (mime === undefined || image.bytes.byteLength > castImageMaxBytes) {
|
|
53
|
+
fail(deps, id, "The provider answered with something that is not a usable PNG or JPEG picture. Press Generate again, or upload a picture instead.");
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const sha256 = storeImageBlob(deps.db, image.bytes, mime, deps.clock.now().toISOString());
|
|
57
|
+
deps.db
|
|
58
|
+
.prepare("UPDATE cast_images SET state='ready',sha256=? WHERE id=? AND state='generating'")
|
|
59
|
+
.run(sha256, id);
|
|
60
|
+
}, (error) => {
|
|
61
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
62
|
+
deps.log?.write("warn", "channels.cast-image.failed", { detail: reason.slice(0, 500) });
|
|
63
|
+
fail(deps, id, `The picture couldn't be made: ${reason.slice(0, 500)} Check the image provider in Settings → Providers, then press Generate again.`);
|
|
64
|
+
});
|
|
65
|
+
return { ok: true, value: imageRow(deps, id) };
|
|
66
|
+
}
|
|
67
|
+
export function deleteCastImage(deps, memberId, imageId) {
|
|
68
|
+
// The bytes stay: a project started with this picture still names it.
|
|
69
|
+
const changed = deps.db
|
|
70
|
+
.prepare("DELETE FROM cast_images WHERE id=? AND member_id=?")
|
|
71
|
+
.run(imageId, memberId);
|
|
72
|
+
return Number(changed.changes) === 0
|
|
73
|
+
? { ok: false, reason: "not-found" }
|
|
74
|
+
: { ok: true, value: { deleted: true } };
|
|
75
|
+
}
|
|
76
|
+
// A picture still being made when Slopify stopped will never land.
|
|
77
|
+
export function settleInterruptedCastImages(deps) {
|
|
78
|
+
const changed = deps.db
|
|
79
|
+
.prepare("UPDATE cast_images SET state='failed',error='Slopify stopped before this picture was finished. Press Generate again.' WHERE state='generating'")
|
|
80
|
+
.run();
|
|
81
|
+
return Number(changed.changes);
|
|
82
|
+
}
|
|
83
|
+
function roomFor(deps, memberId) {
|
|
84
|
+
const member = castMemberById(deps.db, memberId);
|
|
85
|
+
if (member === undefined)
|
|
86
|
+
return { ok: false, reason: "not-found" };
|
|
87
|
+
if (member.images.filter((image) => image.state !== "failed").length >= castImagesPerMember)
|
|
88
|
+
return {
|
|
89
|
+
ok: false,
|
|
90
|
+
reason: "too-many-images",
|
|
91
|
+
message: `${member.name} already has ${String(castImagesPerMember)} pictures. Delete one before adding another.`,
|
|
92
|
+
};
|
|
93
|
+
return { ok: true, value: { kind: member.kind, channelId: member.channelId } };
|
|
94
|
+
}
|
|
95
|
+
function aspectOf(kind) {
|
|
96
|
+
return kind === "character" || kind === "creature" ? "9:16" : "16:9";
|
|
97
|
+
}
|
|
98
|
+
function fail(deps, id, error) {
|
|
99
|
+
deps.db
|
|
100
|
+
.prepare("UPDATE cast_images SET state='failed',error=? WHERE id=? AND state='generating'")
|
|
101
|
+
.run(error, id);
|
|
102
|
+
}
|
|
103
|
+
function imageRow(deps, id) {
|
|
104
|
+
const row = deps.db.prepare("SELECT member_id FROM cast_images WHERE id=?").get(id);
|
|
105
|
+
const member = castMemberById(deps.db, String(row?.member_id));
|
|
106
|
+
const image = member?.images.find((value) => value.id === id);
|
|
107
|
+
if (image === undefined)
|
|
108
|
+
throw new Error("The cast picture could not be read back");
|
|
109
|
+
return image;
|
|
110
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Which cast members a text mentions: a member's name or one of its aliases as a whole word,
|
|
2
|
+
// ignoring case. A word boundary is anything that is not a letter or digit in any script, so
|
|
3
|
+
// "Tiamat's lair" and "(Tiamat)" mention Tiamat and "Tiamatic" or "Tiamats" do not; a plural
|
|
4
|
+
// is an alias to add. Spaces inside a name match any run of whitespace. The members come back
|
|
5
|
+
// in the order the text first mentions them, then in cast order.
|
|
6
|
+
export function castMentions(text, cast) {
|
|
7
|
+
if (cast === undefined || cast.length === 0 || text.trim() === "")
|
|
8
|
+
return [];
|
|
9
|
+
const found = [];
|
|
10
|
+
for (const [order, member] of cast.entries()) {
|
|
11
|
+
let first = Number.POSITIVE_INFINITY;
|
|
12
|
+
for (const term of [member.name, ...member.aliases]) {
|
|
13
|
+
const at = mentionAt(text, term);
|
|
14
|
+
if (at !== undefined && at < first)
|
|
15
|
+
first = at;
|
|
16
|
+
}
|
|
17
|
+
if (Number.isFinite(first))
|
|
18
|
+
found.push({ member, at: first, order });
|
|
19
|
+
}
|
|
20
|
+
return found.sort((a, b) => a.at - b.at || a.order - b.order).map((row) => row.member);
|
|
21
|
+
}
|
|
22
|
+
function mentionAt(text, term) {
|
|
23
|
+
const words = term.trim().split(/\s+/u).filter(Boolean);
|
|
24
|
+
if (words.length === 0)
|
|
25
|
+
return undefined;
|
|
26
|
+
const pattern = new RegExp(`(?<![\\p{L}\\p{N}])${words.map(escapeRegExp).join("\\s+")}(?![\\p{L}\\p{N}])`, "iu");
|
|
27
|
+
const match = pattern.exec(text);
|
|
28
|
+
return match === null ? undefined : match.index;
|
|
29
|
+
}
|
|
30
|
+
function escapeRegExp(word) {
|
|
31
|
+
return word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
32
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { insertImageBlob } from "./repo.js";
|
|
3
|
+
// What a picture sent as a reference may weigh: the host helper's bridge carries at most this
|
|
4
|
+
// much per reference image (`bridgeLimits.reference`).
|
|
5
|
+
export const castImageMaxBytes = 10 * 1024 * 1024;
|
|
6
|
+
// Per cast member. Every one is sent with each image that mentions the member.
|
|
7
|
+
export const castImagesPerMember = 4;
|
|
8
|
+
export function sniffImage(bytes) {
|
|
9
|
+
const png = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
|
|
10
|
+
if (bytes.length >= png.length && png.every((value, index) => bytes[index] === value))
|
|
11
|
+
return "image/png";
|
|
12
|
+
return bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff ? "image/jpeg" : undefined;
|
|
13
|
+
}
|
|
14
|
+
// Stores the bytes under their SHA-256 and returns it; the same picture twice is one row.
|
|
15
|
+
export function storeImageBlob(db, bytes, mime, at) {
|
|
16
|
+
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
|
17
|
+
insertImageBlob(db, { sha256, mime, bytes }, at);
|
|
18
|
+
return sha256;
|
|
19
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Every install has this channel (migration 0032). A template or project whose channel is
|
|
2
|
+
// NULL, or names a channel this install does not have, belongs to it.
|
|
3
|
+
export const defaultChannelId = "00000000-0000-4000-8000-000000000001";
|
|
4
|
+
export const defaultChannelName = "My channel";
|
|
5
|
+
export const castKinds = ["character", "creature", "place", "object"];
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// A project moved to another channel in Edit project, or its brand kit switched on or off:
|
|
2
|
+
// the run takes the new channel's cast as it is now and its brand kit again, as Play does when
|
|
3
|
+
// a run starts (`runs.ts`). The kit fills what is at its default; a value the old channel's kit
|
|
4
|
+
// had put there is replaced by the new kit's, and one the person set themselves is kept.
|
|
5
|
+
// Nothing changes while the channel and the switch stay as they were, so saving any other
|
|
6
|
+
// edit leaves the project's cast and brand exactly as they were.
|
|
7
|
+
import { defaultDocumentTheme } from "../document/model.js";
|
|
8
|
+
import { entryByName } from "../library/repo.js";
|
|
9
|
+
import { channelById, resolveChannelId } from "./repo.js";
|
|
10
|
+
import { brandDocument, castSnapshot } from "./runs.js";
|
|
11
|
+
export function channelChanged(db, before, after) {
|
|
12
|
+
return resolveChannelId(db, before.channelId) !== resolveChannelId(db, after.channelId);
|
|
13
|
+
}
|
|
14
|
+
export function rebrandedEdit(db, base, edit) {
|
|
15
|
+
const oldId = resolveChannelId(db, base.channelId);
|
|
16
|
+
const newId = resolveChannelId(db, edit.config.channelId);
|
|
17
|
+
const oldKit = base.useBrandKit !== false;
|
|
18
|
+
const newKit = edit.config.useBrandKit !== false;
|
|
19
|
+
if (oldId === newId && oldKit === newKit)
|
|
20
|
+
return edit;
|
|
21
|
+
const oldBrand = oldKit ? (channelById(db, oldId)?.brand ?? {}) : {};
|
|
22
|
+
const newBrand = newKit ? (channelById(db, newId)?.brand ?? {}) : {};
|
|
23
|
+
const { useBrandKit: _kit, cast: _cast, ...rest } = edit.config;
|
|
24
|
+
let config = {
|
|
25
|
+
...rest,
|
|
26
|
+
channelId: newId,
|
|
27
|
+
...(newKit ? {} : { useBrandKit: false }),
|
|
28
|
+
};
|
|
29
|
+
// The cast goes with the channel; the brand kit switch never changes it.
|
|
30
|
+
const cast = oldId === newId ? edit.config.cast : castSnapshot(db, newId);
|
|
31
|
+
if (cast !== undefined && cast.length > 0)
|
|
32
|
+
config = { ...config, cast };
|
|
33
|
+
let content = edit.content;
|
|
34
|
+
if (config.subtitles !== undefined) {
|
|
35
|
+
const subtitles = { ...config.subtitles };
|
|
36
|
+
const font = swapped(subtitles.fontId, oldBrand.captionFontId, newBrand.captionFontId, "default");
|
|
37
|
+
subtitles.fontId = font ?? "default";
|
|
38
|
+
assign(subtitles, "color", swapped(subtitles.color, oldBrand.captionColor, newBrand.captionColor));
|
|
39
|
+
assign(subtitles, "outlineColor", swapped(subtitles.outlineColor, oldBrand.captionOutlineColor, newBrand.captionOutlineColor));
|
|
40
|
+
config = { ...config, subtitles };
|
|
41
|
+
}
|
|
42
|
+
const titleStyle = {};
|
|
43
|
+
assign(titleStyle, "fontId", swapped(config.titleStyle?.fontId, oldBrand.titleFontId, newBrand.titleFontId));
|
|
44
|
+
assign(titleStyle, "color", swapped(config.titleStyle?.color, oldBrand.titleColor, newBrand.titleColor));
|
|
45
|
+
const { titleStyle: _title, endScreen: _end, ...unstyled } = config;
|
|
46
|
+
const endScreen = swapped(config.endScreen?.text, oldBrand.endScreenText, newBrand.endScreenText);
|
|
47
|
+
config = {
|
|
48
|
+
...unstyled,
|
|
49
|
+
...(Object.keys(titleStyle).length === 0 ? {} : { titleStyle }),
|
|
50
|
+
...(endScreen === undefined ? {} : { endScreen: { text: endScreen } }),
|
|
51
|
+
};
|
|
52
|
+
// An intro or outro is read only while narration is generated.
|
|
53
|
+
if (config.sources.audio === "generate")
|
|
54
|
+
for (const category of ["intro", "outro"]) {
|
|
55
|
+
const current = config[category]?.name;
|
|
56
|
+
const old = oldBrand[category];
|
|
57
|
+
const fromOldKit = current === undefined || (old !== undefined && current.toLowerCase() === old.toLowerCase());
|
|
58
|
+
if (!fromOldKit)
|
|
59
|
+
continue;
|
|
60
|
+
const name = newBrand[category];
|
|
61
|
+
const entry = name === undefined ? undefined : entryByName(db, category, name);
|
|
62
|
+
if (entry === undefined) {
|
|
63
|
+
if (current === undefined)
|
|
64
|
+
continue;
|
|
65
|
+
const { [category]: _dropped, ...without } = config;
|
|
66
|
+
config = without;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
config = { ...config, [category]: { name: entry.name, mode: entry.mode } };
|
|
70
|
+
content = {
|
|
71
|
+
...content,
|
|
72
|
+
promptTemplates: { ...content.promptTemplates, [category]: entry.body },
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
// The PDF's theme, while the project makes one.
|
|
76
|
+
if (config.document !== undefined) {
|
|
77
|
+
const identity = config.document.custom?.id ?? config.document.theme;
|
|
78
|
+
const fromOldKit = identity === (oldBrand.documentTheme ?? defaultDocumentTheme) ||
|
|
79
|
+
(config.document.custom === undefined && config.document.theme === defaultDocumentTheme);
|
|
80
|
+
if (fromOldKit) {
|
|
81
|
+
const document = brandDocument(db, newBrand) ?? {
|
|
82
|
+
theme: defaultDocumentTheme,
|
|
83
|
+
};
|
|
84
|
+
config = { ...config, document };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return { ...edit, config, content };
|
|
88
|
+
}
|
|
89
|
+
// The value a brand-kit setting takes: the new kit's when the current one is unset, the
|
|
90
|
+
// fallback, or what the old kit put there; otherwise the person's own, kept.
|
|
91
|
+
function swapped(current, old, next, fallback) {
|
|
92
|
+
const fromOldKit = current === undefined || current === fallback || (old !== undefined && current === old);
|
|
93
|
+
return fromOldKit ? (next ?? fallback) : current;
|
|
94
|
+
}
|
|
95
|
+
function assign(target, key, value) {
|
|
96
|
+
if (value === undefined)
|
|
97
|
+
delete target[key];
|
|
98
|
+
else
|
|
99
|
+
target[key] = value;
|
|
100
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { aiDisclosureSettings } from "../studio/disclosure.js";
|
|
3
|
+
import { channelAmbientBedSchema } from "../video/ambient-bed-schema.js";
|
|
4
|
+
import { castKinds, defaultChannelId, } from "./model.js";
|
|
5
|
+
const brandSchema = z
|
|
6
|
+
.object({
|
|
7
|
+
captionFontId: z.string().optional(),
|
|
8
|
+
captionColor: z.string().optional(),
|
|
9
|
+
captionOutlineColor: z.string().optional(),
|
|
10
|
+
titleFontId: z.string().optional(),
|
|
11
|
+
titleColor: z.string().optional(),
|
|
12
|
+
intro: z.string().optional(),
|
|
13
|
+
outro: z.string().optional(),
|
|
14
|
+
endScreenText: z.string().optional(),
|
|
15
|
+
documentTheme: z.string().optional(),
|
|
16
|
+
ambientBed: channelAmbientBedSchema.optional(),
|
|
17
|
+
language: z.string().optional(),
|
|
18
|
+
})
|
|
19
|
+
.readonly();
|
|
20
|
+
const channelRow = z.object({
|
|
21
|
+
id: z.string(),
|
|
22
|
+
name: z.string(),
|
|
23
|
+
is_default: z.number(),
|
|
24
|
+
brand_json: z.string(),
|
|
25
|
+
series_brief: z.string(),
|
|
26
|
+
ai_disclosure: z.enum(aiDisclosureSettings),
|
|
27
|
+
version: z.number(),
|
|
28
|
+
created_at: z.string(),
|
|
29
|
+
updated_at: z.string(),
|
|
30
|
+
});
|
|
31
|
+
const memberRow = z.object({
|
|
32
|
+
id: z.string(),
|
|
33
|
+
channel_id: z.string(),
|
|
34
|
+
kind: z.enum(castKinds),
|
|
35
|
+
name: z.string(),
|
|
36
|
+
aliases_json: z.string(),
|
|
37
|
+
description: z.string(),
|
|
38
|
+
voice_json: z.string().nullable().optional(),
|
|
39
|
+
version: z.number(),
|
|
40
|
+
created_at: z.string(),
|
|
41
|
+
updated_at: z.string(),
|
|
42
|
+
});
|
|
43
|
+
const voiceJson = z.object({
|
|
44
|
+
provider: z.string(),
|
|
45
|
+
model: z.string(),
|
|
46
|
+
voice: z.string(),
|
|
47
|
+
pace: z.number().optional(),
|
|
48
|
+
pronunciations: z.string().optional(),
|
|
49
|
+
});
|
|
50
|
+
const imageRow = z.object({
|
|
51
|
+
id: z.string(),
|
|
52
|
+
member_id: z.string(),
|
|
53
|
+
source: z.enum(["upload", "generate"]),
|
|
54
|
+
prompt: z.string().nullable(),
|
|
55
|
+
state: z.enum(["ready", "generating", "failed"]),
|
|
56
|
+
error: z.string().nullable(),
|
|
57
|
+
sha256: z.string().nullable(),
|
|
58
|
+
created_at: z.string(),
|
|
59
|
+
});
|
|
60
|
+
function channelOf(row) {
|
|
61
|
+
const value = channelRow.parse(row);
|
|
62
|
+
return {
|
|
63
|
+
id: value.id,
|
|
64
|
+
name: value.name,
|
|
65
|
+
isDefault: value.is_default === 1,
|
|
66
|
+
brand: brandSchema.parse(JSON.parse(value.brand_json)),
|
|
67
|
+
seriesBrief: value.series_brief,
|
|
68
|
+
aiDisclosure: value.ai_disclosure,
|
|
69
|
+
version: value.version,
|
|
70
|
+
createdAt: value.created_at,
|
|
71
|
+
updatedAt: value.updated_at,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
export function channelById(db, id) {
|
|
75
|
+
const row = db.prepare("SELECT * FROM channels WHERE id=?").get(id);
|
|
76
|
+
return row === undefined ? undefined : channelOf(row);
|
|
77
|
+
}
|
|
78
|
+
// A channel id as stored on a template or project: NULL or one this install does not have
|
|
79
|
+
// reads as the default channel.
|
|
80
|
+
export function resolveChannelId(db, id) {
|
|
81
|
+
if (id === null || id === undefined)
|
|
82
|
+
return defaultChannelId;
|
|
83
|
+
return db.prepare("SELECT 1 FROM channels WHERE id=?").get(id) === undefined
|
|
84
|
+
? defaultChannelId
|
|
85
|
+
: id;
|
|
86
|
+
}
|
|
87
|
+
export function channelSummaries(db) {
|
|
88
|
+
const templates = counts(db, "SELECT coalesce(channel_id,'') AS id, count(*) AS n FROM project_templates WHERE deleted_at IS NULL GROUP BY channel_id");
|
|
89
|
+
const cast = counts(db, "SELECT channel_id AS id, count(*) AS n FROM cast_members GROUP BY channel_id");
|
|
90
|
+
const rows = db
|
|
91
|
+
.prepare("SELECT * FROM channels ORDER BY is_default DESC, lower(name), id")
|
|
92
|
+
.all()
|
|
93
|
+
.map(channelOf);
|
|
94
|
+
const known = new Set(rows.map((row) => row.id));
|
|
95
|
+
// Templates naming no channel, or one that is gone, are the default channel's.
|
|
96
|
+
const stray = [...templates.entries()]
|
|
97
|
+
.filter(([id]) => !known.has(id))
|
|
98
|
+
.reduce((sum, [, n]) => sum + n, 0);
|
|
99
|
+
return rows.map((row) => ({
|
|
100
|
+
...row,
|
|
101
|
+
templates: (templates.get(row.id) ?? 0) + (row.isDefault ? stray : 0),
|
|
102
|
+
cast: cast.get(row.id) ?? 0,
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
function counts(db, sql) {
|
|
106
|
+
const result = new Map();
|
|
107
|
+
for (const row of db.prepare(sql).all())
|
|
108
|
+
result.set(String(row.id), Number(row.n));
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
export function castOfChannel(db, channelId) {
|
|
112
|
+
const members = db
|
|
113
|
+
.prepare("SELECT * FROM cast_members WHERE channel_id=? ORDER BY lower(name), id")
|
|
114
|
+
.all(channelId)
|
|
115
|
+
.map((row) => memberRow.parse(row));
|
|
116
|
+
const images = new Map();
|
|
117
|
+
for (const row of db
|
|
118
|
+
.prepare("SELECT i.* FROM cast_images i JOIN cast_members m ON m.id=i.member_id WHERE m.channel_id=? ORDER BY i.created_at, i.rowid")
|
|
119
|
+
.all(channelId)) {
|
|
120
|
+
const image = imageRow.parse(row);
|
|
121
|
+
const list = images.get(image.member_id) ?? [];
|
|
122
|
+
list.push(imageOf(image));
|
|
123
|
+
images.set(image.member_id, list);
|
|
124
|
+
}
|
|
125
|
+
return members.map((row) => memberOf(row, images.get(row.id) ?? []));
|
|
126
|
+
}
|
|
127
|
+
export function castMemberById(db, id) {
|
|
128
|
+
const row = db.prepare("SELECT * FROM cast_members WHERE id=?").get(id);
|
|
129
|
+
if (row === undefined)
|
|
130
|
+
return undefined;
|
|
131
|
+
const images = db
|
|
132
|
+
.prepare("SELECT * FROM cast_images WHERE member_id=? ORDER BY created_at, rowid")
|
|
133
|
+
.all(id)
|
|
134
|
+
.map((value) => imageOf(imageRow.parse(value)));
|
|
135
|
+
return memberOf(memberRow.parse(row), images);
|
|
136
|
+
}
|
|
137
|
+
function memberOf(row, images) {
|
|
138
|
+
return {
|
|
139
|
+
id: row.id,
|
|
140
|
+
channelId: row.channel_id,
|
|
141
|
+
kind: row.kind,
|
|
142
|
+
name: row.name,
|
|
143
|
+
aliases: z.array(z.string()).parse(JSON.parse(row.aliases_json)),
|
|
144
|
+
description: row.description,
|
|
145
|
+
...(row.voice_json === null || row.voice_json === undefined
|
|
146
|
+
? {}
|
|
147
|
+
: { voice: voiceJson.parse(JSON.parse(row.voice_json)) }),
|
|
148
|
+
version: row.version,
|
|
149
|
+
images,
|
|
150
|
+
createdAt: row.created_at,
|
|
151
|
+
updatedAt: row.updated_at,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function imageOf(row) {
|
|
155
|
+
return {
|
|
156
|
+
id: row.id,
|
|
157
|
+
source: row.source,
|
|
158
|
+
prompt: row.prompt,
|
|
159
|
+
state: row.state,
|
|
160
|
+
error: row.error,
|
|
161
|
+
sha256: row.sha256,
|
|
162
|
+
createdAt: row.created_at,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
export function templateChannelId(db, templateId) {
|
|
166
|
+
const row = db.prepare("SELECT channel_id FROM project_templates WHERE id=?").get(templateId);
|
|
167
|
+
return resolveChannelId(db, typeof row?.channel_id === "string" ? row.channel_id : null);
|
|
168
|
+
}
|
|
169
|
+
// Every template's channel, for the lists that show or filter by it.
|
|
170
|
+
export function templateChannels(db) {
|
|
171
|
+
const result = new Map();
|
|
172
|
+
for (const row of db.prepare("SELECT id, channel_id FROM project_templates").all())
|
|
173
|
+
result.set(String(row.id), resolveChannelId(db, typeof row.channel_id === "string" ? row.channel_id : null));
|
|
174
|
+
return result;
|
|
175
|
+
}
|
|
176
|
+
// A schedule belongs to the channel of the template it runs, so moving a template moves its
|
|
177
|
+
// schedules with it and the series brief a schedule reads is always its channel's.
|
|
178
|
+
export function scheduleChannel(db, scheduleId) {
|
|
179
|
+
const row = db.prepare("SELECT template_id FROM schedules WHERE id=?").get(scheduleId);
|
|
180
|
+
if (typeof row?.template_id !== "string")
|
|
181
|
+
return undefined;
|
|
182
|
+
return channelById(db, templateChannelId(db, row.template_id));
|
|
183
|
+
}
|
|
184
|
+
export function projectChannelId(db, projectId) {
|
|
185
|
+
const row = db
|
|
186
|
+
.prepare("SELECT channel_id FROM project_channels WHERE project_id=?")
|
|
187
|
+
.get(projectId);
|
|
188
|
+
return resolveChannelId(db, typeof row?.channel_id === "string" ? row.channel_id : null);
|
|
189
|
+
}
|
|
190
|
+
// Every project's channel in one read, for the lists that show or filter by it. A project
|
|
191
|
+
// with no row, or naming a channel this install doesn't have, is in the default channel.
|
|
192
|
+
export function projectChannels(db) {
|
|
193
|
+
const known = new Set(db
|
|
194
|
+
.prepare("SELECT id FROM channels")
|
|
195
|
+
.all()
|
|
196
|
+
.map((row) => String(row.id)));
|
|
197
|
+
const result = new Map();
|
|
198
|
+
for (const row of db.prepare("SELECT project_id, channel_id FROM project_channels").all()) {
|
|
199
|
+
const channel = String(row.channel_id);
|
|
200
|
+
result.set(String(row.project_id), known.has(channel) ? channel : defaultChannelId);
|
|
201
|
+
}
|
|
202
|
+
return result;
|
|
203
|
+
}
|
|
204
|
+
export function setProjectChannel(db, projectId, channelId) {
|
|
205
|
+
db.prepare("INSERT INTO project_channels(project_id,channel_id) VALUES (?,?) ON CONFLICT(project_id) DO UPDATE SET channel_id=excluded.channel_id").run(projectId, channelId);
|
|
206
|
+
}
|
|
207
|
+
export function imageBlob(db, sha256) {
|
|
208
|
+
const row = db.prepare("SELECT mime, bytes FROM image_blobs WHERE sha256=?").get(sha256);
|
|
209
|
+
if (row === undefined)
|
|
210
|
+
return undefined;
|
|
211
|
+
const mime = z.enum(["image/png", "image/jpeg"]).parse(row.mime);
|
|
212
|
+
if (!(row.bytes instanceof Uint8Array))
|
|
213
|
+
return undefined;
|
|
214
|
+
return { bytes: new Uint8Array(row.bytes), mime };
|
|
215
|
+
}
|
|
216
|
+
export function insertImageBlob(db, blob, at) {
|
|
217
|
+
db.prepare("INSERT INTO image_blobs(sha256,mime,bytes,created_at) VALUES (?,?,?,?) ON CONFLICT(sha256) DO NOTHING").run(blob.sha256, blob.mime, blob.bytes, at);
|
|
218
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { languageSchema } from "../../kernel/ports/languages.js";
|
|
2
|
+
import { listDocumentThemes } from "../document/library.js";
|
|
3
|
+
import { documentThemes } from "../document/model.js";
|
|
4
|
+
import { ambientBedFormOf } from "../video/ambient-bed.js";
|
|
5
|
+
import { withCastVoices } from "../voices/cast.js";
|
|
6
|
+
import { castMemberById, castOfChannel, channelById, resolveChannelId, templateChannelId, } from "./repo.js";
|
|
7
|
+
// The channel a Play draft runs in: the one picked on Play, else its template's, else the
|
|
8
|
+
// default channel.
|
|
9
|
+
export function draftChannel(db, document) {
|
|
10
|
+
const id = document.channelId !== undefined
|
|
11
|
+
? resolveChannelId(db, document.channelId)
|
|
12
|
+
: document.templateSource !== undefined
|
|
13
|
+
? templateChannelId(db, document.templateSource.id)
|
|
14
|
+
: resolveChannelId(db, undefined);
|
|
15
|
+
const channel = channelById(db, id);
|
|
16
|
+
if (channel === undefined)
|
|
17
|
+
throw new Error("The default channel is missing");
|
|
18
|
+
return channel;
|
|
19
|
+
}
|
|
20
|
+
// The brand kit fills what the draft leaves at its default: the "default" caption font, no
|
|
21
|
+
// intro or outro, no document theme chosen, no ambient sound chosen. A template that sets its own keeps it, and one
|
|
22
|
+
// with "Use the channel's brand kit" off takes nothing from it. An intro, outro or theme the
|
|
23
|
+
// kit names that no longer exists is skipped, not refused: the draft never named it.
|
|
24
|
+
export function brandedForm(db, form, brand, entries) {
|
|
25
|
+
// The channel's language is not styling, so it applies with the brand kit off too; a
|
|
26
|
+
// language picked on Play (English included) is the draft's own.
|
|
27
|
+
const language = form.language === undefined && brand.language !== undefined
|
|
28
|
+
? { language: languageSchema.parse(brand.language) }
|
|
29
|
+
: {};
|
|
30
|
+
if (form.useBrandKit === false)
|
|
31
|
+
return { ...form, ...language };
|
|
32
|
+
const entry = (category, name) => name === undefined
|
|
33
|
+
? undefined
|
|
34
|
+
: entries.find((row) => row.category === category && row.name.toLowerCase() === name.toLowerCase())?.name;
|
|
35
|
+
const intro = form.intro === "" ? entry("intro", brand.intro) : undefined;
|
|
36
|
+
const outro = form.outro === "" ? entry("outro", brand.outro) : undefined;
|
|
37
|
+
const document = form.document === undefined ? brandDocument(db, brand) : undefined;
|
|
38
|
+
return {
|
|
39
|
+
...form,
|
|
40
|
+
...language,
|
|
41
|
+
...(form.subtitles.fontId === "default" && brand.captionFontId !== undefined
|
|
42
|
+
? { subtitles: { ...form.subtitles, fontId: brand.captionFontId } }
|
|
43
|
+
: {}),
|
|
44
|
+
...(intro === undefined ? {} : { intro }),
|
|
45
|
+
...(outro === undefined ? {} : { outro }),
|
|
46
|
+
...(document === undefined ? {} : { document }),
|
|
47
|
+
// A setup that never touched its ambient sound takes the channel's; one set to None keeps
|
|
48
|
+
// none.
|
|
49
|
+
...(form.ambientBed === undefined && brand.ambientBed !== undefined
|
|
50
|
+
? { ambientBed: ambientBedFormOf(brand.ambientBed) }
|
|
51
|
+
: {}),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export function brandDocument(db, brand) {
|
|
55
|
+
const name = brand.documentTheme;
|
|
56
|
+
if (name === undefined)
|
|
57
|
+
return undefined;
|
|
58
|
+
const builtIn = documentThemes.find((theme) => theme === name);
|
|
59
|
+
if (builtIn !== undefined)
|
|
60
|
+
return { theme: builtIn };
|
|
61
|
+
const saved = listDocumentThemes(db).find((theme) => theme.id === name);
|
|
62
|
+
return saved === undefined
|
|
63
|
+
? undefined
|
|
64
|
+
: { theme: "plain", custom: { id: saved.id, name: saved.name, values: saved.values } };
|
|
65
|
+
}
|
|
66
|
+
// What the run carries from its channel beyond the form: the channel itself, the caption and
|
|
67
|
+
// title colours, the end screen, and the cast. Each only when set, so a run from a channel
|
|
68
|
+
// with an empty kit and no cast is the run it always was.
|
|
69
|
+
export function brandedRun(draft, channel, cast, useBrandKit) {
|
|
70
|
+
const brand = useBrandKit ? channel.brand : {};
|
|
71
|
+
const colours = brand.captionColor === undefined && brand.captionOutlineColor === undefined
|
|
72
|
+
? {}
|
|
73
|
+
: {
|
|
74
|
+
...(brand.captionColor === undefined ? {} : { color: brand.captionColor }),
|
|
75
|
+
...(brand.captionOutlineColor === undefined
|
|
76
|
+
? {}
|
|
77
|
+
: { outlineColor: brand.captionOutlineColor }),
|
|
78
|
+
};
|
|
79
|
+
const titleStyle = {
|
|
80
|
+
...(brand.titleFontId === undefined ? {} : { fontId: brand.titleFontId }),
|
|
81
|
+
...(brand.titleColor === undefined ? {} : { color: brand.titleColor }),
|
|
82
|
+
};
|
|
83
|
+
return {
|
|
84
|
+
...draft,
|
|
85
|
+
channelId: channel.id,
|
|
86
|
+
// Kept only when off, so a run with the kit on saves the config it always did.
|
|
87
|
+
...(useBrandKit ? {} : { useBrandKit: false }),
|
|
88
|
+
...(draft.subtitles !== undefined && Object.keys(colours).length > 0
|
|
89
|
+
? { subtitles: { ...colours, ...draft.subtitles } }
|
|
90
|
+
: {}),
|
|
91
|
+
...(Object.keys(titleStyle).length === 0 || draft.titleStyle !== undefined
|
|
92
|
+
? {}
|
|
93
|
+
: { titleStyle }),
|
|
94
|
+
...(brand.endScreenText === undefined || draft.endScreen !== undefined
|
|
95
|
+
? {}
|
|
96
|
+
: { endScreen: { text: brand.endScreenText } }),
|
|
97
|
+
...(cast.length === 0 ? {} : { cast }),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
// The channel's cast as a run is started with it: members with at least one finished picture.
|
|
101
|
+
export function castSnapshot(db, channelId) {
|
|
102
|
+
return castOfChannel(db, channelId).flatMap((member) => {
|
|
103
|
+
const images = member.images.flatMap((image) => image.state === "ready" && image.sha256 !== null ? [image.sha256] : []);
|
|
104
|
+
return images.length === 0
|
|
105
|
+
? []
|
|
106
|
+
: [
|
|
107
|
+
{
|
|
108
|
+
name: member.name,
|
|
109
|
+
aliases: member.aliases,
|
|
110
|
+
description: member.description,
|
|
111
|
+
images,
|
|
112
|
+
},
|
|
113
|
+
];
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
// Speakers picked from the cast speak with the cast's voices (and show its portraits) as they
|
|
117
|
+
// are when the run starts. Every door a run starts through - Play, a schedule, a batch, the
|
|
118
|
+
// API - passes its draft through here once; a member is found by id, whichever channel it is
|
|
119
|
+
// on, and a draft without cast speakers comes back unchanged.
|
|
120
|
+
export function castVoicedRun(db, draft) {
|
|
121
|
+
if (draft.voices === undefined)
|
|
122
|
+
return draft;
|
|
123
|
+
const ids = new Set(draft.voices.speakers.flatMap((one) => one.castId ?? []));
|
|
124
|
+
if (ids.size === 0)
|
|
125
|
+
return draft;
|
|
126
|
+
const members = [...ids].flatMap((id) => castMemberById(db, id) ?? []);
|
|
127
|
+
const voices = withCastVoices(draft.voices, members);
|
|
128
|
+
return voices === draft.voices ? draft : { ...draft, voices };
|
|
129
|
+
}
|