@gentbajko/slopify 2.4.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -21
- package/NOTICE +12 -0
- package/README.md +21 -40
- package/SUBTITLES.md +1 -1
- package/dist/adapter-registry.js +11 -1
- 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 +55 -5
- package/dist/adapters/host-cli/transport.js +13 -6
- package/dist/adapters/image/bytes.js +2 -2
- package/dist/adapters/image/codex-output.js +60 -18
- package/dist/adapters/image/codex.js +166 -25
- package/dist/adapters/image/fal.js +31 -3
- package/dist/adapters/image/google.js +17 -2
- package/dist/adapters/image/openai.js +45 -16
- package/dist/adapters/image/reference.js +41 -0
- package/dist/adapters/image/replicate.js +14 -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-models.js +2 -1
- package/dist/adapters/llm/codex-server-models.js +2 -1
- 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 +26 -9
- 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/models.yaml +7 -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 +33 -12
- package/dist/catalog/runtime-models.js +1 -1
- package/dist/catalog/schema.js +12 -1
- package/dist/catalog/store.js +147 -8
- package/dist/catalog/validate.js +17 -2
- 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/events/hub.js +11 -0
- package/dist/edge/host-cli.js +1 -1
- package/dist/edge/http/actions.js +32 -10
- 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/backups.js +40 -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 +45 -6
- 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/problem.js +1 -0
- 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 +128 -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 +12 -3
- package/dist/kernel/db/migrations/0022-drop-duplicate-held-work.sql +56 -0
- package/dist/kernel/db/migrations/0024-reference-attachments.sql +18 -0
- 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 +139 -16
- package/dist/kernel/ports/image.js +3 -1
- package/dist/kernel/ports/key-probe.js +1 -0
- package/dist/kernel/ports/languages.js +157 -0
- package/dist/kernel/ports/llm.js +3 -1
- 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 +18 -9
- 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 +164 -9
- 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 +321 -20
- 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 +35 -0
- package/dist/slices/admission/repo.js +24 -4
- package/dist/slices/admission/rules.js +126 -1
- package/dist/slices/admission/schema.js +68 -1
- package/dist/slices/admission/short-mode.js +47 -0
- package/dist/slices/admission/start.js +90 -16
- 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/files.js +135 -0
- package/dist/slices/backups/folder.js +43 -0
- package/dist/slices/backups/model.js +65 -0
- package/dist/slices/backups/repo.js +28 -0
- package/dist/slices/backups/schedule.js +60 -0
- package/dist/slices/backups/service.js +311 -0
- package/dist/slices/batch/index.js +9 -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 +113 -24
- 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 +15 -3
- package/dist/slices/library/snapshot.js +8 -2
- 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 +94 -0
- package/dist/slices/notifications/rules.js +102 -0
- package/dist/slices/notifications/send.js +40 -0
- package/dist/slices/notifications/settings.js +34 -0
- 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 +160 -9
- package/dist/slices/play-drafts/readiness.js +3 -0
- package/dist/slices/play-drafts/repo.js +9 -1
- package/dist/slices/play-drafts/review-inputs.js +22 -3
- package/dist/slices/play-drafts/schema.js +109 -2
- package/dist/slices/play-drafts/service.js +10 -1
- package/dist/slices/play-drafts/start.js +43 -18
- package/dist/slices/play-drafts/uploads.js +2 -1
- package/dist/slices/project-templates/from-project.js +70 -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 +29 -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 +67 -2
- 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 +15 -5
- 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 +43 -1
- package/dist/slices/rebuild/recipe-legacy.js +12 -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 +58 -0
- 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 +32 -5
- package/dist/slices/rebuild/recipe-text.js +114 -19
- package/dist/slices/rebuild/recipe-validation.js +29 -3
- package/dist/slices/rebuild/recipe-visual.js +104 -29
- 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 +14 -2
- 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 +69 -0
- package/dist/slices/rebuild/runtime-local.js +154 -27
- 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 +29 -0
- package/dist/slices/rebuild/runtime-provider.js +134 -19
- 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 +48 -23
- 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/service-readiness.js +15 -1
- package/dist/slices/rebuild/soften.js +49 -0
- package/dist/slices/rebuild/transition-repo.js +10 -3
- 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 +12 -2
- package/dist/slices/revisions/adopt.js +9 -2
- package/dist/slices/revisions/downloads.js +7 -1
- package/dist/slices/revisions/image-plan.js +183 -0
- package/dist/slices/revisions/mutation-assets.js +43 -9
- package/dist/slices/revisions/mutation-cues.js +71 -0
- package/dist/slices/revisions/mutation-prepare.js +8 -10
- package/dist/slices/revisions/mutation-request.js +2 -1
- package/dist/slices/revisions/mutations.js +97 -3
- package/dist/slices/revisions/schema.js +16 -2
- 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 +48 -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 +11 -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 +20 -0
- package/dist/slices/storage/model.js +14 -0
- package/dist/slices/storage/portable.js +95 -12
- package/dist/slices/storage/reconcile.js +19 -2
- 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-CAJNT254.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-BGgYMm2u.css +0 -1
- package/dist/web/assets/index-BVVgtntL.js +0 -171
- package/scripts/docker-run.sh +0 -3
- /package/dist/edge/{docker-projects → docker-install}/tree.js +0 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { transact } from "../../kernel/db/tx.js";
|
|
3
|
+
import { deleteVersions, recordVersion } from "../library/history.js";
|
|
4
|
+
import { nameMax } from "../library/model.js";
|
|
5
|
+
import { entryById, promptById } from "../library/repo.js";
|
|
6
|
+
import { deleteProject, projectBusy } from "../storage/delete-project.js";
|
|
7
|
+
import { trashKinds, trashMs, } from "./model.js";
|
|
8
|
+
// Settings → Trash: what was deleted in the last 30 days, put back or removed for good.
|
|
9
|
+
//
|
|
10
|
+
// Each kind keeps its stamp where it lives: a project in `project_trash` (the projects table is
|
|
11
|
+
// read positionally in many places), a prompt, entry or template in its own `deleted_at`, and a
|
|
12
|
+
// schedule in the `deleted_at` it always had as the tombstone that keeps its run history. A
|
|
13
|
+
// schedule leaves the trash by getting `purged_at`; its row and history stay, as before.
|
|
14
|
+
const dayMs = 24 * 60 * 60_000;
|
|
15
|
+
// A template's name column allows this much (0008's CHECK on project_template_revisions).
|
|
16
|
+
const templateNameMax = 200;
|
|
17
|
+
const row = z.object({
|
|
18
|
+
id: z.string(),
|
|
19
|
+
name: z.string(),
|
|
20
|
+
detail: z.string().nullable(),
|
|
21
|
+
deleted_at: z.string(),
|
|
22
|
+
});
|
|
23
|
+
// Every kind in one statement, newest deletion first.
|
|
24
|
+
const trashedRows = `
|
|
25
|
+
SELECT 'project' AS kind, p.id, p.title AS name, NULL AS detail, t.deleted_at
|
|
26
|
+
FROM project_trash t JOIN projects p ON p.id = t.project_id
|
|
27
|
+
UNION ALL SELECT 'prompt', id, name, kind, deleted_at FROM prompts WHERE deleted_at IS NOT NULL
|
|
28
|
+
UNION ALL SELECT 'entry', id, name, category, deleted_at FROM entries WHERE deleted_at IS NOT NULL
|
|
29
|
+
UNION ALL SELECT 'template', t.id, r.name, NULL, t.deleted_at
|
|
30
|
+
FROM project_templates t
|
|
31
|
+
JOIN project_template_revisions r ON r.template_id = t.id AND r.version = t.head_version
|
|
32
|
+
WHERE t.deleted_at IS NOT NULL
|
|
33
|
+
UNION ALL SELECT 'schedule', id, name, NULL, deleted_at FROM schedules
|
|
34
|
+
WHERE deleted_at IS NOT NULL AND purged_at IS NULL`;
|
|
35
|
+
export function listTrash(deps) {
|
|
36
|
+
const now = deps.clock.now().valueOf();
|
|
37
|
+
return deps.db
|
|
38
|
+
.prepare(`SELECT * FROM (${trashedRows}) ORDER BY deleted_at DESC, kind, id`)
|
|
39
|
+
.all()
|
|
40
|
+
.map((raw) => {
|
|
41
|
+
const parsed = row.extend({ kind: z.enum(trashKinds) }).parse(raw);
|
|
42
|
+
const purgeAt = Date.parse(parsed.deleted_at) + trashMs;
|
|
43
|
+
return {
|
|
44
|
+
kind: parsed.kind,
|
|
45
|
+
id: parsed.id,
|
|
46
|
+
name: parsed.name,
|
|
47
|
+
detail: parsed.detail,
|
|
48
|
+
deletedAt: parsed.deleted_at,
|
|
49
|
+
purgeAt: new Date(purgeAt).toISOString(),
|
|
50
|
+
daysLeft: Math.max(0, Math.ceil((purgeAt - now) / dayMs)),
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
// 07 Projects' Delete. Refused while the run is going, as removing it always was: the work
|
|
55
|
+
// has to be stopped by the user first. A project waiting its turn (a queued batch item, a
|
|
56
|
+
// checkpoint, work left over from a restart) simply stops being picked up: the runner holds a
|
|
57
|
+
// project in the trash like a paused one and the batch queue moves on past it.
|
|
58
|
+
export function trashProject(deps, projectId) {
|
|
59
|
+
return transact(deps.db, () => {
|
|
60
|
+
const found = deps.db
|
|
61
|
+
.prepare("SELECT (SELECT 1 FROM project_trash t WHERE t.project_id = p.id) AS trashed FROM projects p WHERE p.id = ?")
|
|
62
|
+
.get(projectId);
|
|
63
|
+
if (found === undefined || found.trashed === 1)
|
|
64
|
+
return { ok: false, reason: "not-found" };
|
|
65
|
+
if (projectBusy(deps, projectId))
|
|
66
|
+
return { ok: false, reason: "running" };
|
|
67
|
+
deps.db
|
|
68
|
+
.prepare("INSERT INTO project_trash (project_id, deleted_at) VALUES (?, ?)")
|
|
69
|
+
.run(projectId, deps.clock.now().toISOString());
|
|
70
|
+
return { ok: true, value: null };
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
// Puts the item back where it was. A prompt, entry or template whose name a live one has
|
|
74
|
+
// taken meanwhile comes back as "Name (restored)", then "Name (restored 2)"…
|
|
75
|
+
//
|
|
76
|
+
// A schedule comes back paused with no next run, whatever it was when deleted (only a
|
|
77
|
+
// canceled or completed one can be deleted): nothing runs until Resume is pressed on
|
|
78
|
+
// Schedules, which picks the next occurrence from then. Its template must be live: one in the
|
|
79
|
+
// trash is restored first, and one removed for good leaves the schedule nothing to run.
|
|
80
|
+
export function restoreItem(deps, kind, id) {
|
|
81
|
+
return transact(deps.db, () => {
|
|
82
|
+
switch (kind) {
|
|
83
|
+
case "project":
|
|
84
|
+
return restoreProject(deps.db, id);
|
|
85
|
+
case "prompt":
|
|
86
|
+
return restoreLibrary(deps, "prompt", id);
|
|
87
|
+
case "entry":
|
|
88
|
+
return restoreLibrary(deps, "entry", id);
|
|
89
|
+
case "template":
|
|
90
|
+
return restoreTemplate(deps, id);
|
|
91
|
+
case "schedule":
|
|
92
|
+
return restoreSchedule(deps, id);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
function restoreProject(db, id) {
|
|
97
|
+
const found = db
|
|
98
|
+
.prepare("SELECT p.title FROM project_trash t JOIN projects p ON p.id = t.project_id WHERE t.project_id = ?")
|
|
99
|
+
.get(id);
|
|
100
|
+
if (typeof found?.title !== "string")
|
|
101
|
+
return { ok: false, reason: "not-found" };
|
|
102
|
+
db.prepare("DELETE FROM project_trash WHERE project_id = ?").run(id);
|
|
103
|
+
return { ok: true, value: { kind: "project", id, name: found.title, renamedFrom: null } };
|
|
104
|
+
}
|
|
105
|
+
function restoreLibrary(deps, itemKind, id) {
|
|
106
|
+
const table = itemKind === "prompt" ? "prompts" : "entries";
|
|
107
|
+
const group = itemKind === "prompt" ? "kind" : "category";
|
|
108
|
+
const found = deps.db
|
|
109
|
+
.prepare(`SELECT name, ${group} AS grp FROM ${table} WHERE id = ? AND deleted_at IS NOT NULL`)
|
|
110
|
+
.get(id);
|
|
111
|
+
if (typeof found?.name !== "string" || typeof found.grp !== "string")
|
|
112
|
+
return { ok: false, reason: "not-found" };
|
|
113
|
+
const taken = deps.db.prepare(`SELECT 1 FROM ${table} WHERE ${group} = ? AND lower(name) = lower(?) AND deleted_at IS NULL`);
|
|
114
|
+
const grp = found.grp;
|
|
115
|
+
const name = freeName(found.name, nameMax, (candidate) => taken.get(grp, candidate) !== undefined);
|
|
116
|
+
const renamed = name !== found.name;
|
|
117
|
+
deps.db
|
|
118
|
+
.prepare(`UPDATE ${table} SET deleted_at = NULL, name = ?, updated_at = ${renamed ? "?" : "updated_at"} WHERE id = ?`)
|
|
119
|
+
.run(...(renamed ? [name, deps.clock.now().toISOString(), id] : [name, id]));
|
|
120
|
+
// A rename shows in History as a version of its own.
|
|
121
|
+
if (renamed) {
|
|
122
|
+
const item = itemKind === "prompt" ? promptById(deps.db, id) : entryById(deps.db, id);
|
|
123
|
+
if (item !== undefined)
|
|
124
|
+
recordVersion(deps.db, itemKind, item);
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
ok: true,
|
|
128
|
+
value: { kind: itemKind, id, name, renamedFrom: renamed ? found.name : null },
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function restoreTemplate(deps, id) {
|
|
132
|
+
const found = deps.db
|
|
133
|
+
.prepare(`SELECT r.name, r.version, r.document_json FROM project_templates t
|
|
134
|
+
JOIN project_template_revisions r ON r.template_id = t.id AND r.version = t.head_version
|
|
135
|
+
WHERE t.id = ? AND t.deleted_at IS NOT NULL`)
|
|
136
|
+
.get(id);
|
|
137
|
+
if (typeof found?.name !== "string" ||
|
|
138
|
+
typeof found.version !== "number" ||
|
|
139
|
+
typeof found.document_json !== "string")
|
|
140
|
+
return { ok: false, reason: "not-found" };
|
|
141
|
+
const taken = deps.db.prepare(`SELECT 1 FROM project_templates t
|
|
142
|
+
JOIN project_template_revisions r ON r.template_id = t.id AND r.version = t.head_version
|
|
143
|
+
WHERE t.deleted_at IS NULL AND lower(r.name) = lower(?)`);
|
|
144
|
+
const name = freeName(found.name, templateNameMax, (candidate) => taken.get(candidate) !== undefined);
|
|
145
|
+
deps.db.prepare("UPDATE project_templates SET deleted_at = NULL WHERE id = ?").run(id);
|
|
146
|
+
// A rename is a new version, as a rename in the template editor is; the versions a schedule
|
|
147
|
+
// or a draft names stay as they were.
|
|
148
|
+
if (name !== found.name) {
|
|
149
|
+
const version = found.version + 1;
|
|
150
|
+
deps.db
|
|
151
|
+
.prepare("INSERT INTO project_template_revisions(template_id,version,name,document_json,created_at) VALUES (?,?,?,?,?)")
|
|
152
|
+
.run(id, version, name, found.document_json, deps.clock.now().toISOString());
|
|
153
|
+
deps.db.prepare("UPDATE project_templates SET head_version = ? WHERE id = ?").run(version, id);
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
ok: true,
|
|
157
|
+
value: {
|
|
158
|
+
kind: "template",
|
|
159
|
+
id,
|
|
160
|
+
name,
|
|
161
|
+
renamedFrom: name === found.name ? null : found.name,
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function restoreSchedule(deps, id) {
|
|
166
|
+
const found = deps.db
|
|
167
|
+
.prepare(`SELECT s.name, s.template_id,
|
|
168
|
+
(SELECT t.deleted_at IS NOT NULL FROM project_templates t WHERE t.id = s.template_id) AS trashed
|
|
169
|
+
FROM schedules s WHERE s.id = ? AND s.deleted_at IS NOT NULL AND s.purged_at IS NULL`)
|
|
170
|
+
.get(id);
|
|
171
|
+
if (typeof found?.name !== "string")
|
|
172
|
+
return { ok: false, reason: "not-found" };
|
|
173
|
+
if (found.trashed === null)
|
|
174
|
+
return { ok: false, reason: "template-gone" };
|
|
175
|
+
if (found.trashed === 1)
|
|
176
|
+
return { ok: false, reason: "template-in-trash" };
|
|
177
|
+
const at = deps.clock.now().toISOString();
|
|
178
|
+
deps.db
|
|
179
|
+
.prepare(`UPDATE schedules SET deleted_at = NULL, status = 'paused', next_run_at = NULL,
|
|
180
|
+
version = version + 1, updated_at = ? WHERE id = ?`)
|
|
181
|
+
.run(at, id);
|
|
182
|
+
return { ok: true, value: { kind: "schedule", id, name: found.name, renamedFrom: null } };
|
|
183
|
+
}
|
|
184
|
+
// "Delete now": removes one item for good. A project's folder goes first, then its rows
|
|
185
|
+
// (`slices/storage/delete-project.ts`); a folder that will not go leaves it in the trash.
|
|
186
|
+
export function deleteNow(deps, kind, id) {
|
|
187
|
+
switch (kind) {
|
|
188
|
+
case "project": {
|
|
189
|
+
if (deps.db.prepare("SELECT 1 FROM project_trash WHERE project_id = ?").get(id) === undefined)
|
|
190
|
+
return { ok: false, reason: "not-found" };
|
|
191
|
+
const removed = deleteProject(deps, id);
|
|
192
|
+
if (removed.ok)
|
|
193
|
+
return { ok: true, value: null };
|
|
194
|
+
return {
|
|
195
|
+
ok: false,
|
|
196
|
+
reason: removed.reason === "no-project" ? "not-found" : removed.reason,
|
|
197
|
+
...(removed.detail === undefined ? {} : { detail: removed.detail }),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
case "prompt":
|
|
201
|
+
case "entry":
|
|
202
|
+
return transact(deps.db, () => {
|
|
203
|
+
const table = kind === "prompt" ? "prompts" : "entries";
|
|
204
|
+
const changed = deps.db
|
|
205
|
+
.prepare(`DELETE FROM ${table} WHERE id = ? AND deleted_at IS NOT NULL`)
|
|
206
|
+
.run(id).changes;
|
|
207
|
+
if (Number(changed) === 0)
|
|
208
|
+
return { ok: false, reason: "not-found" };
|
|
209
|
+
deleteVersions(deps.db, kind, id);
|
|
210
|
+
return { ok: true, value: null };
|
|
211
|
+
});
|
|
212
|
+
case "template": {
|
|
213
|
+
// Revisions cascade. A schedule in the trash may still name it: that schedule can then
|
|
214
|
+
// no longer be restored (`restoreSchedule`), only removed.
|
|
215
|
+
const changed = deps.db
|
|
216
|
+
.prepare("DELETE FROM project_templates WHERE id = ? AND deleted_at IS NOT NULL")
|
|
217
|
+
.run(id).changes;
|
|
218
|
+
return Number(changed) === 0 ? { ok: false, reason: "not-found" } : { ok: true, value: null };
|
|
219
|
+
}
|
|
220
|
+
case "schedule": {
|
|
221
|
+
// Gone from the trash; the row stays as the history Schedules lists under Deleted.
|
|
222
|
+
const changed = deps.db
|
|
223
|
+
.prepare("UPDATE schedules SET purged_at = ? WHERE id = ? AND deleted_at IS NOT NULL AND purged_at IS NULL")
|
|
224
|
+
.run(deps.clock.now().toISOString(), id).changes;
|
|
225
|
+
return Number(changed) === 0 ? { ok: false, reason: "not-found" } : { ok: true, value: null };
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
// The daily purge: everything deleted more than 30 days ago goes for good. A failure on one
|
|
230
|
+
// item is logged and leaves it for the next pass; the rest still go.
|
|
231
|
+
export function purgeExpired(deps) {
|
|
232
|
+
const cutoff = new Date(deps.clock.now().valueOf() - trashMs).toISOString();
|
|
233
|
+
let removed = 0;
|
|
234
|
+
let kept = 0;
|
|
235
|
+
for (const item of listTrash(deps)) {
|
|
236
|
+
if (item.deletedAt > cutoff)
|
|
237
|
+
continue;
|
|
238
|
+
const result = deleteNow(deps, item.kind, item.id);
|
|
239
|
+
if (result.ok)
|
|
240
|
+
removed++;
|
|
241
|
+
else {
|
|
242
|
+
kept++;
|
|
243
|
+
deps.log.write("warn", "trash.purge", {
|
|
244
|
+
...(item.kind === "project" ? { projectId: item.id } : {}),
|
|
245
|
+
detail: `${item.kind} ${item.id}: ${result.detail ?? result.reason}`,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return { removed, kept };
|
|
250
|
+
}
|
|
251
|
+
export function createTrashPurge(deps) {
|
|
252
|
+
let last;
|
|
253
|
+
return {
|
|
254
|
+
tick: () => {
|
|
255
|
+
const now = deps.clock.now().valueOf();
|
|
256
|
+
if (last !== undefined && now - last < dayMs)
|
|
257
|
+
return undefined;
|
|
258
|
+
last = now;
|
|
259
|
+
return purgeExpired(deps);
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
// The name itself when free, else "Name (restored)", "Name (restored 2)"… cut to fit `max`.
|
|
264
|
+
export function freeName(name, max, taken) {
|
|
265
|
+
if (!taken(name))
|
|
266
|
+
return name;
|
|
267
|
+
for (let n = 1;; n++) {
|
|
268
|
+
const suffix = n === 1 ? " (restored)" : ` (restored ${String(n)})`;
|
|
269
|
+
const candidate = `${name.slice(0, Math.max(1, max - suffix.length)).trimEnd()}${suffix}`;
|
|
270
|
+
if (!taken(candidate))
|
|
271
|
+
return candidate;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function markUploaded(db, projectId, uploaded, at) {
|
|
2
|
+
if (db.prepare("SELECT 1 FROM projects WHERE id=?").get(projectId) === undefined)
|
|
3
|
+
return { ok: false, reason: "not-found" };
|
|
4
|
+
if (!uploaded) {
|
|
5
|
+
db.prepare("DELETE FROM project_uploads WHERE project_id=?").run(projectId);
|
|
6
|
+
return { ok: true, uploadedAt: null };
|
|
7
|
+
}
|
|
8
|
+
// Marking twice keeps the first time: it is when the video went up.
|
|
9
|
+
db.prepare("INSERT INTO project_uploads(project_id,uploaded_at) VALUES (?,?) ON CONFLICT(project_id) DO NOTHING").run(projectId, at);
|
|
10
|
+
const row = db
|
|
11
|
+
.prepare("SELECT uploaded_at FROM project_uploads WHERE project_id=?")
|
|
12
|
+
.get(projectId);
|
|
13
|
+
return { ok: true, uploadedAt: typeof row?.uploaded_at === "string" ? row.uploaded_at : at };
|
|
14
|
+
}
|
|
15
|
+
export function uploadedProjects(db) {
|
|
16
|
+
return new Map(db
|
|
17
|
+
.prepare("SELECT project_id, uploaded_at FROM project_uploads")
|
|
18
|
+
.all()
|
|
19
|
+
.map((row) => [String(row.project_id), String(row.uploaded_at)]));
|
|
20
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ambientBedSources, builtInBeds } from "./ambient-bed.js";
|
|
3
|
+
// The ambient bed as a run's config and a channel's brand kit hold it. The ranges are
|
|
4
|
+
// `ambient-bed.ts`'s, checked by admission and the channel page, not the schema's.
|
|
5
|
+
const numbers = {
|
|
6
|
+
levelDb: z.number(),
|
|
7
|
+
fadeInSeconds: z.number(),
|
|
8
|
+
tailSeconds: z.number(),
|
|
9
|
+
};
|
|
10
|
+
export const ambientBedSchema = z
|
|
11
|
+
.object({ source: z.enum(ambientBedSources), ...numbers })
|
|
12
|
+
.strict();
|
|
13
|
+
export const channelAmbientBedSchema = z
|
|
14
|
+
.object({ source: z.enum(builtInBeds), ...numbers })
|
|
15
|
+
.strict();
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// An ambient sound bed under the long video: rain, fire or wind that ffmpeg makes itself from
|
|
2
|
+
// noise (no downloads, no bundled recordings), or the user's own audio file. It plays under the
|
|
3
|
+
// whole narration, ducked whenever the voice speaks, fades in at the start, and keeps playing
|
|
4
|
+
// for a fade-out tail after the narration ends, which lengthens the video when the tail is
|
|
5
|
+
// longer than the silence already there (`plan.ts`). Only the long video gets it: the shorts
|
|
6
|
+
// have their own background music, and the audio-only WAV export stays the narration alone.
|
|
7
|
+
// Browser-safe: Play, the channel page and the review summary read these names and limits too.
|
|
8
|
+
//
|
|
9
|
+
// A run without one has no `ambientBed` at all, and the render recipe leaves it out of the
|
|
10
|
+
// fingerprint then, so no video made before it existed turns outdated.
|
|
11
|
+
export const builtInBeds = ["rain", "fire", "wind"];
|
|
12
|
+
export const ambientBedSources = [...builtInBeds, "upload"];
|
|
13
|
+
export const ambientBedLabels = {
|
|
14
|
+
rain: "Rain",
|
|
15
|
+
fire: "Fireplace",
|
|
16
|
+
wind: "Wind",
|
|
17
|
+
upload: "My own file",
|
|
18
|
+
};
|
|
19
|
+
// ceiling: below -40 dB the bed is inaudible under a voice; above -6 dB it competes with it.
|
|
20
|
+
export const ambientLevelMin = -40;
|
|
21
|
+
export const ambientLevelMax = -6;
|
|
22
|
+
export const ambientFadeMax = 30;
|
|
23
|
+
export const ambientTailMax = 30;
|
|
24
|
+
export const defaultAmbientBed = {
|
|
25
|
+
levelDb: -18,
|
|
26
|
+
fadeInSeconds: 3,
|
|
27
|
+
tailSeconds: 6,
|
|
28
|
+
};
|
|
29
|
+
// Whether the run's long video gets the bed: it is set, the video renders and there is a
|
|
30
|
+
// narration to lie under. A short-only run (`admission/short-mode.ts`) renders through the
|
|
31
|
+
// Shorts renderer and has no bed.
|
|
32
|
+
export function usesAmbientBed(config) {
|
|
33
|
+
return (config.ambientBed !== undefined &&
|
|
34
|
+
config.mode !== "short" &&
|
|
35
|
+
config.sources.video === "generate" &&
|
|
36
|
+
config.sources.audio !== "off");
|
|
37
|
+
}
|
|
38
|
+
// The words a problem with one of the settings is said in; shared by admission, Play's draft
|
|
39
|
+
// conversion and the channel page.
|
|
40
|
+
export function ambientBedProblems(bed) {
|
|
41
|
+
const problems = [];
|
|
42
|
+
if (!ambientBedSources.includes(bed.source))
|
|
43
|
+
problems.push({
|
|
44
|
+
field: "source",
|
|
45
|
+
message: "Choose Rain, Fireplace, Wind or My own file for the ambient sound.",
|
|
46
|
+
});
|
|
47
|
+
if (!(Number.isInteger(bed.levelDb) &&
|
|
48
|
+
bed.levelDb >= ambientLevelMin &&
|
|
49
|
+
bed.levelDb <= ambientLevelMax))
|
|
50
|
+
problems.push({
|
|
51
|
+
field: "level",
|
|
52
|
+
message: `Enter an ambient sound level between ${String(ambientLevelMin)} and ${String(ambientLevelMax)} dB, in whole dB.`,
|
|
53
|
+
});
|
|
54
|
+
if (!halfSteps(bed.fadeInSeconds, ambientFadeMax))
|
|
55
|
+
problems.push({
|
|
56
|
+
field: "fadeIn",
|
|
57
|
+
message: `Enter a fade-in between 0 and ${String(ambientFadeMax)} seconds, in steps of 0.5.`,
|
|
58
|
+
});
|
|
59
|
+
if (!halfSteps(bed.tailSeconds, ambientTailMax))
|
|
60
|
+
problems.push({
|
|
61
|
+
field: "tail",
|
|
62
|
+
message: `Enter a tail between 0 and ${String(ambientTailMax)} seconds, in steps of 0.5.`,
|
|
63
|
+
});
|
|
64
|
+
return problems;
|
|
65
|
+
}
|
|
66
|
+
function halfSteps(value, max) {
|
|
67
|
+
return Number.isInteger(value * 2) && value >= 0 && value <= max;
|
|
68
|
+
}
|
|
69
|
+
// How much longer the video runs with the bed: the tail plays over the silence already after
|
|
70
|
+
// the narration (`edgeSilenceSeconds`) and only the rest is added.
|
|
71
|
+
export function ambientTailExtension(config, edgeSilenceSeconds) {
|
|
72
|
+
if (!usesAmbientBed(config) || config.ambientBed === undefined)
|
|
73
|
+
return 0;
|
|
74
|
+
return Math.max(0, config.ambientBed.tailSeconds - edgeSilenceSeconds);
|
|
75
|
+
}
|
|
76
|
+
export function ambientBedFormOf(bed) {
|
|
77
|
+
return {
|
|
78
|
+
source: bed.source,
|
|
79
|
+
level: String(bed.levelDb),
|
|
80
|
+
fadeIn: String(bed.fadeInSeconds),
|
|
81
|
+
tail: String(bed.tailSeconds),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
// The settings a form asks for, numbers as typed (NaN for a blank one), or undefined for none.
|
|
85
|
+
export function ambientBedOfForm(form) {
|
|
86
|
+
if (form === undefined || form.source === "none")
|
|
87
|
+
return undefined;
|
|
88
|
+
const number = (raw) => (raw.trim() === "" ? Number.NaN : Number(raw));
|
|
89
|
+
return {
|
|
90
|
+
source: form.source,
|
|
91
|
+
levelDb: number(form.level),
|
|
92
|
+
fadeInSeconds: number(form.fadeIn),
|
|
93
|
+
tailSeconds: number(form.tail),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// How the join mixes the ambient bed under the narration (`ffmpeg.ts`). Every value goes into
|
|
2
|
+
// an argument array, never a shell string.
|
|
3
|
+
//
|
|
4
|
+
// The built-in beds are shaped noise, made by ffmpeg's own anoisesrc with a fixed seed, so the
|
|
5
|
+
// same settings always render the same sound and nothing is downloaded or shipped:
|
|
6
|
+
// - rain: pink noise with the rumble and the fizz filtered off, a steady patter;
|
|
7
|
+
// - wind: brown noise kept low, swelling and dropping in slow gusts;
|
|
8
|
+
// - fire: a low brown-noise roar that flickers, with sparse velvet-noise crackle over it.
|
|
9
|
+
// Each is levelled to about -20 dBFS RMS before the bed's own level, so 0 dB is roughly as loud
|
|
10
|
+
// as a narration and the three sit alike at the same setting.
|
|
11
|
+
//
|
|
12
|
+
// The ducking is a sidechain compressor keyed by the narration itself, the Shorts' music does
|
|
13
|
+
// the same (`shorts/render.ts`): it follows the voice as it is, needs no word timing (which a
|
|
14
|
+
// run without captions, chapters or shorts never makes), and dips under an intro or outro too.
|
|
15
|
+
// ceiling: it dips once the voice passes about -34 dBFS, by up to 8:1, over 40 ms, and comes
|
|
16
|
+
// back over 900 ms after the voice stops: slower than the shorts' music, so a bed under a long
|
|
17
|
+
// narration settles rather than swelling in every pause between sentences.
|
|
18
|
+
const duck = "threshold=0.02:ratio=8:attack=40:release=900:makeup=1";
|
|
19
|
+
const presets = {
|
|
20
|
+
rain: [{ source: "anoisesrc=c=pink:a=1:seed=17", filters: "highpass=f=600,lowpass=f=9000" }],
|
|
21
|
+
wind: [
|
|
22
|
+
{
|
|
23
|
+
source: "anoisesrc=c=brown:a=1:seed=29",
|
|
24
|
+
filters: "highpass=f=60,lowpass=f=500,tremolo=f=0.15:d=0.6,volume=-1dB",
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
fire: [
|
|
28
|
+
{
|
|
29
|
+
source: "anoisesrc=c=brown:a=1:seed=41",
|
|
30
|
+
filters: "highpass=f=80,lowpass=f=800,tremolo=f=6:d=0.25,volume=-3dB",
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
source: "anoisesrc=c=velvet:a=1:seed=43",
|
|
34
|
+
filters: "highpass=f=1500,lowpass=f=6000,volume=-22dB",
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
};
|
|
38
|
+
// The inputs the bed adds after the join's own, for `seconds` of video.
|
|
39
|
+
export function bedInputs(bed, seconds, sampleRate) {
|
|
40
|
+
if (bed.source.kind === "file")
|
|
41
|
+
// Looped endlessly and cut to the video, so a file shorter than the video plays again.
|
|
42
|
+
return ["-stream_loop", "-1", "-i", bed.source.path];
|
|
43
|
+
return presets[bed.source.preset].flatMap((layer) => [
|
|
44
|
+
"-f",
|
|
45
|
+
"lavfi",
|
|
46
|
+
"-t",
|
|
47
|
+
seconds.toFixed(6),
|
|
48
|
+
"-i",
|
|
49
|
+
`${layer.source}:r=${String(sampleRate)}`,
|
|
50
|
+
]);
|
|
51
|
+
}
|
|
52
|
+
// The chains that turn the join's narration `voice` into `out` with the bed under it; `first`
|
|
53
|
+
// is the index of the bed's first input.
|
|
54
|
+
export function bedChains(bed, first, seconds, format, voice, out) {
|
|
55
|
+
const chains = [];
|
|
56
|
+
const layers = bed.source.kind === "file" ? [{ filters: "" }] : presets[bed.source.preset];
|
|
57
|
+
layers.forEach((layer, at) => {
|
|
58
|
+
chains.push(`[${String(first + at)}:a]${layer.filters === "" ? "" : `${layer.filters},`}${format},atrim=end=${seconds.toFixed(6)},asetpts=PTS-STARTPTS[bed${String(at)}]`);
|
|
59
|
+
});
|
|
60
|
+
const joined = layers.length === 1
|
|
61
|
+
? "[bed0]anull"
|
|
62
|
+
: `${layers.map((_layer, at) => `[bed${String(at)}]`).join("")}amix=inputs=${String(layers.length)}:duration=first:normalize=0`;
|
|
63
|
+
const fadeIn = bed.fadeInSeconds > 0 ? `,afade=t=in:st=0:d=${bed.fadeInSeconds.toFixed(3)}` : "";
|
|
64
|
+
// A tail of 0 still fades, over a moment, rather than cutting off mid-sound.
|
|
65
|
+
const fadeOut = `,afade=t=out:st=${bed.fadeOutAt.toFixed(6)}:d=${Math.max(0.25, bed.fadeOutSeconds).toFixed(3)}`;
|
|
66
|
+
chains.push(`${joined},volume=${String(bed.levelDb)}dB${fadeIn}${fadeOut}[bed]`);
|
|
67
|
+
chains.push(`${voice}asplit=2[voice][key]`);
|
|
68
|
+
chains.push(`[bed][key]sidechaincompress=${duck}[ducked]`);
|
|
69
|
+
// The narration keeps its level; `normalize=0` stops amix halving both.
|
|
70
|
+
chains.push(`[voice][ducked]amix=inputs=2:duration=first:dropout_transition=0:normalize=0${out}`);
|
|
71
|
+
return chains;
|
|
72
|
+
}
|
|
@@ -5,9 +5,9 @@ export async function audioInputs(deps, projectId, outputs, signal) {
|
|
|
5
5
|
const dir = projectDir(deps.paths, projectId);
|
|
6
6
|
const body = await audio(deps, outputs, "audio_body", dir, signal);
|
|
7
7
|
if (!body)
|
|
8
|
-
throw new Error("The video has no narration audio to play. Let the Narration stage finish, then
|
|
8
|
+
throw new Error("The video has no narration audio to play. Let the Narration stage finish, then Try again.");
|
|
9
9
|
if (body.seconds <= 0)
|
|
10
|
-
throw new Error("The narration audio contains no sound; the file may be damaged or empty. Regenerate the narration (or upload a different audio file in Edit project → Narration), then
|
|
10
|
+
throw new Error("The narration audio contains no sound; the file may be damaged or empty. Regenerate the narration (or upload a different audio file in Edit project → Narration), then Try again.");
|
|
11
11
|
const intro = await audio(deps, outputs, "audio_intro", dir, signal);
|
|
12
12
|
const outro = await audio(deps, outputs, "audio_outro", dir, signal);
|
|
13
13
|
return { body, intro, outro };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { assColour } from "../subtitles/captions.js";
|
|
1
2
|
// Chapter cards as an ASS script the clips draw with libass, the renderer the captions use,
|
|
2
3
|
// so a card is set in the subtitle font without a font-rendering filter of its own (this
|
|
3
4
|
// ffmpeg has none). Each card is centred, well above where captions sit by default, and fades
|
|
@@ -5,7 +6,9 @@
|
|
|
5
6
|
// clip's first frame (`offset`), since a clip's frames count from zero.
|
|
6
7
|
// ceiling: the owner's eye. The fades take 0.4 s each of the card's 2.5 s.
|
|
7
8
|
const fadeMs = 400;
|
|
8
|
-
export function cardsAss(edit, cards, fontName, offset = 0
|
|
9
|
+
export function cardsAss(edit, cards, fontName, offset = 0,
|
|
10
|
+
// #RRGGBB from the brand kit; absent is white.
|
|
11
|
+
color) {
|
|
9
12
|
const { width, height, fps } = edit;
|
|
10
13
|
// The title is sized from the frame's short side, so a vertical video's card fits too. At
|
|
11
14
|
// 8% it read as a caption rather than a chapter title; 11% and bold gives it presence.
|
|
@@ -24,7 +27,7 @@ export function cardsAss(edit, cards, fontName, offset = 0) {
|
|
|
24
27
|
"[V4+ Styles]",
|
|
25
28
|
"Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding",
|
|
26
29
|
// White bold text, the soft outline, no offset shadow, centred (alignment 5), letter-spaced.
|
|
27
|
-
`Style: Card,${fontName.replace(/,/g, " ")},${String(size)}
|
|
30
|
+
`Style: Card,${fontName.replace(/,/g, " ")},${String(size)},${assColour(color, "&H00FFFFFF")},${assColour(color, "&H00FFFFFF")},&H60000000,&H00000000,-1,0,0,0,100,100,${String(Math.round(size / 12))},0,1,${String(outline)},0,5,${String(Math.round(width * 0.1))},${String(Math.round(width * 0.1))},0,1`,
|
|
28
31
|
"",
|
|
29
32
|
"[Events]",
|
|
30
33
|
"Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { builtInBeds } from "./ambient-bed.js";
|
|
2
3
|
import { atmospheres, colorGrades, lookLevels, } from "./edit-settings.js";
|
|
3
4
|
// The edit list is the whole video as data: what plays, for how many frames, how it moves,
|
|
4
5
|
// and the audio under it. Planning (`plan.ts`) writes one and the renderer (`ffmpeg.ts`,
|
|
@@ -28,6 +29,9 @@ export function withPaths(edit, map) {
|
|
|
28
29
|
...(edit.cardFont === undefined
|
|
29
30
|
? {}
|
|
30
31
|
: { cardFont: { ...edit.cardFont, path: map(edit.cardFont.path) } }),
|
|
32
|
+
...(edit.bed?.source.kind === "file"
|
|
33
|
+
? { bed: { ...edit.bed, source: { kind: "file", path: map(edit.bed.source.path) } } }
|
|
34
|
+
: {}),
|
|
31
35
|
};
|
|
32
36
|
}
|
|
33
37
|
const share = z.number().min(0).max(1);
|
|
@@ -88,6 +92,23 @@ const editListSchema = z
|
|
|
88
92
|
.object({ path: z.string().min(1), name: z.string().min(1) })
|
|
89
93
|
.strict()
|
|
90
94
|
.optional(),
|
|
95
|
+
cardColor: z
|
|
96
|
+
.string()
|
|
97
|
+
.regex(/^#[0-9A-Fa-f]{6}$/)
|
|
98
|
+
.optional(),
|
|
99
|
+
bed: z
|
|
100
|
+
.object({
|
|
101
|
+
source: z.discriminatedUnion("kind", [
|
|
102
|
+
z.object({ kind: z.literal("noise"), preset: z.enum(builtInBeds) }).strict(),
|
|
103
|
+
z.object({ kind: z.literal("file"), path: z.string().min(1) }).strict(),
|
|
104
|
+
]),
|
|
105
|
+
levelDb: z.number(),
|
|
106
|
+
fadeInSeconds: z.number().nonnegative(),
|
|
107
|
+
fadeOutAt: z.number().nonnegative(),
|
|
108
|
+
fadeOutSeconds: z.number().nonnegative(),
|
|
109
|
+
})
|
|
110
|
+
.strict()
|
|
111
|
+
.optional(),
|
|
91
112
|
})
|
|
92
113
|
.strict();
|
|
93
114
|
// Reads a recorded edit list back. Strict, so a list written by a newer Slopify, with a
|
|
@@ -95,9 +116,9 @@ const editListSchema = z
|
|
|
95
116
|
export function readEditList(value) {
|
|
96
117
|
const version = typeof value === "object" && value !== null && "version" in value ? value.version : undefined;
|
|
97
118
|
if (version !== editListVersion)
|
|
98
|
-
throw new Error(`This video's edit list is version ${String(version)}, and this Slopify reads version ${editListVersion}. Update Slopify, or use
|
|
119
|
+
throw new Error(`This video's edit list is version ${String(version)}, and this Slopify reads version ${editListVersion}. Update Slopify, or use More → Render the video again in the Video section to plan it again.`);
|
|
99
120
|
const parsed = editListSchema.safeParse(value);
|
|
100
121
|
if (!parsed.success)
|
|
101
|
-
throw new Error(`This video's edit list is damaged (${parsed.error.issues[0]?.path.join(".") || "the list"}: ${parsed.error.issues[0]?.message ?? "not readable"}). Use
|
|
122
|
+
throw new Error(`This video's edit list is damaged (${parsed.error.issues[0]?.path.join(".") || "the list"}: ${parsed.error.issues[0]?.message ?? "not readable"}). Use More → Render the video again in the Video section to plan it again.`);
|
|
102
123
|
return parsed.data;
|
|
103
124
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { wordTimingUnavailable } from "../../kernel/ports/languages.js";
|
|
1
2
|
// How the Video stage edits the slideshow beyond seconds per image, zoom and motion: where the
|
|
2
3
|
// cuts fall, how one shot hands over to the next, the Look (vignette, grain, colour, an
|
|
3
4
|
// atmosphere overlay and chapter cards), and which images come to life as short clips.
|
|
@@ -49,6 +50,8 @@ export const animatePrompt = "Subtle cinematic motion: a slow, steady camera pus
|
|
|
49
50
|
export const cutFloorShare = 0.4;
|
|
50
51
|
// How long a chapter card stays on screen, fades included.
|
|
51
52
|
export const chapterCardSeconds = 2.5;
|
|
53
|
+
// How long the brand kit's end screen card stays on screen at the end of the video.
|
|
54
|
+
export const endScreenSeconds = 5;
|
|
52
55
|
export const cutModeLabels = {
|
|
53
56
|
interval: "Every N seconds",
|
|
54
57
|
narration: "Follow the narration",
|
|
@@ -87,12 +90,14 @@ export const animateModeLabels = {
|
|
|
87
90
|
export function videoEditOf(config) {
|
|
88
91
|
return config.videoEdit ?? legacyVideoEdit;
|
|
89
92
|
}
|
|
90
|
-
// Following the narration needs narration to follow
|
|
91
|
-
//
|
|
93
|
+
// Following the narration needs narration to follow, a video to cut and word timing in the
|
|
94
|
+
// project language (`wordTimingUnavailable`); otherwise the video falls back to Every N
|
|
95
|
+
// seconds, which is also what its fingerprint says.
|
|
92
96
|
export function usesNarrationCuts(config) {
|
|
93
97
|
return (videoEditOf(config).cuts === "narration" &&
|
|
94
98
|
config.sources.audio !== "off" &&
|
|
95
|
-
config.sources.video === "generate"
|
|
99
|
+
config.sources.video === "generate" &&
|
|
100
|
+
wordTimingUnavailable(config.language) === undefined);
|
|
96
101
|
}
|
|
97
102
|
// Chapter cards are placed from the narration's word timing, so they need narration too.
|
|
98
103
|
export function usesChapterCards(config) {
|
|
@@ -111,6 +116,11 @@ export function usesAnimation(config) {
|
|
|
111
116
|
// know which images open a chapter.
|
|
112
117
|
export function editNeedsTiming(config) {
|
|
113
118
|
return (usesNarrationCuts(config) ||
|
|
119
|
+
// "Show tables and figures on screen" places each card where its description is spoken.
|
|
120
|
+
(config.showFigures === true &&
|
|
121
|
+
config.audio?.describeFigures === true &&
|
|
122
|
+
config.sources.audio === "generate" &&
|
|
123
|
+
config.sources.video === "generate") ||
|
|
114
124
|
usesChapterCards(config) ||
|
|
115
125
|
(usesAnimation(config) && videoEditOf(config).animate === "chapters"));
|
|
116
126
|
}
|