@gentbajko/slopify 2.5.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -21
- package/NOTICE +12 -0
- package/README.md +21 -40
- package/SUBTITLES.md +1 -1
- package/dist/adapter-registry.js +3 -0
- package/dist/adapters/alignment/cache.js +4 -1
- package/dist/adapters/alignment/ctc.js +25 -23
- package/dist/adapters/alignment/index.js +37 -6
- package/dist/adapters/alignment/multilingual.js +267 -0
- package/dist/adapters/alignment/protocol.js +11 -0
- package/dist/adapters/alignment/quality.js +4 -3
- package/dist/adapters/alignment/sentences.js +95 -0
- package/dist/adapters/alignment/spec.js +24 -0
- package/dist/adapters/alignment/spell.js +769 -0
- package/dist/adapters/alignment/text.js +56 -9
- package/dist/adapters/alignment/window.js +34 -30
- package/dist/adapters/alignment/worker.js +23 -21
- package/dist/adapters/explain.js +11 -11
- package/dist/adapters/host-cli/index.js +44 -5
- package/dist/adapters/host-cli/transport.js +7 -5
- package/dist/adapters/image/bytes.js +2 -2
- package/dist/adapters/image/codex-output.js +1 -1
- package/dist/adapters/image/codex.js +87 -22
- package/dist/adapters/image/fal.js +13 -5
- package/dist/adapters/image/google.js +11 -8
- package/dist/adapters/image/openai.js +14 -7
- package/dist/adapters/image/reference.js +34 -0
- package/dist/adapters/image/replicate.js +7 -2
- package/dist/adapters/image/video.js +2 -2
- package/dist/adapters/key-probes.js +36 -0
- package/dist/adapters/llm/claude-code.js +168 -32
- package/dist/adapters/llm/cli-login-error.js +1 -1
- package/dist/adapters/llm/codex-limits.js +157 -0
- package/dist/adapters/llm/codex.js +99 -12
- package/dist/adapters/llm/document-workspace.js +2 -2
- package/dist/adapters/llm/gemini-workspace.js +1 -1
- package/dist/adapters/llm/gemini.js +16 -3
- package/dist/adapters/llm/image-workspace.js +58 -0
- package/dist/adapters/llm/openrouter.js +11 -3
- package/dist/adapters/llm/run-cli.js +12 -7
- package/dist/adapters/tts/cartesia.js +10 -0
- package/dist/adapters/tts/elevenlabs.js +31 -9
- package/dist/adapters/tts/inworld-async.js +9 -5
- package/dist/adapters/tts/inworld.js +17 -3
- package/dist/adapters/tts/openai.js +3 -0
- package/dist/adapters/tts/voice-languages.js +80 -0
- package/dist/assets/fonts/noto/NotoSans-Regular.ttf +0 -0
- package/dist/assets/fonts/noto/NotoSansArabic-Regular.ttf +0 -0
- package/dist/assets/fonts/noto/NotoSansDevanagari-Regular.ttf +0 -0
- package/dist/assets/fonts/noto/NotoSansHebrew-Regular.ttf +0 -0
- package/dist/assets/fonts/noto/NotoSansThai-Regular.ttf +0 -0
- package/dist/assets/fonts/noto/OFL.txt +93 -0
- package/dist/assets/fonts/noto/SOURCE.txt +18 -0
- package/dist/assets/sample/sample-audiobook.tar +0 -0
- package/dist/assets/sample/sample-podcast.tar +0 -0
- package/dist/assets/sample/sample-project.tar +0 -0
- package/dist/catalog/merge.js +132 -0
- package/dist/catalog/registry.js +13 -13
- package/dist/catalog/schema.js +2 -0
- package/dist/catalog/store.js +147 -8
- package/dist/compose.yaml +71 -0
- package/dist/edge/autostart/docker-record.js +32 -0
- package/dist/edge/autostart/docker.js +50 -0
- package/dist/edge/autostart/index.js +78 -0
- package/dist/edge/autostart/launcher.js +176 -0
- package/dist/edge/autostart/model.js +3 -0
- package/dist/edge/autostart/native.js +210 -0
- package/dist/edge/autostart/prompt.js +66 -0
- package/dist/edge/autostart/service.js +81 -0
- package/dist/edge/cli.js +57 -51
- package/dist/edge/docker-install/activation.js +56 -0
- package/dist/edge/docker-install/apply.js +458 -0
- package/dist/edge/docker-install/engine.js +362 -0
- package/dist/edge/docker-install/run.js +154 -0
- package/dist/edge/docker-install/state.js +146 -0
- package/dist/edge/{docker-projects → docker-install}/volume.js +1 -5
- package/dist/edge/docker.js +14 -22
- package/dist/edge/host-cli.js +1 -1
- package/dist/edge/http/actions.js +32 -10
- package/dist/edge/http/app.js +58 -2
- package/dist/edge/http/auditions.js +70 -0
- package/dist/edge/http/autostart.js +37 -0
- package/dist/edge/http/channel-memory.js +92 -0
- package/dist/edge/http/channels.js +128 -0
- package/dist/edge/http/entries.js +22 -3
- package/dist/edge/http/files.js +1 -1
- package/dist/edge/http/folder-location.js +1 -1
- package/dist/edge/http/home.js +39 -0
- package/dist/edge/http/host-cli.js +29 -5
- package/dist/edge/http/narration-peaks.js +76 -0
- package/dist/edge/http/onboarding.js +152 -0
- package/dist/edge/http/open-folder.js +1 -1
- package/dist/edge/http/patch-notes.js +45 -0
- package/dist/edge/http/planning.js +4 -2
- package/dist/edge/http/project-create.js +87 -0
- package/dist/edge/http/projects.js +31 -99
- package/dist/edge/http/prompts.js +40 -3
- package/dist/edge/http/pronunciations.js +23 -4
- package/dist/edge/http/providers.js +86 -0
- package/dist/edge/http/reviews.js +121 -0
- package/dist/edge/http/revision-files.js +1 -1
- package/dist/edge/http/revisions.js +1 -1
- package/dist/edge/http/run-cost.js +25 -0
- package/dist/edge/http/schedules.js +111 -6
- package/dist/edge/http/settings.js +84 -6
- package/dist/edge/http/storage-files.js +50 -0
- package/dist/edge/http/storage.js +29 -2
- package/dist/edge/http/studio.js +207 -0
- package/dist/edge/http/style-preview.js +100 -0
- package/dist/edge/http/subtitles.js +1 -1
- package/dist/edge/http/trash.js +69 -0
- package/dist/edge/http/update.js +24 -3
- package/dist/edge/http/whats-new.js +15 -0
- package/dist/edge/http/youtube-edits.js +83 -0
- package/dist/edge/native-update.js +62 -0
- package/dist/host-cli/open-folder.js +10 -10
- package/dist/host-cli/runtime.js +9 -1
- package/dist/kernel/db/migrations/0025-automatic-reviews.sql +40 -0
- package/dist/kernel/db/migrations/0026-schedule-topic-generation.sql +23 -0
- package/dist/kernel/db/migrations/0027-run-cost.sql +58 -0
- package/dist/kernel/db/migrations/0030-automatic-retries.sql +18 -0
- package/dist/kernel/db/migrations/0031-prompt-history-and-description-edits.sql +33 -0
- package/dist/kernel/db/migrations/0032-channels.sql +65 -0
- package/dist/kernel/db/migrations/0034-script-prompts.sql +23 -0
- package/dist/kernel/db/migrations/0035-narration-aliases.sql +13 -0
- package/dist/kernel/db/migrations/0037-project-uploads.sql +8 -0
- package/dist/kernel/db/migrations/0038-voice-languages.sql +7 -0
- package/dist/kernel/db/migrations/0039-channel-essentials.sql +64 -0
- package/dist/kernel/db/migrations/0040-standalone-usage.sql +35 -0
- package/dist/kernel/paths.js +27 -6
- package/dist/kernel/pipeline.js +4 -1
- package/dist/kernel/ports/host-cli.js +122 -14
- package/dist/kernel/ports/key-probe.js +1 -0
- package/dist/kernel/ports/languages.js +157 -0
- package/dist/kernel/ports/model.js +5 -2
- package/dist/kernel/ports/narration-aliases.js +60 -0
- package/dist/kernel/ports/plan-limits.js +36 -0
- package/dist/kernel/runner/attempt.js +17 -8
- package/dist/kernel/runner/audition.js +36 -0
- package/dist/kernel/runner/graph.js +24 -5
- package/dist/kernel/runner/index.js +35 -5
- package/dist/kernel/runner/meter.js +1 -0
- package/dist/kernel/runner/providers.js +142 -7
- package/dist/kernel/runner/retry-policy.js +43 -0
- package/dist/kernel/runner/standalone.js +134 -0
- package/dist/kernel/runner/work-authority.js +28 -2
- package/dist/main.js +272 -16
- package/dist/patch-notes/0.4.2-to-1.0.md +93 -0
- package/dist/patch-notes/1.0-to-2.0.md +74 -0
- package/dist/patch-notes/2.0-to-3.0.md +257 -0
- package/dist/patch-notes/3.0.0.md +70 -0
- package/dist/patch-notes/index.json +26 -0
- package/dist/slices/admission/model.js +27 -0
- package/dist/slices/admission/repo.js +24 -4
- package/dist/slices/admission/rules.js +79 -1
- package/dist/slices/admission/schema.js +58 -1
- package/dist/slices/admission/short-mode.js +47 -0
- package/dist/slices/admission/start.js +41 -5
- package/dist/slices/article/continuation.js +2 -2
- package/dist/slices/article/provided-entries.js +2 -2
- package/dist/slices/article/run.js +6 -2
- package/dist/slices/article/segments.js +3 -3
- package/dist/slices/backups/folder.js +14 -6
- package/dist/slices/batch/index.js +7 -2
- package/dist/slices/cancel/index.js +1 -1
- package/dist/slices/channels/cast-images.js +110 -0
- package/dist/slices/channels/cast-match.js +32 -0
- package/dist/slices/channels/images.js +19 -0
- package/dist/slices/channels/model.js +5 -0
- package/dist/slices/channels/rebrand.js +100 -0
- package/dist/slices/channels/repo.js +218 -0
- package/dist/slices/channels/runs.js +129 -0
- package/dist/slices/channels/schema.js +129 -0
- package/dist/slices/channels/service.js +180 -0
- package/dist/slices/channels/studio-csv.js +110 -0
- package/dist/slices/channels/videos.js +120 -0
- package/dist/slices/checkpoints/recovery.js +10 -0
- package/dist/slices/control/index.js +2 -1
- package/dist/slices/document/fonts.js +1 -1
- package/dist/slices/document/render.js +1 -1
- package/dist/slices/episodes/related.js +90 -0
- package/dist/slices/episodes/repo.js +60 -0
- package/dist/slices/episodes/service.js +58 -0
- package/dist/slices/episodes/summarize.js +170 -0
- package/dist/slices/estimate/index.js +109 -28
- package/dist/slices/estimate/requests.js +60 -2
- package/dist/slices/fixes/rules.js +73 -0
- package/dist/slices/fonts/coverage.js +82 -0
- package/dist/slices/fonts/sfnt.js +74 -0
- package/dist/slices/images/run.js +8 -5
- package/dist/slices/images/scale.js +84 -0
- package/dist/slices/library/diff.js +122 -0
- package/dist/slices/library/history.js +100 -0
- package/dist/slices/library/model.js +3 -0
- package/dist/slices/library/photorealistic.js +49 -0
- package/dist/slices/library/repo.js +19 -14
- package/dist/slices/library/save.js +62 -12
- package/dist/slices/library/slots.js +7 -1
- package/dist/slices/library/used-by.js +153 -0
- package/dist/slices/loudness/level-pieces.js +41 -0
- package/dist/slices/loudness/loudnorm.js +164 -0
- package/dist/slices/loudness/model.js +153 -0
- package/dist/slices/model-upkeep/model.js +10 -0
- package/dist/slices/model-upkeep/switch.js +217 -0
- package/dist/slices/model-upkeep/usage.js +183 -0
- package/dist/slices/narration/aliases-library.js +31 -0
- package/dist/slices/narration/aliases-schema.js +53 -0
- package/dist/slices/narration/aliases.js +49 -0
- package/dist/slices/narration/blocks.js +352 -0
- package/dist/slices/narration/chunk.js +19 -7
- package/dist/slices/narration/concat.js +2 -2
- package/dist/slices/narration/describe.js +45 -0
- package/dist/slices/narration/pauses-model.js +63 -0
- package/dist/slices/narration/pauses.js +269 -0
- package/dist/slices/narration/peaks-model.js +1 -0
- package/dist/slices/narration/peaks.js +94 -0
- package/dist/slices/narration/preparation.js +21 -3
- package/dist/slices/narration/pronunciation-chunks.js +3 -3
- package/dist/slices/narration/pronunciation.js +72 -25
- package/dist/slices/narration/run.js +6 -6
- package/dist/slices/narration/shared-glossary.js +1 -1
- package/dist/slices/narration/steering.js +15 -4
- package/dist/slices/notifications/notifier.js +21 -3
- package/dist/slices/notifications/rules.js +22 -1
- package/dist/slices/onboarding/copy.js +143 -0
- package/dist/slices/onboarding/first-run.js +46 -0
- package/dist/slices/onboarding/install.js +119 -0
- package/dist/slices/onboarding/model.js +64 -0
- package/dist/slices/onboarding/packs.js +491 -0
- package/dist/slices/onboarding/quick-short.js +129 -0
- package/dist/slices/onboarding/sample.js +132 -0
- package/dist/slices/onboarding/state.js +68 -0
- package/dist/slices/onboarding/template.js +73 -0
- package/dist/slices/patch-notes/library.js +73 -0
- package/dist/slices/patch-notes/seen.js +77 -0
- package/dist/slices/play-drafts/convert.js +125 -6
- package/dist/slices/play-drafts/readiness.js +3 -0
- package/dist/slices/play-drafts/repo.js +2 -0
- package/dist/slices/play-drafts/review-inputs.js +20 -3
- package/dist/slices/play-drafts/schema.js +89 -0
- package/dist/slices/play-drafts/service.js +4 -1
- package/dist/slices/play-drafts/start.js +43 -18
- package/dist/slices/project-templates/from-project.js +50 -4
- package/dist/slices/project-templates/one-off.js +26 -0
- package/dist/slices/project-templates/repo.js +2 -1
- package/dist/slices/project-templates/service.js +15 -4
- package/dist/slices/project-templates/setup.js +18 -2
- package/dist/slices/rebuild/admission-repo.js +1 -1
- package/dist/slices/rebuild/model.js +6 -0
- package/dist/slices/rebuild/preview-details.js +46 -3
- package/dist/slices/rebuild/preview-plan.js +13 -6
- package/dist/slices/rebuild/recipe-audio-parts.js +40 -7
- package/dist/slices/rebuild/recipe-audio.js +66 -12
- package/dist/slices/rebuild/recipe-build.js +8 -4
- package/dist/slices/rebuild/recipe-cast.js +18 -0
- package/dist/slices/rebuild/recipe-checkpoints.js +11 -9
- package/dist/slices/rebuild/recipe-describe.js +93 -0
- package/dist/slices/rebuild/recipe-edit.js +13 -1
- package/dist/slices/rebuild/recipe-exports.js +62 -8
- package/dist/slices/rebuild/recipe-input-schema.js +35 -0
- package/dist/slices/rebuild/recipe-legacy.js +9 -0
- package/dist/slices/rebuild/recipe-loudness.js +45 -0
- package/dist/slices/rebuild/recipe-model.js +25 -6
- package/dist/slices/rebuild/recipe-pauses.js +7 -0
- package/dist/slices/rebuild/recipe-preparation.js +3 -3
- package/dist/slices/rebuild/recipe-provider-choice.js +11 -1
- package/dist/slices/rebuild/recipe-reference.js +15 -1
- package/dist/slices/rebuild/recipe-reviews.js +117 -0
- package/dist/slices/rebuild/recipe-save.js +4 -1
- package/dist/slices/rebuild/recipe-shorts.js +23 -3
- package/dist/slices/rebuild/recipe-text.js +114 -19
- package/dist/slices/rebuild/recipe-validation.js +14 -2
- package/dist/slices/rebuild/recipe-visual.js +96 -33
- package/dist/slices/rebuild/recipe-voices.js +286 -0
- package/dist/slices/rebuild/recipe-work.js +77 -6
- package/dist/slices/rebuild/recipe-youtube.js +2 -0
- package/dist/slices/rebuild/recovery-conflict.js +7 -3
- package/dist/slices/rebuild/recovery-model.js +3 -0
- package/dist/slices/rebuild/recovery-selection.js +28 -2
- package/dist/slices/rebuild/recovery.js +35 -17
- package/dist/slices/rebuild/repo.js +3 -3
- package/dist/slices/rebuild/retry.js +18 -0
- package/dist/slices/rebuild/review-redo.js +110 -0
- package/dist/slices/rebuild/runtime-actions.js +5 -0
- package/dist/slices/rebuild/runtime-admission.js +4 -1
- package/dist/slices/rebuild/runtime-animate.js +4 -4
- package/dist/slices/rebuild/runtime-article.js +5 -5
- package/dist/slices/rebuild/runtime-document.js +4 -4
- package/dist/slices/rebuild/runtime-export-bed.js +43 -0
- package/dist/slices/rebuild/runtime-export-edit.js +70 -11
- package/dist/slices/rebuild/runtime-export-inputs.js +52 -10
- package/dist/slices/rebuild/runtime-export-short.js +85 -0
- package/dist/slices/rebuild/runtime-export.js +84 -19
- package/dist/slices/rebuild/runtime-figure-card.js +87 -0
- package/dist/slices/rebuild/runtime-image.js +21 -2
- package/dist/slices/rebuild/runtime-local.js +148 -24
- package/dist/slices/rebuild/runtime-materialize.js +8 -0
- package/dist/slices/rebuild/runtime-narration-reuse.js +2 -1
- package/dist/slices/rebuild/runtime-narration-text.js +7 -7
- package/dist/slices/rebuild/runtime-pictures.js +28 -0
- package/dist/slices/rebuild/runtime-piece-label.js +27 -0
- package/dist/slices/rebuild/runtime-provider.js +126 -18
- package/dist/slices/rebuild/runtime-publication.js +21 -5
- package/dist/slices/rebuild/runtime-review.js +237 -0
- package/dist/slices/rebuild/runtime-run.js +17 -11
- package/dist/slices/rebuild/runtime-shorts.js +44 -17
- package/dist/slices/rebuild/runtime-store.js +39 -10
- package/dist/slices/rebuild/runtime-subtitles.js +72 -19
- package/dist/slices/rebuild/runtime-voices.js +103 -0
- package/dist/slices/rebuild/runtime-youtube.js +7 -6
- package/dist/slices/rebuild/soften.js +49 -0
- package/dist/slices/rebuild/transition-repo.js +9 -2
- package/dist/slices/research/run.js +4 -4
- package/dist/slices/research/synthesis.js +2 -2
- package/dist/slices/reviews/model.js +105 -0
- package/dist/slices/reviews/outcome.js +22 -0
- package/dist/slices/reviews/repo.js +106 -0
- package/dist/slices/reviews/rules.js +63 -0
- package/dist/slices/reviews/verdict.js +71 -0
- package/dist/slices/reviews/view.js +30 -0
- package/dist/slices/revisions/adopt-content.js +5 -2
- package/dist/slices/revisions/adopt.js +3 -1
- package/dist/slices/revisions/downloads.js +7 -1
- package/dist/slices/revisions/image-plan.js +183 -0
- package/dist/slices/revisions/mutation-assets.js +10 -0
- package/dist/slices/revisions/mutation-cues.js +71 -0
- package/dist/slices/revisions/mutation-request.js +2 -1
- package/dist/slices/revisions/mutations.js +97 -3
- package/dist/slices/revisions/schema.js +11 -1
- package/dist/slices/run-cost/limits.js +128 -0
- package/dist/slices/run-cost/meter.js +47 -0
- package/dist/slices/run-cost/panel.js +234 -0
- package/dist/slices/run-cost/pricing.js +92 -0
- package/dist/slices/run-cost/week.js +108 -0
- package/dist/slices/schedules/agenda.js +158 -0
- package/dist/slices/schedules/repo.js +58 -18
- package/dist/slices/schedules/scheduler.js +67 -16
- package/dist/slices/schedules/schema.js +138 -1
- package/dist/slices/schedules/service.js +43 -4
- package/dist/slices/schedules/similar.js +90 -0
- package/dist/slices/schedules/topic-list.js +163 -0
- package/dist/slices/schedules/topics.js +422 -0
- package/dist/slices/settings/first-run.js +102 -0
- package/dist/slices/settings/health.js +190 -0
- package/dist/slices/settings/key-guides.js +128 -0
- package/dist/slices/settings/key-test.js +85 -0
- package/dist/slices/settings/model.js +6 -1
- package/dist/slices/settings/playback.js +15 -0
- package/dist/slices/settings/repo.js +17 -3
- package/dist/slices/settings/voices.js +64 -4
- package/dist/slices/settings/whats-new.js +64 -0
- package/dist/slices/shorts/captions.js +9 -0
- package/dist/slices/shorts/pick.js +3 -3
- package/dist/slices/shorts/prompts.js +1 -1
- package/dist/slices/shorts/render.js +54 -5
- package/dist/slices/storage/asset-name.js +3 -0
- package/dist/slices/storage/backup-export.js +48 -9
- package/dist/slices/storage/backup-format.js +38 -1
- package/dist/slices/storage/backup-import.js +191 -19
- package/dist/slices/storage/delete-project.js +7 -4
- package/dist/slices/storage/documents.js +79 -0
- package/dist/slices/storage/files-location.js +531 -0
- package/dist/slices/storage/layout.js +14 -4
- package/dist/slices/storage/model.js +11 -0
- package/dist/slices/storage/portable.js +92 -12
- package/dist/slices/storage/reconcile.js +14 -1
- package/dist/slices/storage/repo.js +4 -1
- package/dist/slices/storage/schema.js +11 -0
- package/dist/slices/storage/trim.js +146 -0
- package/dist/slices/studio/disclosure.js +74 -0
- package/dist/slices/studio/model.js +28 -0
- package/dist/slices/studio/pack.js +209 -0
- package/dist/slices/studio/settings.js +133 -0
- package/dist/slices/style-preview/images.js +61 -0
- package/dist/slices/style-preview/render.js +161 -0
- package/dist/slices/style-preview/schema.js +45 -0
- package/dist/slices/style-preview/service.js +93 -0
- package/dist/slices/style-preview/settings.js +89 -0
- package/dist/slices/subtitles/captions.js +42 -9
- package/dist/slices/subtitles/model.js +22 -0
- package/dist/slices/subtitles/prepare.js +27 -16
- package/dist/slices/subtitles/transcript.js +1 -1
- package/dist/slices/thumbnail/run.js +7 -7
- package/dist/slices/trash/model.js +25 -0
- package/dist/slices/trash/service.js +273 -0
- package/dist/slices/uploads/repo.js +20 -0
- package/dist/slices/video/ambient-bed-schema.js +15 -0
- package/dist/slices/video/ambient-bed.js +95 -0
- package/dist/slices/video/ambient-mix.js +72 -0
- package/dist/slices/video/audio-inputs.js +2 -2
- package/dist/slices/video/cards.js +5 -2
- package/dist/slices/video/edit-list.js +23 -2
- package/dist/slices/video/edit-settings.js +13 -3
- package/dist/slices/video/ffmpeg.js +63 -9
- package/dist/slices/video/figure-card.js +492 -0
- package/dist/slices/video/figure-spans.js +51 -0
- package/dist/slices/video/plan.js +125 -13
- package/dist/slices/video/run.js +14 -12
- package/dist/slices/video/slideshow.js +39 -4
- package/dist/slices/voices/attribution.js +69 -0
- package/dist/slices/voices/audio-files.js +82 -0
- package/dist/slices/voices/audition.js +25 -0
- package/dist/slices/voices/cast.js +56 -0
- package/dist/slices/voices/grouping.js +49 -0
- package/dist/slices/voices/join.js +35 -0
- package/dist/slices/voices/languages.js +25 -0
- package/dist/slices/voices/model.js +179 -0
- package/dist/slices/voices/palette.js +26 -0
- package/dist/slices/voices/panel.js +91 -0
- package/dist/slices/voices/portraits.js +55 -0
- package/dist/slices/voices/script.js +136 -0
- package/dist/slices/voices/timing.js +58 -0
- package/dist/slices/youtube/answer.js +1 -1
- package/dist/slices/youtube/chapters.js +126 -0
- package/dist/slices/youtube/edits-repo.js +69 -0
- package/dist/slices/youtube/edits.js +89 -0
- package/dist/slices/youtube/placeholders.js +98 -0
- package/dist/slices/youtube/transcript.js +11 -4
- package/dist/updater/service.js +66 -21
- package/dist/web/assets/index-CUA2sUxU.css +1 -0
- package/dist/web/assets/index-CX590kE-.js +303 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
- package/dist/web/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
- package/dist/web/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
- package/dist/web/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
- package/dist/web/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
- package/dist/web/assets/{pdf-BLF2i8c2.js → pdf-BG8n_uc5.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +4 -4
- package/dist/edge/docker-launch.js +0 -81
- package/dist/edge/docker-projects/activation.js +0 -35
- package/dist/edge/docker-projects/claims.js +0 -85
- package/dist/edge/docker-projects/committed.js +0 -40
- package/dist/edge/docker-projects/engine.js +0 -461
- package/dist/edge/docker-projects/install.js +0 -254
- package/dist/edge/docker-projects/prune.js +0 -66
- package/dist/edge/docker-projects/recover.js +0 -100
- package/dist/edge/docker-projects/state.js +0 -225
- package/dist/web/assets/index-CktpxlJd.js +0 -171
- package/dist/web/assets/index-DB-vOnGU.css +0 -1
- package/scripts/docker-run.sh +0 -3
- /package/dist/edge/{docker-projects → docker-install}/tree.js +0 -0
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import { usesNarrationPreparation, usesPronunciationGlossary } from "../admission/rules.js";
|
|
3
|
+
import { narrationAliasesOf, usesNarrationPreparation, usesPronunciationGlossary, } from "../admission/rules.js";
|
|
4
4
|
import { plainText } from "../article/plain.js";
|
|
5
5
|
import { splitEndMatter } from "../article/split.js";
|
|
6
|
+
import { usesLoudness } from "../loudness/model.js";
|
|
6
7
|
import { outputPath } from "../storage/layout.js";
|
|
7
8
|
import { probeDurationMs } from "../video/ffmpeg.js";
|
|
8
9
|
import { audioTimeline } from "../video/plan.js";
|
|
10
|
+
import { usesVoices } from "../voices/model.js";
|
|
11
|
+
import { levelKey } from "./recipe-loudness.js";
|
|
9
12
|
import { joinedNarration, narrationTextParts } from "./runtime-narration-text.js";
|
|
10
13
|
import { executionPlan, executionView, savedCatalogue } from "./runtime-plan.js";
|
|
11
14
|
export function exportSnapshot(deps, context, piece) {
|
|
@@ -14,19 +17,33 @@ export function exportSnapshot(deps, context, piece) {
|
|
|
14
17
|
.prepare("SELECT recipe_context FROM revision_work WHERE id=?")
|
|
15
18
|
.get(context.work.workId);
|
|
16
19
|
if (view === undefined || row === undefined)
|
|
17
|
-
throw new Error("Slopify hit an internal error (the project version being exported is missing).
|
|
20
|
+
throw new Error("Slopify hit an internal error (the project version being exported is missing). Try again; if it happens again, use Download diagnostics in Settings and report it.");
|
|
18
21
|
const plan = executionPlan(deps, view, savedCatalogue(row.recipe_context));
|
|
19
22
|
const recipe = plan.recipes.find((one) => one.key === piece.key && one.fingerprint === piece.fingerprint);
|
|
20
23
|
if (recipe === undefined || recipe.deferred || recipe.unresolved)
|
|
21
|
-
throw new Error("Slopify hit an internal error (the export no longer matches the project's saved inputs).
|
|
24
|
+
throw new Error("Slopify hit an internal error (the export no longer matches the project's saved inputs). Try again; if it happens again, use Download diagnostics in Settings and report it.");
|
|
22
25
|
return { view, plan };
|
|
23
26
|
}
|
|
24
|
-
export async function revisionAudio(deps, context, view
|
|
27
|
+
export async function revisionAudio(deps, context, view,
|
|
28
|
+
// Level the volume: what plays the sound (the video, a short, the audio files) asks for the
|
|
29
|
+
// levelled joins (`recipe-loudness.ts`); what only times it (the word timing, the description,
|
|
30
|
+
// the shorts' pick) reads the plain ones, which it waits for. An uploaded narration has no
|
|
31
|
+
// levelled join: only the master reaches it.
|
|
32
|
+
options = {}) {
|
|
25
33
|
const config = view.revision.config;
|
|
26
34
|
if (config.sources.audio === "off")
|
|
27
35
|
return [];
|
|
36
|
+
const levelled = options.levelled === true && usesLoudness(config) && config.sources.audio === "generate";
|
|
28
37
|
const input = async (role) => {
|
|
29
|
-
const
|
|
38
|
+
const segment = role === "audio_body" ? "body" : role === "audio_intro" ? "intro" : "outro";
|
|
39
|
+
const row = view.outputs.find((one) => one.selected &&
|
|
40
|
+
one.available &&
|
|
41
|
+
one.state === "ready" &&
|
|
42
|
+
(levelled
|
|
43
|
+
? one.output.role === "audio_levelled" && one.workKey === levelKey(segment)
|
|
44
|
+
: one.output.role === role));
|
|
45
|
+
if (row === undefined && levelled)
|
|
46
|
+
throw new Error(`The levelled ${segment === "body" ? "narration" : segment} isn't finished yet, so the sound can't be exported at an even volume. Let the Narration stage finish (Resume, or Try again on it), then retry this stage; or turn off Level the volume in Edit project → Pauses and volume.`);
|
|
30
47
|
if (row === undefined)
|
|
31
48
|
return undefined;
|
|
32
49
|
const path = outputPath(deps.paths, context.work.projectId, row.output.path);
|
|
@@ -34,12 +51,12 @@ export async function revisionAudio(deps, context, view) {
|
|
|
34
51
|
(await (deps.measureAudio?.(path, context.signal) ??
|
|
35
52
|
probeDurationMs(deps.ffmpeg, path, context.signal, deps.log)));
|
|
36
53
|
if (!Number.isFinite(duration) || duration <= 0)
|
|
37
|
-
throw new Error("Slopify couldn't read the length of the narration audio, so the file may be damaged or empty. Regenerate the narration (or upload your audio file again in Edit project → Narration), then
|
|
54
|
+
throw new Error("Slopify couldn't read the length of the narration audio, so the file may be damaged or empty. Regenerate the narration (or upload your audio file again in Edit project → Narration), then Try again.");
|
|
38
55
|
return { path, seconds: duration / 1000 };
|
|
39
56
|
};
|
|
40
57
|
const body = await input("audio_body");
|
|
41
58
|
if (body === undefined)
|
|
42
|
-
throw new Error("The narration audio isn't finished yet. Let the Narration stage finish (Resume, or
|
|
59
|
+
throw new Error("The narration audio isn't finished yet. Let the Narration stage finish (Resume, or Try again on it), then retry this stage.");
|
|
43
60
|
return audioTimeline({
|
|
44
61
|
body,
|
|
45
62
|
intro: config.sources.audio === "generate" && config.intro !== undefined
|
|
@@ -55,7 +72,10 @@ export async function revisionAudio(deps, context, view) {
|
|
|
55
72
|
export function revisionTranscript(deps, snapshot, kind) {
|
|
56
73
|
const { view, plan } = snapshot;
|
|
57
74
|
if (usesNarrationPreparation(view.revision.config) ||
|
|
58
|
-
usesPronunciationGlossary(view.revision.config)
|
|
75
|
+
usesPronunciationGlossary(view.revision.config) ||
|
|
76
|
+
(kind === "body" && usesVoices(view.revision.config)) ||
|
|
77
|
+
// Aliased requests say something other than the text; the transcript is the text.
|
|
78
|
+
narrationAliasesOf(view.revision.config).length > 0)
|
|
59
79
|
return joinedNarration(narrationTextParts(view, plan, kind));
|
|
60
80
|
const concat = plan.recipes.find((one) => one.key === (kind === "body" ? "audio:body:concat" : `audio:${kind}`));
|
|
61
81
|
if (concat !== undefined) {
|
|
@@ -73,7 +93,7 @@ export function revisionTranscript(deps, snapshot, kind) {
|
|
|
73
93
|
Array.isArray(recipe.input.semantic) &&
|
|
74
94
|
typeof recipe.input.semantic[0] === "string")
|
|
75
95
|
return recipe.input.semantic[0];
|
|
76
|
-
throw new Error("Some narration chunks aren't finished, so the full narration text isn't available. Let the Narration stage finish (Resume, or
|
|
96
|
+
throw new Error("Some narration chunks aren't finished, so the full narration text isn't available. Let the Narration stage finish (Resume, or Try again on it), or regenerate the missing chunk in Edit project → Narration.");
|
|
77
97
|
});
|
|
78
98
|
if (texts.length > 0)
|
|
79
99
|
return texts.join("\n");
|
|
@@ -85,7 +105,7 @@ export function revisionTranscript(deps, snapshot, kind) {
|
|
|
85
105
|
if (view.articleMarkdown !== null)
|
|
86
106
|
return plainText(splitEndMatter(view.articleMarkdown).body);
|
|
87
107
|
}
|
|
88
|
-
throw new Error(`The saved text of the ${kind === "body" ? "article" : kind} is missing, so it can't be matched to the narration. Use
|
|
108
|
+
throw new Error(`The saved text of the ${kind === "body" ? "article" : kind} is missing, so it can't be matched to the narration. Use More → Write the article again in the Article section, then Try again.`);
|
|
89
109
|
}
|
|
90
110
|
export function retainedOutput(deps, view, row) {
|
|
91
111
|
const asset = z
|
|
@@ -113,3 +133,25 @@ export function retainedOutput(deps, view, row) {
|
|
|
113
133
|
output: { ...row.output, id: deps.ids.next(), createdAt: deps.clock.now().toISOString() },
|
|
114
134
|
};
|
|
115
135
|
}
|
|
136
|
+
// The body's speaker turns in narration order, as the plan asked for them: a native
|
|
137
|
+
// multi-speaker request lists its own lines, every other part says whose turn it speaks.
|
|
138
|
+
export function revisionTurns(snapshot) {
|
|
139
|
+
const { view, plan } = snapshot;
|
|
140
|
+
if (!usesVoices(view.revision.config))
|
|
141
|
+
return [];
|
|
142
|
+
const concat = plan.recipes.find((one) => one.key === "audio:body:concat");
|
|
143
|
+
return (concat?.dependsOn ?? []).flatMap((key) => {
|
|
144
|
+
const input = plan.recipes.find((one) => one.key === key)?.input;
|
|
145
|
+
if (input?.kind !== "tts")
|
|
146
|
+
return [];
|
|
147
|
+
if (input.dialogue !== undefined)
|
|
148
|
+
return input.dialogue.map((line) => ({
|
|
149
|
+
speaker: line.speaker,
|
|
150
|
+
turn: line.turn,
|
|
151
|
+
text: line.text,
|
|
152
|
+
}));
|
|
153
|
+
return input.speaker === undefined || input.turn === undefined
|
|
154
|
+
? []
|
|
155
|
+
: [{ speaker: input.speaker, turn: input.turn, text: input.spokenText ?? input.text }];
|
|
156
|
+
});
|
|
157
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { relative, sep } from "node:path";
|
|
2
|
+
import { resolveBoldFont } from "../fonts/index.js";
|
|
3
|
+
import { masterGoal } from "../loudness/model.js";
|
|
4
|
+
import { renderShort, shortFrame } from "../shorts/render.js";
|
|
5
|
+
import { allocateAsset, discardPreparedAssets, sealAsset } from "../storage/assets.js";
|
|
6
|
+
import { projectDir } from "../storage/layout.js";
|
|
7
|
+
import { probeDurationMs } from "../video/ffmpeg.js";
|
|
8
|
+
import { slideshowImages } from "./runtime-export.js";
|
|
9
|
+
import { preparedResult, preparedText, publishResult } from "./runtime-publication.js";
|
|
10
|
+
import { timingWords } from "./runtime-shorts.js";
|
|
11
|
+
// A short-mode project's video (`admission/short-mode.ts`): the whole narration timeline as
|
|
12
|
+
// one clip through the Shorts renderer, so it looks like every short Slopify cuts from a long
|
|
13
|
+
// video - the images moving as the project says, the title on top and big word-by-word
|
|
14
|
+
// captions from the word timing.
|
|
15
|
+
export async function executeShortExport(deps, context, piece, snapshot, timeline) {
|
|
16
|
+
const { view } = snapshot;
|
|
17
|
+
const config = view.revision.config;
|
|
18
|
+
const images = slideshowImages(deps, context, view);
|
|
19
|
+
const words = timingWords(deps, context, view);
|
|
20
|
+
const seconds = timeline.reduce((sum, segment) => sum + segment.seconds, 0);
|
|
21
|
+
if (seconds <= 0)
|
|
22
|
+
throw new Error("The short has no narration to play, so there is nothing to render. Let the Narration stage finish (Resume, or Try again on it), then retry this stage.");
|
|
23
|
+
const font = await resolveBoldFont(deps.paths, config.subtitles?.fontId ?? "default");
|
|
24
|
+
if (!context.maySubmit(piece.id))
|
|
25
|
+
return "held";
|
|
26
|
+
const pending = allocateAsset(deps, context.work.projectId, "video.mp4");
|
|
27
|
+
const prepared = [];
|
|
28
|
+
const directory = projectDir(deps.paths, context.work.projectId);
|
|
29
|
+
const relativePath = (path) => relative(directory, path).split(sep).join("/");
|
|
30
|
+
try {
|
|
31
|
+
const master = await renderShort({
|
|
32
|
+
bin: deps.ffmpeg,
|
|
33
|
+
master: masterGoal(config, "video"),
|
|
34
|
+
timeline,
|
|
35
|
+
start: 0,
|
|
36
|
+
end: seconds,
|
|
37
|
+
images,
|
|
38
|
+
imageSeconds: config.imageSeconds,
|
|
39
|
+
motionStyle: config.motionStyle,
|
|
40
|
+
zoomPercent: config.zoomPercent,
|
|
41
|
+
words,
|
|
42
|
+
font,
|
|
43
|
+
title: config.title,
|
|
44
|
+
output: pending.absolutePath,
|
|
45
|
+
scratch: directory,
|
|
46
|
+
signal: context.signal,
|
|
47
|
+
log: deps.log,
|
|
48
|
+
onProgress: (elapsedMs) => context.emit({
|
|
49
|
+
type: "stage.progress",
|
|
50
|
+
projectId: context.work.projectId,
|
|
51
|
+
stage: "video",
|
|
52
|
+
current: Math.min(100, Math.round(elapsedMs / (seconds * 10))),
|
|
53
|
+
total: 100,
|
|
54
|
+
}),
|
|
55
|
+
});
|
|
56
|
+
context.signal.throwIfAborted();
|
|
57
|
+
const durationMs = await probeDurationMs(deps.ffmpeg, pending.absolutePath, context.signal, deps.log);
|
|
58
|
+
const asset = sealAsset(deps, pending);
|
|
59
|
+
prepared.push(preparedResult(deps, context, piece, "video", asset, durationMs, {
|
|
60
|
+
subtitlesMode: "off",
|
|
61
|
+
...(master === undefined ? {} : { master }),
|
|
62
|
+
}));
|
|
63
|
+
prepared.push(preparedText(deps, context, piece, "render_params", "render.json", JSON.stringify({
|
|
64
|
+
mode: "short",
|
|
65
|
+
...shortFrame,
|
|
66
|
+
totalSeconds: seconds,
|
|
67
|
+
imageSeconds: config.imageSeconds,
|
|
68
|
+
motionStyle: config.motionStyle,
|
|
69
|
+
zoomPercent: config.zoomPercent,
|
|
70
|
+
images: images.map(relativePath),
|
|
71
|
+
audio: timeline.map((segment) => ({
|
|
72
|
+
...segment,
|
|
73
|
+
path: segment.path === null ? null : relativePath(segment.path),
|
|
74
|
+
})),
|
|
75
|
+
fontId: config.subtitles?.fontId ?? "default",
|
|
76
|
+
output: pending.path,
|
|
77
|
+
})));
|
|
78
|
+
await publishResult(deps, context, piece, prepared, { totalSeconds: seconds }, asset);
|
|
79
|
+
deps.count?.("stage.completed", { stage: "video" });
|
|
80
|
+
return "done";
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
discardPreparedAssets(deps, [pending, ...prepared.map((one) => one.asset)]);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { copyFileSync, mkdirSync, mkdtempSync, rmSync } from "node:fs";
|
|
2
2
|
import { extname, join, relative, sep } from "node:path";
|
|
3
|
+
import { usesShortMode } from "../admission/short-mode.js";
|
|
4
|
+
import { masterFile, masterReport } from "../loudness/loudnorm.js";
|
|
5
|
+
import { masterGoal } from "../loudness/model.js";
|
|
3
6
|
import { allocateAsset, discardPreparedAssets, sealAsset } from "../storage/assets.js";
|
|
4
7
|
import { outputPath, projectDir } from "../storage/layout.js";
|
|
5
8
|
import { audioExportArgs } from "../video/audio-export.js";
|
|
@@ -7,8 +10,11 @@ import { withPaths } from "../video/edit-list.js";
|
|
|
7
10
|
import { runFfmpeg } from "../video/ffmpeg.js";
|
|
8
11
|
import { planRender } from "../video/plan.js";
|
|
9
12
|
import { renderSlideshow } from "../video/slideshow.js";
|
|
13
|
+
import { writePortraits } from "../voices/portraits.js";
|
|
14
|
+
import { exportBed } from "./runtime-export-bed.js";
|
|
10
15
|
import { exportEdit } from "./runtime-export-edit.js";
|
|
11
16
|
import { exportSnapshot, retainedOutput, revisionAudio, } from "./runtime-export-inputs.js";
|
|
17
|
+
import { executeShortExport } from "./runtime-export-short.js";
|
|
12
18
|
import { preparedResult, preparedText, publishResult } from "./runtime-publication.js";
|
|
13
19
|
export async function executeExportRecipe(deps, context, piece) {
|
|
14
20
|
if (deps.db
|
|
@@ -20,12 +26,14 @@ export async function executeExportRecipe(deps, context, piece) {
|
|
|
20
26
|
context.signal.throwIfAborted();
|
|
21
27
|
const snapshot = exportSnapshot(deps, context, piece);
|
|
22
28
|
const { view } = snapshot;
|
|
23
|
-
const audio = await revisionAudio(deps, context, view);
|
|
29
|
+
const audio = await revisionAudio(deps, context, view, { levelled: true });
|
|
24
30
|
const wav = piece.key === "export:wav";
|
|
25
31
|
if (!wav && piece.key !== "export:video")
|
|
26
|
-
throw new Error("Slopify hit an internal error (unknown kind of export).
|
|
32
|
+
throw new Error("Slopify hit an internal error (unknown kind of export). Try again; if it happens again, use Download diagnostics in Settings and report it.");
|
|
27
33
|
if (wav && audio.length === 0)
|
|
28
|
-
throw new Error("The audio export has nothing to export because narration is turned off for this project. Turn narration on in Edit project, then
|
|
34
|
+
throw new Error("The audio export has nothing to export because narration is turned off for this project. Turn narration on in Edit project, then Try again.");
|
|
35
|
+
if (!wav && usesShortMode(view.revision.config))
|
|
36
|
+
return executeShortExport(deps, context, piece, snapshot, audio);
|
|
29
37
|
const filename = wav ? "audio.wav" : "video.mp4";
|
|
30
38
|
const pending = allocateAsset(deps, context.work.projectId, filename);
|
|
31
39
|
const prepared = [];
|
|
@@ -35,6 +43,9 @@ export async function executeExportRecipe(deps, context, piece) {
|
|
|
35
43
|
const burn = !wav && view.revision.config.subtitles?.mode === "burn-in";
|
|
36
44
|
if (burn)
|
|
37
45
|
directory = captionDirectory(deps, context, snapshot);
|
|
46
|
+
const portraits = directory === undefined
|
|
47
|
+
? undefined
|
|
48
|
+
: writePortraits(deps.db, view.revision.config, directory);
|
|
38
49
|
const mode = burn
|
|
39
50
|
? "burn-in"
|
|
40
51
|
: captions.some((row) => row.output.role === "subtitles_srt") &&
|
|
@@ -48,13 +59,9 @@ export async function executeExportRecipe(deps, context, piece) {
|
|
|
48
59
|
? undefined
|
|
49
60
|
: { path: row.path, seconds: row.seconds };
|
|
50
61
|
};
|
|
51
|
-
const images =
|
|
52
|
-
const row = view.outputs.find((one) => one.workKey === `image:${key}` && one.selected && one.available && one.state === "ready");
|
|
53
|
-
if (row === undefined)
|
|
54
|
-
throw new Error("The video export can't find one of the slideshow images. Use Re-run section on Images (or regenerate that image in Edit project → Images), then Retry stage.");
|
|
55
|
-
return outputPath(deps.paths, context.work.projectId, row.output.path);
|
|
56
|
-
});
|
|
62
|
+
const images = wav ? [] : slideshowImages(deps, context, view);
|
|
57
63
|
const edited = wav ? undefined : await exportEdit(deps, context, view, images);
|
|
64
|
+
const bed = wav ? undefined : await exportBed(deps, context, view);
|
|
58
65
|
const plan = wav
|
|
59
66
|
? undefined
|
|
60
67
|
: planRender({
|
|
@@ -70,6 +77,7 @@ export async function executeExportRecipe(deps, context, piece) {
|
|
|
70
77
|
images,
|
|
71
78
|
output: pending.absolutePath,
|
|
72
79
|
edit: edited?.edit,
|
|
80
|
+
bed,
|
|
73
81
|
});
|
|
74
82
|
const totalSeconds = plan?.totalSeconds ?? audio.reduce((sum, row) => sum + row.seconds, 0);
|
|
75
83
|
const projectDirectory = projectDir(deps.paths, context.work.projectId);
|
|
@@ -93,6 +101,7 @@ export async function executeExportRecipe(deps, context, piece) {
|
|
|
93
101
|
output: pending.path,
|
|
94
102
|
editList: withPaths(plan.editList, relativePath),
|
|
95
103
|
subtitles: config.subtitles,
|
|
104
|
+
...(portraits === undefined ? {} : { speakerPortraits: portraits.recorded }),
|
|
96
105
|
...(edited?.settings === undefined ? {} : { videoEdit: edited.settings }),
|
|
97
106
|
...(edited === undefined || edited.warnings.length === 0
|
|
98
107
|
? {}
|
|
@@ -107,25 +116,71 @@ export async function executeExportRecipe(deps, context, piece) {
|
|
|
107
116
|
current: Math.min(100, Math.round(elapsedMs / (totalSeconds * 10))),
|
|
108
117
|
total: 100,
|
|
109
118
|
});
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
119
|
+
// Level the volume: the audio-only file is mastered to the audio files' target, the video to
|
|
120
|
+
// the video's (`loudness/model.ts`).
|
|
121
|
+
const goal = masterGoal(config, plan === undefined ? "audioFiles" : "video");
|
|
122
|
+
let master;
|
|
123
|
+
if (plan === undefined) {
|
|
124
|
+
const mixed = goal === undefined ? pending.absolutePath : `${pending.absolutePath}.mix.wav`;
|
|
125
|
+
try {
|
|
126
|
+
await runFfmpeg({
|
|
127
|
+
bin: deps.ffmpeg,
|
|
128
|
+
args: audioExportArgs(audio, mixed),
|
|
129
|
+
signal: context.signal,
|
|
130
|
+
log: deps.log,
|
|
131
|
+
onProgress,
|
|
132
|
+
});
|
|
133
|
+
if (goal !== undefined) {
|
|
134
|
+
const run = { bin: deps.ffmpeg, log: deps.log, signal: context.signal };
|
|
135
|
+
await masterFile(run, mixed, `${pending.absolutePath}.master.wav`, goal, {
|
|
136
|
+
sampleRate: 48000,
|
|
137
|
+
channels: 2,
|
|
138
|
+
});
|
|
139
|
+
// The export is 16-bit PCM, as it always was.
|
|
140
|
+
await runFfmpeg({
|
|
141
|
+
bin: deps.ffmpeg,
|
|
142
|
+
args: [
|
|
143
|
+
"-hide_banner",
|
|
144
|
+
"-nostdin",
|
|
145
|
+
"-loglevel",
|
|
146
|
+
"error",
|
|
147
|
+
"-nostats",
|
|
148
|
+
"-y",
|
|
149
|
+
"-i",
|
|
150
|
+
`${pending.absolutePath}.master.wav`,
|
|
151
|
+
"-c:a",
|
|
152
|
+
"pcm_s16le",
|
|
153
|
+
"-f",
|
|
154
|
+
"wav",
|
|
155
|
+
pending.absolutePath,
|
|
156
|
+
],
|
|
157
|
+
signal: context.signal,
|
|
158
|
+
log: deps.log,
|
|
159
|
+
onProgress: () => { },
|
|
160
|
+
});
|
|
161
|
+
master = await masterReport(run, pending.absolutePath, goal);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
finally {
|
|
165
|
+
if (goal !== undefined) {
|
|
166
|
+
rmSync(mixed, { force: true });
|
|
167
|
+
rmSync(`${pending.absolutePath}.master.wav`, { force: true });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
118
171
|
else
|
|
119
|
-
await renderSlideshow({
|
|
172
|
+
master = await renderSlideshow({
|
|
120
173
|
bin: deps.ffmpeg,
|
|
121
174
|
edit: plan.editList,
|
|
122
175
|
output: plan.output,
|
|
123
176
|
burnSubtitles: burn,
|
|
124
177
|
cwd: directory,
|
|
178
|
+
...(portraits === undefined ? {} : { portraits: portraits.overlays }),
|
|
125
179
|
scratch: projectDirectory,
|
|
126
180
|
signal: context.signal,
|
|
127
181
|
log: deps.log,
|
|
128
182
|
onProgress,
|
|
183
|
+
...(goal === undefined ? {} : { master: goal }),
|
|
129
184
|
});
|
|
130
185
|
context.signal.throwIfAborted();
|
|
131
186
|
const asset = sealAsset(deps, pending);
|
|
@@ -134,6 +189,7 @@ export async function executeExportRecipe(deps, context, piece) {
|
|
|
134
189
|
...(edited === undefined || edited.warnings.length === 0
|
|
135
190
|
? {}
|
|
136
191
|
: { warnings: edited.warnings }),
|
|
192
|
+
...(master === undefined ? {} : { master }),
|
|
137
193
|
}));
|
|
138
194
|
prepared.push(preparedText(deps, context, piece, "render_params", "render.json", JSON.stringify(record)));
|
|
139
195
|
prepared.push(...captions.map((row) => retainedOutput(deps, view, row)));
|
|
@@ -147,12 +203,21 @@ export async function executeExportRecipe(deps, context, piece) {
|
|
|
147
203
|
rmSync(directory, { recursive: true, force: true });
|
|
148
204
|
}
|
|
149
205
|
}
|
|
206
|
+
// The project's images in slideshow order, as files.
|
|
207
|
+
export function slideshowImages(deps, context, view) {
|
|
208
|
+
return view.revision.content.imageOrder.map((key) => {
|
|
209
|
+
const row = view.outputs.find((one) => one.workKey === `image:${key}` && one.selected && one.available && one.state === "ready");
|
|
210
|
+
if (row === undefined)
|
|
211
|
+
throw new Error("The video export can't find one of the slideshow images. Use More → make it again in its section on Images (or regenerate that image in Edit project → Images), then Try again.");
|
|
212
|
+
return outputPath(deps.paths, context.work.projectId, row.output.path);
|
|
213
|
+
});
|
|
214
|
+
}
|
|
150
215
|
function captionDirectory(deps, context, snapshot) {
|
|
151
216
|
const find = (role) => snapshot.view.outputs.find((row) => row.selected && row.available && row.state === "ready" && row.output.role === role);
|
|
152
217
|
const ass = find("subtitle_ass");
|
|
153
218
|
const font = find("subtitle_font");
|
|
154
219
|
if (ass === undefined || font === undefined)
|
|
155
|
-
throw new Error("Burned-in captions are missing their caption file or font. Use
|
|
220
|
+
throw new Error("Burned-in captions are missing their caption file or font. Use More → Render the video again in the Video section (or choose the caption font again in Edit project → Subtitles), then Try again.");
|
|
156
221
|
const directory = mkdtempSync(join(projectDir(deps.paths, context.work.projectId), "render-"));
|
|
157
222
|
try {
|
|
158
223
|
mkdirSync(join(directory, "fonts"), { mode: 0o700 });
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { copyFileSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { captionFont, captionFontDeps } from "../fonts/coverage.js";
|
|
5
|
+
import { describedKinds } from "../narration/blocks.js";
|
|
6
|
+
import { allocateAsset, discardPreparedAssets, sealAsset } from "../storage/assets.js";
|
|
7
|
+
import { projectDir } from "../storage/layout.js";
|
|
8
|
+
import { defaultCardStyle, renderCard } from "../video/figure-card.js";
|
|
9
|
+
import { articlePicture } from "./runtime-pictures.js";
|
|
10
|
+
import { executionView } from "./runtime-plan.js";
|
|
11
|
+
import { preparedResult, publishResult } from "./runtime-publication.js";
|
|
12
|
+
// A `figure:card:<n>` step: the block drawn as a card in each frame the project needs (the
|
|
13
|
+
// video's, and upright for the Shorts of a 16:9 video), with the ffmpeg Slopify ships.
|
|
14
|
+
const values = z.object({
|
|
15
|
+
kind: z.enum(describedKinds),
|
|
16
|
+
index: z.number().int().positive(),
|
|
17
|
+
source: z.string(),
|
|
18
|
+
section: z.string().nullable(),
|
|
19
|
+
image: z.string().nullable(),
|
|
20
|
+
formats: z.array(z.enum(["16:9", "9:16"])).min(1),
|
|
21
|
+
fontId: z.string(),
|
|
22
|
+
color: z.string().nullable(),
|
|
23
|
+
language: z.string(),
|
|
24
|
+
});
|
|
25
|
+
const frames = { "16:9": [1920, 1080], "9:16": [1080, 1920] };
|
|
26
|
+
export async function executeFigureCard(deps, context, piece) {
|
|
27
|
+
if (piece.input.kind !== "local")
|
|
28
|
+
throw new Error("Slopify hit an internal error (a figure card step was set up wrongly). Try again; if it happens again, use Download diagnostics in Settings and report it.");
|
|
29
|
+
const card = values.parse(piece.input.values);
|
|
30
|
+
const view = executionView(deps, context.work.projectId, context.work.revisionId);
|
|
31
|
+
// The article's own picture for a figure, when the project has it as an upload.
|
|
32
|
+
const picture = card.kind === "figure" && view !== undefined
|
|
33
|
+
? articlePicture(deps, view, card.image ?? undefined)
|
|
34
|
+
: undefined;
|
|
35
|
+
const font = await captionFont(captionFontDeps(deps.paths), card.fontId, card.language === "en" ? undefined : card.language);
|
|
36
|
+
const workspace = mkdtempSync(join(projectDir(deps.paths, context.work.projectId), "card-"));
|
|
37
|
+
const pending = card.formats.map(() => allocateAsset(deps, context.work.projectId, `card-${String(card.index)}.png`));
|
|
38
|
+
const prepared = [];
|
|
39
|
+
try {
|
|
40
|
+
mkdirSync(join(workspace, "fonts"), { mode: 0o700 });
|
|
41
|
+
copyFileSync(font.path, join(workspace, "fonts", `card${font.extension}`));
|
|
42
|
+
for (const [at, format] of card.formats.entries()) {
|
|
43
|
+
const target = pending[at];
|
|
44
|
+
if (target === undefined)
|
|
45
|
+
continue;
|
|
46
|
+
const [width, height] = frames[format];
|
|
47
|
+
await renderCard({
|
|
48
|
+
bin: deps.ffmpeg,
|
|
49
|
+
input: {
|
|
50
|
+
kind: card.kind,
|
|
51
|
+
source: card.source,
|
|
52
|
+
section: card.section,
|
|
53
|
+
width,
|
|
54
|
+
height,
|
|
55
|
+
style: {
|
|
56
|
+
...defaultCardStyle,
|
|
57
|
+
font: font.assName,
|
|
58
|
+
...(card.color === null ? {} : { accent: card.color }),
|
|
59
|
+
},
|
|
60
|
+
picture: picture !== undefined,
|
|
61
|
+
},
|
|
62
|
+
cwd: workspace,
|
|
63
|
+
output: target.absolutePath,
|
|
64
|
+
picture,
|
|
65
|
+
signal: context.signal,
|
|
66
|
+
log: deps.log,
|
|
67
|
+
}, (ass) => writeFileSync(join(workspace, "card.ass"), ass, { mode: 0o600 }));
|
|
68
|
+
context.signal.throwIfAborted();
|
|
69
|
+
const asset = sealAsset(deps, target);
|
|
70
|
+
const output = preparedResult(deps, context, piece, "figure_card", asset, null, {
|
|
71
|
+
index: card.index,
|
|
72
|
+
format,
|
|
73
|
+
});
|
|
74
|
+
prepared.push({ ...output, slot: `${piece.key}:${format}` });
|
|
75
|
+
}
|
|
76
|
+
await publishResult(deps, context, piece, prepared, {
|
|
77
|
+
index: card.index,
|
|
78
|
+
kind: card.kind,
|
|
79
|
+
picture: picture !== undefined,
|
|
80
|
+
});
|
|
81
|
+
return "done";
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
rmSync(workspace, { recursive: true, force: true });
|
|
85
|
+
discardPreparedAssets(deps, [...pending, ...prepared.map((one) => one.asset)]);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import { imageBlob } from "../channels/repo.js";
|
|
3
4
|
import { outputPath } from "../storage/layout.js";
|
|
4
5
|
// The provider call an image step makes: its request, the effort when one is chosen and, while
|
|
5
6
|
// the project's Establishing image is on, that image's bytes as the visual reference.
|
|
6
7
|
export function imageCall(deps, projectId, input, previewLabel) {
|
|
7
8
|
const reference = referenceImage(deps, projectId, input);
|
|
9
|
+
const cast = castImages(deps, input);
|
|
8
10
|
return {
|
|
9
11
|
provider: input.provider,
|
|
10
12
|
model: input.model,
|
|
@@ -12,6 +14,7 @@ export function imageCall(deps, projectId, input, previewLabel) {
|
|
|
12
14
|
aspect: input.aspect,
|
|
13
15
|
...(input.thinking === undefined ? {} : { thinking: input.thinking }),
|
|
14
16
|
...(reference === undefined ? {} : { reference }),
|
|
17
|
+
...(cast === undefined ? {} : { cast }),
|
|
15
18
|
...(previewLabel === undefined ? {} : { previewLabel }),
|
|
16
19
|
};
|
|
17
20
|
}
|
|
@@ -19,7 +22,7 @@ function referenceImage(deps, projectId, input) {
|
|
|
19
22
|
if (input.reference === undefined)
|
|
20
23
|
return undefined;
|
|
21
24
|
// The step waits for the establishing image, so a missing one went away since.
|
|
22
|
-
const missing = "The establishing image this image is drawn from is missing. Use Regenerate on the establishing image in the Images section (or upload it again in Edit project → Images), then use
|
|
25
|
+
const missing = "The establishing image this image is drawn from is missing. Use Regenerate on the establishing image in the Images section (or upload it again in Edit project → Images), then use Try again.";
|
|
23
26
|
if (input.reference.assetId === null)
|
|
24
27
|
throw new Error(missing);
|
|
25
28
|
const row = z
|
|
@@ -39,9 +42,25 @@ function referenceImage(deps, projectId, input) {
|
|
|
39
42
|
}
|
|
40
43
|
const mime = imageMime(bytes);
|
|
41
44
|
if (mime === undefined)
|
|
42
|
-
throw new Error("The establishing image is not a PNG or JPEG file, so it can't be sent as a reference. Upload a PNG or JPEG in Edit project → Images → Establishing image, then use
|
|
45
|
+
throw new Error("The establishing image is not a PNG or JPEG file, so it can't be sent as a reference. Upload a PNG or JPEG in Edit project → Images → Establishing image, then use Try again.");
|
|
43
46
|
return { bytes, mime };
|
|
44
47
|
}
|
|
48
|
+
// The cast pictures the request names, read by hash from the database. They are never removed
|
|
49
|
+
// while a project may name them, so a missing one came from a backup of another install.
|
|
50
|
+
function castImages(deps, input) {
|
|
51
|
+
if (input.cast === undefined)
|
|
52
|
+
return undefined;
|
|
53
|
+
return input.cast.map((member) => ({
|
|
54
|
+
name: member.name,
|
|
55
|
+
description: member.description,
|
|
56
|
+
images: member.images.map((sha256) => {
|
|
57
|
+
const blob = imageBlob(deps.db, sha256);
|
|
58
|
+
if (blob === undefined)
|
|
59
|
+
throw new Error(`A picture of ${member.name} that this project was started with is not in this Slopify's database, so it can't be sent as a reference. Add the picture again in Channels → Cast, then start the video again from Play.`);
|
|
60
|
+
return blob;
|
|
61
|
+
}),
|
|
62
|
+
}));
|
|
63
|
+
}
|
|
45
64
|
function imageMime(bytes) {
|
|
46
65
|
const png = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
|
|
47
66
|
if (png.every((value, index) => bytes[index] === value))
|