@gentbajko/slopify 2.4.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -21
- package/NOTICE +12 -0
- package/README.md +21 -40
- package/SUBTITLES.md +1 -1
- package/dist/adapter-registry.js +11 -1
- package/dist/adapters/alignment/cache.js +4 -1
- package/dist/adapters/alignment/ctc.js +25 -23
- package/dist/adapters/alignment/index.js +37 -6
- package/dist/adapters/alignment/multilingual.js +267 -0
- package/dist/adapters/alignment/protocol.js +11 -0
- package/dist/adapters/alignment/quality.js +4 -3
- package/dist/adapters/alignment/sentences.js +95 -0
- package/dist/adapters/alignment/spec.js +24 -0
- package/dist/adapters/alignment/spell.js +769 -0
- package/dist/adapters/alignment/text.js +56 -9
- package/dist/adapters/alignment/window.js +34 -30
- package/dist/adapters/alignment/worker.js +23 -21
- package/dist/adapters/explain.js +11 -11
- package/dist/adapters/host-cli/index.js +55 -5
- package/dist/adapters/host-cli/transport.js +13 -6
- package/dist/adapters/image/bytes.js +2 -2
- package/dist/adapters/image/codex-output.js +60 -18
- package/dist/adapters/image/codex.js +166 -25
- package/dist/adapters/image/fal.js +31 -3
- package/dist/adapters/image/google.js +17 -2
- package/dist/adapters/image/openai.js +45 -16
- package/dist/adapters/image/reference.js +41 -0
- package/dist/adapters/image/replicate.js +14 -2
- package/dist/adapters/image/video.js +2 -2
- package/dist/adapters/key-probes.js +36 -0
- package/dist/adapters/llm/claude-code.js +168 -32
- package/dist/adapters/llm/cli-login-error.js +1 -1
- package/dist/adapters/llm/codex-limits.js +157 -0
- package/dist/adapters/llm/codex-models.js +2 -1
- package/dist/adapters/llm/codex-server-models.js +2 -1
- package/dist/adapters/llm/codex.js +99 -12
- package/dist/adapters/llm/document-workspace.js +2 -2
- package/dist/adapters/llm/gemini-workspace.js +1 -1
- package/dist/adapters/llm/gemini.js +16 -3
- package/dist/adapters/llm/image-workspace.js +58 -0
- package/dist/adapters/llm/openrouter.js +11 -3
- package/dist/adapters/llm/run-cli.js +26 -9
- package/dist/adapters/tts/cartesia.js +10 -0
- package/dist/adapters/tts/elevenlabs.js +31 -9
- package/dist/adapters/tts/inworld-async.js +9 -5
- package/dist/adapters/tts/inworld.js +17 -3
- package/dist/adapters/tts/openai.js +3 -0
- package/dist/adapters/tts/voice-languages.js +80 -0
- package/dist/assets/fonts/noto/NotoSans-Regular.ttf +0 -0
- package/dist/assets/fonts/noto/NotoSansArabic-Regular.ttf +0 -0
- package/dist/assets/fonts/noto/NotoSansDevanagari-Regular.ttf +0 -0
- package/dist/assets/fonts/noto/NotoSansHebrew-Regular.ttf +0 -0
- package/dist/assets/fonts/noto/NotoSansThai-Regular.ttf +0 -0
- package/dist/assets/fonts/noto/OFL.txt +93 -0
- package/dist/assets/fonts/noto/SOURCE.txt +18 -0
- package/dist/assets/models.yaml +7 -0
- package/dist/assets/sample/sample-audiobook.tar +0 -0
- package/dist/assets/sample/sample-podcast.tar +0 -0
- package/dist/assets/sample/sample-project.tar +0 -0
- package/dist/catalog/merge.js +132 -0
- package/dist/catalog/registry.js +33 -12
- package/dist/catalog/runtime-models.js +1 -1
- package/dist/catalog/schema.js +12 -1
- package/dist/catalog/store.js +147 -8
- package/dist/catalog/validate.js +17 -2
- package/dist/compose.yaml +71 -0
- package/dist/edge/autostart/docker-record.js +32 -0
- package/dist/edge/autostart/docker.js +50 -0
- package/dist/edge/autostart/index.js +78 -0
- package/dist/edge/autostart/launcher.js +176 -0
- package/dist/edge/autostart/model.js +3 -0
- package/dist/edge/autostart/native.js +210 -0
- package/dist/edge/autostart/prompt.js +66 -0
- package/dist/edge/autostart/service.js +81 -0
- package/dist/edge/cli.js +57 -51
- package/dist/edge/docker-install/activation.js +56 -0
- package/dist/edge/docker-install/apply.js +458 -0
- package/dist/edge/docker-install/engine.js +362 -0
- package/dist/edge/docker-install/run.js +154 -0
- package/dist/edge/docker-install/state.js +146 -0
- package/dist/edge/{docker-projects → docker-install}/volume.js +1 -5
- package/dist/edge/docker.js +14 -22
- package/dist/edge/events/hub.js +11 -0
- package/dist/edge/host-cli.js +1 -1
- package/dist/edge/http/actions.js +32 -10
- package/dist/edge/http/app.js +60 -2
- package/dist/edge/http/auditions.js +70 -0
- package/dist/edge/http/autostart.js +37 -0
- package/dist/edge/http/backups.js +40 -0
- package/dist/edge/http/channel-memory.js +92 -0
- package/dist/edge/http/channels.js +128 -0
- package/dist/edge/http/entries.js +22 -3
- package/dist/edge/http/files.js +1 -1
- package/dist/edge/http/folder-location.js +1 -1
- package/dist/edge/http/home.js +39 -0
- package/dist/edge/http/host-cli.js +45 -6
- package/dist/edge/http/narration-peaks.js +76 -0
- package/dist/edge/http/onboarding.js +152 -0
- package/dist/edge/http/open-folder.js +1 -1
- package/dist/edge/http/patch-notes.js +45 -0
- package/dist/edge/http/planning.js +4 -2
- package/dist/edge/http/problem.js +1 -0
- package/dist/edge/http/project-create.js +87 -0
- package/dist/edge/http/projects.js +31 -99
- package/dist/edge/http/prompts.js +40 -3
- package/dist/edge/http/pronunciations.js +23 -4
- package/dist/edge/http/providers.js +86 -0
- package/dist/edge/http/reviews.js +121 -0
- package/dist/edge/http/revision-files.js +1 -1
- package/dist/edge/http/revisions.js +1 -1
- package/dist/edge/http/run-cost.js +25 -0
- package/dist/edge/http/schedules.js +111 -6
- package/dist/edge/http/settings.js +128 -6
- package/dist/edge/http/storage-files.js +50 -0
- package/dist/edge/http/storage.js +29 -2
- package/dist/edge/http/studio.js +207 -0
- package/dist/edge/http/style-preview.js +100 -0
- package/dist/edge/http/subtitles.js +1 -1
- package/dist/edge/http/trash.js +69 -0
- package/dist/edge/http/update.js +24 -3
- package/dist/edge/http/whats-new.js +15 -0
- package/dist/edge/http/youtube-edits.js +83 -0
- package/dist/edge/native-update.js +62 -0
- package/dist/host-cli/open-folder.js +10 -10
- package/dist/host-cli/runtime.js +12 -3
- package/dist/kernel/db/migrations/0022-drop-duplicate-held-work.sql +56 -0
- package/dist/kernel/db/migrations/0024-reference-attachments.sql +18 -0
- package/dist/kernel/db/migrations/0025-automatic-reviews.sql +40 -0
- package/dist/kernel/db/migrations/0026-schedule-topic-generation.sql +23 -0
- package/dist/kernel/db/migrations/0027-run-cost.sql +58 -0
- package/dist/kernel/db/migrations/0030-automatic-retries.sql +18 -0
- package/dist/kernel/db/migrations/0031-prompt-history-and-description-edits.sql +33 -0
- package/dist/kernel/db/migrations/0032-channels.sql +65 -0
- package/dist/kernel/db/migrations/0034-script-prompts.sql +23 -0
- package/dist/kernel/db/migrations/0035-narration-aliases.sql +13 -0
- package/dist/kernel/db/migrations/0037-project-uploads.sql +8 -0
- package/dist/kernel/db/migrations/0038-voice-languages.sql +7 -0
- package/dist/kernel/db/migrations/0039-channel-essentials.sql +64 -0
- package/dist/kernel/db/migrations/0040-standalone-usage.sql +35 -0
- package/dist/kernel/paths.js +27 -6
- package/dist/kernel/pipeline.js +4 -1
- package/dist/kernel/ports/host-cli.js +139 -16
- package/dist/kernel/ports/image.js +3 -1
- package/dist/kernel/ports/key-probe.js +1 -0
- package/dist/kernel/ports/languages.js +157 -0
- package/dist/kernel/ports/llm.js +3 -1
- package/dist/kernel/ports/model.js +5 -2
- package/dist/kernel/ports/narration-aliases.js +60 -0
- package/dist/kernel/ports/plan-limits.js +36 -0
- package/dist/kernel/runner/attempt.js +18 -9
- package/dist/kernel/runner/audition.js +36 -0
- package/dist/kernel/runner/graph.js +24 -5
- package/dist/kernel/runner/index.js +35 -5
- package/dist/kernel/runner/meter.js +1 -0
- package/dist/kernel/runner/providers.js +164 -9
- package/dist/kernel/runner/retry-policy.js +43 -0
- package/dist/kernel/runner/standalone.js +134 -0
- package/dist/kernel/runner/work-authority.js +28 -2
- package/dist/main.js +321 -20
- package/dist/patch-notes/0.4.2-to-1.0.md +93 -0
- package/dist/patch-notes/1.0-to-2.0.md +74 -0
- package/dist/patch-notes/2.0-to-3.0.md +257 -0
- package/dist/patch-notes/3.0.0.md +70 -0
- package/dist/patch-notes/index.json +26 -0
- package/dist/slices/admission/model.js +35 -0
- package/dist/slices/admission/repo.js +24 -4
- package/dist/slices/admission/rules.js +126 -1
- package/dist/slices/admission/schema.js +68 -1
- package/dist/slices/admission/short-mode.js +47 -0
- package/dist/slices/admission/start.js +90 -16
- package/dist/slices/article/continuation.js +2 -2
- package/dist/slices/article/provided-entries.js +2 -2
- package/dist/slices/article/run.js +6 -2
- package/dist/slices/article/segments.js +3 -3
- package/dist/slices/backups/files.js +135 -0
- package/dist/slices/backups/folder.js +43 -0
- package/dist/slices/backups/model.js +65 -0
- package/dist/slices/backups/repo.js +28 -0
- package/dist/slices/backups/schedule.js +60 -0
- package/dist/slices/backups/service.js +311 -0
- package/dist/slices/batch/index.js +9 -2
- package/dist/slices/cancel/index.js +1 -1
- package/dist/slices/channels/cast-images.js +110 -0
- package/dist/slices/channels/cast-match.js +32 -0
- package/dist/slices/channels/images.js +19 -0
- package/dist/slices/channels/model.js +5 -0
- package/dist/slices/channels/rebrand.js +100 -0
- package/dist/slices/channels/repo.js +218 -0
- package/dist/slices/channels/runs.js +129 -0
- package/dist/slices/channels/schema.js +129 -0
- package/dist/slices/channels/service.js +180 -0
- package/dist/slices/channels/studio-csv.js +110 -0
- package/dist/slices/channels/videos.js +120 -0
- package/dist/slices/checkpoints/recovery.js +10 -0
- package/dist/slices/control/index.js +2 -1
- package/dist/slices/document/fonts.js +1 -1
- package/dist/slices/document/render.js +1 -1
- package/dist/slices/episodes/related.js +90 -0
- package/dist/slices/episodes/repo.js +60 -0
- package/dist/slices/episodes/service.js +58 -0
- package/dist/slices/episodes/summarize.js +170 -0
- package/dist/slices/estimate/index.js +113 -24
- package/dist/slices/estimate/requests.js +60 -2
- package/dist/slices/fixes/rules.js +73 -0
- package/dist/slices/fonts/coverage.js +82 -0
- package/dist/slices/fonts/sfnt.js +74 -0
- package/dist/slices/images/run.js +8 -5
- package/dist/slices/images/scale.js +84 -0
- package/dist/slices/library/diff.js +122 -0
- package/dist/slices/library/history.js +100 -0
- package/dist/slices/library/model.js +3 -0
- package/dist/slices/library/photorealistic.js +49 -0
- package/dist/slices/library/repo.js +19 -14
- package/dist/slices/library/save.js +62 -12
- package/dist/slices/library/slots.js +15 -3
- package/dist/slices/library/snapshot.js +8 -2
- package/dist/slices/library/used-by.js +153 -0
- package/dist/slices/loudness/level-pieces.js +41 -0
- package/dist/slices/loudness/loudnorm.js +164 -0
- package/dist/slices/loudness/model.js +153 -0
- package/dist/slices/model-upkeep/model.js +10 -0
- package/dist/slices/model-upkeep/switch.js +217 -0
- package/dist/slices/model-upkeep/usage.js +183 -0
- package/dist/slices/narration/aliases-library.js +31 -0
- package/dist/slices/narration/aliases-schema.js +53 -0
- package/dist/slices/narration/aliases.js +49 -0
- package/dist/slices/narration/blocks.js +352 -0
- package/dist/slices/narration/chunk.js +19 -7
- package/dist/slices/narration/concat.js +2 -2
- package/dist/slices/narration/describe.js +45 -0
- package/dist/slices/narration/pauses-model.js +63 -0
- package/dist/slices/narration/pauses.js +269 -0
- package/dist/slices/narration/peaks-model.js +1 -0
- package/dist/slices/narration/peaks.js +94 -0
- package/dist/slices/narration/preparation.js +21 -3
- package/dist/slices/narration/pronunciation-chunks.js +3 -3
- package/dist/slices/narration/pronunciation.js +72 -25
- package/dist/slices/narration/run.js +6 -6
- package/dist/slices/narration/shared-glossary.js +1 -1
- package/dist/slices/narration/steering.js +15 -4
- package/dist/slices/notifications/notifier.js +94 -0
- package/dist/slices/notifications/rules.js +102 -0
- package/dist/slices/notifications/send.js +40 -0
- package/dist/slices/notifications/settings.js +34 -0
- package/dist/slices/onboarding/copy.js +143 -0
- package/dist/slices/onboarding/first-run.js +46 -0
- package/dist/slices/onboarding/install.js +119 -0
- package/dist/slices/onboarding/model.js +64 -0
- package/dist/slices/onboarding/packs.js +491 -0
- package/dist/slices/onboarding/quick-short.js +129 -0
- package/dist/slices/onboarding/sample.js +132 -0
- package/dist/slices/onboarding/state.js +68 -0
- package/dist/slices/onboarding/template.js +73 -0
- package/dist/slices/patch-notes/library.js +73 -0
- package/dist/slices/patch-notes/seen.js +77 -0
- package/dist/slices/play-drafts/convert.js +160 -9
- package/dist/slices/play-drafts/readiness.js +3 -0
- package/dist/slices/play-drafts/repo.js +9 -1
- package/dist/slices/play-drafts/review-inputs.js +22 -3
- package/dist/slices/play-drafts/schema.js +109 -2
- package/dist/slices/play-drafts/service.js +10 -1
- package/dist/slices/play-drafts/start.js +43 -18
- package/dist/slices/play-drafts/uploads.js +2 -1
- package/dist/slices/project-templates/from-project.js +70 -4
- package/dist/slices/project-templates/one-off.js +26 -0
- package/dist/slices/project-templates/repo.js +2 -1
- package/dist/slices/project-templates/service.js +15 -4
- package/dist/slices/project-templates/setup.js +29 -2
- package/dist/slices/rebuild/admission-repo.js +1 -1
- package/dist/slices/rebuild/model.js +6 -0
- package/dist/slices/rebuild/preview-details.js +67 -2
- package/dist/slices/rebuild/preview-plan.js +13 -6
- package/dist/slices/rebuild/recipe-audio-parts.js +40 -7
- package/dist/slices/rebuild/recipe-audio.js +66 -12
- package/dist/slices/rebuild/recipe-build.js +15 -5
- package/dist/slices/rebuild/recipe-cast.js +18 -0
- package/dist/slices/rebuild/recipe-checkpoints.js +11 -9
- package/dist/slices/rebuild/recipe-describe.js +93 -0
- package/dist/slices/rebuild/recipe-edit.js +13 -1
- package/dist/slices/rebuild/recipe-exports.js +62 -8
- package/dist/slices/rebuild/recipe-input-schema.js +43 -1
- package/dist/slices/rebuild/recipe-legacy.js +12 -0
- package/dist/slices/rebuild/recipe-loudness.js +45 -0
- package/dist/slices/rebuild/recipe-model.js +25 -6
- package/dist/slices/rebuild/recipe-pauses.js +7 -0
- package/dist/slices/rebuild/recipe-preparation.js +3 -3
- package/dist/slices/rebuild/recipe-provider-choice.js +11 -1
- package/dist/slices/rebuild/recipe-reference.js +58 -0
- package/dist/slices/rebuild/recipe-reviews.js +117 -0
- package/dist/slices/rebuild/recipe-save.js +4 -1
- package/dist/slices/rebuild/recipe-shorts.js +32 -5
- package/dist/slices/rebuild/recipe-text.js +114 -19
- package/dist/slices/rebuild/recipe-validation.js +29 -3
- package/dist/slices/rebuild/recipe-visual.js +104 -29
- package/dist/slices/rebuild/recipe-voices.js +286 -0
- package/dist/slices/rebuild/recipe-work.js +77 -6
- package/dist/slices/rebuild/recipe-youtube.js +2 -0
- package/dist/slices/rebuild/recovery-conflict.js +7 -3
- package/dist/slices/rebuild/recovery-model.js +3 -0
- package/dist/slices/rebuild/recovery-selection.js +28 -2
- package/dist/slices/rebuild/recovery.js +35 -17
- package/dist/slices/rebuild/repo.js +3 -3
- package/dist/slices/rebuild/retry.js +18 -0
- package/dist/slices/rebuild/review-redo.js +110 -0
- package/dist/slices/rebuild/runtime-actions.js +14 -2
- package/dist/slices/rebuild/runtime-admission.js +4 -1
- package/dist/slices/rebuild/runtime-animate.js +4 -4
- package/dist/slices/rebuild/runtime-article.js +5 -5
- package/dist/slices/rebuild/runtime-document.js +4 -4
- package/dist/slices/rebuild/runtime-export-bed.js +43 -0
- package/dist/slices/rebuild/runtime-export-edit.js +70 -11
- package/dist/slices/rebuild/runtime-export-inputs.js +52 -10
- package/dist/slices/rebuild/runtime-export-short.js +85 -0
- package/dist/slices/rebuild/runtime-export.js +84 -19
- package/dist/slices/rebuild/runtime-figure-card.js +87 -0
- package/dist/slices/rebuild/runtime-image.js +69 -0
- package/dist/slices/rebuild/runtime-local.js +154 -27
- package/dist/slices/rebuild/runtime-materialize.js +8 -0
- package/dist/slices/rebuild/runtime-narration-reuse.js +2 -1
- package/dist/slices/rebuild/runtime-narration-text.js +7 -7
- package/dist/slices/rebuild/runtime-pictures.js +28 -0
- package/dist/slices/rebuild/runtime-piece-label.js +29 -0
- package/dist/slices/rebuild/runtime-provider.js +134 -19
- package/dist/slices/rebuild/runtime-publication.js +21 -5
- package/dist/slices/rebuild/runtime-review.js +237 -0
- package/dist/slices/rebuild/runtime-run.js +17 -11
- package/dist/slices/rebuild/runtime-shorts.js +48 -23
- package/dist/slices/rebuild/runtime-store.js +39 -10
- package/dist/slices/rebuild/runtime-subtitles.js +72 -19
- package/dist/slices/rebuild/runtime-voices.js +103 -0
- package/dist/slices/rebuild/runtime-youtube.js +7 -6
- package/dist/slices/rebuild/service-readiness.js +15 -1
- package/dist/slices/rebuild/soften.js +49 -0
- package/dist/slices/rebuild/transition-repo.js +10 -3
- package/dist/slices/research/run.js +4 -4
- package/dist/slices/research/synthesis.js +2 -2
- package/dist/slices/reviews/model.js +105 -0
- package/dist/slices/reviews/outcome.js +22 -0
- package/dist/slices/reviews/repo.js +106 -0
- package/dist/slices/reviews/rules.js +63 -0
- package/dist/slices/reviews/verdict.js +71 -0
- package/dist/slices/reviews/view.js +30 -0
- package/dist/slices/revisions/adopt-content.js +12 -2
- package/dist/slices/revisions/adopt.js +9 -2
- package/dist/slices/revisions/downloads.js +7 -1
- package/dist/slices/revisions/image-plan.js +183 -0
- package/dist/slices/revisions/mutation-assets.js +43 -9
- package/dist/slices/revisions/mutation-cues.js +71 -0
- package/dist/slices/revisions/mutation-prepare.js +8 -10
- package/dist/slices/revisions/mutation-request.js +2 -1
- package/dist/slices/revisions/mutations.js +97 -3
- package/dist/slices/revisions/schema.js +16 -2
- package/dist/slices/run-cost/limits.js +128 -0
- package/dist/slices/run-cost/meter.js +47 -0
- package/dist/slices/run-cost/panel.js +234 -0
- package/dist/slices/run-cost/pricing.js +92 -0
- package/dist/slices/run-cost/week.js +108 -0
- package/dist/slices/schedules/agenda.js +158 -0
- package/dist/slices/schedules/repo.js +58 -18
- package/dist/slices/schedules/scheduler.js +67 -16
- package/dist/slices/schedules/schema.js +138 -1
- package/dist/slices/schedules/service.js +48 -4
- package/dist/slices/schedules/similar.js +90 -0
- package/dist/slices/schedules/topic-list.js +163 -0
- package/dist/slices/schedules/topics.js +422 -0
- package/dist/slices/settings/first-run.js +102 -0
- package/dist/slices/settings/health.js +190 -0
- package/dist/slices/settings/key-guides.js +128 -0
- package/dist/slices/settings/key-test.js +85 -0
- package/dist/slices/settings/model.js +11 -1
- package/dist/slices/settings/playback.js +15 -0
- package/dist/slices/settings/repo.js +17 -3
- package/dist/slices/settings/voices.js +64 -4
- package/dist/slices/settings/whats-new.js +64 -0
- package/dist/slices/shorts/captions.js +9 -0
- package/dist/slices/shorts/pick.js +3 -3
- package/dist/slices/shorts/prompts.js +1 -1
- package/dist/slices/shorts/render.js +54 -5
- package/dist/slices/storage/asset-name.js +3 -0
- package/dist/slices/storage/backup-export.js +48 -9
- package/dist/slices/storage/backup-format.js +38 -1
- package/dist/slices/storage/backup-import.js +191 -19
- package/dist/slices/storage/delete-project.js +7 -4
- package/dist/slices/storage/documents.js +79 -0
- package/dist/slices/storage/files-location.js +531 -0
- package/dist/slices/storage/layout.js +20 -0
- package/dist/slices/storage/model.js +14 -0
- package/dist/slices/storage/portable.js +95 -12
- package/dist/slices/storage/reconcile.js +19 -2
- package/dist/slices/storage/repo.js +4 -1
- package/dist/slices/storage/schema.js +11 -0
- package/dist/slices/storage/trim.js +146 -0
- package/dist/slices/studio/disclosure.js +74 -0
- package/dist/slices/studio/model.js +28 -0
- package/dist/slices/studio/pack.js +209 -0
- package/dist/slices/studio/settings.js +133 -0
- package/dist/slices/style-preview/images.js +61 -0
- package/dist/slices/style-preview/render.js +161 -0
- package/dist/slices/style-preview/schema.js +45 -0
- package/dist/slices/style-preview/service.js +93 -0
- package/dist/slices/style-preview/settings.js +89 -0
- package/dist/slices/subtitles/captions.js +42 -9
- package/dist/slices/subtitles/model.js +22 -0
- package/dist/slices/subtitles/prepare.js +27 -16
- package/dist/slices/subtitles/transcript.js +1 -1
- package/dist/slices/thumbnail/run.js +7 -7
- package/dist/slices/trash/model.js +25 -0
- package/dist/slices/trash/service.js +273 -0
- package/dist/slices/uploads/repo.js +20 -0
- package/dist/slices/video/ambient-bed-schema.js +15 -0
- package/dist/slices/video/ambient-bed.js +95 -0
- package/dist/slices/video/ambient-mix.js +72 -0
- package/dist/slices/video/audio-inputs.js +2 -2
- package/dist/slices/video/cards.js +5 -2
- package/dist/slices/video/edit-list.js +23 -2
- package/dist/slices/video/edit-settings.js +13 -3
- package/dist/slices/video/ffmpeg.js +63 -9
- package/dist/slices/video/figure-card.js +492 -0
- package/dist/slices/video/figure-spans.js +51 -0
- package/dist/slices/video/plan.js +125 -13
- package/dist/slices/video/run.js +14 -12
- package/dist/slices/video/slideshow.js +39 -4
- package/dist/slices/voices/attribution.js +69 -0
- package/dist/slices/voices/audio-files.js +82 -0
- package/dist/slices/voices/audition.js +25 -0
- package/dist/slices/voices/cast.js +56 -0
- package/dist/slices/voices/grouping.js +49 -0
- package/dist/slices/voices/join.js +35 -0
- package/dist/slices/voices/languages.js +25 -0
- package/dist/slices/voices/model.js +179 -0
- package/dist/slices/voices/palette.js +26 -0
- package/dist/slices/voices/panel.js +91 -0
- package/dist/slices/voices/portraits.js +55 -0
- package/dist/slices/voices/script.js +136 -0
- package/dist/slices/voices/timing.js +58 -0
- package/dist/slices/youtube/answer.js +1 -1
- package/dist/slices/youtube/chapters.js +126 -0
- package/dist/slices/youtube/edits-repo.js +69 -0
- package/dist/slices/youtube/edits.js +89 -0
- package/dist/slices/youtube/placeholders.js +98 -0
- package/dist/slices/youtube/transcript.js +11 -4
- package/dist/updater/service.js +66 -21
- package/dist/web/assets/index-CUA2sUxU.css +1 -0
- package/dist/web/assets/index-CX590kE-.js +303 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
- package/dist/web/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
- package/dist/web/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
- package/dist/web/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
- package/dist/web/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
- package/dist/web/assets/{pdf-CAJNT254.js → pdf-BG8n_uc5.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +4 -4
- package/dist/edge/docker-launch.js +0 -81
- package/dist/edge/docker-projects/activation.js +0 -35
- package/dist/edge/docker-projects/claims.js +0 -85
- package/dist/edge/docker-projects/committed.js +0 -40
- package/dist/edge/docker-projects/engine.js +0 -461
- package/dist/edge/docker-projects/install.js +0 -254
- package/dist/edge/docker-projects/prune.js +0 -66
- package/dist/edge/docker-projects/recover.js +0 -100
- package/dist/edge/docker-projects/state.js +0 -225
- package/dist/web/assets/index-BGgYMm2u.css +0 -1
- package/dist/web/assets/index-BVVgtntL.js +0 -171
- package/scripts/docker-run.sh +0 -3
- /package/dist/edge/{docker-projects → docker-install}/tree.js +0 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { imageBlob } from "../channels/repo.js";
|
|
4
|
+
import { outputPath } from "../storage/layout.js";
|
|
5
|
+
// The provider call an image step makes: its request, the effort when one is chosen and, while
|
|
6
|
+
// the project's Establishing image is on, that image's bytes as the visual reference.
|
|
7
|
+
export function imageCall(deps, projectId, input, previewLabel) {
|
|
8
|
+
const reference = referenceImage(deps, projectId, input);
|
|
9
|
+
const cast = castImages(deps, input);
|
|
10
|
+
return {
|
|
11
|
+
provider: input.provider,
|
|
12
|
+
model: input.model,
|
|
13
|
+
prompt: input.prompt,
|
|
14
|
+
aspect: input.aspect,
|
|
15
|
+
...(input.thinking === undefined ? {} : { thinking: input.thinking }),
|
|
16
|
+
...(reference === undefined ? {} : { reference }),
|
|
17
|
+
...(cast === undefined ? {} : { cast }),
|
|
18
|
+
...(previewLabel === undefined ? {} : { previewLabel }),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function referenceImage(deps, projectId, input) {
|
|
22
|
+
if (input.reference === undefined)
|
|
23
|
+
return undefined;
|
|
24
|
+
// The step waits for the establishing image, so a missing one went away since.
|
|
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.";
|
|
26
|
+
if (input.reference.assetId === null)
|
|
27
|
+
throw new Error(missing);
|
|
28
|
+
const row = z
|
|
29
|
+
.object({ path: z.string() })
|
|
30
|
+
.optional()
|
|
31
|
+
.parse(deps.db
|
|
32
|
+
.prepare("SELECT path FROM project_assets WHERE id=? AND project_id=?")
|
|
33
|
+
.get(input.reference.assetId, projectId));
|
|
34
|
+
if (row === undefined)
|
|
35
|
+
throw new Error(missing);
|
|
36
|
+
let bytes;
|
|
37
|
+
try {
|
|
38
|
+
bytes = new Uint8Array(readFileSync(outputPath(deps.paths, projectId, row.path)));
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
throw new Error(missing);
|
|
42
|
+
}
|
|
43
|
+
const mime = imageMime(bytes);
|
|
44
|
+
if (mime === undefined)
|
|
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.");
|
|
46
|
+
return { bytes, mime };
|
|
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
|
+
}
|
|
64
|
+
function imageMime(bytes) {
|
|
65
|
+
const png = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
|
|
66
|
+
if (png.every((value, index) => bytes[index] === value))
|
|
67
|
+
return "image/png";
|
|
68
|
+
return bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff ? "image/jpeg" : undefined;
|
|
69
|
+
}
|
|
@@ -1,11 +1,18 @@
|
|
|
1
|
-
import { statSync } from "node:fs";
|
|
1
|
+
import { rmSync, statSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
+
import { referenceKey } from "../admission/model.js";
|
|
4
5
|
import { plainText } from "../article/plain.js";
|
|
5
6
|
import { splitEndMatter } from "../article/split.js";
|
|
7
|
+
import { levelPieces } from "../loudness/level-pieces.js";
|
|
6
8
|
import { joinNarration } from "../narration/concat.js";
|
|
9
|
+
import { applyPauses, planPieces } from "../narration/pauses.js";
|
|
7
10
|
import { allocateAsset, discardPreparedAssets, sealAsset } from "../storage/assets.js";
|
|
8
11
|
import { outputPath } from "../storage/layout.js";
|
|
12
|
+
import { probeDurationMs, runFfmpeg } from "../video/ffmpeg.js";
|
|
13
|
+
import { turnJoinArgs } from "../voices/join.js";
|
|
14
|
+
import { levelOperation } from "./recipe-loudness.js";
|
|
15
|
+
import { executeFigureCard } from "./runtime-figure-card.js";
|
|
9
16
|
import { publishNarrationText } from "./runtime-narration-text.js";
|
|
10
17
|
import { executionPlan, executionView, savedCatalogue } from "./runtime-plan.js";
|
|
11
18
|
import { preparedResult, preparedText, preparedTexts, publishResult, } from "./runtime-publication.js";
|
|
@@ -19,7 +26,7 @@ export async function executeLocalRecipe(deps, context, piece) {
|
|
|
19
26
|
const input = piece.input;
|
|
20
27
|
if (input.kind === "provided") {
|
|
21
28
|
if (input.assetId === null)
|
|
22
|
-
throw new Error("A file you uploaded for this project is missing. Upload it again in Edit project, then
|
|
29
|
+
throw new Error("A file you uploaded for this project is missing. Upload it again in Edit project, then Try again.");
|
|
23
30
|
const row = z
|
|
24
31
|
.object({ id: z.string(), project_id: z.string(), path: z.string(), created_at: z.string() })
|
|
25
32
|
.parse(deps.db
|
|
@@ -36,19 +43,25 @@ export async function executeLocalRecipe(deps, context, piece) {
|
|
|
36
43
|
? "audio_body"
|
|
37
44
|
: piece.key === "thumbnail:image"
|
|
38
45
|
? "thumbnail"
|
|
39
|
-
: piece.key
|
|
40
|
-
? "
|
|
41
|
-
:
|
|
46
|
+
: piece.key === referenceKey
|
|
47
|
+
? "reference"
|
|
48
|
+
: piece.key.startsWith("image:")
|
|
49
|
+
? "image"
|
|
50
|
+
: undefined;
|
|
42
51
|
await publishResult(deps, context, piece, role === undefined ? [] : [preparedResult(deps, context, piece, role, asset)], {}, asset);
|
|
43
52
|
return "done";
|
|
44
53
|
}
|
|
45
54
|
if (input.kind !== "local")
|
|
46
|
-
throw new Error("Slopify hit an internal error (a local step was set up wrongly).
|
|
55
|
+
throw new Error("Slopify hit an internal error (a local step was set up wrongly). Try again; if it happens again, use Download diagnostics in Settings and report it.");
|
|
56
|
+
if (input.operation === "figure-card-v1")
|
|
57
|
+
return executeFigureCard(deps, context, piece);
|
|
47
58
|
if (input.operation === "narration-files-v1") {
|
|
48
59
|
await publishNarrationText(deps, context, piece);
|
|
49
60
|
return "done";
|
|
50
61
|
}
|
|
51
|
-
if (input.operation === "concat-narration"
|
|
62
|
+
if (input.operation === "concat-narration" ||
|
|
63
|
+
input.operation === "concat-turns-v1" ||
|
|
64
|
+
input.operation === levelOperation) {
|
|
52
65
|
await concatenate(deps, context, piece);
|
|
53
66
|
return "done";
|
|
54
67
|
}
|
|
@@ -75,19 +88,19 @@ export async function executeLocalRecipe(deps, context, piece) {
|
|
|
75
88
|
});
|
|
76
89
|
return "done";
|
|
77
90
|
}
|
|
78
|
-
throw new Error(`Slopify hit an internal error (no handler for the step "${input.operation}").
|
|
91
|
+
throw new Error(`Slopify hit an internal error (no handler for the step "${input.operation}"). Try again; if it happens again, use Download diagnostics in Settings and report it.`);
|
|
79
92
|
}
|
|
80
93
|
async function concatenate(deps, context, piece) {
|
|
81
94
|
const view = executionView(deps, context.work.projectId, context.work.revisionId);
|
|
82
95
|
if (view === undefined)
|
|
83
|
-
throw new Error("Slopify hit an internal error (the project version being built is missing).
|
|
96
|
+
throw new Error("Slopify hit an internal error (the project version being built is missing). Try again; if it happens again, use Download diagnostics in Settings and report it.");
|
|
84
97
|
const row = deps.db
|
|
85
98
|
.prepare("SELECT recipe_context FROM revision_work WHERE id=?")
|
|
86
99
|
.get(context.work.workId);
|
|
87
100
|
const plan = executionPlan(deps, view, savedCatalogue(row?.recipe_context));
|
|
88
101
|
const recipe = plan.recipes.find((candidate) => candidate.key === piece.key && candidate.fingerprint === piece.fingerprint);
|
|
89
102
|
if (recipe === undefined)
|
|
90
|
-
throw new Error("Slopify hit an internal error (the narration chunks to join no longer match the project's saved inputs).
|
|
103
|
+
throw new Error("Slopify hit an internal error (the narration chunks to join no longer match the project's saved inputs). Try again; if it happens again, use Download diagnostics in Settings and report it.");
|
|
91
104
|
const files = recipe.dependsOn.map((key) => {
|
|
92
105
|
const input = view.pieces.find((candidate) => candidate.key === key &&
|
|
93
106
|
candidate.selected &&
|
|
@@ -95,36 +108,73 @@ async function concatenate(deps, context, piece) {
|
|
|
95
108
|
candidate.piece.state === "done" &&
|
|
96
109
|
candidate.fingerprint === plan.recipes.find((value) => value.key === key)?.fingerprint);
|
|
97
110
|
if (input?.assetId === null || input === undefined)
|
|
98
|
-
throw new Error("One narration chunk has no saved audio, so the narration can't be joined together. Regenerate the missing chunk in Edit project → Narration, then
|
|
111
|
+
throw new Error("One narration chunk has no saved audio, so the narration can't be joined together. Regenerate the missing chunk in Edit project → Narration, then Try again.");
|
|
99
112
|
const asset = deps.db
|
|
100
113
|
.prepare("SELECT path FROM project_assets WHERE id=? AND project_id=?")
|
|
101
114
|
.get(input.assetId, context.work.projectId);
|
|
102
115
|
return outputPath(deps.paths, context.work.projectId, z.string().parse(asset?.path));
|
|
103
116
|
});
|
|
117
|
+
// Level the volume repeats the join its recipe names, from the same pieces, each levelled
|
|
118
|
+
// first (`recipe-loudness.ts`).
|
|
119
|
+
const level = recipe.input.kind === "local" && recipe.input.operation === levelOperation;
|
|
120
|
+
const [operation, values] = recipe.input.kind !== "local"
|
|
121
|
+
? ["", undefined]
|
|
122
|
+
: level
|
|
123
|
+
? z.tuple([z.string(), z.unknown(), z.unknown()]).parse(recipe.input.values)
|
|
124
|
+
: [recipe.input.operation, recipe.input.values];
|
|
104
125
|
const pending = allocateAsset(deps, context.work.projectId, "narration.mp3");
|
|
126
|
+
const piecesDirectory = join(dirname(pending.absolutePath), "levelled");
|
|
105
127
|
try {
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
128
|
+
const run = { bin: deps.ffmpeg, log: deps.log, signal: context.signal };
|
|
129
|
+
const levelled = level ? await levelPieces(run, files, piecesDirectory) : undefined;
|
|
130
|
+
// Pauses between sentences: planned from the pieces as spoken, made in what is joined.
|
|
131
|
+
const pauses = pausesIn(values);
|
|
132
|
+
const paced = pauses === undefined
|
|
133
|
+
? undefined
|
|
134
|
+
: await applyPauses(run, levelled?.files ?? files, paceScaled(await planPieces(run, files.map((file, at) => ({
|
|
135
|
+
file,
|
|
136
|
+
text: pieceText(plan.recipes.find((one) => one.key === recipe.dependsOn[at])),
|
|
137
|
+
next: pauses.next[at] ?? "end",
|
|
138
|
+
})), pauses.settings), operation === "concat-turns-v1" ? values : undefined), join(piecesDirectory, "paced"));
|
|
139
|
+
const joined = paced?.files ?? levelled?.files ?? files;
|
|
140
|
+
const durationMs = operation === "concat-turns-v1"
|
|
141
|
+
? await joinTurns(run, joined, values, pending.absolutePath)
|
|
142
|
+
: await joinNarration({ bin: deps.ffmpeg, log: deps.log }, {
|
|
143
|
+
files: joined,
|
|
144
|
+
output: pending.absolutePath,
|
|
145
|
+
listPath: join(dirname(pending.absolutePath), "parts.txt"),
|
|
146
|
+
signal: context.signal,
|
|
147
|
+
reencode: level || paced !== undefined,
|
|
148
|
+
});
|
|
149
|
+
rmSync(piecesDirectory, { recursive: true, force: true });
|
|
112
150
|
context.signal.throwIfAborted();
|
|
113
151
|
const asset = sealAsset(deps, pending);
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
152
|
+
const segment = /^(?:audio|level):(intro|outro)$/.exec(piece.key)?.[1] ?? "body";
|
|
153
|
+
const role = level
|
|
154
|
+
? "audio_levelled"
|
|
155
|
+
: piece.key === "audio:intro"
|
|
156
|
+
? "audio_intro"
|
|
157
|
+
: piece.key === "audio:outro"
|
|
158
|
+
? "audio_outro"
|
|
159
|
+
: "audio_body";
|
|
160
|
+
const voice = view.revision.config.audio === undefined
|
|
161
|
+
? {}
|
|
162
|
+
: {
|
|
163
|
+
provider: view.revision.config.audio.provider,
|
|
164
|
+
model: view.revision.config.audio.model,
|
|
165
|
+
voice: view.revision.config.audio.voice,
|
|
166
|
+
};
|
|
119
167
|
await publishResult(deps, context, piece, [
|
|
120
|
-
preparedResult(deps, context, piece, role, asset, durationMs,
|
|
121
|
-
?
|
|
168
|
+
preparedResult(deps, context, piece, role, asset, durationMs, levelled === undefined
|
|
169
|
+
? voice
|
|
122
170
|
: {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
171
|
+
...voice,
|
|
172
|
+
segment: segment,
|
|
173
|
+
loudness: levelled.report,
|
|
126
174
|
}),
|
|
127
175
|
], { durationMs }, asset);
|
|
176
|
+
if (level)
|
|
177
|
+
return;
|
|
128
178
|
for (const key of recipe.dependsOn) {
|
|
129
179
|
const retained = view.pieces.find((candidate) => candidate.key === key && candidate.selected && candidate.available);
|
|
130
180
|
if (retained?.publicationId === null || retained === undefined)
|
|
@@ -137,6 +187,83 @@ async function concatenate(deps, context, piece) {
|
|
|
137
187
|
}
|
|
138
188
|
}
|
|
139
189
|
finally {
|
|
190
|
+
rmSync(piecesDirectory, { recursive: true, force: true });
|
|
140
191
|
discardPreparedAssets(deps, [pending]);
|
|
141
192
|
}
|
|
142
193
|
}
|
|
194
|
+
// The pauses a join's values ask for (`recipe-pauses.ts`): its third value, when there is one.
|
|
195
|
+
function pausesIn(values) {
|
|
196
|
+
const value = Array.isArray(values) ? values[2] : undefined;
|
|
197
|
+
const parsed = z
|
|
198
|
+
.tuple([
|
|
199
|
+
z.literal("pauses-v1"),
|
|
200
|
+
z.number(),
|
|
201
|
+
z.number(),
|
|
202
|
+
z.array(z.enum(["end", "turn", "sentence", "paragraph"])),
|
|
203
|
+
])
|
|
204
|
+
.safeParse(value);
|
|
205
|
+
if (!parsed.success)
|
|
206
|
+
return undefined;
|
|
207
|
+
const [, sentenceSeconds, paragraphSeconds, next] = parsed.data;
|
|
208
|
+
return { settings: { sentenceSeconds, paragraphSeconds }, next };
|
|
209
|
+
}
|
|
210
|
+
// A speaker's pace plays their turn faster or slower, pauses included, so a turn's added
|
|
211
|
+
// silence is scaled by it to come out at the minimum.
|
|
212
|
+
function paceScaled(plans, turnValues) {
|
|
213
|
+
const layout = Array.isArray(turnValues) ? turnValues[1] : undefined;
|
|
214
|
+
if (!Array.isArray(layout))
|
|
215
|
+
return plans;
|
|
216
|
+
return plans.map((plan, at) => {
|
|
217
|
+
const pace = z.tuple([z.number().positive(), z.number()]).safeParse(layout[at]);
|
|
218
|
+
const factor = pace.success ? pace.data[0] : 1;
|
|
219
|
+
if (factor === 1)
|
|
220
|
+
return plan;
|
|
221
|
+
return {
|
|
222
|
+
...plan,
|
|
223
|
+
padEnd: plan.padEnd * factor,
|
|
224
|
+
pauses: {
|
|
225
|
+
...plan.pauses,
|
|
226
|
+
inserts: plan.pauses.inserts.map((one) => ({ ...one, seconds: one.seconds * factor })),
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
// The words a narration piece speaks, where its sentences end: the clean words of a request, or
|
|
232
|
+
// of a piece supplied as a file.
|
|
233
|
+
function pieceText(recipe) {
|
|
234
|
+
if (recipe === undefined)
|
|
235
|
+
return "";
|
|
236
|
+
const input = recipe.input;
|
|
237
|
+
// The words as said: a prepared request's delivery tags take no time of their own.
|
|
238
|
+
if (input.kind === "tts")
|
|
239
|
+
return (input.spokenText ?? input.text).replace(/\[[^\]]*\]/g, "");
|
|
240
|
+
if (input.kind === "provided" &&
|
|
241
|
+
Array.isArray(input.semantic) &&
|
|
242
|
+
typeof input.semantic[0] === "string")
|
|
243
|
+
return input.semantic[0];
|
|
244
|
+
return "";
|
|
245
|
+
}
|
|
246
|
+
// A multi-voice narration: every turn at its speaker's pace with the gap between turns, as the
|
|
247
|
+
// recipe laid it out (`recipe-voices.ts`), measured off the file that was written. `values` is
|
|
248
|
+
// the concat-turns-v1 recipe's own values.
|
|
249
|
+
export async function joinTurns(run, files, values, output) {
|
|
250
|
+
const layout = z
|
|
251
|
+
.tuple([z.unknown(), z.array(z.tuple([z.number().positive(), z.number().nonnegative()]))])
|
|
252
|
+
// The pauses between sentences may follow (`recipe-pauses.ts`); they were made already.
|
|
253
|
+
.rest(z.unknown())
|
|
254
|
+
.parse(values)[1];
|
|
255
|
+
if (layout.length !== files.length || files.length === 0)
|
|
256
|
+
throw new Error("Slopify hit an internal error (the speaker turns to join don't match their audio). Try again; if it happens again, use Download diagnostics in Settings and report it.");
|
|
257
|
+
await runFfmpeg({
|
|
258
|
+
bin: run.bin,
|
|
259
|
+
args: turnJoinArgs(files.map((path, index) => ({
|
|
260
|
+
path,
|
|
261
|
+
pace: layout[index]?.[0] ?? 1,
|
|
262
|
+
gapAfter: layout[index]?.[1] ?? 0,
|
|
263
|
+
})), output),
|
|
264
|
+
signal: run.signal,
|
|
265
|
+
log: run.log,
|
|
266
|
+
onProgress: () => { },
|
|
267
|
+
});
|
|
268
|
+
return probeDurationMs(run.bin, output, run.signal, run.log);
|
|
269
|
+
}
|
|
@@ -136,6 +136,10 @@ function futureKey(recipe) {
|
|
|
136
136
|
return `narration:prepare:${recipe.input.preparation.segment}:future`;
|
|
137
137
|
if (recipe.input.kind === "tts")
|
|
138
138
|
return `audio:${recipe.input.segment}:future`;
|
|
139
|
+
// The descriptions unfold from the one step that stood in for them while the text was
|
|
140
|
+
// unwritten.
|
|
141
|
+
if (recipe.key.startsWith("narration:describe:") || recipe.key.startsWith("figure:card:"))
|
|
142
|
+
return "narration:describe:future";
|
|
139
143
|
if (recipe.key.startsWith("research:chapter:"))
|
|
140
144
|
return "research:planner";
|
|
141
145
|
if (recipe.key === "article:continuation")
|
|
@@ -143,6 +147,10 @@ function futureKey(recipe) {
|
|
|
143
147
|
// A short's images and render unfold from its image prompts, admitted with them: under the
|
|
144
148
|
// pick on a first run, or on their own when one short is made again and the pick is kept.
|
|
145
149
|
// The prompts unfold from the pick's answer.
|
|
150
|
+
// A short's review unfolds with the short it reviews.
|
|
151
|
+
const reviewed = /^review:(shorts:\d+)$/.exec(recipe.key);
|
|
152
|
+
if (reviewed?.[1] !== undefined)
|
|
153
|
+
return `${reviewed[1]}:prompts`;
|
|
146
154
|
const clip = /^(shorts:\d+):(?:image:\d+|render)$/.exec(recipe.key);
|
|
147
155
|
if (clip?.[1] !== undefined)
|
|
148
156
|
return `${clip[1]}:prompts`;
|
|
@@ -3,7 +3,8 @@ import { insertManifestOutput, insertManifestPiece, selectOutputRecord, selectPi
|
|
|
3
3
|
import { retainedNarrationPieces } from "./narration-history.js";
|
|
4
4
|
import { matchingNarrationPiece } from "./narration-reuse.js";
|
|
5
5
|
export function bindNarrationReuse(deps, view, recipe, ordinal) {
|
|
6
|
-
if (recipe.input.kind === "local" &&
|
|
6
|
+
if (recipe.input.kind === "local" &&
|
|
7
|
+
(recipe.input.operation === "concat-narration" || recipe.input.operation === "concat-turns-v1")) {
|
|
7
8
|
const completed = view.pieces.find((one) => one.available &&
|
|
8
9
|
one.piece.state === "done" &&
|
|
9
10
|
one.key === recipe.key &&
|