@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,269 @@
|
|
|
1
|
+
import { mkdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { measureFile } from "../loudness/loudnorm.js";
|
|
4
|
+
import { pieceLufs, pieceTruePeak } from "../loudness/model.js";
|
|
5
|
+
import { probeDurationMs, runFfmpeg } from "../video/ffmpeg.js";
|
|
6
|
+
import { sentences } from "./chunk.js";
|
|
7
|
+
// The sentence ends inside a text, not counting its last.
|
|
8
|
+
export function textBoundaries(text) {
|
|
9
|
+
const parts = [...sentences(text)];
|
|
10
|
+
const total = Array.from(text).length;
|
|
11
|
+
if (parts.length < 2 || total === 0)
|
|
12
|
+
return [];
|
|
13
|
+
const boundaries = [];
|
|
14
|
+
let offset = 0;
|
|
15
|
+
for (const part of parts.slice(0, -1)) {
|
|
16
|
+
offset += Array.from(part).length;
|
|
17
|
+
const trailing = /\s*$/.exec(part)?.[0] ?? "";
|
|
18
|
+
// Only a real sentence: text that is only spacing is no sentence of its own.
|
|
19
|
+
if (part.trim() === "")
|
|
20
|
+
continue;
|
|
21
|
+
boundaries.push({
|
|
22
|
+
ratio: offset / total,
|
|
23
|
+
kind: /\n[ \t\r]*\n/.test(trailing) || /\n/.test(trailing) ? "paragraph" : "sentence",
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
return boundaries;
|
|
27
|
+
}
|
|
28
|
+
// silencedetect prints `silence_start: 1.23` and `silence_end: 1.56 | silence_duration: 0.33`.
|
|
29
|
+
// A stretch still open at the end runs to the end of the piece.
|
|
30
|
+
export function parseSilences(stderr, durationSeconds) {
|
|
31
|
+
const silences = [];
|
|
32
|
+
let open;
|
|
33
|
+
for (const match of stderr.matchAll(/silence_(start|end):\s*(-?[0-9.]+)/g)) {
|
|
34
|
+
const value = Math.max(0, Number(match[2]));
|
|
35
|
+
if (!Number.isFinite(value))
|
|
36
|
+
continue;
|
|
37
|
+
if (match[1] === "start")
|
|
38
|
+
open = value;
|
|
39
|
+
else if (open !== undefined) {
|
|
40
|
+
silences.push({ start: open, end: Math.min(durationSeconds, value) });
|
|
41
|
+
open = undefined;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (open !== undefined && open < durationSeconds)
|
|
45
|
+
silences.push({ start: open, end: durationSeconds });
|
|
46
|
+
return silences;
|
|
47
|
+
}
|
|
48
|
+
// ceiling: a sentence end more than two seconds (or a fifth of the piece) from where its words
|
|
49
|
+
// put it is not trusted to be that sentence's pause.
|
|
50
|
+
const windowSeconds = 2;
|
|
51
|
+
const windowShare = 0.2;
|
|
52
|
+
// ceiling: a second of quiet counts for three seconds of distance, so a full stop's pause wins
|
|
53
|
+
// over a comma's breath a little nearer to where the words put the end.
|
|
54
|
+
const durationWeight = 3;
|
|
55
|
+
// A stretch this near either end of the piece is the piece's own lead-in or tail.
|
|
56
|
+
const edgeSeconds = 0.02;
|
|
57
|
+
export function planPauses(input) {
|
|
58
|
+
const { durationSeconds, silences, boundaries, settings } = input;
|
|
59
|
+
const leadingSilence = silences.find((one) => one.start <= edgeSeconds);
|
|
60
|
+
const trailingSilence = silences.find((one) => one.end >= durationSeconds - edgeSeconds);
|
|
61
|
+
const leading = leadingSilence === undefined ? 0 : leadingSilence.end;
|
|
62
|
+
const trailing = trailingSilence === undefined || trailingSilence === leadingSilence
|
|
63
|
+
? 0
|
|
64
|
+
: durationSeconds - trailingSilence.start;
|
|
65
|
+
const speechStart = leading;
|
|
66
|
+
const speechEnd = Math.max(speechStart, durationSeconds - trailing);
|
|
67
|
+
const speech = speechEnd - speechStart;
|
|
68
|
+
const inner = silences.filter((one) => one !== leadingSilence && one !== trailingSilence);
|
|
69
|
+
if (speech <= 0 || inner.length === 0 || boundaries.length === 0)
|
|
70
|
+
return { inserts: [], leading, trailing };
|
|
71
|
+
const window = Math.max(windowSeconds, speech * windowShare);
|
|
72
|
+
const expected = boundaries.map((one) => speechStart + one.ratio * speech);
|
|
73
|
+
// The best in-order matching: every sentence end takes a later stretch than the one before,
|
|
74
|
+
// or none. Cost is the distance from where the words put the end, less the stretch's length
|
|
75
|
+
// weighted (a full stop's pause is longer than a comma's); leaving an end unmatched costs the
|
|
76
|
+
// window.
|
|
77
|
+
const m = boundaries.length;
|
|
78
|
+
const n = inner.length;
|
|
79
|
+
const cost = Array.from({ length: m + 1 }, () => Array.from({ length: n + 1 }, () => Number.POSITIVE_INFINITY));
|
|
80
|
+
const choice = Array.from({ length: m + 1 }, () => Array.from({ length: n + 1 }, () => "skip-end"));
|
|
81
|
+
for (let j = 0; j <= n; j += 1) {
|
|
82
|
+
const row = cost[0];
|
|
83
|
+
if (row !== undefined)
|
|
84
|
+
row[j] = 0;
|
|
85
|
+
}
|
|
86
|
+
for (let i = 1; i <= m; i += 1) {
|
|
87
|
+
const row = cost[i];
|
|
88
|
+
const previous = cost[i - 1];
|
|
89
|
+
const picks = choice[i];
|
|
90
|
+
row[0] = (previous[0] ?? 0) + window;
|
|
91
|
+
picks[0] = "skip-end";
|
|
92
|
+
for (let j = 1; j <= n; j += 1) {
|
|
93
|
+
const silence = inner[j - 1];
|
|
94
|
+
const middle = (silence.start + silence.end) / 2;
|
|
95
|
+
const distance = Math.abs(middle - (expected[i - 1] ?? 0));
|
|
96
|
+
const options = [
|
|
97
|
+
[(previous[j] ?? 0) + window, "skip-end"],
|
|
98
|
+
[row[j - 1] ?? Number.POSITIVE_INFINITY, "skip-silence"],
|
|
99
|
+
];
|
|
100
|
+
if (distance <= window)
|
|
101
|
+
options.push([
|
|
102
|
+
(previous[j - 1] ?? 0) + distance - durationWeight * (silence.end - silence.start),
|
|
103
|
+
"match",
|
|
104
|
+
]);
|
|
105
|
+
const best = options.reduce((low, one) => (one[0] < low[0] ? one : low));
|
|
106
|
+
row[j] = best[0];
|
|
107
|
+
picks[j] = best[1];
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const matched = [];
|
|
111
|
+
let i = m;
|
|
112
|
+
let j = n;
|
|
113
|
+
while (i > 0) {
|
|
114
|
+
const pick = choice[i]?.[j] ?? "skip-end";
|
|
115
|
+
if (j > 0 && pick === "match") {
|
|
116
|
+
matched.push({
|
|
117
|
+
boundary: boundaries[i - 1],
|
|
118
|
+
silence: inner[j - 1],
|
|
119
|
+
});
|
|
120
|
+
i -= 1;
|
|
121
|
+
j -= 1;
|
|
122
|
+
}
|
|
123
|
+
else if (j > 0 && pick === "skip-silence")
|
|
124
|
+
j -= 1;
|
|
125
|
+
else
|
|
126
|
+
i -= 1;
|
|
127
|
+
}
|
|
128
|
+
const inserts = matched.reverse().flatMap(({ boundary, silence }) => {
|
|
129
|
+
const wanted = minimumFor(boundary.kind, settings);
|
|
130
|
+
const have = silence.end - silence.start;
|
|
131
|
+
return wanted > have
|
|
132
|
+
? [{ at: round((silence.start + silence.end) / 2), seconds: round(wanted - have) }]
|
|
133
|
+
: [];
|
|
134
|
+
});
|
|
135
|
+
return { inserts, leading, trailing };
|
|
136
|
+
}
|
|
137
|
+
export function minimumFor(kind, settings) {
|
|
138
|
+
return kind === "paragraph"
|
|
139
|
+
? Math.max(settings.sentenceSeconds, settings.paragraphSeconds)
|
|
140
|
+
: settings.sentenceSeconds;
|
|
141
|
+
}
|
|
142
|
+
// The piece with its silences added and `padEnd` seconds of silence after it, as 16-bit PCM
|
|
143
|
+
// WAV. Every stretch between two inserts is cut out of the piece as it is and played in order.
|
|
144
|
+
export function insertArgs(input, output, inserts, padEnd, format) {
|
|
145
|
+
const layout = format.channels === 1 ? "mono" : "stereo";
|
|
146
|
+
const base = `aformat=sample_fmts=fltp:sample_rates=${String(format.sampleRate)}:channel_layouts=${layout}`;
|
|
147
|
+
const chains = [
|
|
148
|
+
`[0:a]${base},asplit=${String(inserts.length + 1)}${inserts.map((_one, at) => `[s${String(at)}]`).join("")}[s${String(inserts.length)}]`,
|
|
149
|
+
];
|
|
150
|
+
const cuts = [0, ...inserts.map((one) => one.at)];
|
|
151
|
+
cuts.forEach((from, at) => {
|
|
152
|
+
const to = inserts[at]?.at;
|
|
153
|
+
const trim = `atrim=start=${from.toFixed(6)}${to === undefined ? "" : `:end=${to.toFixed(6)}`},asetpts=PTS-STARTPTS`;
|
|
154
|
+
const before = at === 0 ? undefined : inserts[at - 1]?.seconds;
|
|
155
|
+
const delay = before === undefined ? "" : `,adelay=delays=${Math.round(before * 1000)}:all=1`;
|
|
156
|
+
const pad = to === undefined && padEnd > 0 ? `,apad=pad_dur=${padEnd.toFixed(3)}` : "";
|
|
157
|
+
chains.push(`[s${String(at)}]${trim}${delay}${pad}[p${String(at)}]`);
|
|
158
|
+
});
|
|
159
|
+
chains.push(`${cuts.map((_one, at) => `[p${String(at)}]`).join("")}concat=n=${String(cuts.length)}:v=0:a=1[a]`);
|
|
160
|
+
return [
|
|
161
|
+
"-hide_banner",
|
|
162
|
+
"-nostdin",
|
|
163
|
+
"-loglevel",
|
|
164
|
+
"error",
|
|
165
|
+
"-nostats",
|
|
166
|
+
"-y",
|
|
167
|
+
"-i",
|
|
168
|
+
input,
|
|
169
|
+
"-filter_complex",
|
|
170
|
+
chains.join(";"),
|
|
171
|
+
"-map",
|
|
172
|
+
"[a]",
|
|
173
|
+
"-c:a",
|
|
174
|
+
"pcm_s16le",
|
|
175
|
+
"-f",
|
|
176
|
+
"wav",
|
|
177
|
+
output,
|
|
178
|
+
];
|
|
179
|
+
}
|
|
180
|
+
// ceiling: quiet is 22 dB under the piece's own loudness (never above -35 dBFS), held for at
|
|
181
|
+
// least 60 ms: under a sentence's breath, over a room's hiss.
|
|
182
|
+
const quietBelow = 22;
|
|
183
|
+
const quietFloor = -35;
|
|
184
|
+
const quietSeconds = 0.06;
|
|
185
|
+
// Where a piece is quiet, measured on the piece as spoken.
|
|
186
|
+
export async function measurePiece(run, file) {
|
|
187
|
+
const durationSeconds = (await probeDurationMs(run.bin, file, run.signal, run.log)) / 1000;
|
|
188
|
+
const loudness = await measureFile(run, file, { lufs: pieceLufs, truePeak: pieceTruePeak });
|
|
189
|
+
const noise = Number.isFinite(loudness.integrated)
|
|
190
|
+
? Math.min(quietFloor, loudness.integrated - quietBelow)
|
|
191
|
+
: quietFloor;
|
|
192
|
+
let stderr = "";
|
|
193
|
+
await runFfmpeg({
|
|
194
|
+
bin: run.bin,
|
|
195
|
+
args: [
|
|
196
|
+
"-hide_banner",
|
|
197
|
+
"-nostdin",
|
|
198
|
+
"-loglevel",
|
|
199
|
+
"info",
|
|
200
|
+
"-nostats",
|
|
201
|
+
"-i",
|
|
202
|
+
file,
|
|
203
|
+
"-af",
|
|
204
|
+
`silencedetect=noise=${noise.toFixed(1)}dB:d=${String(quietSeconds)}`,
|
|
205
|
+
"-f",
|
|
206
|
+
"null",
|
|
207
|
+
"-",
|
|
208
|
+
],
|
|
209
|
+
signal: run.signal,
|
|
210
|
+
log: run.log,
|
|
211
|
+
onProgress: () => { },
|
|
212
|
+
onStderr: (text) => {
|
|
213
|
+
stderr += text;
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
return { durationSeconds, silences: parseSilences(stderr, durationSeconds) };
|
|
217
|
+
}
|
|
218
|
+
// Every piece's plan, from the pieces as spoken: silences inside, and at each piece's end what
|
|
219
|
+
// the gap to the next piece lacks.
|
|
220
|
+
export async function planPieces(run, pieces, settings) {
|
|
221
|
+
const measured = [];
|
|
222
|
+
for (const piece of pieces) {
|
|
223
|
+
run.signal.throwIfAborted();
|
|
224
|
+
const { durationSeconds, silences } = await measurePiece(run, piece.file);
|
|
225
|
+
measured.push(planPauses({
|
|
226
|
+
durationSeconds,
|
|
227
|
+
silences,
|
|
228
|
+
boundaries: textBoundaries(piece.text),
|
|
229
|
+
settings,
|
|
230
|
+
}));
|
|
231
|
+
}
|
|
232
|
+
return measured.map((pauses, at) => {
|
|
233
|
+
const piece = pieces[at];
|
|
234
|
+
const following = measured[at + 1];
|
|
235
|
+
if (piece.next === "end" || piece.next === "turn" || following === undefined)
|
|
236
|
+
return { pauses, padEnd: 0 };
|
|
237
|
+
const gap = pauses.trailing + following.leading;
|
|
238
|
+
return { pauses, padEnd: round(Math.max(0, minimumFor(piece.next, settings) - gap)) };
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
// The pieces written again with their pauses, as WAVs in `directory`, in the order given.
|
|
242
|
+
// `files` are what is joined (the spoken pieces, or their levelled copies); `plans` are from
|
|
243
|
+
// the spoken pieces.
|
|
244
|
+
export async function applyPauses(run, files, plans, directory) {
|
|
245
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
246
|
+
const written = [];
|
|
247
|
+
let added = 0;
|
|
248
|
+
for (const [index, file] of files.entries()) {
|
|
249
|
+
run.signal.throwIfAborted();
|
|
250
|
+
const plan = plans[index];
|
|
251
|
+
const output = join(directory, `paced-${String(index + 1).padStart(4, "0")}.wav`);
|
|
252
|
+
await runFfmpeg({
|
|
253
|
+
bin: run.bin,
|
|
254
|
+
args: insertArgs(file, output, plan?.pauses.inserts ?? [], plan?.padEnd ?? 0, {
|
|
255
|
+
sampleRate: 44100,
|
|
256
|
+
channels: 1,
|
|
257
|
+
}),
|
|
258
|
+
signal: run.signal,
|
|
259
|
+
log: run.log,
|
|
260
|
+
onProgress: () => { },
|
|
261
|
+
});
|
|
262
|
+
added += (plan?.pauses.inserts.length ?? 0) + ((plan?.padEnd ?? 0) > 0 ? 1 : 0);
|
|
263
|
+
written.push(output);
|
|
264
|
+
}
|
|
265
|
+
return { files: written, added };
|
|
266
|
+
}
|
|
267
|
+
function round(value) {
|
|
268
|
+
return Math.round(value * 1000) / 1000;
|
|
269
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
// The live view's narration waveform: each finished narration piece as a row of peaks, in the
|
|
3
|
+
// order it is spoken, growing as pieces land. Once the whole narration is joined (or was
|
|
4
|
+
// uploaded) that one file replaces the pieces.
|
|
5
|
+
// ceiling: ten bars a second draws a two-minute short in 1200 bars and a two-hour video in
|
|
6
|
+
// 72000, which the browser scales down to the width it has.
|
|
7
|
+
export const peaksPerSecond = 10;
|
|
8
|
+
// The rate the audio is decoded at for peaks only; loudness needs no more than this.
|
|
9
|
+
const decodeRate = 4000;
|
|
10
|
+
// ceiling: three hours of narration at the decode rate, 16-bit mono.
|
|
11
|
+
const decodeMaxBytes = 3 * 3600 * decodeRate * 2;
|
|
12
|
+
// The saved narration audio of a revision, in spoken order: the joined narration when it is
|
|
13
|
+
// ready, else each finished request of the intro, the body and the outro.
|
|
14
|
+
export function narrationAudio(view) {
|
|
15
|
+
const joined = view.outputs.find((row) => row.selected && row.available && row.state === "ready" && row.output.role === "audio_body");
|
|
16
|
+
if (joined !== undefined)
|
|
17
|
+
return {
|
|
18
|
+
complete: true,
|
|
19
|
+
pieces: [{ key: joined.workKey, assetId: joined.assetId }],
|
|
20
|
+
};
|
|
21
|
+
const order = (key) => key.startsWith("audio:intro") ? 0 : key.startsWith("audio:outro") ? 2 : 1;
|
|
22
|
+
return {
|
|
23
|
+
complete: false,
|
|
24
|
+
pieces: view.pieces
|
|
25
|
+
.filter((row) => row.selected &&
|
|
26
|
+
row.available &&
|
|
27
|
+
row.stageKind === "audio" &&
|
|
28
|
+
row.piece.state === "done" &&
|
|
29
|
+
row.assetId !== null &&
|
|
30
|
+
row.piece.payload !== null &&
|
|
31
|
+
spoken(row.piece.payload))
|
|
32
|
+
.toSorted((left, right) => order(left.key) - order(right.key) ||
|
|
33
|
+
left.key.localeCompare(right.key, "en", { numeric: true }))
|
|
34
|
+
.flatMap((row) => (row.assetId === null ? [] : [{ key: row.key, assetId: row.assetId }])),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function spoken(payload) {
|
|
38
|
+
try {
|
|
39
|
+
const value = JSON.parse(payload);
|
|
40
|
+
return typeof value === "object" && value !== null && "segment" in value;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// The loudest sample of each slice of `perSecond` a second, from 0 to 1.
|
|
47
|
+
export function peaksOf(samples, sampleRate, perSecond) {
|
|
48
|
+
const size = Math.max(1, Math.round(sampleRate / perSecond));
|
|
49
|
+
const peaks = [];
|
|
50
|
+
for (let at = 0; at < samples.length; at += size) {
|
|
51
|
+
let loudest = 0;
|
|
52
|
+
const end = Math.min(samples.length, at + size);
|
|
53
|
+
for (let index = at; index < end; index++) {
|
|
54
|
+
const value = Math.abs(samples[index] ?? 0);
|
|
55
|
+
if (value > loudest)
|
|
56
|
+
loudest = value;
|
|
57
|
+
}
|
|
58
|
+
peaks.push(Math.round((loudest / 32768) * 1000) / 1000);
|
|
59
|
+
}
|
|
60
|
+
return peaks;
|
|
61
|
+
}
|
|
62
|
+
// Decodes an audio file with ffmpeg to mono 16-bit samples and returns its peaks.
|
|
63
|
+
export function decodePeaks(ffmpeg, path, signal) {
|
|
64
|
+
return new Promise((resolve, reject) => {
|
|
65
|
+
execFile(ffmpeg, [
|
|
66
|
+
"-hide_banner",
|
|
67
|
+
"-nostdin",
|
|
68
|
+
"-loglevel",
|
|
69
|
+
"error",
|
|
70
|
+
"-i",
|
|
71
|
+
path,
|
|
72
|
+
"-ac",
|
|
73
|
+
"1",
|
|
74
|
+
"-ar",
|
|
75
|
+
String(decodeRate),
|
|
76
|
+
"-f",
|
|
77
|
+
"s16le",
|
|
78
|
+
"pipe:1",
|
|
79
|
+
], { encoding: "buffer", maxBuffer: decodeMaxBytes, signal, windowsHide: true }, (error, stdout) => {
|
|
80
|
+
if (error !== null) {
|
|
81
|
+
reject(new Error(`ffmpeg could not read the narration audio (${error.message}).`));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
// Copied so the samples start on an even byte, which a pooled Buffer may not.
|
|
85
|
+
const bytes = new Uint8Array(stdout.byteLength - (stdout.byteLength % 2));
|
|
86
|
+
bytes.set(stdout.subarray(0, bytes.byteLength));
|
|
87
|
+
const samples = new Int16Array(bytes.buffer);
|
|
88
|
+
resolve({
|
|
89
|
+
seconds: samples.length / decodeRate,
|
|
90
|
+
peaks: peaksOf(samples, decodeRate, peaksPerSecond),
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { languageInfo } from "../../kernel/ports/languages.js";
|
|
3
|
+
import { aliasedSentences } from "./aliases.js";
|
|
2
4
|
const sentence = z.number().int().positive();
|
|
3
5
|
const instruction = z
|
|
4
6
|
.string()
|
|
@@ -64,12 +66,28 @@ const contract = [
|
|
|
64
66
|
"Never return or rewrite narration. Do not translate, correct, abbreviate or add dialogue.",
|
|
65
67
|
"Use cues sparingly; an empty cues array is valid. Combine simultaneous directions.",
|
|
66
68
|
].join("\n");
|
|
67
|
-
|
|
69
|
+
// `aliases` are the narration aliases found in `source`: the model reads the sentences as
|
|
70
|
+
// they will be said. Without any, the request is exactly what it always was. A narration in
|
|
71
|
+
// another `language`: its directions still steer the voice in English, but the model has to
|
|
72
|
+
// know the sentences are not English and must stay as they are. English gets the contract
|
|
73
|
+
// alone, as it always did. A turn of a multi-voice script names its `speaker` too, so the
|
|
74
|
+
// directions suit a narrator, a host or the character saying the line; one voice names none.
|
|
75
|
+
export function preparationMessages(prompt, source, aliases = [], language, speaker) {
|
|
76
|
+
const name = language === undefined || language === "en" ? undefined : languageInfo(language);
|
|
68
77
|
return [
|
|
69
|
-
{
|
|
78
|
+
{
|
|
79
|
+
role: "system",
|
|
80
|
+
content: name === undefined
|
|
81
|
+
? contract
|
|
82
|
+
: `${contract}\nThe narration is in ${name.name}; keep it in ${name.name}. Directions stay in English.`,
|
|
83
|
+
},
|
|
70
84
|
{
|
|
71
85
|
role: "user",
|
|
72
|
-
content: JSON.stringify({
|
|
86
|
+
content: JSON.stringify({
|
|
87
|
+
direction: prompt,
|
|
88
|
+
...(speaker === undefined ? {} : { speaker: `${speaker.name} (${speaker.role})` }),
|
|
89
|
+
sentences: aliasedSentences(sourceSentences(source), aliases),
|
|
90
|
+
}),
|
|
73
91
|
},
|
|
74
92
|
];
|
|
75
93
|
}
|
|
@@ -18,7 +18,7 @@ export function pronunciationChunks(source, chunking, entries, overriddenKeys =
|
|
|
18
18
|
const chunks = chunkNarration(source, chunking).map((text) => {
|
|
19
19
|
const start = source.indexOf(text, offset);
|
|
20
20
|
if (start < 0)
|
|
21
|
-
throw new Error("Slopify hit an internal error (a narration chunk isn't in the article text).
|
|
21
|
+
throw new Error("Slopify hit an internal error (a narration chunk isn't in the article text). Try again; if it happens again, use Download diagnostics in Settings and report it.");
|
|
22
22
|
offset = start + text.length;
|
|
23
23
|
return { key: nextKey(text), text, start, end: offset };
|
|
24
24
|
});
|
|
@@ -78,7 +78,7 @@ export function pronunciationChunks(source, chunking, entries, overriddenKeys =
|
|
|
78
78
|
}
|
|
79
79
|
const chunk = chunks[index];
|
|
80
80
|
if (chunk === undefined)
|
|
81
|
-
throw new Error("Slopify hit an internal error (a narration chunk went missing).
|
|
81
|
+
throw new Error("Slopify hit an internal error (a narration chunk went missing). Try again; if it happens again, use Download diagnostics in Settings and report it.");
|
|
82
82
|
while ((matches[matchIndex]?.end ?? Infinity) <= chunk.end)
|
|
83
83
|
matchIndex++;
|
|
84
84
|
const match = matches[matchIndex];
|
|
@@ -90,7 +90,7 @@ export function pronunciationChunks(source, chunking, entries, overriddenKeys =
|
|
|
90
90
|
continue;
|
|
91
91
|
const first = chunks[from];
|
|
92
92
|
if (first === undefined)
|
|
93
|
-
throw new Error("Slopify hit an internal error (a group of narration chunks is empty).
|
|
93
|
+
throw new Error("Slopify hit an internal error (a group of narration chunks is empty). Try again; if it happens again, use Download diagnostics in Settings and report it.");
|
|
94
94
|
const text = source.slice(first.start, chunk.end);
|
|
95
95
|
result.push(from === index
|
|
96
96
|
? { key: chunk.key, text }
|
|
@@ -4,6 +4,10 @@ import remarkGfm from "remark-gfm";
|
|
|
4
4
|
// https://docs.inworld.ai/tts/capabilities/custom-pronunciation
|
|
5
5
|
const ipaAtom = "[ˈˌ]?[abdefghijklmnoprstuvwxzæðŋθɑɒɔəɚɛɜɝɡɪɹʃʊʌʒʔɫɾ][\\u0303\\u031a\\u0325\\u0329\\u032a\\u032c\\u032f\\u035c\\u0361ʰʲʷ]*[ːˑ]?";
|
|
6
6
|
const ipaSymbols = new RegExp(`^(?:${ipaAtom})+(?:\\.(?:${ipaAtom})+)*$`, "u");
|
|
7
|
+
// Another language's glossary needs that language's sounds: the full IPA consonant and vowel
|
|
8
|
+
// charts (ç, ʁ, ø, y, ɲ, ʎ, x, β, …) and its diacritics, still never ARPAbet or tags.
|
|
9
|
+
const worldAtom = "[ˈˌ]?[a-zæçðøħŋœɐɑɒɓɔɕɖɗɘəɚɛɜɝɞɟɠɡɢɣɤɥɦɧɨɪɫɬɭɮɯɰɱɲɳɴɵɶɸɹɺɻɽɾʀʁʂʃʄʈʉʊʋʌʍʎʏʐʑʒʔʕʙʛʜʝʟʡʢβθχ][\\u0300-\\u036fʰʱʲʷʼˠˤ˞]*[ːˑ]?";
|
|
10
|
+
const worldSymbols = new RegExp(`^(?:${worldAtom})+(?:\\.(?:${worldAtom})+)*$`, "u");
|
|
7
11
|
function textOf(node) {
|
|
8
12
|
if (node.type === "break")
|
|
9
13
|
return "\n";
|
|
@@ -18,7 +22,12 @@ function rowsOf(node) {
|
|
|
18
22
|
const rows = node.children ?? [];
|
|
19
23
|
return rows.slice(1).map((row) => {
|
|
20
24
|
const cells = row.children ?? [];
|
|
21
|
-
|
|
25
|
+
if (cells.length < 2)
|
|
26
|
+
return textOf(row);
|
|
27
|
+
const [term, ipa] = cells.slice(0, 2).map((cell) => textOf(cell).trim());
|
|
28
|
+
// A table's IPA column is often written without the slashes ("| Cleopatra | kliːəˈpætrə |");
|
|
29
|
+
// the column already says what the cell is, so a bare cell reads as one /…/ pronunciation.
|
|
30
|
+
return `${term}: ${ipa !== undefined && ipa !== "" && !ipa.includes("/") ? `/${ipa}/` : ipa}`;
|
|
22
31
|
});
|
|
23
32
|
}
|
|
24
33
|
if (node.type === "paragraph")
|
|
@@ -27,16 +36,6 @@ function rowsOf(node) {
|
|
|
27
36
|
return (node.children ?? []).flatMap(rowsOf);
|
|
28
37
|
return ["(unsupported glossary block)"];
|
|
29
38
|
}
|
|
30
|
-
function refused(row, reason) {
|
|
31
|
-
return {
|
|
32
|
-
ok: false,
|
|
33
|
-
reason: "Pronunciation Glossary entry " +
|
|
34
|
-
row +
|
|
35
|
-
": " +
|
|
36
|
-
reason +
|
|
37
|
-
". Edit the glossary or turn off Use Pronunciation Glossary.",
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
39
|
function termIdentity(term) {
|
|
41
40
|
return Array.from(term, (character) => {
|
|
42
41
|
const lower = character.toLowerCase();
|
|
@@ -48,10 +47,19 @@ function termIdentity(term) {
|
|
|
48
47
|
return match.test(folded) ? folded : match.test(lower) ? lower : character;
|
|
49
48
|
}).join("");
|
|
50
49
|
}
|
|
51
|
-
|
|
50
|
+
// `language` is the project's (`kernel/ports/languages.ts`): absent or English accepts the
|
|
51
|
+
// standard-English IPA Inworld asks for, as it always did; any other accepts full IPA.
|
|
52
|
+
export function parsePronunciationGlossary(markdown, language) {
|
|
53
|
+
const english = language === undefined || language === "en";
|
|
54
|
+
const symbols = english ? ipaSymbols : worldSymbols;
|
|
55
|
+
const kind = english ? "standard-English IPA" : "IPA";
|
|
52
56
|
const rows = rowsOf(remark().use(remarkGfm).parse(markdown));
|
|
53
57
|
const entries = new Map();
|
|
58
|
+
const skipped = [];
|
|
54
59
|
for (const [index, row] of rows.entries()) {
|
|
60
|
+
const skip = (reason) => {
|
|
61
|
+
skipped.push({ row: index + 1, reason });
|
|
62
|
+
};
|
|
55
63
|
const trimmed = row.trim();
|
|
56
64
|
if (trimmed === "" || /^pronunciation glossary:?\s*$/iu.test(trimmed))
|
|
57
65
|
continue;
|
|
@@ -61,24 +69,63 @@ export function parsePronunciationGlossary(markdown) {
|
|
|
61
69
|
if (term === undefined ||
|
|
62
70
|
pronunciation === undefined ||
|
|
63
71
|
!/[\p{L}\p{N}]/u.test(term) ||
|
|
64
|
-
/[/[\]<>\p{Cc}]/u.test(term))
|
|
65
|
-
|
|
72
|
+
/[/[\]<>\p{Cc}]/u.test(term)) {
|
|
73
|
+
skip("use Term: /IPA/ or a Term | IPA table");
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
66
76
|
const notation = /^(\/[^/]+\/(?:\s+\/[^/]+\/)*)(?:\s+[^/]+)?$/u.exec(pronunciation);
|
|
67
|
-
if (notation?.[1] === undefined)
|
|
68
|
-
|
|
77
|
+
if (notation?.[1] === undefined) {
|
|
78
|
+
skip(`use slash-delimited ${kind}`);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
69
81
|
const ipa = Array.from(notation[1].matchAll(/\/([^/]+)\//gu)).flatMap((match) => (match[1] ?? "").trim().split(/\s+/u));
|
|
70
|
-
if (ipa.length === 0 || ipa.some((word) => !
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
82
|
+
if (ipa.length === 0 || ipa.some((word) => !symbols.test(word))) {
|
|
83
|
+
skip(english
|
|
84
|
+
? "use standard-English IPA only, not ARPAbet, delivery tags or non-English sounds; give a foreign name an English approximation"
|
|
85
|
+
: "use IPA, not ARPAbet or delivery tags");
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (term.split(" ").length !== ipa.length) {
|
|
89
|
+
skip("supply one IPA word for each written word");
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
74
92
|
const key = termIdentity(term);
|
|
75
93
|
const previous = entries.get(key);
|
|
76
|
-
if (previous !== undefined && previous.ipa.join(" ") !== ipa.join(" "))
|
|
77
|
-
|
|
94
|
+
if (previous !== undefined && previous.ipa.join(" ") !== ipa.join(" ")) {
|
|
95
|
+
skip("an earlier entry already gives this term a different pronunciation");
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
78
98
|
if (previous === undefined)
|
|
79
99
|
entries.set(key, { term, ipa });
|
|
80
100
|
}
|
|
81
|
-
return
|
|
101
|
+
return skipped.length === 0
|
|
102
|
+
? { ok: true, entries: [...entries.values()] }
|
|
103
|
+
: { ok: true, entries: [...entries.values()], skipped };
|
|
104
|
+
}
|
|
105
|
+
// What a person reads about the skipped rows: where they are and why, never their text.
|
|
106
|
+
export function skippedGlossaryNotice(skipped) {
|
|
107
|
+
if (skipped.length === 0)
|
|
108
|
+
return null;
|
|
109
|
+
const rows = skipped.map((one) => `entry ${one.row}: ${one.reason}`).join("; ");
|
|
110
|
+
return (`Pronunciation Glossary: ${skipped.length === 1 ? "1 entry is" : `${skipped.length} entries are`} ` +
|
|
111
|
+
`skipped and read as ordinary text (${rows}). The rest of the glossary is used. ` +
|
|
112
|
+
"To use them, fix those entries in the Pronunciation Glossary at the end of the article in Edit project → Article.");
|
|
113
|
+
}
|
|
114
|
+
export function skippedSpeakerPronunciations(speakers, language) {
|
|
115
|
+
return speakers.flatMap((speaker) => {
|
|
116
|
+
if (!speaker.pronunciations?.trim())
|
|
117
|
+
return [];
|
|
118
|
+
const parsed = parsePronunciationGlossary(speaker.pronunciations, language);
|
|
119
|
+
const skipped = parsed.ok ? (parsed.skipped ?? []) : [];
|
|
120
|
+
return skipped.length === 0 ? [] : [{ speaker: speaker.name.trim() || "A speaker", skipped }];
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
export function skippedSpeakerPronunciationsNotice(rows) {
|
|
124
|
+
if (rows.length === 0)
|
|
125
|
+
return null;
|
|
126
|
+
const parts = rows.map((row) => `${row.speaker}: ${row.skipped.map((one) => `entry ${one.row}: ${one.reason}`).join("; ")}`);
|
|
127
|
+
return (`Speaker pronunciations: some entries are skipped and read as ordinary text (${parts.join(". ")}). ` +
|
|
128
|
+
"The rest are used. To use them, fix those entries under Speakers → Pronunciations for that speaker (Play → Narration, or Edit project → Providers).");
|
|
82
129
|
}
|
|
83
130
|
export function pronunciationMatches(source, entries) {
|
|
84
131
|
if (entries.length === 0)
|
|
@@ -105,7 +152,7 @@ export function pronunciationMatches(source, entries) {
|
|
|
105
152
|
continue;
|
|
106
153
|
const entry = ordered.find((_, index) => match[index + 1] !== undefined);
|
|
107
154
|
if (entry === undefined)
|
|
108
|
-
throw new Error("Slopify hit an internal error (a pronunciation glossary term has no pronunciation).
|
|
155
|
+
throw new Error("Slopify hit an internal error (a pronunciation glossary term has no pronunciation). Try again; if it happens again, use Download diagnostics in Settings and report it.");
|
|
109
156
|
matches.push({ start: match.index, end, entry });
|
|
110
157
|
}
|
|
111
158
|
return matches;
|
|
@@ -117,7 +164,7 @@ export function pronunciationSpans(source, entries) {
|
|
|
117
164
|
for (const [index, word] of Array.from(words).entries()) {
|
|
118
165
|
const ipa = match.entry.ipa[index];
|
|
119
166
|
if (ipa === undefined)
|
|
120
|
-
throw new Error("Slopify hit an internal error (a pronunciation glossary term is missing a word).
|
|
167
|
+
throw new Error("Slopify hit an internal error (a pronunciation glossary term is missing a word). Try again; if it happens again, use Download diagnostics in Settings and report it.");
|
|
121
168
|
const start = match.start + word.index;
|
|
122
169
|
spans.push({ start, end: start + word[0].length, text: `/${ipa}/` });
|
|
123
170
|
}
|
|
@@ -14,7 +14,7 @@ import { planLegacyNarration } from "./legacy-plan.js";
|
|
|
14
14
|
import { observeNarration } from "./live.js";
|
|
15
15
|
// An empty narration source fails the stage immediately with no retries. Thrown from the
|
|
16
16
|
// slice rather than from a provider call, so the attempt wrapper never sees it.
|
|
17
|
-
export const nothingToNarrate = "There is nothing to narrate because the article is empty. Add article text in Edit project → Article, then
|
|
17
|
+
export const nothingToNarrate = "There is nothing to narrate because the article is empty. Add article text in Edit project → Article, then Try again.";
|
|
18
18
|
// What a chunk piece carries between runs: the text that was sent, and the file it came
|
|
19
19
|
// back as once the provider answered. That is the whole of the resume.
|
|
20
20
|
const chunkPayload = z.object({ text: z.string(), file: z.string().optional() });
|
|
@@ -41,7 +41,7 @@ export async function runNarration(deps, context, providers) {
|
|
|
41
41
|
if (choice === undefined) {
|
|
42
42
|
// Admission refuses a run whose audio is Generate without a provider and a voice, so
|
|
43
43
|
// reaching here is a bug in admission rather than something the user did.
|
|
44
|
-
throw new Error("No voice is set for the narration. Choose a voice in Edit project → Providers, then
|
|
44
|
+
throw new Error("No voice is set for the narration. Choose a voice in Edit project → Providers, then Try again.");
|
|
45
45
|
}
|
|
46
46
|
// Read once, before anything is written: the segment steps below ask it what a previous
|
|
47
47
|
// run already stored, and the body step is the only thing that adds to it.
|
|
@@ -168,7 +168,7 @@ async function speakSegments(deps, context, providers, choice, outputs) {
|
|
|
168
168
|
if (text === "") {
|
|
169
169
|
// The same rule, applied to a segment: there is nothing to say, and silently
|
|
170
170
|
// dropping an intro the user picked would lose it without telling anyone.
|
|
171
|
-
throw new Error(`The ${segment.category} is empty, so there is nothing to narrate for it. Write the ${segment.category} or remove it in Edit project, then
|
|
171
|
+
throw new Error(`The ${segment.category} is empty, so there is nothing to narrate for it. Write the ${segment.category} or remove it in Edit project, then Try again.`);
|
|
172
172
|
}
|
|
173
173
|
const spoken = await providers.forPiece(piece.id).tts({
|
|
174
174
|
provider: choice.provider,
|
|
@@ -200,7 +200,7 @@ function segmentPieces(deps, projectId) {
|
|
|
200
200
|
if (article === undefined) {
|
|
201
201
|
// Admission writes all six stage rows with the project, so a project without an article
|
|
202
202
|
// stage is a bug rather than a run the user configured.
|
|
203
|
-
throw new Error("Slopify hit an internal error (the project has no article stage).
|
|
203
|
+
throw new Error("Slopify hit an internal error (the project has no article stage). Try again; if it happens again, use Download diagnostics in Settings and report it.");
|
|
204
204
|
}
|
|
205
205
|
return piecesOf(deps.db, article.id, "segment");
|
|
206
206
|
}
|
|
@@ -257,13 +257,13 @@ function store(deps, projectId, role, path, durationMs, choice) {
|
|
|
257
257
|
}
|
|
258
258
|
function chunkOf(piece) {
|
|
259
259
|
if (piece.payload === null) {
|
|
260
|
-
throw new Error("Slopify hit an internal error (a saved narration chunk is empty).
|
|
260
|
+
throw new Error("Slopify hit an internal error (a saved narration chunk is empty). Try again; if it happens again, use Download diagnostics in Settings and report it.");
|
|
261
261
|
}
|
|
262
262
|
return chunkPayload.parse(JSON.parse(piece.payload));
|
|
263
263
|
}
|
|
264
264
|
function segmentOf(piece) {
|
|
265
265
|
if (piece.payload === null) {
|
|
266
|
-
throw new Error("Slopify hit an internal error (a saved intro or outro is empty).
|
|
266
|
+
throw new Error("Slopify hit an internal error (a saved intro or outro is empty). Try again; if it happens again, use Download diagnostics in Settings and report it.");
|
|
267
267
|
}
|
|
268
268
|
return segmentPayload.parse(JSON.parse(piece.payload));
|
|
269
269
|
}
|
|
@@ -22,7 +22,7 @@ export function collectSharedGlossary(deps, exceptProjectId) {
|
|
|
22
22
|
continue;
|
|
23
23
|
}
|
|
24
24
|
const parsed = parsePronunciationGlossary(text);
|
|
25
|
-
//
|
|
25
|
+
// Rows it had to skip are left out; a glossary that does not parse at all shares nothing.
|
|
26
26
|
if (!parsed.ok)
|
|
27
27
|
continue;
|
|
28
28
|
for (const entry of parsed.entries) {
|