@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
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// Chapters as YouTube takes them. YouTube turns the timestamps of a description into chapters
|
|
2
|
+
// only when the first is at 0:00, there are at least three, each lasts at least ten seconds and
|
|
3
|
+
// they go up in time; otherwise it ignores the whole list. The step's answer is checked against
|
|
4
|
+
// those rules when it is written (`answer.ts`), but a hand edit, or a video cut again to another
|
|
5
|
+
// length after the description was written, can break them. So the chapters are fitted when the
|
|
6
|
+
// description is shown or copied, never stored: the generated file, the user's edit and every
|
|
7
|
+
// fingerprint stay as they were, and the page says what was changed. Pure and browser-safe.
|
|
8
|
+
import { minChapterSeconds, minChapters } from "./model.js";
|
|
9
|
+
import { parseTimestamp, youtubeTimestamp } from "./timestamps.js";
|
|
10
|
+
// "M:SS Title": a time YouTube reads, then a title.
|
|
11
|
+
function chapterLine(line) {
|
|
12
|
+
const trimmed = line.trim();
|
|
13
|
+
const match = /^(\S+)\s+(.+)$/u.exec(trimmed);
|
|
14
|
+
if (match === null)
|
|
15
|
+
return undefined;
|
|
16
|
+
const start = parseTimestamp(match[1] ?? "");
|
|
17
|
+
const title = (match[2] ?? "").trim();
|
|
18
|
+
return start === undefined || title === "" ? undefined : { start, title };
|
|
19
|
+
}
|
|
20
|
+
// Fits one chapters field (one "M:SS Title" per line) to YouTube's rules, in this order:
|
|
21
|
+
// 1. chapters out of time order are put in order, and of two at the same time the one listed
|
|
22
|
+
// first stays;
|
|
23
|
+
// 2. with the video's length known, a chapter starting at or after its end is removed;
|
|
24
|
+
// 3. the first chapter moves to 0:00;
|
|
25
|
+
// 4. the first chapter shorter than ten seconds is merged into its neighbour, again until none
|
|
26
|
+
// is: into the one before it (whose title stays), or, for the first chapter, into the one
|
|
27
|
+
// after it, which then starts at 0:00. The last chapter's length is only known with the
|
|
28
|
+
// video's length;
|
|
29
|
+
// 5. with fewer than three chapters left, the list is dropped, since YouTube would ignore it.
|
|
30
|
+
// Other lines in the field (a note the user typed) stay where they were. When anything changed,
|
|
31
|
+
// the chapter lines are rewritten in YouTube's M:SS form.
|
|
32
|
+
export function fitChapters(text, durationSeconds) {
|
|
33
|
+
const lines = text.replace(/\r\n?/gu, "\n").split("\n");
|
|
34
|
+
const slots = [];
|
|
35
|
+
const found = [];
|
|
36
|
+
for (const [index, line] of lines.entries()) {
|
|
37
|
+
const chapter = chapterLine(line);
|
|
38
|
+
if (chapter === undefined)
|
|
39
|
+
continue;
|
|
40
|
+
slots.push(index);
|
|
41
|
+
found.push(chapter);
|
|
42
|
+
}
|
|
43
|
+
if (found.length === 0)
|
|
44
|
+
return { text, adjustments: [] };
|
|
45
|
+
const adjustments = [];
|
|
46
|
+
const quoted = (chapter) => `"${chapter.title}"`;
|
|
47
|
+
let chapters = [...found].sort((a, b) => a.start - b.start);
|
|
48
|
+
if (chapters.some((chapter, index) => chapter !== found[index]))
|
|
49
|
+
adjustments.push("put the chapters in time order");
|
|
50
|
+
// Two chapters at the same time: the one listed first keeps it.
|
|
51
|
+
const distinct = [];
|
|
52
|
+
for (const chapter of chapters) {
|
|
53
|
+
const previous = distinct.at(-1);
|
|
54
|
+
if (previous !== undefined && previous.start === chapter.start)
|
|
55
|
+
adjustments.push(`merged ${quoted(chapter)} (0 s) into ${quoted(previous)}`);
|
|
56
|
+
else
|
|
57
|
+
distinct.push(chapter);
|
|
58
|
+
}
|
|
59
|
+
chapters = distinct;
|
|
60
|
+
const end = durationSeconds !== undefined && Number.isFinite(durationSeconds) && durationSeconds > 0
|
|
61
|
+
? durationSeconds
|
|
62
|
+
: undefined;
|
|
63
|
+
if (end !== undefined) {
|
|
64
|
+
const after = chapters.filter((chapter, index) => index > 0 && chapter.start >= end);
|
|
65
|
+
for (const chapter of after)
|
|
66
|
+
adjustments.push(`removed ${quoted(chapter)} (${youtubeTimestamp(chapter.start)}), which starts after the video ends at ${youtubeTimestamp(end)}`);
|
|
67
|
+
chapters = chapters.filter((chapter) => !after.includes(chapter));
|
|
68
|
+
}
|
|
69
|
+
const first = chapters[0];
|
|
70
|
+
if (first !== undefined && first.start !== 0) {
|
|
71
|
+
adjustments.push(`moved the first, ${quoted(first)}, from ${youtubeTimestamp(first.start)} to 0:00`);
|
|
72
|
+
chapters = [{ start: 0, title: first.title }, ...chapters.slice(1)];
|
|
73
|
+
}
|
|
74
|
+
const length = (index) => {
|
|
75
|
+
const next = chapters[index + 1];
|
|
76
|
+
const start = chapters[index]?.start ?? 0;
|
|
77
|
+
if (next !== undefined)
|
|
78
|
+
return next.start - start;
|
|
79
|
+
return end === undefined ? Number.POSITIVE_INFINITY : end - start;
|
|
80
|
+
};
|
|
81
|
+
const seconds = (value) => `${String(Math.max(0, Math.floor(value)))} s`;
|
|
82
|
+
while (chapters.length > 1) {
|
|
83
|
+
const short = chapters.findIndex((_, index) => length(index) < minChapterSeconds);
|
|
84
|
+
if (short === -1)
|
|
85
|
+
break;
|
|
86
|
+
const chapter = chapters[short];
|
|
87
|
+
if (chapter === undefined)
|
|
88
|
+
break;
|
|
89
|
+
const lasted = seconds(length(short));
|
|
90
|
+
if (short === 0) {
|
|
91
|
+
const next = chapters[1];
|
|
92
|
+
if (next === undefined)
|
|
93
|
+
break;
|
|
94
|
+
adjustments.push(`merged ${quoted(chapter)} (${lasted}) into ${quoted(next)}`);
|
|
95
|
+
chapters = [{ start: 0, title: next.title }, ...chapters.slice(2)];
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
const previous = chapters[short - 1];
|
|
99
|
+
if (previous === undefined)
|
|
100
|
+
break;
|
|
101
|
+
adjustments.push(`merged ${quoted(chapter)} (${lasted}) into ${quoted(previous)}`);
|
|
102
|
+
chapters = chapters.filter((_, index) => index !== short);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (chapters.length < minChapters) {
|
|
106
|
+
adjustments.push(`left the chapter list out, since YouTube needs at least ${String(minChapters)} chapters of ${String(minChapterSeconds)} s or more and only ${String(chapters.length)} ${chapters.length === 1 ? "is" : "are"} left`);
|
|
107
|
+
chapters = [];
|
|
108
|
+
}
|
|
109
|
+
if (adjustments.length === 0)
|
|
110
|
+
return { text, adjustments };
|
|
111
|
+
// The chapters go back into the first slots in order; the slots left over are removed.
|
|
112
|
+
const bySlot = new Map(slots.map((slot, index) => [slot, chapters[index]]));
|
|
113
|
+
const rewritten = lines.flatMap((line, index) => {
|
|
114
|
+
if (!bySlot.has(index))
|
|
115
|
+
return [line];
|
|
116
|
+
const chapter = bySlot.get(index);
|
|
117
|
+
return chapter === undefined ? [] : [`${youtubeTimestamp(chapter.start)} ${chapter.title}`];
|
|
118
|
+
});
|
|
119
|
+
return { text: rewritten.join("\n").trim(), adjustments };
|
|
120
|
+
}
|
|
121
|
+
// The sentence shown beside the description; undefined when nothing was changed.
|
|
122
|
+
export function chapterNotice(adjustments) {
|
|
123
|
+
return adjustments.length === 0
|
|
124
|
+
? undefined
|
|
125
|
+
: `Chapters adjusted for YouTube: ${adjustments.join("; ")}.`;
|
|
126
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Where the hand edits and the channel links are kept: the edits per project in
|
|
2
|
+
// `youtube_description_edits`, the Settings list of links as one `settings` row.
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { readSetting, writeSetting } from "../settings/repo.js";
|
|
5
|
+
import { chapterNotice, fitChapters } from "./chapters.js";
|
|
6
|
+
import { composeDescription, descriptionFields, resolveFields, shownFields, splitDescription, } from "./edits.js";
|
|
7
|
+
import { fillPlaceholders, mergeLinks } from "./placeholders.js";
|
|
8
|
+
export const channelLinksKey = "channel_links";
|
|
9
|
+
const fieldEdit = z.object({ base: z.string(), text: z.string() }).strict();
|
|
10
|
+
const editsSchema = z
|
|
11
|
+
.object(Object.fromEntries(descriptionFields.map((field) => [field, fieldEdit.optional()])))
|
|
12
|
+
.strict();
|
|
13
|
+
const linksSchema = z.array(z.object({ name: z.string(), url: z.string() }).strict());
|
|
14
|
+
const none = { fields: {}, links: [] };
|
|
15
|
+
export function readDescriptionEdits(db, projectId) {
|
|
16
|
+
const row = db
|
|
17
|
+
.prepare("SELECT fields_json,links_json FROM youtube_description_edits WHERE project_id=?")
|
|
18
|
+
.get(projectId);
|
|
19
|
+
if (row === undefined)
|
|
20
|
+
return none;
|
|
21
|
+
const parsed = z.object({ fields_json: z.string(), links_json: z.string() }).parse(row);
|
|
22
|
+
return {
|
|
23
|
+
fields: editsSchema.parse(JSON.parse(parsed.fields_json)),
|
|
24
|
+
links: linksSchema.parse(JSON.parse(parsed.links_json)),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export function writeDescriptionEdits(db, projectId, edits, updatedAt) {
|
|
28
|
+
if (Object.keys(edits.fields).length === 0 && edits.links.length === 0) {
|
|
29
|
+
db.prepare("DELETE FROM youtube_description_edits WHERE project_id=?").run(projectId);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
db.prepare(`INSERT INTO youtube_description_edits (project_id,fields_json,links_json,updated_at) VALUES (?,?,?,?)
|
|
33
|
+
ON CONFLICT(project_id) DO UPDATE SET fields_json=excluded.fields_json,links_json=excluded.links_json,updated_at=excluded.updated_at`).run(projectId, JSON.stringify(edits.fields), JSON.stringify(edits.links), updatedAt);
|
|
34
|
+
}
|
|
35
|
+
export function readChannelLinks(db) {
|
|
36
|
+
const value = readSetting(db, channelLinksKey);
|
|
37
|
+
if (value === undefined)
|
|
38
|
+
return [];
|
|
39
|
+
// A hand-edited row that no longer parses reads as no links rather than breaking the page;
|
|
40
|
+
// saving the list in Settings writes a good one again.
|
|
41
|
+
try {
|
|
42
|
+
const parsed = linksSchema.safeParse(JSON.parse(value));
|
|
43
|
+
return parsed.success ? parsed.data : [];
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export function writeChannelLinks(db, links) {
|
|
50
|
+
writeSetting(db, channelLinksKey, JSON.stringify(links.map((link) => ({ name: link.name.trim(), url: link.url.trim() }))));
|
|
51
|
+
}
|
|
52
|
+
// The description and tags as the project page shows and copies them: the user's edits over
|
|
53
|
+
// the generated text, the chapters fitted to YouTube's rules (`chapters.ts`; the last one's
|
|
54
|
+
// length only checked when the video's length is given), placeholders filled from the
|
|
55
|
+
// project's and Settings' links (one with no link stays as typed). For anything that hands the
|
|
56
|
+
// description on, such as Prepare upload. `chapterNotice` says what the fitting changed.
|
|
57
|
+
export function effectiveDescription(db, projectId, generated) {
|
|
58
|
+
const edits = readDescriptionEdits(db, projectId);
|
|
59
|
+
const shown = shownFields(resolveFields(splitDescription(generated.description, generated.tags), edits.fields));
|
|
60
|
+
const fitted = fitChapters(shown.chapters, generated.durationSeconds);
|
|
61
|
+
const notice = chapterNotice(fitted.adjustments);
|
|
62
|
+
const links = mergeLinks(readChannelLinks(db), edits.links);
|
|
63
|
+
return {
|
|
64
|
+
description: fillPlaceholders(composeDescription({ ...shown, chapters: fitted.text }), links)
|
|
65
|
+
.text,
|
|
66
|
+
tags: fillPlaceholders(shown.tags, links).text,
|
|
67
|
+
...(notice === undefined ? {} : { chapterNotice: notice }),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Hand edits to the YouTube description. The project page lets the user edit the summary,
|
|
2
|
+
// the chapters, the hashtags and the tags in place; each edit is saved with the generated
|
|
3
|
+
// text it was made from (`base`), so when the description is written again the page can tell
|
|
4
|
+
// a field the user changed from one they left alone. Browser-safe: the project page applies
|
|
5
|
+
// these rules to what it shows, and the server only stores the edits.
|
|
6
|
+
import { parseTimestamp } from "./timestamps.js";
|
|
7
|
+
export const descriptionFields = ["summary", "chapters", "hashtags", "tags"];
|
|
8
|
+
// Splits a written description back into its three parts. `assembleDescription` (answer.ts) lays it out as
|
|
9
|
+
// the summary, a blank line, one chapter per line, a blank line and the hashtags; reading it
|
|
10
|
+
// from the end keeps a summary with blank lines of its own whole.
|
|
11
|
+
export function splitDescription(description, tags) {
|
|
12
|
+
const lines = description.replace(/\r\n?/gu, "\n").split("\n");
|
|
13
|
+
while (lines.length > 0 && lines.at(-1)?.trim() === "")
|
|
14
|
+
lines.pop();
|
|
15
|
+
let hashtags = "";
|
|
16
|
+
const last = lines.at(-1)?.trim() ?? "";
|
|
17
|
+
if (last !== "" && last.split(/\s+/u).every((word) => /^#[\p{L}\p{N}_]+$/u.test(word))) {
|
|
18
|
+
hashtags = last;
|
|
19
|
+
lines.pop();
|
|
20
|
+
}
|
|
21
|
+
while (lines.length > 0 && lines.at(-1)?.trim() === "")
|
|
22
|
+
lines.pop();
|
|
23
|
+
const chapters = [];
|
|
24
|
+
while (lines.length > 0 && isChapterLine(lines.at(-1) ?? "")) {
|
|
25
|
+
chapters.unshift((lines.pop() ?? "").trim());
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
summary: lines.join("\n").trim(),
|
|
29
|
+
chapters: chapters.join("\n"),
|
|
30
|
+
hashtags,
|
|
31
|
+
tags: tags.trim(),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function isChapterLine(line) {
|
|
35
|
+
const [time = "", ...title] = line.trim().split(/\s+/u);
|
|
36
|
+
return parseTimestamp(time) !== undefined && title.length > 0;
|
|
37
|
+
}
|
|
38
|
+
// The description as YouTube takes it, from the fields as shown: the same layout
|
|
39
|
+
// `assembleDescription` writes, leaving out a part the user emptied.
|
|
40
|
+
export function composeDescription(fields) {
|
|
41
|
+
return [fields.summary.trim(), fields.chapters.trim(), fields.hashtags.trim()]
|
|
42
|
+
.filter((part) => part !== "")
|
|
43
|
+
.join("\n\n");
|
|
44
|
+
}
|
|
45
|
+
// The rules for one field:
|
|
46
|
+
// - never edited: the generated text;
|
|
47
|
+
// - edited, and the generated text is still the one the edit started from: the user's text;
|
|
48
|
+
// - edited, and the generated text changed since: the user's text, with the new generated text
|
|
49
|
+
// pending for the user to take or dismiss - unless the two now agree, and then there is
|
|
50
|
+
// nothing to choose;
|
|
51
|
+
// - not written yet: the user's text if there is one, otherwise nothing.
|
|
52
|
+
export function resolveField(generated, edit) {
|
|
53
|
+
if (edit === undefined)
|
|
54
|
+
return { text: generated ?? "", edited: false };
|
|
55
|
+
if (generated === undefined || edit.base === generated)
|
|
56
|
+
return { text: edit.text, edited: true };
|
|
57
|
+
if (edit.text === generated)
|
|
58
|
+
return { text: generated, edited: false };
|
|
59
|
+
return { text: edit.text, edited: true, pending: generated };
|
|
60
|
+
}
|
|
61
|
+
export function resolveFields(generated, edits) {
|
|
62
|
+
const one = (field) => resolveField(generated?.[field], edits[field]);
|
|
63
|
+
return {
|
|
64
|
+
summary: one("summary"),
|
|
65
|
+
chapters: one("chapters"),
|
|
66
|
+
hashtags: one("hashtags"),
|
|
67
|
+
tags: one("tags"),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
export function shownFields(resolved) {
|
|
71
|
+
return {
|
|
72
|
+
summary: resolved.summary.text,
|
|
73
|
+
chapters: resolved.chapters.text,
|
|
74
|
+
hashtags: resolved.hashtags.text,
|
|
75
|
+
tags: resolved.tags.text,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
// Saving an edit: text equal to the generated text is no edit at all, so the field follows the
|
|
79
|
+
// next regeneration again.
|
|
80
|
+
export function withEdit(edits, field, text, generated) {
|
|
81
|
+
if (text === generated)
|
|
82
|
+
return withoutEdit(edits, field);
|
|
83
|
+
return { ...edits, [field]: { base: generated, text } };
|
|
84
|
+
}
|
|
85
|
+
// "Use it": the field follows the generated text again.
|
|
86
|
+
export function withoutEdit(edits, field) {
|
|
87
|
+
const { [field]: _dropped, ...rest } = edits;
|
|
88
|
+
return rest;
|
|
89
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// Channel links and the placeholders that use them. A description (the generated one, the
|
|
2
|
+
// user's edit, or what a Description prompt told the model to write) can hold `{{Patreon}}` or
|
|
3
|
+
// `{{Previous video}}`; they are filled from the saved links when the description is shown or
|
|
4
|
+
// copied, never when it is written, so changing a link changes every description at once.
|
|
5
|
+
// A placeholder with no link of that name stays in the text as typed and is marked, never
|
|
6
|
+
// silently dropped. Browser-safe.
|
|
7
|
+
// The link YouTube descriptions most often point at, and the one that changes per project:
|
|
8
|
+
// the project page offers it by name, and a project's own value wins over the Settings list.
|
|
9
|
+
export const previousVideoLink = "Previous video";
|
|
10
|
+
export const channelLinkNameMax = 60;
|
|
11
|
+
export const channelLinkUrlMax = 2000;
|
|
12
|
+
export const channelLinksMax = 50;
|
|
13
|
+
const placeholder = /\{\{([^{}\n]{1,80})\}\}/gu;
|
|
14
|
+
// Names match without regard to case or repeated spaces: `{{previous video}}` is the
|
|
15
|
+
// "Previous video" link.
|
|
16
|
+
export function linkKey(name) {
|
|
17
|
+
return name.trim().replace(/\s+/gu, " ").toLowerCase();
|
|
18
|
+
}
|
|
19
|
+
// The project's own links first, so one of the same name replaces the Settings one.
|
|
20
|
+
export function mergeLinks(settings, project) {
|
|
21
|
+
const seen = new Set();
|
|
22
|
+
const merged = [];
|
|
23
|
+
for (const link of [...project, ...settings]) {
|
|
24
|
+
const key = linkKey(link.name);
|
|
25
|
+
if (seen.has(key) || link.url.trim() === "")
|
|
26
|
+
continue;
|
|
27
|
+
seen.add(key);
|
|
28
|
+
merged.push(link);
|
|
29
|
+
}
|
|
30
|
+
return merged;
|
|
31
|
+
}
|
|
32
|
+
export function placeholderParts(text, links) {
|
|
33
|
+
const byName = new Map(links.map((link) => [linkKey(link.name), link.url]));
|
|
34
|
+
const parts = [];
|
|
35
|
+
let at = 0;
|
|
36
|
+
for (const match of text.matchAll(placeholder)) {
|
|
37
|
+
const index = match.index;
|
|
38
|
+
if (index > at)
|
|
39
|
+
parts.push({ kind: "text", text: text.slice(at, index) });
|
|
40
|
+
const name = (match[1] ?? "").trim();
|
|
41
|
+
parts.push({ kind: "placeholder", raw: match[0], name, url: byName.get(linkKey(name)) });
|
|
42
|
+
at = index + match[0].length;
|
|
43
|
+
}
|
|
44
|
+
if (at < text.length)
|
|
45
|
+
parts.push({ kind: "text", text: text.slice(at) });
|
|
46
|
+
return parts;
|
|
47
|
+
}
|
|
48
|
+
export function fillPlaceholders(text, links) {
|
|
49
|
+
const unknown = [];
|
|
50
|
+
const filled = placeholderParts(text, links)
|
|
51
|
+
.map((part) => {
|
|
52
|
+
if (part.kind === "text")
|
|
53
|
+
return part.text;
|
|
54
|
+
if (part.url !== undefined)
|
|
55
|
+
return part.url;
|
|
56
|
+
if (!unknown.some((name) => linkKey(name) === linkKey(part.name)))
|
|
57
|
+
unknown.push(part.name);
|
|
58
|
+
return part.raw;
|
|
59
|
+
})
|
|
60
|
+
.join("");
|
|
61
|
+
return { text: filled, unknown };
|
|
62
|
+
}
|
|
63
|
+
// Why a list of links cannot be saved, in the words Settings shows; undefined when it can.
|
|
64
|
+
export function channelLinksProblem(links) {
|
|
65
|
+
if (links.length > channelLinksMax)
|
|
66
|
+
return `Keep at most ${String(channelLinksMax)} channel links. Remove some in Settings → Channel links.`;
|
|
67
|
+
const seen = new Set();
|
|
68
|
+
for (const [index, link] of links.entries()) {
|
|
69
|
+
const row = `Link ${String(index + 1)}`;
|
|
70
|
+
const name = link.name.trim();
|
|
71
|
+
if (name === "")
|
|
72
|
+
return `${row} has no name. Give it one, such as Patreon.`;
|
|
73
|
+
if (name.length > channelLinkNameMax)
|
|
74
|
+
return `${row}'s name is over ${String(channelLinkNameMax)} characters. Shorten it.`;
|
|
75
|
+
if (/[{}]/u.test(name))
|
|
76
|
+
return `${row}'s name contains { or }. Use the name alone; the description writes it as {{${name.replace(/[{}]/gu, "")}}}.`;
|
|
77
|
+
if (seen.has(linkKey(name)))
|
|
78
|
+
return `Two links are named "${name}". Rename one, since {{${name}}} can only fill with one.`;
|
|
79
|
+
seen.add(linkKey(name));
|
|
80
|
+
const url = link.url.trim();
|
|
81
|
+
if (url.length > channelLinkUrlMax)
|
|
82
|
+
return `The "${name}" link is over ${String(channelLinkUrlMax)} characters. Use a shorter address.`;
|
|
83
|
+
if (url === "")
|
|
84
|
+
return `The "${name}" link has no address. Paste it, starting with https://.`;
|
|
85
|
+
if (!isWebAddress(url))
|
|
86
|
+
return `The "${name}" link is not a web address. Paste the full address, starting with https://.`;
|
|
87
|
+
}
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
function isWebAddress(url) {
|
|
91
|
+
try {
|
|
92
|
+
const parsed = new URL(url);
|
|
93
|
+
return parsed.protocol === "https:" || parsed.protocol === "http:";
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -51,7 +51,9 @@ const sentenceLongestSeconds = 30;
|
|
|
51
51
|
// The narration as numbered sentences, for the Shorts step: the model picks clips by
|
|
52
52
|
// sentence number and Slopify cuts at the sentences' own word times, so a clip never starts
|
|
53
53
|
// or ends mid-word. A sentence closes at its full stop, question or exclamation mark, at a
|
|
54
|
-
// pause of 1.5 s (the gap between intro, body and outro), or after 30 s.
|
|
54
|
+
// pause of 1.5 s (the gap between intro, body and outro), or after 30 s. On a multi-voice run
|
|
55
|
+
// it also closes where one speaker's turn ends, so a clip always starts and ends on a turn's
|
|
56
|
+
// sentence and never cuts a speaker off mid-line.
|
|
55
57
|
export function transcriptSentences(words) {
|
|
56
58
|
const sentences = [];
|
|
57
59
|
let first = -1;
|
|
@@ -73,6 +75,7 @@ export function transcriptSentences(words) {
|
|
|
73
75
|
text,
|
|
74
76
|
firstWord: first,
|
|
75
77
|
lastWord: last,
|
|
78
|
+
...(opening.speaker === undefined ? {} : { speaker: opening.speaker }),
|
|
76
79
|
});
|
|
77
80
|
first = -1;
|
|
78
81
|
};
|
|
@@ -82,6 +85,7 @@ export function transcriptSentences(words) {
|
|
|
82
85
|
if (opening !== undefined &&
|
|
83
86
|
previous !== undefined &&
|
|
84
87
|
(word.start - previous.end >= pauseSeconds ||
|
|
88
|
+
word.turn !== previous.turn ||
|
|
85
89
|
word.end - opening.start > sentenceLongestSeconds))
|
|
86
90
|
flush(index - 1);
|
|
87
91
|
if (first < 0)
|
|
@@ -93,9 +97,12 @@ export function transcriptSentences(words) {
|
|
|
93
97
|
return sentences;
|
|
94
98
|
}
|
|
95
99
|
// One sentence per line, led by its number and its span in the video, which is what the
|
|
96
|
-
// model picks from.
|
|
97
|
-
export function sentencesText(sentences) {
|
|
100
|
+
// model picks from; on a multi-voice run, with the speaker's name before the words.
|
|
101
|
+
export function sentencesText(sentences, names) {
|
|
98
102
|
return sentences
|
|
99
|
-
.map((sentence) =>
|
|
103
|
+
.map((sentence) => {
|
|
104
|
+
const name = sentence.speaker === undefined ? undefined : names?.[sentence.speaker];
|
|
105
|
+
return `[${String(sentence.number)}] (${youtubeTimestamp(sentence.start)}-${youtubeTimestamp(sentence.end)}) ${name === undefined ? "" : `${name}: `}${sentence.text}`;
|
|
106
|
+
})
|
|
100
107
|
.join("\n");
|
|
101
108
|
}
|
package/dist/updater/service.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { timingSafeEqual } from "node:crypto";
|
|
2
2
|
import { setImmediate } from "node:timers";
|
|
3
3
|
import { isStableVersion, isUpdateToken, newerVersion } from "./model.js";
|
|
4
|
+
const waitCheckMs = 5_000;
|
|
4
5
|
export function createUpdater(deps) {
|
|
5
6
|
let latestVersion = null;
|
|
6
7
|
let checkedAt = -Infinity;
|
|
@@ -13,17 +14,30 @@ export function createUpdater(deps) {
|
|
|
13
14
|
let checking;
|
|
14
15
|
let settling;
|
|
15
16
|
let mutations = 0;
|
|
17
|
+
// The version a `waiting` update will install, and the stop of the timer watching for idle.
|
|
18
|
+
let pending;
|
|
19
|
+
let stopWatching;
|
|
20
|
+
const every = deps.every ??
|
|
21
|
+
((callback, ms) => {
|
|
22
|
+
const timer = setInterval(callback, ms);
|
|
23
|
+
timer.unref();
|
|
24
|
+
return () => clearInterval(timer);
|
|
25
|
+
});
|
|
16
26
|
const busyNow = () => deps.busy() || mutations > 0;
|
|
17
27
|
const locked = () => status === "installing" || status === "restarting";
|
|
28
|
+
// A function, not a comparison, because an await in between may have changed it.
|
|
29
|
+
const waiting = () => status === "waiting";
|
|
18
30
|
function info() {
|
|
19
31
|
const busy = busyNow();
|
|
20
32
|
const available = latestVersion !== null && newerVersion(latestVersion, deps.currentVersion);
|
|
33
|
+
// Running work no longer blocks an update: it waits for the work (`waiting`).
|
|
21
34
|
const blockedReason = deps.unsupported() ??
|
|
22
|
-
(
|
|
23
|
-
? "
|
|
24
|
-
:
|
|
25
|
-
? "An update is already
|
|
35
|
+
(locked()
|
|
36
|
+
? "An update is already in progress."
|
|
37
|
+
: status === "waiting"
|
|
38
|
+
? "An update is already waiting for running work to finish."
|
|
26
39
|
: undefined);
|
|
40
|
+
const waitingFor = busy ? deps.busyWith?.() : undefined;
|
|
27
41
|
return {
|
|
28
42
|
currentVersion: deps.currentVersion,
|
|
29
43
|
latestVersion,
|
|
@@ -33,10 +47,41 @@ export function createUpdater(deps) {
|
|
|
33
47
|
status,
|
|
34
48
|
...(error === undefined ? {} : { error }),
|
|
35
49
|
...(blockedReason === undefined ? {} : { blockedReason }),
|
|
50
|
+
...(status === "waiting" && pending !== undefined ? { pendingVersion: pending } : {}),
|
|
51
|
+
...(waitingFor === undefined ? {} : { waitingFor }),
|
|
36
52
|
};
|
|
37
53
|
}
|
|
54
|
+
function install(version) {
|
|
55
|
+
status = "installing";
|
|
56
|
+
error = undefined;
|
|
57
|
+
void Promise.resolve()
|
|
58
|
+
.then(() => deps.install(version, () => {
|
|
59
|
+
status = "restarting";
|
|
60
|
+
}))
|
|
61
|
+
.catch(() => {
|
|
62
|
+
status = "error";
|
|
63
|
+
error =
|
|
64
|
+
"The update could not be installed, so Slopify kept your current version. Check your internet connection and try again; the reason is in logs/updates.log inside your Slopify data folder.";
|
|
65
|
+
installError = error;
|
|
66
|
+
deps.report(error);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
// Looks again every few seconds and installs the moment no work is running.
|
|
70
|
+
function wait(version) {
|
|
71
|
+
pending = version;
|
|
72
|
+
status = "waiting";
|
|
73
|
+
error = undefined;
|
|
74
|
+
stopWatching = every(() => {
|
|
75
|
+
if (status !== "waiting" || busyNow())
|
|
76
|
+
return;
|
|
77
|
+
stopWatching?.();
|
|
78
|
+
stopWatching = undefined;
|
|
79
|
+
pending = undefined;
|
|
80
|
+
install(version);
|
|
81
|
+
}, waitCheckMs);
|
|
82
|
+
}
|
|
38
83
|
async function check(refresh = false) {
|
|
39
|
-
if (locked())
|
|
84
|
+
if (locked() || status === "waiting")
|
|
40
85
|
return info();
|
|
41
86
|
if (checking !== undefined) {
|
|
42
87
|
await checking;
|
|
@@ -122,7 +167,7 @@ export function createUpdater(deps) {
|
|
|
122
167
|
};
|
|
123
168
|
},
|
|
124
169
|
start: async () => {
|
|
125
|
-
if (locked() ||
|
|
170
|
+
if (locked() || waiting())
|
|
126
171
|
return { ok: false, code: 409, info: info() };
|
|
127
172
|
installError = undefined;
|
|
128
173
|
await check(true);
|
|
@@ -130,25 +175,25 @@ export function createUpdater(deps) {
|
|
|
130
175
|
if (!current.canUpdate || latestVersion === null || current.status === "error") {
|
|
131
176
|
return {
|
|
132
177
|
ok: false,
|
|
133
|
-
code:
|
|
178
|
+
code: locked() || waiting() ? 409 : current.status === "error" ? 503 : 400,
|
|
134
179
|
info: current,
|
|
135
180
|
};
|
|
136
181
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
status = "restarting";
|
|
143
|
-
}))
|
|
144
|
-
.catch(() => {
|
|
145
|
-
status = "error";
|
|
146
|
-
error =
|
|
147
|
-
"The update could not be installed, so Slopify kept your current version. Check your internet connection and try again; the reason is in logs/updates.log inside your Slopify data folder.";
|
|
148
|
-
installError = error;
|
|
149
|
-
deps.report(error);
|
|
150
|
-
});
|
|
182
|
+
// Rechecked after the registry await: a project may have started meanwhile.
|
|
183
|
+
if (busyNow())
|
|
184
|
+
wait(latestVersion);
|
|
185
|
+
else
|
|
186
|
+
install(latestVersion);
|
|
151
187
|
return { ok: true, info: info() };
|
|
152
188
|
},
|
|
189
|
+
cancelWaiting: () => {
|
|
190
|
+
if (status !== "waiting")
|
|
191
|
+
return false;
|
|
192
|
+
stopWatching?.();
|
|
193
|
+
stopWatching = undefined;
|
|
194
|
+
pending = undefined;
|
|
195
|
+
status = "idle";
|
|
196
|
+
return true;
|
|
197
|
+
},
|
|
153
198
|
};
|
|
154
199
|
}
|