@gentbajko/slopify 2.5.0 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -21
- package/NOTICE +12 -0
- package/README.md +24 -41
- package/SUBTITLES.md +1 -1
- package/dist/adapter-registry.js +31 -0
- package/dist/adapters/alignment/cache.js +4 -1
- package/dist/adapters/alignment/ctc.js +25 -23
- package/dist/adapters/alignment/index.js +37 -6
- package/dist/adapters/alignment/multilingual.js +267 -0
- package/dist/adapters/alignment/protocol.js +11 -0
- package/dist/adapters/alignment/quality.js +4 -3
- package/dist/adapters/alignment/sentences.js +95 -0
- package/dist/adapters/alignment/spec.js +24 -0
- package/dist/adapters/alignment/spell.js +769 -0
- package/dist/adapters/alignment/text.js +56 -9
- package/dist/adapters/alignment/window.js +34 -30
- package/dist/adapters/alignment/worker.js +23 -21
- package/dist/adapters/explain.js +11 -11
- package/dist/adapters/host-cli/index.js +44 -5
- package/dist/adapters/host-cli/transport.js +7 -5
- package/dist/adapters/image/bytes.js +2 -2
- package/dist/adapters/image/codex-output.js +1 -1
- package/dist/adapters/image/codex.js +87 -22
- package/dist/adapters/image/fal.js +13 -5
- package/dist/adapters/image/google.js +11 -8
- package/dist/adapters/image/openai.js +14 -7
- package/dist/adapters/image/reference.js +34 -0
- package/dist/adapters/image/replicate.js +7 -2
- package/dist/adapters/image/video.js +2 -2
- package/dist/adapters/key-probes.js +85 -0
- package/dist/adapters/llm/claude-code.js +168 -32
- package/dist/adapters/llm/cli-login-error.js +1 -1
- package/dist/adapters/llm/codex-limits.js +157 -0
- package/dist/adapters/llm/codex.js +99 -12
- package/dist/adapters/llm/document-workspace.js +2 -2
- package/dist/adapters/llm/gemini-workspace.js +1 -1
- package/dist/adapters/llm/gemini.js +16 -3
- package/dist/adapters/llm/image-workspace.js +58 -0
- package/dist/adapters/llm/openrouter.js +11 -3
- package/dist/adapters/llm/run-cli.js +12 -7
- package/dist/adapters/tts/cartesia.js +10 -0
- package/dist/adapters/tts/elevenlabs.js +31 -9
- package/dist/adapters/tts/gemini.js +206 -0
- package/dist/adapters/tts/inworld-async.js +9 -5
- package/dist/adapters/tts/inworld.js +17 -3
- package/dist/adapters/tts/openai.js +3 -0
- package/dist/adapters/tts/pcm-mp3.js +62 -0
- package/dist/adapters/tts/system.js +147 -0
- package/dist/adapters/tts/voice-languages.js +80 -0
- package/dist/assets/fonts/noto/NotoSans-Regular.ttf +0 -0
- package/dist/assets/fonts/noto/NotoSansArabic-Regular.ttf +0 -0
- package/dist/assets/fonts/noto/NotoSansDevanagari-Regular.ttf +0 -0
- package/dist/assets/fonts/noto/NotoSansHebrew-Regular.ttf +0 -0
- package/dist/assets/fonts/noto/NotoSansThai-Regular.ttf +0 -0
- package/dist/assets/fonts/noto/OFL.txt +93 -0
- package/dist/assets/fonts/noto/SOURCE.txt +18 -0
- package/dist/assets/models.yaml +38 -0
- package/dist/assets/sample/sample-audiobook.tar +0 -0
- package/dist/assets/sample/sample-podcast.tar +0 -0
- package/dist/assets/sample/sample-project.tar +0 -0
- package/dist/assets/style-preview/narration.mp3 +0 -0
- package/dist/assets/style-preview/tall-1.jpg +0 -0
- package/dist/assets/style-preview/tall-2.jpg +0 -0
- package/dist/assets/style-preview/tall-3.jpg +0 -0
- package/dist/assets/style-preview/wide-1.jpg +0 -0
- package/dist/assets/style-preview/wide-2.jpg +0 -0
- package/dist/assets/style-preview/wide-3.jpg +0 -0
- package/dist/catalog/merge.js +132 -0
- package/dist/catalog/registry.js +17 -14
- package/dist/catalog/schema.js +2 -0
- package/dist/catalog/store.js +147 -8
- package/dist/catalog/validate.js +2 -2
- package/dist/compose.yaml +71 -0
- package/dist/edge/autostart/docker-record.js +32 -0
- package/dist/edge/autostart/docker.js +118 -0
- package/dist/edge/autostart/index.js +78 -0
- package/dist/edge/autostart/launcher.js +176 -0
- package/dist/edge/autostart/model.js +3 -0
- package/dist/edge/autostart/native.js +210 -0
- package/dist/edge/autostart/prompt.js +72 -0
- package/dist/edge/autostart/service.js +90 -0
- package/dist/edge/cli-args.js +71 -0
- package/dist/edge/cli.js +64 -63
- package/dist/edge/docker-install/activation.js +56 -0
- package/dist/edge/docker-install/apply.js +458 -0
- package/dist/edge/docker-install/engine.js +362 -0
- package/dist/edge/docker-install/run.js +160 -0
- package/dist/edge/docker-install/state.js +146 -0
- package/dist/edge/{docker-projects → docker-install}/volume.js +1 -5
- package/dist/edge/docker.js +14 -22
- package/dist/edge/events/hub.js +3 -1
- package/dist/edge/host-cli.js +1 -1
- package/dist/edge/http/actions.js +38 -11
- package/dist/edge/http/app.js +60 -2
- package/dist/edge/http/auditions.js +70 -0
- package/dist/edge/http/autostart.js +37 -0
- package/dist/edge/http/byte-range.js +42 -0
- package/dist/edge/http/channel-memory.js +92 -0
- package/dist/edge/http/channels.js +128 -0
- package/dist/edge/http/diagnostics.js +1 -0
- package/dist/edge/http/draft-files.js +2 -4
- package/dist/edge/http/entries.js +22 -3
- package/dist/edge/http/files.js +11 -7
- package/dist/edge/http/folder-location.js +1 -1
- package/dist/edge/http/home.js +39 -0
- package/dist/edge/http/host-cli.js +29 -5
- package/dist/edge/http/narration-peaks.js +76 -0
- package/dist/edge/http/onboarding.js +212 -0
- package/dist/edge/http/open-folder.js +1 -1
- package/dist/edge/http/patch-notes.js +45 -0
- package/dist/edge/http/planning.js +4 -2
- package/dist/edge/http/project-create.js +87 -0
- package/dist/edge/http/project-templates.js +19 -2
- package/dist/edge/http/projects.js +37 -100
- package/dist/edge/http/prompts.js +40 -3
- package/dist/edge/http/pronunciations.js +23 -4
- package/dist/edge/http/providers.js +119 -3
- package/dist/edge/http/reviews.js +131 -0
- package/dist/edge/http/revision-files.js +3 -5
- package/dist/edge/http/revisions.js +1 -1
- package/dist/edge/http/run-cost.js +25 -0
- package/dist/edge/http/schedules.js +115 -6
- package/dist/edge/http/settings.js +84 -6
- package/dist/edge/http/storage-files.js +50 -0
- package/dist/edge/http/storage.js +43 -2
- package/dist/edge/http/studio.js +287 -0
- package/dist/edge/http/style-preview.js +61 -0
- package/dist/edge/http/subtitles.js +1 -1
- package/dist/edge/http/trash.js +69 -0
- package/dist/edge/http/tutorials.js +59 -0
- package/dist/edge/http/update.js +24 -3
- package/dist/edge/http/whats-new.js +15 -0
- package/dist/edge/http/youtube-edits.js +92 -0
- package/dist/edge/native-update.js +62 -0
- package/dist/extension/slopify-studio-chrome.zip +0 -0
- package/dist/extension/slopify-studio-firefox.zip +0 -0
- package/dist/host-cli/gemini-login.js +83 -0
- package/dist/host-cli/open-folder.js +10 -10
- package/dist/host-cli/runtime.js +9 -1
- package/dist/host-cli/status.js +4 -2
- package/dist/kernel/db/migrations/0025-automatic-reviews.sql +40 -0
- package/dist/kernel/db/migrations/0026-schedule-topic-generation.sql +23 -0
- package/dist/kernel/db/migrations/0027-run-cost.sql +58 -0
- package/dist/kernel/db/migrations/0030-automatic-retries.sql +18 -0
- package/dist/kernel/db/migrations/0031-prompt-history-and-description-edits.sql +33 -0
- package/dist/kernel/db/migrations/0032-channels.sql +65 -0
- package/dist/kernel/db/migrations/0034-script-prompts.sql +23 -0
- package/dist/kernel/db/migrations/0035-narration-aliases.sql +13 -0
- package/dist/kernel/db/migrations/0037-project-uploads.sql +8 -0
- package/dist/kernel/db/migrations/0038-voice-languages.sql +7 -0
- package/dist/kernel/db/migrations/0039-channel-essentials.sql +64 -0
- package/dist/kernel/db/migrations/0040-standalone-usage.sql +35 -0
- package/dist/kernel/db/migrations/0041-cast-hosts.sql +3 -0
- package/dist/kernel/paths.js +27 -6
- package/dist/kernel/pipeline.js +4 -1
- package/dist/kernel/ports/host-cli.js +122 -14
- package/dist/kernel/ports/key-probe.js +1 -0
- package/dist/kernel/ports/languages.js +157 -0
- package/dist/kernel/ports/model.js +10 -5
- package/dist/kernel/ports/narration-aliases.js +60 -0
- package/dist/kernel/ports/plan-limits.js +36 -0
- package/dist/kernel/ports/system-speech.js +203 -0
- package/dist/kernel/runner/attempt.js +17 -8
- package/dist/kernel/runner/audition.js +36 -0
- package/dist/kernel/runner/graph.js +40 -5
- package/dist/kernel/runner/index.js +39 -9
- package/dist/kernel/runner/meter.js +1 -0
- package/dist/kernel/runner/providers.js +142 -7
- package/dist/kernel/runner/retry-policy.js +43 -0
- package/dist/kernel/runner/standalone.js +134 -0
- package/dist/kernel/runner/work-authority.js +28 -2
- package/dist/main.js +277 -16
- package/dist/model-catalog.js +2 -0
- package/dist/patch-notes/0.4.2-to-1.0.md +93 -0
- package/dist/patch-notes/1.0-to-2.0.md +74 -0
- package/dist/patch-notes/2.0-to-3.0.md +257 -0
- package/dist/patch-notes/3.0.0.md +70 -0
- package/dist/patch-notes/3.0.1.md +105 -0
- package/dist/patch-notes/index.json +32 -0
- package/dist/slices/admission/model.js +27 -0
- package/dist/slices/admission/repo.js +24 -4
- package/dist/slices/admission/rules.js +80 -2
- package/dist/slices/admission/schema.js +58 -1
- package/dist/slices/admission/short-mode.js +47 -0
- package/dist/slices/admission/start.js +42 -6
- package/dist/slices/article/continuation.js +2 -2
- package/dist/slices/article/provided-entries.js +2 -2
- package/dist/slices/article/run.js +6 -2
- package/dist/slices/article/segments.js +3 -3
- package/dist/slices/backups/folder.js +14 -6
- package/dist/slices/batch/index.js +7 -2
- package/dist/slices/cancel/index.js +1 -1
- package/dist/slices/channels/cast-images.js +110 -0
- package/dist/slices/channels/cast-match.js +32 -0
- package/dist/slices/channels/images.js +19 -0
- package/dist/slices/channels/model.js +5 -0
- package/dist/slices/channels/rebrand.js +100 -0
- package/dist/slices/channels/repo.js +221 -0
- package/dist/slices/channels/runs.js +129 -0
- package/dist/slices/channels/schema.js +152 -0
- package/dist/slices/channels/service.js +180 -0
- package/dist/slices/channels/studio-csv.js +110 -0
- package/dist/slices/channels/videos.js +120 -0
- package/dist/slices/checkpoints/recovery.js +10 -0
- package/dist/slices/control/index.js +5 -2
- package/dist/slices/control/providers.js +4 -1
- package/dist/slices/document/fonts.js +1 -1
- package/dist/slices/document/render.js +1 -1
- package/dist/slices/episodes/related.js +90 -0
- package/dist/slices/episodes/repo.js +60 -0
- package/dist/slices/episodes/service.js +58 -0
- package/dist/slices/episodes/summarize.js +170 -0
- package/dist/slices/estimate/index.js +109 -28
- package/dist/slices/estimate/requests.js +68 -2
- package/dist/slices/eta/history.js +56 -0
- package/dist/slices/eta/model.js +63 -0
- package/dist/slices/eta/view.js +25 -0
- package/dist/slices/fixes/rules.js +73 -0
- package/dist/slices/fonts/coverage.js +82 -0
- package/dist/slices/fonts/sfnt.js +74 -0
- package/dist/slices/images/run.js +8 -5
- package/dist/slices/images/scale.js +84 -0
- package/dist/slices/library/diff.js +122 -0
- package/dist/slices/library/history.js +100 -0
- package/dist/slices/library/model.js +3 -0
- package/dist/slices/library/photorealistic.js +49 -0
- package/dist/slices/library/repo.js +19 -14
- package/dist/slices/library/save.js +62 -12
- package/dist/slices/library/slots.js +7 -1
- package/dist/slices/library/used-by.js +153 -0
- package/dist/slices/loudness/level-pieces.js +41 -0
- package/dist/slices/loudness/loudnorm.js +164 -0
- package/dist/slices/loudness/model.js +153 -0
- package/dist/slices/model-upkeep/model.js +10 -0
- package/dist/slices/model-upkeep/switch.js +217 -0
- package/dist/slices/model-upkeep/usage.js +185 -0
- package/dist/slices/narration/aliases-library.js +31 -0
- package/dist/slices/narration/aliases-schema.js +53 -0
- package/dist/slices/narration/aliases.js +49 -0
- package/dist/slices/narration/blocks.js +352 -0
- package/dist/slices/narration/chunk.js +19 -7
- package/dist/slices/narration/concat.js +2 -2
- package/dist/slices/narration/describe.js +45 -0
- package/dist/slices/narration/pauses-model.js +63 -0
- package/dist/slices/narration/pauses.js +269 -0
- package/dist/slices/narration/peaks-model.js +1 -0
- package/dist/slices/narration/peaks.js +94 -0
- package/dist/slices/narration/preparation.js +21 -3
- package/dist/slices/narration/pronunciation-chunks.js +3 -3
- package/dist/slices/narration/pronunciation.js +72 -25
- package/dist/slices/narration/run.js +6 -6
- package/dist/slices/narration/shared-glossary.js +1 -1
- package/dist/slices/narration/steering.js +15 -4
- package/dist/slices/notifications/notifier.js +47 -3
- package/dist/slices/notifications/rules.js +48 -3
- package/dist/slices/onboarding/copy.js +143 -0
- package/dist/slices/onboarding/first-run.js +72 -0
- package/dist/slices/onboarding/full-video.js +81 -0
- package/dist/slices/onboarding/install.js +119 -0
- package/dist/slices/onboarding/model.js +85 -0
- package/dist/slices/onboarding/packs.js +491 -0
- package/dist/slices/onboarding/quick-short.js +144 -0
- package/dist/slices/onboarding/sample.js +132 -0
- package/dist/slices/onboarding/state.js +68 -0
- package/dist/slices/onboarding/template.js +73 -0
- package/dist/slices/patch-notes/library.js +73 -0
- package/dist/slices/patch-notes/seen.js +77 -0
- package/dist/slices/play-drafts/convert.js +127 -8
- package/dist/slices/play-drafts/readiness.js +4 -1
- package/dist/slices/play-drafts/repo.js +2 -0
- package/dist/slices/play-drafts/review-inputs.js +20 -3
- package/dist/slices/play-drafts/schema.js +89 -0
- package/dist/slices/play-drafts/service.js +4 -1
- package/dist/slices/play-drafts/start.js +43 -18
- package/dist/slices/project-templates/from-project.js +59 -6
- package/dist/slices/project-templates/next-chapter.js +94 -0
- package/dist/slices/project-templates/one-off.js +26 -0
- package/dist/slices/project-templates/repo.js +2 -1
- package/dist/slices/project-templates/schema.js +7 -1
- package/dist/slices/project-templates/service.js +15 -4
- package/dist/slices/project-templates/setup.js +18 -2
- package/dist/slices/rebuild/admission-repo.js +1 -1
- package/dist/slices/rebuild/model.js +6 -0
- package/dist/slices/rebuild/preview-details.js +46 -3
- package/dist/slices/rebuild/preview-plan.js +13 -6
- package/dist/slices/rebuild/recipe-audio-parts.js +40 -7
- package/dist/slices/rebuild/recipe-audio.js +66 -12
- package/dist/slices/rebuild/recipe-build.js +8 -4
- package/dist/slices/rebuild/recipe-cast.js +18 -0
- package/dist/slices/rebuild/recipe-checkpoints.js +11 -9
- package/dist/slices/rebuild/recipe-describe.js +93 -0
- package/dist/slices/rebuild/recipe-edit.js +13 -1
- package/dist/slices/rebuild/recipe-exports.js +67 -8
- package/dist/slices/rebuild/recipe-input-schema.js +35 -0
- package/dist/slices/rebuild/recipe-legacy.js +9 -0
- package/dist/slices/rebuild/recipe-loudness.js +45 -0
- package/dist/slices/rebuild/recipe-model.js +25 -6
- package/dist/slices/rebuild/recipe-pauses.js +7 -0
- package/dist/slices/rebuild/recipe-preparation.js +3 -3
- package/dist/slices/rebuild/recipe-provider-choice.js +11 -1
- package/dist/slices/rebuild/recipe-reference.js +15 -1
- package/dist/slices/rebuild/recipe-reviews.js +117 -0
- package/dist/slices/rebuild/recipe-save.js +4 -1
- package/dist/slices/rebuild/recipe-shorts.js +23 -3
- package/dist/slices/rebuild/recipe-text.js +114 -19
- package/dist/slices/rebuild/recipe-validation.js +14 -2
- package/dist/slices/rebuild/recipe-visual.js +96 -33
- package/dist/slices/rebuild/recipe-voices.js +286 -0
- package/dist/slices/rebuild/recipe-work.js +77 -6
- package/dist/slices/rebuild/recipe-youtube.js +2 -0
- package/dist/slices/rebuild/recovery-conflict.js +7 -3
- package/dist/slices/rebuild/recovery-model.js +3 -0
- package/dist/slices/rebuild/recovery-selection.js +28 -2
- package/dist/slices/rebuild/recovery.js +35 -17
- package/dist/slices/rebuild/repo.js +3 -3
- package/dist/slices/rebuild/retry.js +18 -0
- package/dist/slices/rebuild/review-redo.js +125 -0
- package/dist/slices/rebuild/runtime-actions.js +5 -0
- package/dist/slices/rebuild/runtime-admission.js +4 -1
- package/dist/slices/rebuild/runtime-animate.js +4 -4
- package/dist/slices/rebuild/runtime-article.js +5 -5
- package/dist/slices/rebuild/runtime-document.js +4 -4
- package/dist/slices/rebuild/runtime-export-bed.js +43 -0
- package/dist/slices/rebuild/runtime-export-edit.js +70 -11
- package/dist/slices/rebuild/runtime-export-inputs.js +52 -10
- package/dist/slices/rebuild/runtime-export-short.js +85 -0
- package/dist/slices/rebuild/runtime-export.js +84 -19
- package/dist/slices/rebuild/runtime-figure-card.js +87 -0
- package/dist/slices/rebuild/runtime-image.js +21 -2
- package/dist/slices/rebuild/runtime-local.js +148 -24
- package/dist/slices/rebuild/runtime-materialize.js +8 -0
- package/dist/slices/rebuild/runtime-narration-reuse.js +2 -1
- package/dist/slices/rebuild/runtime-narration-text.js +7 -7
- package/dist/slices/rebuild/runtime-pictures.js +28 -0
- package/dist/slices/rebuild/runtime-piece-label.js +27 -0
- package/dist/slices/rebuild/runtime-provider.js +126 -18
- package/dist/slices/rebuild/runtime-publication.js +21 -5
- package/dist/slices/rebuild/runtime-review.js +267 -0
- package/dist/slices/rebuild/runtime-run.js +17 -11
- package/dist/slices/rebuild/runtime-shorts.js +44 -17
- package/dist/slices/rebuild/runtime-store.js +62 -11
- package/dist/slices/rebuild/runtime-subtitles.js +72 -19
- package/dist/slices/rebuild/runtime-voices.js +103 -0
- package/dist/slices/rebuild/runtime-youtube.js +7 -6
- package/dist/slices/rebuild/service-readiness.js +3 -3
- package/dist/slices/rebuild/soften.js +49 -0
- package/dist/slices/rebuild/transition-repo.js +9 -2
- package/dist/slices/research/run.js +4 -4
- package/dist/slices/research/synthesis.js +2 -2
- package/dist/slices/reviews/model.js +105 -0
- package/dist/slices/reviews/outcome.js +22 -0
- package/dist/slices/reviews/repo.js +118 -0
- package/dist/slices/reviews/rules.js +63 -0
- package/dist/slices/reviews/verdict.js +71 -0
- package/dist/slices/reviews/view.js +30 -0
- package/dist/slices/revisions/adopt-content.js +5 -2
- package/dist/slices/revisions/adopt.js +3 -1
- package/dist/slices/revisions/downloads.js +7 -1
- package/dist/slices/revisions/image-plan.js +183 -0
- package/dist/slices/revisions/mutation-assets.js +10 -0
- package/dist/slices/revisions/mutation-cues.js +71 -0
- package/dist/slices/revisions/mutation-request.js +2 -1
- package/dist/slices/revisions/mutations.js +97 -3
- package/dist/slices/revisions/schema.js +11 -1
- package/dist/slices/run-cost/limits.js +128 -0
- package/dist/slices/run-cost/meter.js +47 -0
- package/dist/slices/run-cost/panel.js +256 -0
- package/dist/slices/run-cost/pricing.js +95 -0
- package/dist/slices/run-cost/week.js +108 -0
- package/dist/slices/schedules/agenda.js +222 -0
- package/dist/slices/schedules/repo.js +58 -18
- package/dist/slices/schedules/scheduler.js +68 -17
- package/dist/slices/schedules/schema.js +179 -1
- package/dist/slices/schedules/service.js +43 -4
- package/dist/slices/schedules/similar.js +128 -0
- package/dist/slices/schedules/topic-list.js +163 -0
- package/dist/slices/schedules/topics.js +523 -0
- package/dist/slices/settings/first-run.js +106 -0
- package/dist/slices/settings/health.js +247 -0
- package/dist/slices/settings/key-guides.js +138 -0
- package/dist/slices/settings/key-test.js +93 -0
- package/dist/slices/settings/keys.js +10 -5
- package/dist/slices/settings/model-reach.js +52 -0
- package/dist/slices/settings/model.js +55 -1
- package/dist/slices/settings/models.js +5 -1
- package/dist/slices/settings/playback.js +15 -0
- package/dist/slices/settings/readiness.js +14 -2
- package/dist/slices/settings/repo.js +17 -3
- package/dist/slices/settings/tutorial-schema.js +5 -0
- package/dist/slices/settings/voices.js +64 -4
- package/dist/slices/settings/whats-new.js +64 -0
- package/dist/slices/shorts/captions.js +9 -0
- package/dist/slices/shorts/pick.js +3 -3
- package/dist/slices/shorts/prompts.js +1 -1
- package/dist/slices/shorts/render.js +63 -13
- package/dist/slices/storage/asset-name.js +3 -0
- package/dist/slices/storage/backup-export.js +48 -9
- package/dist/slices/storage/backup-format.js +38 -1
- package/dist/slices/storage/backup-import.js +191 -19
- package/dist/slices/storage/delete-project.js +7 -4
- package/dist/slices/storage/documents.js +87 -0
- package/dist/slices/storage/downloads.js +33 -1
- package/dist/slices/storage/files-location.js +531 -0
- package/dist/slices/storage/layout.js +14 -4
- package/dist/slices/storage/model.js +11 -0
- package/dist/slices/storage/portable.js +94 -12
- package/dist/slices/storage/reconcile.js +14 -1
- package/dist/slices/storage/repo.js +4 -1
- package/dist/slices/storage/schema.js +11 -0
- package/dist/slices/storage/trim.js +146 -0
- package/dist/slices/studio/disclosure.js +74 -0
- package/dist/slices/studio/model.js +30 -0
- package/dist/slices/studio/pack.js +211 -0
- package/dist/slices/studio/queue.js +69 -0
- package/dist/slices/studio/settings.js +170 -0
- package/dist/slices/style-preview/images.js +61 -0
- package/dist/slices/style-preview/narration.js +33 -0
- package/dist/slices/style-preview/render.js +148 -0
- package/dist/slices/style-preview/schema.js +59 -0
- package/dist/slices/style-preview/service.js +92 -0
- package/dist/slices/style-preview/settings.js +139 -0
- package/dist/slices/subtitles/captions.js +42 -9
- package/dist/slices/subtitles/model.js +22 -0
- package/dist/slices/subtitles/prepare.js +27 -16
- package/dist/slices/subtitles/transcript.js +1 -1
- package/dist/slices/thumbnail/run.js +7 -7
- package/dist/slices/trash/model.js +25 -0
- package/dist/slices/trash/service.js +273 -0
- package/dist/slices/tutorials/anchors.js +57 -0
- package/dist/slices/tutorials/library.js +185 -0
- package/dist/slices/uploads/repo.js +20 -0
- package/dist/slices/video/ambient-bed-schema.js +15 -0
- package/dist/slices/video/ambient-bed.js +95 -0
- package/dist/slices/video/ambient-mix.js +72 -0
- package/dist/slices/video/audio-inputs.js +2 -2
- package/dist/slices/video/cards.js +5 -2
- package/dist/slices/video/edit-list.js +23 -2
- package/dist/slices/video/edit-settings.js +13 -3
- package/dist/slices/video/ffmpeg.js +63 -9
- package/dist/slices/video/figure-card.js +492 -0
- package/dist/slices/video/figure-spans.js +51 -0
- package/dist/slices/video/plan.js +125 -13
- package/dist/slices/video/run.js +14 -12
- package/dist/slices/video/slideshow.js +39 -4
- package/dist/slices/voices/attribution.js +69 -0
- package/dist/slices/voices/audio-files.js +87 -0
- package/dist/slices/voices/audition.js +25 -0
- package/dist/slices/voices/cast.js +74 -0
- package/dist/slices/voices/grouping.js +59 -0
- package/dist/slices/voices/join.js +35 -0
- package/dist/slices/voices/languages.js +25 -0
- package/dist/slices/voices/model.js +218 -0
- package/dist/slices/voices/palette.js +26 -0
- package/dist/slices/voices/panel.js +91 -0
- package/dist/slices/voices/portraits.js +55 -0
- package/dist/slices/voices/script.js +136 -0
- package/dist/slices/voices/timing.js +58 -0
- package/dist/slices/youtube/answer.js +1 -1
- package/dist/slices/youtube/chapters.js +126 -0
- package/dist/slices/youtube/edits-repo.js +84 -0
- package/dist/slices/youtube/edits.js +89 -0
- package/dist/slices/youtube/placeholders.js +98 -0
- package/dist/slices/youtube/transcript.js +11 -4
- package/dist/tutorials/AI-CLIs.md +146 -0
- package/dist/tutorials/Backups.md +121 -0
- package/dist/tutorials/Calendar.md +103 -0
- package/dist/tutorials/Cast-Library.md +112 -0
- package/dist/tutorials/Channels.md +148 -0
- package/dist/tutorials/Costs-and-Run-Cost.md +136 -0
- package/dist/tutorials/Docker.md +171 -0
- package/dist/tutorials/Document-Themes.md +207 -0
- package/dist/tutorials/Editing-a-Project.md +204 -0
- package/dist/tutorials/FAQ.md +134 -0
- package/dist/tutorials/First-Launch-and-Welcome.md +153 -0
- package/dist/tutorials/Home-and-Projects.md +174 -0
- package/dist/tutorials/Home.md +119 -0
- package/dist/tutorials/Install.md +154 -0
- package/dist/tutorials/Intros-and-Outros.md +86 -0
- package/dist/tutorials/Keyboard-Shortcuts-and-Command-Palette.md +215 -0
- package/dist/tutorials/Library-Overview.md +75 -0
- package/dist/tutorials/Models.md +133 -0
- package/dist/tutorials/Multiple-Voices.md +161 -0
- package/dist/tutorials/Narration-Aliases-and-Glossary.md +129 -0
- package/dist/tutorials/Notifications.md +86 -0
- package/dist/tutorials/Other-Languages.md +110 -0
- package/dist/tutorials/PDF-Documents.md +100 -0
- package/dist/tutorials/Play-Images.md +135 -0
- package/dist/tutorials/Play-Narration.md +167 -0
- package/dist/tutorials/Play-Outputs.md +109 -0
- package/dist/tutorials/Play-Overview.md +216 -0
- package/dist/tutorials/Play-Title-and-Article.md +135 -0
- package/dist/tutorials/Play-Video-and-Style.md +143 -0
- package/dist/tutorials/Project-Page.md +228 -0
- package/dist/tutorials/Prompts.md +136 -0
- package/dist/tutorials/Providers-and-Keys.md +290 -0
- package/dist/tutorials/Publishing-to-YouTube.md +135 -0
- package/dist/tutorials/Recovery-and-Retries.md +141 -0
- package/dist/tutorials/Reviews-and-Checkpoints.md +143 -0
- package/dist/tutorials/Schedules.md +154 -0
- package/dist/tutorials/Settings-Reference.md +258 -0
- package/dist/tutorials/Shorts.md +116 -0
- package/dist/tutorials/Start-at-Login.md +126 -0
- package/dist/tutorials/Studio-Extension.md +146 -0
- package/dist/tutorials/Templates.md +146 -0
- package/dist/tutorials/Trash.md +87 -0
- package/dist/tutorials/Troubleshooting.md +149 -0
- package/dist/tutorials/Uninstalling-and-Moving.md +113 -0
- package/dist/tutorials/Updating-and-Patch-Notes.md +120 -0
- package/dist/tutorials/Video-Editing.md +135 -0
- package/dist/tutorials/Where-Your-Files-Live.md +149 -0
- package/dist/tutorials/YouTube-Description.md +132 -0
- package/dist/tutorials/Your-First-Short.md +125 -0
- package/dist/tutorials/_Footer.md +1 -0
- package/dist/tutorials/_Sidebar.md +69 -0
- package/dist/updater/service.js +66 -21
- package/dist/web/assets/index-DBLSfqA-.css +1 -0
- package/dist/web/assets/index-DFdIhmm0.js +307 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
- package/dist/web/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
- package/dist/web/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
- package/dist/web/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
- package/dist/web/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
- package/dist/web/assets/{pdf-BLF2i8c2.js → pdf-B9OY1ioX.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +5 -5
- package/dist/edge/docker-launch.js +0 -81
- package/dist/edge/docker-projects/activation.js +0 -35
- package/dist/edge/docker-projects/claims.js +0 -85
- package/dist/edge/docker-projects/committed.js +0 -40
- package/dist/edge/docker-projects/engine.js +0 -461
- package/dist/edge/docker-projects/install.js +0 -254
- package/dist/edge/docker-projects/prune.js +0 -66
- package/dist/edge/docker-projects/recover.js +0 -100
- package/dist/edge/docker-projects/state.js +0 -225
- package/dist/web/assets/index-CktpxlJd.js +0 -171
- package/dist/web/assets/index-DB-vOnGU.css +0 -1
- package/scripts/docker-run.sh +0 -3
- /package/dist/edge/{docker-projects → docker-install}/tree.js +0 -0
|
@@ -1,12 +1,21 @@
|
|
|
1
|
+
import { thumbnailCountOf, thumbnailKey } from "../admission/model.js";
|
|
2
|
+
import { usesShortMode } from "../admission/short-mode.js";
|
|
1
3
|
import { render } from "../admission/substitute.js";
|
|
4
|
+
import { usesAmbientBed } from "../video/ambient-bed.js";
|
|
5
|
+
import { castFor } from "./recipe-cast.js";
|
|
6
|
+
import { noMaster } from "./recipe-loudness.js";
|
|
2
7
|
import { recipe, resourceIdentity, } from "./recipe-model.js";
|
|
3
|
-
import { imageChoice } from "./recipe-reference.js";
|
|
8
|
+
import { castField, imageChoice } from "./recipe-reference.js";
|
|
4
9
|
import { matchingText, renderedPrompt } from "./recipe-text.js";
|
|
5
10
|
export function visualRecipes(config, content, audioFingerprint, captionFingerprint,
|
|
6
11
|
// What the Video stage's edit settings add (`recipe-edit.ts`), from the images planned here.
|
|
7
12
|
edit,
|
|
8
13
|
// The establishing image the generated images are drawn from, when it is on.
|
|
9
|
-
reference
|
|
14
|
+
reference,
|
|
15
|
+
// The word timing's identity, which a short's captions are drawn from.
|
|
16
|
+
timing = null,
|
|
17
|
+
// Level the volume: the levelled narration and the master (`recipe-loudness.ts`).
|
|
18
|
+
master = noMaster) {
|
|
10
19
|
const recipes = [];
|
|
11
20
|
const imageKeys = config.sources.images === "off" ? [] : content.imageOrder;
|
|
12
21
|
for (const key of imageKeys) {
|
|
@@ -26,10 +35,19 @@ reference) {
|
|
|
26
35
|
aspect: config.format,
|
|
27
36
|
prompt: prompt ?? "",
|
|
28
37
|
...(reference === undefined ? {} : { reference: reference.input }),
|
|
38
|
+
...castField(castFor(config, prompt)),
|
|
29
39
|
}, image.source === "provide" || reference === undefined ? [] : [reference.key], { unresolved: image.source === "provide" ? image.assetId === null : !prompt?.trim() }));
|
|
30
40
|
}
|
|
31
41
|
if (config.sources.video !== "off") {
|
|
32
|
-
const
|
|
42
|
+
const short = usesShortMode(config);
|
|
43
|
+
// A short is rendered by the Shorts renderer, which has no cuts, transitions or Look.
|
|
44
|
+
const edited = (short ? undefined : edit?.([...recipes])) ?? {
|
|
45
|
+
recipes: [],
|
|
46
|
+
values: [],
|
|
47
|
+
dependsOn: [],
|
|
48
|
+
};
|
|
49
|
+
// A short (rendered by the Shorts renderer) never has the bed.
|
|
50
|
+
const bed = short ? undefined : ambientBedValues(config, content);
|
|
33
51
|
const audioKeys = config.sources.audio === "off"
|
|
34
52
|
? []
|
|
35
53
|
: [
|
|
@@ -64,18 +82,48 @@ reference) {
|
|
|
64
82
|
// Only what the edit settings change; nothing at all for today's slideshow, so
|
|
65
83
|
// its fingerprint is the one it always had.
|
|
66
84
|
...(edited.values.length === 0 ? [] : [["video-edit", ...edited.values]]),
|
|
85
|
+
// Only for a short, so every long video keeps its fingerprint: the word-by-word
|
|
86
|
+
// captions are drawn from the word timing in the caption font, under the title.
|
|
87
|
+
...(short
|
|
88
|
+
? [["short-v1", config.subtitles?.fontId ?? "default", config.title, timing]]
|
|
89
|
+
: []),
|
|
90
|
+
// Only while the video has an ambient bed, so every video without one keeps the
|
|
91
|
+
// fingerprint it always had.
|
|
92
|
+
...(bed === undefined ? [] : [bed]),
|
|
93
|
+
// Only while the volume is levelled, so every video without it keeps its fingerprint.
|
|
94
|
+
...master.values,
|
|
67
95
|
],
|
|
68
96
|
}, [
|
|
69
97
|
...recipes.map((value) => value.key),
|
|
70
98
|
...audioKeys,
|
|
99
|
+
...master.keys,
|
|
71
100
|
...(config.subtitles?.mode === "burn-in" ? ["subtitles:files"] : []),
|
|
101
|
+
...(short ? ["subtitles:timing"] : []),
|
|
72
102
|
...edited.dependsOn,
|
|
73
|
-
], {
|
|
103
|
+
], {
|
|
104
|
+
unresolved: imageKeys.length === 0 ||
|
|
105
|
+
(config.ambientBed?.source === "upload" && content.ambientBed === undefined),
|
|
106
|
+
}));
|
|
74
107
|
// After the export, so the images' recipes above stay the only ones its first values list.
|
|
75
108
|
recipes.splice(recipes.length - 1, 0, ...edited.recipes);
|
|
76
109
|
}
|
|
77
110
|
return recipes;
|
|
78
111
|
}
|
|
112
|
+
// What the ambient bed adds to the render's fingerprint: its settings and, for the user's own
|
|
113
|
+
// file, the project asset it plays. Undefined without a bed.
|
|
114
|
+
export function ambientBedValues(config, content) {
|
|
115
|
+
const bed = config.ambientBed;
|
|
116
|
+
if (bed === undefined || !usesAmbientBed(config))
|
|
117
|
+
return undefined;
|
|
118
|
+
return [
|
|
119
|
+
"ambient-bed-v1",
|
|
120
|
+
bed.source,
|
|
121
|
+
bed.levelDb,
|
|
122
|
+
bed.fadeInSeconds,
|
|
123
|
+
bed.tailSeconds,
|
|
124
|
+
bed.source === "upload" ? (content.ambientBed ?? null) : null,
|
|
125
|
+
];
|
|
126
|
+
}
|
|
79
127
|
export function thumbnailRecipes(context, textRecipes,
|
|
80
128
|
// The establishing image, when it is on and the thumbnail is drawn from it too.
|
|
81
129
|
reference) {
|
|
@@ -95,35 +143,50 @@ reference) {
|
|
|
95
143
|
const prompt = config.sources.thumbnail === "prompt_by_llm" && promptRecipe !== undefined
|
|
96
144
|
? matchingText(context, promptRecipe, "prompt")
|
|
97
145
|
: renderedPrompt(context, "thumbnailPrompt");
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
],
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
},
|
|
123
|
-
|
|
124
|
-
...(
|
|
125
|
-
|
|
126
|
-
|
|
146
|
+
const variants = Array.from({ length: thumbnailCountOf(config) }, (_, index) => index + 1);
|
|
147
|
+
return variants.map((variant) => recipe(context, thumbnailKey(variant), "thumbnail", prompt === null
|
|
148
|
+
? {
|
|
149
|
+
kind: "deferred",
|
|
150
|
+
version: 1,
|
|
151
|
+
operation: "thumbnail-image",
|
|
152
|
+
template: [
|
|
153
|
+
promptRecipe?.fingerprint ?? null,
|
|
154
|
+
config.images?.provider ?? null,
|
|
155
|
+
config.images?.model ?? null,
|
|
156
|
+
config.format,
|
|
157
|
+
// Only what is in use, so a thumbnail made before these existed keeps its
|
|
158
|
+
// fingerprint.
|
|
159
|
+
...(config.images?.thinking === undefined ? [] : [config.images.thinking]),
|
|
160
|
+
...(reference === undefined ? [] : [reference.input.fingerprint]),
|
|
161
|
+
...(variant === 1 ? [] : [["thumbnail-variant", variant]]),
|
|
162
|
+
],
|
|
163
|
+
}
|
|
164
|
+
: {
|
|
165
|
+
kind: "image",
|
|
166
|
+
version: 1,
|
|
167
|
+
...imageChoice(config),
|
|
168
|
+
aspect: config.format,
|
|
169
|
+
prompt: thumbnailVariantPrompt(prompt, variant),
|
|
170
|
+
...(reference === undefined ? {} : { reference: reference.input }),
|
|
171
|
+
// The thumbnail stands for the whole video, like the establishing image.
|
|
172
|
+
...castField(castFor(config, config.title, prompt)),
|
|
173
|
+
}, [
|
|
174
|
+
...(promptRecipe === undefined ? [] : [promptRecipe.key]),
|
|
175
|
+
...(reference === undefined ? [] : [reference.key]),
|
|
176
|
+
]));
|
|
177
|
+
}
|
|
178
|
+
// The second and third thumbnails are the same prompt asked for another composition, so the
|
|
179
|
+
// three test one idea against itself rather than three ideas. The first is the prompt as it
|
|
180
|
+
// is, which keeps the thumbnail every project already has.
|
|
181
|
+
const variantFraming = {
|
|
182
|
+
2: "a tight close-up where the main subject fills most of the frame, seen from a different angle",
|
|
183
|
+
3: "a wider shot that shows the main subject in its setting, placed off-centre with room around it",
|
|
184
|
+
};
|
|
185
|
+
export function thumbnailVariantPrompt(prompt, variant) {
|
|
186
|
+
const framing = variantFraming[variant];
|
|
187
|
+
if (framing === undefined)
|
|
188
|
+
return prompt;
|
|
189
|
+
return `${prompt}\n\nThis is thumbnail ${String(variant)} of 3 for a YouTube thumbnail test. Keep the subject, style, colours and any text asked for above, but use a clearly different composition: ${framing}.`;
|
|
127
190
|
}
|
|
128
191
|
export function visualAssets(context, recipes) {
|
|
129
192
|
const images = recipes.filter((value) => value.stage === "images");
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { aliasMatches, applyAliases } from "../../kernel/ports/narration-aliases.js";
|
|
2
|
+
import { splitText } from "../../kernel/ports/text.js";
|
|
3
|
+
import { narrationAliasesOf, usesNarrationPreparation } from "../admission/rules.js";
|
|
4
|
+
import { aliasSpans, withAliasSpans } from "../narration/aliases.js";
|
|
5
|
+
import { normalizeNarrationText } from "../narration/plan.js";
|
|
6
|
+
import { parsePronunciationGlossary, pronunciationSpans, } from "../narration/pronunciation.js";
|
|
7
|
+
import { withSharedGlossary } from "../narration/shared-glossary.js";
|
|
8
|
+
import { prepareRequests } from "../narration/steering.js";
|
|
9
|
+
import { groupTurns } from "../voices/grouping.js";
|
|
10
|
+
import { speakerPace } from "../voices/model.js";
|
|
11
|
+
import { parseScript } from "../voices/script.js";
|
|
12
|
+
import { describedText, describeFuture, describing } from "./recipe-describe.js";
|
|
13
|
+
import { recipe, resourceIdentity, } from "./recipe-model.js";
|
|
14
|
+
import { pauseValues } from "./recipe-pauses.js";
|
|
15
|
+
import { preparationForGroup, preparationFuture, preparationTemplate, } from "./recipe-preparation.js";
|
|
16
|
+
export function voiceBodyRecipes(context, voices, script, glossary) {
|
|
17
|
+
const parts = [];
|
|
18
|
+
const preparations = [];
|
|
19
|
+
const layout = [];
|
|
20
|
+
// Which parts end a turn, for the pauses between sentences (a turn gap of 0 is still a turn).
|
|
21
|
+
const turnEnds = new Set();
|
|
22
|
+
let transcript = script.fingerprint ?? script.text ?? null;
|
|
23
|
+
const describe = describing(context);
|
|
24
|
+
const parsed = script.text === null ? undefined : parseScript(script.text, voices.speakers, describe);
|
|
25
|
+
const prepare = voices.speakers.some((speaker) => preparesTurns(context.config, speaker));
|
|
26
|
+
// The narration still to be worked out: the whole script's while it is being written, or
|
|
27
|
+
// every turn's while a turn's delivery cues are (as a single voice waits for all of its own).
|
|
28
|
+
const future = () => recipe(context, "audio:body:future", "audio", {
|
|
29
|
+
kind: "deferred",
|
|
30
|
+
version: 1,
|
|
31
|
+
operation: "body-narration",
|
|
32
|
+
template: [
|
|
33
|
+
script.fingerprint ?? script.text ?? null,
|
|
34
|
+
speakerValues(voices),
|
|
35
|
+
...(prepare ? [preparationTemplate(context)] : []),
|
|
36
|
+
...(described.length === 0
|
|
37
|
+
? []
|
|
38
|
+
: [["described-v1", described.map((row) => row.fingerprint)]]),
|
|
39
|
+
],
|
|
40
|
+
}, [
|
|
41
|
+
...new Set([
|
|
42
|
+
...script.dependsOn,
|
|
43
|
+
...described.map((row) => row.key),
|
|
44
|
+
...preparations.map((row) => row.key),
|
|
45
|
+
]),
|
|
46
|
+
]);
|
|
47
|
+
let pending = false;
|
|
48
|
+
// A table, figure, equation or code block inside a turn is described within that turn: the
|
|
49
|
+
// turn says the description where the block was, in the same voice.
|
|
50
|
+
const described = [];
|
|
51
|
+
const cards = [];
|
|
52
|
+
let turns = parsed?.ok === true ? parsed.script.turns : [];
|
|
53
|
+
if (describe && script.text === null) {
|
|
54
|
+
const from = script.dependsOn[0];
|
|
55
|
+
if (from !== undefined)
|
|
56
|
+
described.push(describeFuture(context, { key: from, fingerprint: script.fingerprint ?? from }));
|
|
57
|
+
}
|
|
58
|
+
else if (describe && parsed?.ok === true) {
|
|
59
|
+
let offset = 0;
|
|
60
|
+
turns = turns.map((turn) => {
|
|
61
|
+
if (turn.markdown === undefined)
|
|
62
|
+
return turn;
|
|
63
|
+
const said = describedText(context, turn.markdown, script.dependsOn, offset);
|
|
64
|
+
if (said.recipes.length === 0)
|
|
65
|
+
return turn;
|
|
66
|
+
offset += said.recipes.length;
|
|
67
|
+
described.push(...said.recipes);
|
|
68
|
+
cards.push(...said.cards);
|
|
69
|
+
if (said.spoken === null) {
|
|
70
|
+
pending = true;
|
|
71
|
+
return turn;
|
|
72
|
+
}
|
|
73
|
+
return { ...turn, text: said.spoken.trim().replace(/\s*\n\s*/g, " ") };
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
preparations.push(...described);
|
|
77
|
+
if (script.text === null) {
|
|
78
|
+
// Turns not yet known are prepared behind one future step, as a single voice's text is.
|
|
79
|
+
const from = script.dependsOn[0];
|
|
80
|
+
if (prepare && from !== undefined)
|
|
81
|
+
preparations.push(preparationFuture(context, "body", { key: from, fingerprint: script.fingerprint ?? from }));
|
|
82
|
+
parts.push(future());
|
|
83
|
+
}
|
|
84
|
+
else if (parsed !== undefined && !parsed.ok) {
|
|
85
|
+
parts.push(refused(context, "audio:body:turn:1", script.dependsOn, `The script can't be narrated: ${parsed.reason} Fix it in Edit project → Article, then Try again.`));
|
|
86
|
+
}
|
|
87
|
+
else if (pending) {
|
|
88
|
+
parts.push(future());
|
|
89
|
+
}
|
|
90
|
+
else if (parsed !== undefined) {
|
|
91
|
+
transcript = ["voices-transcript-v1", turns.map((turn) => [turn.speaker, turn.text])];
|
|
92
|
+
const aliases = narrationAliasesOf(context.config);
|
|
93
|
+
for (const group of groupTurns(turns, voices.speakers, voices.nativeDialogue, (text) => applyAliases(text, aliases).length)) {
|
|
94
|
+
const first = group.turns[0];
|
|
95
|
+
const speaker = voices.speakers.find((one) => one.id === first?.speaker);
|
|
96
|
+
if (first === undefined || speaker === undefined)
|
|
97
|
+
continue;
|
|
98
|
+
const made = groupParts(context, voices, group.native, group.turns, speaker, glossary, script, preparations);
|
|
99
|
+
if (made.length === 0)
|
|
100
|
+
pending = true;
|
|
101
|
+
made.forEach((part, index) => {
|
|
102
|
+
parts.push(part);
|
|
103
|
+
if (index === made.length - 1)
|
|
104
|
+
turnEnds.add(layout.length);
|
|
105
|
+
layout.push([speakerPace(speaker), index === made.length - 1 ? voices.turnGapSeconds : 0]);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
// No gap after the last turn: the timeline's own gap follows it.
|
|
109
|
+
const last = layout.at(-1);
|
|
110
|
+
if (last !== undefined)
|
|
111
|
+
last[1] = 0;
|
|
112
|
+
if (pending) {
|
|
113
|
+
parts.length = 0;
|
|
114
|
+
layout.length = 0;
|
|
115
|
+
turnEnds.clear();
|
|
116
|
+
parts.push(future());
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const body = recipe(context, "audio:body:concat", "audio", {
|
|
120
|
+
kind: "local",
|
|
121
|
+
version: 1,
|
|
122
|
+
operation: "concat-turns-v1",
|
|
123
|
+
values: [
|
|
124
|
+
parts.map((part) => resourceIdentity(context, part)),
|
|
125
|
+
layout,
|
|
126
|
+
// Pauses between sentences inside a turn; the turn gap stays the layout's.
|
|
127
|
+
...pauseValues(context.config, layout.map(([, gap], at) => at === layout.length - 1 ? "end" : gap > 0 || turnEnds.has(at) ? "turn" : "sentence")),
|
|
128
|
+
],
|
|
129
|
+
}, parts.map((part) => part.key), { unresolved: parsed?.ok !== true || pending });
|
|
130
|
+
return {
|
|
131
|
+
preparations,
|
|
132
|
+
cards,
|
|
133
|
+
parts,
|
|
134
|
+
body,
|
|
135
|
+
transcript,
|
|
136
|
+
...(parsed?.ok === true ? { sections: parsed.script.sections } : {}),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
function groupParts(context, voices, native, turns, speaker, glossary, script, preparations) {
|
|
140
|
+
const first = turns[0];
|
|
141
|
+
if (first === undefined)
|
|
142
|
+
return [];
|
|
143
|
+
const logicalKey = `audio:body:turn:${String(first.index)}`;
|
|
144
|
+
const override = context.content.narrationOverrides[logicalKey];
|
|
145
|
+
if (override?.kind === "asset")
|
|
146
|
+
return [
|
|
147
|
+
recipe(context, `${logicalKey}:1`, "audio", {
|
|
148
|
+
kind: "provided",
|
|
149
|
+
version: 1,
|
|
150
|
+
assetId: override.assetId,
|
|
151
|
+
semantic: [turns.map((turn) => turn.text).join("\n"), speaker.id],
|
|
152
|
+
}, script.dependsOn, { tokenKey: logicalKey }),
|
|
153
|
+
];
|
|
154
|
+
if (native) {
|
|
155
|
+
const byId = new Map(voices.speakers.map((one) => [one.id, one]));
|
|
156
|
+
// Narration aliases reach every line of the one request, each turn on its own so an alias
|
|
157
|
+
// never joins the end of one turn to the start of the next. The clean turns stay the
|
|
158
|
+
// transcript, as they do for a voice that gets one request per turn.
|
|
159
|
+
const aliases = narrationAliasesOf(context.config);
|
|
160
|
+
const spoken = turns.map((turn) => applyAliases(turn.text, aliases));
|
|
161
|
+
const logicalText = turns.map((turn) => turn.text).join("\n");
|
|
162
|
+
const text = spoken.join("\n");
|
|
163
|
+
return [
|
|
164
|
+
recipe(context, `${logicalKey}:1`, "audio", {
|
|
165
|
+
kind: "tts",
|
|
166
|
+
version: 1,
|
|
167
|
+
provider: speaker.voice.provider,
|
|
168
|
+
model: speaker.voice.model,
|
|
169
|
+
voice: speaker.voice.voice,
|
|
170
|
+
text,
|
|
171
|
+
...(text === logicalText ? {} : { spokenText: logicalText }),
|
|
172
|
+
logicalKey,
|
|
173
|
+
logicalText,
|
|
174
|
+
segment: "body",
|
|
175
|
+
pronunciation: null,
|
|
176
|
+
dialogue: turns.map((turn, index) => ({
|
|
177
|
+
speaker: turn.speaker,
|
|
178
|
+
turn: turn.index,
|
|
179
|
+
voice: byId.get(turn.speaker)?.voice.voice ?? "",
|
|
180
|
+
text: spoken[index] ?? turn.text,
|
|
181
|
+
})),
|
|
182
|
+
}, script.dependsOn, { tokenKey: logicalKey }),
|
|
183
|
+
];
|
|
184
|
+
}
|
|
185
|
+
const logicalText = normalizeNarrationText(override?.kind === "text" ? override.text : first.text);
|
|
186
|
+
const entries = speakerGlossary(context, speaker, glossary);
|
|
187
|
+
if (!entries.ok)
|
|
188
|
+
return [refused(context, logicalKey, script.dependsOn, entries.reason)];
|
|
189
|
+
const maxCharacters = context.catalogue?.tts.find((row) => row.provider === speaker.voice.provider &&
|
|
190
|
+
row.id === speaker.voice.model &&
|
|
191
|
+
row.enabled &&
|
|
192
|
+
!row.deprecated)?.tts.maxCharacters ?? Math.max(2, logicalText.length * 4);
|
|
193
|
+
// Narration aliases apply to every voice, the speaker's pronunciations to IPA voices only.
|
|
194
|
+
const spans = withAliasSpans(pronunciationSpans(logicalText, entries.entries), aliasSpans(logicalText, narrationAliasesOf(context.config)));
|
|
195
|
+
if (preparesTurns(context.config, speaker)) {
|
|
196
|
+
const prepared = preparationForGroup(context, logicalKey, logicalText, "body", script.dependsOn, maxCharacters, spans, aliasMatches(logicalText, narrationAliasesOf(context.config)), { name: speaker.name.trim(), role: speaker.role });
|
|
197
|
+
preparations.push(prepared.preparation);
|
|
198
|
+
if (prepared.refusal !== null)
|
|
199
|
+
return [refused(context, logicalKey, [prepared.preparation.key], prepared.refusal)];
|
|
200
|
+
return (prepared.requests ?? []).map((request, index) => recipe(context, `${logicalKey}:${String(index + 1)}`, "audio", {
|
|
201
|
+
kind: "tts",
|
|
202
|
+
version: 1,
|
|
203
|
+
provider: speaker.voice.provider,
|
|
204
|
+
model: speaker.voice.model,
|
|
205
|
+
voice: speaker.voice.voice,
|
|
206
|
+
text: request.text,
|
|
207
|
+
spokenText: request.spokenText,
|
|
208
|
+
logicalKey,
|
|
209
|
+
logicalText,
|
|
210
|
+
segment: "body",
|
|
211
|
+
pronunciation: null,
|
|
212
|
+
speaker: speaker.id,
|
|
213
|
+
turn: first.index,
|
|
214
|
+
}, [prepared.preparation.key], { tokenKey: logicalKey, unresolved: logicalText.length === 0 }));
|
|
215
|
+
}
|
|
216
|
+
let requests;
|
|
217
|
+
if (spans.length > 0) {
|
|
218
|
+
const prepared = prepareRequests(logicalText, [], maxCharacters, spans);
|
|
219
|
+
if (!prepared.ok)
|
|
220
|
+
return [refused(context, logicalKey, script.dependsOn, prepared.reason)];
|
|
221
|
+
requests = prepared.requests;
|
|
222
|
+
}
|
|
223
|
+
else
|
|
224
|
+
requests = splitText(logicalText, maxCharacters).map((text) => ({ text }));
|
|
225
|
+
return requests.map((request, index) => recipe(context, `${logicalKey}:${String(index + 1)}`, "audio", {
|
|
226
|
+
kind: "tts",
|
|
227
|
+
version: 1,
|
|
228
|
+
provider: speaker.voice.provider,
|
|
229
|
+
model: speaker.voice.model,
|
|
230
|
+
voice: speaker.voice.voice,
|
|
231
|
+
text: request.text,
|
|
232
|
+
...(request.spokenText === undefined ? {} : { spokenText: request.spokenText }),
|
|
233
|
+
logicalKey,
|
|
234
|
+
logicalText,
|
|
235
|
+
segment: "body",
|
|
236
|
+
pronunciation: null,
|
|
237
|
+
speaker: speaker.id,
|
|
238
|
+
turn: first.index,
|
|
239
|
+
}, script.dependsOn, { tokenKey: logicalKey, unresolved: logicalText.length === 0 }));
|
|
240
|
+
}
|
|
241
|
+
// IPA pronunciations reach only the voices that read IPA: the speaker's own first, then the
|
|
242
|
+
// run's glossary where Use Pronunciation Glossary is on.
|
|
243
|
+
function speakerGlossary(context, speaker, glossary) {
|
|
244
|
+
if (!readsIpa(speaker))
|
|
245
|
+
return { ok: true, entries: [] };
|
|
246
|
+
const own = speaker.pronunciations?.trim()
|
|
247
|
+
? parsePronunciationGlossary(speaker.pronunciations, context.config.language)
|
|
248
|
+
: { ok: true, entries: [] };
|
|
249
|
+
if (!own.ok)
|
|
250
|
+
return {
|
|
251
|
+
ok: false,
|
|
252
|
+
reason: `${speaker.name.trim()}'s pronunciations can't be used: ${own.reason.replace("Edit the glossary or turn off Use Pronunciation Glossary.", "Fix them under Speakers (Play → Narration, or Edit project → Providers).")}`,
|
|
253
|
+
};
|
|
254
|
+
const run = context.config.audio?.usePronunciationGlossary === true && glossary?.ok === true
|
|
255
|
+
? glossary.entries
|
|
256
|
+
: [];
|
|
257
|
+
return { ok: true, entries: withSharedGlossary(own.entries, run) };
|
|
258
|
+
}
|
|
259
|
+
// A turn is prepared when the run has a Narration Preparation prompt and its speaker's voice
|
|
260
|
+
// takes Inworld TTS-2's delivery tags; every other speaker's turns are spoken as written.
|
|
261
|
+
export function preparesTurns(config, speaker) {
|
|
262
|
+
return (usesNarrationPreparation(config) &&
|
|
263
|
+
speaker.voice.provider === "inworld" &&
|
|
264
|
+
speaker.voice.model === "inworld-tts-2");
|
|
265
|
+
}
|
|
266
|
+
export function readsIpa(speaker) {
|
|
267
|
+
return (speaker.voice.provider === "inworld" &&
|
|
268
|
+
(speaker.voice.model === "inworld-tts-2" || speaker.voice.model === "inworld-tts-2-flash"));
|
|
269
|
+
}
|
|
270
|
+
function refused(context, logicalKey, dependsOn, reason) {
|
|
271
|
+
return recipe(context, `${logicalKey}:1`, "audio", { kind: "deferred", version: 1, operation: "resolve-revision-recipe", template: reason }, dependsOn, { unresolved: true, refusal: reason });
|
|
272
|
+
}
|
|
273
|
+
// What a pending script will be spoken with, so a voice change before the script lands is
|
|
274
|
+
// still a change.
|
|
275
|
+
export function speakerValues(voices) {
|
|
276
|
+
return [
|
|
277
|
+
voices.nativeDialogue,
|
|
278
|
+
voices.speakers.map((speaker) => [
|
|
279
|
+
speaker.id,
|
|
280
|
+
speaker.voice.provider,
|
|
281
|
+
speaker.voice.model,
|
|
282
|
+
speaker.voice.voice,
|
|
283
|
+
speaker.pronunciations ?? null,
|
|
284
|
+
]),
|
|
285
|
+
];
|
|
286
|
+
}
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { estimateRequests } from "../estimate/index.js";
|
|
3
|
+
import { skippedGlossaryNotice, skippedSpeakerPronunciations, skippedSpeakerPronunciationsNotice, } from "../narration/pronunciation.js";
|
|
4
|
+
import { isLocalCliProvider } from "../settings/model.js";
|
|
3
5
|
import { defaultShortsPrompt, shortsImageUpperBound } from "../shorts/model.js";
|
|
6
|
+
import { usesVoices } from "../voices/model.js";
|
|
4
7
|
import { planDependencies } from "./dependencies.js";
|
|
5
8
|
import { matchingNarrationPiece } from "./narration-reuse.js";
|
|
6
9
|
import { buildRecipes } from "./recipe-build.js";
|
|
7
10
|
import { manualCuesNeedReview } from "./recipe-exports.js";
|
|
8
11
|
import { selectedReference, } from "./recipe-model.js";
|
|
12
|
+
import { recipeProviderChoice } from "./recipe-provider-choice.js";
|
|
13
|
+
import { narrationGlossary } from "./recipe-text.js";
|
|
14
|
+
import { readsIpa } from "./recipe-voices.js";
|
|
9
15
|
export function planRevisionWork(revision, manifest, catalogue, availableAssetIds, resolved, retainedNarration = []) {
|
|
10
16
|
const selected = {
|
|
11
17
|
outputs: manifest.outputs.filter(selectedReference),
|
|
@@ -27,6 +33,7 @@ export function planRevisionWork(revision, manifest, catalogue, availableAssetId
|
|
|
27
33
|
resolved: { ...resolved, articleMarkdown: usableArticle ? resolved.articleMarkdown : null },
|
|
28
34
|
};
|
|
29
35
|
const logical = buildRecipes(logicalContext);
|
|
36
|
+
const glossary = narrationGlossary(logicalContext);
|
|
30
37
|
const context = { ...logicalContext, catalogue };
|
|
31
38
|
const recipes = buildRecipes(context).map((row) => ({
|
|
32
39
|
...row,
|
|
@@ -64,7 +71,7 @@ export function planRevisionWork(revision, manifest, catalogue, availableAssetId
|
|
|
64
71
|
null,
|
|
65
72
|
after: row.fingerprint,
|
|
66
73
|
}));
|
|
67
|
-
const priced = work.flatMap((row) => priceRecipes(row, recipes.find((one) => one.key === row.key)));
|
|
74
|
+
const priced = work.flatMap((row) => priceRecipes(row, recipes.find((one) => one.key === row.key), revision.config));
|
|
68
75
|
return {
|
|
69
76
|
recipes,
|
|
70
77
|
work,
|
|
@@ -74,6 +81,13 @@ export function planRevisionWork(revision, manifest, catalogue, availableAssetId
|
|
|
74
81
|
(revision.config.chunking?.mode ?? "whole") === "whole"
|
|
75
82
|
? "Whole-text narration is one logical request. Editing its text rebuilds every provider part of that request."
|
|
76
83
|
: null,
|
|
84
|
+
glossaryNotice: glossary?.ok === true && glossary.skipped !== undefined
|
|
85
|
+
? skippedGlossaryNotice(glossary.skipped)
|
|
86
|
+
: null,
|
|
87
|
+
// Only speakers whose voice reads IPA use their pronunciations at all.
|
|
88
|
+
speakerPronunciationNotice: usesVoices(revision.config) && revision.config.voices !== undefined
|
|
89
|
+
? skippedSpeakerPronunciationsNotice(skippedSpeakerPronunciations(revision.config.voices.speakers.filter(readsIpa), revision.config.language))
|
|
90
|
+
: null,
|
|
77
91
|
costs: estimateRequests(priced, catalogue),
|
|
78
92
|
};
|
|
79
93
|
}
|
|
@@ -187,14 +201,16 @@ const shortsTranscriptEstimate = 24000;
|
|
|
187
201
|
// What a step is charged as. Most steps are one request; what stands in for the shorts
|
|
188
202
|
// until they are picked is every clip's image prompts and, at the longest length allowed,
|
|
189
203
|
// every image, which is the most the step can cost.
|
|
190
|
-
export function priceRecipes(work, value
|
|
204
|
+
export function priceRecipes(work, value,
|
|
205
|
+
// The revision's settings, which name the provider of a request built only when it runs.
|
|
206
|
+
config) {
|
|
191
207
|
const input = value?.input;
|
|
192
208
|
const unfolds = value?.unfoldsImages;
|
|
193
209
|
// A short's image prompts once the clips are picked: the call, and the images it leads to,
|
|
194
210
|
// as many as the clip's length asks for.
|
|
195
211
|
if (work.disposition === "generate" && unfolds !== undefined)
|
|
196
212
|
return [
|
|
197
|
-
priceRecipe(work, value),
|
|
213
|
+
priceRecipe(work, value, config),
|
|
198
214
|
...Array.from({ length: unfolds.count }, () => ({
|
|
199
215
|
kind: "image",
|
|
200
216
|
stage: work.key,
|
|
@@ -223,7 +239,7 @@ export function priceRecipes(work, value) {
|
|
|
223
239
|
input?.kind !== "deferred" ||
|
|
224
240
|
input.operation !== "shorts" ||
|
|
225
241
|
!Array.isArray(input.template))
|
|
226
|
-
return [priceRecipe(work, value)];
|
|
242
|
+
return [priceRecipe(work, value, config)];
|
|
227
243
|
const [, style, provider, model, imageSeconds, count, maxSeconds, llmProvider, llmModel] = input.template;
|
|
228
244
|
const clips = typeof count === "number" ? count : 0;
|
|
229
245
|
const images = typeof imageSeconds === "number" && typeof maxSeconds === "number"
|
|
@@ -249,7 +265,7 @@ export function priceRecipes(work, value) {
|
|
|
249
265
|
})),
|
|
250
266
|
];
|
|
251
267
|
}
|
|
252
|
-
export function priceRecipe(work, value) {
|
|
268
|
+
export function priceRecipe(work, value, config) {
|
|
253
269
|
if (value === undefined || work.disposition !== "generate")
|
|
254
270
|
return {
|
|
255
271
|
kind: "local",
|
|
@@ -259,12 +275,21 @@ export function priceRecipe(work, value) {
|
|
|
259
275
|
: "Local, provided, or blocked work; no admitted generation charge.",
|
|
260
276
|
};
|
|
261
277
|
const input = value.input;
|
|
278
|
+
const onPlan = config === undefined ? undefined : cliRequest(work, value, config);
|
|
279
|
+
if (onPlan !== undefined)
|
|
280
|
+
return onPlan;
|
|
262
281
|
if (input.kind === "deferred" && input.operation === "narration-preparation")
|
|
263
282
|
return {
|
|
264
283
|
kind: "unknown",
|
|
265
284
|
stage: work.key,
|
|
266
285
|
detail: "Narration Preparation: one LLM call per future logical chunk or entry; source length is not yet known.",
|
|
267
286
|
};
|
|
287
|
+
if (input.kind === "deferred" && input.operation === "narration-description")
|
|
288
|
+
return {
|
|
289
|
+
kind: "unknown",
|
|
290
|
+
stage: work.key,
|
|
291
|
+
detail: "Narration descriptions: one LLM call per table, figure, equation or code block in the article; how many is known once the article is written.",
|
|
292
|
+
};
|
|
268
293
|
if (input.kind === "llm")
|
|
269
294
|
return {
|
|
270
295
|
kind: "llm",
|
|
@@ -273,7 +298,7 @@ export function priceRecipe(work, value) {
|
|
|
273
298
|
model: input.model,
|
|
274
299
|
inputCharacters: input.messages.reduce((sum, message) => sum + message.content.length, 0) +
|
|
275
300
|
(input.documents ?? []).reduce((sum, document) => sum + document.content.length, 0),
|
|
276
|
-
outputCharacters: work.key === "article:body" ? 9000 : 2400,
|
|
301
|
+
outputCharacters: work.key === "article:body" ? 9000 : input.describe !== undefined ? 600 : 2400,
|
|
277
302
|
detail: "Actual saved request input; output length is estimated. Retries and tools are excluded.",
|
|
278
303
|
};
|
|
279
304
|
if (input.kind === "tts")
|
|
@@ -299,6 +324,21 @@ export function priceRecipe(work, value) {
|
|
|
299
324
|
detail: "The numbered transcript is built when the step runs; its length is estimated. One more call is made when the first answer breaks the rules. Retries are excluded.",
|
|
300
325
|
};
|
|
301
326
|
}
|
|
327
|
+
// A review: the stage's prompt and the item (the article's text, or a short brief beside an
|
|
328
|
+
// image, which the provider prices as it does).
|
|
329
|
+
if (input.kind === "local" && input.operation === "review-v1") {
|
|
330
|
+
const [stage, , , provider, model, , prompt] = Array.isArray(input.values) ? input.values : [];
|
|
331
|
+
return {
|
|
332
|
+
kind: "llm",
|
|
333
|
+
stage: work.key,
|
|
334
|
+
provider: typeof provider === "string" ? provider : "",
|
|
335
|
+
model: typeof model === "string" ? model : "",
|
|
336
|
+
inputCharacters: (typeof prompt === "string" && prompt !== "" ? prompt.length : 600) +
|
|
337
|
+
(stage === "article" || stage === "narration" ? 20000 : 1500),
|
|
338
|
+
outputCharacters: 400,
|
|
339
|
+
detail: "The item is read when the step runs; its length is estimated. A failed item made again is reviewed again, which is not in this estimate.",
|
|
340
|
+
};
|
|
341
|
+
}
|
|
302
342
|
if (input.kind === "local" && input.operation === "youtube-description-v1") {
|
|
303
343
|
const [, provider, model, , prompt] = Array.isArray(input.values) ? input.values : [];
|
|
304
344
|
return {
|
|
@@ -320,3 +360,34 @@ export function priceRecipe(work, value) {
|
|
|
320
360
|
detail: "Future generated input is unknown. This group materializes under the same admitted revision; its final request count and charge are unknown.",
|
|
321
361
|
};
|
|
322
362
|
}
|
|
363
|
+
// A request built only when its step runs, on a command-line provider: it runs on the user's
|
|
364
|
+
// plan, so it is known at $0, as Play's review prices the same step. Its size is estimated
|
|
365
|
+
// for the API figure beside it; Narration Preparation's number of calls is not known yet, so
|
|
366
|
+
// it has none.
|
|
367
|
+
function cliRequest(work, value, config) {
|
|
368
|
+
if (value.input.kind !== "deferred")
|
|
369
|
+
return undefined;
|
|
370
|
+
const choice = recipeProviderChoice(value, config);
|
|
371
|
+
if (choice === undefined || !isLocalCliProvider(choice.provider))
|
|
372
|
+
return undefined;
|
|
373
|
+
if (choice.family === "image")
|
|
374
|
+
return { kind: "image", stage: work.key, provider: choice.provider, model: choice.model };
|
|
375
|
+
if (choice.family !== "llm")
|
|
376
|
+
return undefined;
|
|
377
|
+
const preparation = value.input.operation === "narration-preparation" ||
|
|
378
|
+
value.input.operation === "narration-description";
|
|
379
|
+
return {
|
|
380
|
+
kind: "llm",
|
|
381
|
+
stage: work.key,
|
|
382
|
+
provider: choice.provider,
|
|
383
|
+
model: choice.model,
|
|
384
|
+
inputCharacters: 6000,
|
|
385
|
+
outputCharacters: work.key === "article:body" ? 9000 : 2400,
|
|
386
|
+
...(preparation ? { apiUnknown: true } : {}),
|
|
387
|
+
detail: preparation
|
|
388
|
+
? value.input.operation === "narration-description"
|
|
389
|
+
? "Runs on your CLI plan, so it adds no charge. Narration descriptions make one call per table, figure, equation or code block; how many is known once the article is written, so no API figure is given."
|
|
390
|
+
: "Runs on your CLI plan, so it adds no charge. Narration Preparation makes one call per logical chunk or entry; how many is known when the step runs, so no API figure is given."
|
|
391
|
+
: "Runs on your CLI plan, so it adds no charge. The request is built when the step runs, so its API figure uses an estimated length.",
|
|
392
|
+
};
|
|
393
|
+
}
|
|
@@ -25,6 +25,8 @@ export function youtubeRecipes(context, exports) {
|
|
|
25
25
|
// ships as a new operation version rather than a silent change here.
|
|
26
26
|
config.descriptionPrompt?.trim() ? renderedPrompt(context, "description") : "",
|
|
27
27
|
config.title,
|
|
28
|
+
// The project language, only when it is not English, so English keeps its fingerprint.
|
|
29
|
+
...(config.language === undefined || config.language === "en" ? [] : [config.language]),
|
|
28
30
|
],
|
|
29
31
|
}, [timing.key],
|
|
30
32
|
// A paid LLM call: readiness, the charge warning and the estimate treat it like one.
|