@gentbajko/slopify 2.5.0 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -21
- package/NOTICE +12 -0
- package/README.md +24 -41
- package/SUBTITLES.md +1 -1
- package/dist/adapter-registry.js +31 -0
- package/dist/adapters/alignment/cache.js +4 -1
- package/dist/adapters/alignment/ctc.js +25 -23
- package/dist/adapters/alignment/index.js +37 -6
- package/dist/adapters/alignment/multilingual.js +267 -0
- package/dist/adapters/alignment/protocol.js +11 -0
- package/dist/adapters/alignment/quality.js +4 -3
- package/dist/adapters/alignment/sentences.js +95 -0
- package/dist/adapters/alignment/spec.js +24 -0
- package/dist/adapters/alignment/spell.js +769 -0
- package/dist/adapters/alignment/text.js +56 -9
- package/dist/adapters/alignment/window.js +34 -30
- package/dist/adapters/alignment/worker.js +23 -21
- package/dist/adapters/explain.js +11 -11
- package/dist/adapters/host-cli/index.js +44 -5
- package/dist/adapters/host-cli/transport.js +7 -5
- package/dist/adapters/image/bytes.js +2 -2
- package/dist/adapters/image/codex-output.js +1 -1
- package/dist/adapters/image/codex.js +87 -22
- package/dist/adapters/image/fal.js +13 -5
- package/dist/adapters/image/google.js +11 -8
- package/dist/adapters/image/openai.js +14 -7
- package/dist/adapters/image/reference.js +34 -0
- package/dist/adapters/image/replicate.js +7 -2
- package/dist/adapters/image/video.js +2 -2
- package/dist/adapters/key-probes.js +85 -0
- package/dist/adapters/llm/claude-code.js +168 -32
- package/dist/adapters/llm/cli-login-error.js +1 -1
- package/dist/adapters/llm/codex-limits.js +157 -0
- package/dist/adapters/llm/codex.js +99 -12
- package/dist/adapters/llm/document-workspace.js +2 -2
- package/dist/adapters/llm/gemini-workspace.js +1 -1
- package/dist/adapters/llm/gemini.js +16 -3
- package/dist/adapters/llm/image-workspace.js +58 -0
- package/dist/adapters/llm/openrouter.js +11 -3
- package/dist/adapters/llm/run-cli.js +12 -7
- package/dist/adapters/tts/cartesia.js +10 -0
- package/dist/adapters/tts/elevenlabs.js +31 -9
- package/dist/adapters/tts/gemini.js +206 -0
- package/dist/adapters/tts/inworld-async.js +9 -5
- package/dist/adapters/tts/inworld.js +17 -3
- package/dist/adapters/tts/openai.js +3 -0
- package/dist/adapters/tts/pcm-mp3.js +62 -0
- package/dist/adapters/tts/system.js +147 -0
- package/dist/adapters/tts/voice-languages.js +80 -0
- package/dist/assets/fonts/noto/NotoSans-Regular.ttf +0 -0
- package/dist/assets/fonts/noto/NotoSansArabic-Regular.ttf +0 -0
- package/dist/assets/fonts/noto/NotoSansDevanagari-Regular.ttf +0 -0
- package/dist/assets/fonts/noto/NotoSansHebrew-Regular.ttf +0 -0
- package/dist/assets/fonts/noto/NotoSansThai-Regular.ttf +0 -0
- package/dist/assets/fonts/noto/OFL.txt +93 -0
- package/dist/assets/fonts/noto/SOURCE.txt +18 -0
- package/dist/assets/models.yaml +38 -0
- package/dist/assets/sample/sample-audiobook.tar +0 -0
- package/dist/assets/sample/sample-podcast.tar +0 -0
- package/dist/assets/sample/sample-project.tar +0 -0
- package/dist/assets/style-preview/narration.mp3 +0 -0
- package/dist/assets/style-preview/tall-1.jpg +0 -0
- package/dist/assets/style-preview/tall-2.jpg +0 -0
- package/dist/assets/style-preview/tall-3.jpg +0 -0
- package/dist/assets/style-preview/wide-1.jpg +0 -0
- package/dist/assets/style-preview/wide-2.jpg +0 -0
- package/dist/assets/style-preview/wide-3.jpg +0 -0
- package/dist/catalog/merge.js +132 -0
- package/dist/catalog/registry.js +17 -14
- package/dist/catalog/schema.js +2 -0
- package/dist/catalog/store.js +147 -8
- package/dist/catalog/validate.js +2 -2
- package/dist/compose.yaml +71 -0
- package/dist/edge/autostart/docker-record.js +32 -0
- package/dist/edge/autostart/docker.js +118 -0
- package/dist/edge/autostart/index.js +78 -0
- package/dist/edge/autostart/launcher.js +176 -0
- package/dist/edge/autostart/model.js +3 -0
- package/dist/edge/autostart/native.js +210 -0
- package/dist/edge/autostart/prompt.js +72 -0
- package/dist/edge/autostart/service.js +90 -0
- package/dist/edge/cli-args.js +71 -0
- package/dist/edge/cli.js +64 -63
- package/dist/edge/docker-install/activation.js +56 -0
- package/dist/edge/docker-install/apply.js +458 -0
- package/dist/edge/docker-install/engine.js +362 -0
- package/dist/edge/docker-install/run.js +160 -0
- package/dist/edge/docker-install/state.js +146 -0
- package/dist/edge/{docker-projects → docker-install}/volume.js +1 -5
- package/dist/edge/docker.js +14 -22
- package/dist/edge/events/hub.js +3 -1
- package/dist/edge/host-cli.js +1 -1
- package/dist/edge/http/actions.js +38 -11
- package/dist/edge/http/app.js +60 -2
- package/dist/edge/http/auditions.js +70 -0
- package/dist/edge/http/autostart.js +37 -0
- package/dist/edge/http/byte-range.js +42 -0
- package/dist/edge/http/channel-memory.js +92 -0
- package/dist/edge/http/channels.js +128 -0
- package/dist/edge/http/diagnostics.js +1 -0
- package/dist/edge/http/draft-files.js +2 -4
- package/dist/edge/http/entries.js +22 -3
- package/dist/edge/http/files.js +11 -7
- package/dist/edge/http/folder-location.js +1 -1
- package/dist/edge/http/home.js +39 -0
- package/dist/edge/http/host-cli.js +29 -5
- package/dist/edge/http/narration-peaks.js +76 -0
- package/dist/edge/http/onboarding.js +212 -0
- package/dist/edge/http/open-folder.js +1 -1
- package/dist/edge/http/patch-notes.js +45 -0
- package/dist/edge/http/planning.js +4 -2
- package/dist/edge/http/project-create.js +87 -0
- package/dist/edge/http/project-templates.js +19 -2
- package/dist/edge/http/projects.js +37 -100
- package/dist/edge/http/prompts.js +40 -3
- package/dist/edge/http/pronunciations.js +23 -4
- package/dist/edge/http/providers.js +119 -3
- package/dist/edge/http/reviews.js +131 -0
- package/dist/edge/http/revision-files.js +3 -5
- package/dist/edge/http/revisions.js +1 -1
- package/dist/edge/http/run-cost.js +25 -0
- package/dist/edge/http/schedules.js +115 -6
- package/dist/edge/http/settings.js +84 -6
- package/dist/edge/http/storage-files.js +50 -0
- package/dist/edge/http/storage.js +43 -2
- package/dist/edge/http/studio.js +287 -0
- package/dist/edge/http/style-preview.js +61 -0
- package/dist/edge/http/subtitles.js +1 -1
- package/dist/edge/http/trash.js +69 -0
- package/dist/edge/http/tutorials.js +59 -0
- package/dist/edge/http/update.js +24 -3
- package/dist/edge/http/whats-new.js +15 -0
- package/dist/edge/http/youtube-edits.js +92 -0
- package/dist/edge/native-update.js +62 -0
- package/dist/extension/slopify-studio-chrome.zip +0 -0
- package/dist/extension/slopify-studio-firefox.zip +0 -0
- package/dist/host-cli/gemini-login.js +83 -0
- package/dist/host-cli/open-folder.js +10 -10
- package/dist/host-cli/runtime.js +9 -1
- package/dist/host-cli/status.js +4 -2
- package/dist/kernel/db/migrations/0025-automatic-reviews.sql +40 -0
- package/dist/kernel/db/migrations/0026-schedule-topic-generation.sql +23 -0
- package/dist/kernel/db/migrations/0027-run-cost.sql +58 -0
- package/dist/kernel/db/migrations/0030-automatic-retries.sql +18 -0
- package/dist/kernel/db/migrations/0031-prompt-history-and-description-edits.sql +33 -0
- package/dist/kernel/db/migrations/0032-channels.sql +65 -0
- package/dist/kernel/db/migrations/0034-script-prompts.sql +23 -0
- package/dist/kernel/db/migrations/0035-narration-aliases.sql +13 -0
- package/dist/kernel/db/migrations/0037-project-uploads.sql +8 -0
- package/dist/kernel/db/migrations/0038-voice-languages.sql +7 -0
- package/dist/kernel/db/migrations/0039-channel-essentials.sql +64 -0
- package/dist/kernel/db/migrations/0040-standalone-usage.sql +35 -0
- package/dist/kernel/db/migrations/0041-cast-hosts.sql +3 -0
- package/dist/kernel/paths.js +27 -6
- package/dist/kernel/pipeline.js +4 -1
- package/dist/kernel/ports/host-cli.js +122 -14
- package/dist/kernel/ports/key-probe.js +1 -0
- package/dist/kernel/ports/languages.js +157 -0
- package/dist/kernel/ports/model.js +10 -5
- package/dist/kernel/ports/narration-aliases.js +60 -0
- package/dist/kernel/ports/plan-limits.js +36 -0
- package/dist/kernel/ports/system-speech.js +203 -0
- package/dist/kernel/runner/attempt.js +17 -8
- package/dist/kernel/runner/audition.js +36 -0
- package/dist/kernel/runner/graph.js +40 -5
- package/dist/kernel/runner/index.js +39 -9
- package/dist/kernel/runner/meter.js +1 -0
- package/dist/kernel/runner/providers.js +142 -7
- package/dist/kernel/runner/retry-policy.js +43 -0
- package/dist/kernel/runner/standalone.js +134 -0
- package/dist/kernel/runner/work-authority.js +28 -2
- package/dist/main.js +277 -16
- package/dist/model-catalog.js +2 -0
- package/dist/patch-notes/0.4.2-to-1.0.md +93 -0
- package/dist/patch-notes/1.0-to-2.0.md +74 -0
- package/dist/patch-notes/2.0-to-3.0.md +257 -0
- package/dist/patch-notes/3.0.0.md +70 -0
- package/dist/patch-notes/3.0.1.md +105 -0
- package/dist/patch-notes/index.json +32 -0
- package/dist/slices/admission/model.js +27 -0
- package/dist/slices/admission/repo.js +24 -4
- package/dist/slices/admission/rules.js +80 -2
- package/dist/slices/admission/schema.js +58 -1
- package/dist/slices/admission/short-mode.js +47 -0
- package/dist/slices/admission/start.js +42 -6
- package/dist/slices/article/continuation.js +2 -2
- package/dist/slices/article/provided-entries.js +2 -2
- package/dist/slices/article/run.js +6 -2
- package/dist/slices/article/segments.js +3 -3
- package/dist/slices/backups/folder.js +14 -6
- package/dist/slices/batch/index.js +7 -2
- package/dist/slices/cancel/index.js +1 -1
- package/dist/slices/channels/cast-images.js +110 -0
- package/dist/slices/channels/cast-match.js +32 -0
- package/dist/slices/channels/images.js +19 -0
- package/dist/slices/channels/model.js +5 -0
- package/dist/slices/channels/rebrand.js +100 -0
- package/dist/slices/channels/repo.js +221 -0
- package/dist/slices/channels/runs.js +129 -0
- package/dist/slices/channels/schema.js +152 -0
- package/dist/slices/channels/service.js +180 -0
- package/dist/slices/channels/studio-csv.js +110 -0
- package/dist/slices/channels/videos.js +120 -0
- package/dist/slices/checkpoints/recovery.js +10 -0
- package/dist/slices/control/index.js +5 -2
- package/dist/slices/control/providers.js +4 -1
- package/dist/slices/document/fonts.js +1 -1
- package/dist/slices/document/render.js +1 -1
- package/dist/slices/episodes/related.js +90 -0
- package/dist/slices/episodes/repo.js +60 -0
- package/dist/slices/episodes/service.js +58 -0
- package/dist/slices/episodes/summarize.js +170 -0
- package/dist/slices/estimate/index.js +109 -28
- package/dist/slices/estimate/requests.js +68 -2
- package/dist/slices/eta/history.js +56 -0
- package/dist/slices/eta/model.js +63 -0
- package/dist/slices/eta/view.js +25 -0
- package/dist/slices/fixes/rules.js +73 -0
- package/dist/slices/fonts/coverage.js +82 -0
- package/dist/slices/fonts/sfnt.js +74 -0
- package/dist/slices/images/run.js +8 -5
- package/dist/slices/images/scale.js +84 -0
- package/dist/slices/library/diff.js +122 -0
- package/dist/slices/library/history.js +100 -0
- package/dist/slices/library/model.js +3 -0
- package/dist/slices/library/photorealistic.js +49 -0
- package/dist/slices/library/repo.js +19 -14
- package/dist/slices/library/save.js +62 -12
- package/dist/slices/library/slots.js +7 -1
- package/dist/slices/library/used-by.js +153 -0
- package/dist/slices/loudness/level-pieces.js +41 -0
- package/dist/slices/loudness/loudnorm.js +164 -0
- package/dist/slices/loudness/model.js +153 -0
- package/dist/slices/model-upkeep/model.js +10 -0
- package/dist/slices/model-upkeep/switch.js +217 -0
- package/dist/slices/model-upkeep/usage.js +185 -0
- package/dist/slices/narration/aliases-library.js +31 -0
- package/dist/slices/narration/aliases-schema.js +53 -0
- package/dist/slices/narration/aliases.js +49 -0
- package/dist/slices/narration/blocks.js +352 -0
- package/dist/slices/narration/chunk.js +19 -7
- package/dist/slices/narration/concat.js +2 -2
- package/dist/slices/narration/describe.js +45 -0
- package/dist/slices/narration/pauses-model.js +63 -0
- package/dist/slices/narration/pauses.js +269 -0
- package/dist/slices/narration/peaks-model.js +1 -0
- package/dist/slices/narration/peaks.js +94 -0
- package/dist/slices/narration/preparation.js +21 -3
- package/dist/slices/narration/pronunciation-chunks.js +3 -3
- package/dist/slices/narration/pronunciation.js +72 -25
- package/dist/slices/narration/run.js +6 -6
- package/dist/slices/narration/shared-glossary.js +1 -1
- package/dist/slices/narration/steering.js +15 -4
- package/dist/slices/notifications/notifier.js +47 -3
- package/dist/slices/notifications/rules.js +48 -3
- package/dist/slices/onboarding/copy.js +143 -0
- package/dist/slices/onboarding/first-run.js +72 -0
- package/dist/slices/onboarding/full-video.js +81 -0
- package/dist/slices/onboarding/install.js +119 -0
- package/dist/slices/onboarding/model.js +85 -0
- package/dist/slices/onboarding/packs.js +491 -0
- package/dist/slices/onboarding/quick-short.js +144 -0
- package/dist/slices/onboarding/sample.js +132 -0
- package/dist/slices/onboarding/state.js +68 -0
- package/dist/slices/onboarding/template.js +73 -0
- package/dist/slices/patch-notes/library.js +73 -0
- package/dist/slices/patch-notes/seen.js +77 -0
- package/dist/slices/play-drafts/convert.js +127 -8
- package/dist/slices/play-drafts/readiness.js +4 -1
- package/dist/slices/play-drafts/repo.js +2 -0
- package/dist/slices/play-drafts/review-inputs.js +20 -3
- package/dist/slices/play-drafts/schema.js +89 -0
- package/dist/slices/play-drafts/service.js +4 -1
- package/dist/slices/play-drafts/start.js +43 -18
- package/dist/slices/project-templates/from-project.js +59 -6
- package/dist/slices/project-templates/next-chapter.js +94 -0
- package/dist/slices/project-templates/one-off.js +26 -0
- package/dist/slices/project-templates/repo.js +2 -1
- package/dist/slices/project-templates/schema.js +7 -1
- package/dist/slices/project-templates/service.js +15 -4
- package/dist/slices/project-templates/setup.js +18 -2
- package/dist/slices/rebuild/admission-repo.js +1 -1
- package/dist/slices/rebuild/model.js +6 -0
- package/dist/slices/rebuild/preview-details.js +46 -3
- package/dist/slices/rebuild/preview-plan.js +13 -6
- package/dist/slices/rebuild/recipe-audio-parts.js +40 -7
- package/dist/slices/rebuild/recipe-audio.js +66 -12
- package/dist/slices/rebuild/recipe-build.js +8 -4
- package/dist/slices/rebuild/recipe-cast.js +18 -0
- package/dist/slices/rebuild/recipe-checkpoints.js +11 -9
- package/dist/slices/rebuild/recipe-describe.js +93 -0
- package/dist/slices/rebuild/recipe-edit.js +13 -1
- package/dist/slices/rebuild/recipe-exports.js +67 -8
- package/dist/slices/rebuild/recipe-input-schema.js +35 -0
- package/dist/slices/rebuild/recipe-legacy.js +9 -0
- package/dist/slices/rebuild/recipe-loudness.js +45 -0
- package/dist/slices/rebuild/recipe-model.js +25 -6
- package/dist/slices/rebuild/recipe-pauses.js +7 -0
- package/dist/slices/rebuild/recipe-preparation.js +3 -3
- package/dist/slices/rebuild/recipe-provider-choice.js +11 -1
- package/dist/slices/rebuild/recipe-reference.js +15 -1
- package/dist/slices/rebuild/recipe-reviews.js +117 -0
- package/dist/slices/rebuild/recipe-save.js +4 -1
- package/dist/slices/rebuild/recipe-shorts.js +23 -3
- package/dist/slices/rebuild/recipe-text.js +114 -19
- package/dist/slices/rebuild/recipe-validation.js +14 -2
- package/dist/slices/rebuild/recipe-visual.js +96 -33
- package/dist/slices/rebuild/recipe-voices.js +286 -0
- package/dist/slices/rebuild/recipe-work.js +77 -6
- package/dist/slices/rebuild/recipe-youtube.js +2 -0
- package/dist/slices/rebuild/recovery-conflict.js +7 -3
- package/dist/slices/rebuild/recovery-model.js +3 -0
- package/dist/slices/rebuild/recovery-selection.js +28 -2
- package/dist/slices/rebuild/recovery.js +35 -17
- package/dist/slices/rebuild/repo.js +3 -3
- package/dist/slices/rebuild/retry.js +18 -0
- package/dist/slices/rebuild/review-redo.js +125 -0
- package/dist/slices/rebuild/runtime-actions.js +5 -0
- package/dist/slices/rebuild/runtime-admission.js +4 -1
- package/dist/slices/rebuild/runtime-animate.js +4 -4
- package/dist/slices/rebuild/runtime-article.js +5 -5
- package/dist/slices/rebuild/runtime-document.js +4 -4
- package/dist/slices/rebuild/runtime-export-bed.js +43 -0
- package/dist/slices/rebuild/runtime-export-edit.js +70 -11
- package/dist/slices/rebuild/runtime-export-inputs.js +52 -10
- package/dist/slices/rebuild/runtime-export-short.js +85 -0
- package/dist/slices/rebuild/runtime-export.js +84 -19
- package/dist/slices/rebuild/runtime-figure-card.js +87 -0
- package/dist/slices/rebuild/runtime-image.js +21 -2
- package/dist/slices/rebuild/runtime-local.js +148 -24
- package/dist/slices/rebuild/runtime-materialize.js +8 -0
- package/dist/slices/rebuild/runtime-narration-reuse.js +2 -1
- package/dist/slices/rebuild/runtime-narration-text.js +7 -7
- package/dist/slices/rebuild/runtime-pictures.js +28 -0
- package/dist/slices/rebuild/runtime-piece-label.js +27 -0
- package/dist/slices/rebuild/runtime-provider.js +126 -18
- package/dist/slices/rebuild/runtime-publication.js +21 -5
- package/dist/slices/rebuild/runtime-review.js +267 -0
- package/dist/slices/rebuild/runtime-run.js +17 -11
- package/dist/slices/rebuild/runtime-shorts.js +44 -17
- package/dist/slices/rebuild/runtime-store.js +62 -11
- package/dist/slices/rebuild/runtime-subtitles.js +72 -19
- package/dist/slices/rebuild/runtime-voices.js +103 -0
- package/dist/slices/rebuild/runtime-youtube.js +7 -6
- package/dist/slices/rebuild/service-readiness.js +3 -3
- package/dist/slices/rebuild/soften.js +49 -0
- package/dist/slices/rebuild/transition-repo.js +9 -2
- package/dist/slices/research/run.js +4 -4
- package/dist/slices/research/synthesis.js +2 -2
- package/dist/slices/reviews/model.js +105 -0
- package/dist/slices/reviews/outcome.js +22 -0
- package/dist/slices/reviews/repo.js +118 -0
- package/dist/slices/reviews/rules.js +63 -0
- package/dist/slices/reviews/verdict.js +71 -0
- package/dist/slices/reviews/view.js +30 -0
- package/dist/slices/revisions/adopt-content.js +5 -2
- package/dist/slices/revisions/adopt.js +3 -1
- package/dist/slices/revisions/downloads.js +7 -1
- package/dist/slices/revisions/image-plan.js +183 -0
- package/dist/slices/revisions/mutation-assets.js +10 -0
- package/dist/slices/revisions/mutation-cues.js +71 -0
- package/dist/slices/revisions/mutation-request.js +2 -1
- package/dist/slices/revisions/mutations.js +97 -3
- package/dist/slices/revisions/schema.js +11 -1
- package/dist/slices/run-cost/limits.js +128 -0
- package/dist/slices/run-cost/meter.js +47 -0
- package/dist/slices/run-cost/panel.js +256 -0
- package/dist/slices/run-cost/pricing.js +95 -0
- package/dist/slices/run-cost/week.js +108 -0
- package/dist/slices/schedules/agenda.js +222 -0
- package/dist/slices/schedules/repo.js +58 -18
- package/dist/slices/schedules/scheduler.js +68 -17
- package/dist/slices/schedules/schema.js +179 -1
- package/dist/slices/schedules/service.js +43 -4
- package/dist/slices/schedules/similar.js +128 -0
- package/dist/slices/schedules/topic-list.js +163 -0
- package/dist/slices/schedules/topics.js +523 -0
- package/dist/slices/settings/first-run.js +106 -0
- package/dist/slices/settings/health.js +247 -0
- package/dist/slices/settings/key-guides.js +138 -0
- package/dist/slices/settings/key-test.js +93 -0
- package/dist/slices/settings/keys.js +10 -5
- package/dist/slices/settings/model-reach.js +52 -0
- package/dist/slices/settings/model.js +55 -1
- package/dist/slices/settings/models.js +5 -1
- package/dist/slices/settings/playback.js +15 -0
- package/dist/slices/settings/readiness.js +14 -2
- package/dist/slices/settings/repo.js +17 -3
- package/dist/slices/settings/tutorial-schema.js +5 -0
- package/dist/slices/settings/voices.js +64 -4
- package/dist/slices/settings/whats-new.js +64 -0
- package/dist/slices/shorts/captions.js +9 -0
- package/dist/slices/shorts/pick.js +3 -3
- package/dist/slices/shorts/prompts.js +1 -1
- package/dist/slices/shorts/render.js +63 -13
- package/dist/slices/storage/asset-name.js +3 -0
- package/dist/slices/storage/backup-export.js +48 -9
- package/dist/slices/storage/backup-format.js +38 -1
- package/dist/slices/storage/backup-import.js +191 -19
- package/dist/slices/storage/delete-project.js +7 -4
- package/dist/slices/storage/documents.js +87 -0
- package/dist/slices/storage/downloads.js +33 -1
- package/dist/slices/storage/files-location.js +531 -0
- package/dist/slices/storage/layout.js +14 -4
- package/dist/slices/storage/model.js +11 -0
- package/dist/slices/storage/portable.js +94 -12
- package/dist/slices/storage/reconcile.js +14 -1
- package/dist/slices/storage/repo.js +4 -1
- package/dist/slices/storage/schema.js +11 -0
- package/dist/slices/storage/trim.js +146 -0
- package/dist/slices/studio/disclosure.js +74 -0
- package/dist/slices/studio/model.js +30 -0
- package/dist/slices/studio/pack.js +211 -0
- package/dist/slices/studio/queue.js +69 -0
- package/dist/slices/studio/settings.js +170 -0
- package/dist/slices/style-preview/images.js +61 -0
- package/dist/slices/style-preview/narration.js +33 -0
- package/dist/slices/style-preview/render.js +148 -0
- package/dist/slices/style-preview/schema.js +59 -0
- package/dist/slices/style-preview/service.js +92 -0
- package/dist/slices/style-preview/settings.js +139 -0
- package/dist/slices/subtitles/captions.js +42 -9
- package/dist/slices/subtitles/model.js +22 -0
- package/dist/slices/subtitles/prepare.js +27 -16
- package/dist/slices/subtitles/transcript.js +1 -1
- package/dist/slices/thumbnail/run.js +7 -7
- package/dist/slices/trash/model.js +25 -0
- package/dist/slices/trash/service.js +273 -0
- package/dist/slices/tutorials/anchors.js +57 -0
- package/dist/slices/tutorials/library.js +185 -0
- package/dist/slices/uploads/repo.js +20 -0
- package/dist/slices/video/ambient-bed-schema.js +15 -0
- package/dist/slices/video/ambient-bed.js +95 -0
- package/dist/slices/video/ambient-mix.js +72 -0
- package/dist/slices/video/audio-inputs.js +2 -2
- package/dist/slices/video/cards.js +5 -2
- package/dist/slices/video/edit-list.js +23 -2
- package/dist/slices/video/edit-settings.js +13 -3
- package/dist/slices/video/ffmpeg.js +63 -9
- package/dist/slices/video/figure-card.js +492 -0
- package/dist/slices/video/figure-spans.js +51 -0
- package/dist/slices/video/plan.js +125 -13
- package/dist/slices/video/run.js +14 -12
- package/dist/slices/video/slideshow.js +39 -4
- package/dist/slices/voices/attribution.js +69 -0
- package/dist/slices/voices/audio-files.js +87 -0
- package/dist/slices/voices/audition.js +25 -0
- package/dist/slices/voices/cast.js +74 -0
- package/dist/slices/voices/grouping.js +59 -0
- package/dist/slices/voices/join.js +35 -0
- package/dist/slices/voices/languages.js +25 -0
- package/dist/slices/voices/model.js +218 -0
- package/dist/slices/voices/palette.js +26 -0
- package/dist/slices/voices/panel.js +91 -0
- package/dist/slices/voices/portraits.js +55 -0
- package/dist/slices/voices/script.js +136 -0
- package/dist/slices/voices/timing.js +58 -0
- package/dist/slices/youtube/answer.js +1 -1
- package/dist/slices/youtube/chapters.js +126 -0
- package/dist/slices/youtube/edits-repo.js +84 -0
- package/dist/slices/youtube/edits.js +89 -0
- package/dist/slices/youtube/placeholders.js +98 -0
- package/dist/slices/youtube/transcript.js +11 -4
- package/dist/tutorials/AI-CLIs.md +146 -0
- package/dist/tutorials/Backups.md +121 -0
- package/dist/tutorials/Calendar.md +103 -0
- package/dist/tutorials/Cast-Library.md +112 -0
- package/dist/tutorials/Channels.md +148 -0
- package/dist/tutorials/Costs-and-Run-Cost.md +136 -0
- package/dist/tutorials/Docker.md +171 -0
- package/dist/tutorials/Document-Themes.md +207 -0
- package/dist/tutorials/Editing-a-Project.md +204 -0
- package/dist/tutorials/FAQ.md +134 -0
- package/dist/tutorials/First-Launch-and-Welcome.md +153 -0
- package/dist/tutorials/Home-and-Projects.md +174 -0
- package/dist/tutorials/Home.md +119 -0
- package/dist/tutorials/Install.md +154 -0
- package/dist/tutorials/Intros-and-Outros.md +86 -0
- package/dist/tutorials/Keyboard-Shortcuts-and-Command-Palette.md +215 -0
- package/dist/tutorials/Library-Overview.md +75 -0
- package/dist/tutorials/Models.md +133 -0
- package/dist/tutorials/Multiple-Voices.md +161 -0
- package/dist/tutorials/Narration-Aliases-and-Glossary.md +129 -0
- package/dist/tutorials/Notifications.md +86 -0
- package/dist/tutorials/Other-Languages.md +110 -0
- package/dist/tutorials/PDF-Documents.md +100 -0
- package/dist/tutorials/Play-Images.md +135 -0
- package/dist/tutorials/Play-Narration.md +167 -0
- package/dist/tutorials/Play-Outputs.md +109 -0
- package/dist/tutorials/Play-Overview.md +216 -0
- package/dist/tutorials/Play-Title-and-Article.md +135 -0
- package/dist/tutorials/Play-Video-and-Style.md +143 -0
- package/dist/tutorials/Project-Page.md +228 -0
- package/dist/tutorials/Prompts.md +136 -0
- package/dist/tutorials/Providers-and-Keys.md +290 -0
- package/dist/tutorials/Publishing-to-YouTube.md +135 -0
- package/dist/tutorials/Recovery-and-Retries.md +141 -0
- package/dist/tutorials/Reviews-and-Checkpoints.md +143 -0
- package/dist/tutorials/Schedules.md +154 -0
- package/dist/tutorials/Settings-Reference.md +258 -0
- package/dist/tutorials/Shorts.md +116 -0
- package/dist/tutorials/Start-at-Login.md +126 -0
- package/dist/tutorials/Studio-Extension.md +146 -0
- package/dist/tutorials/Templates.md +146 -0
- package/dist/tutorials/Trash.md +87 -0
- package/dist/tutorials/Troubleshooting.md +149 -0
- package/dist/tutorials/Uninstalling-and-Moving.md +113 -0
- package/dist/tutorials/Updating-and-Patch-Notes.md +120 -0
- package/dist/tutorials/Video-Editing.md +135 -0
- package/dist/tutorials/Where-Your-Files-Live.md +149 -0
- package/dist/tutorials/YouTube-Description.md +132 -0
- package/dist/tutorials/Your-First-Short.md +125 -0
- package/dist/tutorials/_Footer.md +1 -0
- package/dist/tutorials/_Sidebar.md +69 -0
- package/dist/updater/service.js +66 -21
- package/dist/web/assets/index-DBLSfqA-.css +1 -0
- package/dist/web/assets/index-DFdIhmm0.js +307 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
- package/dist/web/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
- package/dist/web/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
- package/dist/web/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
- package/dist/web/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
- package/dist/web/assets/{pdf-BLF2i8c2.js → pdf-B9OY1ioX.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +5 -5
- package/dist/edge/docker-launch.js +0 -81
- package/dist/edge/docker-projects/activation.js +0 -35
- package/dist/edge/docker-projects/claims.js +0 -85
- package/dist/edge/docker-projects/committed.js +0 -40
- package/dist/edge/docker-projects/engine.js +0 -461
- package/dist/edge/docker-projects/install.js +0 -254
- package/dist/edge/docker-projects/prune.js +0 -66
- package/dist/edge/docker-projects/recover.js +0 -100
- package/dist/edge/docker-projects/state.js +0 -225
- package/dist/web/assets/index-CktpxlJd.js +0 -171
- package/dist/web/assets/index-DB-vOnGU.css +0 -1
- package/scripts/docker-run.sh +0 -3
- /package/dist/edge/{docker-projects → docker-install}/tree.js +0 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { copyFileSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { defaultMotionStyle, defaultZoomPercent } from "../admission/rules.js";
|
|
4
|
+
import { isMissingFont, resolveBoldFont, resolveFont } from "../fonts/index.js";
|
|
5
|
+
import { renderShort } from "../shorts/render.js";
|
|
6
|
+
import { serializeAss } from "../subtitles/captions.js";
|
|
7
|
+
import { subtitleFrame } from "../subtitles/layout.js";
|
|
8
|
+
import { planRender } from "../video/plan.js";
|
|
9
|
+
import { renderSlideshow } from "../video/slideshow.js";
|
|
10
|
+
import { bundledSampleAssets, sampleNarrationSeconds } from "./narration.js";
|
|
11
|
+
import { stylePreviewSeconds } from "./schema.js";
|
|
12
|
+
import { previewCues, previewWords } from "./settings.js";
|
|
13
|
+
export class StylePreviewError extends Error {
|
|
14
|
+
reason;
|
|
15
|
+
constructor(reason, message) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.reason = reason;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
// ceiling: small enough to render in a few seconds on a laptop, big enough to judge a font.
|
|
21
|
+
// The short side is 270 px; the captions' script keeps its full-size frame and libass scales
|
|
22
|
+
// it, so a caption sits exactly where it would in the real video.
|
|
23
|
+
const previewFrame = {
|
|
24
|
+
"16:9": { width: 480, height: 270 },
|
|
25
|
+
"9:16": { width: 270, height: 480 },
|
|
26
|
+
};
|
|
27
|
+
const shotSeconds = stylePreviewSeconds / 3;
|
|
28
|
+
const chapterTitle = "Chapter one";
|
|
29
|
+
export function ffmpegStylePreview(deps) {
|
|
30
|
+
const assets = deps.assets ?? bundledSampleAssets();
|
|
31
|
+
return async (settings, output, signal, picture) => {
|
|
32
|
+
try {
|
|
33
|
+
// A real picture is shown for all three shots: the motion, the Look and the captions read
|
|
34
|
+
// on it, and the transition and chapter card still show at the cuts.
|
|
35
|
+
const images = picture !== undefined
|
|
36
|
+
? [picture, picture, picture]
|
|
37
|
+
: settings.format === "9:16"
|
|
38
|
+
? assets.tall
|
|
39
|
+
: assets.wide;
|
|
40
|
+
if (settings.short !== undefined)
|
|
41
|
+
await renderShortPreview(deps, settings, settings.short, assets, images, output, signal);
|
|
42
|
+
else
|
|
43
|
+
await renderPreview(deps, settings, assets, images, output, signal);
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
throw explained(deps.log, error);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
async function renderPreview(deps, settings, assets, images, output, signal) {
|
|
51
|
+
mkdirSync(deps.dir, { recursive: true, mode: 0o700 });
|
|
52
|
+
const workspace = mkdtempSync(join(deps.dir, "work-"));
|
|
53
|
+
try {
|
|
54
|
+
const cardFont = settings.chapterCard === null
|
|
55
|
+
? undefined
|
|
56
|
+
: await resolveFont(deps.paths, settings.chapterCard.fontId);
|
|
57
|
+
const plan = planRender({
|
|
58
|
+
format: settings.format,
|
|
59
|
+
gapSeconds: 0,
|
|
60
|
+
edgeSeconds: 0,
|
|
61
|
+
imageSeconds: shotSeconds,
|
|
62
|
+
zoomPercent: defaultZoomPercent,
|
|
63
|
+
motionStyle: defaultMotionStyle,
|
|
64
|
+
images,
|
|
65
|
+
output,
|
|
66
|
+
edit: {
|
|
67
|
+
...(settings.transition === null ? {} : { transition: settings.transition }),
|
|
68
|
+
look: settings.look,
|
|
69
|
+
...(cardFont === undefined
|
|
70
|
+
? {}
|
|
71
|
+
: {
|
|
72
|
+
cards: {
|
|
73
|
+
// On the cut into the second still, so the transition and the card both show.
|
|
74
|
+
chapters: [{ title: chapterTitle, start: shotSeconds }],
|
|
75
|
+
font: { path: cardFont.path, name: cardFont.assName },
|
|
76
|
+
},
|
|
77
|
+
}),
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
const edit = {
|
|
81
|
+
...plan.editList,
|
|
82
|
+
...previewFrame[settings.format],
|
|
83
|
+
audio: [{ kind: "body", path: assets.narration, seconds: plan.totalSeconds }],
|
|
84
|
+
};
|
|
85
|
+
const captions = settings.captions;
|
|
86
|
+
if (captions !== null) {
|
|
87
|
+
const font = await resolveFont(deps.paths, captions.fontId);
|
|
88
|
+
mkdirSync(join(workspace, "fonts"), { mode: 0o700 });
|
|
89
|
+
copyFileSync(font.path, join(workspace, "fonts", `selected${font.extension}`));
|
|
90
|
+
writeFileSync(join(workspace, "subtitles.ass"), serializeAss(previewCues(captions.text), {
|
|
91
|
+
...subtitleFrame(settings.format),
|
|
92
|
+
fontName: font.assName,
|
|
93
|
+
fontSize: captions.fontSize,
|
|
94
|
+
position: captions.position,
|
|
95
|
+
}), { mode: 0o600 });
|
|
96
|
+
}
|
|
97
|
+
await renderSlideshow({
|
|
98
|
+
bin: deps.ffmpeg,
|
|
99
|
+
edit,
|
|
100
|
+
output,
|
|
101
|
+
burnSubtitles: captions !== null,
|
|
102
|
+
cwd: workspace,
|
|
103
|
+
scratch: workspace,
|
|
104
|
+
signal,
|
|
105
|
+
log: deps.log,
|
|
106
|
+
onProgress: () => { },
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
rmSync(workspace, { recursive: true, force: true });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// The Shorts layout, drawn at the preview's size: the same audio cut, image timing, captions
|
|
114
|
+
// and headline a short gets, from the sample narration.
|
|
115
|
+
async function renderShortPreview(deps, settings, short, assets, images, output, signal) {
|
|
116
|
+
mkdirSync(deps.dir, { recursive: true, mode: 0o700 });
|
|
117
|
+
await renderShort({
|
|
118
|
+
bin: deps.ffmpeg,
|
|
119
|
+
timeline: [{ kind: "body", path: assets.narration, seconds: sampleNarrationSeconds }],
|
|
120
|
+
start: 0,
|
|
121
|
+
end: sampleNarrationSeconds,
|
|
122
|
+
images,
|
|
123
|
+
imageSeconds: shotSeconds,
|
|
124
|
+
motionStyle: defaultMotionStyle,
|
|
125
|
+
zoomPercent: defaultZoomPercent,
|
|
126
|
+
words: previewWords(short.text),
|
|
127
|
+
font: await resolveBoldFont(deps.paths, short.fontId),
|
|
128
|
+
...(short.title === null ? {} : { title: short.title }),
|
|
129
|
+
speed: short.speed,
|
|
130
|
+
frame: previewFrame[settings.format],
|
|
131
|
+
output,
|
|
132
|
+
scratch: deps.dir,
|
|
133
|
+
signal,
|
|
134
|
+
log: deps.log,
|
|
135
|
+
onProgress: () => { },
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
function explained(log, error) {
|
|
139
|
+
if (error instanceof StylePreviewError)
|
|
140
|
+
return error;
|
|
141
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
142
|
+
if (isMissingFont(error))
|
|
143
|
+
return new StylePreviewError("font-missing", "The style preview couldn't render because the chosen caption font is no longer available; it may have been uninstalled or deleted. Choose another font under Subtitles, then press Render again.");
|
|
144
|
+
if (message.startsWith("Slopify could not start ffmpeg") || /\bENOENT\b/.test(message))
|
|
145
|
+
return new StylePreviewError("ffmpeg-missing", "The style preview couldn't render because ffmpeg wasn't found. Install ffmpeg, or use the Docker image, then press Render again.");
|
|
146
|
+
log.write("error", "style-preview.render", { detail: message });
|
|
147
|
+
return new StylePreviewError("render-failed", "The style preview couldn't render because ffmpeg stopped with an error. Press Render again; if it fails again, use Download diagnostics in Settings and report it.");
|
|
148
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { formats } from "../../kernel/pipeline.js";
|
|
3
|
+
import { videoEditSchema } from "../admission/schema.js";
|
|
4
|
+
import { subtitleModes, subtitlePositions } from "../subtitles/model.js";
|
|
5
|
+
// "See it before you make it": a few seconds rendered by the real renderer with a project's
|
|
6
|
+
// caption and Look settings, on the bundled sample's images and narration. Browser-safe, so
|
|
7
|
+
// Play and Edit project read the same request and reply shapes the server checks.
|
|
8
|
+
// How long the preview runs: three images of two seconds, so a transition and a chapter
|
|
9
|
+
// card both show, under six seconds of the sample narration.
|
|
10
|
+
export const stylePreviewSeconds = 6;
|
|
11
|
+
export const stylePreviewTextMax = 200;
|
|
12
|
+
// The sample text Play starts with. It, or no text, captions the sample narration with its own
|
|
13
|
+
// words; any other text is spread over the stretches the narration speaks.
|
|
14
|
+
export const defaultPreviewText = "Every story begins with a word.";
|
|
15
|
+
export const shortTitlePreviewMax = 100;
|
|
16
|
+
// A picture the preview is drawn on instead of the sample stills: the establishing image a
|
|
17
|
+
// draft uploaded (its staged file), one a project already has (its output), or a channel cast
|
|
18
|
+
// member's picture (by content hash). One the server can no longer find falls back to the
|
|
19
|
+
// stills rather than failing the preview.
|
|
20
|
+
export const stylePreviewImageSchema = z.discriminatedUnion("kind", [
|
|
21
|
+
z.object({ kind: z.literal("upload"), stagedFileId: z.string().regex(/^[A-Za-z0-9_-]{1,64}$/) }),
|
|
22
|
+
z.object({ kind: z.literal("output"), outputId: z.string().regex(/^[A-Za-z0-9_-]{1,64}$/) }),
|
|
23
|
+
z.object({ kind: z.literal("picture"), sha256: z.string().regex(/^[a-f0-9]{64}$/) }),
|
|
24
|
+
]);
|
|
25
|
+
export const stylePreviewRequestSchema = z.object({
|
|
26
|
+
format: z.enum(formats),
|
|
27
|
+
subtitles: z.object({
|
|
28
|
+
mode: z.enum(subtitleModes),
|
|
29
|
+
fontId: z
|
|
30
|
+
.string()
|
|
31
|
+
.max(160)
|
|
32
|
+
.regex(/^[A-Za-z0-9_-]+$/),
|
|
33
|
+
fontSize: z.number().finite().min(16).max(120),
|
|
34
|
+
position: z.enum(subtitlePositions),
|
|
35
|
+
}),
|
|
36
|
+
videoEdit: videoEditSchema.optional(),
|
|
37
|
+
previewText: z.string().max(stylePreviewTextMax).optional(),
|
|
38
|
+
image: stylePreviewImageSchema.optional(),
|
|
39
|
+
// The Shorts layout instead of the video's: 9:16 through the Shorts renderer, with its
|
|
40
|
+
// big word-by-word captions in the caption font, the title as a headline when it is on,
|
|
41
|
+
// and the clips' speed. The caption size, position and the Look don't apply to a short.
|
|
42
|
+
shorts: z
|
|
43
|
+
.object({
|
|
44
|
+
titleOnScreen: z.boolean(),
|
|
45
|
+
speed: z.number().finite().min(1).max(1.25).optional(),
|
|
46
|
+
// The headline; absent or blank shows the sample's title.
|
|
47
|
+
title: z.string().max(shortTitlePreviewMax).optional(),
|
|
48
|
+
})
|
|
49
|
+
.optional(),
|
|
50
|
+
// Render again even when this exact preview is already saved.
|
|
51
|
+
force: z.boolean().optional(),
|
|
52
|
+
});
|
|
53
|
+
export const stylePreviewReplySchema = z.object({
|
|
54
|
+
hash: z.string().regex(/^[a-f0-9]{64}$/),
|
|
55
|
+
// GET it for the mp4; its query changes whenever the file is rendered again.
|
|
56
|
+
url: z.string().min(1),
|
|
57
|
+
cached: z.boolean(),
|
|
58
|
+
seconds: z.number().positive(),
|
|
59
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdirSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { normalizeStylePreview, previewSeconds, stylePreviewHash } from "./settings.js";
|
|
5
|
+
// Rendered previews are kept in the data folder by the hash of their settings, so a setting
|
|
6
|
+
// changed back, or the same style in another project, plays at once. `force` renders again
|
|
7
|
+
// anyway. Requests for a preview already being rendered wait for that render instead of
|
|
8
|
+
// starting a second one.
|
|
9
|
+
export function stylePreviewDir(dataDir) {
|
|
10
|
+
return join(dataDir, "cache", "style-preview");
|
|
11
|
+
}
|
|
12
|
+
const hashPattern = /^[a-f0-9]{64}$/;
|
|
13
|
+
export function createStylePreviews(deps) {
|
|
14
|
+
const inflight = new Map();
|
|
15
|
+
const pathOf = (hash) => join(deps.dir, `${hash}.mp4`);
|
|
16
|
+
const file = (hash) => {
|
|
17
|
+
if (!hashPattern.test(hash))
|
|
18
|
+
return undefined;
|
|
19
|
+
try {
|
|
20
|
+
const stats = statSync(pathOf(hash));
|
|
21
|
+
return stats.isFile() && stats.size > 0
|
|
22
|
+
? { path: pathOf(hash), bytes: stats.size, version: Math.round(stats.mtimeMs) }
|
|
23
|
+
: undefined;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
const run = async (hash, settings, picture) => {
|
|
30
|
+
mkdirSync(deps.dir, { recursive: true, mode: 0o700 });
|
|
31
|
+
const work = `${hash}.${randomUUID()}`;
|
|
32
|
+
const part = join(deps.dir, `${work}.part.mp4`);
|
|
33
|
+
// The picture is copied beside the render for its length only, so nothing but the
|
|
34
|
+
// finished previews is kept.
|
|
35
|
+
const still = picture === undefined ? undefined : join(deps.dir, `${work}.still${picture.extension}`);
|
|
36
|
+
try {
|
|
37
|
+
if (still !== undefined && picture !== undefined)
|
|
38
|
+
writeFileSync(still, picture.bytes, { mode: 0o600 });
|
|
39
|
+
await deps.render(settings, part, AbortSignal.timeout(deps.timeoutMs ?? 120_000), still);
|
|
40
|
+
renameSync(part, pathOf(hash));
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
rmSync(part, { force: true });
|
|
44
|
+
if (still !== undefined)
|
|
45
|
+
rmSync(still, { force: true });
|
|
46
|
+
}
|
|
47
|
+
prune(deps);
|
|
48
|
+
const saved = file(hash);
|
|
49
|
+
if (saved === undefined)
|
|
50
|
+
throw new Error("The style preview render finished without writing its video.");
|
|
51
|
+
return { hash, cached: false, seconds: previewSeconds(settings), version: saved.version };
|
|
52
|
+
};
|
|
53
|
+
return {
|
|
54
|
+
file,
|
|
55
|
+
render: (request) => {
|
|
56
|
+
const picture = request.image === undefined ? undefined : deps.pictures?.(request.image);
|
|
57
|
+
const settings = normalizeStylePreview(request, picture?.sha256);
|
|
58
|
+
const hash = stylePreviewHash(settings);
|
|
59
|
+
const running = inflight.get(hash);
|
|
60
|
+
if (running !== undefined)
|
|
61
|
+
return running;
|
|
62
|
+
const saved = request.force === true ? undefined : file(hash);
|
|
63
|
+
if (saved !== undefined)
|
|
64
|
+
return Promise.resolve({
|
|
65
|
+
hash,
|
|
66
|
+
cached: true,
|
|
67
|
+
seconds: previewSeconds(settings),
|
|
68
|
+
version: saved.version,
|
|
69
|
+
});
|
|
70
|
+
const started = run(hash, settings, picture).finally(() => {
|
|
71
|
+
inflight.delete(hash);
|
|
72
|
+
});
|
|
73
|
+
inflight.set(hash, started);
|
|
74
|
+
return started;
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function prune(deps) {
|
|
79
|
+
try {
|
|
80
|
+
const kept = readdirSync(deps.dir)
|
|
81
|
+
.filter((name) => /^[a-f0-9]{64}\.mp4$/.test(name))
|
|
82
|
+
.map((name) => ({ name, at: statSync(join(deps.dir, name)).mtimeMs }))
|
|
83
|
+
.toSorted((left, right) => right.at - left.at);
|
|
84
|
+
for (const old of kept.slice(deps.keep ?? 200))
|
|
85
|
+
rmSync(join(deps.dir, old.name), { force: true });
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
deps.log.write("warn", "style-preview.prune", {
|
|
89
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { captionCues } from "../subtitles/captions.js";
|
|
3
|
+
import { legacyVideoEdit, transitionSecondsMax, transitionSecondsMin, videoEditOf, } from "../video/edit-settings.js";
|
|
4
|
+
import { sampleNarrationText, sampleNarrationWords, sampleShortTitle } from "./narration.js";
|
|
5
|
+
import { defaultPreviewText, stylePreviewSeconds } from "./schema.js";
|
|
6
|
+
export function normalizeStylePreview(request,
|
|
7
|
+
// The sha256 of the picture the server found for request.image, if it found one.
|
|
8
|
+
image) {
|
|
9
|
+
const edit = videoEditOf(request);
|
|
10
|
+
const { subtitles } = request;
|
|
11
|
+
const typed = (request.previewText ?? "").replace(/\s+/g, " ").trim();
|
|
12
|
+
const text = typed === "" || typed === defaultPreviewText ? sampleNarrationText : typed;
|
|
13
|
+
const shorts = request.shorts;
|
|
14
|
+
if (shorts !== undefined) {
|
|
15
|
+
const title = shorts.title?.replace(/\s+/g, " ").trim() ?? "";
|
|
16
|
+
const none = videoEditOf({ videoEdit: legacyVideoEdit });
|
|
17
|
+
return {
|
|
18
|
+
version: 2,
|
|
19
|
+
format: "9:16",
|
|
20
|
+
captions: null,
|
|
21
|
+
look: {
|
|
22
|
+
vignette: none.vignette,
|
|
23
|
+
grain: none.grain,
|
|
24
|
+
grade: none.grade,
|
|
25
|
+
atmosphere: none.atmosphere,
|
|
26
|
+
},
|
|
27
|
+
transition: null,
|
|
28
|
+
chapterCard: null,
|
|
29
|
+
short: {
|
|
30
|
+
fontId: subtitles.fontId,
|
|
31
|
+
text,
|
|
32
|
+
title: shorts.titleOnScreen ? (title === "" ? sampleShortTitle : title) : null,
|
|
33
|
+
speed: Math.round(Math.min(1.25, Math.max(1, shorts.speed ?? 1)) * 100) / 100,
|
|
34
|
+
},
|
|
35
|
+
...(image === undefined ? {} : { image }),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
version: 2,
|
|
40
|
+
format: request.format,
|
|
41
|
+
captions: subtitles.mode === "burn-in"
|
|
42
|
+
? {
|
|
43
|
+
fontId: subtitles.fontId,
|
|
44
|
+
fontSize: Math.round(subtitles.fontSize),
|
|
45
|
+
position: subtitles.position,
|
|
46
|
+
text,
|
|
47
|
+
}
|
|
48
|
+
: null,
|
|
49
|
+
look: {
|
|
50
|
+
vignette: edit.vignette,
|
|
51
|
+
grain: edit.grain,
|
|
52
|
+
grade: edit.grade,
|
|
53
|
+
atmosphere: edit.atmosphere,
|
|
54
|
+
},
|
|
55
|
+
transition: edit.transition === "cut"
|
|
56
|
+
? null
|
|
57
|
+
: {
|
|
58
|
+
kind: edit.transition,
|
|
59
|
+
// Tenths, within the range the project editor offers.
|
|
60
|
+
seconds: Math.round(Math.min(transitionSecondsMax, Math.max(transitionSecondsMin, Number.isFinite(edit.transitionSeconds) ? edit.transitionSeconds : 0.6)) * 10) / 10,
|
|
61
|
+
},
|
|
62
|
+
chapterCard: edit.chapterCards ? { fontId: subtitles.fontId } : null,
|
|
63
|
+
...(image === undefined ? {} : { image }),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
// JSON with every object's keys in order, so equal settings always hash alike.
|
|
67
|
+
export function stableJson(value) {
|
|
68
|
+
if (Array.isArray(value))
|
|
69
|
+
return `[${value.map(stableJson).join(",")}]`;
|
|
70
|
+
if (value !== null && typeof value === "object")
|
|
71
|
+
return `{${Object.entries(value)
|
|
72
|
+
.filter(([, one]) => one !== undefined)
|
|
73
|
+
.toSorted(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
|
|
74
|
+
.map(([key, one]) => `${JSON.stringify(key)}:${stableJson(one)}`)
|
|
75
|
+
.join(",")}}`;
|
|
76
|
+
return JSON.stringify(value);
|
|
77
|
+
}
|
|
78
|
+
export function stylePreviewHash(settings) {
|
|
79
|
+
return createHash("sha256").update(stableJson(settings)).digest("hex");
|
|
80
|
+
}
|
|
81
|
+
// How long the rendered preview runs: a Shorts preview at a higher speed is shorter.
|
|
82
|
+
export function previewSeconds(settings) {
|
|
83
|
+
return settings.short === undefined
|
|
84
|
+
? stylePreviewSeconds
|
|
85
|
+
: round(stylePreviewSeconds / settings.short.speed);
|
|
86
|
+
}
|
|
87
|
+
// ceiling: a pause this long between two words of the sample narration starts a new phrase,
|
|
88
|
+
// the same pause `captionCues` starts a new cue at.
|
|
89
|
+
const phraseGap = 0.7;
|
|
90
|
+
// The caption text as timed words. The sample narration's own text is its word timing as it
|
|
91
|
+
// was aligned; any other text is spread over the stretches the narration speaks, each phrase
|
|
92
|
+
// taking a share of the words by its length, the words of each evenly spaced. `captionCues`
|
|
93
|
+
// and the Shorts captions then group and wrap them exactly as they do real narration.
|
|
94
|
+
export function previewWords(text, narration = sampleNarrationWords) {
|
|
95
|
+
const words = text.split(/\s+/).filter((word) => word !== "");
|
|
96
|
+
if (words.length === 0)
|
|
97
|
+
return [];
|
|
98
|
+
if (words.join(" ") === narration.map((word) => word.text).join(" "))
|
|
99
|
+
return narration;
|
|
100
|
+
const phrases = [];
|
|
101
|
+
for (const word of narration) {
|
|
102
|
+
const last = phrases.at(-1);
|
|
103
|
+
if (last !== undefined && word.start - last.end < phraseGap)
|
|
104
|
+
last.end = word.end;
|
|
105
|
+
else
|
|
106
|
+
phrases.push({ start: word.start, end: word.end });
|
|
107
|
+
}
|
|
108
|
+
if (phrases.length === 0)
|
|
109
|
+
phrases.push({ start: 0.4, end: stylePreviewSeconds - 0.4 });
|
|
110
|
+
const spoken = phrases.reduce((sum, phrase) => sum + (phrase.end - phrase.start), 0);
|
|
111
|
+
const timed = [];
|
|
112
|
+
let used = 0;
|
|
113
|
+
let before = 0;
|
|
114
|
+
for (const [at, phrase] of phrases.entries()) {
|
|
115
|
+
before += phrase.end - phrase.start;
|
|
116
|
+
// The words up to the end of this phrase, by its share of the speech; the last takes
|
|
117
|
+
// what is left, and none is left empty while words remain.
|
|
118
|
+
const upTo = at === phrases.length - 1
|
|
119
|
+
? words.length
|
|
120
|
+
: Math.min(words.length - (phrases.length - 1 - at), Math.max(used + 1, Math.round((words.length * before) / spoken)));
|
|
121
|
+
const chunk = words.slice(used, Math.max(used, upTo));
|
|
122
|
+
used += chunk.length;
|
|
123
|
+
const each = (phrase.end - phrase.start) / Math.max(1, chunk.length);
|
|
124
|
+
chunk.forEach((word, index) => {
|
|
125
|
+
timed.push({
|
|
126
|
+
text: word,
|
|
127
|
+
start: round(phrase.start + index * each),
|
|
128
|
+
end: round(phrase.start + (index + 1) * each),
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return timed;
|
|
133
|
+
}
|
|
134
|
+
export function previewCues(text) {
|
|
135
|
+
return captionCues(previewWords(text));
|
|
136
|
+
}
|
|
137
|
+
function round(value) {
|
|
138
|
+
return Math.round(value * 1000) / 1000;
|
|
139
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { assInlineColour } from "../voices/palette.js";
|
|
1
2
|
import { subtitlePlacement } from "./layout.js";
|
|
2
|
-
// Keep spoken spelling and punctuation. Word times come only from acoustic alignment.
|
|
3
|
+
// Keep spoken spelling and punctuation. Word times come only from acoustic alignment. A cue
|
|
4
|
+
// never holds two speakers' words.
|
|
3
5
|
export function captionCues(words) {
|
|
4
6
|
const cues = [];
|
|
5
7
|
let pending = [];
|
|
@@ -12,6 +14,7 @@ export function captionCues(words) {
|
|
|
12
14
|
start: first.start,
|
|
13
15
|
end: last.end,
|
|
14
16
|
text: wrap(pending.map((word) => word.text)),
|
|
17
|
+
...(first.speaker === undefined ? {} : { speaker: first.speaker }),
|
|
15
18
|
});
|
|
16
19
|
}
|
|
17
20
|
pending = [];
|
|
@@ -21,11 +24,12 @@ export function captionCues(words) {
|
|
|
21
24
|
!Number.isFinite(word.end) ||
|
|
22
25
|
word.start < previousEnd - 0.001 ||
|
|
23
26
|
word.end <= word.start) {
|
|
24
|
-
throw new Error("The saved caption timing is damaged. Use
|
|
27
|
+
throw new Error("The saved caption timing is damaged. Use More → Render the video again in the Video section to time them again, then Try again.");
|
|
25
28
|
}
|
|
26
29
|
const first = pending[0];
|
|
27
30
|
if (first !== undefined &&
|
|
28
31
|
(word.start - previousEnd > 0.7 ||
|
|
32
|
+
word.speaker !== first.speaker ||
|
|
29
33
|
word.end - first.start > 5 ||
|
|
30
34
|
wrap([...pending.map((one) => one.text), word.text]).split("\n").length > 2)) {
|
|
31
35
|
flush();
|
|
@@ -52,23 +56,44 @@ function wrap(words) {
|
|
|
52
56
|
lines.push(line);
|
|
53
57
|
return lines.join("\n");
|
|
54
58
|
}
|
|
55
|
-
export function serializeSrt(cues) {
|
|
59
|
+
export function serializeSrt(cues, speakers) {
|
|
56
60
|
return cues
|
|
57
|
-
.map((cue, index) => `${index + 1}\n${stamp(cue.start, ",")} --> ${stamp(cue.end, ",")}\n${markup(cue.text)}\n`)
|
|
61
|
+
.map((cue, index) => `${index + 1}\n${stamp(cue.start, ",")} --> ${stamp(cue.end, ",")}\n${markup(`${nameTag(cue, speakers)}${cue.text}`)}\n`)
|
|
58
62
|
.join("\n");
|
|
59
63
|
}
|
|
60
|
-
|
|
61
|
-
|
|
64
|
+
// WebVTT names the speaker in its own voice span, which players can show or style.
|
|
65
|
+
export function serializeVtt(cues, speakers) {
|
|
66
|
+
return `WEBVTT\n\n${cues.map((cue) => `${stamp(cue.start, ".")} --> ${stamp(cue.end, ".")}\n${voiceSpan(cue, speakers)}${markup(cue.text)}\n`).join("\n")}`;
|
|
67
|
+
}
|
|
68
|
+
function styleOf(cue, speakers) {
|
|
69
|
+
return cue.speaker === undefined ? undefined : speakers?.styles[cue.speaker];
|
|
70
|
+
}
|
|
71
|
+
function nameTag(cue, speakers) {
|
|
72
|
+
const style = styleOf(cue, speakers);
|
|
73
|
+
return style !== undefined && speakers?.nameTags === true ? `${style.name}: ` : "";
|
|
74
|
+
}
|
|
75
|
+
function voiceSpan(cue, speakers) {
|
|
76
|
+
const style = styleOf(cue, speakers);
|
|
77
|
+
return style === undefined ? "" : `<v ${style.name.replace(/[<>&]/g, "")}>`;
|
|
78
|
+
}
|
|
79
|
+
// The speaker's colour, and their name in bold ahead of the words when name tags are on.
|
|
80
|
+
function speakerAss(cue, speakers) {
|
|
81
|
+
const style = styleOf(cue, speakers);
|
|
82
|
+
if (style === undefined)
|
|
83
|
+
return "";
|
|
84
|
+
const colour = `{\\1c${assInlineColour(style.colour)}}`;
|
|
85
|
+
return speakers?.nameTags === true ? `${colour}{\\b1}${assText(style.name)}:{\\b0} ` : colour;
|
|
62
86
|
}
|
|
63
87
|
export function serializeAss(cues, style) {
|
|
64
88
|
const name = style.fontName.replace(/[\p{Cc},]/gu, " ").trim();
|
|
65
89
|
const placement = subtitlePlacement(style.position ?? "bottom", style.height);
|
|
66
90
|
const position = `{\\an${placement.alignment}\\pos(${style.width / 2},${placement.y})}`;
|
|
67
|
-
const header = `[Script Info]\nScriptType: v4.00+\nPlayResX: ${style.width}\nPlayResY: ${style.height}\nWrapStyle: 0\nScaledBorderAndShadow: yes\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Default,${name},${style.fontSize}
|
|
91
|
+
const header = `[Script Info]\nScriptType: v4.00+\nPlayResX: ${style.width}\nPlayResY: ${style.height}\nWrapStyle: 0\nScaledBorderAndShadow: yes\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Default,${name},${style.fontSize},${assColour(style.color, "&H00FFFFFF")},${assColour(style.color, "&H00FFFFFF")},${assColour(style.outlineColor, "&H00101010")},&H80000000,0,0,0,0,100,100,0,0,1,2.5,1,2,60,60,60,1\n\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n`;
|
|
68
92
|
return (header +
|
|
69
93
|
cues
|
|
70
|
-
.map((cue) => `Dialogue: 0,${assStamp(cue.start)},${assStamp(cue.end)},Default,,0,0,0,,${position}${assText(cue.text)}\n`)
|
|
71
|
-
.join("")
|
|
94
|
+
.map((cue) => `Dialogue: 0,${assStamp(cue.start)},${assStamp(cue.end)},Default,,0,0,0,,${position}${speakerAss(cue, style.speakers)}${assText(cue.text)}\n`)
|
|
95
|
+
.join("") +
|
|
96
|
+
(style.overlay ?? []).map((line) => `${line}\n`).join(""));
|
|
72
97
|
}
|
|
73
98
|
function markup(text) {
|
|
74
99
|
return text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
@@ -89,3 +114,11 @@ function assStamp(seconds) {
|
|
|
89
114
|
const cs = Math.max(0, Math.round(seconds * 100));
|
|
90
115
|
return `${Math.floor(cs / 360_000)}:${String(Math.floor(cs / 6000) % 60).padStart(2, "0")}:${String(Math.floor(cs / 100) % 60).padStart(2, "0")}.${String(cs % 100).padStart(2, "0")}`;
|
|
91
116
|
}
|
|
117
|
+
// ASS colours are &HAABBGGRR, alpha 00 being opaque.
|
|
118
|
+
export function assColour(hex, fallback) {
|
|
119
|
+
const match = hex === undefined ? null : /^#([0-9A-Fa-f]{2})([0-9A-Fa-f]{2})([0-9A-Fa-f]{2})$/.exec(hex);
|
|
120
|
+
if (match === null)
|
|
121
|
+
return fallback;
|
|
122
|
+
const [, r, g, b] = match;
|
|
123
|
+
return `&H00${(b ?? "").toUpperCase()}${(g ?? "").toUpperCase()}${(r ?? "").toUpperCase()}`;
|
|
124
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { languageInfo } from "../../kernel/ports/languages.js";
|
|
2
3
|
export const subtitleModes = ["off", "files", "burn-in"];
|
|
3
4
|
export const subtitlePositions = [
|
|
4
5
|
"top",
|
|
@@ -17,6 +18,16 @@ export const subtitleConfigSchema = z.object({
|
|
|
17
18
|
.default("default"),
|
|
18
19
|
position: z.enum(subtitlePositions).default("bottom"),
|
|
19
20
|
fontSize: z.number().int().min(16).max(120).default(48),
|
|
21
|
+
// #RRGGBB, from the channel's brand kit. Absent is white text with a dark outline, as
|
|
22
|
+
// every caption before them.
|
|
23
|
+
color: z
|
|
24
|
+
.string()
|
|
25
|
+
.regex(/^#[0-9A-Fa-f]{6}$/)
|
|
26
|
+
.optional(),
|
|
27
|
+
outlineColor: z
|
|
28
|
+
.string()
|
|
29
|
+
.regex(/^#[0-9A-Fa-f]{6}$/)
|
|
30
|
+
.optional(),
|
|
20
31
|
});
|
|
21
32
|
export const defaultSubtitles = {
|
|
22
33
|
mode: "off",
|
|
@@ -25,3 +36,14 @@ export const defaultSubtitles = {
|
|
|
25
36
|
fontSize: 48,
|
|
26
37
|
position: "bottom",
|
|
27
38
|
};
|
|
39
|
+
// The word-timing operation of a project language, which is part of the timing's fingerprint:
|
|
40
|
+
// English keeps the operation it always had, so no English project is re-timed; the other
|
|
41
|
+
// languages each name the model (or the sentence fallback) that times them.
|
|
42
|
+
export const timingOperations = {
|
|
43
|
+
english: "wav2vec2-en-a19f851-v2-omissions",
|
|
44
|
+
multilingual: "wav2vec2-xlsr56-2d48b01-v1",
|
|
45
|
+
sentences: "sentence-timing-v1",
|
|
46
|
+
};
|
|
47
|
+
export function timingOperation(language) {
|
|
48
|
+
return timingOperations[languageInfo(language).timing];
|
|
49
|
+
}
|