@gentbajko/slopify 2.5.0 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -21
- package/NOTICE +12 -0
- package/README.md +24 -41
- package/SUBTITLES.md +1 -1
- package/dist/adapter-registry.js +31 -0
- package/dist/adapters/alignment/cache.js +4 -1
- package/dist/adapters/alignment/ctc.js +25 -23
- package/dist/adapters/alignment/index.js +37 -6
- package/dist/adapters/alignment/multilingual.js +267 -0
- package/dist/adapters/alignment/protocol.js +11 -0
- package/dist/adapters/alignment/quality.js +4 -3
- package/dist/adapters/alignment/sentences.js +95 -0
- package/dist/adapters/alignment/spec.js +24 -0
- package/dist/adapters/alignment/spell.js +769 -0
- package/dist/adapters/alignment/text.js +56 -9
- package/dist/adapters/alignment/window.js +34 -30
- package/dist/adapters/alignment/worker.js +23 -21
- package/dist/adapters/explain.js +11 -11
- package/dist/adapters/host-cli/index.js +44 -5
- package/dist/adapters/host-cli/transport.js +7 -5
- package/dist/adapters/image/bytes.js +2 -2
- package/dist/adapters/image/codex-output.js +1 -1
- package/dist/adapters/image/codex.js +87 -22
- package/dist/adapters/image/fal.js +13 -5
- package/dist/adapters/image/google.js +11 -8
- package/dist/adapters/image/openai.js +14 -7
- package/dist/adapters/image/reference.js +34 -0
- package/dist/adapters/image/replicate.js +7 -2
- package/dist/adapters/image/video.js +2 -2
- package/dist/adapters/key-probes.js +85 -0
- package/dist/adapters/llm/claude-code.js +168 -32
- package/dist/adapters/llm/cli-login-error.js +1 -1
- package/dist/adapters/llm/codex-limits.js +157 -0
- package/dist/adapters/llm/codex.js +99 -12
- package/dist/adapters/llm/document-workspace.js +2 -2
- package/dist/adapters/llm/gemini-workspace.js +1 -1
- package/dist/adapters/llm/gemini.js +16 -3
- package/dist/adapters/llm/image-workspace.js +58 -0
- package/dist/adapters/llm/openrouter.js +11 -3
- package/dist/adapters/llm/run-cli.js +12 -7
- package/dist/adapters/tts/cartesia.js +10 -0
- package/dist/adapters/tts/elevenlabs.js +31 -9
- package/dist/adapters/tts/gemini.js +206 -0
- package/dist/adapters/tts/inworld-async.js +9 -5
- package/dist/adapters/tts/inworld.js +17 -3
- package/dist/adapters/tts/openai.js +3 -0
- package/dist/adapters/tts/pcm-mp3.js +62 -0
- package/dist/adapters/tts/system.js +147 -0
- package/dist/adapters/tts/voice-languages.js +80 -0
- package/dist/assets/fonts/noto/NotoSans-Regular.ttf +0 -0
- package/dist/assets/fonts/noto/NotoSansArabic-Regular.ttf +0 -0
- package/dist/assets/fonts/noto/NotoSansDevanagari-Regular.ttf +0 -0
- package/dist/assets/fonts/noto/NotoSansHebrew-Regular.ttf +0 -0
- package/dist/assets/fonts/noto/NotoSansThai-Regular.ttf +0 -0
- package/dist/assets/fonts/noto/OFL.txt +93 -0
- package/dist/assets/fonts/noto/SOURCE.txt +18 -0
- package/dist/assets/models.yaml +38 -0
- package/dist/assets/sample/sample-audiobook.tar +0 -0
- package/dist/assets/sample/sample-podcast.tar +0 -0
- package/dist/assets/sample/sample-project.tar +0 -0
- package/dist/assets/style-preview/narration.mp3 +0 -0
- package/dist/assets/style-preview/tall-1.jpg +0 -0
- package/dist/assets/style-preview/tall-2.jpg +0 -0
- package/dist/assets/style-preview/tall-3.jpg +0 -0
- package/dist/assets/style-preview/wide-1.jpg +0 -0
- package/dist/assets/style-preview/wide-2.jpg +0 -0
- package/dist/assets/style-preview/wide-3.jpg +0 -0
- package/dist/catalog/merge.js +132 -0
- package/dist/catalog/registry.js +17 -14
- package/dist/catalog/schema.js +2 -0
- package/dist/catalog/store.js +147 -8
- package/dist/catalog/validate.js +2 -2
- package/dist/compose.yaml +71 -0
- package/dist/edge/autostart/docker-record.js +32 -0
- package/dist/edge/autostart/docker.js +118 -0
- package/dist/edge/autostart/index.js +78 -0
- package/dist/edge/autostart/launcher.js +176 -0
- package/dist/edge/autostart/model.js +3 -0
- package/dist/edge/autostart/native.js +210 -0
- package/dist/edge/autostart/prompt.js +72 -0
- package/dist/edge/autostart/service.js +90 -0
- package/dist/edge/cli-args.js +71 -0
- package/dist/edge/cli.js +64 -63
- package/dist/edge/docker-install/activation.js +56 -0
- package/dist/edge/docker-install/apply.js +458 -0
- package/dist/edge/docker-install/engine.js +362 -0
- package/dist/edge/docker-install/run.js +160 -0
- package/dist/edge/docker-install/state.js +146 -0
- package/dist/edge/{docker-projects → docker-install}/volume.js +1 -5
- package/dist/edge/docker.js +14 -22
- package/dist/edge/events/hub.js +3 -1
- package/dist/edge/host-cli.js +1 -1
- package/dist/edge/http/actions.js +38 -11
- package/dist/edge/http/app.js +60 -2
- package/dist/edge/http/auditions.js +70 -0
- package/dist/edge/http/autostart.js +37 -0
- package/dist/edge/http/byte-range.js +42 -0
- package/dist/edge/http/channel-memory.js +92 -0
- package/dist/edge/http/channels.js +128 -0
- package/dist/edge/http/diagnostics.js +1 -0
- package/dist/edge/http/draft-files.js +2 -4
- package/dist/edge/http/entries.js +22 -3
- package/dist/edge/http/files.js +11 -7
- package/dist/edge/http/folder-location.js +1 -1
- package/dist/edge/http/home.js +39 -0
- package/dist/edge/http/host-cli.js +29 -5
- package/dist/edge/http/narration-peaks.js +76 -0
- package/dist/edge/http/onboarding.js +212 -0
- package/dist/edge/http/open-folder.js +1 -1
- package/dist/edge/http/patch-notes.js +45 -0
- package/dist/edge/http/planning.js +4 -2
- package/dist/edge/http/project-create.js +87 -0
- package/dist/edge/http/project-templates.js +19 -2
- package/dist/edge/http/projects.js +37 -100
- package/dist/edge/http/prompts.js +40 -3
- package/dist/edge/http/pronunciations.js +23 -4
- package/dist/edge/http/providers.js +119 -3
- package/dist/edge/http/reviews.js +131 -0
- package/dist/edge/http/revision-files.js +3 -5
- package/dist/edge/http/revisions.js +1 -1
- package/dist/edge/http/run-cost.js +25 -0
- package/dist/edge/http/schedules.js +115 -6
- package/dist/edge/http/settings.js +84 -6
- package/dist/edge/http/storage-files.js +50 -0
- package/dist/edge/http/storage.js +43 -2
- package/dist/edge/http/studio.js +287 -0
- package/dist/edge/http/style-preview.js +61 -0
- package/dist/edge/http/subtitles.js +1 -1
- package/dist/edge/http/trash.js +69 -0
- package/dist/edge/http/tutorials.js +59 -0
- package/dist/edge/http/update.js +24 -3
- package/dist/edge/http/whats-new.js +15 -0
- package/dist/edge/http/youtube-edits.js +92 -0
- package/dist/edge/native-update.js +62 -0
- package/dist/extension/slopify-studio-chrome.zip +0 -0
- package/dist/extension/slopify-studio-firefox.zip +0 -0
- package/dist/host-cli/gemini-login.js +83 -0
- package/dist/host-cli/open-folder.js +10 -10
- package/dist/host-cli/runtime.js +9 -1
- package/dist/host-cli/status.js +4 -2
- package/dist/kernel/db/migrations/0025-automatic-reviews.sql +40 -0
- package/dist/kernel/db/migrations/0026-schedule-topic-generation.sql +23 -0
- package/dist/kernel/db/migrations/0027-run-cost.sql +58 -0
- package/dist/kernel/db/migrations/0030-automatic-retries.sql +18 -0
- package/dist/kernel/db/migrations/0031-prompt-history-and-description-edits.sql +33 -0
- package/dist/kernel/db/migrations/0032-channels.sql +65 -0
- package/dist/kernel/db/migrations/0034-script-prompts.sql +23 -0
- package/dist/kernel/db/migrations/0035-narration-aliases.sql +13 -0
- package/dist/kernel/db/migrations/0037-project-uploads.sql +8 -0
- package/dist/kernel/db/migrations/0038-voice-languages.sql +7 -0
- package/dist/kernel/db/migrations/0039-channel-essentials.sql +64 -0
- package/dist/kernel/db/migrations/0040-standalone-usage.sql +35 -0
- package/dist/kernel/db/migrations/0041-cast-hosts.sql +3 -0
- package/dist/kernel/paths.js +27 -6
- package/dist/kernel/pipeline.js +4 -1
- package/dist/kernel/ports/host-cli.js +122 -14
- package/dist/kernel/ports/key-probe.js +1 -0
- package/dist/kernel/ports/languages.js +157 -0
- package/dist/kernel/ports/model.js +10 -5
- package/dist/kernel/ports/narration-aliases.js +60 -0
- package/dist/kernel/ports/plan-limits.js +36 -0
- package/dist/kernel/ports/system-speech.js +203 -0
- package/dist/kernel/runner/attempt.js +17 -8
- package/dist/kernel/runner/audition.js +36 -0
- package/dist/kernel/runner/graph.js +40 -5
- package/dist/kernel/runner/index.js +39 -9
- package/dist/kernel/runner/meter.js +1 -0
- package/dist/kernel/runner/providers.js +142 -7
- package/dist/kernel/runner/retry-policy.js +43 -0
- package/dist/kernel/runner/standalone.js +134 -0
- package/dist/kernel/runner/work-authority.js +28 -2
- package/dist/main.js +277 -16
- package/dist/model-catalog.js +2 -0
- package/dist/patch-notes/0.4.2-to-1.0.md +93 -0
- package/dist/patch-notes/1.0-to-2.0.md +74 -0
- package/dist/patch-notes/2.0-to-3.0.md +257 -0
- package/dist/patch-notes/3.0.0.md +70 -0
- package/dist/patch-notes/3.0.1.md +105 -0
- package/dist/patch-notes/index.json +32 -0
- package/dist/slices/admission/model.js +27 -0
- package/dist/slices/admission/repo.js +24 -4
- package/dist/slices/admission/rules.js +80 -2
- package/dist/slices/admission/schema.js +58 -1
- package/dist/slices/admission/short-mode.js +47 -0
- package/dist/slices/admission/start.js +42 -6
- package/dist/slices/article/continuation.js +2 -2
- package/dist/slices/article/provided-entries.js +2 -2
- package/dist/slices/article/run.js +6 -2
- package/dist/slices/article/segments.js +3 -3
- package/dist/slices/backups/folder.js +14 -6
- package/dist/slices/batch/index.js +7 -2
- package/dist/slices/cancel/index.js +1 -1
- package/dist/slices/channels/cast-images.js +110 -0
- package/dist/slices/channels/cast-match.js +32 -0
- package/dist/slices/channels/images.js +19 -0
- package/dist/slices/channels/model.js +5 -0
- package/dist/slices/channels/rebrand.js +100 -0
- package/dist/slices/channels/repo.js +221 -0
- package/dist/slices/channels/runs.js +129 -0
- package/dist/slices/channels/schema.js +152 -0
- package/dist/slices/channels/service.js +180 -0
- package/dist/slices/channels/studio-csv.js +110 -0
- package/dist/slices/channels/videos.js +120 -0
- package/dist/slices/checkpoints/recovery.js +10 -0
- package/dist/slices/control/index.js +5 -2
- package/dist/slices/control/providers.js +4 -1
- package/dist/slices/document/fonts.js +1 -1
- package/dist/slices/document/render.js +1 -1
- package/dist/slices/episodes/related.js +90 -0
- package/dist/slices/episodes/repo.js +60 -0
- package/dist/slices/episodes/service.js +58 -0
- package/dist/slices/episodes/summarize.js +170 -0
- package/dist/slices/estimate/index.js +109 -28
- package/dist/slices/estimate/requests.js +68 -2
- package/dist/slices/eta/history.js +56 -0
- package/dist/slices/eta/model.js +63 -0
- package/dist/slices/eta/view.js +25 -0
- package/dist/slices/fixes/rules.js +73 -0
- package/dist/slices/fonts/coverage.js +82 -0
- package/dist/slices/fonts/sfnt.js +74 -0
- package/dist/slices/images/run.js +8 -5
- package/dist/slices/images/scale.js +84 -0
- package/dist/slices/library/diff.js +122 -0
- package/dist/slices/library/history.js +100 -0
- package/dist/slices/library/model.js +3 -0
- package/dist/slices/library/photorealistic.js +49 -0
- package/dist/slices/library/repo.js +19 -14
- package/dist/slices/library/save.js +62 -12
- package/dist/slices/library/slots.js +7 -1
- package/dist/slices/library/used-by.js +153 -0
- package/dist/slices/loudness/level-pieces.js +41 -0
- package/dist/slices/loudness/loudnorm.js +164 -0
- package/dist/slices/loudness/model.js +153 -0
- package/dist/slices/model-upkeep/model.js +10 -0
- package/dist/slices/model-upkeep/switch.js +217 -0
- package/dist/slices/model-upkeep/usage.js +185 -0
- package/dist/slices/narration/aliases-library.js +31 -0
- package/dist/slices/narration/aliases-schema.js +53 -0
- package/dist/slices/narration/aliases.js +49 -0
- package/dist/slices/narration/blocks.js +352 -0
- package/dist/slices/narration/chunk.js +19 -7
- package/dist/slices/narration/concat.js +2 -2
- package/dist/slices/narration/describe.js +45 -0
- package/dist/slices/narration/pauses-model.js +63 -0
- package/dist/slices/narration/pauses.js +269 -0
- package/dist/slices/narration/peaks-model.js +1 -0
- package/dist/slices/narration/peaks.js +94 -0
- package/dist/slices/narration/preparation.js +21 -3
- package/dist/slices/narration/pronunciation-chunks.js +3 -3
- package/dist/slices/narration/pronunciation.js +72 -25
- package/dist/slices/narration/run.js +6 -6
- package/dist/slices/narration/shared-glossary.js +1 -1
- package/dist/slices/narration/steering.js +15 -4
- package/dist/slices/notifications/notifier.js +47 -3
- package/dist/slices/notifications/rules.js +48 -3
- package/dist/slices/onboarding/copy.js +143 -0
- package/dist/slices/onboarding/first-run.js +72 -0
- package/dist/slices/onboarding/full-video.js +81 -0
- package/dist/slices/onboarding/install.js +119 -0
- package/dist/slices/onboarding/model.js +85 -0
- package/dist/slices/onboarding/packs.js +491 -0
- package/dist/slices/onboarding/quick-short.js +144 -0
- package/dist/slices/onboarding/sample.js +132 -0
- package/dist/slices/onboarding/state.js +68 -0
- package/dist/slices/onboarding/template.js +73 -0
- package/dist/slices/patch-notes/library.js +73 -0
- package/dist/slices/patch-notes/seen.js +77 -0
- package/dist/slices/play-drafts/convert.js +127 -8
- package/dist/slices/play-drafts/readiness.js +4 -1
- package/dist/slices/play-drafts/repo.js +2 -0
- package/dist/slices/play-drafts/review-inputs.js +20 -3
- package/dist/slices/play-drafts/schema.js +89 -0
- package/dist/slices/play-drafts/service.js +4 -1
- package/dist/slices/play-drafts/start.js +43 -18
- package/dist/slices/project-templates/from-project.js +59 -6
- package/dist/slices/project-templates/next-chapter.js +94 -0
- package/dist/slices/project-templates/one-off.js +26 -0
- package/dist/slices/project-templates/repo.js +2 -1
- package/dist/slices/project-templates/schema.js +7 -1
- package/dist/slices/project-templates/service.js +15 -4
- package/dist/slices/project-templates/setup.js +18 -2
- package/dist/slices/rebuild/admission-repo.js +1 -1
- package/dist/slices/rebuild/model.js +6 -0
- package/dist/slices/rebuild/preview-details.js +46 -3
- package/dist/slices/rebuild/preview-plan.js +13 -6
- package/dist/slices/rebuild/recipe-audio-parts.js +40 -7
- package/dist/slices/rebuild/recipe-audio.js +66 -12
- package/dist/slices/rebuild/recipe-build.js +8 -4
- package/dist/slices/rebuild/recipe-cast.js +18 -0
- package/dist/slices/rebuild/recipe-checkpoints.js +11 -9
- package/dist/slices/rebuild/recipe-describe.js +93 -0
- package/dist/slices/rebuild/recipe-edit.js +13 -1
- package/dist/slices/rebuild/recipe-exports.js +67 -8
- package/dist/slices/rebuild/recipe-input-schema.js +35 -0
- package/dist/slices/rebuild/recipe-legacy.js +9 -0
- package/dist/slices/rebuild/recipe-loudness.js +45 -0
- package/dist/slices/rebuild/recipe-model.js +25 -6
- package/dist/slices/rebuild/recipe-pauses.js +7 -0
- package/dist/slices/rebuild/recipe-preparation.js +3 -3
- package/dist/slices/rebuild/recipe-provider-choice.js +11 -1
- package/dist/slices/rebuild/recipe-reference.js +15 -1
- package/dist/slices/rebuild/recipe-reviews.js +117 -0
- package/dist/slices/rebuild/recipe-save.js +4 -1
- package/dist/slices/rebuild/recipe-shorts.js +23 -3
- package/dist/slices/rebuild/recipe-text.js +114 -19
- package/dist/slices/rebuild/recipe-validation.js +14 -2
- package/dist/slices/rebuild/recipe-visual.js +96 -33
- package/dist/slices/rebuild/recipe-voices.js +286 -0
- package/dist/slices/rebuild/recipe-work.js +77 -6
- package/dist/slices/rebuild/recipe-youtube.js +2 -0
- package/dist/slices/rebuild/recovery-conflict.js +7 -3
- package/dist/slices/rebuild/recovery-model.js +3 -0
- package/dist/slices/rebuild/recovery-selection.js +28 -2
- package/dist/slices/rebuild/recovery.js +35 -17
- package/dist/slices/rebuild/repo.js +3 -3
- package/dist/slices/rebuild/retry.js +18 -0
- package/dist/slices/rebuild/review-redo.js +125 -0
- package/dist/slices/rebuild/runtime-actions.js +5 -0
- package/dist/slices/rebuild/runtime-admission.js +4 -1
- package/dist/slices/rebuild/runtime-animate.js +4 -4
- package/dist/slices/rebuild/runtime-article.js +5 -5
- package/dist/slices/rebuild/runtime-document.js +4 -4
- package/dist/slices/rebuild/runtime-export-bed.js +43 -0
- package/dist/slices/rebuild/runtime-export-edit.js +70 -11
- package/dist/slices/rebuild/runtime-export-inputs.js +52 -10
- package/dist/slices/rebuild/runtime-export-short.js +85 -0
- package/dist/slices/rebuild/runtime-export.js +84 -19
- package/dist/slices/rebuild/runtime-figure-card.js +87 -0
- package/dist/slices/rebuild/runtime-image.js +21 -2
- package/dist/slices/rebuild/runtime-local.js +148 -24
- package/dist/slices/rebuild/runtime-materialize.js +8 -0
- package/dist/slices/rebuild/runtime-narration-reuse.js +2 -1
- package/dist/slices/rebuild/runtime-narration-text.js +7 -7
- package/dist/slices/rebuild/runtime-pictures.js +28 -0
- package/dist/slices/rebuild/runtime-piece-label.js +27 -0
- package/dist/slices/rebuild/runtime-provider.js +126 -18
- package/dist/slices/rebuild/runtime-publication.js +21 -5
- package/dist/slices/rebuild/runtime-review.js +267 -0
- package/dist/slices/rebuild/runtime-run.js +17 -11
- package/dist/slices/rebuild/runtime-shorts.js +44 -17
- package/dist/slices/rebuild/runtime-store.js +62 -11
- package/dist/slices/rebuild/runtime-subtitles.js +72 -19
- package/dist/slices/rebuild/runtime-voices.js +103 -0
- package/dist/slices/rebuild/runtime-youtube.js +7 -6
- package/dist/slices/rebuild/service-readiness.js +3 -3
- package/dist/slices/rebuild/soften.js +49 -0
- package/dist/slices/rebuild/transition-repo.js +9 -2
- package/dist/slices/research/run.js +4 -4
- package/dist/slices/research/synthesis.js +2 -2
- package/dist/slices/reviews/model.js +105 -0
- package/dist/slices/reviews/outcome.js +22 -0
- package/dist/slices/reviews/repo.js +118 -0
- package/dist/slices/reviews/rules.js +63 -0
- package/dist/slices/reviews/verdict.js +71 -0
- package/dist/slices/reviews/view.js +30 -0
- package/dist/slices/revisions/adopt-content.js +5 -2
- package/dist/slices/revisions/adopt.js +3 -1
- package/dist/slices/revisions/downloads.js +7 -1
- package/dist/slices/revisions/image-plan.js +183 -0
- package/dist/slices/revisions/mutation-assets.js +10 -0
- package/dist/slices/revisions/mutation-cues.js +71 -0
- package/dist/slices/revisions/mutation-request.js +2 -1
- package/dist/slices/revisions/mutations.js +97 -3
- package/dist/slices/revisions/schema.js +11 -1
- package/dist/slices/run-cost/limits.js +128 -0
- package/dist/slices/run-cost/meter.js +47 -0
- package/dist/slices/run-cost/panel.js +256 -0
- package/dist/slices/run-cost/pricing.js +95 -0
- package/dist/slices/run-cost/week.js +108 -0
- package/dist/slices/schedules/agenda.js +222 -0
- package/dist/slices/schedules/repo.js +58 -18
- package/dist/slices/schedules/scheduler.js +68 -17
- package/dist/slices/schedules/schema.js +179 -1
- package/dist/slices/schedules/service.js +43 -4
- package/dist/slices/schedules/similar.js +128 -0
- package/dist/slices/schedules/topic-list.js +163 -0
- package/dist/slices/schedules/topics.js +523 -0
- package/dist/slices/settings/first-run.js +106 -0
- package/dist/slices/settings/health.js +247 -0
- package/dist/slices/settings/key-guides.js +138 -0
- package/dist/slices/settings/key-test.js +93 -0
- package/dist/slices/settings/keys.js +10 -5
- package/dist/slices/settings/model-reach.js +52 -0
- package/dist/slices/settings/model.js +55 -1
- package/dist/slices/settings/models.js +5 -1
- package/dist/slices/settings/playback.js +15 -0
- package/dist/slices/settings/readiness.js +14 -2
- package/dist/slices/settings/repo.js +17 -3
- package/dist/slices/settings/tutorial-schema.js +5 -0
- package/dist/slices/settings/voices.js +64 -4
- package/dist/slices/settings/whats-new.js +64 -0
- package/dist/slices/shorts/captions.js +9 -0
- package/dist/slices/shorts/pick.js +3 -3
- package/dist/slices/shorts/prompts.js +1 -1
- package/dist/slices/shorts/render.js +63 -13
- package/dist/slices/storage/asset-name.js +3 -0
- package/dist/slices/storage/backup-export.js +48 -9
- package/dist/slices/storage/backup-format.js +38 -1
- package/dist/slices/storage/backup-import.js +191 -19
- package/dist/slices/storage/delete-project.js +7 -4
- package/dist/slices/storage/documents.js +87 -0
- package/dist/slices/storage/downloads.js +33 -1
- package/dist/slices/storage/files-location.js +531 -0
- package/dist/slices/storage/layout.js +14 -4
- package/dist/slices/storage/model.js +11 -0
- package/dist/slices/storage/portable.js +94 -12
- package/dist/slices/storage/reconcile.js +14 -1
- package/dist/slices/storage/repo.js +4 -1
- package/dist/slices/storage/schema.js +11 -0
- package/dist/slices/storage/trim.js +146 -0
- package/dist/slices/studio/disclosure.js +74 -0
- package/dist/slices/studio/model.js +30 -0
- package/dist/slices/studio/pack.js +211 -0
- package/dist/slices/studio/queue.js +69 -0
- package/dist/slices/studio/settings.js +170 -0
- package/dist/slices/style-preview/images.js +61 -0
- package/dist/slices/style-preview/narration.js +33 -0
- package/dist/slices/style-preview/render.js +148 -0
- package/dist/slices/style-preview/schema.js +59 -0
- package/dist/slices/style-preview/service.js +92 -0
- package/dist/slices/style-preview/settings.js +139 -0
- package/dist/slices/subtitles/captions.js +42 -9
- package/dist/slices/subtitles/model.js +22 -0
- package/dist/slices/subtitles/prepare.js +27 -16
- package/dist/slices/subtitles/transcript.js +1 -1
- package/dist/slices/thumbnail/run.js +7 -7
- package/dist/slices/trash/model.js +25 -0
- package/dist/slices/trash/service.js +273 -0
- package/dist/slices/tutorials/anchors.js +57 -0
- package/dist/slices/tutorials/library.js +185 -0
- package/dist/slices/uploads/repo.js +20 -0
- package/dist/slices/video/ambient-bed-schema.js +15 -0
- package/dist/slices/video/ambient-bed.js +95 -0
- package/dist/slices/video/ambient-mix.js +72 -0
- package/dist/slices/video/audio-inputs.js +2 -2
- package/dist/slices/video/cards.js +5 -2
- package/dist/slices/video/edit-list.js +23 -2
- package/dist/slices/video/edit-settings.js +13 -3
- package/dist/slices/video/ffmpeg.js +63 -9
- package/dist/slices/video/figure-card.js +492 -0
- package/dist/slices/video/figure-spans.js +51 -0
- package/dist/slices/video/plan.js +125 -13
- package/dist/slices/video/run.js +14 -12
- package/dist/slices/video/slideshow.js +39 -4
- package/dist/slices/voices/attribution.js +69 -0
- package/dist/slices/voices/audio-files.js +87 -0
- package/dist/slices/voices/audition.js +25 -0
- package/dist/slices/voices/cast.js +74 -0
- package/dist/slices/voices/grouping.js +59 -0
- package/dist/slices/voices/join.js +35 -0
- package/dist/slices/voices/languages.js +25 -0
- package/dist/slices/voices/model.js +218 -0
- package/dist/slices/voices/palette.js +26 -0
- package/dist/slices/voices/panel.js +91 -0
- package/dist/slices/voices/portraits.js +55 -0
- package/dist/slices/voices/script.js +136 -0
- package/dist/slices/voices/timing.js +58 -0
- package/dist/slices/youtube/answer.js +1 -1
- package/dist/slices/youtube/chapters.js +126 -0
- package/dist/slices/youtube/edits-repo.js +84 -0
- package/dist/slices/youtube/edits.js +89 -0
- package/dist/slices/youtube/placeholders.js +98 -0
- package/dist/slices/youtube/transcript.js +11 -4
- package/dist/tutorials/AI-CLIs.md +146 -0
- package/dist/tutorials/Backups.md +121 -0
- package/dist/tutorials/Calendar.md +103 -0
- package/dist/tutorials/Cast-Library.md +112 -0
- package/dist/tutorials/Channels.md +148 -0
- package/dist/tutorials/Costs-and-Run-Cost.md +136 -0
- package/dist/tutorials/Docker.md +171 -0
- package/dist/tutorials/Document-Themes.md +207 -0
- package/dist/tutorials/Editing-a-Project.md +204 -0
- package/dist/tutorials/FAQ.md +134 -0
- package/dist/tutorials/First-Launch-and-Welcome.md +153 -0
- package/dist/tutorials/Home-and-Projects.md +174 -0
- package/dist/tutorials/Home.md +119 -0
- package/dist/tutorials/Install.md +154 -0
- package/dist/tutorials/Intros-and-Outros.md +86 -0
- package/dist/tutorials/Keyboard-Shortcuts-and-Command-Palette.md +215 -0
- package/dist/tutorials/Library-Overview.md +75 -0
- package/dist/tutorials/Models.md +133 -0
- package/dist/tutorials/Multiple-Voices.md +161 -0
- package/dist/tutorials/Narration-Aliases-and-Glossary.md +129 -0
- package/dist/tutorials/Notifications.md +86 -0
- package/dist/tutorials/Other-Languages.md +110 -0
- package/dist/tutorials/PDF-Documents.md +100 -0
- package/dist/tutorials/Play-Images.md +135 -0
- package/dist/tutorials/Play-Narration.md +167 -0
- package/dist/tutorials/Play-Outputs.md +109 -0
- package/dist/tutorials/Play-Overview.md +216 -0
- package/dist/tutorials/Play-Title-and-Article.md +135 -0
- package/dist/tutorials/Play-Video-and-Style.md +143 -0
- package/dist/tutorials/Project-Page.md +228 -0
- package/dist/tutorials/Prompts.md +136 -0
- package/dist/tutorials/Providers-and-Keys.md +290 -0
- package/dist/tutorials/Publishing-to-YouTube.md +135 -0
- package/dist/tutorials/Recovery-and-Retries.md +141 -0
- package/dist/tutorials/Reviews-and-Checkpoints.md +143 -0
- package/dist/tutorials/Schedules.md +154 -0
- package/dist/tutorials/Settings-Reference.md +258 -0
- package/dist/tutorials/Shorts.md +116 -0
- package/dist/tutorials/Start-at-Login.md +126 -0
- package/dist/tutorials/Studio-Extension.md +146 -0
- package/dist/tutorials/Templates.md +146 -0
- package/dist/tutorials/Trash.md +87 -0
- package/dist/tutorials/Troubleshooting.md +149 -0
- package/dist/tutorials/Uninstalling-and-Moving.md +113 -0
- package/dist/tutorials/Updating-and-Patch-Notes.md +120 -0
- package/dist/tutorials/Video-Editing.md +135 -0
- package/dist/tutorials/Where-Your-Files-Live.md +149 -0
- package/dist/tutorials/YouTube-Description.md +132 -0
- package/dist/tutorials/Your-First-Short.md +125 -0
- package/dist/tutorials/_Footer.md +1 -0
- package/dist/tutorials/_Sidebar.md +69 -0
- package/dist/updater/service.js +66 -21
- package/dist/web/assets/index-DBLSfqA-.css +1 -0
- package/dist/web/assets/index-DFdIhmm0.js +307 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
- package/dist/web/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
- package/dist/web/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
- package/dist/web/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
- package/dist/web/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
- package/dist/web/assets/{pdf-BLF2i8c2.js → pdf-B9OY1ioX.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +5 -5
- package/dist/edge/docker-launch.js +0 -81
- package/dist/edge/docker-projects/activation.js +0 -35
- package/dist/edge/docker-projects/claims.js +0 -85
- package/dist/edge/docker-projects/committed.js +0 -40
- package/dist/edge/docker-projects/engine.js +0 -461
- package/dist/edge/docker-projects/install.js +0 -254
- package/dist/edge/docker-projects/prune.js +0 -66
- package/dist/edge/docker-projects/recover.js +0 -100
- package/dist/edge/docker-projects/state.js +0 -225
- package/dist/web/assets/index-CktpxlJd.js +0 -171
- package/dist/web/assets/index-DB-vOnGU.css +0 -1
- package/scripts/docker-run.sh +0 -3
- /package/dist/edge/{docker-projects → docker-install}/tree.js +0 -0
|
@@ -0,0 +1,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
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { languageInfo } from "../../kernel/ports/languages.js";
|
|
2
|
+
// Which saved voices speak a project's language. A voice whose languages are unknown (saved
|
|
3
|
+
// before voices had any, or one the provider said nothing about) is offered for every
|
|
4
|
+
// language: hiding it would hide most voices people already have.
|
|
5
|
+
export function voiceSpeaks(voice, language) {
|
|
6
|
+
if (voice.languages === undefined)
|
|
7
|
+
return true;
|
|
8
|
+
return voice.languages.includes(languageInfo(language).code);
|
|
9
|
+
}
|
|
10
|
+
// The voice picker's list: the voices that speak the language, or every voice when the person
|
|
11
|
+
// asked to see them all. The chosen voice always stays listed so the picker never goes blank.
|
|
12
|
+
export function voicesForLanguage(voices, language, showAll, chosen) {
|
|
13
|
+
if (showAll)
|
|
14
|
+
return voices;
|
|
15
|
+
return voices.filter((voice) => voiceSpeaks(voice, language) || voice.voiceId === chosen);
|
|
16
|
+
}
|
|
17
|
+
// A warning, never a refusal: the provider's language list may be incomplete, and many voices
|
|
18
|
+
// read other languages well. Undefined when there is nothing to say.
|
|
19
|
+
export function voiceLanguageWarning(voice, language) {
|
|
20
|
+
if (voice === undefined || voiceSpeaks(voice, language))
|
|
21
|
+
return undefined;
|
|
22
|
+
const wanted = languageInfo(language).name;
|
|
23
|
+
const spoken = (voice.languages ?? []).map((code) => languageInfo(code).code === code ? languageInfo(code).name : code);
|
|
24
|
+
return `${voice.name} is listed for ${spoken.join(", ")}, not ${wanted}, so it may read the ${wanted} narration with the wrong accent. Pick a ${wanted} voice under Narration, or correct the voice's languages in Settings → Voices.`;
|
|
25
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
// Multiple voices: a run whose narration is a script of speaker turns rather than one voice
|
|
3
|
+
// reading an article. Absent on a run is the Narration format, which is every project saved
|
|
4
|
+
// before this existed, and nothing about such a run reads anything in this slice.
|
|
5
|
+
export const voiceFormats = ["audiobook", "podcast", "drama", "interview"];
|
|
6
|
+
export const voiceFormatLabels = {
|
|
7
|
+
audiobook: "Audiobook",
|
|
8
|
+
podcast: "Podcast",
|
|
9
|
+
drama: "Radio drama",
|
|
10
|
+
interview: "Interview",
|
|
11
|
+
};
|
|
12
|
+
export const speakerRoles = ["narrator", "host", "guest", "character"];
|
|
13
|
+
// Where the script comes from. `script` asks the text model for speaker turns with a Script
|
|
14
|
+
// prompt; `attribute` (audiobooks) writes or takes the article as usual and has the text model
|
|
15
|
+
// hand its dialogue to the speakers.
|
|
16
|
+
export const scriptSources = ["script", "attribute"];
|
|
17
|
+
export const speakersMax = 10;
|
|
18
|
+
export const speakerNameMax = 40;
|
|
19
|
+
export const paceSteps = [0.8, 0.9, 1, 1.1, 1.2];
|
|
20
|
+
export const turnGapSteps = [0, 0.2, 0.35, 0.5, 0.8, 1.2];
|
|
21
|
+
export const defaultTurnGapSeconds = 0.35;
|
|
22
|
+
const voiceChoiceSchema = z.object({
|
|
23
|
+
provider: z.string(),
|
|
24
|
+
model: z.string(),
|
|
25
|
+
voice: z.string(),
|
|
26
|
+
});
|
|
27
|
+
export const speakerSchema = z.object({
|
|
28
|
+
// Stable across renames, so a turn keeps its speaker when the name is edited.
|
|
29
|
+
id: z
|
|
30
|
+
.string()
|
|
31
|
+
.min(1)
|
|
32
|
+
.max(60)
|
|
33
|
+
.regex(/^[a-z0-9-]+$/),
|
|
34
|
+
name: z.string().max(speakerNameMax),
|
|
35
|
+
role: z.enum(speakerRoles),
|
|
36
|
+
voice: voiceChoiceSchema,
|
|
37
|
+
// Played faster or slower after synthesis, so a change re-joins the audio and pays for nothing.
|
|
38
|
+
pace: z.number().optional(),
|
|
39
|
+
// `Term: /IPA/` lines, the Pronunciation Glossary's own format, for this speaker only.
|
|
40
|
+
pronunciations: z.string().max(20_000).optional(),
|
|
41
|
+
// The cast entry this speaker was picked from, when the cast library holds it.
|
|
42
|
+
castId: z.string().max(200).optional(),
|
|
43
|
+
// SHA-256 of the cast member's first picture when the run started: the portrait in a podcast
|
|
44
|
+
// or interview's speaker panel. Absent is the initials tile.
|
|
45
|
+
portrait: z
|
|
46
|
+
.string()
|
|
47
|
+
.regex(/^[0-9a-f]{64}$/)
|
|
48
|
+
.optional(),
|
|
49
|
+
});
|
|
50
|
+
// A book: a series of audiobook projects, one per chapter, that share their speakers, voices,
|
|
51
|
+
// cast and settings. "Make the next chapter" on a finished chapter starts the next one in Play.
|
|
52
|
+
// The MP3 and M4B carry it as their album and track. Absent is a project on its own, which is
|
|
53
|
+
// every project made before this.
|
|
54
|
+
export const bookTitleMax = 200;
|
|
55
|
+
export const bookChapterMax = 9999;
|
|
56
|
+
export const bookSchema = z
|
|
57
|
+
.object({ title: z.string().max(bookTitleMax), chapter: z.number() })
|
|
58
|
+
.strict()
|
|
59
|
+
.readonly();
|
|
60
|
+
export function bookLabel(book) {
|
|
61
|
+
return `${book.title.trim()} · Chapter ${String(book.chapter)}`;
|
|
62
|
+
}
|
|
63
|
+
export const voicesSettingsSchema = z.object({
|
|
64
|
+
format: z.enum(voiceFormats),
|
|
65
|
+
source: z.enum(scriptSources),
|
|
66
|
+
speakers: z.array(speakerSchema).max(speakersMax),
|
|
67
|
+
turnGapSeconds: z.number(),
|
|
68
|
+
// The speaker's name before their captions ("Ada: ...").
|
|
69
|
+
nameTags: z.boolean(),
|
|
70
|
+
// Consecutive turns go to a provider's own multi-speaker request where it has one.
|
|
71
|
+
nativeDialogue: z.boolean(),
|
|
72
|
+
// MP3 and M4B files with chapter markers beside the video.
|
|
73
|
+
audioFiles: z.boolean(),
|
|
74
|
+
// The book this project is a chapter of. Absent is none.
|
|
75
|
+
book: bookSchema.optional(),
|
|
76
|
+
});
|
|
77
|
+
// `hosts` are the channel's hosts as speakers (`castHosts`): a podcast or an interview starts
|
|
78
|
+
// with them in place of the placeholder hosts, and the other formats ignore them.
|
|
79
|
+
export function defaultVoicesSettings(format, hosts = []) {
|
|
80
|
+
return {
|
|
81
|
+
format,
|
|
82
|
+
source: "script",
|
|
83
|
+
speakers: withHosts(format, starterSpeakers(format), hosts),
|
|
84
|
+
turnGapSeconds: defaultTurnGapSeconds,
|
|
85
|
+
nameTags: format === "podcast" || format === "interview",
|
|
86
|
+
nativeDialogue: true,
|
|
87
|
+
audioFiles: true,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
// A podcast keeps a placeholder host only while it has fewer than two; an interview keeps its
|
|
91
|
+
// guest after the hosts.
|
|
92
|
+
function withHosts(format, starters, hosts) {
|
|
93
|
+
if (hosts.length === 0 || (format !== "podcast" && format !== "interview"))
|
|
94
|
+
return [...starters];
|
|
95
|
+
const cast = hosts.slice(0, speakersMax - 1).map((host) => ({ ...host, role: "host" }));
|
|
96
|
+
const fill = format === "podcast"
|
|
97
|
+
? starters.slice(0, Math.max(0, 2 - cast.length))
|
|
98
|
+
: starters.filter((one) => one.role !== "host");
|
|
99
|
+
return [...cast, ...fill.filter((one) => !cast.some((host) => host.id === one.id))];
|
|
100
|
+
}
|
|
101
|
+
function starterSpeakers(format) {
|
|
102
|
+
const blank = { provider: "", model: "", voice: "" };
|
|
103
|
+
const speaker = (id, name, role) => ({
|
|
104
|
+
id,
|
|
105
|
+
name,
|
|
106
|
+
role,
|
|
107
|
+
voice: blank,
|
|
108
|
+
});
|
|
109
|
+
switch (format) {
|
|
110
|
+
case "audiobook":
|
|
111
|
+
return [speaker("narrator", "Narrator", "narrator")];
|
|
112
|
+
case "podcast":
|
|
113
|
+
return [speaker("host-1", "Alex", "host"), speaker("host-2", "Sam", "host")];
|
|
114
|
+
case "drama":
|
|
115
|
+
return [
|
|
116
|
+
speaker("narrator", "Narrator", "narrator"),
|
|
117
|
+
speaker("character-1", "Mara", "character"),
|
|
118
|
+
];
|
|
119
|
+
case "interview":
|
|
120
|
+
return [speaker("host-1", "Host", "host"), speaker("guest-1", "Guest", "guest")];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
export function usesVoices(draft) {
|
|
124
|
+
return draft.voices !== undefined && draft.sources.audio === "generate";
|
|
125
|
+
}
|
|
126
|
+
// The article prompt a script run picks is a Script prompt, and the article is the script.
|
|
127
|
+
export function usesScriptPrompt(draft) {
|
|
128
|
+
return usesVoices(draft) && draft.voices?.source === "script";
|
|
129
|
+
}
|
|
130
|
+
// The rules a multi-voice run must pass before it starts, named for the control to fix.
|
|
131
|
+
export function voicesProblems(settings) {
|
|
132
|
+
const problems = [];
|
|
133
|
+
const { speakers } = settings;
|
|
134
|
+
if (speakers.length === 0)
|
|
135
|
+
problems.push({
|
|
136
|
+
field: "voices.speakers",
|
|
137
|
+
message: "Add at least one speaker under Speakers (Play → Narration, or Edit project → Providers).",
|
|
138
|
+
});
|
|
139
|
+
if (settings.source === "attribute" && settings.format !== "audiobook")
|
|
140
|
+
problems.push({
|
|
141
|
+
field: "voices.source",
|
|
142
|
+
message: "Only audiobooks can hand an existing text's dialogue to speakers. Choose Write a script under Speakers (Play → Narration, or Edit project → Providers).",
|
|
143
|
+
});
|
|
144
|
+
if ((settings.format === "podcast" || settings.format === "interview") && speakers.length < 2)
|
|
145
|
+
problems.push({
|
|
146
|
+
field: "voices.speakers",
|
|
147
|
+
message: `A ${voiceFormatLabels[settings.format].toLowerCase()} needs at least two speakers. Add one under Speakers (Play → Narration, or Edit project → Providers).`,
|
|
148
|
+
});
|
|
149
|
+
if ((settings.format === "audiobook" || settings.format === "drama") &&
|
|
150
|
+
!speakers.some((speaker) => speaker.role === "narrator"))
|
|
151
|
+
problems.push({
|
|
152
|
+
field: "voices.speakers",
|
|
153
|
+
message: `An ${settings.format === "drama" ? "radio drama" : "audiobook"} needs a narrator. Set one speaker's role to Narrator under Speakers (Play → Narration, or Edit project → Providers).`,
|
|
154
|
+
});
|
|
155
|
+
const names = new Set();
|
|
156
|
+
const ids = new Set();
|
|
157
|
+
for (const [index, speaker] of speakers.entries()) {
|
|
158
|
+
const field = `voices.speakers.${String(index)}`;
|
|
159
|
+
const name = speaker.name.trim();
|
|
160
|
+
if (name === "")
|
|
161
|
+
problems.push({ field: `${field}.name`, message: "Enter a name for this speaker." });
|
|
162
|
+
else if (!/^[\p{L}\p{N}][\p{L}\p{N} .'-]*$/u.test(name))
|
|
163
|
+
problems.push({
|
|
164
|
+
field: `${field}.name`,
|
|
165
|
+
message: "Use only letters, numbers, spaces, dots, apostrophes and hyphens in a speaker name, starting with a letter or number.",
|
|
166
|
+
});
|
|
167
|
+
else if (names.has(name.toLowerCase()))
|
|
168
|
+
problems.push({
|
|
169
|
+
field: `${field}.name`,
|
|
170
|
+
message: `Two speakers are called "${name}". Give each speaker a different name.`,
|
|
171
|
+
});
|
|
172
|
+
names.add(name.toLowerCase());
|
|
173
|
+
if (ids.has(speaker.id))
|
|
174
|
+
problems.push({
|
|
175
|
+
field: `${field}.name`,
|
|
176
|
+
message: "Two speakers share one id. Remove one of them and add it again.",
|
|
177
|
+
});
|
|
178
|
+
ids.add(speaker.id);
|
|
179
|
+
if (speaker.voice.provider.trim() === "" || speaker.voice.model.trim() === "")
|
|
180
|
+
problems.push({
|
|
181
|
+
field: `${field}.voice`,
|
|
182
|
+
message: `Choose a voice provider and model for ${name || "this speaker"}.`,
|
|
183
|
+
});
|
|
184
|
+
else if (speaker.voice.voice.trim() === "")
|
|
185
|
+
problems.push({
|
|
186
|
+
field: `${field}.voice.voice`,
|
|
187
|
+
message: `Choose a voice for ${name || "this speaker"}.`,
|
|
188
|
+
});
|
|
189
|
+
if (speaker.pace !== undefined && !paceSteps.includes(speaker.pace))
|
|
190
|
+
problems.push({
|
|
191
|
+
field: `${field}.pace`,
|
|
192
|
+
message: `Pick a pace from the list for ${name || "this speaker"}.`,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
if (settings.book !== undefined) {
|
|
196
|
+
if (settings.book.title.trim() === "")
|
|
197
|
+
problems.push({
|
|
198
|
+
field: "voices.book.title",
|
|
199
|
+
message: "Enter the book's title under Speakers (Play → Narration), or remove the book to make a project on its own.",
|
|
200
|
+
});
|
|
201
|
+
if (!Number.isInteger(settings.book.chapter) ||
|
|
202
|
+
settings.book.chapter < 1 ||
|
|
203
|
+
settings.book.chapter > bookChapterMax)
|
|
204
|
+
problems.push({
|
|
205
|
+
field: "voices.book.chapter",
|
|
206
|
+
message: `Enter a chapter number from 1 to ${String(bookChapterMax)} under Speakers (Play → Narration).`,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
if (!turnGapSteps.includes(settings.turnGapSeconds))
|
|
210
|
+
problems.push({
|
|
211
|
+
field: "voices.turnGapSeconds",
|
|
212
|
+
message: "Pick a gap between turns from the list under Speakers (Play → Narration, or Edit project → Providers).",
|
|
213
|
+
});
|
|
214
|
+
return problems;
|
|
215
|
+
}
|
|
216
|
+
export function speakerPace(speaker) {
|
|
217
|
+
return speaker.pace ?? 1;
|
|
218
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// One colour per speaker, by their place in the speaker list, for captions, the speaker panel
|
|
2
|
+
// and the script view. The same six values are the `--color-speaker-N` design tokens in
|
|
3
|
+
// `packages/web/src/styles/index.css`; a seventh speaker starts the list again.
|
|
4
|
+
export const speakerPalette = [
|
|
5
|
+
"#f2c14e",
|
|
6
|
+
"#6ec3f4",
|
|
7
|
+
"#f28482",
|
|
8
|
+
"#9bcb4f",
|
|
9
|
+
"#c39bd3",
|
|
10
|
+
"#f5a65b",
|
|
11
|
+
];
|
|
12
|
+
export function speakerColour(index) {
|
|
13
|
+
return (speakerPalette[((index % speakerPalette.length) + speakerPalette.length) % speakerPalette.length] ?? "#ffffff");
|
|
14
|
+
}
|
|
15
|
+
// ASS writes colours blue first: &HAABBGGRR in a style (alpha 00 is opaque) and &HBBGGRR& in
|
|
16
|
+
// an inline override.
|
|
17
|
+
export function assColour(hex, alpha = 0) {
|
|
18
|
+
return `&H${alpha.toString(16).padStart(2, "0")}${bgr(hex)}`.toUpperCase();
|
|
19
|
+
}
|
|
20
|
+
export function assInlineColour(hex) {
|
|
21
|
+
return `&H${bgr(hex)}&`.toUpperCase();
|
|
22
|
+
}
|
|
23
|
+
function bgr(hex) {
|
|
24
|
+
const value = hex.replace("#", "");
|
|
25
|
+
return `${value.slice(4, 6)}${value.slice(2, 4)}${value.slice(0, 2)}`;
|
|
26
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { assInlineColour } from "./palette.js";
|
|
2
|
+
// The podcast and interview layout, drawn by libass with the captions: a row of speaker tiles
|
|
3
|
+
// over the image flow, the speaker who is talking lit in their colour, and their name as a
|
|
4
|
+
// lower third. A tile shows the speaker's initials, or, when their cast member has a picture,
|
|
5
|
+
// is left empty for the renderer to lay the portrait into (`panelTiles` is where each tile
|
|
6
|
+
// sits, `video/ffmpeg.ts` the overlay) and the lit outline is drawn over it.
|
|
7
|
+
export function usesSpeakerPanel(format) {
|
|
8
|
+
return format === "podcast" || format === "interview";
|
|
9
|
+
}
|
|
10
|
+
// Consecutive cues of one speaker are one run; a pause under 1.5 s does not end it, so the
|
|
11
|
+
// tile does not flicker between sentences.
|
|
12
|
+
export function speakerRuns(cues) {
|
|
13
|
+
const runs = [];
|
|
14
|
+
for (const cue of cues) {
|
|
15
|
+
if (cue.speaker === undefined)
|
|
16
|
+
continue;
|
|
17
|
+
const last = runs.at(-1);
|
|
18
|
+
if (last !== undefined && last.speaker === cue.speaker && cue.start - last.end < 1.5)
|
|
19
|
+
runs[runs.length - 1] = { ...last, end: cue.end };
|
|
20
|
+
else
|
|
21
|
+
runs.push({ speaker: cue.speaker, start: cue.start, end: cue.end });
|
|
22
|
+
}
|
|
23
|
+
return runs;
|
|
24
|
+
}
|
|
25
|
+
export function initials(name) {
|
|
26
|
+
const parts = name.trim().split(/\s+/).filter(Boolean);
|
|
27
|
+
const letters = parts.length > 1
|
|
28
|
+
? `${parts[0]?.[0] ?? ""}${parts.at(-1)?.[0] ?? ""}`
|
|
29
|
+
: (parts[0] ?? "").slice(0, 2);
|
|
30
|
+
return letters.toUpperCase();
|
|
31
|
+
}
|
|
32
|
+
// Where each speaker's square tile sits: one row, centred near the top of the frame.
|
|
33
|
+
export function panelTiles(count, frame) {
|
|
34
|
+
const { width, height } = frame;
|
|
35
|
+
const size = Math.round(Math.min(width, height) * 0.11);
|
|
36
|
+
const gap = Math.round(size * 0.3);
|
|
37
|
+
const row = count * size + (count - 1) * gap;
|
|
38
|
+
const left = Math.round((width - row) / 2);
|
|
39
|
+
const y = Math.round(height * 0.05);
|
|
40
|
+
return Array.from({ length: count }, (_value, index) => ({
|
|
41
|
+
x: left + index * (size + gap),
|
|
42
|
+
y,
|
|
43
|
+
size,
|
|
44
|
+
}));
|
|
45
|
+
}
|
|
46
|
+
export function speakerPanelEvents(cues, speakers, frame, totalSeconds) {
|
|
47
|
+
if (speakers.length === 0 || totalSeconds <= 0)
|
|
48
|
+
return [];
|
|
49
|
+
const { width, height } = frame;
|
|
50
|
+
const tiles = panelTiles(speakers.length, frame);
|
|
51
|
+
const size = tiles[0]?.size ?? 0;
|
|
52
|
+
const top = tiles[0]?.y ?? 0;
|
|
53
|
+
const end = stamp(totalSeconds);
|
|
54
|
+
const box = (w, h) => `m 0 0 l ${w} 0 ${w} ${h} 0 ${h}`;
|
|
55
|
+
const events = [];
|
|
56
|
+
for (const [index, speaker] of speakers.entries()) {
|
|
57
|
+
const x = tiles[index]?.x ?? 0;
|
|
58
|
+
// The portrait fills this tile in the render; the initials would only cover it.
|
|
59
|
+
if (speaker.portrait === true)
|
|
60
|
+
continue;
|
|
61
|
+
events.push(`Dialogue: 1,0:00:00.00,${end},Default,,0,0,0,,{\\an7\\pos(${x},${top})\\p1\\bord0\\shad0\\1c&H202020&\\1a&H40&}${box(size, size)}{\\p0}`, `Dialogue: 3,0:00:00.00,${end},Default,,0,0,0,,{\\an5\\pos(${x + size / 2},${top + size / 2})\\fs${Math.round(size * 0.4)}\\b1\\bord0\\shad0\\1c${assInlineColour(speaker.colour)}}${plain(initials(speaker.name))}`);
|
|
62
|
+
}
|
|
63
|
+
const index = new Map(speakers.map((speaker, at) => [speaker.id, at]));
|
|
64
|
+
const lowerSize = Math.round(Math.min(width, height) * 0.045);
|
|
65
|
+
for (const run of speakerRuns(cues)) {
|
|
66
|
+
const at = index.get(run.speaker);
|
|
67
|
+
const speaker = at === undefined ? undefined : speakers[at];
|
|
68
|
+
if (at === undefined || speaker === undefined)
|
|
69
|
+
continue;
|
|
70
|
+
const x = tiles[at]?.x ?? 0;
|
|
71
|
+
const colour = assInlineColour(speaker.colour);
|
|
72
|
+
const from = stamp(run.start);
|
|
73
|
+
const to = stamp(run.end);
|
|
74
|
+
events.push(
|
|
75
|
+
// The lit tile: an outline in the speaker's colour round their tile.
|
|
76
|
+
`Dialogue: 2,${from},${to},Default,,0,0,0,,{\\an7\\pos(${x},${top})\\p1\\bord${Math.max(3, Math.round(size / 20))}\\shad0\\1a&HFF&\\3c${colour}}${box(size, size)}{\\p0}`,
|
|
77
|
+
// The lower third: the name on a dark band at the left, above where captions sit.
|
|
78
|
+
`Dialogue: 2,${from},${to},Default,,0,0,0,,{\\an1\\pos(${Math.round(width * 0.05)},${Math.round(height * 0.74)})\\fs${lowerSize}\\b1\\bord${Math.round(lowerSize / 3)}\\3c&H101010&\\3a&H60&\\shad0\\1c${colour}\\fad(200,200)}${plain(speaker.name)}`);
|
|
79
|
+
}
|
|
80
|
+
return events;
|
|
81
|
+
}
|
|
82
|
+
function plain(text) {
|
|
83
|
+
return text
|
|
84
|
+
.replace(/[{}\\]/g, "")
|
|
85
|
+
.replace(/\s+/g, " ")
|
|
86
|
+
.trim();
|
|
87
|
+
}
|
|
88
|
+
function stamp(seconds) {
|
|
89
|
+
const cs = Math.max(0, Math.round(seconds * 100));
|
|
90
|
+
return `${Math.floor(cs / 360_000)}:${String(Math.floor(cs / 6000) % 60).padStart(2, "0")}:${String(Math.floor(cs / 100) % 60).padStart(2, "0")}.${String(cs % 100).padStart(2, "0")}`;
|
|
91
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { sniffImage } from "../channels/images.js";
|
|
4
|
+
import { imageBlob } from "../channels/repo.js";
|
|
5
|
+
import { usesVoices } from "./model.js";
|
|
6
|
+
import { panelTiles, usesSpeakerPanel } from "./panel.js";
|
|
7
|
+
export function panelPortraits(db, voices) {
|
|
8
|
+
if (voices === undefined || !usesSpeakerPanel(voices.format))
|
|
9
|
+
return [];
|
|
10
|
+
return voices.speakers.map((speaker) => {
|
|
11
|
+
if (speaker.portrait === undefined)
|
|
12
|
+
return undefined;
|
|
13
|
+
const blob = imageBlob(db, speaker.portrait);
|
|
14
|
+
const mime = blob === undefined ? undefined : sniffImage(blob.bytes);
|
|
15
|
+
return blob === undefined || mime === undefined
|
|
16
|
+
? undefined
|
|
17
|
+
: {
|
|
18
|
+
sha256: speaker.portrait,
|
|
19
|
+
bytes: blob.bytes,
|
|
20
|
+
extension: mime === "image/png" ? ".png" : ".jpg",
|
|
21
|
+
};
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
// What the portraits add to the caption file's fingerprint: nothing unless a panel speaker has
|
|
25
|
+
// one, so captions made before portraits (or without any) keep the fingerprint they had.
|
|
26
|
+
export function portraitValues(voices) {
|
|
27
|
+
if (!usesSpeakerPanel(voices.format))
|
|
28
|
+
return undefined;
|
|
29
|
+
const values = voices.speakers.map((speaker) => speaker.portrait ?? null);
|
|
30
|
+
return values.some((value) => value !== null) ? values : undefined;
|
|
31
|
+
}
|
|
32
|
+
// The portraits written beside the caption file the join reads, at the tiles the caption file
|
|
33
|
+
// left empty for them. Undefined when no speaker has one, so every other video renders with the
|
|
34
|
+
// arguments it always had.
|
|
35
|
+
export function writePortraits(db, config, directory) {
|
|
36
|
+
const voices = usesVoices(config) ? config.voices : undefined;
|
|
37
|
+
const portraits = panelPortraits(db, voices);
|
|
38
|
+
if (voices === undefined || portraits.every((one) => one === undefined))
|
|
39
|
+
return undefined;
|
|
40
|
+
// The caption file's frame (`rebuild/runtime-subtitles.ts`), so a portrait lands in its tile.
|
|
41
|
+
const tiles = panelTiles(voices.speakers.length, config.format === "16:9" ? { width: 1920, height: 1080 } : { width: 1080, height: 1920 });
|
|
42
|
+
const overlays = [];
|
|
43
|
+
const recorded = [];
|
|
44
|
+
portraits.forEach((portrait, index) => {
|
|
45
|
+
const tile = tiles[index];
|
|
46
|
+
const speaker = voices.speakers[index];
|
|
47
|
+
if (portrait === undefined || tile === undefined || speaker === undefined)
|
|
48
|
+
return;
|
|
49
|
+
const path = `portrait-${String(index)}${portrait.extension}`;
|
|
50
|
+
writeFileSync(join(directory, path), portrait.bytes, { mode: 0o600 });
|
|
51
|
+
overlays.push({ path, x: tile.x, y: tile.y, size: tile.size });
|
|
52
|
+
recorded.push({ speaker: speaker.id, sha256: portrait.sha256 });
|
|
53
|
+
});
|
|
54
|
+
return { overlays, recorded };
|
|
55
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { voiceFormatLabels } from "./model.js";
|
|
2
|
+
const heading = /^#{1,6}\s+(.+?)\s*#*\s*$/;
|
|
3
|
+
const turnLine = /^\*{0,2}([^:*\n]{1,60}?)\*{0,2}\s*:\s*\*{0,2}\s*(.*)$/;
|
|
4
|
+
// With `keepMarkdown`, every turn also carries its lines as written (`markdown`).
|
|
5
|
+
export function parseScript(markdown, speakers, keepMarkdown = false) {
|
|
6
|
+
const byName = new Map(speakers.map((speaker) => [speaker.name.trim().toLowerCase(), speaker]));
|
|
7
|
+
const names = speakers.map((speaker) => speaker.name.trim()).join(", ");
|
|
8
|
+
const turns = [];
|
|
9
|
+
const sections = [];
|
|
10
|
+
let open = false;
|
|
11
|
+
for (const [at, raw] of markdown.replace(/\r\n?/g, "\n").split("\n").entries()) {
|
|
12
|
+
const line = raw.trim();
|
|
13
|
+
const number = at + 1;
|
|
14
|
+
if (line === "") {
|
|
15
|
+
open = false;
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
const title = heading.exec(line);
|
|
19
|
+
if (title !== null) {
|
|
20
|
+
sections.push({ title: clean(title[1] ?? ""), firstTurn: turns.length + 1 });
|
|
21
|
+
open = false;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
// Horizontal rules and the like carry no words.
|
|
25
|
+
if (/^([-*_=~])\1{2,}$/.test(line)) {
|
|
26
|
+
open = false;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
const turn = turnLine.exec(line);
|
|
30
|
+
const speaker = turn === null ? undefined : byName.get((turn[1] ?? "").trim().toLowerCase());
|
|
31
|
+
if (turn !== null && speaker !== undefined) {
|
|
32
|
+
turns.push({ speaker: speaker.id, lines: [turn[2] ?? ""], section: sections.length - 1 });
|
|
33
|
+
open = true;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
const last = turns.at(-1);
|
|
37
|
+
if (open && last !== undefined) {
|
|
38
|
+
last.lines.push(line);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
ok: false,
|
|
43
|
+
reason: turn === null
|
|
44
|
+
? `Line ${String(number)} of the script is not a turn. Start every turn with a speaker's name and a colon (${names}), one turn per paragraph.`
|
|
45
|
+
: `Line ${String(number)} of the script is spoken by "${(turn[1] ?? "").trim()}", who is not one of the speakers (${names}). Use only these names, or add the speaker under Speakers (Play → Narration, or Edit project → Providers).`,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
const spoken = [];
|
|
49
|
+
for (const turn of turns) {
|
|
50
|
+
const text = clean(turn.lines.join(" "));
|
|
51
|
+
if (text === "")
|
|
52
|
+
continue;
|
|
53
|
+
spoken.push({
|
|
54
|
+
index: spoken.length + 1,
|
|
55
|
+
speaker: turn.speaker,
|
|
56
|
+
text,
|
|
57
|
+
section: turn.section,
|
|
58
|
+
...(keepMarkdown ? { markdown: turn.lines.join("\n") } : {}),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
if (spoken.length === 0)
|
|
62
|
+
return {
|
|
63
|
+
ok: false,
|
|
64
|
+
reason: `The script has no spoken turns. Write each turn as a speaker's name, a colon and their words (${names}).`,
|
|
65
|
+
};
|
|
66
|
+
// A section keeps its place by its first turn that has words; one without words is dropped.
|
|
67
|
+
const kept = [];
|
|
68
|
+
const sectionMap = new Map();
|
|
69
|
+
for (const [at, section] of sections.entries()) {
|
|
70
|
+
const first = spoken.find((turn) => turn.section === at);
|
|
71
|
+
if (first === undefined || section.title === "")
|
|
72
|
+
continue;
|
|
73
|
+
sectionMap.set(at, kept.length);
|
|
74
|
+
kept.push({ title: section.title, firstTurn: first.index });
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
ok: true,
|
|
78
|
+
script: {
|
|
79
|
+
turns: spoken.map((turn) => ({ ...turn, section: sectionMap.get(turn.section) ?? -1 })),
|
|
80
|
+
sections: kept,
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
// Emphasis, links and stage directions come out; the words stay.
|
|
85
|
+
function clean(text) {
|
|
86
|
+
return text
|
|
87
|
+
.replace(/\[[^\]]*\]\([^)]*\)/g, (link) => link.slice(1, link.indexOf("]")))
|
|
88
|
+
.replace(/\[[^\]]*\]/g, " ")
|
|
89
|
+
.replace(/(\*{1,3}|_{1,3})(\S(?:.*?\S)?)\1/g, "$2")
|
|
90
|
+
.replace(/`/g, "")
|
|
91
|
+
.replace(/\s+/g, " ")
|
|
92
|
+
.trim();
|
|
93
|
+
}
|
|
94
|
+
// The script as plain text for the reading view and the Script download: the same format,
|
|
95
|
+
// normalised, so it parses back to itself.
|
|
96
|
+
export function formatScript(script, speakers) {
|
|
97
|
+
const names = new Map(speakers.map((speaker) => [speaker.id, speaker.name.trim()]));
|
|
98
|
+
const lines = [];
|
|
99
|
+
for (const turn of script.turns) {
|
|
100
|
+
const section = script.sections.find((one) => one.firstTurn === turn.index);
|
|
101
|
+
if (section !== undefined)
|
|
102
|
+
lines.push(`# ${section.title}`, "");
|
|
103
|
+
lines.push(`${names.get(turn.speaker) ?? turn.speaker}: ${turn.text}`, "");
|
|
104
|
+
}
|
|
105
|
+
return `${lines.join("\n").trim()}\n`;
|
|
106
|
+
}
|
|
107
|
+
const formatGuidance = {
|
|
108
|
+
audiobook: "An audiobook: the narrator reads the prose; characters speak only their own quoted dialogue.",
|
|
109
|
+
podcast: "A podcast: the hosts talk with each other naturally, taking short turns, reacting and building on what the other said.",
|
|
110
|
+
drama: "A radio drama: the narrator sets scenes briefly; the characters carry the story through what they say.",
|
|
111
|
+
interview: "An interview or debate: the host asks and steers; guests answer at length and may disagree.",
|
|
112
|
+
};
|
|
113
|
+
export function scriptContract(format, speakers) {
|
|
114
|
+
return [
|
|
115
|
+
`Write a script for ${voiceFormatLabels[format].toLowerCase()} narration. ${formatGuidance[format]}`,
|
|
116
|
+
"Speakers (name - role):",
|
|
117
|
+
...speakers.map((speaker) => `- ${speaker.name.trim()} - ${speaker.role}`),
|
|
118
|
+
"Format rules, followed exactly:",
|
|
119
|
+
"- Every turn is one paragraph that starts with the speaker's name and a colon, e.g. `Name: words`.",
|
|
120
|
+
"- Separate turns with a blank line. Use only the names above.",
|
|
121
|
+
"- Start each section with a Markdown heading line (`# Title`); sections become chapters.",
|
|
122
|
+
"- Write only words to be spoken: no stage directions, no bracketed notes, no lists or tables.",
|
|
123
|
+
].join("\n");
|
|
124
|
+
}
|
|
125
|
+
export function scriptMessages(format, speakers, prompt, notes) {
|
|
126
|
+
const trimmed = notes?.trim();
|
|
127
|
+
return [
|
|
128
|
+
{ role: "system", content: scriptContract(format, speakers) },
|
|
129
|
+
{
|
|
130
|
+
role: "user",
|
|
131
|
+
content: trimmed === undefined || trimmed === ""
|
|
132
|
+
? prompt
|
|
133
|
+
: `Research notes\n\n${trimmed}\n\n${prompt}`,
|
|
134
|
+
},
|
|
135
|
+
];
|
|
136
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// How far ahead a word is looked for: past it, the aligner is more likely to have left the
|
|
2
|
+
// word out than the transcript to hold that many words it did not hear.
|
|
3
|
+
const lookahead = 60;
|
|
4
|
+
export function attributeWords(turns, words) {
|
|
5
|
+
const tokens = turns.flatMap((turn) => turn.text
|
|
6
|
+
.trim()
|
|
7
|
+
.split(/\s+/)
|
|
8
|
+
.filter((token) => token !== "")
|
|
9
|
+
.map((token) => ({ token, speaker: turn.speaker, turn: turn.turn })));
|
|
10
|
+
const first = tokens[0];
|
|
11
|
+
let at = 0;
|
|
12
|
+
let speaker = first?.speaker;
|
|
13
|
+
let turn = first?.turn;
|
|
14
|
+
return words.map((word) => {
|
|
15
|
+
const parts = word.text.trim().split(/\s+/);
|
|
16
|
+
const head = parts[0] ?? "";
|
|
17
|
+
for (let look = at; look < Math.min(tokens.length, at + lookahead); look += 1) {
|
|
18
|
+
const token = tokens[look];
|
|
19
|
+
if (token === undefined || token.token !== head)
|
|
20
|
+
continue;
|
|
21
|
+
speaker = token.speaker;
|
|
22
|
+
turn = token.turn;
|
|
23
|
+
at = look + parts.length;
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
26
|
+
return speaker === undefined ? word : { ...word, speaker, turn };
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
// Who speaks a caption cue edited by hand: the speaker whose words fill most of its time, or,
|
|
30
|
+
// for a cue moved into a pause, the speaker of the nearest word. Undefined when no word has a
|
|
31
|
+
// speaker (a one-voice run).
|
|
32
|
+
export function cueSpeaker(cue, words) {
|
|
33
|
+
const overlap = new Map();
|
|
34
|
+
let nearest;
|
|
35
|
+
for (const word of words) {
|
|
36
|
+
if (word.speaker === undefined)
|
|
37
|
+
continue;
|
|
38
|
+
const shared = Math.min(cue.end, word.end) - Math.max(cue.start, word.start);
|
|
39
|
+
if (shared > 0)
|
|
40
|
+
overlap.set(word.speaker, (overlap.get(word.speaker) ?? 0) + shared);
|
|
41
|
+
const distance = Math.max(0, word.start - cue.end, cue.start - word.end);
|
|
42
|
+
if (nearest === undefined || distance < nearest.distance)
|
|
43
|
+
nearest = { speaker: word.speaker, distance };
|
|
44
|
+
}
|
|
45
|
+
let best;
|
|
46
|
+
for (const entry of overlap)
|
|
47
|
+
if (best === undefined || entry[1] > best[1])
|
|
48
|
+
best = entry;
|
|
49
|
+
return best?.[0] ?? nearest?.speaker;
|
|
50
|
+
}
|
|
51
|
+
// Where each turn starts on the final timeline: its first timed word.
|
|
52
|
+
export function turnStarts(words) {
|
|
53
|
+
const starts = new Map();
|
|
54
|
+
for (const word of words)
|
|
55
|
+
if (word.turn !== undefined && !starts.has(word.turn))
|
|
56
|
+
starts.set(word.turn, word.start);
|
|
57
|
+
return starts;
|
|
58
|
+
}
|
|
@@ -43,7 +43,7 @@ const answerSchema = z.object({
|
|
|
43
43
|
hashtags: z.array(z.string()),
|
|
44
44
|
tags: z.array(z.string()),
|
|
45
45
|
});
|
|
46
|
-
const fix = "
|
|
46
|
+
const fix = "Try again, or choose another model in Edit project → Providers.";
|
|
47
47
|
// Reads and checks the model's answer. The reason is the sentence the stage shows; the
|
|
48
48
|
// provider wrapper's `check` asks the model again while attempts remain.
|
|
49
49
|
export function checkDescriptionAnswer(text, durationSeconds) {
|