@gentbajko/slopify 2.4.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -21
- package/NOTICE +12 -0
- package/README.md +21 -40
- package/SUBTITLES.md +1 -1
- package/dist/adapter-registry.js +11 -1
- package/dist/adapters/alignment/cache.js +4 -1
- package/dist/adapters/alignment/ctc.js +25 -23
- package/dist/adapters/alignment/index.js +37 -6
- package/dist/adapters/alignment/multilingual.js +267 -0
- package/dist/adapters/alignment/protocol.js +11 -0
- package/dist/adapters/alignment/quality.js +4 -3
- package/dist/adapters/alignment/sentences.js +95 -0
- package/dist/adapters/alignment/spec.js +24 -0
- package/dist/adapters/alignment/spell.js +769 -0
- package/dist/adapters/alignment/text.js +56 -9
- package/dist/adapters/alignment/window.js +34 -30
- package/dist/adapters/alignment/worker.js +23 -21
- package/dist/adapters/explain.js +11 -11
- package/dist/adapters/host-cli/index.js +55 -5
- package/dist/adapters/host-cli/transport.js +13 -6
- package/dist/adapters/image/bytes.js +2 -2
- package/dist/adapters/image/codex-output.js +60 -18
- package/dist/adapters/image/codex.js +166 -25
- package/dist/adapters/image/fal.js +31 -3
- package/dist/adapters/image/google.js +17 -2
- package/dist/adapters/image/openai.js +45 -16
- package/dist/adapters/image/reference.js +41 -0
- package/dist/adapters/image/replicate.js +14 -2
- package/dist/adapters/image/video.js +2 -2
- package/dist/adapters/key-probes.js +36 -0
- package/dist/adapters/llm/claude-code.js +168 -32
- package/dist/adapters/llm/cli-login-error.js +1 -1
- package/dist/adapters/llm/codex-limits.js +157 -0
- package/dist/adapters/llm/codex-models.js +2 -1
- package/dist/adapters/llm/codex-server-models.js +2 -1
- package/dist/adapters/llm/codex.js +99 -12
- package/dist/adapters/llm/document-workspace.js +2 -2
- package/dist/adapters/llm/gemini-workspace.js +1 -1
- package/dist/adapters/llm/gemini.js +16 -3
- package/dist/adapters/llm/image-workspace.js +58 -0
- package/dist/adapters/llm/openrouter.js +11 -3
- package/dist/adapters/llm/run-cli.js +26 -9
- package/dist/adapters/tts/cartesia.js +10 -0
- package/dist/adapters/tts/elevenlabs.js +31 -9
- package/dist/adapters/tts/inworld-async.js +9 -5
- package/dist/adapters/tts/inworld.js +17 -3
- package/dist/adapters/tts/openai.js +3 -0
- package/dist/adapters/tts/voice-languages.js +80 -0
- package/dist/assets/fonts/noto/NotoSans-Regular.ttf +0 -0
- package/dist/assets/fonts/noto/NotoSansArabic-Regular.ttf +0 -0
- package/dist/assets/fonts/noto/NotoSansDevanagari-Regular.ttf +0 -0
- package/dist/assets/fonts/noto/NotoSansHebrew-Regular.ttf +0 -0
- package/dist/assets/fonts/noto/NotoSansThai-Regular.ttf +0 -0
- package/dist/assets/fonts/noto/OFL.txt +93 -0
- package/dist/assets/fonts/noto/SOURCE.txt +18 -0
- package/dist/assets/models.yaml +7 -0
- package/dist/assets/sample/sample-audiobook.tar +0 -0
- package/dist/assets/sample/sample-podcast.tar +0 -0
- package/dist/assets/sample/sample-project.tar +0 -0
- package/dist/catalog/merge.js +132 -0
- package/dist/catalog/registry.js +33 -12
- package/dist/catalog/runtime-models.js +1 -1
- package/dist/catalog/schema.js +12 -1
- package/dist/catalog/store.js +147 -8
- package/dist/catalog/validate.js +17 -2
- package/dist/compose.yaml +71 -0
- package/dist/edge/autostart/docker-record.js +32 -0
- package/dist/edge/autostart/docker.js +50 -0
- package/dist/edge/autostart/index.js +78 -0
- package/dist/edge/autostart/launcher.js +176 -0
- package/dist/edge/autostart/model.js +3 -0
- package/dist/edge/autostart/native.js +210 -0
- package/dist/edge/autostart/prompt.js +66 -0
- package/dist/edge/autostart/service.js +81 -0
- package/dist/edge/cli.js +57 -51
- package/dist/edge/docker-install/activation.js +56 -0
- package/dist/edge/docker-install/apply.js +458 -0
- package/dist/edge/docker-install/engine.js +362 -0
- package/dist/edge/docker-install/run.js +154 -0
- package/dist/edge/docker-install/state.js +146 -0
- package/dist/edge/{docker-projects → docker-install}/volume.js +1 -5
- package/dist/edge/docker.js +14 -22
- package/dist/edge/events/hub.js +11 -0
- package/dist/edge/host-cli.js +1 -1
- package/dist/edge/http/actions.js +32 -10
- package/dist/edge/http/app.js +60 -2
- package/dist/edge/http/auditions.js +70 -0
- package/dist/edge/http/autostart.js +37 -0
- package/dist/edge/http/backups.js +40 -0
- package/dist/edge/http/channel-memory.js +92 -0
- package/dist/edge/http/channels.js +128 -0
- package/dist/edge/http/entries.js +22 -3
- package/dist/edge/http/files.js +1 -1
- package/dist/edge/http/folder-location.js +1 -1
- package/dist/edge/http/home.js +39 -0
- package/dist/edge/http/host-cli.js +45 -6
- package/dist/edge/http/narration-peaks.js +76 -0
- package/dist/edge/http/onboarding.js +152 -0
- package/dist/edge/http/open-folder.js +1 -1
- package/dist/edge/http/patch-notes.js +45 -0
- package/dist/edge/http/planning.js +4 -2
- package/dist/edge/http/problem.js +1 -0
- package/dist/edge/http/project-create.js +87 -0
- package/dist/edge/http/projects.js +31 -99
- package/dist/edge/http/prompts.js +40 -3
- package/dist/edge/http/pronunciations.js +23 -4
- package/dist/edge/http/providers.js +86 -0
- package/dist/edge/http/reviews.js +121 -0
- package/dist/edge/http/revision-files.js +1 -1
- package/dist/edge/http/revisions.js +1 -1
- package/dist/edge/http/run-cost.js +25 -0
- package/dist/edge/http/schedules.js +111 -6
- package/dist/edge/http/settings.js +128 -6
- package/dist/edge/http/storage-files.js +50 -0
- package/dist/edge/http/storage.js +29 -2
- package/dist/edge/http/studio.js +207 -0
- package/dist/edge/http/style-preview.js +100 -0
- package/dist/edge/http/subtitles.js +1 -1
- package/dist/edge/http/trash.js +69 -0
- package/dist/edge/http/update.js +24 -3
- package/dist/edge/http/whats-new.js +15 -0
- package/dist/edge/http/youtube-edits.js +83 -0
- package/dist/edge/native-update.js +62 -0
- package/dist/host-cli/open-folder.js +10 -10
- package/dist/host-cli/runtime.js +12 -3
- package/dist/kernel/db/migrations/0022-drop-duplicate-held-work.sql +56 -0
- package/dist/kernel/db/migrations/0024-reference-attachments.sql +18 -0
- package/dist/kernel/db/migrations/0025-automatic-reviews.sql +40 -0
- package/dist/kernel/db/migrations/0026-schedule-topic-generation.sql +23 -0
- package/dist/kernel/db/migrations/0027-run-cost.sql +58 -0
- package/dist/kernel/db/migrations/0030-automatic-retries.sql +18 -0
- package/dist/kernel/db/migrations/0031-prompt-history-and-description-edits.sql +33 -0
- package/dist/kernel/db/migrations/0032-channels.sql +65 -0
- package/dist/kernel/db/migrations/0034-script-prompts.sql +23 -0
- package/dist/kernel/db/migrations/0035-narration-aliases.sql +13 -0
- package/dist/kernel/db/migrations/0037-project-uploads.sql +8 -0
- package/dist/kernel/db/migrations/0038-voice-languages.sql +7 -0
- package/dist/kernel/db/migrations/0039-channel-essentials.sql +64 -0
- package/dist/kernel/db/migrations/0040-standalone-usage.sql +35 -0
- package/dist/kernel/paths.js +27 -6
- package/dist/kernel/pipeline.js +4 -1
- package/dist/kernel/ports/host-cli.js +139 -16
- package/dist/kernel/ports/image.js +3 -1
- package/dist/kernel/ports/key-probe.js +1 -0
- package/dist/kernel/ports/languages.js +157 -0
- package/dist/kernel/ports/llm.js +3 -1
- package/dist/kernel/ports/model.js +5 -2
- package/dist/kernel/ports/narration-aliases.js +60 -0
- package/dist/kernel/ports/plan-limits.js +36 -0
- package/dist/kernel/runner/attempt.js +18 -9
- package/dist/kernel/runner/audition.js +36 -0
- package/dist/kernel/runner/graph.js +24 -5
- package/dist/kernel/runner/index.js +35 -5
- package/dist/kernel/runner/meter.js +1 -0
- package/dist/kernel/runner/providers.js +164 -9
- package/dist/kernel/runner/retry-policy.js +43 -0
- package/dist/kernel/runner/standalone.js +134 -0
- package/dist/kernel/runner/work-authority.js +28 -2
- package/dist/main.js +321 -20
- package/dist/patch-notes/0.4.2-to-1.0.md +93 -0
- package/dist/patch-notes/1.0-to-2.0.md +74 -0
- package/dist/patch-notes/2.0-to-3.0.md +257 -0
- package/dist/patch-notes/3.0.0.md +70 -0
- package/dist/patch-notes/index.json +26 -0
- package/dist/slices/admission/model.js +35 -0
- package/dist/slices/admission/repo.js +24 -4
- package/dist/slices/admission/rules.js +126 -1
- package/dist/slices/admission/schema.js +68 -1
- package/dist/slices/admission/short-mode.js +47 -0
- package/dist/slices/admission/start.js +90 -16
- package/dist/slices/article/continuation.js +2 -2
- package/dist/slices/article/provided-entries.js +2 -2
- package/dist/slices/article/run.js +6 -2
- package/dist/slices/article/segments.js +3 -3
- package/dist/slices/backups/files.js +135 -0
- package/dist/slices/backups/folder.js +43 -0
- package/dist/slices/backups/model.js +65 -0
- package/dist/slices/backups/repo.js +28 -0
- package/dist/slices/backups/schedule.js +60 -0
- package/dist/slices/backups/service.js +311 -0
- package/dist/slices/batch/index.js +9 -2
- package/dist/slices/cancel/index.js +1 -1
- package/dist/slices/channels/cast-images.js +110 -0
- package/dist/slices/channels/cast-match.js +32 -0
- package/dist/slices/channels/images.js +19 -0
- package/dist/slices/channels/model.js +5 -0
- package/dist/slices/channels/rebrand.js +100 -0
- package/dist/slices/channels/repo.js +218 -0
- package/dist/slices/channels/runs.js +129 -0
- package/dist/slices/channels/schema.js +129 -0
- package/dist/slices/channels/service.js +180 -0
- package/dist/slices/channels/studio-csv.js +110 -0
- package/dist/slices/channels/videos.js +120 -0
- package/dist/slices/checkpoints/recovery.js +10 -0
- package/dist/slices/control/index.js +2 -1
- package/dist/slices/document/fonts.js +1 -1
- package/dist/slices/document/render.js +1 -1
- package/dist/slices/episodes/related.js +90 -0
- package/dist/slices/episodes/repo.js +60 -0
- package/dist/slices/episodes/service.js +58 -0
- package/dist/slices/episodes/summarize.js +170 -0
- package/dist/slices/estimate/index.js +113 -24
- package/dist/slices/estimate/requests.js +60 -2
- package/dist/slices/fixes/rules.js +73 -0
- package/dist/slices/fonts/coverage.js +82 -0
- package/dist/slices/fonts/sfnt.js +74 -0
- package/dist/slices/images/run.js +8 -5
- package/dist/slices/images/scale.js +84 -0
- package/dist/slices/library/diff.js +122 -0
- package/dist/slices/library/history.js +100 -0
- package/dist/slices/library/model.js +3 -0
- package/dist/slices/library/photorealistic.js +49 -0
- package/dist/slices/library/repo.js +19 -14
- package/dist/slices/library/save.js +62 -12
- package/dist/slices/library/slots.js +15 -3
- package/dist/slices/library/snapshot.js +8 -2
- package/dist/slices/library/used-by.js +153 -0
- package/dist/slices/loudness/level-pieces.js +41 -0
- package/dist/slices/loudness/loudnorm.js +164 -0
- package/dist/slices/loudness/model.js +153 -0
- package/dist/slices/model-upkeep/model.js +10 -0
- package/dist/slices/model-upkeep/switch.js +217 -0
- package/dist/slices/model-upkeep/usage.js +183 -0
- package/dist/slices/narration/aliases-library.js +31 -0
- package/dist/slices/narration/aliases-schema.js +53 -0
- package/dist/slices/narration/aliases.js +49 -0
- package/dist/slices/narration/blocks.js +352 -0
- package/dist/slices/narration/chunk.js +19 -7
- package/dist/slices/narration/concat.js +2 -2
- package/dist/slices/narration/describe.js +45 -0
- package/dist/slices/narration/pauses-model.js +63 -0
- package/dist/slices/narration/pauses.js +269 -0
- package/dist/slices/narration/peaks-model.js +1 -0
- package/dist/slices/narration/peaks.js +94 -0
- package/dist/slices/narration/preparation.js +21 -3
- package/dist/slices/narration/pronunciation-chunks.js +3 -3
- package/dist/slices/narration/pronunciation.js +72 -25
- package/dist/slices/narration/run.js +6 -6
- package/dist/slices/narration/shared-glossary.js +1 -1
- package/dist/slices/narration/steering.js +15 -4
- package/dist/slices/notifications/notifier.js +94 -0
- package/dist/slices/notifications/rules.js +102 -0
- package/dist/slices/notifications/send.js +40 -0
- package/dist/slices/notifications/settings.js +34 -0
- package/dist/slices/onboarding/copy.js +143 -0
- package/dist/slices/onboarding/first-run.js +46 -0
- package/dist/slices/onboarding/install.js +119 -0
- package/dist/slices/onboarding/model.js +64 -0
- package/dist/slices/onboarding/packs.js +491 -0
- package/dist/slices/onboarding/quick-short.js +129 -0
- package/dist/slices/onboarding/sample.js +132 -0
- package/dist/slices/onboarding/state.js +68 -0
- package/dist/slices/onboarding/template.js +73 -0
- package/dist/slices/patch-notes/library.js +73 -0
- package/dist/slices/patch-notes/seen.js +77 -0
- package/dist/slices/play-drafts/convert.js +160 -9
- package/dist/slices/play-drafts/readiness.js +3 -0
- package/dist/slices/play-drafts/repo.js +9 -1
- package/dist/slices/play-drafts/review-inputs.js +22 -3
- package/dist/slices/play-drafts/schema.js +109 -2
- package/dist/slices/play-drafts/service.js +10 -1
- package/dist/slices/play-drafts/start.js +43 -18
- package/dist/slices/play-drafts/uploads.js +2 -1
- package/dist/slices/project-templates/from-project.js +70 -4
- package/dist/slices/project-templates/one-off.js +26 -0
- package/dist/slices/project-templates/repo.js +2 -1
- package/dist/slices/project-templates/service.js +15 -4
- package/dist/slices/project-templates/setup.js +29 -2
- package/dist/slices/rebuild/admission-repo.js +1 -1
- package/dist/slices/rebuild/model.js +6 -0
- package/dist/slices/rebuild/preview-details.js +67 -2
- package/dist/slices/rebuild/preview-plan.js +13 -6
- package/dist/slices/rebuild/recipe-audio-parts.js +40 -7
- package/dist/slices/rebuild/recipe-audio.js +66 -12
- package/dist/slices/rebuild/recipe-build.js +15 -5
- package/dist/slices/rebuild/recipe-cast.js +18 -0
- package/dist/slices/rebuild/recipe-checkpoints.js +11 -9
- package/dist/slices/rebuild/recipe-describe.js +93 -0
- package/dist/slices/rebuild/recipe-edit.js +13 -1
- package/dist/slices/rebuild/recipe-exports.js +62 -8
- package/dist/slices/rebuild/recipe-input-schema.js +43 -1
- package/dist/slices/rebuild/recipe-legacy.js +12 -0
- package/dist/slices/rebuild/recipe-loudness.js +45 -0
- package/dist/slices/rebuild/recipe-model.js +25 -6
- package/dist/slices/rebuild/recipe-pauses.js +7 -0
- package/dist/slices/rebuild/recipe-preparation.js +3 -3
- package/dist/slices/rebuild/recipe-provider-choice.js +11 -1
- package/dist/slices/rebuild/recipe-reference.js +58 -0
- package/dist/slices/rebuild/recipe-reviews.js +117 -0
- package/dist/slices/rebuild/recipe-save.js +4 -1
- package/dist/slices/rebuild/recipe-shorts.js +32 -5
- package/dist/slices/rebuild/recipe-text.js +114 -19
- package/dist/slices/rebuild/recipe-validation.js +29 -3
- package/dist/slices/rebuild/recipe-visual.js +104 -29
- package/dist/slices/rebuild/recipe-voices.js +286 -0
- package/dist/slices/rebuild/recipe-work.js +77 -6
- package/dist/slices/rebuild/recipe-youtube.js +2 -0
- package/dist/slices/rebuild/recovery-conflict.js +7 -3
- package/dist/slices/rebuild/recovery-model.js +3 -0
- package/dist/slices/rebuild/recovery-selection.js +28 -2
- package/dist/slices/rebuild/recovery.js +35 -17
- package/dist/slices/rebuild/repo.js +3 -3
- package/dist/slices/rebuild/retry.js +18 -0
- package/dist/slices/rebuild/review-redo.js +110 -0
- package/dist/slices/rebuild/runtime-actions.js +14 -2
- package/dist/slices/rebuild/runtime-admission.js +4 -1
- package/dist/slices/rebuild/runtime-animate.js +4 -4
- package/dist/slices/rebuild/runtime-article.js +5 -5
- package/dist/slices/rebuild/runtime-document.js +4 -4
- package/dist/slices/rebuild/runtime-export-bed.js +43 -0
- package/dist/slices/rebuild/runtime-export-edit.js +70 -11
- package/dist/slices/rebuild/runtime-export-inputs.js +52 -10
- package/dist/slices/rebuild/runtime-export-short.js +85 -0
- package/dist/slices/rebuild/runtime-export.js +84 -19
- package/dist/slices/rebuild/runtime-figure-card.js +87 -0
- package/dist/slices/rebuild/runtime-image.js +69 -0
- package/dist/slices/rebuild/runtime-local.js +154 -27
- package/dist/slices/rebuild/runtime-materialize.js +8 -0
- package/dist/slices/rebuild/runtime-narration-reuse.js +2 -1
- package/dist/slices/rebuild/runtime-narration-text.js +7 -7
- package/dist/slices/rebuild/runtime-pictures.js +28 -0
- package/dist/slices/rebuild/runtime-piece-label.js +29 -0
- package/dist/slices/rebuild/runtime-provider.js +134 -19
- package/dist/slices/rebuild/runtime-publication.js +21 -5
- package/dist/slices/rebuild/runtime-review.js +237 -0
- package/dist/slices/rebuild/runtime-run.js +17 -11
- package/dist/slices/rebuild/runtime-shorts.js +48 -23
- package/dist/slices/rebuild/runtime-store.js +39 -10
- package/dist/slices/rebuild/runtime-subtitles.js +72 -19
- package/dist/slices/rebuild/runtime-voices.js +103 -0
- package/dist/slices/rebuild/runtime-youtube.js +7 -6
- package/dist/slices/rebuild/service-readiness.js +15 -1
- package/dist/slices/rebuild/soften.js +49 -0
- package/dist/slices/rebuild/transition-repo.js +10 -3
- package/dist/slices/research/run.js +4 -4
- package/dist/slices/research/synthesis.js +2 -2
- package/dist/slices/reviews/model.js +105 -0
- package/dist/slices/reviews/outcome.js +22 -0
- package/dist/slices/reviews/repo.js +106 -0
- package/dist/slices/reviews/rules.js +63 -0
- package/dist/slices/reviews/verdict.js +71 -0
- package/dist/slices/reviews/view.js +30 -0
- package/dist/slices/revisions/adopt-content.js +12 -2
- package/dist/slices/revisions/adopt.js +9 -2
- package/dist/slices/revisions/downloads.js +7 -1
- package/dist/slices/revisions/image-plan.js +183 -0
- package/dist/slices/revisions/mutation-assets.js +43 -9
- package/dist/slices/revisions/mutation-cues.js +71 -0
- package/dist/slices/revisions/mutation-prepare.js +8 -10
- package/dist/slices/revisions/mutation-request.js +2 -1
- package/dist/slices/revisions/mutations.js +97 -3
- package/dist/slices/revisions/schema.js +16 -2
- package/dist/slices/run-cost/limits.js +128 -0
- package/dist/slices/run-cost/meter.js +47 -0
- package/dist/slices/run-cost/panel.js +234 -0
- package/dist/slices/run-cost/pricing.js +92 -0
- package/dist/slices/run-cost/week.js +108 -0
- package/dist/slices/schedules/agenda.js +158 -0
- package/dist/slices/schedules/repo.js +58 -18
- package/dist/slices/schedules/scheduler.js +67 -16
- package/dist/slices/schedules/schema.js +138 -1
- package/dist/slices/schedules/service.js +48 -4
- package/dist/slices/schedules/similar.js +90 -0
- package/dist/slices/schedules/topic-list.js +163 -0
- package/dist/slices/schedules/topics.js +422 -0
- package/dist/slices/settings/first-run.js +102 -0
- package/dist/slices/settings/health.js +190 -0
- package/dist/slices/settings/key-guides.js +128 -0
- package/dist/slices/settings/key-test.js +85 -0
- package/dist/slices/settings/model.js +11 -1
- package/dist/slices/settings/playback.js +15 -0
- package/dist/slices/settings/repo.js +17 -3
- package/dist/slices/settings/voices.js +64 -4
- package/dist/slices/settings/whats-new.js +64 -0
- package/dist/slices/shorts/captions.js +9 -0
- package/dist/slices/shorts/pick.js +3 -3
- package/dist/slices/shorts/prompts.js +1 -1
- package/dist/slices/shorts/render.js +54 -5
- package/dist/slices/storage/asset-name.js +3 -0
- package/dist/slices/storage/backup-export.js +48 -9
- package/dist/slices/storage/backup-format.js +38 -1
- package/dist/slices/storage/backup-import.js +191 -19
- package/dist/slices/storage/delete-project.js +7 -4
- package/dist/slices/storage/documents.js +79 -0
- package/dist/slices/storage/files-location.js +531 -0
- package/dist/slices/storage/layout.js +20 -0
- package/dist/slices/storage/model.js +14 -0
- package/dist/slices/storage/portable.js +95 -12
- package/dist/slices/storage/reconcile.js +19 -2
- package/dist/slices/storage/repo.js +4 -1
- package/dist/slices/storage/schema.js +11 -0
- package/dist/slices/storage/trim.js +146 -0
- package/dist/slices/studio/disclosure.js +74 -0
- package/dist/slices/studio/model.js +28 -0
- package/dist/slices/studio/pack.js +209 -0
- package/dist/slices/studio/settings.js +133 -0
- package/dist/slices/style-preview/images.js +61 -0
- package/dist/slices/style-preview/render.js +161 -0
- package/dist/slices/style-preview/schema.js +45 -0
- package/dist/slices/style-preview/service.js +93 -0
- package/dist/slices/style-preview/settings.js +89 -0
- package/dist/slices/subtitles/captions.js +42 -9
- package/dist/slices/subtitles/model.js +22 -0
- package/dist/slices/subtitles/prepare.js +27 -16
- package/dist/slices/subtitles/transcript.js +1 -1
- package/dist/slices/thumbnail/run.js +7 -7
- package/dist/slices/trash/model.js +25 -0
- package/dist/slices/trash/service.js +273 -0
- package/dist/slices/uploads/repo.js +20 -0
- package/dist/slices/video/ambient-bed-schema.js +15 -0
- package/dist/slices/video/ambient-bed.js +95 -0
- package/dist/slices/video/ambient-mix.js +72 -0
- package/dist/slices/video/audio-inputs.js +2 -2
- package/dist/slices/video/cards.js +5 -2
- package/dist/slices/video/edit-list.js +23 -2
- package/dist/slices/video/edit-settings.js +13 -3
- package/dist/slices/video/ffmpeg.js +63 -9
- package/dist/slices/video/figure-card.js +492 -0
- package/dist/slices/video/figure-spans.js +51 -0
- package/dist/slices/video/plan.js +125 -13
- package/dist/slices/video/run.js +14 -12
- package/dist/slices/video/slideshow.js +39 -4
- package/dist/slices/voices/attribution.js +69 -0
- package/dist/slices/voices/audio-files.js +82 -0
- package/dist/slices/voices/audition.js +25 -0
- package/dist/slices/voices/cast.js +56 -0
- package/dist/slices/voices/grouping.js +49 -0
- package/dist/slices/voices/join.js +35 -0
- package/dist/slices/voices/languages.js +25 -0
- package/dist/slices/voices/model.js +179 -0
- package/dist/slices/voices/palette.js +26 -0
- package/dist/slices/voices/panel.js +91 -0
- package/dist/slices/voices/portraits.js +55 -0
- package/dist/slices/voices/script.js +136 -0
- package/dist/slices/voices/timing.js +58 -0
- package/dist/slices/youtube/answer.js +1 -1
- package/dist/slices/youtube/chapters.js +126 -0
- package/dist/slices/youtube/edits-repo.js +69 -0
- package/dist/slices/youtube/edits.js +89 -0
- package/dist/slices/youtube/placeholders.js +98 -0
- package/dist/slices/youtube/transcript.js +11 -4
- package/dist/updater/service.js +66 -21
- package/dist/web/assets/index-CUA2sUxU.css +1 -0
- package/dist/web/assets/index-CX590kE-.js +303 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
- package/dist/web/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
- package/dist/web/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
- package/dist/web/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
- package/dist/web/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
- package/dist/web/assets/{pdf-CAJNT254.js → pdf-BG8n_uc5.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +4 -4
- package/dist/edge/docker-launch.js +0 -81
- package/dist/edge/docker-projects/activation.js +0 -35
- package/dist/edge/docker-projects/claims.js +0 -85
- package/dist/edge/docker-projects/committed.js +0 -40
- package/dist/edge/docker-projects/engine.js +0 -461
- package/dist/edge/docker-projects/install.js +0 -254
- package/dist/edge/docker-projects/prune.js +0 -66
- package/dist/edge/docker-projects/recover.js +0 -100
- package/dist/edge/docker-projects/state.js +0 -225
- package/dist/web/assets/index-BGgYMm2u.css +0 -1
- package/dist/web/assets/index-BVVgtntL.js +0 -171
- package/scripts/docker-run.sh +0 -3
- /package/dist/edge/{docker-projects → docker-install}/tree.js +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { isAbsolute, resolve as resolvePath } from "node:path";
|
|
3
|
+
import { bedChains, bedInputs } from "./ambient-mix.js";
|
|
3
4
|
import { gradeFilter, lookChain, lookEncoding, placed } from "./look.js";
|
|
4
5
|
import { decimal, zoomRange } from "./plan.js";
|
|
5
6
|
import { segments, transitionHalves } from "./transitions.js";
|
|
@@ -43,7 +44,7 @@ export function slideshowClips(edit) {
|
|
|
43
44
|
named.set(key, name);
|
|
44
45
|
const shot = edit.shots[segment.kind === "shot" ? segment.shot : segment.to];
|
|
45
46
|
if (shot === undefined)
|
|
46
|
-
throw new Error("Slopify hit an internal error (the video's edit list lost a shot). Use
|
|
47
|
+
throw new Error("Slopify hit an internal error (the video's edit list lost a shot). Use More → Render the video again in the Video section; if it happens again, use Download diagnostics in Settings and report it.");
|
|
47
48
|
clips.push({ name, segment, shot, frames: segment.count });
|
|
48
49
|
}
|
|
49
50
|
return name;
|
|
@@ -92,7 +93,7 @@ cardsFile = "cards.ass") {
|
|
|
92
93
|
const chains = [];
|
|
93
94
|
const picture = (shot, from, label) => {
|
|
94
95
|
if (shot === undefined)
|
|
95
|
-
throw new Error("Slopify hit an internal error (the video's edit list lost a shot). Use
|
|
96
|
+
throw new Error("Slopify hit an internal error (the video's edit list lost a shot). Use More → Render the video again in the Video section; if it happens again, use Download diagnostics in Settings and report it.");
|
|
96
97
|
const at = inputs.filter((one) => one === "-i").length;
|
|
97
98
|
const built = pictureChain(edit, shot, from, segment.count, at, label, gradeFilter(edit.look));
|
|
98
99
|
inputs.push(...built.inputs);
|
|
@@ -207,11 +208,13 @@ function seconds6(value) {
|
|
|
207
208
|
export function concatList(order) {
|
|
208
209
|
return `ffconcat version 1.0\n${order.map((name) => `file ${name}`).join("\n")}\n`;
|
|
209
210
|
}
|
|
210
|
-
|
|
211
|
-
|
|
211
|
+
// The video's sound: the narration's segments and silences joined, with the ambient bed under
|
|
212
|
+
// them when there is one, as the inputs from `first` on and the chains ending in `[a]`.
|
|
213
|
+
function audioMix(edit, first) {
|
|
214
|
+
const inputs = [];
|
|
212
215
|
const audioAt = [];
|
|
213
216
|
for (const segment of edit.audio) {
|
|
214
|
-
audioAt.push(
|
|
217
|
+
audioAt.push(first + audioAt.length);
|
|
215
218
|
if (segment.path === null) {
|
|
216
219
|
inputs.push("-f", "lavfi", "-t", seconds(segment.seconds), "-i", `anullsrc=r=${sampleRate}:cl=${channelLayout}`);
|
|
217
220
|
continue;
|
|
@@ -219,15 +222,65 @@ export function joinArgs(edit, output, list, burnSubtitles = false) {
|
|
|
219
222
|
inputs.push("-i", segment.path);
|
|
220
223
|
}
|
|
221
224
|
const chains = [];
|
|
222
|
-
if (burnSubtitles)
|
|
223
|
-
chains.push("[0:v]ass=filename=subtitles.ass:fontsdir=fonts[v]");
|
|
224
225
|
audioAt.forEach((input, at) => {
|
|
225
226
|
// The segments come from different files and the silence from lavfi, so they are
|
|
226
227
|
// brought to one format before concat, which refuses to join mismatched streams.
|
|
227
228
|
chains.push(`[${input}:a]aformat=sample_fmts=fltp:sample_rates=${sampleRate}:channel_layouts=${channelLayout}[a${at}]`);
|
|
228
229
|
});
|
|
230
|
+
// The ambient bed lies under the joined narration, ducked by it (`ambient-mix.ts`); without
|
|
231
|
+
// one the arguments are the ones every video was joined with.
|
|
232
|
+
const bed = audioAt.length > 0 ? edit.bed : undefined;
|
|
229
233
|
if (audioAt.length > 0) {
|
|
230
|
-
chains.push(`${audioAt.map((_input, at) => `[a${at}]`).join("")}concat=n=${audioAt.length}:v=0:a=1[a]`);
|
|
234
|
+
chains.push(`${audioAt.map((_input, at) => `[a${at}]`).join("")}concat=n=${audioAt.length}:v=0:a=1${bed === undefined ? "[a]" : "[narration]"}`);
|
|
235
|
+
}
|
|
236
|
+
if (bed !== undefined) {
|
|
237
|
+
const total = edit.audio.reduce((sum, segment) => sum + segment.seconds, 0);
|
|
238
|
+
inputs.push(...bedInputs(bed, total, sampleRate));
|
|
239
|
+
chains.push(...bedChains(bed, first + audioAt.length, total, `aformat=sample_fmts=fltp:sample_rates=${sampleRate}:channel_layouts=${channelLayout}`, "[narration]", "[a]"));
|
|
240
|
+
}
|
|
241
|
+
return { inputs, chains };
|
|
242
|
+
}
|
|
243
|
+
// The video's sound alone, as a WAV: what Level the volume masters before the join plays it
|
|
244
|
+
// (`slideshow.ts`).
|
|
245
|
+
export function audioMixArgs(edit, output) {
|
|
246
|
+
const mix = audioMix(edit, 0);
|
|
247
|
+
return [
|
|
248
|
+
...progressArgs,
|
|
249
|
+
...mix.inputs,
|
|
250
|
+
"-filter_complex",
|
|
251
|
+
mix.chains.join(";"),
|
|
252
|
+
"-map",
|
|
253
|
+
"[a]",
|
|
254
|
+
"-c:a",
|
|
255
|
+
"pcm_s16le",
|
|
256
|
+
"-f",
|
|
257
|
+
"wav",
|
|
258
|
+
output,
|
|
259
|
+
];
|
|
260
|
+
}
|
|
261
|
+
export function joinArgs(edit, output, list, burnSubtitles = false, portraits = []) {
|
|
262
|
+
const mix = audioMix(edit, 1);
|
|
263
|
+
const inputs = ["-f", "concat", "-i", list, ...mix.inputs];
|
|
264
|
+
const chains = [];
|
|
265
|
+
const overlaid = burnSubtitles && portraits.length > 0;
|
|
266
|
+
if (burnSubtitles && !overlaid)
|
|
267
|
+
chains.push("[0:v]ass=filename=subtitles.ass:fontsdir=fonts[v]");
|
|
268
|
+
chains.push(...mix.chains);
|
|
269
|
+
// The portraits go in after every other input and under the captions, so the panel's lit
|
|
270
|
+
// outline is drawn over them; a video without any is joined exactly as before. A still
|
|
271
|
+
// image is one frame, which overlay holds to the end (`eof_action=repeat`).
|
|
272
|
+
if (overlaid) {
|
|
273
|
+
let first = inputs.filter((value) => value === "-i").length;
|
|
274
|
+
let source = "[0:v]";
|
|
275
|
+
const video = [];
|
|
276
|
+
portraits.forEach((portrait, at) => {
|
|
277
|
+
inputs.push("-i", portrait.path);
|
|
278
|
+
const size = String(portrait.size);
|
|
279
|
+
video.push(`[${String(first)}:v]scale=${size}:${size}:force_original_aspect_ratio=increase,crop=${size}:${size},setsar=1[pic${String(at)}]`, `${source}[pic${String(at)}]overlay=x=${String(portrait.x)}:y=${String(portrait.y)}:eof_action=repeat[panel${String(at)}]`);
|
|
280
|
+
source = `[panel${String(at)}]`;
|
|
281
|
+
first += 1;
|
|
282
|
+
});
|
|
283
|
+
chains.unshift(...video, `${source}ass=filename=subtitles.ass:fontsdir=fonts[v]`);
|
|
231
284
|
}
|
|
232
285
|
return [
|
|
233
286
|
...progressArgs,
|
|
@@ -359,6 +412,7 @@ export function runFfmpeg(run) {
|
|
|
359
412
|
child.stderr.setEncoding("utf8");
|
|
360
413
|
child.stderr.on("data", (chunk) => {
|
|
361
414
|
guarded("video.stderr.read", () => {
|
|
415
|
+
run.onStderr?.(chunk);
|
|
362
416
|
for (const line of chunk.split("\n")) {
|
|
363
417
|
if (line.trim() !== "") {
|
|
364
418
|
errors.push(line.trim());
|
|
@@ -436,5 +490,5 @@ function failure(code, signal, errors) {
|
|
|
436
490
|
const said = errors.length === 0
|
|
437
491
|
? `ffmpeg ${ended} and wrote nothing to its error stream`
|
|
438
492
|
: `ffmpeg ${ended}: ${errors.join(" / ")}`;
|
|
439
|
-
return `The audio/video export failed (${said}).
|
|
493
|
+
return `The audio/video export failed (${said}). Try again; if it fails again, make sure your disk has free space (see Settings → Backup & storage), then use Download diagnostics in Settings and report it.`;
|
|
440
494
|
}
|
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
import { remark } from "remark";
|
|
2
|
+
import remarkGfm from "remark-gfm";
|
|
3
|
+
import { runFfmpeg } from "./ffmpeg.js";
|
|
4
|
+
// The 3.0 design tokens (`packages/web/src/styles/index.css`): the surface graphite, ink,
|
|
5
|
+
// the second ink, Slopify lime and the line colour, in the bundled Barlow.
|
|
6
|
+
export const defaultCardStyle = {
|
|
7
|
+
ground: "#19191c",
|
|
8
|
+
ink: "#ece9e2",
|
|
9
|
+
muted: "#a9a69e",
|
|
10
|
+
accent: "#a6d45c",
|
|
11
|
+
line: "#2a2a30",
|
|
12
|
+
font: "Barlow",
|
|
13
|
+
mono: "DejaVu Sans Mono",
|
|
14
|
+
};
|
|
15
|
+
// ceiling: the owner's eye at 1080p. Text never smaller than 2.4% of the short side (26 px on
|
|
16
|
+
// a 1080-line frame), so a card stays readable on a phone; a table with more rows than fit at
|
|
17
|
+
// that size says how many it left out.
|
|
18
|
+
const minShare = 0.024;
|
|
19
|
+
const rowSpacing = 1.55;
|
|
20
|
+
const lineSpacing = 1.35;
|
|
21
|
+
// Barlow's average advance is about half its size; a monospace face's is 0.6.
|
|
22
|
+
const textAdvance = 0.52;
|
|
23
|
+
const monoAdvance = 0.6;
|
|
24
|
+
export function cardLayout(input) {
|
|
25
|
+
const { width, height } = input;
|
|
26
|
+
const short = Math.min(width, height);
|
|
27
|
+
const margin = Math.round(short * 0.07);
|
|
28
|
+
const events = [];
|
|
29
|
+
let top = margin;
|
|
30
|
+
const label = labelOf(input);
|
|
31
|
+
if (label !== "") {
|
|
32
|
+
const size = Math.round(short * 0.03);
|
|
33
|
+
events.push(text(margin, top, 7, `{\\fs${String(size)}\\c${colour(input.style.accent)}}${assText(label)}`));
|
|
34
|
+
top += Math.round(size * 1.9);
|
|
35
|
+
}
|
|
36
|
+
const area = { x: margin, y: top, width: width - margin * 2, height: height - top - margin };
|
|
37
|
+
let box;
|
|
38
|
+
switch (input.kind) {
|
|
39
|
+
case "table":
|
|
40
|
+
events.push(...tableEvents(input, area));
|
|
41
|
+
break;
|
|
42
|
+
case "figure": {
|
|
43
|
+
const caption = figureCaption(input.source);
|
|
44
|
+
const size = Math.round(short * 0.032);
|
|
45
|
+
const lines = wrap(caption, Math.floor(area.width / (size * textAdvance)), 3);
|
|
46
|
+
const captionHeight = lines.length === 0 ? 0 : Math.round(lines.length * size * lineSpacing);
|
|
47
|
+
if (input.picture) {
|
|
48
|
+
box = {
|
|
49
|
+
x: area.x,
|
|
50
|
+
y: area.y,
|
|
51
|
+
width: area.width,
|
|
52
|
+
height: Math.max(1, area.height - captionHeight - (lines.length === 0 ? 0 : size)),
|
|
53
|
+
};
|
|
54
|
+
for (const [at, line] of lines.entries())
|
|
55
|
+
events.push(text(width / 2, area.y + area.height - captionHeight + at * size * lineSpacing, 8, `{\\fs${String(size)}\\c${colour(input.style.muted)}}${assText(line)}`));
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
// No picture to show: the caption itself, large, as the figure's card.
|
|
59
|
+
const big = Math.round(short * 0.05);
|
|
60
|
+
const said = wrap(caption || "Figure", Math.floor(area.width / (big * textAdvance)), 6);
|
|
61
|
+
const start = area.y + (area.height - said.length * big * lineSpacing) / 2;
|
|
62
|
+
for (const [at, line] of said.entries())
|
|
63
|
+
events.push(text(width / 2, start + at * big * lineSpacing, 8, `{\\fs${String(big)}\\c${colour(input.style.ink)}}${assText(line)}`));
|
|
64
|
+
}
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
case "math":
|
|
68
|
+
events.push(...mathEvents(input, area));
|
|
69
|
+
break;
|
|
70
|
+
case "code":
|
|
71
|
+
case "diagram":
|
|
72
|
+
events.push(...codeEvents(input, area));
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
return { ass: script(input, events), ...(box === undefined ? {} : { box }) };
|
|
76
|
+
}
|
|
77
|
+
// The ffmpeg arguments that draw the card to `output` (a PNG) in `cwd`, which holds
|
|
78
|
+
// `card.ass` and a `fonts` folder.
|
|
79
|
+
export function cardArgs(input, layout, output, picture) {
|
|
80
|
+
const ground = `color=c=0x${input.style.ground.slice(1)}:s=${String(input.width)}x${String(input.height)}:d=1`;
|
|
81
|
+
const box = layout.box;
|
|
82
|
+
if (picture === undefined || box === undefined)
|
|
83
|
+
return [
|
|
84
|
+
...["-hide_banner", "-nostdin", "-loglevel", "error", "-y", "-f", "lavfi", "-i", ground],
|
|
85
|
+
...["-vf", "ass=filename=card.ass:fontsdir=fonts", "-frames:v", "1", output],
|
|
86
|
+
];
|
|
87
|
+
return [
|
|
88
|
+
...[
|
|
89
|
+
"-hide_banner",
|
|
90
|
+
"-nostdin",
|
|
91
|
+
"-loglevel",
|
|
92
|
+
"error",
|
|
93
|
+
"-y",
|
|
94
|
+
"-f",
|
|
95
|
+
"lavfi",
|
|
96
|
+
"-i",
|
|
97
|
+
ground,
|
|
98
|
+
"-i",
|
|
99
|
+
picture,
|
|
100
|
+
],
|
|
101
|
+
"-filter_complex",
|
|
102
|
+
[
|
|
103
|
+
`[1:v]scale=${String(box.width)}:${String(box.height)}:force_original_aspect_ratio=decrease:flags=lanczos,format=rgba[pic]`,
|
|
104
|
+
`[0:v][pic]overlay=x=${String(box.x)}+(${String(box.width)}-overlay_w)/2:y=${String(box.y)}+(${String(box.height)}-overlay_h)/2:format=auto,ass=filename=card.ass:fontsdir=fonts[v]`,
|
|
105
|
+
].join(";"),
|
|
106
|
+
...["-map", "[v]", "-frames:v", "1", output],
|
|
107
|
+
];
|
|
108
|
+
}
|
|
109
|
+
export async function renderCard(run, write) {
|
|
110
|
+
const layout = cardLayout(run.input);
|
|
111
|
+
write(layout.ass);
|
|
112
|
+
await runFfmpeg({
|
|
113
|
+
bin: run.bin,
|
|
114
|
+
args: cardArgs(run.input, layout, run.output, run.picture),
|
|
115
|
+
cwd: run.cwd,
|
|
116
|
+
signal: run.signal,
|
|
117
|
+
log: run.log,
|
|
118
|
+
onProgress: () => undefined,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
function labelOf(input) {
|
|
122
|
+
const section = input.section?.trim() ?? "";
|
|
123
|
+
if (input.kind === "code") {
|
|
124
|
+
const lang = /^```\s*([\w+#.-]+)/.exec(input.source)?.[1];
|
|
125
|
+
return [section, lang].filter((part) => part !== undefined && part !== "").join(" · ");
|
|
126
|
+
}
|
|
127
|
+
return section;
|
|
128
|
+
}
|
|
129
|
+
// A table as a clean grid: the header in the accent colour over a rule, a hairline between
|
|
130
|
+
// rows, numbers right-aligned. Sized to fit; past the smallest readable size the rows that do
|
|
131
|
+
// not fit are left out and counted.
|
|
132
|
+
function tableEvents(input, area) {
|
|
133
|
+
const rows = tableRows(input.source);
|
|
134
|
+
const header = rows[0] ?? [];
|
|
135
|
+
const body = rows.slice(1);
|
|
136
|
+
const columns = Math.max(1, ...rows.map((row) => row.length));
|
|
137
|
+
const short = Math.min(input.width, input.height);
|
|
138
|
+
const smallest = Math.round(short * minShare);
|
|
139
|
+
const largest = Math.round(short * 0.045);
|
|
140
|
+
// Every row but the header, and a line for what was left out when some are.
|
|
141
|
+
let shown = body.length;
|
|
142
|
+
let size = Math.floor(area.height / ((shown + 1) * rowSpacing));
|
|
143
|
+
if (size < smallest) {
|
|
144
|
+
size = smallest;
|
|
145
|
+
shown = Math.max(1, Math.floor(area.height / (size * rowSpacing)) - 2);
|
|
146
|
+
}
|
|
147
|
+
size = Math.min(size, largest);
|
|
148
|
+
const cells = (row) => Array.from({ length: columns }, (_value, at) => row[at] ?? "");
|
|
149
|
+
const kept = [cells(header), ...body.slice(0, shown).map(cells)];
|
|
150
|
+
// Each column as wide as its widest cell (at most 28 letters); cut to fit if need be.
|
|
151
|
+
const letters = Array.from({ length: columns }, (_value, at) => Math.min(28, Math.max(3, ...kept.map((row) => (row[at] ?? "").length))));
|
|
152
|
+
const gap = 1.6;
|
|
153
|
+
const wanted = (letters.reduce((sum, n) => sum + n, 0) + gap * columns) * size * textAdvance;
|
|
154
|
+
if (wanted > area.width)
|
|
155
|
+
size = Math.max(smallest, Math.floor((size * area.width) / wanted));
|
|
156
|
+
const scale = area.width /
|
|
157
|
+
Math.max(area.width, (letters.reduce((sum, n) => sum + n, 0) + gap * columns) * size * textAdvance);
|
|
158
|
+
const widths = letters.map((n) => (n + gap) * size * textAdvance * scale);
|
|
159
|
+
const room = (at) => Math.max(3, Math.floor((widths[at] ?? 0) / (size * textAdvance)) - 1);
|
|
160
|
+
const rowHeight = Math.round(size * rowSpacing);
|
|
161
|
+
const events = [];
|
|
162
|
+
// Centred across the card; a narrow table is not pushed into a corner.
|
|
163
|
+
const tableWidth = widths.reduce((sum, n) => sum + n, 0);
|
|
164
|
+
const left = area.x + Math.max(0, (area.width - tableWidth) / 2);
|
|
165
|
+
const right = left + tableWidth;
|
|
166
|
+
kept.forEach((row, index) => {
|
|
167
|
+
const y = area.y + index * rowHeight;
|
|
168
|
+
let x = left;
|
|
169
|
+
row.forEach((cell, at) => {
|
|
170
|
+
const value = clip(cell, room(at));
|
|
171
|
+
const numeric = index > 0 && /^[-+−]?[\d.,]+\s*%?$|^[-+−]?[\d.,]+\s*[a-zA-Zµ%/]{0,4}$/.test(cell.trim());
|
|
172
|
+
const cellWidth = widths[at] ?? 0;
|
|
173
|
+
const colourTag = index === 0 ? colour(input.style.accent) : colour(input.style.ink);
|
|
174
|
+
events.push(numeric
|
|
175
|
+
? text(x + cellWidth - size * 0.8, y + rowHeight / 2, 6, `{\\fs${String(size)}\\c${colourTag}}${assText(value)}`)
|
|
176
|
+
: text(x, y + rowHeight / 2, 4, `{\\fs${String(size)}\\c${colourTag}${index === 0 ? "\\b1" : ""}}${assText(value)}`));
|
|
177
|
+
x += cellWidth;
|
|
178
|
+
});
|
|
179
|
+
// The rule under the header is the accent's; the rest are hairlines.
|
|
180
|
+
const under = y + rowHeight;
|
|
181
|
+
const thick = index === 0 ? Math.max(2, Math.round(size / 10)) : 1;
|
|
182
|
+
events.push(rule(left, under, right - left, thick, index === 0 ? input.style.accent : input.style.line));
|
|
183
|
+
});
|
|
184
|
+
const leftOut = body.length - Math.min(shown, body.length);
|
|
185
|
+
if (leftOut > 0)
|
|
186
|
+
events.push(text(left, area.y + kept.length * rowHeight + rowHeight / 2, 4, `{\\fs${String(size)}\\c${colour(input.style.muted)}\\i1}${assText(`…and ${String(leftOut)} more ${leftOut === 1 ? "row" : "rows"}`)}`));
|
|
187
|
+
return events;
|
|
188
|
+
}
|
|
189
|
+
function tableRows(markdown) {
|
|
190
|
+
const tree = remark().use(remarkGfm).parse(markdown);
|
|
191
|
+
const table = tree.children.find((node) => node.type === "table");
|
|
192
|
+
if (table === undefined)
|
|
193
|
+
return [];
|
|
194
|
+
return table.children.map((row) => row.children.map((cell) => plain(cell).trim()));
|
|
195
|
+
}
|
|
196
|
+
function plain(node) {
|
|
197
|
+
if ("value" in node && typeof node.value === "string")
|
|
198
|
+
return node.value;
|
|
199
|
+
if (node.type === "image")
|
|
200
|
+
return node.alt ?? "";
|
|
201
|
+
if ("children" in node)
|
|
202
|
+
return node.children.map((child) => plain(child)).join("");
|
|
203
|
+
return "";
|
|
204
|
+
}
|
|
205
|
+
// Code and drawings in the monospace face, one line per line, with simple colouring: comments
|
|
206
|
+
// muted, strings and numbers in the ink's warm second tone, keywords in the accent.
|
|
207
|
+
function codeEvents(input, area) {
|
|
208
|
+
const lines = fenced(input.source).split("\n");
|
|
209
|
+
const longest = Math.max(12, ...lines.map((line) => line.length));
|
|
210
|
+
const short = Math.min(input.width, input.height);
|
|
211
|
+
const smallest = Math.round(short * minShare * 0.9);
|
|
212
|
+
let size = Math.min(Math.round(short * 0.04), Math.floor(area.width / (longest * monoAdvance)), Math.floor(area.height / (lines.length * lineSpacing)));
|
|
213
|
+
let shown = lines.length;
|
|
214
|
+
if (size < smallest) {
|
|
215
|
+
size = smallest;
|
|
216
|
+
shown = Math.max(1, Math.floor(area.height / (size * lineSpacing)) - 1);
|
|
217
|
+
}
|
|
218
|
+
const room = Math.floor(area.width / (size * monoAdvance));
|
|
219
|
+
// A short program sits in the middle of the card rather than at its top.
|
|
220
|
+
const top = area.y + Math.max(0, (area.height - shown * size * lineSpacing) / 2);
|
|
221
|
+
const events = lines
|
|
222
|
+
.slice(0, shown)
|
|
223
|
+
.map((line, at) => text(area.x, top + at * size * lineSpacing, 7, `{\\fn${input.style.mono}\\fs${String(size)}\\c${colour(input.style.ink)}}${input.kind === "code"
|
|
224
|
+
? coloured(clip(line, room), input.style)
|
|
225
|
+
: assText(clip(line, room), true)}`));
|
|
226
|
+
if (shown < lines.length)
|
|
227
|
+
events.push(text(area.x, top + shown * size * lineSpacing, 7, `{\\fs${String(size)}\\c${colour(input.style.muted)}\\i1}${assText(`…and ${String(lines.length - shown)} more lines`)}`));
|
|
228
|
+
return events;
|
|
229
|
+
}
|
|
230
|
+
const keywords = new Set("fn let mut pub use mod impl struct enum trait match if else for while loop return break continue in as where const static async await function var class extends new this def import from export type interface yield lambda try catch except finally raise throw public private protected void int float double bool boolean char string true false null None nil self Self package func go defer select case switch default".split(" "));
|
|
231
|
+
function coloured(line, style) {
|
|
232
|
+
const comment = /(\/\/|#(?!\[)|--\s).*$/.exec(line);
|
|
233
|
+
const code = comment === null ? line : line.slice(0, comment.index);
|
|
234
|
+
const out = code.replace(/("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|\b\d[\d._]*\b|\b[A-Za-z_]\w*\b|[^"'\w]+)/g, (token) => {
|
|
235
|
+
if (/^["']/.test(token))
|
|
236
|
+
return `{\\c${colour(style.muted)}}${assText(token, true)}{\\c${colour(style.ink)}}`;
|
|
237
|
+
if (/^\d/.test(token))
|
|
238
|
+
return `{\\c${colour(style.muted)}}${assText(token, true)}{\\c${colour(style.ink)}}`;
|
|
239
|
+
if (keywords.has(token))
|
|
240
|
+
return `{\\c${colour(style.accent)}}${token}{\\c${colour(style.ink)}}`;
|
|
241
|
+
return assText(token, true);
|
|
242
|
+
});
|
|
243
|
+
return comment === null
|
|
244
|
+
? out
|
|
245
|
+
: `${out}{\\c${colour(style.muted)}\\i1}${assText(comment[0], true)}`;
|
|
246
|
+
}
|
|
247
|
+
// An equation set in readable text: TeX's commands as the symbols they stand for, powers
|
|
248
|
+
// and indices as superscripts and subscripts where Unicode has them, centred and large.
|
|
249
|
+
function mathEvents(input, area) {
|
|
250
|
+
const tex = input.source
|
|
251
|
+
.replace(/^\$\$|\$\$$/g, "")
|
|
252
|
+
.replace(/^\\\[|\\\]$/g, "")
|
|
253
|
+
.replace(/^```\s*\w*\n?|```$/g, "")
|
|
254
|
+
.trim();
|
|
255
|
+
// A paragraph with formulas in it shows the paragraph with its formulas set.
|
|
256
|
+
const said = /\$[^$]+\$/.test(tex)
|
|
257
|
+
? tex.replace(/\$\$?([^$]+)\$\$?/g, (_all, inner) => typeset(inner))
|
|
258
|
+
: typeset(tex);
|
|
259
|
+
const short = Math.min(input.width, input.height);
|
|
260
|
+
const size = Math.round(short * (said.length > 60 ? 0.04 : 0.06));
|
|
261
|
+
const lines = wrap(said, Math.floor(area.width / (size * textAdvance)), 6);
|
|
262
|
+
const start = area.y + (area.height - lines.length * size * lineSpacing) / 2;
|
|
263
|
+
return lines.map((line, at) => text(input.width / 2, start + at * size * lineSpacing, 8, `{\\fs${String(size)}\\c${colour(input.style.ink)}}${assText(line)}`));
|
|
264
|
+
}
|
|
265
|
+
const symbols = {
|
|
266
|
+
alpha: "α",
|
|
267
|
+
beta: "β",
|
|
268
|
+
gamma: "γ",
|
|
269
|
+
delta: "δ",
|
|
270
|
+
epsilon: "ε",
|
|
271
|
+
varepsilon: "ε",
|
|
272
|
+
zeta: "ζ",
|
|
273
|
+
eta: "η",
|
|
274
|
+
theta: "θ",
|
|
275
|
+
iota: "ι",
|
|
276
|
+
kappa: "κ",
|
|
277
|
+
lambda: "λ",
|
|
278
|
+
mu: "μ",
|
|
279
|
+
nu: "ν",
|
|
280
|
+
xi: "ξ",
|
|
281
|
+
pi: "π",
|
|
282
|
+
rho: "ρ",
|
|
283
|
+
sigma: "σ",
|
|
284
|
+
tau: "τ",
|
|
285
|
+
upsilon: "υ",
|
|
286
|
+
phi: "φ",
|
|
287
|
+
varphi: "φ",
|
|
288
|
+
chi: "χ",
|
|
289
|
+
psi: "ψ",
|
|
290
|
+
omega: "ω",
|
|
291
|
+
Gamma: "Γ",
|
|
292
|
+
Delta: "Δ",
|
|
293
|
+
Theta: "Θ",
|
|
294
|
+
Lambda: "Λ",
|
|
295
|
+
Xi: "Ξ",
|
|
296
|
+
Pi: "Π",
|
|
297
|
+
Sigma: "Σ",
|
|
298
|
+
Phi: "Φ",
|
|
299
|
+
Psi: "Ψ",
|
|
300
|
+
Omega: "Ω",
|
|
301
|
+
cdot: "·",
|
|
302
|
+
times: "×",
|
|
303
|
+
div: "÷",
|
|
304
|
+
pm: "±",
|
|
305
|
+
mp: "∓",
|
|
306
|
+
le: "≤",
|
|
307
|
+
leq: "≤",
|
|
308
|
+
ge: "≥",
|
|
309
|
+
geq: "≥",
|
|
310
|
+
neq: "≠",
|
|
311
|
+
ne: "≠",
|
|
312
|
+
approx: "≈",
|
|
313
|
+
sim: "∼",
|
|
314
|
+
equiv: "≡",
|
|
315
|
+
propto: "∝",
|
|
316
|
+
infty: "∞",
|
|
317
|
+
partial: "∂",
|
|
318
|
+
nabla: "∇",
|
|
319
|
+
sum: "Σ",
|
|
320
|
+
prod: "Π",
|
|
321
|
+
int: "∫",
|
|
322
|
+
to: "→",
|
|
323
|
+
rightarrow: "→",
|
|
324
|
+
leftarrow: "←",
|
|
325
|
+
Rightarrow: "⇒",
|
|
326
|
+
in: "∈",
|
|
327
|
+
forall: "∀",
|
|
328
|
+
exists: "∃",
|
|
329
|
+
ldots: "…",
|
|
330
|
+
cdots: "⋯",
|
|
331
|
+
degree: "°",
|
|
332
|
+
circ: "°",
|
|
333
|
+
};
|
|
334
|
+
const superscripts = {
|
|
335
|
+
"0": "⁰",
|
|
336
|
+
"1": "¹",
|
|
337
|
+
"2": "²",
|
|
338
|
+
"3": "³",
|
|
339
|
+
"4": "⁴",
|
|
340
|
+
"5": "⁵",
|
|
341
|
+
"6": "⁶",
|
|
342
|
+
"7": "⁷",
|
|
343
|
+
"8": "⁸",
|
|
344
|
+
"9": "⁹",
|
|
345
|
+
"+": "⁺",
|
|
346
|
+
"-": "⁻",
|
|
347
|
+
"=": "⁼",
|
|
348
|
+
"(": "⁽",
|
|
349
|
+
")": "⁾",
|
|
350
|
+
n: "ⁿ",
|
|
351
|
+
i: "ⁱ",
|
|
352
|
+
};
|
|
353
|
+
const subscripts = {
|
|
354
|
+
"0": "₀",
|
|
355
|
+
"1": "₁",
|
|
356
|
+
"2": "₂",
|
|
357
|
+
"3": "₃",
|
|
358
|
+
"4": "₄",
|
|
359
|
+
"5": "₅",
|
|
360
|
+
"6": "₆",
|
|
361
|
+
"7": "₇",
|
|
362
|
+
"8": "₈",
|
|
363
|
+
"9": "₉",
|
|
364
|
+
"+": "₊",
|
|
365
|
+
"-": "₋",
|
|
366
|
+
"=": "₌",
|
|
367
|
+
"(": "₍",
|
|
368
|
+
")": "₎",
|
|
369
|
+
a: "ₐ",
|
|
370
|
+
e: "ₑ",
|
|
371
|
+
o: "ₒ",
|
|
372
|
+
x: "ₓ",
|
|
373
|
+
i: "ᵢ",
|
|
374
|
+
j: "ⱼ",
|
|
375
|
+
k: "ₖ",
|
|
376
|
+
n: "ₙ",
|
|
377
|
+
t: "ₜ",
|
|
378
|
+
s: "ₛ",
|
|
379
|
+
r: "ᵣ",
|
|
380
|
+
m: "ₘ",
|
|
381
|
+
p: "ₚ",
|
|
382
|
+
};
|
|
383
|
+
export function typeset(tex) {
|
|
384
|
+
let out = tex
|
|
385
|
+
.replace(/\\(left|right|big|Big|bigg|Bigg|displaystyle|,|;|!|quad|qquad)\b/g, " ")
|
|
386
|
+
.replace(/\\(mathrm|mathbf|mathit|text|operatorname)\{([^}]*)\}/g, "$2");
|
|
387
|
+
// Innermost first, so a fraction inside a root comes out whole.
|
|
388
|
+
for (let pass = 0; pass < 4; pass++)
|
|
389
|
+
out = out
|
|
390
|
+
.replace(/\\frac\{([^{}]*)\}\{([^{}]*)\}/g, (_all, a, b) => `${group(a)}/${group(b)}`)
|
|
391
|
+
.replace(/\\sqrt\{([^{}]*)\}/g, (_all, a) => `√${group(a)}`);
|
|
392
|
+
out = out
|
|
393
|
+
.replace(/\\([A-Za-z]+)/g, (_all, name) => symbols[name] ?? name)
|
|
394
|
+
.replace(/\^\{([^{}]*)\}|\^(\S)/g, (_all, a, b) => scripted(a ?? b ?? "", superscripts, "^"))
|
|
395
|
+
.replace(/_\{([^{}]*)\}|_(\S)/g, (_all, a, b) => scripted(a ?? b ?? "", subscripts, "_"))
|
|
396
|
+
.replace(/[{}]/g, "")
|
|
397
|
+
.replace(/\s*([=<>≤≥≈≠+×·−])\s*/g, " $1 ")
|
|
398
|
+
.replace(/\s+/g, " ")
|
|
399
|
+
.trim();
|
|
400
|
+
return out;
|
|
401
|
+
}
|
|
402
|
+
function group(text) {
|
|
403
|
+
const clean = text.trim();
|
|
404
|
+
return /^[\w.]+$/.test(clean) ? clean : `(${clean})`;
|
|
405
|
+
}
|
|
406
|
+
function scripted(text, map, mark) {
|
|
407
|
+
const letters = [...text];
|
|
408
|
+
return letters.every((letter) => map[letter] !== undefined)
|
|
409
|
+
? letters.map((letter) => map[letter]).join("")
|
|
410
|
+
: `${mark}(${text})`;
|
|
411
|
+
}
|
|
412
|
+
function figureCaption(source) {
|
|
413
|
+
const lines = source.split("\n");
|
|
414
|
+
const captions = lines
|
|
415
|
+
.filter((line) => line.startsWith("Caption: "))
|
|
416
|
+
.map((line) => line.slice(9));
|
|
417
|
+
const alt = lines.find((line) => line.startsWith("Alt text: "))?.slice(10);
|
|
418
|
+
return (captions.length > 0 ? captions.join(" ") : (alt ?? "")).trim();
|
|
419
|
+
}
|
|
420
|
+
function fenced(source) {
|
|
421
|
+
return source
|
|
422
|
+
.replace(/^\s*(```|~~~)[^\n]*\n?/, "")
|
|
423
|
+
.replace(/\n?(```|~~~)\s*$/, "")
|
|
424
|
+
.replace(/\t/g, " ")
|
|
425
|
+
.replace(/\s+$/, "");
|
|
426
|
+
}
|
|
427
|
+
function wrap(textValue, width, most) {
|
|
428
|
+
const words = textValue.split(/\s+/).filter(Boolean);
|
|
429
|
+
const lines = [];
|
|
430
|
+
let line = "";
|
|
431
|
+
for (const word of words) {
|
|
432
|
+
if (line === "")
|
|
433
|
+
line = word;
|
|
434
|
+
else if (line.length + 1 + word.length <= width)
|
|
435
|
+
line += ` ${word}`;
|
|
436
|
+
else {
|
|
437
|
+
lines.push(line);
|
|
438
|
+
line = word;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
if (line !== "")
|
|
442
|
+
lines.push(line);
|
|
443
|
+
if (lines.length <= most)
|
|
444
|
+
return lines;
|
|
445
|
+
const kept = lines.slice(0, most);
|
|
446
|
+
kept[most - 1] = clip(`${kept[most - 1] ?? ""} …`, width);
|
|
447
|
+
return kept;
|
|
448
|
+
}
|
|
449
|
+
function clip(value, room) {
|
|
450
|
+
return value.length <= room ? value : `${value.slice(0, Math.max(1, room - 1))}…`;
|
|
451
|
+
}
|
|
452
|
+
// Text as libass draws it literally: braces and backslashes escaped rather than read as
|
|
453
|
+
// override tags; code keeps its spaces, prose has them collapsed.
|
|
454
|
+
function assText(value, keepSpaces = false) {
|
|
455
|
+
const escaped = value.replace(/\\/g, "\\\\").replace(/\{/g, "\\{").replace(/\}/g, "\\}");
|
|
456
|
+
return keepSpaces
|
|
457
|
+
? escaped.replace(/ {2,}/g, (run) => "\\h".repeat(run.length))
|
|
458
|
+
: escaped.replace(/\s+/g, " ");
|
|
459
|
+
}
|
|
460
|
+
function colour(hex) {
|
|
461
|
+
const match = /^#([0-9A-Fa-f]{2})([0-9A-Fa-f]{2})([0-9A-Fa-f]{2})$/.exec(hex);
|
|
462
|
+
if (match === null)
|
|
463
|
+
return "&HFFFFFF&";
|
|
464
|
+
const [, r, g, b] = match;
|
|
465
|
+
return `&H${(b ?? "").toUpperCase()}${(g ?? "").toUpperCase()}${(r ?? "").toUpperCase()}&`;
|
|
466
|
+
}
|
|
467
|
+
function text(x, y, align, body) {
|
|
468
|
+
return `Dialogue: 0,0:00:00.00,0:00:10.00,Card,,0,0,0,,{\\an${String(align)}\\pos(${String(Math.round(x))},${String(Math.round(y))})}${body}`;
|
|
469
|
+
}
|
|
470
|
+
function rule(x, y, width, thick, hex) {
|
|
471
|
+
const [x0, y0, x1, y1] = [x, y, x + width, y + thick].map((n) => String(Math.round(n)));
|
|
472
|
+
return `Dialogue: 0,0:00:00.00,0:00:10.00,Card,,0,0,0,,{\\an7\\pos(0,0)\\p1\\bord0\\shad0\\c${colour(hex)}}m ${x0} ${y0} l ${x1} ${y0} l ${x1} ${y1} l ${x0} ${y1}{\\p0}`;
|
|
473
|
+
}
|
|
474
|
+
function script(input, events) {
|
|
475
|
+
const { width, height, style } = input;
|
|
476
|
+
return `${[
|
|
477
|
+
"[Script Info]",
|
|
478
|
+
"ScriptType: v4.00+",
|
|
479
|
+
`PlayResX: ${String(width)}`,
|
|
480
|
+
`PlayResY: ${String(height)}`,
|
|
481
|
+
"WrapStyle: 2",
|
|
482
|
+
"ScaledBorderAndShadow: yes",
|
|
483
|
+
"",
|
|
484
|
+
"[V4+ Styles]",
|
|
485
|
+
"Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding",
|
|
486
|
+
`Style: Card,${style.font.replace(/,/g, " ")},${String(Math.round(Math.min(width, height) * 0.03))},&H00FFFFFF,&H00FFFFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,0,0,7,0,0,0,1`,
|
|
487
|
+
"",
|
|
488
|
+
"[Events]",
|
|
489
|
+
"Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text",
|
|
490
|
+
...events,
|
|
491
|
+
].join("\n")}\n`;
|
|
492
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// ceiling: the words compared at each end of a passage, and how many of them must match.
|
|
2
|
+
const edgeWords = 4;
|
|
3
|
+
const matchShare = 0.75;
|
|
4
|
+
export function passageSpans(words, passages) {
|
|
5
|
+
const flat = [];
|
|
6
|
+
for (const [index, word] of words.entries())
|
|
7
|
+
for (const token of tokens(word.text))
|
|
8
|
+
flat.push({ token, word: index });
|
|
9
|
+
let cursor = 0;
|
|
10
|
+
return passages.map((passage) => {
|
|
11
|
+
const wanted = tokens(passage);
|
|
12
|
+
if (wanted.length === 0)
|
|
13
|
+
return undefined;
|
|
14
|
+
const k = Math.min(edgeWords, wanted.length);
|
|
15
|
+
const head = wanted.slice(0, k);
|
|
16
|
+
const start = find(flat, head, cursor, flat.length);
|
|
17
|
+
if (start === undefined)
|
|
18
|
+
return undefined;
|
|
19
|
+
const tail = wanted.slice(-k);
|
|
20
|
+
const from = start + Math.max(0, Math.floor(wanted.length / 2) - k);
|
|
21
|
+
const to = Math.min(flat.length, start + Math.ceil(wanted.length * 1.5) + k);
|
|
22
|
+
const last = find(flat, tail, from, to);
|
|
23
|
+
const endToken = Math.min(flat.length - 1, last === undefined ? start + wanted.length - 1 : last + k - 1);
|
|
24
|
+
cursor = endToken + 1;
|
|
25
|
+
const first = words[flat[start]?.word ?? 0];
|
|
26
|
+
const final = words[flat[endToken]?.word ?? 0];
|
|
27
|
+
return first === undefined || final === undefined
|
|
28
|
+
? undefined
|
|
29
|
+
: { start: first.start, end: Math.max(first.start, final.end) };
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
function find(flat, wanted, from, to) {
|
|
33
|
+
const need = Math.ceil(wanted.length * matchShare);
|
|
34
|
+
for (let at = from; at + wanted.length <= Math.max(to, from + wanted.length) && at + wanted.length <= flat.length; at++) {
|
|
35
|
+
let hits = 0;
|
|
36
|
+
for (const [offset, token] of wanted.entries())
|
|
37
|
+
if (flat[at + offset]?.token === token)
|
|
38
|
+
hits += 1;
|
|
39
|
+
if (hits >= need)
|
|
40
|
+
return at;
|
|
41
|
+
}
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
function tokens(text) {
|
|
45
|
+
return text
|
|
46
|
+
.toLowerCase()
|
|
47
|
+
.normalize("NFKD")
|
|
48
|
+
.replace(/\p{M}/gu, "")
|
|
49
|
+
.split(/[^\p{L}\p{N}]+/u)
|
|
50
|
+
.filter(Boolean);
|
|
51
|
+
}
|