@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,6 +1,6 @@
|
|
|
1
1
|
import { chapterCuts, narrationShotFrames } from "./cuts.js";
|
|
2
2
|
import { editListVersion, } from "./edit-list.js";
|
|
3
|
-
import { chapterCardSeconds } from "./edit-settings.js";
|
|
3
|
+
import { chapterCardSeconds, endScreenSeconds } from "./edit-settings.js";
|
|
4
4
|
import { hasLook } from "./look.js";
|
|
5
5
|
import { motionFor } from "./motion.js";
|
|
6
6
|
import { withTransitions } from "./transitions.js";
|
|
@@ -31,13 +31,21 @@ export function decimal(thousandths) {
|
|
|
31
31
|
const whole = String(Math.floor(thousandths / 1000));
|
|
32
32
|
return fraction === "" ? whole : `${whole}.${fraction}`;
|
|
33
33
|
}
|
|
34
|
+
// ceiling: the owner's eye. A card comes up a moment before its description starts and stays
|
|
35
|
+
// a moment after it ends; a gap between two cards, or before the first or after the last,
|
|
36
|
+
// shorter than a second is given to the card rather than flashing an image.
|
|
37
|
+
export const figureLeadSeconds = 0.3;
|
|
38
|
+
export const figureTailSeconds = 0.3;
|
|
39
|
+
const figureGapSeconds = 1;
|
|
34
40
|
export function planRender(input) {
|
|
35
41
|
if (input.images.length === 0) {
|
|
36
42
|
// Admission permits Video only with an image source, so an empty set is a bug upstream.
|
|
37
|
-
throw new Error("The video needs at least one image, but there are none. Check the Images section in Edit project, then
|
|
43
|
+
throw new Error("The video needs at least one image, but there are none. Check the Images section in Edit project, then Try again.");
|
|
38
44
|
}
|
|
39
45
|
const frame = frames[input.format];
|
|
40
|
-
const
|
|
46
|
+
const bed = input.body === undefined ? undefined : input.bed;
|
|
47
|
+
const timeline = input.body === undefined ? [] : audioTimeline({ ...input, body: input.body });
|
|
48
|
+
const audio = bed === undefined ? timeline : withTail(timeline, bed.tailSeconds);
|
|
41
49
|
// A silent video shows every image once, which is the one length it has to go on.
|
|
42
50
|
const totalSeconds = input.body === undefined
|
|
43
51
|
? input.images.length * input.imageSeconds
|
|
@@ -45,20 +53,25 @@ export function planRender(input) {
|
|
|
45
53
|
const totalFrames = Math.max(1, Math.round(totalSeconds * fps));
|
|
46
54
|
const edit = input.edit;
|
|
47
55
|
const narration = input.body === undefined ? undefined : edit?.narration;
|
|
48
|
-
const
|
|
49
|
-
? everyLengths(Math.max(1, Math.round(input.imageSeconds * fps)),
|
|
56
|
+
const lengthsFor = (from, frameCount) => narration === undefined
|
|
57
|
+
? everyLengths(Math.max(1, Math.round(input.imageSeconds * fps)), frameCount)
|
|
50
58
|
: narrationShotFrames({
|
|
51
|
-
totalFrames,
|
|
59
|
+
totalFrames: frameCount,
|
|
52
60
|
fps,
|
|
53
61
|
imageSeconds: input.imageSeconds,
|
|
54
|
-
cutPoints: narration.cutPoints,
|
|
55
|
-
chapterStarts: narration.chapterStarts
|
|
62
|
+
cutPoints: narration.cutPoints.map((point) => point - from / fps),
|
|
63
|
+
chapterStarts: narration.chapterStarts
|
|
64
|
+
.map((start) => start - from / fps)
|
|
65
|
+
.filter((start) => start >= 0 && start < frameCount / fps),
|
|
56
66
|
});
|
|
57
|
-
const
|
|
67
|
+
const figures = figureFrames(edit?.figures ?? [], totalFrames);
|
|
68
|
+
const planned = figures.length === 0
|
|
69
|
+
? shots(input, lengthsFor(0, totalFrames))
|
|
70
|
+
: aroundFigures(input, figures, totalFrames, lengthsFor);
|
|
58
71
|
const transition = edit?.transition;
|
|
59
72
|
const cards = edit?.cards === undefined
|
|
60
73
|
? []
|
|
61
|
-
: chapterCards(edit.cards.chapters, narration?.cutPoints ?? [], totalFrames);
|
|
74
|
+
: withEndScreen(chapterCards(edit.cards.chapters, narration?.cutPoints ?? [], totalFrames), edit.cards.endScreen, totalFrames);
|
|
62
75
|
return {
|
|
63
76
|
gapSeconds: input.gapSeconds,
|
|
64
77
|
edgeSeconds: input.edgeSeconds,
|
|
@@ -76,14 +89,45 @@ export function planRender(input) {
|
|
|
76
89
|
: withTransitions(planned, transition.kind, Math.round(transition.seconds * fps)),
|
|
77
90
|
...(edit?.look !== undefined && hasLook(edit.look) ? { look: edit.look } : {}),
|
|
78
91
|
...(cards.length > 0 && edit?.cards !== undefined
|
|
79
|
-
? {
|
|
92
|
+
? {
|
|
93
|
+
cards,
|
|
94
|
+
cardFont: edit.cards.font,
|
|
95
|
+
...(edit.cards.color === undefined ? {} : { cardColor: edit.cards.color }),
|
|
96
|
+
}
|
|
80
97
|
: {}),
|
|
98
|
+
...(bed === undefined
|
|
99
|
+
? {}
|
|
100
|
+
: {
|
|
101
|
+
bed: {
|
|
102
|
+
source: bed.source,
|
|
103
|
+
levelDb: bed.levelDb,
|
|
104
|
+
fadeInSeconds: bed.fadeInSeconds,
|
|
105
|
+
fadeOutAt: narrationEnd(timeline),
|
|
106
|
+
fadeOutSeconds: bed.tailSeconds,
|
|
107
|
+
},
|
|
108
|
+
}),
|
|
81
109
|
},
|
|
82
110
|
totalFrames,
|
|
83
111
|
totalSeconds,
|
|
84
112
|
output: input.output,
|
|
85
113
|
};
|
|
86
114
|
}
|
|
115
|
+
// The timeline with the ambient bed's tail: the silence after the last spoken segment runs at
|
|
116
|
+
// least `tail` seconds, so the bed can fade out under the end of the picture. A tail no longer
|
|
117
|
+
// than that silence changes nothing, so the video keeps its length.
|
|
118
|
+
export function withTail(audio, tail) {
|
|
119
|
+
const last = audio.at(-1);
|
|
120
|
+
if (last === undefined || tail <= 0)
|
|
121
|
+
return audio;
|
|
122
|
+
if (last.kind === "edge")
|
|
123
|
+
return last.seconds >= tail ? audio : [...audio.slice(0, -1), { ...last, seconds: tail }];
|
|
124
|
+
return [...audio, { kind: "edge", path: null, seconds: tail }];
|
|
125
|
+
}
|
|
126
|
+
// Where the last spoken segment ends, in seconds from the start of the video.
|
|
127
|
+
function narrationEnd(audio) {
|
|
128
|
+
const trailing = audio.at(-1)?.kind === "edge" ? (audio.at(-1)?.seconds ?? 0) : 0;
|
|
129
|
+
return audio.reduce((sum, segment) => sum + segment.seconds, 0) - trailing;
|
|
130
|
+
}
|
|
87
131
|
// A card at each chapter start, on the cut the chapter's shot starts with (a chapter at the
|
|
88
132
|
// very start stays there), for 2.5 s or what is left of the video.
|
|
89
133
|
function chapterCards(chapters, cutPoints, totalFrames) {
|
|
@@ -104,6 +148,22 @@ function chapterCards(chapters, cutPoints, totalFrames) {
|
|
|
104
148
|
}
|
|
105
149
|
return cards;
|
|
106
150
|
}
|
|
151
|
+
// The end screen over the last seconds (or the whole of a shorter video). A chapter card that
|
|
152
|
+
// would still be showing is cut short where the end screen starts; one starting inside it is
|
|
153
|
+
// left out.
|
|
154
|
+
export function withEndScreen(cards, text, totalFrames) {
|
|
155
|
+
const title = text?.trim() ?? "";
|
|
156
|
+
if (title === "")
|
|
157
|
+
return cards;
|
|
158
|
+
const frames = Math.min(totalFrames, Math.round(endScreenSeconds * fps));
|
|
159
|
+
const startFrame = totalFrames - frames;
|
|
160
|
+
return [
|
|
161
|
+
...cards.flatMap((card) => card.startFrame >= startFrame
|
|
162
|
+
? []
|
|
163
|
+
: [{ ...card, frames: Math.min(card.frames, startFrame - card.startFrame) }]),
|
|
164
|
+
{ title, startFrame, frames },
|
|
165
|
+
];
|
|
166
|
+
}
|
|
107
167
|
export function spoken(kind) {
|
|
108
168
|
return kind !== "gap" && kind !== "edge";
|
|
109
169
|
}
|
|
@@ -148,8 +208,9 @@ function everyLengths(each, totalFrames) {
|
|
|
148
208
|
// the timeline is full. The motion goes by the shot's place, not the image (`motion.ts`), so
|
|
149
209
|
// an image that comes round again may move another way. A moving clip in an image's place
|
|
150
210
|
// plays as it is, with no motion of its own.
|
|
151
|
-
function shots(input, lengths) {
|
|
152
|
-
return lengths.map((frames,
|
|
211
|
+
function shots(input, lengths, first = 0) {
|
|
212
|
+
return lengths.map((frames, place) => {
|
|
213
|
+
const at = first + place;
|
|
153
214
|
const index = at % input.images.length;
|
|
154
215
|
const clip = input.edit?.clips?.[index];
|
|
155
216
|
return clip === undefined
|
|
@@ -161,3 +222,54 @@ function shots(input, lengths) {
|
|
|
161
222
|
: { source: clip, frames, motion: { kind: "still" } };
|
|
162
223
|
});
|
|
163
224
|
}
|
|
225
|
+
// The cards on the frame grid, in order, with the lead-in and tail added, a card that would
|
|
226
|
+
// overlap the one before it starting where that one ends, and gaps too short for an image
|
|
227
|
+
// given to the card beside them.
|
|
228
|
+
export function figureFrames(figures, totalFrames) {
|
|
229
|
+
const gap = Math.round(figureGapSeconds * fps);
|
|
230
|
+
const placed = [];
|
|
231
|
+
for (const figure of [...figures].toSorted((a, b) => a.start - b.start)) {
|
|
232
|
+
const previous = placed.at(-1);
|
|
233
|
+
const from = Math.max(previous?.to ?? 0, Math.round(Math.max(0, figure.start - figureLeadSeconds) * fps));
|
|
234
|
+
const to = Math.min(totalFrames, Math.round((figure.end + figureTailSeconds) * fps));
|
|
235
|
+
if (to - from < 1)
|
|
236
|
+
continue;
|
|
237
|
+
if (previous !== undefined && from - previous.to < gap)
|
|
238
|
+
previous.to = from;
|
|
239
|
+
placed.push({ path: figure.path, from, to });
|
|
240
|
+
}
|
|
241
|
+
const first = placed[0];
|
|
242
|
+
if (first !== undefined && first.from < gap)
|
|
243
|
+
first.from = 0;
|
|
244
|
+
const last = placed.at(-1);
|
|
245
|
+
if (last !== undefined && totalFrames - last.to < gap)
|
|
246
|
+
last.to = totalFrames;
|
|
247
|
+
return placed.map((one) => ({ path: one.path, startFrame: one.from, frames: one.to - one.from }));
|
|
248
|
+
}
|
|
249
|
+
// The images take turns in the stretches between the cards, laid as they would be over a
|
|
250
|
+
// whole video of that length, and keep counting across them so no image or motion repeats
|
|
251
|
+
// where a card interrupts. Each card is shown still, whole.
|
|
252
|
+
function aroundFigures(input, figures, totalFrames, lengthsFor) {
|
|
253
|
+
const out = [];
|
|
254
|
+
let cursor = 0;
|
|
255
|
+
let slot = 0;
|
|
256
|
+
const stretch = (to) => {
|
|
257
|
+
if (to <= cursor)
|
|
258
|
+
return;
|
|
259
|
+
const lengths = lengthsFor(cursor, to - cursor);
|
|
260
|
+
out.push(...shots(input, lengths, slot));
|
|
261
|
+
slot += lengths.length;
|
|
262
|
+
cursor = to;
|
|
263
|
+
};
|
|
264
|
+
for (const figure of figures) {
|
|
265
|
+
stretch(figure.startFrame);
|
|
266
|
+
out.push({
|
|
267
|
+
source: { kind: "image", path: figure.path },
|
|
268
|
+
frames: figure.frames,
|
|
269
|
+
motion: { kind: "still" },
|
|
270
|
+
});
|
|
271
|
+
cursor = figure.startFrame + figure.frames;
|
|
272
|
+
}
|
|
273
|
+
stretch(totalFrames);
|
|
274
|
+
return out;
|
|
275
|
+
}
|
package/dist/slices/video/run.js
CHANGED
|
@@ -16,7 +16,7 @@ export async function renderVideo(deps, context) {
|
|
|
16
16
|
throw new Error("This project no longer exists; it may have been deleted while it was running.");
|
|
17
17
|
if (project.config.sources.video === "off") {
|
|
18
18
|
if (project.config.sources.audio === "off")
|
|
19
|
-
throw new Error("There is nothing to export because both video and narration are turned off for this project. Turn one on in Edit project, then
|
|
19
|
+
throw new Error("There is nothing to export because both video and narration are turned off for this project. Turn one on in Edit project, then Try again.");
|
|
20
20
|
await exportAudioWav(deps, context, project.config.silenceGapSeconds, project.config.edgeSilenceSeconds);
|
|
21
21
|
return;
|
|
22
22
|
}
|
|
@@ -44,17 +44,19 @@ export async function renderVideo(deps, context) {
|
|
|
44
44
|
totalSeconds: plan.totalSeconds,
|
|
45
45
|
record: { ...recorded(plan, dir), subtitles: project.config.subtitles ?? null },
|
|
46
46
|
subtitles,
|
|
47
|
-
render: (part, onProgress) =>
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
47
|
+
render: async (part, onProgress) => {
|
|
48
|
+
await renderSlideshow({
|
|
49
|
+
bin: deps.ffmpeg,
|
|
50
|
+
edit: plan.editList,
|
|
51
|
+
output: part,
|
|
52
|
+
burnSubtitles: subtitles?.burnIn ?? false,
|
|
53
|
+
cwd: subtitles?.directory,
|
|
54
|
+
scratch: dir,
|
|
55
|
+
signal: context.signal,
|
|
56
|
+
log: deps.log,
|
|
57
|
+
onProgress,
|
|
58
|
+
});
|
|
59
|
+
},
|
|
58
60
|
});
|
|
59
61
|
deps.count("stage.completed", { stage: "video" });
|
|
60
62
|
}
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { copyFileSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { extname, join } from "node:path";
|
|
3
|
+
import { masterFile, masterReport } from "../loudness/loudnorm.js";
|
|
3
4
|
import { cardsAss } from "./cards.js";
|
|
4
|
-
import { concatList, joinArgs, runFfmpeg, segmentArgs, slideshowClips } from "./ffmpeg.js";
|
|
5
|
+
import { audioMixArgs, concatList, joinArgs, runFfmpeg, segmentArgs, slideshowClips, } from "./ffmpeg.js";
|
|
5
6
|
import { cardsIn } from "./look.js";
|
|
6
7
|
// estimate: a zoompan frame from a 4x still costs several plain encoded frames, and a
|
|
7
8
|
// copying join is nearly free, so progress is weighted by that rather than by clock time.
|
|
8
9
|
const clipWeight = 4;
|
|
9
10
|
const copyWeight = 0.05;
|
|
11
|
+
// The sound the join plays, as the join itself brings every segment to it.
|
|
12
|
+
const masterFormat = { sampleRate: 44100, channels: 2 };
|
|
13
|
+
// What the master measured, when the run asked for one.
|
|
10
14
|
export async function renderSlideshow(run) {
|
|
11
15
|
const { edit } = run;
|
|
12
16
|
const { clips, order } = slideshowClips(edit);
|
|
@@ -26,7 +30,7 @@ export async function renderSlideshow(run) {
|
|
|
26
30
|
const font = edit.cardFont;
|
|
27
31
|
if (cards.length > 0) {
|
|
28
32
|
if (font === undefined)
|
|
29
|
-
throw new Error("Slopify hit an internal error (the chapter cards have no font). Use
|
|
33
|
+
throw new Error("Slopify hit an internal error (the chapter cards have no font). Use More → Render the video again in the Video section; if it happens again, use Download diagnostics in Settings and report it.");
|
|
30
34
|
mkdirSync(join(workspace, "fonts"), { mode: 0o700 });
|
|
31
35
|
copyFileSync(font.path, join(workspace, "fonts", `card${extname(font.path)}`));
|
|
32
36
|
}
|
|
@@ -36,7 +40,7 @@ export async function renderSlideshow(run) {
|
|
|
36
40
|
const script = clip.name.replace(/\.mp4$/, ".ass");
|
|
37
41
|
const shown = cardsIn(cards, clip.segment.start, clip.segment.count);
|
|
38
42
|
if (shown.length > 0 && font !== undefined)
|
|
39
|
-
writeFileSync(join(workspace, script), cardsAss(edit, shown, font.name, clip.segment.start), { mode: 0o600 });
|
|
43
|
+
writeFileSync(join(workspace, script), cardsAss(edit, shown, font.name, clip.segment.start, edit.cardColor), { mode: 0o600 });
|
|
40
44
|
await runFfmpeg({
|
|
41
45
|
bin: run.bin,
|
|
42
46
|
...(cards.length > 0 ? { cwd: workspace } : {}),
|
|
@@ -49,14 +53,45 @@ export async function renderSlideshow(run) {
|
|
|
49
53
|
}
|
|
50
54
|
const list = join(workspace, "slides.ffconcat");
|
|
51
55
|
writeFileSync(list, concatList(order), { mode: 0o600 });
|
|
56
|
+
// Level the volume: the sound, with its bed, is mixed alone and mastered first, and the join
|
|
57
|
+
// plays that one file.
|
|
58
|
+
let joined = edit;
|
|
59
|
+
let master;
|
|
60
|
+
if (run.master !== undefined && edit.audio.length > 0) {
|
|
61
|
+
const mixed = join(workspace, "mix.wav");
|
|
62
|
+
await runFfmpeg({
|
|
63
|
+
bin: run.bin,
|
|
64
|
+
args: audioMixArgs(edit, mixed),
|
|
65
|
+
signal: run.signal,
|
|
66
|
+
log: run.log,
|
|
67
|
+
onProgress: () => { },
|
|
68
|
+
});
|
|
69
|
+
const mastered = join(workspace, "master.wav");
|
|
70
|
+
await masterFile(run, mixed, mastered, run.master, masterFormat);
|
|
71
|
+
joined = {
|
|
72
|
+
...edit,
|
|
73
|
+
audio: [
|
|
74
|
+
{
|
|
75
|
+
kind: "body",
|
|
76
|
+
path: mastered,
|
|
77
|
+
seconds: edit.audio.reduce((sum, segment) => sum + segment.seconds, 0),
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
bed: undefined,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
52
83
|
await runFfmpeg({
|
|
53
84
|
bin: run.bin,
|
|
54
85
|
cwd: run.cwd,
|
|
55
|
-
args: joinArgs(
|
|
86
|
+
args: joinArgs(joined, run.output, list, run.burnSubtitles, run.portraits),
|
|
56
87
|
signal: run.signal,
|
|
57
88
|
log: run.log,
|
|
58
89
|
onProgress: (elapsedMs) => report(rendered + elapsedMs * (run.burnSubtitles ? 1 : copyWeight)),
|
|
59
90
|
});
|
|
91
|
+
// What the finished video measures, after its AAC encode.
|
|
92
|
+
if (run.master !== undefined && edit.audio.length > 0)
|
|
93
|
+
master = await masterReport(run, run.output, run.master);
|
|
94
|
+
return master;
|
|
60
95
|
}
|
|
61
96
|
finally {
|
|
62
97
|
rmSync(workspace, { recursive: true, force: true });
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { parseScript } from "./script.js";
|
|
2
|
+
// Audiobooks from an existing text: the text model hands each passage to a speaker - the
|
|
3
|
+
// narrator's prose to the narrator, each quoted line to the character who says it - and
|
|
4
|
+
// answers in the script format. It may not write: the words it returns are checked against the
|
|
5
|
+
// source, so a model that summarises or embellishes fails the attempt and is asked again.
|
|
6
|
+
export function attributionMessages(source, speakers) {
|
|
7
|
+
const narrator = speakers.find((speaker) => speaker.role === "narrator")?.name.trim();
|
|
8
|
+
return [
|
|
9
|
+
{
|
|
10
|
+
role: "system",
|
|
11
|
+
content: [
|
|
12
|
+
"You split a text into speaker turns for an audiobook. You never write, shorten, reword or translate anything.",
|
|
13
|
+
"Speakers (name - role):",
|
|
14
|
+
...speakers.map((speaker) => `- ${speaker.name.trim()} - ${speaker.role}`),
|
|
15
|
+
`Give narration, description and dialogue tags to ${narrator ?? "the narrator"}. Give each quoted line of dialogue, without its quotation marks, to the character who says it; a character not in the list is read by ${narrator ?? "the narrator"}.`,
|
|
16
|
+
"Answer only with the script, in this exact format:",
|
|
17
|
+
"- Every turn is one paragraph that starts with the speaker's name and a colon, e.g. `Name: words`.",
|
|
18
|
+
"- Separate turns with a blank line. Use only the names above.",
|
|
19
|
+
"- Keep the text's own chapter headings as Markdown heading lines (`# Title`).",
|
|
20
|
+
"- Every word of the text appears exactly once, in order.",
|
|
21
|
+
].join("\n"),
|
|
22
|
+
},
|
|
23
|
+
{ role: "user", content: source },
|
|
24
|
+
];
|
|
25
|
+
}
|
|
26
|
+
// The answer must parse as a script and keep the source's words: at least 90% of them in
|
|
27
|
+
// order, and no more than 10% words of its own.
|
|
28
|
+
export function parseAttribution(answer, source, speakers) {
|
|
29
|
+
const parsed = parseScript(answer, speakers);
|
|
30
|
+
if (!parsed.ok)
|
|
31
|
+
return parsed;
|
|
32
|
+
const expected = words(source);
|
|
33
|
+
const got = words(parsed.script.turns.map((turn) => turn.text).join(" "));
|
|
34
|
+
const kept = commonInOrder(expected, got);
|
|
35
|
+
if (expected.length > 0 && kept / expected.length < 0.9)
|
|
36
|
+
return {
|
|
37
|
+
ok: false,
|
|
38
|
+
reason: `The speaker split left out ${String(expected.length - kept)} of the text's ${String(expected.length)} words. The text model must hand every word to a speaker; Try again, or choose another model in Edit project → Providers.`,
|
|
39
|
+
};
|
|
40
|
+
if (got.length - kept > Math.max(3, expected.length * 0.1))
|
|
41
|
+
return {
|
|
42
|
+
ok: false,
|
|
43
|
+
reason: `The speaker split added ${String(got.length - kept)} words that are not in the text. The text model must not write anything of its own; Try again, or choose another model in Edit project → Providers.`,
|
|
44
|
+
};
|
|
45
|
+
return parsed;
|
|
46
|
+
}
|
|
47
|
+
function words(text) {
|
|
48
|
+
return text
|
|
49
|
+
.toLowerCase()
|
|
50
|
+
.normalize("NFKD")
|
|
51
|
+
.replace(/[^\p{L}\p{N}\s]/gu, " ")
|
|
52
|
+
.split(/\s+/)
|
|
53
|
+
.filter((word) => word !== "");
|
|
54
|
+
}
|
|
55
|
+
// A greedy walk rather than a full longest-common-subsequence: both lists are the same words in
|
|
56
|
+
// the same order save for what a model dropped or added, and a book chapter is too long for
|
|
57
|
+
// the quadratic table.
|
|
58
|
+
function commonInOrder(expected, got) {
|
|
59
|
+
let at = 0;
|
|
60
|
+
let kept = 0;
|
|
61
|
+
for (const word of got) {
|
|
62
|
+
const found = expected.indexOf(word, at);
|
|
63
|
+
if (found === -1 || found - at > 40)
|
|
64
|
+
continue;
|
|
65
|
+
kept += 1;
|
|
66
|
+
at = found + 1;
|
|
67
|
+
}
|
|
68
|
+
return kept;
|
|
69
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// One chapter per script section that was heard, each running to the next; the first starts
|
|
2
|
+
// at zero so the lead-in and intro belong to it. A script without sections is one chapter.
|
|
3
|
+
export function audioChapters(input) {
|
|
4
|
+
const totalMs = Math.max(1, Math.round(input.totalSeconds * 1000));
|
|
5
|
+
const points = [];
|
|
6
|
+
for (const section of input.sections) {
|
|
7
|
+
const start = input.turnStarts.get(section.firstTurn);
|
|
8
|
+
if (start === undefined)
|
|
9
|
+
continue;
|
|
10
|
+
const startMs = points.length === 0 ? 0 : Math.round(start * 1000);
|
|
11
|
+
const last = points.at(-1);
|
|
12
|
+
// Two sections starting at the same moment (an empty one) keep the later title.
|
|
13
|
+
if (last !== undefined && startMs <= last.startMs) {
|
|
14
|
+
points[points.length - 1] = { title: section.title, startMs: last.startMs };
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
if (startMs >= totalMs)
|
|
18
|
+
continue;
|
|
19
|
+
points.push({ title: section.title, startMs });
|
|
20
|
+
}
|
|
21
|
+
if (points.length === 0)
|
|
22
|
+
points.push({ title: input.title, startMs: 0 });
|
|
23
|
+
return points.map((point, at) => ({
|
|
24
|
+
title: point.title,
|
|
25
|
+
startMs: point.startMs,
|
|
26
|
+
endMs: points[at + 1]?.startMs ?? totalMs,
|
|
27
|
+
}));
|
|
28
|
+
}
|
|
29
|
+
// FFMETADATA1: `=`, `;`, `#`, `\` and line breaks are escaped with a backslash.
|
|
30
|
+
export function ffmetadata(title, chapters) {
|
|
31
|
+
const lines = [";FFMETADATA1", `title=${escapeMeta(title)}`];
|
|
32
|
+
for (const chapter of chapters)
|
|
33
|
+
lines.push("", "[CHAPTER]", "TIMEBASE=1/1000", `START=${String(chapter.startMs)}`, `END=${String(chapter.endMs)}`, `title=${escapeMeta(chapter.title)}`);
|
|
34
|
+
return `${lines.join("\n")}\n`;
|
|
35
|
+
}
|
|
36
|
+
function escapeMeta(text) {
|
|
37
|
+
return text.replace(/[=;#\\\n]/g, (character) => `\\${character === "\n" ? "\n" : character}`);
|
|
38
|
+
}
|
|
39
|
+
const rate = 44100;
|
|
40
|
+
export function audioFileArgs(audio, metadataPath, output, kind) {
|
|
41
|
+
const inputs = audio.flatMap((segment) => segment.path === null
|
|
42
|
+
? [
|
|
43
|
+
"-f",
|
|
44
|
+
"lavfi",
|
|
45
|
+
"-t",
|
|
46
|
+
segment.seconds.toFixed(6),
|
|
47
|
+
"-i",
|
|
48
|
+
`anullsrc=r=${String(rate)}:cl=stereo`,
|
|
49
|
+
]
|
|
50
|
+
: ["-i", segment.path]);
|
|
51
|
+
const metadataInput = audio.length;
|
|
52
|
+
const chains = audio.map((_segment, index) => `[${String(index)}:a]aformat=sample_fmts=fltp:sample_rates=${String(rate)}:channel_layouts=stereo,asetpts=PTS-STARTPTS[a${String(index)}]`);
|
|
53
|
+
chains.push(`${audio.map((_segment, index) => `[a${String(index)}]`).join("")}concat=n=${String(audio.length)}:v=0:a=1[a]`);
|
|
54
|
+
return [
|
|
55
|
+
"-hide_banner",
|
|
56
|
+
"-nostdin",
|
|
57
|
+
"-loglevel",
|
|
58
|
+
"error",
|
|
59
|
+
"-progress",
|
|
60
|
+
"pipe:1",
|
|
61
|
+
"-nostats",
|
|
62
|
+
"-y",
|
|
63
|
+
...inputs,
|
|
64
|
+
"-f",
|
|
65
|
+
"ffmetadata",
|
|
66
|
+
"-i",
|
|
67
|
+
metadataPath,
|
|
68
|
+
"-filter_complex",
|
|
69
|
+
chains.join(";"),
|
|
70
|
+
"-map",
|
|
71
|
+
"[a]",
|
|
72
|
+
"-map_metadata",
|
|
73
|
+
String(metadataInput),
|
|
74
|
+
"-map_chapters",
|
|
75
|
+
String(metadataInput),
|
|
76
|
+
"-vn",
|
|
77
|
+
...(kind === "mp3"
|
|
78
|
+
? ["-c:a", "libmp3lame", "-b:a", "128k", "-id3v2_version", "3", "-f", "mp3"]
|
|
79
|
+
: ["-c:a", "aac", "-b:a", "96k", "-movflags", "+faststart", "-f", "mp4"]),
|
|
80
|
+
output,
|
|
81
|
+
];
|
|
82
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// What each speaker reads in an audition: their first line of the script, cut at a sentence
|
|
2
|
+
// end near 200 characters, or a sample line in their name while there is no script yet.
|
|
3
|
+
export const auditionLineMax = 300;
|
|
4
|
+
export function auditionLine(speaker, script) {
|
|
5
|
+
const first = script?.turns.find((turn) => turn.speaker === speaker.id)?.text;
|
|
6
|
+
const text = first ?? sampleLine(speaker);
|
|
7
|
+
if (text.length <= 200)
|
|
8
|
+
return text;
|
|
9
|
+
const cut = text.slice(0, 200);
|
|
10
|
+
const end = Math.max(cut.lastIndexOf(". "), cut.lastIndexOf("? "), cut.lastIndexOf("! "));
|
|
11
|
+
return end > 60 ? cut.slice(0, end + 1) : `${cut.slice(0, cut.lastIndexOf(" "))}…`;
|
|
12
|
+
}
|
|
13
|
+
function sampleLine(speaker) {
|
|
14
|
+
const name = speaker.name.trim() || "your speaker";
|
|
15
|
+
switch (speaker.role) {
|
|
16
|
+
case "narrator":
|
|
17
|
+
return `The night was quiet, and the old house held its breath. I'm ${name}, and I'll be telling this story.`;
|
|
18
|
+
case "host":
|
|
19
|
+
return `Hi everyone, I'm ${name}, and welcome back to the show. Today's topic is a good one.`;
|
|
20
|
+
case "guest":
|
|
21
|
+
return `Thanks for having me. I'm ${name}, and I've been looking forward to this conversation.`;
|
|
22
|
+
case "character":
|
|
23
|
+
return `You really thought I wouldn't find out? My name is ${name}, and I always find out.`;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// The cast library as a source of speakers: every member of the channel with a voice can be
|
|
2
|
+
// cast in a run. A speaker picked from the cast keeps the member's id, and every run started
|
|
3
|
+
// later takes the member's voice as it is then, so a character or a host sounds the same in
|
|
4
|
+
// every episode; editing the cast later changes no project already made.
|
|
5
|
+
export function castSpeakers(members) {
|
|
6
|
+
return {
|
|
7
|
+
speakers: () => members.flatMap((member) => member.voice === undefined ? [] : [speakerFromCast(member, "character")]),
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export function speakerFromCast(member, role) {
|
|
11
|
+
const voice = member.voice;
|
|
12
|
+
return {
|
|
13
|
+
id: `cast-${member.id
|
|
14
|
+
.slice(0, 8)
|
|
15
|
+
.toLowerCase()
|
|
16
|
+
.replace(/[^a-z0-9-]/g, "")}`,
|
|
17
|
+
name: member.name.slice(0, 40),
|
|
18
|
+
role,
|
|
19
|
+
voice: voice === undefined
|
|
20
|
+
? { provider: "", model: "", voice: "" }
|
|
21
|
+
: { provider: voice.provider, model: voice.model, voice: voice.voice },
|
|
22
|
+
...(voice?.pace === undefined || voice.pace === 1 ? {} : { pace: voice.pace }),
|
|
23
|
+
...(voice?.pronunciations?.trim() ? { pronunciations: voice.pronunciations } : {}),
|
|
24
|
+
castId: member.id,
|
|
25
|
+
...portraitOf(member),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
// The member's first finished picture, the one the speaker panel shows. Nothing when they have
|
|
29
|
+
// none, so a speaker without a picture is the speaker it always was.
|
|
30
|
+
function portraitOf(member) {
|
|
31
|
+
const sha256 = member.images.find((one) => one.state === "ready" && one.sha256 !== null)?.sha256;
|
|
32
|
+
return sha256 === undefined || sha256 === null ? {} : { portrait: sha256 };
|
|
33
|
+
}
|
|
34
|
+
// A run's speakers with each cast speaker's voice, pace, pronunciations and portrait taken from
|
|
35
|
+
// the cast as it is now. A member since deleted, or without a voice, leaves the speaker as saved.
|
|
36
|
+
export function withCastVoices(settings, members) {
|
|
37
|
+
let changed = false;
|
|
38
|
+
const speakers = settings.speakers.map((speaker) => {
|
|
39
|
+
const member = speaker.castId === undefined ? undefined : members.find((one) => one.id === speaker.castId);
|
|
40
|
+
if (member?.voice === undefined)
|
|
41
|
+
return speaker;
|
|
42
|
+
const fresh = speakerFromCast(member, speaker.role);
|
|
43
|
+
const { pace: _pace, pronunciations: _pronunciations, portrait: _portrait, ...rest } = speaker;
|
|
44
|
+
const next = {
|
|
45
|
+
...rest,
|
|
46
|
+
voice: fresh.voice,
|
|
47
|
+
...(fresh.pace === undefined ? {} : { pace: fresh.pace }),
|
|
48
|
+
...(fresh.pronunciations === undefined ? {} : { pronunciations: fresh.pronunciations }),
|
|
49
|
+
...(fresh.portrait === undefined ? {} : { portrait: fresh.portrait }),
|
|
50
|
+
};
|
|
51
|
+
if (JSON.stringify(next) !== JSON.stringify(speaker))
|
|
52
|
+
changed = true;
|
|
53
|
+
return next;
|
|
54
|
+
});
|
|
55
|
+
return changed ? { ...settings, speakers } : settings;
|
|
56
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { speakerPace } from "./model.js";
|
|
2
|
+
export const dialogueCapabilities = [
|
|
3
|
+
{ provider: "elevenlabs", models: ["eleven_v3"], maxVoices: 10, maxCharacters: 2000 },
|
|
4
|
+
];
|
|
5
|
+
export function dialogueCapability(provider, model) {
|
|
6
|
+
return dialogueCapabilities.find((capability) => capability.provider === provider && capability.models.includes(model));
|
|
7
|
+
}
|
|
8
|
+
// `spokenLength` is how long a turn's text is once it goes to the voice (narration aliases can
|
|
9
|
+
// make it longer than the script), so a request never passes the provider's limit.
|
|
10
|
+
export function groupTurns(turns, speakers, nativeDialogue, spokenLength = (text) => text.length) {
|
|
11
|
+
const byId = new Map(speakers.map((speaker) => [speaker.id, speaker]));
|
|
12
|
+
const groups = [];
|
|
13
|
+
let current = [];
|
|
14
|
+
let capability;
|
|
15
|
+
let signature = "";
|
|
16
|
+
const close = () => {
|
|
17
|
+
if (current.length > 0)
|
|
18
|
+
groups.push({ turns: current, native: current.length > 1 });
|
|
19
|
+
current = [];
|
|
20
|
+
capability = undefined;
|
|
21
|
+
signature = "";
|
|
22
|
+
};
|
|
23
|
+
for (const turn of turns) {
|
|
24
|
+
const speaker = byId.get(turn.speaker);
|
|
25
|
+
const own = nativeDialogue && speaker !== undefined
|
|
26
|
+
? dialogueCapability(speaker.voice.provider, speaker.voice.model)
|
|
27
|
+
: undefined;
|
|
28
|
+
if (own === undefined || speaker === undefined || spokenLength(turn.text) > own.maxCharacters) {
|
|
29
|
+
close();
|
|
30
|
+
groups.push({ turns: [turn], native: false });
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
// One provider, one model and one pace per request: the pace is applied to the request's
|
|
34
|
+
// audio as a whole after it comes back.
|
|
35
|
+
const mine = `${speaker.voice.provider}\n${speaker.voice.model}\n${String(speakerPace(speaker))}`;
|
|
36
|
+
const voices = new Set([...current, turn].map((one) => byId.get(one.speaker)?.voice.voice ?? ""));
|
|
37
|
+
const characters = [...current, turn].reduce((sum, one) => sum + spokenLength(one.text), 0);
|
|
38
|
+
if (current.length > 0 &&
|
|
39
|
+
(mine !== signature ||
|
|
40
|
+
voices.size > (capability?.maxVoices ?? 0) ||
|
|
41
|
+
characters > (capability?.maxCharacters ?? 0)))
|
|
42
|
+
close();
|
|
43
|
+
current.push(turn);
|
|
44
|
+
capability = own;
|
|
45
|
+
signature = mine;
|
|
46
|
+
}
|
|
47
|
+
close();
|
|
48
|
+
return groups;
|
|
49
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Joining a multi-voice narration: every part is brought to one format (the speakers' voices
|
|
2
|
+
// may come from providers with different sample rates, which the concat demuxer would join
|
|
3
|
+
// wrongly), played at its speaker's pace, and followed by the gap between turns unless the
|
|
4
|
+
// next part continues the same turn. Hand-rolled like every ffmpeg argument list here.
|
|
5
|
+
const rate = 44100;
|
|
6
|
+
export function turnJoinArgs(parts, output) {
|
|
7
|
+
const chains = parts.map((part, index) => {
|
|
8
|
+
const filters = [
|
|
9
|
+
`aformat=sample_fmts=fltp:sample_rates=${String(rate)}:channel_layouts=mono`,
|
|
10
|
+
...(part.pace === 1 ? [] : [`atempo=${String(part.pace)}`]),
|
|
11
|
+
...(part.gapAfter > 0 ? [`apad=pad_dur=${part.gapAfter.toFixed(3)}`] : []),
|
|
12
|
+
"asetpts=PTS-STARTPTS",
|
|
13
|
+
];
|
|
14
|
+
return `[${String(index)}:a]${filters.join(",")}[p${String(index)}]`;
|
|
15
|
+
});
|
|
16
|
+
chains.push(`${parts.map((_part, index) => `[p${String(index)}]`).join("")}concat=n=${String(parts.length)}:v=0:a=1[a]`);
|
|
17
|
+
return [
|
|
18
|
+
"-hide_banner",
|
|
19
|
+
"-nostdin",
|
|
20
|
+
"-loglevel",
|
|
21
|
+
"error",
|
|
22
|
+
"-nostats",
|
|
23
|
+
"-y",
|
|
24
|
+
...parts.flatMap((part) => ["-i", part.path]),
|
|
25
|
+
"-filter_complex",
|
|
26
|
+
chains.join(";"),
|
|
27
|
+
"-map",
|
|
28
|
+
"[a]",
|
|
29
|
+
"-c:a",
|
|
30
|
+
"libmp3lame",
|
|
31
|
+
"-b:a",
|
|
32
|
+
"128k",
|
|
33
|
+
output,
|
|
34
|
+
];
|
|
35
|
+
}
|