@gentbajko/slopify 2.5.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 +3 -0
- package/dist/adapters/alignment/cache.js +4 -1
- package/dist/adapters/alignment/ctc.js +25 -23
- package/dist/adapters/alignment/index.js +37 -6
- package/dist/adapters/alignment/multilingual.js +267 -0
- package/dist/adapters/alignment/protocol.js +11 -0
- package/dist/adapters/alignment/quality.js +4 -3
- package/dist/adapters/alignment/sentences.js +95 -0
- package/dist/adapters/alignment/spec.js +24 -0
- package/dist/adapters/alignment/spell.js +769 -0
- package/dist/adapters/alignment/text.js +56 -9
- package/dist/adapters/alignment/window.js +34 -30
- package/dist/adapters/alignment/worker.js +23 -21
- package/dist/adapters/explain.js +11 -11
- package/dist/adapters/host-cli/index.js +44 -5
- package/dist/adapters/host-cli/transport.js +7 -5
- package/dist/adapters/image/bytes.js +2 -2
- package/dist/adapters/image/codex-output.js +1 -1
- package/dist/adapters/image/codex.js +87 -22
- package/dist/adapters/image/fal.js +13 -5
- package/dist/adapters/image/google.js +11 -8
- package/dist/adapters/image/openai.js +14 -7
- package/dist/adapters/image/reference.js +34 -0
- package/dist/adapters/image/replicate.js +7 -2
- package/dist/adapters/image/video.js +2 -2
- package/dist/adapters/key-probes.js +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.js +99 -12
- package/dist/adapters/llm/document-workspace.js +2 -2
- package/dist/adapters/llm/gemini-workspace.js +1 -1
- package/dist/adapters/llm/gemini.js +16 -3
- package/dist/adapters/llm/image-workspace.js +58 -0
- package/dist/adapters/llm/openrouter.js +11 -3
- package/dist/adapters/llm/run-cli.js +12 -7
- package/dist/adapters/tts/cartesia.js +10 -0
- package/dist/adapters/tts/elevenlabs.js +31 -9
- package/dist/adapters/tts/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/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 +13 -13
- package/dist/catalog/schema.js +2 -0
- package/dist/catalog/store.js +147 -8
- 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/host-cli.js +1 -1
- package/dist/edge/http/actions.js +32 -10
- package/dist/edge/http/app.js +58 -2
- package/dist/edge/http/auditions.js +70 -0
- package/dist/edge/http/autostart.js +37 -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 +29 -5
- 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/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 +84 -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 +9 -1
- 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 +122 -14
- package/dist/kernel/ports/key-probe.js +1 -0
- package/dist/kernel/ports/languages.js +157 -0
- package/dist/kernel/ports/model.js +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 +17 -8
- 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 +142 -7
- package/dist/kernel/runner/retry-policy.js +43 -0
- package/dist/kernel/runner/standalone.js +134 -0
- package/dist/kernel/runner/work-authority.js +28 -2
- package/dist/main.js +272 -16
- 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 +27 -0
- package/dist/slices/admission/repo.js +24 -4
- package/dist/slices/admission/rules.js +79 -1
- package/dist/slices/admission/schema.js +58 -1
- package/dist/slices/admission/short-mode.js +47 -0
- package/dist/slices/admission/start.js +41 -5
- package/dist/slices/article/continuation.js +2 -2
- package/dist/slices/article/provided-entries.js +2 -2
- package/dist/slices/article/run.js +6 -2
- package/dist/slices/article/segments.js +3 -3
- package/dist/slices/backups/folder.js +14 -6
- package/dist/slices/batch/index.js +7 -2
- package/dist/slices/cancel/index.js +1 -1
- package/dist/slices/channels/cast-images.js +110 -0
- package/dist/slices/channels/cast-match.js +32 -0
- package/dist/slices/channels/images.js +19 -0
- package/dist/slices/channels/model.js +5 -0
- package/dist/slices/channels/rebrand.js +100 -0
- package/dist/slices/channels/repo.js +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 +109 -28
- 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 +7 -1
- package/dist/slices/library/used-by.js +153 -0
- package/dist/slices/loudness/level-pieces.js +41 -0
- package/dist/slices/loudness/loudnorm.js +164 -0
- package/dist/slices/loudness/model.js +153 -0
- package/dist/slices/model-upkeep/model.js +10 -0
- package/dist/slices/model-upkeep/switch.js +217 -0
- package/dist/slices/model-upkeep/usage.js +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 +21 -3
- package/dist/slices/notifications/rules.js +22 -1
- 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 +125 -6
- package/dist/slices/play-drafts/readiness.js +3 -0
- package/dist/slices/play-drafts/repo.js +2 -0
- package/dist/slices/play-drafts/review-inputs.js +20 -3
- package/dist/slices/play-drafts/schema.js +89 -0
- package/dist/slices/play-drafts/service.js +4 -1
- package/dist/slices/play-drafts/start.js +43 -18
- package/dist/slices/project-templates/from-project.js +50 -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 +18 -2
- package/dist/slices/rebuild/admission-repo.js +1 -1
- package/dist/slices/rebuild/model.js +6 -0
- package/dist/slices/rebuild/preview-details.js +46 -3
- package/dist/slices/rebuild/preview-plan.js +13 -6
- package/dist/slices/rebuild/recipe-audio-parts.js +40 -7
- package/dist/slices/rebuild/recipe-audio.js +66 -12
- package/dist/slices/rebuild/recipe-build.js +8 -4
- package/dist/slices/rebuild/recipe-cast.js +18 -0
- package/dist/slices/rebuild/recipe-checkpoints.js +11 -9
- package/dist/slices/rebuild/recipe-describe.js +93 -0
- package/dist/slices/rebuild/recipe-edit.js +13 -1
- package/dist/slices/rebuild/recipe-exports.js +62 -8
- package/dist/slices/rebuild/recipe-input-schema.js +35 -0
- package/dist/slices/rebuild/recipe-legacy.js +9 -0
- package/dist/slices/rebuild/recipe-loudness.js +45 -0
- package/dist/slices/rebuild/recipe-model.js +25 -6
- package/dist/slices/rebuild/recipe-pauses.js +7 -0
- package/dist/slices/rebuild/recipe-preparation.js +3 -3
- package/dist/slices/rebuild/recipe-provider-choice.js +11 -1
- package/dist/slices/rebuild/recipe-reference.js +15 -1
- package/dist/slices/rebuild/recipe-reviews.js +117 -0
- package/dist/slices/rebuild/recipe-save.js +4 -1
- package/dist/slices/rebuild/recipe-shorts.js +23 -3
- package/dist/slices/rebuild/recipe-text.js +114 -19
- package/dist/slices/rebuild/recipe-validation.js +14 -2
- package/dist/slices/rebuild/recipe-visual.js +96 -33
- package/dist/slices/rebuild/recipe-voices.js +286 -0
- package/dist/slices/rebuild/recipe-work.js +77 -6
- package/dist/slices/rebuild/recipe-youtube.js +2 -0
- package/dist/slices/rebuild/recovery-conflict.js +7 -3
- package/dist/slices/rebuild/recovery-model.js +3 -0
- package/dist/slices/rebuild/recovery-selection.js +28 -2
- package/dist/slices/rebuild/recovery.js +35 -17
- package/dist/slices/rebuild/repo.js +3 -3
- package/dist/slices/rebuild/retry.js +18 -0
- package/dist/slices/rebuild/review-redo.js +110 -0
- package/dist/slices/rebuild/runtime-actions.js +5 -0
- package/dist/slices/rebuild/runtime-admission.js +4 -1
- package/dist/slices/rebuild/runtime-animate.js +4 -4
- package/dist/slices/rebuild/runtime-article.js +5 -5
- package/dist/slices/rebuild/runtime-document.js +4 -4
- package/dist/slices/rebuild/runtime-export-bed.js +43 -0
- package/dist/slices/rebuild/runtime-export-edit.js +70 -11
- package/dist/slices/rebuild/runtime-export-inputs.js +52 -10
- package/dist/slices/rebuild/runtime-export-short.js +85 -0
- package/dist/slices/rebuild/runtime-export.js +84 -19
- package/dist/slices/rebuild/runtime-figure-card.js +87 -0
- package/dist/slices/rebuild/runtime-image.js +21 -2
- package/dist/slices/rebuild/runtime-local.js +148 -24
- package/dist/slices/rebuild/runtime-materialize.js +8 -0
- package/dist/slices/rebuild/runtime-narration-reuse.js +2 -1
- package/dist/slices/rebuild/runtime-narration-text.js +7 -7
- package/dist/slices/rebuild/runtime-pictures.js +28 -0
- package/dist/slices/rebuild/runtime-piece-label.js +27 -0
- package/dist/slices/rebuild/runtime-provider.js +126 -18
- package/dist/slices/rebuild/runtime-publication.js +21 -5
- package/dist/slices/rebuild/runtime-review.js +237 -0
- package/dist/slices/rebuild/runtime-run.js +17 -11
- package/dist/slices/rebuild/runtime-shorts.js +44 -17
- package/dist/slices/rebuild/runtime-store.js +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/soften.js +49 -0
- package/dist/slices/rebuild/transition-repo.js +9 -2
- package/dist/slices/research/run.js +4 -4
- package/dist/slices/research/synthesis.js +2 -2
- package/dist/slices/reviews/model.js +105 -0
- package/dist/slices/reviews/outcome.js +22 -0
- package/dist/slices/reviews/repo.js +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 +5 -2
- package/dist/slices/revisions/adopt.js +3 -1
- package/dist/slices/revisions/downloads.js +7 -1
- package/dist/slices/revisions/image-plan.js +183 -0
- package/dist/slices/revisions/mutation-assets.js +10 -0
- package/dist/slices/revisions/mutation-cues.js +71 -0
- package/dist/slices/revisions/mutation-request.js +2 -1
- package/dist/slices/revisions/mutations.js +97 -3
- package/dist/slices/revisions/schema.js +11 -1
- package/dist/slices/run-cost/limits.js +128 -0
- package/dist/slices/run-cost/meter.js +47 -0
- package/dist/slices/run-cost/panel.js +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 +43 -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 +6 -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 +14 -4
- package/dist/slices/storage/model.js +11 -0
- package/dist/slices/storage/portable.js +92 -12
- package/dist/slices/storage/reconcile.js +14 -1
- package/dist/slices/storage/repo.js +4 -1
- package/dist/slices/storage/schema.js +11 -0
- package/dist/slices/storage/trim.js +146 -0
- package/dist/slices/studio/disclosure.js +74 -0
- package/dist/slices/studio/model.js +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-BLF2i8c2.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-CktpxlJd.js +0 -171
- package/dist/web/assets/index-DB-vOnGU.css +0 -1
- package/scripts/docker-run.sh +0 -3
- /package/dist/edge/{docker-projects → docker-install}/tree.js +0 -0
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import {
|
|
3
|
+
import { withLanguage, wordTimingUnavailable } from "../../kernel/ports/languages.js";
|
|
4
|
+
import { usesFigureCards, usesShorts } from "../admission/rules.js";
|
|
5
|
+
import { captionBoldFont } from "../fonts/coverage.js";
|
|
4
6
|
import { resolveBoldFont } from "../fonts/index.js";
|
|
7
|
+
import { masterGoal } from "../loudness/model.js";
|
|
5
8
|
import { clipWords } from "../shorts/captions.js";
|
|
6
9
|
import { effectiveClips, pickedShortsOf } from "../shorts/clips.js";
|
|
7
10
|
import { defaultShortsPrompt, musicVolumeOf, shortImageCount, shortsSpeedOf, } from "../shorts/model.js";
|
|
@@ -11,8 +14,10 @@ import { renderShort } from "../shorts/render.js";
|
|
|
11
14
|
import { allocateAsset, discardPreparedAssets, sealAsset, writeAsset } from "../storage/assets.js";
|
|
12
15
|
import { outputPath, projectDir } from "../storage/layout.js";
|
|
13
16
|
import { probeDurationMs } from "../video/ffmpeg.js";
|
|
17
|
+
import { usesVoices } from "../voices/model.js";
|
|
14
18
|
import { sentencesText, transcriptSentences } from "../youtube/transcript.js";
|
|
15
19
|
import { shortsPickKey } from "./recipe-shorts.js";
|
|
20
|
+
import { figureShots } from "./runtime-export-edit.js";
|
|
16
21
|
import { exportSnapshot, revisionAudio } from "./runtime-export-inputs.js";
|
|
17
22
|
import { imageCall } from "./runtime-image.js";
|
|
18
23
|
import { savedCatalogue } from "./runtime-plan.js";
|
|
@@ -20,7 +25,7 @@ import { preparedResult, preparedTexts, publishResult } from "./runtime-publicat
|
|
|
20
25
|
import { wordsSchema } from "./runtime-subtitles.js";
|
|
21
26
|
// The Shorts step's four kinds of work (`recipe-shorts.ts`): the pick, each clip's image
|
|
22
27
|
// prompts, each image, and each clip's render. Each is one piece of its own, so a failure
|
|
23
|
-
// names the clip it belongs to and
|
|
28
|
+
// names the clip it belongs to and Try again redoes only what did not finish.
|
|
24
29
|
export async function executeShortsRecipe(deps, context, providers, piece) {
|
|
25
30
|
if (deps.db
|
|
26
31
|
.prepare("SELECT state FROM revision_work_pieces WHERE id=? AND work_id=?")
|
|
@@ -40,7 +45,7 @@ export async function executeShortsRecipe(deps, context, providers, piece) {
|
|
|
40
45
|
throw setupError();
|
|
41
46
|
const clip = savedClips(snapshot).find((one) => one.number === number);
|
|
42
47
|
if (clip === undefined)
|
|
43
|
-
throw new Error("The clip this short was cut from is no longer among the picked shorts. Use Pick different moments in Edit project → Shorts, then
|
|
48
|
+
throw new Error("The clip this short was cut from is no longer among the picked shorts. Use Pick different moments in Edit project → Shorts, then Try again.");
|
|
44
49
|
if (matched[2] === "prompts")
|
|
45
50
|
return prompts(deps, context, providers, piece, snapshot, clip);
|
|
46
51
|
if (matched[2] === "render")
|
|
@@ -58,10 +63,10 @@ async function pick(deps, context, providers, piece, snapshot) {
|
|
|
58
63
|
throw setupError();
|
|
59
64
|
const llm = config.llm;
|
|
60
65
|
if (llm === undefined || llm.provider.trim() === "" || llm.model.trim() === "")
|
|
61
|
-
throw new Error("Shorts need an AI text model to pick the clips, and none is chosen. Choose one in Edit project → Providers, then
|
|
66
|
+
throw new Error("Shorts need an AI text model to pick the clips, and none is chosen. Choose one in Edit project → Providers, then Try again.");
|
|
62
67
|
const sentences = transcriptSentences(timingWords(deps, context, view));
|
|
63
68
|
if (sentences.length === 0)
|
|
64
|
-
throw new Error("The narration's word timing holds no words, so there is nothing to cut shorts from. Use
|
|
69
|
+
throw new Error("The narration's word timing holds no words, so there is nothing to cut shorts from. Use More → Render the video again in the Video section, then Try again.");
|
|
65
70
|
const audio = await revisionAudio(deps, context, view);
|
|
66
71
|
const durationSeconds = audio.reduce((sum, segment) => sum + segment.seconds, 0);
|
|
67
72
|
const limits = {
|
|
@@ -81,13 +86,17 @@ async function pick(deps, context, providers, piece, snapshot) {
|
|
|
81
86
|
count: shorts.count,
|
|
82
87
|
minSeconds: shorts.minSeconds,
|
|
83
88
|
maxSeconds: shorts.maxSeconds,
|
|
84
|
-
sentences: sentencesText(sentences)
|
|
89
|
+
sentences: sentencesText(sentences, usesVoices(config) && config.voices !== undefined
|
|
90
|
+
? Object.fromEntries(config.voices.speakers.map((speaker) => [speaker.id, speaker.name.trim()]))
|
|
91
|
+
: undefined),
|
|
85
92
|
};
|
|
86
93
|
const ask = llmCall(deps, context, piece, view, (text) => {
|
|
87
94
|
const checked = checkPicks(text, sentences, limits);
|
|
88
95
|
return checked.ok ? undefined : checked.reason;
|
|
89
96
|
});
|
|
90
|
-
const first = await providers
|
|
97
|
+
const first = await providers
|
|
98
|
+
.forPiece(piece.id)
|
|
99
|
+
.llm(ask(withLanguage(pickMessages(brief), config.language)));
|
|
91
100
|
if (!first.ok)
|
|
92
101
|
return "held";
|
|
93
102
|
let checked = checkPicks(first.value.text, sentences, limits);
|
|
@@ -100,7 +109,7 @@ async function pick(deps, context, providers, piece, snapshot) {
|
|
|
100
109
|
return "held";
|
|
101
110
|
const second = await providers
|
|
102
111
|
.forPiece(piece.id)
|
|
103
|
-
.llm(ask(pickRetryMessages(brief, first.value.text, checked.problems)));
|
|
112
|
+
.llm(ask(withLanguage(pickRetryMessages(brief, first.value.text, checked.problems), config.language)));
|
|
104
113
|
if (!second.ok)
|
|
105
114
|
return "held";
|
|
106
115
|
const again = checkPicks(second.value.text, sentences, limits);
|
|
@@ -191,13 +200,27 @@ async function render(deps, context, piece, snapshot, clip) {
|
|
|
191
200
|
.map((recipe) => {
|
|
192
201
|
const row = view.outputs.find((one) => one.workKey === recipe.key && one.selected && one.available && one.state === "ready");
|
|
193
202
|
if (row === undefined)
|
|
194
|
-
throw new Error(`Short ${String(clip.number)} is missing one of its images.
|
|
203
|
+
throw new Error(`Short ${String(clip.number)} is missing one of its images. Try again to make it again; if it keeps happening, use Make this short again in Edit project → Shorts.`);
|
|
195
204
|
return outputPath(deps.paths, context.work.projectId, row.output.path);
|
|
196
205
|
});
|
|
197
|
-
const
|
|
198
|
-
const
|
|
206
|
+
const allWords = timingWords(deps, context, view);
|
|
207
|
+
const words = clipWords(allWords, clip.start, clip.end);
|
|
208
|
+
// The upright cards whose descriptions fall in the clip, on the clip's own timeline.
|
|
209
|
+
const figures = usesFigureCards(config)
|
|
210
|
+
? figureShots(deps, context, view, "9:16", allWords).flatMap((figure) => figure.end <= clip.start || figure.start >= clip.end
|
|
211
|
+
? []
|
|
212
|
+
: [
|
|
213
|
+
{
|
|
214
|
+
path: figure.path,
|
|
215
|
+
start: Math.max(0, figure.start - clip.start),
|
|
216
|
+
end: Math.min(clip.end, figure.end) - clip.start,
|
|
217
|
+
},
|
|
218
|
+
])
|
|
219
|
+
: [];
|
|
220
|
+
const timeline = await revisionAudio(deps, context, view, { levelled: true });
|
|
199
221
|
// The bundled Barlow is drawn in its own Bold face; any other font is emboldened.
|
|
200
|
-
|
|
222
|
+
// In another language, a font that has its letters (`fonts/coverage.ts`).
|
|
223
|
+
const font = await captionBoldFont(deps.paths, config.subtitles?.fontId ?? "default", config.language);
|
|
201
224
|
const shorts = config.shorts;
|
|
202
225
|
const speed = shorts === undefined ? 1 : shortsSpeedOf(shorts);
|
|
203
226
|
const music = await backgroundMusic(deps, context, view);
|
|
@@ -207,8 +230,9 @@ async function render(deps, context, piece, snapshot, clip) {
|
|
|
207
230
|
const prepared = [];
|
|
208
231
|
const seconds = (clip.end - clip.start) / speed;
|
|
209
232
|
try {
|
|
210
|
-
await renderShort({
|
|
233
|
+
const master = await renderShort({
|
|
211
234
|
bin: deps.ffmpeg,
|
|
235
|
+
master: masterGoal(config, "video"),
|
|
212
236
|
timeline,
|
|
213
237
|
start: clip.start,
|
|
214
238
|
end: clip.end,
|
|
@@ -217,8 +241,10 @@ async function render(deps, context, piece, snapshot, clip) {
|
|
|
217
241
|
motionStyle: config.motionStyle,
|
|
218
242
|
zoomPercent: config.zoomPercent,
|
|
219
243
|
words,
|
|
244
|
+
...(wordTimingUnavailable(config.language) === undefined ? {} : { wordByWord: false }),
|
|
220
245
|
font,
|
|
221
246
|
...(shorts?.titleOnScreen === true ? { title: clip.title } : {}),
|
|
247
|
+
...(figures.length === 0 ? {} : { figures }),
|
|
222
248
|
speed,
|
|
223
249
|
music,
|
|
224
250
|
output: pending.absolutePath,
|
|
@@ -239,6 +265,7 @@ async function render(deps, context, piece, snapshot, clip) {
|
|
|
239
265
|
prepared.push(preparedResult(deps, context, piece, "short_video", asset, durationMs, {
|
|
240
266
|
short: clip.number,
|
|
241
267
|
sentences: [clip.first, clip.last],
|
|
268
|
+
...(master === undefined ? {} : { master }),
|
|
242
269
|
}));
|
|
243
270
|
await publishResult(deps, context, piece, prepared, { short: clip.number, start: clip.start, end: clip.end, durationMs }, asset);
|
|
244
271
|
// One per rendered short, with no stage: stage "video" would read as a finished video.
|
|
@@ -271,7 +298,7 @@ async function backgroundMusic(deps, context, view) {
|
|
|
271
298
|
const shorts = view.revision.config.shorts;
|
|
272
299
|
if (assetId === undefined || shorts === undefined)
|
|
273
300
|
return undefined;
|
|
274
|
-
const fix = "Choose another file under Background music in Edit project → Shorts, or remove the music, then
|
|
301
|
+
const fix = "Choose another file under Background music in Edit project → Shorts, or remove the music, then Try again.";
|
|
275
302
|
const row = z
|
|
276
303
|
.object({ path: z.string() })
|
|
277
304
|
.safeParse(deps.db
|
|
@@ -292,14 +319,14 @@ async function backgroundMusic(deps, context, view) {
|
|
|
292
319
|
throw new Error(`The shorts' background music file holds no sound. ${fix}`);
|
|
293
320
|
return { path, volume: musicVolumeOf(shorts) };
|
|
294
321
|
}
|
|
295
|
-
function timingWords(deps, context, view) {
|
|
322
|
+
export function timingWords(deps, context, view) {
|
|
296
323
|
const timing = view.outputs.find((row) => row.selected &&
|
|
297
324
|
row.available &&
|
|
298
325
|
row.state === "ready" &&
|
|
299
326
|
row.workKey === "subtitles:timing" &&
|
|
300
327
|
row.output.role === "subtitle_words");
|
|
301
328
|
if (timing === undefined)
|
|
302
|
-
throw new Error("The narration's word timing, which the shorts are cut and captioned from, is missing. Use
|
|
329
|
+
throw new Error("The narration's word timing, which the shorts are cut and captioned from, is missing. Use More → Render the video again in the Video section, then Try again.");
|
|
303
330
|
return wordsSchema.parse(JSON.parse(readFileSync(outputPath(deps.paths, context.work.projectId, timing.output.path), "utf8"))).words;
|
|
304
331
|
}
|
|
305
332
|
// The project's text model, with the thinking settings saved for this step's catalogue.
|
|
@@ -324,5 +351,5 @@ function imageIndex(key) {
|
|
|
324
351
|
return Number(key.split(":").at(-1));
|
|
325
352
|
}
|
|
326
353
|
function setupError() {
|
|
327
|
-
return new Error("Slopify hit an internal error (a Shorts step was set up wrongly).
|
|
354
|
+
return new Error("Slopify hit an internal error (a Shorts step was set up wrongly). Try again; if it happens again, use Download diagnostics in Settings and report it.");
|
|
328
355
|
}
|
|
@@ -2,6 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import { stageKinds, stageStates } from "../../kernel/pipeline.js";
|
|
3
3
|
import { sourceOf } from "../admission/model.js";
|
|
4
4
|
import { usesShorts, usesYoutubeDescription } from "../admission/rules.js";
|
|
5
|
+
import { reviewsNarration } from "../reviews/rules.js";
|
|
5
6
|
import { currentRevisionId } from "../revisions/repo.js";
|
|
6
7
|
import { getRevisionView } from "../revisions/view.js";
|
|
7
8
|
import { settleScheduleRunsForProject } from "../schedules/repo.js";
|
|
@@ -40,7 +41,8 @@ export function executionStages(deps, projectId) {
|
|
|
40
41
|
view?.revision.content.subtitleCues !== undefined &&
|
|
41
42
|
!usesYoutubeDescription(view.revision.config) &&
|
|
42
43
|
!usesShorts(view.revision.config) &&
|
|
43
|
-
!editNeedsTiming(view.revision.config)
|
|
44
|
+
!editNeedsTiming(view.revision.config) &&
|
|
45
|
+
!reviewsNarration(view.revision.config))
|
|
44
46
|
return false;
|
|
45
47
|
if (piece.input.kind === "deferred" && piece.key.endsWith(":future")) {
|
|
46
48
|
const cacheKey = String(row.id);
|
|
@@ -78,10 +80,13 @@ export function executionStages(deps, projectId) {
|
|
|
78
80
|
}
|
|
79
81
|
export function invocationReady(deps, work) {
|
|
80
82
|
const row = deps.db
|
|
81
|
-
.prepare("SELECT recipe_context FROM revision_work WHERE id=? AND project_id=? AND revision_id=?")
|
|
83
|
+
.prepare("SELECT recipe_context,retry_at FROM revision_work WHERE id=? AND project_id=? AND revision_id=?")
|
|
82
84
|
.get(work.workId, work.projectId, work.revisionId);
|
|
83
85
|
if (row?.recipe_context === null || row === undefined)
|
|
84
86
|
return false;
|
|
87
|
+
// Waiting out a failure time can fix: `dueRetries` clears the wait when it runs out.
|
|
88
|
+
if (row.retry_at !== null)
|
|
89
|
+
return false;
|
|
85
90
|
const view = executionView(deps, work.projectId, work.revisionId);
|
|
86
91
|
if (view === undefined)
|
|
87
92
|
return false;
|
|
@@ -129,6 +134,9 @@ export function executionStandings(deps, projectId, derived) {
|
|
|
129
134
|
return retained ? { ...entry, state: "done" } : entry;
|
|
130
135
|
});
|
|
131
136
|
let state = source === "off" ? "skipped" : source === "provide" ? "provided" : "pending";
|
|
137
|
+
const retryAt = group.some((entry) => entry.state === "failed")
|
|
138
|
+
? null
|
|
139
|
+
: latestRetry(deps, group);
|
|
132
140
|
if (group.some((entry) => entry.state === "running"))
|
|
133
141
|
state = "running";
|
|
134
142
|
else if (group.some((entry) => entry.state === "failed"))
|
|
@@ -141,9 +149,23 @@ export function executionStandings(deps, projectId, derived) {
|
|
|
141
149
|
? "provided"
|
|
142
150
|
: "done"
|
|
143
151
|
: "pending";
|
|
144
|
-
return { kind, state };
|
|
152
|
+
return state === "pending" && retryAt !== null ? { kind, state, retryAt } : { kind, state };
|
|
145
153
|
});
|
|
146
154
|
}
|
|
155
|
+
// The latest wait among a stage's steps that went back to wait after a failure time can fix.
|
|
156
|
+
function latestRetry(deps, group) {
|
|
157
|
+
let latest = null;
|
|
158
|
+
for (const entry of group) {
|
|
159
|
+
if (entry.state !== "pending")
|
|
160
|
+
continue;
|
|
161
|
+
const at = deps.db
|
|
162
|
+
.prepare("SELECT retry_at FROM revision_work WHERE id=?")
|
|
163
|
+
.get(entry.work.workId)?.retry_at;
|
|
164
|
+
if (typeof at === "string" && (latest === null || at > latest))
|
|
165
|
+
latest = at;
|
|
166
|
+
}
|
|
167
|
+
return latest;
|
|
168
|
+
}
|
|
147
169
|
export function projectStandings(deps, projectId) {
|
|
148
170
|
// One derivation serves every stage: each builds the whole execution plan.
|
|
149
171
|
const work = executionStages(deps, projectId);
|
|
@@ -159,15 +181,19 @@ export function projectStandings(deps, projectId) {
|
|
|
159
181
|
.get(entry.work.workId);
|
|
160
182
|
return total + fraction(progress?.progress_current, progress?.progress_total);
|
|
161
183
|
}, 0);
|
|
162
|
-
|
|
184
|
+
// A failed step, or else one waiting to run again, says why on the stage row.
|
|
185
|
+
const failed = all.find((work) => work.state === "failed") ??
|
|
186
|
+
(stage.retryAt === undefined || stage.retryAt === null
|
|
187
|
+
? undefined
|
|
188
|
+
: all.find((work) => work.state === "pending"));
|
|
163
189
|
const failure = failed === undefined
|
|
164
|
-
?
|
|
165
|
-
:
|
|
166
|
-
.prepare("SELECT failure_reason FROM revision_work WHERE id=?")
|
|
167
|
-
.get(failed.work.workId)
|
|
190
|
+
? undefined
|
|
191
|
+
: deps.db
|
|
192
|
+
.prepare("SELECT failure_reason,failure_kind FROM revision_work WHERE id=?")
|
|
193
|
+
.get(failed.work.workId);
|
|
168
194
|
deps.db
|
|
169
|
-
.prepare("UPDATE stages SET state=?,failure_reason=?,progress_current=?,progress_total=?,finished_at=CASE WHEN ?='done' THEN ? ELSE NULL END WHERE project_id=? AND kind=?")
|
|
170
|
-
.run(stage.state, failure, invocations.length === 0 ? null : current, invocations.length === 0 ? null : invocations.length, stage.state, deps.clock.now().toISOString(), projectId, stage.kind);
|
|
195
|
+
.prepare("UPDATE stages SET state=?,failure_reason=?,failure_kind=?,retry_at=?,progress_current=?,progress_total=?,finished_at=CASE WHEN ?='done' THEN ? ELSE NULL END WHERE project_id=? AND kind=?")
|
|
196
|
+
.run(stage.state, textOf(failure?.failure_reason), textOf(failure?.failure_kind), stage.retryAt ?? null, invocations.length === 0 ? null : current, invocations.length === 0 ? null : invocations.length, stage.state, deps.clock.now().toISOString(), projectId, stage.kind);
|
|
171
197
|
}
|
|
172
198
|
settleScheduleRunsForProject(deps.db, projectId, deps.clock.now().toISOString());
|
|
173
199
|
}
|
|
@@ -225,6 +251,9 @@ const spokenKey = /^audio:(?:intro|body|outro)(?::(?!concat$|future$).+)?$/;
|
|
|
225
251
|
function spoken(deps, workId) {
|
|
226
252
|
return workPieces(deps.db, workId).some((piece) => spokenKey.test(piece.key));
|
|
227
253
|
}
|
|
254
|
+
function textOf(value) {
|
|
255
|
+
return typeof value === "string" ? value : null;
|
|
256
|
+
}
|
|
228
257
|
function fraction(current, total) {
|
|
229
258
|
if (typeof current !== "number" || typeof total !== "number" || total <= 0)
|
|
230
259
|
return 0;
|
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { subtitleModelDir } from "../../kernel/paths.js";
|
|
4
|
+
import { projectLanguage } from "../../kernel/ports/languages.js";
|
|
4
5
|
import { SubtitleMismatch } from "../../kernel/ports/subtitles.js";
|
|
6
|
+
import { narrationAliasesOf } from "../admission/rules.js";
|
|
7
|
+
import { captionFont, captionFontDeps } from "../fonts/coverage.js";
|
|
5
8
|
import { resolveFont } from "../fonts/index.js";
|
|
6
9
|
import { validateCues } from "../revisions/rules.js";
|
|
7
10
|
import { discardPreparedAssets, writeAsset } from "../storage/assets.js";
|
|
8
11
|
import { outputPath } from "../storage/layout.js";
|
|
9
12
|
import { captionCues, serializeAss, serializeSrt, serializeVtt } from "../subtitles/captions.js";
|
|
10
13
|
import { spoken } from "../video/plan.js";
|
|
11
|
-
import {
|
|
14
|
+
import { usesVoices } from "../voices/model.js";
|
|
15
|
+
import { speakerColour } from "../voices/palette.js";
|
|
16
|
+
import { speakerPanelEvents, usesSpeakerPanel } from "../voices/panel.js";
|
|
17
|
+
import { panelPortraits } from "../voices/portraits.js";
|
|
18
|
+
import { attributeWords } from "../voices/timing.js";
|
|
19
|
+
import { exportSnapshot, retainedOutput, revisionAudio, revisionTranscript, revisionTurns, } from "./runtime-export-inputs.js";
|
|
12
20
|
import { narrationChunks, narrationTextParts, } from "./runtime-narration-text.js";
|
|
13
21
|
import { preparedResult, preparedText, publishResult } from "./runtime-publication.js";
|
|
14
22
|
// The saved `subtitles.json`: every word at its time in the final video. The YouTube
|
|
@@ -20,6 +28,9 @@ export const wordsSchema = z.object({
|
|
|
20
28
|
start: z.number().finite().nonnegative(),
|
|
21
29
|
end: z.number().finite().positive(),
|
|
22
30
|
confidence: z.number().optional(),
|
|
31
|
+
// A multi-voice run's speaker id and script turn (`voices/timing.ts`).
|
|
32
|
+
speaker: z.string().optional(),
|
|
33
|
+
turn: z.number().int().positive().optional(),
|
|
23
34
|
})),
|
|
24
35
|
omissions: z
|
|
25
36
|
.array(z.object({ start: z.number().finite().nonnegative(), text: z.string() }))
|
|
@@ -30,6 +41,7 @@ const cuesSchema = z.object({
|
|
|
30
41
|
text: z.string(),
|
|
31
42
|
start: z.number().finite().nonnegative(),
|
|
32
43
|
end: z.number().finite().positive(),
|
|
44
|
+
speaker: z.string().optional(),
|
|
33
45
|
})),
|
|
34
46
|
omissions: wordsSchema.shape.omissions,
|
|
35
47
|
});
|
|
@@ -44,19 +56,21 @@ export async function executeSubtitleRecipe(deps, context, piece) {
|
|
|
44
56
|
return cues(deps, context, piece, snapshot);
|
|
45
57
|
if (piece.key === "subtitles:files")
|
|
46
58
|
return files(deps, context, piece, snapshot);
|
|
47
|
-
throw new Error("Slopify hit an internal error (unknown kind of caption step).
|
|
59
|
+
throw new Error("Slopify hit an internal error (unknown kind of caption step). Try again; if it happens again, use Download diagnostics in Settings and report it.");
|
|
48
60
|
}
|
|
49
61
|
async function timing(deps, context, piece, snapshot) {
|
|
50
62
|
const audio = await revisionAudio(deps, context, snapshot.view);
|
|
51
63
|
if (audio.length === 0)
|
|
52
|
-
throw new Error("Captions and the YouTube description need narration audio, but this project has none. Turn captions and the YouTube description off in Edit project, or turn narration on, then
|
|
64
|
+
throw new Error("Captions and the YouTube description need narration audio, but this project has none. Turn captions and the YouTube description off in Edit project, or turn narration on, then Try again.");
|
|
53
65
|
const alignSubtitles = deps.alignSubtitles;
|
|
54
66
|
if (alignSubtitles === undefined)
|
|
55
|
-
throw new Error("Slopify hit an internal error (the caption timing tool is missing from this build).
|
|
67
|
+
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.");
|
|
56
68
|
if (!context.maySubmit(piece.id))
|
|
57
69
|
return "held";
|
|
58
70
|
const words = [];
|
|
59
71
|
const omissions = [];
|
|
72
|
+
const turns = revisionTurns(snapshot);
|
|
73
|
+
const language = projectLanguage(snapshot.view.revision.config);
|
|
60
74
|
let offset = 0;
|
|
61
75
|
const total = audio.reduce((sum, segment) => sum + segment.seconds, 0);
|
|
62
76
|
for (const segment of audio) {
|
|
@@ -66,7 +80,9 @@ async function timing(deps, context, piece, snapshot) {
|
|
|
66
80
|
const aligned = await located(snapshot, kind, () => alignSubtitles({
|
|
67
81
|
audioPath: path,
|
|
68
82
|
text: revisionTranscript(deps, snapshot, kind),
|
|
69
|
-
|
|
83
|
+
aliases: narrationAliasesOf(snapshot.view.revision.config),
|
|
84
|
+
cacheDir: subtitleModelDir(deps.paths.dataDir, language),
|
|
85
|
+
...(language === "en" ? {} : { language }),
|
|
70
86
|
ffmpeg: deps.ffmpeg,
|
|
71
87
|
signal: context.signal,
|
|
72
88
|
onOmission: (value) => omissions.push({ ...value, start: value.start + offset }),
|
|
@@ -78,9 +94,11 @@ async function timing(deps, context, piece, snapshot) {
|
|
|
78
94
|
total: 100,
|
|
79
95
|
}),
|
|
80
96
|
}));
|
|
81
|
-
for (const word of
|
|
97
|
+
for (const word of kind === "body" && turns.length > 0
|
|
98
|
+
? attributeWords(turns, aligned)
|
|
99
|
+
: aligned) {
|
|
82
100
|
if (word.end > segment.seconds + 0.1)
|
|
83
|
-
throw new Error("Caption timing came out longer than the narration audio.
|
|
101
|
+
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.");
|
|
84
102
|
words.push({
|
|
85
103
|
...word,
|
|
86
104
|
start: word.start + offset,
|
|
@@ -91,7 +109,7 @@ async function timing(deps, context, piece, snapshot) {
|
|
|
91
109
|
offset += segment.seconds;
|
|
92
110
|
}
|
|
93
111
|
if (captionCues(words).length === 0)
|
|
94
|
-
throw new Error("None of the narration could be matched to the article text, so captions can't be timed.
|
|
112
|
+
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.");
|
|
95
113
|
context.signal.throwIfAborted();
|
|
96
114
|
const output = preparedText(deps, context, piece, "subtitle_words", "subtitles.json", JSON.stringify({ key: piece.logicalFingerprint ?? piece.fingerprint, words, omissions }));
|
|
97
115
|
await publishResult(deps, context, piece, [output], { words, omissions }, output.asset);
|
|
@@ -103,7 +121,7 @@ async function cues(deps, context, piece, snapshot) {
|
|
|
103
121
|
let value;
|
|
104
122
|
if (manual !== undefined) {
|
|
105
123
|
if (plan.work.find((one) => one.key === piece.key)?.disposition === "review")
|
|
106
|
-
throw new Error("Your edited captions were written for an older version of the narration. Review them in Edit project → Captions and save, then
|
|
124
|
+
throw new Error("Your edited captions were written for an older version of the narration. Review them in Edit project → Captions and save, then Try again.");
|
|
107
125
|
const audio = await revisionAudio(deps, context, view);
|
|
108
126
|
const errors = validateCues(manual.cues, audio.reduce((sum, segment) => sum + segment.seconds, 0));
|
|
109
127
|
if (errors.length > 0)
|
|
@@ -116,7 +134,7 @@ async function cues(deps, context, piece, snapshot) {
|
|
|
116
134
|
one.state === "ready" &&
|
|
117
135
|
one.workKey === "subtitles:timing");
|
|
118
136
|
if (output === undefined)
|
|
119
|
-
throw new Error("Caption timing is missing. Use
|
|
137
|
+
throw new Error("Caption timing is missing. Use More → Render the video again in the Video section, then Try again.");
|
|
120
138
|
const timed = wordsSchema.parse(JSON.parse(readFileSync(outputPath(deps.paths, context.work.projectId, output.output.path), "utf8")));
|
|
121
139
|
value = { cues: [...captionCues(timed.words)], omissions: timed.omissions };
|
|
122
140
|
}
|
|
@@ -130,10 +148,10 @@ async function files(deps, context, piece, snapshot) {
|
|
|
130
148
|
const { view } = snapshot;
|
|
131
149
|
const config = view.revision.config.subtitles;
|
|
132
150
|
if (config === undefined || config.mode === "off")
|
|
133
|
-
throw new Error("Slopify hit an internal error (captions are off for this version of the project).
|
|
151
|
+
throw new Error("Slopify hit an internal error (captions are off for this version of the project). Try again; if it happens again, use Download diagnostics in Settings and report it.");
|
|
134
152
|
const row = view.pieces.find((one) => one.key === "subtitles:cues" && one.selected && one.available && one.piece.state === "done");
|
|
135
153
|
if (row?.piece.payload === null || row?.piece.payload === undefined)
|
|
136
|
-
throw new Error("The captions haven't been prepared yet. Use
|
|
154
|
+
throw new Error("The captions haven't been prepared yet. Use More → Render the video again in the Video section, then Try again.");
|
|
137
155
|
const value = cuesSchema.parse(JSON.parse(row.piece.payload));
|
|
138
156
|
const previous = view.outputs.find((one) => one.output.role === "subtitle_font" && one.selected && one.available);
|
|
139
157
|
const previousFiles = view.pieces.find((one) => one.key === "subtitles:files" &&
|
|
@@ -152,9 +170,17 @@ async function files(deps, context, piece, snapshot) {
|
|
|
152
170
|
.optional(),
|
|
153
171
|
})
|
|
154
172
|
.parse(JSON.parse(previousFiles?.piece.payload ?? "{}")).font;
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
173
|
+
// Another language's captions may need a font with its letters (`fonts/coverage.ts`); an
|
|
174
|
+
// English project keeps the saved font as it always did.
|
|
175
|
+
const language = projectLanguage(view.revision.config);
|
|
176
|
+
const font = language !== "en"
|
|
177
|
+
? await captionFont(captionFontDeps(deps.paths), config.fontId, language)
|
|
178
|
+
: savedFont?.id === config.fontId && previous !== undefined
|
|
179
|
+
? {
|
|
180
|
+
...savedFont,
|
|
181
|
+
path: outputPath(deps.paths, context.work.projectId, previous.output.path),
|
|
182
|
+
}
|
|
183
|
+
: await resolveFont(deps.paths, config.fontId);
|
|
158
184
|
if (!context.maySubmit(piece.id))
|
|
159
185
|
return "held";
|
|
160
186
|
const prepared = [];
|
|
@@ -162,9 +188,20 @@ async function files(deps, context, piece, snapshot) {
|
|
|
162
188
|
const frame = view.revision.config.format === "16:9"
|
|
163
189
|
? { width: 1920, height: 1080 }
|
|
164
190
|
: { width: 1080, height: 1920 };
|
|
191
|
+
const voices = captionVoices(view.revision.config);
|
|
192
|
+
const portraits = panelPortraits(deps.db, voices);
|
|
193
|
+
const overlay = voices !== undefined && usesSpeakerPanel(voices.format)
|
|
194
|
+
? speakerPanelEvents(value.cues, voices.speakers.map((speaker, index) => ({
|
|
195
|
+
id: speaker.id,
|
|
196
|
+
name: speaker.name.trim(),
|
|
197
|
+
colour: speakerColour(index),
|
|
198
|
+
...(portraits[index] === undefined ? {} : { portrait: true }),
|
|
199
|
+
})), frame, (await revisionAudio(deps, context, view)).reduce((sum, segment) => sum + segment.seconds, 0))
|
|
200
|
+
: [];
|
|
201
|
+
const speakers = voices === undefined ? undefined : captionSpeakers(voices);
|
|
165
202
|
for (const [role, filename, text] of [
|
|
166
|
-
["subtitles_srt", "subtitles.srt", serializeSrt(value.cues)],
|
|
167
|
-
["subtitles_vtt", "subtitles.vtt", serializeVtt(value.cues)],
|
|
203
|
+
["subtitles_srt", "subtitles.srt", serializeSrt(value.cues, speakers)],
|
|
204
|
+
["subtitles_vtt", "subtitles.vtt", serializeVtt(value.cues, speakers)],
|
|
168
205
|
[
|
|
169
206
|
"subtitle_ass",
|
|
170
207
|
"subtitles.ass",
|
|
@@ -173,6 +210,10 @@ async function files(deps, context, piece, snapshot) {
|
|
|
173
210
|
fontName: font.assName,
|
|
174
211
|
fontSize: config.fontSize,
|
|
175
212
|
position: config.position,
|
|
213
|
+
color: config.color,
|
|
214
|
+
outlineColor: config.outlineColor,
|
|
215
|
+
speakers,
|
|
216
|
+
overlay,
|
|
176
217
|
}),
|
|
177
218
|
],
|
|
178
219
|
])
|
|
@@ -242,8 +283,8 @@ export function describeMismatch(chunks, segment, mismatch) {
|
|
|
242
283
|
const expected = mismatch.expected === "" ? "the end of the text" : `"${mismatch.expected}…"`;
|
|
243
284
|
const heard = mismatch.heard === "" ? "no more speech" : `"${mismatch.heard.toLowerCase()}…"`;
|
|
244
285
|
const fix = chunk === undefined
|
|
245
|
-
? "Check that part of the narration, regenerate it in Edit project → Narration, then
|
|
246
|
-
: `In Edit project → Narration, regenerate narration chunk ${String(at + 1)}, then
|
|
286
|
+
? "Check that part of the narration, regenerate it in Edit project → Narration, then Continue the run."
|
|
287
|
+
: `In Edit project → Narration, regenerate narration chunk ${String(at + 1)}, then Continue the run.`;
|
|
247
288
|
return `Subtitles stopped matching the audio at ${where}. The text expected ${expected} but the audio has ${heard} The recording there probably skips or changes words. ${fix}`;
|
|
248
289
|
}
|
|
249
290
|
function comparable(text) {
|
|
@@ -266,3 +307,15 @@ function clock(seconds) {
|
|
|
266
307
|
? `${String(hours)}:${String(minutes).padStart(2, "0")}:${rest}`
|
|
267
308
|
: `${String(minutes)}:${rest}`;
|
|
268
309
|
}
|
|
310
|
+
function captionVoices(config) {
|
|
311
|
+
return usesVoices(config) ? config.voices : undefined;
|
|
312
|
+
}
|
|
313
|
+
function captionSpeakers(voices) {
|
|
314
|
+
return {
|
|
315
|
+
styles: Object.fromEntries(voices.speakers.map((speaker, index) => [
|
|
316
|
+
speaker.id,
|
|
317
|
+
{ name: speaker.name.trim(), colour: speakerColour(index) },
|
|
318
|
+
])),
|
|
319
|
+
nameTags: voices.nameTags,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { masterFile, masterReport } from "../loudness/loudnorm.js";
|
|
5
|
+
import { masterGoal } from "../loudness/model.js";
|
|
6
|
+
import { allocateAsset, discardPreparedAssets, sealAsset } from "../storage/assets.js";
|
|
7
|
+
import { outputPath, projectDir } from "../storage/layout.js";
|
|
8
|
+
import { audioExportArgs } from "../video/audio-export-args.js";
|
|
9
|
+
import { runFfmpeg } from "../video/ffmpeg.js";
|
|
10
|
+
import { audioChapters, audioFileArgs, ffmetadata } from "../voices/audio-files.js";
|
|
11
|
+
import { turnStarts } from "../voices/timing.js";
|
|
12
|
+
import { exportSnapshot, revisionAudio } from "./runtime-export-inputs.js";
|
|
13
|
+
import { preparedResult, publishResult } from "./runtime-publication.js";
|
|
14
|
+
import { wordsSchema } from "./runtime-subtitles.js";
|
|
15
|
+
// The listening files of a multi-voice run: the MP3 and the M4B of the narration timeline,
|
|
16
|
+
// with a chapter at each section of the script where its first turn is heard.
|
|
17
|
+
export async function executeVoicesRecipe(deps, context, piece) {
|
|
18
|
+
if (deps.db
|
|
19
|
+
.prepare("SELECT state FROM revision_work_pieces WHERE id=? AND work_id=?")
|
|
20
|
+
.get(piece.id, context.work.workId)?.state === "done")
|
|
21
|
+
return "done";
|
|
22
|
+
if (!context.maySubmit(piece.id))
|
|
23
|
+
return "held";
|
|
24
|
+
if (piece.key !== "voices:files" || piece.input.kind !== "local")
|
|
25
|
+
throw new Error("Slopify hit an internal error (unknown kind of listening-file step). Try again; if it happens again, use Download diagnostics in Settings and report it.");
|
|
26
|
+
context.signal.throwIfAborted();
|
|
27
|
+
const { view } = exportSnapshot(deps, context, piece);
|
|
28
|
+
const sections = z
|
|
29
|
+
.tuple([z.unknown(), z.unknown(), z.array(z.tuple([z.string(), z.number()])), z.string()])
|
|
30
|
+
// Level the volume's master may follow (`recipe-loudness.ts`).
|
|
31
|
+
.rest(z.unknown())
|
|
32
|
+
.parse(piece.input.values)[2]
|
|
33
|
+
.map(([title, firstTurn]) => ({ title, firstTurn }));
|
|
34
|
+
const timing = view.outputs.find((row) => row.selected &&
|
|
35
|
+
row.available &&
|
|
36
|
+
row.state === "ready" &&
|
|
37
|
+
row.workKey === "subtitles:timing" &&
|
|
38
|
+
row.output.role === "subtitle_words");
|
|
39
|
+
if (timing === undefined)
|
|
40
|
+
throw new Error("The narration's word timing, which the chapter markers come from, is missing. Use More → Render the video again in the Video section, then Try again.");
|
|
41
|
+
const { words } = wordsSchema.parse(JSON.parse(readFileSync(outputPath(deps.paths, context.work.projectId, timing.output.path), "utf8")));
|
|
42
|
+
const audio = await revisionAudio(deps, context, view, { levelled: true });
|
|
43
|
+
if (audio.length === 0)
|
|
44
|
+
throw new Error("The MP3 and M4B files need narration audio, but this project has none. Turn narration on in Edit project, or turn Audio files off under Speakers (Play → Audio, or Edit project → Providers), then Try again.");
|
|
45
|
+
const totalSeconds = audio.reduce((sum, segment) => sum + segment.seconds, 0);
|
|
46
|
+
const chapters = audioChapters({
|
|
47
|
+
title: view.revision.config.title,
|
|
48
|
+
sections,
|
|
49
|
+
turnStarts: turnStarts(words),
|
|
50
|
+
totalSeconds,
|
|
51
|
+
});
|
|
52
|
+
const directory = mkdtempSync(join(projectDir(deps.paths, context.work.projectId), "audio-"));
|
|
53
|
+
const pending = [
|
|
54
|
+
allocateAsset(deps, context.work.projectId, "narration.mp3"),
|
|
55
|
+
allocateAsset(deps, context.work.projectId, "audiobook.m4b"),
|
|
56
|
+
];
|
|
57
|
+
const prepared = [];
|
|
58
|
+
try {
|
|
59
|
+
const metadata = join(directory, "chapters.txt");
|
|
60
|
+
writeFileSync(metadata, ffmetadata(view.revision.config.title, chapters), { mode: 0o600 });
|
|
61
|
+
// Level the volume: the sound is mixed once and mastered to the audio files' target, and both
|
|
62
|
+
// files are encoded from that (`loudness/model.ts`).
|
|
63
|
+
const goal = masterGoal(view.revision.config, "audioFiles");
|
|
64
|
+
let sound = audio;
|
|
65
|
+
if (goal !== undefined) {
|
|
66
|
+
const mixed = join(directory, "mix.wav");
|
|
67
|
+
await runFfmpeg({
|
|
68
|
+
bin: deps.ffmpeg,
|
|
69
|
+
args: audioExportArgs(audio, mixed),
|
|
70
|
+
signal: context.signal,
|
|
71
|
+
log: deps.log,
|
|
72
|
+
onProgress: () => { },
|
|
73
|
+
});
|
|
74
|
+
const mastered = join(directory, "master.wav");
|
|
75
|
+
await masterFile({ bin: deps.ffmpeg, log: deps.log, signal: context.signal }, mixed, mastered, goal, { sampleRate: 44100, channels: 2 });
|
|
76
|
+
sound = [{ path: mastered, seconds: totalSeconds }];
|
|
77
|
+
}
|
|
78
|
+
for (const [at, kind] of ["mp3", "m4b"].entries()) {
|
|
79
|
+
const target = pending[at];
|
|
80
|
+
if (target === undefined)
|
|
81
|
+
continue;
|
|
82
|
+
await runFfmpeg({
|
|
83
|
+
bin: deps.ffmpeg,
|
|
84
|
+
args: audioFileArgs(sound, metadata, target.absolutePath, kind),
|
|
85
|
+
signal: context.signal,
|
|
86
|
+
log: deps.log,
|
|
87
|
+
onProgress: () => { },
|
|
88
|
+
});
|
|
89
|
+
context.signal.throwIfAborted();
|
|
90
|
+
// What each finished file measures, after its encode.
|
|
91
|
+
const master = goal === undefined
|
|
92
|
+
? undefined
|
|
93
|
+
: await masterReport({ bin: deps.ffmpeg, log: deps.log, signal: context.signal }, target.absolutePath, goal);
|
|
94
|
+
prepared.push(preparedResult(deps, context, piece, kind === "mp3" ? "audio_mp3" : "audio_m4b", sealAsset(deps, target), Math.round(totalSeconds * 1000), master === undefined ? {} : { master }));
|
|
95
|
+
}
|
|
96
|
+
await publishResult(deps, context, piece, prepared, { chapters });
|
|
97
|
+
return "done";
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
discardPreparedAssets(deps, [...pending, ...prepared.map((one) => one.asset)]);
|
|
101
|
+
rmSync(directory, { recursive: true, force: true });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
+
import { withLanguage } from "../../kernel/ports/languages.js";
|
|
2
3
|
import { outputPath } from "../storage/layout.js";
|
|
3
4
|
import { assembleDescription, checkDescriptionAnswer, descriptionMessages, tagsText, } from "../youtube/answer.js";
|
|
4
5
|
import { defaultDescriptionPrompt } from "../youtube/model.js";
|
|
@@ -21,35 +22,35 @@ export async function executeYoutubeRecipe(deps, context, providers, piece) {
|
|
|
21
22
|
context.signal.throwIfAborted();
|
|
22
23
|
const input = piece.input;
|
|
23
24
|
if (input.kind !== "local" || input.operation !== "youtube-description-v1")
|
|
24
|
-
throw new Error("Slopify hit an internal error (the YouTube description step was set up wrongly).
|
|
25
|
+
throw new Error("Slopify hit an internal error (the YouTube description step was set up wrongly). Try again; if it happens again, use Download diagnostics in Settings and report it.");
|
|
25
26
|
const { view } = exportSnapshot(deps, context, piece);
|
|
26
27
|
const config = view.revision.config;
|
|
27
28
|
const llm = config.llm;
|
|
28
29
|
if (llm === undefined || llm.provider.trim() === "" || llm.model.trim() === "")
|
|
29
|
-
throw new Error("The YouTube description needs an AI text model, and none is chosen. Choose one in Edit project → Providers, then
|
|
30
|
+
throw new Error("The YouTube description needs an AI text model, and none is chosen. Choose one in Edit project → Providers, then Try again.");
|
|
30
31
|
const timing = view.outputs.find((row) => row.selected &&
|
|
31
32
|
row.available &&
|
|
32
33
|
row.state === "ready" &&
|
|
33
34
|
row.workKey === "subtitles:timing" &&
|
|
34
35
|
row.output.role === "subtitle_words");
|
|
35
36
|
if (timing === undefined)
|
|
36
|
-
throw new Error("The narration's word timing, which the YouTube description's chapters come from, is missing. Use
|
|
37
|
+
throw new Error("The narration's word timing, which the YouTube description's chapters come from, is missing. Use More → Render the video again in the Video section, then Try again.");
|
|
37
38
|
const { words } = wordsSchema.parse(JSON.parse(readFileSync(outputPath(deps.paths, context.work.projectId, timing.output.path), "utf8")));
|
|
38
39
|
const passages = transcriptPassages(words);
|
|
39
40
|
if (passages.length === 0)
|
|
40
|
-
throw new Error("The narration's word timing holds no words, so there is nothing to write the YouTube description from. Use
|
|
41
|
+
throw new Error("The narration's word timing holds no words, so there is nothing to write the YouTube description from. Use More → Render the video again in the Video section, then Try again.");
|
|
41
42
|
// The same timeline the timing walked: silence at the start, intro, gaps, body, outro and
|
|
42
43
|
// silence at the end.
|
|
43
44
|
const audio = await revisionAudio(deps, context, view);
|
|
44
45
|
const durationSeconds = audio.reduce((sum, segment) => sum + segment.seconds, 0);
|
|
45
46
|
const saved = Array.isArray(input.values) ? input.values[4] : undefined;
|
|
46
47
|
const instruction = typeof saved === "string" && saved.trim() !== "" ? saved : defaultDescriptionPrompt;
|
|
47
|
-
const messages = descriptionMessages({
|
|
48
|
+
const messages = withLanguage(descriptionMessages({
|
|
48
49
|
instruction,
|
|
49
50
|
title: config.title,
|
|
50
51
|
durationSeconds,
|
|
51
52
|
transcript: transcriptText(passages),
|
|
52
|
-
});
|
|
53
|
+
}), config.language);
|
|
53
54
|
const row = deps.db
|
|
54
55
|
.prepare("SELECT recipe_context FROM revision_work WHERE id=?")
|
|
55
56
|
.get(context.work.workId);
|