@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,93 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdirSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { stylePreviewSeconds } from "./schema.js";
|
|
5
|
+
import { normalizeStylePreview, stylePreviewHash } from "./settings.js";
|
|
6
|
+
// Rendered previews are kept in the data folder by the hash of their settings, so a setting
|
|
7
|
+
// changed back, or the same style in another project, plays at once. `force` renders again
|
|
8
|
+
// anyway. Requests for a preview already being rendered wait for that render instead of
|
|
9
|
+
// starting a second one.
|
|
10
|
+
export function stylePreviewDir(dataDir) {
|
|
11
|
+
return join(dataDir, "cache", "style-preview");
|
|
12
|
+
}
|
|
13
|
+
const hashPattern = /^[a-f0-9]{64}$/;
|
|
14
|
+
export function createStylePreviews(deps) {
|
|
15
|
+
const inflight = new Map();
|
|
16
|
+
const pathOf = (hash) => join(deps.dir, `${hash}.mp4`);
|
|
17
|
+
const file = (hash) => {
|
|
18
|
+
if (!hashPattern.test(hash))
|
|
19
|
+
return undefined;
|
|
20
|
+
try {
|
|
21
|
+
const stats = statSync(pathOf(hash));
|
|
22
|
+
return stats.isFile() && stats.size > 0
|
|
23
|
+
? { path: pathOf(hash), bytes: stats.size, version: Math.round(stats.mtimeMs) }
|
|
24
|
+
: undefined;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
const run = async (hash, settings, picture) => {
|
|
31
|
+
mkdirSync(deps.dir, { recursive: true, mode: 0o700 });
|
|
32
|
+
const work = `${hash}.${randomUUID()}`;
|
|
33
|
+
const part = join(deps.dir, `${work}.part.mp4`);
|
|
34
|
+
// The picture is copied beside the render for its length only, so nothing but the
|
|
35
|
+
// finished previews is kept.
|
|
36
|
+
const still = picture === undefined ? undefined : join(deps.dir, `${work}.still${picture.extension}`);
|
|
37
|
+
try {
|
|
38
|
+
if (still !== undefined && picture !== undefined)
|
|
39
|
+
writeFileSync(still, picture.bytes, { mode: 0o600 });
|
|
40
|
+
await deps.render(settings, part, AbortSignal.timeout(deps.timeoutMs ?? 120_000), still);
|
|
41
|
+
renameSync(part, pathOf(hash));
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
rmSync(part, { force: true });
|
|
45
|
+
if (still !== undefined)
|
|
46
|
+
rmSync(still, { force: true });
|
|
47
|
+
}
|
|
48
|
+
prune(deps);
|
|
49
|
+
const saved = file(hash);
|
|
50
|
+
if (saved === undefined)
|
|
51
|
+
throw new Error("The style preview render finished without writing its video.");
|
|
52
|
+
return { hash, cached: false, seconds: stylePreviewSeconds, version: saved.version };
|
|
53
|
+
};
|
|
54
|
+
return {
|
|
55
|
+
file,
|
|
56
|
+
render: (request) => {
|
|
57
|
+
const picture = request.image === undefined ? undefined : deps.pictures?.(request.image);
|
|
58
|
+
const settings = normalizeStylePreview(request, picture?.sha256);
|
|
59
|
+
const hash = stylePreviewHash(settings);
|
|
60
|
+
const running = inflight.get(hash);
|
|
61
|
+
if (running !== undefined)
|
|
62
|
+
return running;
|
|
63
|
+
const saved = request.force === true ? undefined : file(hash);
|
|
64
|
+
if (saved !== undefined)
|
|
65
|
+
return Promise.resolve({
|
|
66
|
+
hash,
|
|
67
|
+
cached: true,
|
|
68
|
+
seconds: stylePreviewSeconds,
|
|
69
|
+
version: saved.version,
|
|
70
|
+
});
|
|
71
|
+
const started = run(hash, settings, picture).finally(() => {
|
|
72
|
+
inflight.delete(hash);
|
|
73
|
+
});
|
|
74
|
+
inflight.set(hash, started);
|
|
75
|
+
return started;
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function prune(deps) {
|
|
80
|
+
try {
|
|
81
|
+
const kept = readdirSync(deps.dir)
|
|
82
|
+
.filter((name) => /^[a-f0-9]{64}\.mp4$/.test(name))
|
|
83
|
+
.map((name) => ({ name, at: statSync(join(deps.dir, name)).mtimeMs }))
|
|
84
|
+
.toSorted((left, right) => right.at - left.at);
|
|
85
|
+
for (const old of kept.slice(deps.keep ?? 200))
|
|
86
|
+
rmSync(join(deps.dir, old.name), { force: true });
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
deps.log.write("warn", "style-preview.prune", {
|
|
90
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { captionCues } from "../subtitles/captions.js";
|
|
3
|
+
import { transitionSecondsMax, transitionSecondsMin, videoEditOf } from "../video/edit-settings.js";
|
|
4
|
+
import { defaultPreviewText, stylePreviewSeconds } from "./schema.js";
|
|
5
|
+
export function normalizeStylePreview(request,
|
|
6
|
+
// The sha256 of the picture the server found for request.image, if it found one.
|
|
7
|
+
image) {
|
|
8
|
+
const edit = videoEditOf(request);
|
|
9
|
+
const { subtitles } = request;
|
|
10
|
+
const text = (request.previewText ?? "").replace(/\s+/g, " ").trim();
|
|
11
|
+
return {
|
|
12
|
+
version: 1,
|
|
13
|
+
format: request.format,
|
|
14
|
+
captions: subtitles.mode === "burn-in"
|
|
15
|
+
? {
|
|
16
|
+
fontId: subtitles.fontId,
|
|
17
|
+
fontSize: Math.round(subtitles.fontSize),
|
|
18
|
+
position: subtitles.position,
|
|
19
|
+
text: text === "" ? defaultPreviewText : text,
|
|
20
|
+
}
|
|
21
|
+
: null,
|
|
22
|
+
look: {
|
|
23
|
+
vignette: edit.vignette,
|
|
24
|
+
grain: edit.grain,
|
|
25
|
+
grade: edit.grade,
|
|
26
|
+
atmosphere: edit.atmosphere,
|
|
27
|
+
},
|
|
28
|
+
transition: edit.transition === "cut"
|
|
29
|
+
? null
|
|
30
|
+
: {
|
|
31
|
+
kind: edit.transition,
|
|
32
|
+
// Tenths, within the range the project editor offers.
|
|
33
|
+
seconds: Math.round(Math.min(transitionSecondsMax, Math.max(transitionSecondsMin, Number.isFinite(edit.transitionSeconds) ? edit.transitionSeconds : 0.6)) * 10) / 10,
|
|
34
|
+
},
|
|
35
|
+
chapterCard: edit.chapterCards ? { fontId: subtitles.fontId } : null,
|
|
36
|
+
...(image === undefined ? {} : { image }),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
// JSON with every object's keys in order, so equal settings always hash alike.
|
|
40
|
+
export function stableJson(value) {
|
|
41
|
+
if (Array.isArray(value))
|
|
42
|
+
return `[${value.map(stableJson).join(",")}]`;
|
|
43
|
+
if (value !== null && typeof value === "object")
|
|
44
|
+
return `{${Object.entries(value)
|
|
45
|
+
.filter(([, one]) => one !== undefined)
|
|
46
|
+
.toSorted(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
|
|
47
|
+
.map(([key, one]) => `${JSON.stringify(key)}:${stableJson(one)}`)
|
|
48
|
+
.join(",")}}`;
|
|
49
|
+
return JSON.stringify(value);
|
|
50
|
+
}
|
|
51
|
+
export function stylePreviewHash(settings) {
|
|
52
|
+
return createHash("sha256").update(stableJson(settings)).digest("hex");
|
|
53
|
+
}
|
|
54
|
+
// ceiling: three cues read comfortably in six seconds; a short text gets one per word.
|
|
55
|
+
const cueCount = 3;
|
|
56
|
+
// Longer than the 0.7 s pause `captionCues` starts a new cue at.
|
|
57
|
+
const cueGap = 0.8;
|
|
58
|
+
const lead = 0.4;
|
|
59
|
+
// The sample text as timed words: split into up to three groups spread over the preview with
|
|
60
|
+
// a pause between them, the words of each evenly spaced. `captionCues` then groups and wraps
|
|
61
|
+
// them exactly as it does real narration.
|
|
62
|
+
export function sampleWords(text, seconds = stylePreviewSeconds) {
|
|
63
|
+
const words = text.split(/\s+/).filter((word) => word !== "");
|
|
64
|
+
if (words.length === 0)
|
|
65
|
+
return [];
|
|
66
|
+
const groups = Math.min(cueCount, words.length);
|
|
67
|
+
const per = Math.ceil(words.length / groups);
|
|
68
|
+
const chunks = Array.from({ length: groups }, (_value, at) => words.slice(at * per, (at + 1) * per)).filter((chunk) => chunk.length > 0);
|
|
69
|
+
const span = (seconds - 2 * lead - cueGap * (chunks.length - 1)) / chunks.length;
|
|
70
|
+
const timed = [];
|
|
71
|
+
chunks.forEach((chunk, at) => {
|
|
72
|
+
const start = lead + at * (span + cueGap);
|
|
73
|
+
const each = span / chunk.length;
|
|
74
|
+
chunk.forEach((word, index) => {
|
|
75
|
+
timed.push({
|
|
76
|
+
text: word,
|
|
77
|
+
start: round(start + index * each),
|
|
78
|
+
end: round(start + (index + 1) * each),
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
return timed;
|
|
83
|
+
}
|
|
84
|
+
export function sampleCues(text, seconds = stylePreviewSeconds) {
|
|
85
|
+
return captionCues(sampleWords(text, seconds));
|
|
86
|
+
}
|
|
87
|
+
function round(value) {
|
|
88
|
+
return Math.round(value * 1000) / 1000;
|
|
89
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { assInlineColour } from "../voices/palette.js";
|
|
1
2
|
import { subtitlePlacement } from "./layout.js";
|
|
2
|
-
// Keep spoken spelling and punctuation. Word times come only from acoustic alignment.
|
|
3
|
+
// Keep spoken spelling and punctuation. Word times come only from acoustic alignment. A cue
|
|
4
|
+
// never holds two speakers' words.
|
|
3
5
|
export function captionCues(words) {
|
|
4
6
|
const cues = [];
|
|
5
7
|
let pending = [];
|
|
@@ -12,6 +14,7 @@ export function captionCues(words) {
|
|
|
12
14
|
start: first.start,
|
|
13
15
|
end: last.end,
|
|
14
16
|
text: wrap(pending.map((word) => word.text)),
|
|
17
|
+
...(first.speaker === undefined ? {} : { speaker: first.speaker }),
|
|
15
18
|
});
|
|
16
19
|
}
|
|
17
20
|
pending = [];
|
|
@@ -21,11 +24,12 @@ export function captionCues(words) {
|
|
|
21
24
|
!Number.isFinite(word.end) ||
|
|
22
25
|
word.start < previousEnd - 0.001 ||
|
|
23
26
|
word.end <= word.start) {
|
|
24
|
-
throw new Error("The saved caption timing is damaged. Use
|
|
27
|
+
throw new Error("The saved caption timing is damaged. Use More → Render the video again in the Video section to time them again, then Try again.");
|
|
25
28
|
}
|
|
26
29
|
const first = pending[0];
|
|
27
30
|
if (first !== undefined &&
|
|
28
31
|
(word.start - previousEnd > 0.7 ||
|
|
32
|
+
word.speaker !== first.speaker ||
|
|
29
33
|
word.end - first.start > 5 ||
|
|
30
34
|
wrap([...pending.map((one) => one.text), word.text]).split("\n").length > 2)) {
|
|
31
35
|
flush();
|
|
@@ -52,23 +56,44 @@ function wrap(words) {
|
|
|
52
56
|
lines.push(line);
|
|
53
57
|
return lines.join("\n");
|
|
54
58
|
}
|
|
55
|
-
export function serializeSrt(cues) {
|
|
59
|
+
export function serializeSrt(cues, speakers) {
|
|
56
60
|
return cues
|
|
57
|
-
.map((cue, index) => `${index + 1}\n${stamp(cue.start, ",")} --> ${stamp(cue.end, ",")}\n${markup(cue.text)}\n`)
|
|
61
|
+
.map((cue, index) => `${index + 1}\n${stamp(cue.start, ",")} --> ${stamp(cue.end, ",")}\n${markup(`${nameTag(cue, speakers)}${cue.text}`)}\n`)
|
|
58
62
|
.join("\n");
|
|
59
63
|
}
|
|
60
|
-
|
|
61
|
-
|
|
64
|
+
// WebVTT names the speaker in its own voice span, which players can show or style.
|
|
65
|
+
export function serializeVtt(cues, speakers) {
|
|
66
|
+
return `WEBVTT\n\n${cues.map((cue) => `${stamp(cue.start, ".")} --> ${stamp(cue.end, ".")}\n${voiceSpan(cue, speakers)}${markup(cue.text)}\n`).join("\n")}`;
|
|
67
|
+
}
|
|
68
|
+
function styleOf(cue, speakers) {
|
|
69
|
+
return cue.speaker === undefined ? undefined : speakers?.styles[cue.speaker];
|
|
70
|
+
}
|
|
71
|
+
function nameTag(cue, speakers) {
|
|
72
|
+
const style = styleOf(cue, speakers);
|
|
73
|
+
return style !== undefined && speakers?.nameTags === true ? `${style.name}: ` : "";
|
|
74
|
+
}
|
|
75
|
+
function voiceSpan(cue, speakers) {
|
|
76
|
+
const style = styleOf(cue, speakers);
|
|
77
|
+
return style === undefined ? "" : `<v ${style.name.replace(/[<>&]/g, "")}>`;
|
|
78
|
+
}
|
|
79
|
+
// The speaker's colour, and their name in bold ahead of the words when name tags are on.
|
|
80
|
+
function speakerAss(cue, speakers) {
|
|
81
|
+
const style = styleOf(cue, speakers);
|
|
82
|
+
if (style === undefined)
|
|
83
|
+
return "";
|
|
84
|
+
const colour = `{\\1c${assInlineColour(style.colour)}}`;
|
|
85
|
+
return speakers?.nameTags === true ? `${colour}{\\b1}${assText(style.name)}:{\\b0} ` : colour;
|
|
62
86
|
}
|
|
63
87
|
export function serializeAss(cues, style) {
|
|
64
88
|
const name = style.fontName.replace(/[\p{Cc},]/gu, " ").trim();
|
|
65
89
|
const placement = subtitlePlacement(style.position ?? "bottom", style.height);
|
|
66
90
|
const position = `{\\an${placement.alignment}\\pos(${style.width / 2},${placement.y})}`;
|
|
67
|
-
const header = `[Script Info]\nScriptType: v4.00+\nPlayResX: ${style.width}\nPlayResY: ${style.height}\nWrapStyle: 0\nScaledBorderAndShadow: yes\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Default,${name},${style.fontSize}
|
|
91
|
+
const header = `[Script Info]\nScriptType: v4.00+\nPlayResX: ${style.width}\nPlayResY: ${style.height}\nWrapStyle: 0\nScaledBorderAndShadow: yes\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Default,${name},${style.fontSize},${assColour(style.color, "&H00FFFFFF")},${assColour(style.color, "&H00FFFFFF")},${assColour(style.outlineColor, "&H00101010")},&H80000000,0,0,0,0,100,100,0,0,1,2.5,1,2,60,60,60,1\n\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n`;
|
|
68
92
|
return (header +
|
|
69
93
|
cues
|
|
70
|
-
.map((cue) => `Dialogue: 0,${assStamp(cue.start)},${assStamp(cue.end)},Default,,0,0,0,,${position}${assText(cue.text)}\n`)
|
|
71
|
-
.join("")
|
|
94
|
+
.map((cue) => `Dialogue: 0,${assStamp(cue.start)},${assStamp(cue.end)},Default,,0,0,0,,${position}${speakerAss(cue, style.speakers)}${assText(cue.text)}\n`)
|
|
95
|
+
.join("") +
|
|
96
|
+
(style.overlay ?? []).map((line) => `${line}\n`).join(""));
|
|
72
97
|
}
|
|
73
98
|
function markup(text) {
|
|
74
99
|
return text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
@@ -89,3 +114,11 @@ function assStamp(seconds) {
|
|
|
89
114
|
const cs = Math.max(0, Math.round(seconds * 100));
|
|
90
115
|
return `${Math.floor(cs / 360_000)}:${String(Math.floor(cs / 6000) % 60).padStart(2, "0")}:${String(Math.floor(cs / 100) % 60).padStart(2, "0")}.${String(cs % 100).padStart(2, "0")}`;
|
|
91
116
|
}
|
|
117
|
+
// ASS colours are &HAABBGGRR, alpha 00 being opaque.
|
|
118
|
+
export function assColour(hex, fallback) {
|
|
119
|
+
const match = hex === undefined ? null : /^#([0-9A-Fa-f]{2})([0-9A-Fa-f]{2})([0-9A-Fa-f]{2})$/.exec(hex);
|
|
120
|
+
if (match === null)
|
|
121
|
+
return fallback;
|
|
122
|
+
const [, r, g, b] = match;
|
|
123
|
+
return `&H00${(b ?? "").toUpperCase()}${(g ?? "").toUpperCase()}${(r ?? "").toUpperCase()}`;
|
|
124
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { languageInfo } from "../../kernel/ports/languages.js";
|
|
2
3
|
export const subtitleModes = ["off", "files", "burn-in"];
|
|
3
4
|
export const subtitlePositions = [
|
|
4
5
|
"top",
|
|
@@ -17,6 +18,16 @@ export const subtitleConfigSchema = z.object({
|
|
|
17
18
|
.default("default"),
|
|
18
19
|
position: z.enum(subtitlePositions).default("bottom"),
|
|
19
20
|
fontSize: z.number().int().min(16).max(120).default(48),
|
|
21
|
+
// #RRGGBB, from the channel's brand kit. Absent is white text with a dark outline, as
|
|
22
|
+
// every caption before them.
|
|
23
|
+
color: z
|
|
24
|
+
.string()
|
|
25
|
+
.regex(/^#[0-9A-Fa-f]{6}$/)
|
|
26
|
+
.optional(),
|
|
27
|
+
outlineColor: z
|
|
28
|
+
.string()
|
|
29
|
+
.regex(/^#[0-9A-Fa-f]{6}$/)
|
|
30
|
+
.optional(),
|
|
20
31
|
});
|
|
21
32
|
export const defaultSubtitles = {
|
|
22
33
|
mode: "off",
|
|
@@ -25,3 +36,14 @@ export const defaultSubtitles = {
|
|
|
25
36
|
fontSize: 48,
|
|
26
37
|
position: "bottom",
|
|
27
38
|
};
|
|
39
|
+
// The word-timing operation of a project language, which is part of the timing's fingerprint:
|
|
40
|
+
// English keeps the operation it always had, so no English project is re-timed; the other
|
|
41
|
+
// languages each name the model (or the sentence fallback) that times them.
|
|
42
|
+
export const timingOperations = {
|
|
43
|
+
english: "wav2vec2-en-a19f851-v2-omissions",
|
|
44
|
+
multilingual: "wav2vec2-xlsr56-2d48b01-v1",
|
|
45
|
+
sentences: "sentence-timing-v1",
|
|
46
|
+
};
|
|
47
|
+
export function timingOperation(language) {
|
|
48
|
+
return timingOperations[languageInfo(language).timing];
|
|
49
|
+
}
|
|
@@ -3,13 +3,15 @@ import { copyFileSync, createReadStream, existsSync, mkdirSync, mkdtempSync, rea
|
|
|
3
3
|
import { join, relative, sep } from "node:path";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { subtitleModelDir } from "../../kernel/paths.js";
|
|
6
|
+
import { projectLanguage } from "../../kernel/ports/languages.js";
|
|
6
7
|
import { projectById, setStageProgress } from "../admission/repo.js";
|
|
8
|
+
import { captionFont, captionFontDeps } from "../fonts/coverage.js";
|
|
7
9
|
import { resolveFont } from "../fonts/index.js";
|
|
8
10
|
import { outputPath, projectDir } from "../storage/layout.js";
|
|
9
11
|
import { outputsOf } from "../storage/repo.js";
|
|
10
12
|
import { spoken } from "../video/plan.js";
|
|
11
13
|
import { captionCues, serializeAss, serializeSrt, serializeVtt } from "./captions.js";
|
|
12
|
-
import { defaultSubtitles } from "./model.js";
|
|
14
|
+
import { defaultSubtitles, timingOperation } from "./model.js";
|
|
13
15
|
import { spokenText } from "./transcript.js";
|
|
14
16
|
export const subtitleRoles = [
|
|
15
17
|
"subtitles_srt",
|
|
@@ -52,24 +54,27 @@ export async function prepareSubtitles(deps, context, audio, frame) {
|
|
|
52
54
|
setStageProgress(deps.db, context.stage.id, 0, 100);
|
|
53
55
|
context.emit({ type: "stage.progress", projectId, stage: "video", current: 0, total: 100 });
|
|
54
56
|
if (audio.length === 0)
|
|
55
|
-
throw new Error("Captions need narration audio, but this project has none. Turn captions off in Edit project → Subtitles, or turn narration on, then
|
|
57
|
+
throw new Error("Captions need narration audio, but this project has none. Turn captions off in Edit project → Subtitles, or turn narration on, then Try again.");
|
|
56
58
|
const outputs = outputsOf(deps.db, projectId);
|
|
57
59
|
const segments = audio.map((segment) => ({
|
|
58
60
|
...segment,
|
|
59
61
|
text: spoken(segment.kind) ? spokenText(deps, projectId, segment.kind, outputs) : "",
|
|
60
62
|
}));
|
|
61
|
-
const
|
|
63
|
+
const language = projectLanguage(project.config);
|
|
64
|
+
const key = await timingKey(segments, language, context.signal);
|
|
62
65
|
const cache = readCache(deps, projectId, outputs);
|
|
63
66
|
const dir = projectDir(deps.paths, projectId);
|
|
64
67
|
const directory = mkdtempSync(join(dir, "captions-"));
|
|
65
68
|
try {
|
|
66
|
-
const font = await snapshotFont(deps, projectId, outputs, config.fontId, cache, directory);
|
|
69
|
+
const font = await snapshotFont(deps, projectId, outputs, config.fontId, cache, directory, language);
|
|
67
70
|
const omissions = cache?.key === key ? [...cache.omissions] : [];
|
|
68
|
-
const words = cache?.key === key
|
|
71
|
+
const words = cache?.key === key
|
|
72
|
+
? cache.words
|
|
73
|
+
: await alignSegments(deps, context, segments, omissions, language);
|
|
69
74
|
context.signal.throwIfAborted();
|
|
70
75
|
const cues = captionCues(words);
|
|
71
76
|
if (cues.length === 0)
|
|
72
|
-
throw new Error("None of the narration could be matched to the article text, so captions can't be timed.
|
|
77
|
+
throw new Error("None of the narration could be matched to the article text, so captions can't be timed. The narration must be in the project language set in Edit project → Language; if you uploaded your own audio, make sure it reads the article text, then Try again.");
|
|
73
78
|
writeFileSync(join(directory, "subtitles.srt"), serializeSrt(cues), { mode: 0o600 });
|
|
74
79
|
writeFileSync(join(directory, "subtitles.vtt"), serializeVtt(cues), { mode: 0o600 });
|
|
75
80
|
writeFileSync(join(directory, "subtitles.ass"), serializeAss(cues, {
|
|
@@ -77,6 +82,8 @@ export async function prepareSubtitles(deps, context, audio, frame) {
|
|
|
77
82
|
fontSize: config.fontSize,
|
|
78
83
|
fontName: font.assName,
|
|
79
84
|
position: config.position,
|
|
85
|
+
color: config.color,
|
|
86
|
+
outlineColor: config.outlineColor,
|
|
80
87
|
}), { mode: 0o600 });
|
|
81
88
|
writeFileSync(join(directory, "subtitles.json"), JSON.stringify({ key, words, font, omissions }), {
|
|
82
89
|
mode: 0o600,
|
|
@@ -103,9 +110,10 @@ export async function prepareSubtitles(deps, context, audio, frame) {
|
|
|
103
110
|
throw error;
|
|
104
111
|
}
|
|
105
112
|
}
|
|
106
|
-
async function timingKey(segments, signal) {
|
|
113
|
+
async function timingKey(segments, language, signal) {
|
|
107
114
|
// Bump when alignment normalization/model changes. Hash file contents, not timestamps.
|
|
108
|
-
|
|
115
|
+
// English hashes the operation it always did (`timingOperation`).
|
|
116
|
+
const hash = createHash("sha256").update(timingOperation(language));
|
|
109
117
|
for (const segment of segments) {
|
|
110
118
|
signal.throwIfAborted();
|
|
111
119
|
hash.update(JSON.stringify({ kind: segment.kind, seconds: segment.seconds, text: segment.text }));
|
|
@@ -117,9 +125,9 @@ async function timingKey(segments, signal) {
|
|
|
117
125
|
}
|
|
118
126
|
return hash.digest("hex");
|
|
119
127
|
}
|
|
120
|
-
async function alignSegments(deps, context, segments, omissions) {
|
|
128
|
+
async function alignSegments(deps, context, segments, omissions, language) {
|
|
121
129
|
if (deps.alignSubtitles === undefined)
|
|
122
|
-
throw new Error("Slopify hit an internal error (the caption timing tool is missing from this build).
|
|
130
|
+
throw new Error("Slopify hit an internal error (the caption timing tool is missing from this build). Try again; if it happens again, use Download diagnostics in Settings and report it.");
|
|
123
131
|
const words = [];
|
|
124
132
|
const total = segments.reduce((sum, segment) => sum + segment.seconds, 0);
|
|
125
133
|
let offset = 0;
|
|
@@ -130,7 +138,8 @@ async function alignSegments(deps, context, segments, omissions) {
|
|
|
130
138
|
audioPath: segment.path,
|
|
131
139
|
onOmission: (omission) => omissions.push({ ...omission, start: omission.start + offset }),
|
|
132
140
|
text: segment.text,
|
|
133
|
-
cacheDir: subtitleModelDir(deps.paths.dataDir),
|
|
141
|
+
cacheDir: subtitleModelDir(deps.paths.dataDir, language),
|
|
142
|
+
...(language === "en" ? {} : { language }),
|
|
134
143
|
ffmpeg: deps.ffmpeg,
|
|
135
144
|
signal: context.signal,
|
|
136
145
|
onProgress: (current, maximum) => {
|
|
@@ -147,7 +156,7 @@ async function alignSegments(deps, context, segments, omissions) {
|
|
|
147
156
|
});
|
|
148
157
|
for (const word of aligned) {
|
|
149
158
|
if (word.end > segment.seconds + 0.1)
|
|
150
|
-
throw new Error("Caption timing came out longer than the narration audio.
|
|
159
|
+
throw new Error("Caption timing came out longer than the narration audio. Try again; if it happens again, use Download diagnostics in Settings and report it.");
|
|
151
160
|
words.push({
|
|
152
161
|
...word,
|
|
153
162
|
start: word.start + offset,
|
|
@@ -170,12 +179,14 @@ function readCache(deps, projectId, outputs) {
|
|
|
170
179
|
return undefined;
|
|
171
180
|
}
|
|
172
181
|
}
|
|
173
|
-
async function snapshotFont(deps, projectId, outputs, id, cache, directory) {
|
|
182
|
+
async function snapshotFont(deps, projectId, outputs, id, cache, directory, language) {
|
|
174
183
|
const previous = outputs.find((one) => one.role === "subtitle_font");
|
|
175
184
|
const previousPath = previous === undefined ? undefined : outputPath(deps.paths, projectId, previous.path);
|
|
176
|
-
const saved =
|
|
177
|
-
?
|
|
178
|
-
:
|
|
185
|
+
const saved = language !== "en"
|
|
186
|
+
? await captionFont(captionFontDeps(deps.paths), id, language)
|
|
187
|
+
: cache?.font.id === id && previousPath !== undefined && existsSync(previousPath)
|
|
188
|
+
? { ...cache.font, path: previousPath }
|
|
189
|
+
: await resolveFont(deps.paths, id);
|
|
179
190
|
mkdirSync(join(directory, "fonts"), { mode: 0o700 });
|
|
180
191
|
copyFileSync(saved.path, join(directory, "fonts", `selected${saved.extension}`));
|
|
181
192
|
return { id: saved.id, name: saved.name, assName: saved.assName, extension: saved.extension };
|
|
@@ -27,7 +27,7 @@ export function spokenText(deps, projectId, kind, outputs) {
|
|
|
27
27
|
return segment.text;
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
|
-
throw new Error(`The saved text of the ${kind === "body" ? "article" : kind} is missing, and captions need the text spoken in the narration. Use
|
|
30
|
+
throw new Error(`The saved text of the ${kind === "body" ? "article" : kind} is missing, and captions need the text spoken in the narration. Use More → Write the article again in the Article section, then Try again.`);
|
|
31
31
|
}
|
|
32
32
|
function payload(raw) {
|
|
33
33
|
return textPayload.parse(JSON.parse(raw ?? "null"));
|
|
@@ -22,13 +22,13 @@ export async function runThumbnail(deps, context, providers) {
|
|
|
22
22
|
if (source !== "from_prompt" && source !== "prompt_by_llm") {
|
|
23
23
|
// Off is `skipped` and Provide is `provided` at project creation,
|
|
24
24
|
// so the runner never starts this stage for either.
|
|
25
|
-
throw new Error(`Slopify hit an internal error (the thumbnail stage cannot run with its source set to ${source}).
|
|
25
|
+
throw new Error(`Slopify hit an internal error (the thumbnail stage cannot run with its source set to ${source}). Try again; if it happens again, use Download diagnostics in Settings and report it.`);
|
|
26
26
|
}
|
|
27
27
|
const choice = project.config.images;
|
|
28
28
|
if (choice === undefined) {
|
|
29
29
|
// Admission requires the image provider whenever the thumbnail is generated, so reaching
|
|
30
30
|
// here is a bug in admission rather than the user's.
|
|
31
|
-
throw new Error("No image model is set for the thumbnail. Choose one in Edit project → Providers, then
|
|
31
|
+
throw new Error("No image model is set for the thumbnail. Choose one in Edit project → Providers, then Try again.");
|
|
32
32
|
}
|
|
33
33
|
const result = source === "from_prompt" ? undefined : await byLlm(deps, context, providers, project);
|
|
34
34
|
if (result !== undefined && !result.ok)
|
|
@@ -60,7 +60,7 @@ export async function runThumbnail(deps, context, providers) {
|
|
|
60
60
|
function fromTemplate(project) {
|
|
61
61
|
const rendered = project.config.rendered.thumbnailPrompt;
|
|
62
62
|
if (rendered === undefined) {
|
|
63
|
-
throw new Error("Slopify hit an internal error (the thumbnail prompt was never filled in).
|
|
63
|
+
throw new Error("Slopify hit an internal error (the thumbnail prompt was never filled in). Try again; if it happens again, use Download diagnostics in Settings and report it.");
|
|
64
64
|
}
|
|
65
65
|
return rendered;
|
|
66
66
|
}
|
|
@@ -83,7 +83,7 @@ async function byLlm(deps, context, providers, project) {
|
|
|
83
83
|
}
|
|
84
84
|
if (llm === undefined) {
|
|
85
85
|
// Admission requires the LLM row when the thumbnail source is Prompt by LLM.
|
|
86
|
-
throw new Error("No AI model is set for writing the thumbnail prompt. Choose one in Edit project → Providers, then
|
|
86
|
+
throw new Error("No AI model is set for writing the thumbnail prompt. Choose one in Edit project → Providers, then Try again.");
|
|
87
87
|
}
|
|
88
88
|
const messages = thumbnailMessages(brief(deps, project));
|
|
89
89
|
const answer = await providers.llm({
|
|
@@ -117,7 +117,7 @@ function named(choice) {
|
|
|
117
117
|
function brief(deps, project) {
|
|
118
118
|
const instruction = project.config.rendered.thumbnailPrompt;
|
|
119
119
|
if (instruction === undefined) {
|
|
120
|
-
throw new Error("Slopify hit an internal error (the thumbnail prompt was never filled in).
|
|
120
|
+
throw new Error("Slopify hit an internal error (the thumbnail prompt was never filled in). Try again; if it happens again, use Download diagnostics in Settings and report it.");
|
|
121
121
|
}
|
|
122
122
|
return {
|
|
123
123
|
instruction,
|
|
@@ -133,7 +133,7 @@ function brief(deps, project) {
|
|
|
133
133
|
function articleText(deps, projectId) {
|
|
134
134
|
const article = outputsOf(deps.db, projectId).find((output) => output.role === "article_txt");
|
|
135
135
|
if (article === undefined) {
|
|
136
|
-
throw new Error("The thumbnail prompt is written from the article, but the article isn't ready. Let the Article stage finish, then
|
|
136
|
+
throw new Error("The thumbnail prompt is written from the article, but the article isn't ready. Let the Article stage finish, then Try again.");
|
|
137
137
|
}
|
|
138
138
|
return readFileSync(outputPath(deps.paths, projectId, article.path), "utf8");
|
|
139
139
|
}
|
|
@@ -212,7 +212,7 @@ function instructionsText(messages) {
|
|
|
212
212
|
}
|
|
213
213
|
function payloadOf(piece) {
|
|
214
214
|
if (piece.payload === null) {
|
|
215
|
-
throw new Error("Slopify hit an internal error (the saved thumbnail prompt is empty).
|
|
215
|
+
throw new Error("Slopify hit an internal error (the saved thumbnail prompt is empty). Try again; if it happens again, use Download diagnostics in Settings and report it.");
|
|
216
216
|
}
|
|
217
217
|
return writtenPayload.parse(JSON.parse(piece.payload));
|
|
218
218
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
// Settings → Trash. Deleting a project, a Library prompt or intro/outro, a template or a
|
|
3
|
+
// schedule stamps it instead of removing it; it stays here this long, then the daily purge
|
|
4
|
+
// removes it for good (a project's folder with it).
|
|
5
|
+
export const trashDays = 30;
|
|
6
|
+
export const trashMs = trashDays * 24 * 60 * 60_000;
|
|
7
|
+
export const trashKinds = ["project", "prompt", "entry", "template", "schedule"];
|
|
8
|
+
export const trashItemSchema = z.object({
|
|
9
|
+
kind: z.enum(trashKinds),
|
|
10
|
+
id: z.string(),
|
|
11
|
+
name: z.string(),
|
|
12
|
+
// What sort of prompt ("article", "image"…) or entry ("intro", "outro"); null otherwise.
|
|
13
|
+
detail: z.string().nullable(),
|
|
14
|
+
deletedAt: z.string(),
|
|
15
|
+
// When the daily purge removes it for good.
|
|
16
|
+
purgeAt: z.string(),
|
|
17
|
+
daysLeft: z.number().int().nonnegative(),
|
|
18
|
+
});
|
|
19
|
+
export const restoredSchema = z.object({
|
|
20
|
+
kind: z.enum(trashKinds),
|
|
21
|
+
id: z.string(),
|
|
22
|
+
name: z.string(),
|
|
23
|
+
// The name it came back under when a live item had taken the old one meanwhile.
|
|
24
|
+
renamedFrom: z.string().nullable(),
|
|
25
|
+
});
|