@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
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
// Image prompts and their Numbers changed in Edit project. A project's images were planned per
|
|
2
|
+
// prompt when its run was adopted (`adopt-content.ts`): each image is a definition under
|
|
3
|
+
// `imagePrompts.N`, Number times per prompt. Changing which prompts are used, or a Number,
|
|
4
|
+
// re-derives those definitions here, the way Play's plan lays them out (prompts in selection
|
|
5
|
+
// order, Number images each), while keeping every definition a prompt already has: an image
|
|
6
|
+
// keeps its key and its wording, so its fingerprint - and the image made for it - stays.
|
|
7
|
+
//
|
|
8
|
+
// The edit's content is read in the numbering of `from` (the prompts the revision was saved
|
|
9
|
+
// with); the result is in the numbering of `to`. The browser calls this too, with stand-in
|
|
10
|
+
// keys, to say what saving will add and remove.
|
|
11
|
+
import { imagesPerRunMax, numberPerPromptMax } from "../admission/rules.js";
|
|
12
|
+
import { render } from "../admission/substitute.js";
|
|
13
|
+
export function imagePromptKey(index) {
|
|
14
|
+
return `imagePrompts.${String(index)}`;
|
|
15
|
+
}
|
|
16
|
+
function promptIndex(key) {
|
|
17
|
+
const match = /^imagePrompts\.(\d+)$/.exec(key ?? "");
|
|
18
|
+
return match?.[1] === undefined ? undefined : Number(match[1]);
|
|
19
|
+
}
|
|
20
|
+
export function sameImagePrompts(from, to) {
|
|
21
|
+
return (from.length === to.length &&
|
|
22
|
+
from.every((one, index) => one.name === to[index]?.name && one.number === to[index]?.number));
|
|
23
|
+
}
|
|
24
|
+
// The same bounds Play's admission holds a run to.
|
|
25
|
+
export function imagePromptFields(to) {
|
|
26
|
+
const fields = [];
|
|
27
|
+
if (to.length === 0)
|
|
28
|
+
fields.push({
|
|
29
|
+
field: "imagePrompts",
|
|
30
|
+
message: "Tick at least one image prompt under Images, or set Images to Off or Provide on Inputs.",
|
|
31
|
+
});
|
|
32
|
+
const names = new Set();
|
|
33
|
+
for (const [index, prompt] of to.entries()) {
|
|
34
|
+
if (prompt.name.trim() === "")
|
|
35
|
+
fields.push({
|
|
36
|
+
field: `imagePrompts.${String(index)}.name`,
|
|
37
|
+
message: "Pick an image prompt.",
|
|
38
|
+
});
|
|
39
|
+
else if (names.has(prompt.name))
|
|
40
|
+
fields.push({
|
|
41
|
+
field: `imagePrompts.${String(index)}.name`,
|
|
42
|
+
message: `The image prompt "${prompt.name}" is ticked twice. Untick one under Images.`,
|
|
43
|
+
});
|
|
44
|
+
names.add(prompt.name);
|
|
45
|
+
if (!Number.isInteger(prompt.number) || prompt.number < 1 || prompt.number > numberPerPromptMax)
|
|
46
|
+
fields.push({
|
|
47
|
+
field: `imagePrompts.${String(index)}.number`,
|
|
48
|
+
message: `Enter a whole number between 1 and ${String(numberPerPromptMax)} for "${prompt.name}" under Images.`,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
const total = to.reduce((sum, prompt) => sum + prompt.number, 0);
|
|
52
|
+
if (total > imagesPerRunMax)
|
|
53
|
+
fields.push({
|
|
54
|
+
field: "imagePrompts",
|
|
55
|
+
message: `A video can have at most ${String(imagesPerRunMax)} images; these prompts ask for ${String(total)}. Lower the Numbers under Images.`,
|
|
56
|
+
});
|
|
57
|
+
return fields;
|
|
58
|
+
}
|
|
59
|
+
export function replanImagePrompts(input) {
|
|
60
|
+
const { from, to, content } = input;
|
|
61
|
+
// Where each prompt of `to` was in `from`, by name: a prompt is the same prompt while its
|
|
62
|
+
// name is, wherever it now sits.
|
|
63
|
+
const was = to.map((prompt) => from.findIndex((one) => one.name === prompt.name));
|
|
64
|
+
const fields = [];
|
|
65
|
+
const templates = {};
|
|
66
|
+
const rendered = {};
|
|
67
|
+
for (const [key, value] of Object.entries(content.promptTemplates))
|
|
68
|
+
if (promptIndex(key) === undefined)
|
|
69
|
+
templates[key] = value;
|
|
70
|
+
for (const [key, value] of Object.entries(input.rendered))
|
|
71
|
+
if (promptIndex(key) === undefined)
|
|
72
|
+
rendered[key] = value;
|
|
73
|
+
for (const [index, prompt] of to.entries()) {
|
|
74
|
+
const key = imagePromptKey(index);
|
|
75
|
+
const old = was[index] ?? -1;
|
|
76
|
+
if (old >= 0) {
|
|
77
|
+
const oldKey = imagePromptKey(old);
|
|
78
|
+
if (Object.hasOwn(content.promptTemplates, oldKey))
|
|
79
|
+
templates[key] = content.promptTemplates[oldKey] ?? null;
|
|
80
|
+
const text = input.rendered[oldKey];
|
|
81
|
+
if (text !== undefined)
|
|
82
|
+
rendered[key] = text;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const body = input.body(prompt.name);
|
|
86
|
+
if (body === undefined) {
|
|
87
|
+
fields.push({
|
|
88
|
+
field: `imagePrompts.${String(index)}.name`,
|
|
89
|
+
message: `The image prompt "${prompt.name}" is not in the Library any more. Untick it under Images, or add it back on Library → Prompts.`,
|
|
90
|
+
});
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
templates[key] = body;
|
|
94
|
+
rendered[key] = render(body, input.values);
|
|
95
|
+
}
|
|
96
|
+
if (fields.length > 0)
|
|
97
|
+
return { ok: false, fields };
|
|
98
|
+
// The definitions each prompt of `from` has now, in slideshow order.
|
|
99
|
+
const groups = new Map();
|
|
100
|
+
for (const key of content.imageOrder) {
|
|
101
|
+
const index = promptIndex(content.imageDefinitions[key]?.templateKey);
|
|
102
|
+
if (index === undefined || index >= from.length)
|
|
103
|
+
continue;
|
|
104
|
+
groups.set(index, [...(groups.get(index) ?? []), key]);
|
|
105
|
+
}
|
|
106
|
+
const definitions = {};
|
|
107
|
+
const added = [];
|
|
108
|
+
const removed = [];
|
|
109
|
+
// New images for a prompt: its first image's wording when it has one, else the prompt's own.
|
|
110
|
+
const fresh = (index, like) => {
|
|
111
|
+
const key = imagePromptKey(index);
|
|
112
|
+
const template = templates[key];
|
|
113
|
+
return like === undefined
|
|
114
|
+
? {
|
|
115
|
+
source: "generate",
|
|
116
|
+
assetId: null,
|
|
117
|
+
prompt: rendered[key] ?? null,
|
|
118
|
+
templateKey: template === undefined || template === null ? null : key,
|
|
119
|
+
}
|
|
120
|
+
: { ...like, source: "generate", assetId: null, templateKey: like.templateKey ?? null };
|
|
121
|
+
};
|
|
122
|
+
const extra = (index, count, like) => Array.from({ length: Math.max(0, count) }, () => {
|
|
123
|
+
const id = input.key();
|
|
124
|
+
definitions[id] = fresh(index, like);
|
|
125
|
+
added.push(id);
|
|
126
|
+
return id;
|
|
127
|
+
});
|
|
128
|
+
const order = [];
|
|
129
|
+
for (const key of content.imageOrder) {
|
|
130
|
+
const definition = content.imageDefinitions[key];
|
|
131
|
+
if (definition === undefined)
|
|
132
|
+
continue;
|
|
133
|
+
const old = promptIndex(definition.templateKey);
|
|
134
|
+
if (old === undefined || old >= from.length) {
|
|
135
|
+
// An image added on its own, or uploaded: not any prompt's, so it stays where it is.
|
|
136
|
+
definitions[key] = definition;
|
|
137
|
+
order.push(key);
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
const index = was.indexOf(old);
|
|
141
|
+
if (index === -1) {
|
|
142
|
+
removed.push(key);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
const group = groups.get(old) ?? [];
|
|
146
|
+
const changed = from[old]?.number !== to[index]?.number;
|
|
147
|
+
const keep = changed ? (to[index]?.number ?? 0) : group.length;
|
|
148
|
+
const at = group.indexOf(key);
|
|
149
|
+
if (at >= keep) {
|
|
150
|
+
removed.push(key);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
const moved = definition.templateKey === imagePromptKey(index)
|
|
154
|
+
? definition
|
|
155
|
+
: { ...definition, templateKey: imagePromptKey(index) };
|
|
156
|
+
definitions[key] = moved;
|
|
157
|
+
order.push(key);
|
|
158
|
+
// A raised Number adds its images right after the prompt's last one.
|
|
159
|
+
if (changed && at === Math.min(group.length, keep) - 1)
|
|
160
|
+
order.push(...extra(index, keep - group.length, moved));
|
|
161
|
+
}
|
|
162
|
+
for (const [index, prompt] of to.entries()) {
|
|
163
|
+
const old = was[index] ?? -1;
|
|
164
|
+
const group = old >= 0 ? (groups.get(old) ?? []) : [];
|
|
165
|
+
// A prompt with no image left (every one removed by hand) makes its Number again only when
|
|
166
|
+
// the Number changed; a prompt new to the project makes all of them, at the end.
|
|
167
|
+
if (old >= 0 && (group.length > 0 || from[old]?.number === prompt.number))
|
|
168
|
+
continue;
|
|
169
|
+
order.push(...extra(index, prompt.number, undefined));
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
ok: true,
|
|
173
|
+
content: {
|
|
174
|
+
...content,
|
|
175
|
+
imageOrder: order,
|
|
176
|
+
imageDefinitions: definitions,
|
|
177
|
+
promptTemplates: templates,
|
|
178
|
+
},
|
|
179
|
+
rendered,
|
|
180
|
+
added,
|
|
181
|
+
removed,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
@@ -144,6 +144,16 @@ export function validateAssetReferences(deps, projectId, content, prepared) {
|
|
|
144
144
|
field: "content.shortsMusic",
|
|
145
145
|
message: "This music file does not belong to this project. Reload the page and choose the music again.",
|
|
146
146
|
});
|
|
147
|
+
// The ambient bed's own file is only ever the one the project was started with.
|
|
148
|
+
const bed = content.ambientBed;
|
|
149
|
+
if (bed !== undefined &&
|
|
150
|
+
deps.db
|
|
151
|
+
.prepare("SELECT 1 FROM project_revisions WHERE project_id=? AND json_extract(content,'$.ambientBed')=? LIMIT 1")
|
|
152
|
+
.get(projectId, bed) === undefined)
|
|
153
|
+
fields.push({
|
|
154
|
+
field: "content.ambientBed",
|
|
155
|
+
message: "This ambient sound file does not belong to this project. Reload the page and save again.",
|
|
156
|
+
});
|
|
147
157
|
return fields;
|
|
148
158
|
}
|
|
149
159
|
export async function measureAudio(deps, projectId, path) {
|
|
@@ -1,6 +1,59 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
1
2
|
import { isDeepStrictEqual } from "node:util";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { outputPath } from "../storage/layout.js";
|
|
5
|
+
import { usesVoices } from "../voices/model.js";
|
|
6
|
+
import { cueSpeaker } from "../voices/timing.js";
|
|
2
7
|
import { assetPath, measureAudio } from "./mutation-assets.js";
|
|
3
8
|
import { validateCues } from "./rules.js";
|
|
9
|
+
// Edited captions of a multi-voice run keep their speakers. A cue that comes back without one
|
|
10
|
+
// (the caption editor edits text and times only) takes the speaker it had before, found by the
|
|
11
|
+
// cue's id, and a new cue the speaker of the narration under it, from the word timing. A cue
|
|
12
|
+
// sent with a speaker keeps it. Runs with one voice are left as they are.
|
|
13
|
+
export function withCueSpeakers(deps, base, edit) {
|
|
14
|
+
const edited = edit.content.subtitleCues;
|
|
15
|
+
if (edited === undefined ||
|
|
16
|
+
!usesVoices(edit.config) ||
|
|
17
|
+
!cuesChanged(base, edit) ||
|
|
18
|
+
edited.cues.every((cue) => cue.speaker !== undefined))
|
|
19
|
+
return edit;
|
|
20
|
+
const before = new Map((base.revision.content.subtitleCues?.cues ?? []).flatMap((cue) => cue.speaker === undefined ? [] : [[cue.id, cue.speaker]]));
|
|
21
|
+
let words;
|
|
22
|
+
const timing = () => {
|
|
23
|
+
if (words !== undefined)
|
|
24
|
+
return words;
|
|
25
|
+
const row = base.outputs.find((one) => one.selected &&
|
|
26
|
+
one.available &&
|
|
27
|
+
one.state === "ready" &&
|
|
28
|
+
one.workKey === "subtitles:timing" &&
|
|
29
|
+
one.output.role === "subtitle_words");
|
|
30
|
+
try {
|
|
31
|
+
words =
|
|
32
|
+
row === undefined
|
|
33
|
+
? []
|
|
34
|
+
: timedWords.parse(JSON.parse(readFileSync(outputPath(deps.paths, base.revision.projectId, row.output.path), "utf8"))).words;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// Unreadable timing leaves the new cues without a speaker; they still save.
|
|
38
|
+
words = [];
|
|
39
|
+
}
|
|
40
|
+
return words;
|
|
41
|
+
};
|
|
42
|
+
const cues = edited.cues.map((cue) => {
|
|
43
|
+
if (cue.speaker !== undefined)
|
|
44
|
+
return cue;
|
|
45
|
+
const speaker = before.get(cue.id) ?? cueSpeaker(cue, timing() ?? []);
|
|
46
|
+
return speaker === undefined ? cue : { ...cue, speaker };
|
|
47
|
+
});
|
|
48
|
+
return { ...edit, content: { ...edit.content, subtitleCues: { ...edited, cues } } };
|
|
49
|
+
}
|
|
50
|
+
const timedWords = z.object({
|
|
51
|
+
words: z.array(z.object({
|
|
52
|
+
start: z.number(),
|
|
53
|
+
end: z.number(),
|
|
54
|
+
speaker: z.string().optional(),
|
|
55
|
+
})),
|
|
56
|
+
});
|
|
4
57
|
export function cuesChanged(base, edit) {
|
|
5
58
|
return (edit.content.subtitleCues !== undefined &&
|
|
6
59
|
!isDeepStrictEqual(base.revision.content.subtitleCues, edit.content.subtitleCues));
|
|
@@ -54,12 +107,30 @@ export function validateProposedCues(base, edit, plan, prepared, durations) {
|
|
|
54
107
|
return undefined;
|
|
55
108
|
if (recipe.input.kind === "provided" && recipe.input.assetId !== null)
|
|
56
109
|
return durations.get(recipe.input.assetId);
|
|
110
|
+
// Pauses between sentences lengthen the join by what only the join measures.
|
|
111
|
+
if (recipe.input.kind === "local" &&
|
|
112
|
+
Array.isArray(recipe.input.values) &&
|
|
113
|
+
recipe.input.values[2] !== undefined)
|
|
114
|
+
return undefined;
|
|
57
115
|
if (recipe.input.kind === "local" && recipe.input.operation === "concat-narration") {
|
|
58
116
|
const parts = recipe.dependsOn.map(durationFor);
|
|
59
117
|
return parts.some((one) => one === undefined)
|
|
60
118
|
? undefined
|
|
61
119
|
: parts.reduce((sum, one) => sum + (one ?? 0), 0);
|
|
62
120
|
}
|
|
121
|
+
// Speaker turns: each at its pace, with the gap after it (`recipe-voices.ts`).
|
|
122
|
+
if (recipe.input.kind === "local" && recipe.input.operation === "concat-turns-v1") {
|
|
123
|
+
const layout = Array.isArray(recipe.input.values) ? recipe.input.values[1] : undefined;
|
|
124
|
+
const parts = recipe.dependsOn.map(durationFor);
|
|
125
|
+
if (!Array.isArray(layout) || parts.some((one) => one === undefined))
|
|
126
|
+
return undefined;
|
|
127
|
+
return parts.reduce((sum, one, index) => {
|
|
128
|
+
const [pace, gap] = Array.isArray(layout[index]) ? layout[index] : [1, 0];
|
|
129
|
+
return (sum +
|
|
130
|
+
(one ?? 0) / (typeof pace === "number" && pace > 0 ? pace : 1) +
|
|
131
|
+
(typeof gap === "number" ? gap * 1000 : 0));
|
|
132
|
+
}, 0);
|
|
133
|
+
}
|
|
63
134
|
if (plan.baseFingerprints[key] !== plan.fingerprints[key])
|
|
64
135
|
return undefined;
|
|
65
136
|
const prefix = key.replace(/:1$/, ":");
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import { projectExists } from "../admission/repo.js";
|
|
3
4
|
import { currentRevisionId } from "./repo.js";
|
|
4
5
|
import { getRevisionView } from "./view.js";
|
|
5
6
|
export function requestHash(operation, baseRevisionId, input) {
|
|
@@ -43,7 +44,7 @@ export function checkMutation(deps, input) {
|
|
|
43
44
|
.prepare("SELECT 1 FROM project_recovery_requests WHERE project_id=? AND idempotency_key=?")
|
|
44
45
|
.get(input.projectId, input.idempotencyKey) !== undefined)
|
|
45
46
|
return refusal(deps, input.projectId, "idempotency-conflict");
|
|
46
|
-
if (db
|
|
47
|
+
if (!projectExists(db, input.projectId))
|
|
47
48
|
return refusal(deps, input.projectId, "no-project");
|
|
48
49
|
if (currentRevisionId(db, input.projectId) !== input.baseRevisionId)
|
|
49
50
|
return refusal(deps, input.projectId, "conflict");
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { transact } from "../../kernel/db/tx.js";
|
|
3
|
+
import { rebrandedEdit } from "../channels/rebrand.js";
|
|
4
|
+
import { projectChannelId, resolveChannelId, setProjectChannel } from "../channels/repo.js";
|
|
3
5
|
import { carryCheckpointGates } from "../checkpoints/recovery.js";
|
|
6
|
+
import { imageCountsOf, imageScaleProblem } from "../images/scale.js";
|
|
7
|
+
import { promptByName } from "../library/repo.js";
|
|
4
8
|
import { narrationRegenerationKey } from "../narration/plan.js";
|
|
5
9
|
import { recipeInputSchema } from "../rebuild/recipe-input-schema.js";
|
|
6
10
|
import { planRevision } from "../rebuild/recipe-save.js";
|
|
@@ -9,8 +13,10 @@ import { projectStandings } from "../rebuild/runtime-store.js";
|
|
|
9
13
|
import { discardPreparedAssets } from "../storage/assets.js";
|
|
10
14
|
import { deleteStagedFile } from "../storage/repo.js";
|
|
11
15
|
import { releaseStagedFile } from "../storage/staging-refs.js";
|
|
16
|
+
import { ambientBedProblems } from "../video/ambient-bed.js";
|
|
17
|
+
import { imagePromptFields, replanImagePrompts, sameImagePrompts } from "./image-plan.js";
|
|
12
18
|
import { validateAssetReferences, validateReplacementAvailability, validateUploads, } from "./mutation-assets.js";
|
|
13
|
-
import { cuesChanged, inspectCueAudio, measuredOutputs, validateProposedCues, } from "./mutation-cues.js";
|
|
19
|
+
import { cuesChanged, inspectCueAudio, measuredOutputs, validateProposedCues, withCueSpeakers, } from "./mutation-cues.js";
|
|
14
20
|
import { prepareEditAssets } from "./mutation-prepare.js";
|
|
15
21
|
import { checkMutation, insertReceipt, requestHash, requiredView } from "./mutation-request.js";
|
|
16
22
|
import { preserveDeferredIntent } from "./mutation-work.js";
|
|
@@ -43,8 +49,17 @@ export async function saveRevision(deps, input) {
|
|
|
43
49
|
if (checked !== undefined)
|
|
44
50
|
return checked;
|
|
45
51
|
const base = requiredView(deps, input.projectId, input.baseRevisionId);
|
|
46
|
-
const
|
|
52
|
+
const replanned = replannedImages(deps, base, submitted);
|
|
53
|
+
if (!replanned.ok)
|
|
54
|
+
return {
|
|
55
|
+
ok: false,
|
|
56
|
+
reason: "invalid-edit",
|
|
57
|
+
currentRevisionId: base.revision.id,
|
|
58
|
+
fields: replanned.fields,
|
|
59
|
+
};
|
|
60
|
+
const edit = bindNarrationSources(base, rebrandedEdit(deps.db, base.revision.config, replanned.edit));
|
|
47
61
|
const fields = [
|
|
62
|
+
...settingProblems(edit),
|
|
48
63
|
...validateUploads(deps, edit),
|
|
49
64
|
...validateTemplateIntent(base, edit),
|
|
50
65
|
...validateNarrationIntent(base, edit),
|
|
@@ -60,7 +75,8 @@ export async function saveRevision(deps, input) {
|
|
|
60
75
|
currentRevisionId: base.revision.id,
|
|
61
76
|
fields: unavailable,
|
|
62
77
|
};
|
|
63
|
-
const { edit:
|
|
78
|
+
const { edit: uploaded, assets: prepared } = await prepareEditAssets(deps, base, edit);
|
|
79
|
+
const supplied = withCueSpeakers(deps, base, uploaded);
|
|
64
80
|
try {
|
|
65
81
|
const durations = await inspectCueAudio(deps, base, supplied, prepared);
|
|
66
82
|
const result = transact(deps.db, () => {
|
|
@@ -173,12 +189,90 @@ export async function saveRevision(deps, input) {
|
|
|
173
189
|
.filter((asset) => deps.db.prepare("SELECT 1 FROM project_assets WHERE id=?").get(asset.id) === undefined));
|
|
174
190
|
}
|
|
175
191
|
}
|
|
192
|
+
// Settings Edit project changes that admission checks at Play's Start: the ambient bed's
|
|
193
|
+
// numbers and More images for long videos. Said where Edit project shows them.
|
|
194
|
+
function settingProblems(edit) {
|
|
195
|
+
const bed = edit.config.ambientBed;
|
|
196
|
+
const scale = edit.config.imageScale;
|
|
197
|
+
const scaleProblem = scale === undefined ? undefined : imageScaleProblem(scale);
|
|
198
|
+
return [
|
|
199
|
+
...(bed === undefined
|
|
200
|
+
? []
|
|
201
|
+
: ambientBedProblems(bed).map((problem) => ({
|
|
202
|
+
field: `ambientBed.${problem.field}`,
|
|
203
|
+
message: `${problem.message} Change it in Edit project → Inputs → Ambient sound.`,
|
|
204
|
+
}))),
|
|
205
|
+
...(scaleProblem === undefined
|
|
206
|
+
? []
|
|
207
|
+
: [
|
|
208
|
+
{
|
|
209
|
+
field: "imageScale",
|
|
210
|
+
message: `${scaleProblem} Change it in Edit project → Images → More images for long videos.`,
|
|
211
|
+
},
|
|
212
|
+
]),
|
|
213
|
+
];
|
|
214
|
+
}
|
|
215
|
+
// The image prompts or their Numbers changed: the image definitions are planned again from
|
|
216
|
+
// them (`image-plan.ts`), keeping every image an unchanged prompt already has.
|
|
217
|
+
function replannedImages(deps, base, edit) {
|
|
218
|
+
// Planned in the counts each prompt actually makes: its Number, plus its share of the extra
|
|
219
|
+
// images More images for long videos adds (`images/scale.ts`), as the run was adopted.
|
|
220
|
+
const effective = (config) => {
|
|
221
|
+
const counts = imageCountsOf(config);
|
|
222
|
+
return config.imagePrompts.map((prompt, index) => ({
|
|
223
|
+
...prompt,
|
|
224
|
+
number: counts[index] ?? prompt.number,
|
|
225
|
+
}));
|
|
226
|
+
};
|
|
227
|
+
if (edit.config.sources.images !== "generate" &&
|
|
228
|
+
sameImagePrompts(base.revision.config.imagePrompts, edit.config.imagePrompts))
|
|
229
|
+
return { ok: true, edit };
|
|
230
|
+
const from = effective(base.revision.config);
|
|
231
|
+
const to = effective(edit.config);
|
|
232
|
+
if (sameImagePrompts(from, to))
|
|
233
|
+
return { ok: true, edit };
|
|
234
|
+
if (edit.config.sources.images !== "generate")
|
|
235
|
+
return {
|
|
236
|
+
ok: false,
|
|
237
|
+
fields: [
|
|
238
|
+
{
|
|
239
|
+
field: "imagePrompts",
|
|
240
|
+
message: "Image prompts are used only while Images is set to Generate. Set Images to Generate on Inputs, or leave the prompts as they were.",
|
|
241
|
+
},
|
|
242
|
+
],
|
|
243
|
+
};
|
|
244
|
+
const fields = imagePromptFields(edit.config.imagePrompts);
|
|
245
|
+
if (fields.length > 0)
|
|
246
|
+
return { ok: false, fields };
|
|
247
|
+
const plan = replanImagePrompts({
|
|
248
|
+
from,
|
|
249
|
+
to,
|
|
250
|
+
content: edit.content,
|
|
251
|
+
rendered: edit.config.rendered,
|
|
252
|
+
values: edit.config.values,
|
|
253
|
+
body: (name) => promptByName(deps.db, "image", name)?.body,
|
|
254
|
+
key: () => deps.ids.next(),
|
|
255
|
+
});
|
|
256
|
+
if (!plan.ok)
|
|
257
|
+
return plan;
|
|
258
|
+
return {
|
|
259
|
+
ok: true,
|
|
260
|
+
edit: { ...edit, config: { ...edit.config, rendered: plan.rendered }, content: plan.content },
|
|
261
|
+
};
|
|
262
|
+
}
|
|
176
263
|
export function advanceHead(db, projectId, baseRevisionId, revision) {
|
|
177
264
|
const result = db
|
|
178
265
|
.prepare("UPDATE project_heads SET revision_id = ? WHERE project_id = ? AND revision_id = ?")
|
|
179
266
|
.run(revision.id, projectId, baseRevisionId);
|
|
180
267
|
if (Number(result.changes) !== 1)
|
|
181
268
|
return false;
|
|
269
|
+
// The project's channel follows its settings: moved in Edit project, or restored to a
|
|
270
|
+
// version from another channel. A channel since deleted leaves the project where it is.
|
|
271
|
+
const channelId = revision.config.channelId;
|
|
272
|
+
if (channelId !== undefined &&
|
|
273
|
+
resolveChannelId(db, channelId) === channelId &&
|
|
274
|
+
projectChannelId(db, projectId) !== channelId)
|
|
275
|
+
setProjectChannel(db, projectId, channelId);
|
|
182
276
|
db.prepare("UPDATE projects SET title = ?, format = ?, config = ?, updated_at = ? WHERE id = ?").run(revision.config.title, revision.config.format, JSON.stringify(revision.config), revision.createdAt, projectId);
|
|
183
277
|
return true;
|
|
184
278
|
}
|
|
@@ -2,6 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import { stageKinds } from "../../kernel/pipeline.js";
|
|
3
3
|
import { pieceKinds, pieceStates } from "../../kernel/runner/piece-repo.js";
|
|
4
4
|
import { runConfigSchema } from "../admission/schema.js";
|
|
5
|
+
import { scaledImagesMax } from "../images/scale.js";
|
|
5
6
|
import { outputSchema } from "../storage/schema.js";
|
|
6
7
|
const id = z
|
|
7
8
|
.string()
|
|
@@ -16,6 +17,13 @@ const cue = z
|
|
|
16
17
|
text: z.string().trim().min(1).max(10000),
|
|
17
18
|
start: z.number().finite().nonnegative(),
|
|
18
19
|
end: z.number().finite().positive(),
|
|
20
|
+
// The speaker's id on a multi-voice run (`voices/model.ts`); absent everywhere else.
|
|
21
|
+
speaker: z
|
|
22
|
+
.string()
|
|
23
|
+
.min(1)
|
|
24
|
+
.max(60)
|
|
25
|
+
.regex(/^[a-z0-9-]+$/)
|
|
26
|
+
.optional(),
|
|
19
27
|
})
|
|
20
28
|
.strict()
|
|
21
29
|
.refine((value) => value.end > value.start, {
|
|
@@ -35,7 +43,8 @@ export const revisionContentSchema = z
|
|
|
35
43
|
reference: id.optional(),
|
|
36
44
|
})
|
|
37
45
|
.strict(),
|
|
38
|
-
|
|
46
|
+
// The most any project may hold; `rebuild/recipe-validation.ts` holds each to its own cap.
|
|
47
|
+
imageOrder: z.array(id).max(scaledImagesMax),
|
|
39
48
|
imageDefinitions: z.record(id, z
|
|
40
49
|
.object({
|
|
41
50
|
source: z.enum(["generate", "provide"]),
|
|
@@ -79,6 +88,7 @@ export const revisionContentSchema = z
|
|
|
79
88
|
})
|
|
80
89
|
.strict())
|
|
81
90
|
.optional(),
|
|
91
|
+
ambientBed: id.optional(),
|
|
82
92
|
})
|
|
83
93
|
.strict();
|
|
84
94
|
export const revisionEditSchema = z
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { causedBy } from "../../kernel/errors.js";
|
|
3
|
+
// Living within a CLI's plan limits. When a CLI says its allowance is used up, the time it
|
|
4
|
+
// returns is stored per account; every call to that CLI - from any project, and after a
|
|
5
|
+
// restart - waits for it instead of failing. Other providers are untouched, so a project
|
|
6
|
+
// whose narration is keyed carries on while its Codex images wait.
|
|
7
|
+
// Waking a little after the stated reset, so the first call is not refused again at the edge.
|
|
8
|
+
export const resetMarginMs = 2 * 60_000;
|
|
9
|
+
// When the CLI names no reset time, the wait checks again this often.
|
|
10
|
+
export const recheckMs = 30 * 60_000;
|
|
11
|
+
const waitRow = z.object({ retry_at: z.string() });
|
|
12
|
+
export function createLimitGate(deps) {
|
|
13
|
+
// How many calls of one stage wait on one account: a stage drawing four Codex images at
|
|
14
|
+
// once shows one wait, removed when the last of them wakes.
|
|
15
|
+
const waiting = new Map();
|
|
16
|
+
const retryAt = (account) => {
|
|
17
|
+
const row = deps.db
|
|
18
|
+
.prepare("SELECT retry_at FROM plan_limit_waits WHERE account = ?")
|
|
19
|
+
.get(account);
|
|
20
|
+
if (row === undefined)
|
|
21
|
+
return undefined;
|
|
22
|
+
return Date.parse(waitRow.parse(row).retry_at);
|
|
23
|
+
};
|
|
24
|
+
const begin = (account, waiter) => {
|
|
25
|
+
const key = JSON.stringify([waiter.projectId, waiter.stage, account]);
|
|
26
|
+
waiting.set(key, (waiting.get(key) ?? 0) + 1);
|
|
27
|
+
const inserted = deps.db
|
|
28
|
+
.prepare("INSERT OR IGNORE INTO plan_limit_waiters (project_id, stage, account, since) VALUES (?, ?, ?, ?)")
|
|
29
|
+
.run(waiter.projectId, waiter.stage, account, deps.clock.now().toISOString());
|
|
30
|
+
if (inserted.changes === 1)
|
|
31
|
+
deps.changed(waiter.projectId);
|
|
32
|
+
};
|
|
33
|
+
// Only a wait that ran its course removes the row. One cut short by cancel, pause or the
|
|
34
|
+
// app stopping leaves it, and the next start decides what it means (resumeAfterRestart).
|
|
35
|
+
const end = (account, waiter, woke) => {
|
|
36
|
+
const key = JSON.stringify([waiter.projectId, waiter.stage, account]);
|
|
37
|
+
const left = (waiting.get(key) ?? 1) - 1;
|
|
38
|
+
if (left > 0) {
|
|
39
|
+
waiting.set(key, left);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
waiting.delete(key);
|
|
43
|
+
if (!woke)
|
|
44
|
+
return;
|
|
45
|
+
deps.db
|
|
46
|
+
.prepare("DELETE FROM plan_limit_waiters WHERE project_id = ? AND stage = ? AND account = ?")
|
|
47
|
+
.run(waiter.projectId, waiter.stage, account);
|
|
48
|
+
deps.changed(waiter.projectId);
|
|
49
|
+
};
|
|
50
|
+
return {
|
|
51
|
+
ready: async (account, waiter, signal) => {
|
|
52
|
+
for (;;) {
|
|
53
|
+
signal.throwIfAborted();
|
|
54
|
+
const due = retryAt(account);
|
|
55
|
+
if (due === undefined)
|
|
56
|
+
return;
|
|
57
|
+
const left = due - deps.clock.now().getTime();
|
|
58
|
+
if (left <= 0) {
|
|
59
|
+
// Past the reset: the allowance is back. A call refused again stores a new wait.
|
|
60
|
+
deps.db
|
|
61
|
+
.prepare("DELETE FROM plan_limit_waits WHERE account = ? AND retry_at <= ?")
|
|
62
|
+
.run(account, deps.clock.now().toISOString());
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
begin(account, waiter);
|
|
66
|
+
let woke = false;
|
|
67
|
+
try {
|
|
68
|
+
await deps.clock.sleep(left, signal);
|
|
69
|
+
woke = true;
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
end(account, waiter, woke);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
exhausted: (hit) => {
|
|
77
|
+
const now = deps.clock.now().getTime();
|
|
78
|
+
const reset = hit.resetsAt === null ? Number.NaN : Date.parse(hit.resetsAt);
|
|
79
|
+
// A stated reset already past (a clock skew, a stale message) is checked again soon.
|
|
80
|
+
const due = Number.isFinite(reset) && reset > now ? reset + resetMarginMs : now + recheckMs;
|
|
81
|
+
const retry = new Date(due).toISOString();
|
|
82
|
+
try {
|
|
83
|
+
deps.db
|
|
84
|
+
.prepare(`INSERT INTO plan_limit_waits (account, resets_at, retry_at, detected_at) VALUES (?, ?, ?, ?)
|
|
85
|
+
ON CONFLICT(account) DO UPDATE SET resets_at = excluded.resets_at, retry_at = excluded.retry_at, detected_at = excluded.detected_at
|
|
86
|
+
WHERE excluded.retry_at > plan_limit_waits.retry_at`)
|
|
87
|
+
.run(hit.account, Number.isFinite(reset) && reset > now ? new Date(reset).toISOString() : null, retry, new Date(now).toISOString());
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
deps.log.write("error", "plan-limits", { detail: causedBy(error) });
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
deps.log.write("info", "plan-limits", {
|
|
94
|
+
detail: `${hit.account} plan limit used up; waiting until ${retry}`,
|
|
95
|
+
});
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
// At start, a stage that was waiting on a plan limit when the app stopped was marked
|
|
100
|
+
// interrupted like any other (main.ts markInterruptedStages). Those projects are resumed so
|
|
101
|
+
// the wait carries on by itself - their calls meet the stored reset and wait again. A project
|
|
102
|
+
// paused or cancelled meanwhile is left as it is.
|
|
103
|
+
export async function resumeAfterRestart(db, resume, log) {
|
|
104
|
+
const projects = db
|
|
105
|
+
.prepare(`SELECT DISTINCT w.project_id AS id FROM plan_limit_waiters w
|
|
106
|
+
JOIN stages s ON s.project_id = w.project_id AND s.kind = w.stage
|
|
107
|
+
WHERE s.state = 'failed' AND s.failure_reason = 'interrupted'
|
|
108
|
+
AND NOT EXISTS (SELECT 1 FROM project_controls c WHERE c.project_id = w.project_id AND c.paused = 1)`)
|
|
109
|
+
.all()
|
|
110
|
+
.map((row) => z.string().parse(row.id));
|
|
111
|
+
db.prepare("DELETE FROM plan_limit_waiters").run();
|
|
112
|
+
let resumed = 0;
|
|
113
|
+
for (const projectId of projects) {
|
|
114
|
+
try {
|
|
115
|
+
if (await resume(projectId))
|
|
116
|
+
resumed += 1;
|
|
117
|
+
else
|
|
118
|
+
log.write("warn", "plan-limits", {
|
|
119
|
+
projectId,
|
|
120
|
+
detail: "A project waiting for CLI limits could not be resumed after the restart; use Continue the run on the project page.",
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
log.write("error", "plan-limits", { projectId, detail: causedBy(error) });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return resumed;
|
|
128
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { planAccountOf } from "../../kernel/ports/plan-limits.js";
|
|
2
|
+
import { resolveChannelId, scheduleChannel } from "../channels/repo.js";
|
|
3
|
+
import { priceCall } from "./pricing.js";
|
|
4
|
+
// Prices each call as it lands and keeps it with the rates it used; a CLI's plan windows go
|
|
5
|
+
// beside it. Separate from the anonymous telemetry log: this is per project, never leaves the
|
|
6
|
+
// machine, and is kept whether or not the usage-stats notice was dismissed.
|
|
7
|
+
export function createUsageMeter(deps) {
|
|
8
|
+
return {
|
|
9
|
+
record: (call) => {
|
|
10
|
+
const priced = priceCall(call, deps.catalogue());
|
|
11
|
+
const at = deps.clock.now().toISOString();
|
|
12
|
+
deps.db
|
|
13
|
+
.prepare(`INSERT INTO provider_usage (id, project_id, stage, kind, provider, model, tokens_in, tokens_out, tokens_cached, characters, images, seconds, size, quality, wall_ms, on_plan, cost, api_model, api_cost, price_json, created_at)
|
|
14
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
15
|
+
.run(deps.ids.next(), call.projectId, call.stage, call.kind, call.provider, call.model, call.tokensIn ?? null, call.tokensOut ?? null, call.cachedTokens ?? null, call.characters ?? null, call.images ?? null, call.seconds ?? null, call.size ?? null, call.quality ?? null, Math.round(call.wallMs), priced.onPlan ? 1 : 0, priced.cost, priced.apiModel, priced.apiCost, priced.price === null ? null : JSON.stringify(priced.price), at);
|
|
16
|
+
const account = planAccountOf(call.provider);
|
|
17
|
+
if (account !== undefined && call.limits !== undefined)
|
|
18
|
+
deps.db
|
|
19
|
+
.prepare("INSERT INTO plan_limit_readings (id, project_id, account, reading_json, created_at) VALUES (?, ?, ?, ?, ?)")
|
|
20
|
+
.run(deps.ids.next(), call.projectId, account, JSON.stringify(call.limits), at);
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
// The same pricing for a call made outside a project (`kernel/runner/standalone.ts`), kept
|
|
25
|
+
// against its schedule or channel with the channel it counts under on Home.
|
|
26
|
+
export function createStandaloneMeter(deps) {
|
|
27
|
+
return {
|
|
28
|
+
record: (call) => {
|
|
29
|
+
const priced = priceCall(call, deps.catalogue());
|
|
30
|
+
const account = planAccountOf(call.provider);
|
|
31
|
+
const reading = account !== undefined && call.limits !== undefined
|
|
32
|
+
? { account, json: JSON.stringify(call.limits) }
|
|
33
|
+
: undefined;
|
|
34
|
+
deps.db
|
|
35
|
+
.prepare(`INSERT INTO standalone_usage (id, owner_kind, owner_id, channel_id, purpose, kind, provider, model, tokens_in, tokens_out, tokens_cached, characters, images, seconds, size, quality, wall_ms, on_plan, cost, api_model, api_cost, price_json, account, reading_json, created_at)
|
|
36
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
37
|
+
.run(deps.ids.next(), call.owner.kind, call.owner.id, channelOf(deps.db, call.owner), call.purpose, call.kind, call.provider, call.model, call.tokensIn ?? null, call.tokensOut ?? null, call.cachedTokens ?? null, call.characters ?? null, call.images ?? null, call.seconds ?? null, call.size ?? null, call.quality ?? null, Math.round(call.wallMs), priced.onPlan ? 1 : 0, priced.cost, priced.apiModel, priced.apiCost, priced.price === null ? null : JSON.stringify(priced.price), reading?.account ?? null, reading?.json ?? null, deps.clock.now().toISOString());
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
// A schedule counts under its template's channel; one whose template is gone, like a channel
|
|
42
|
+
// this install doesn't have, under the default channel.
|
|
43
|
+
function channelOf(db, owner) {
|
|
44
|
+
return owner.kind === "schedule"
|
|
45
|
+
? resolveChannelId(db, scheduleChannel(db, owner.id)?.id)
|
|
46
|
+
: resolveChannelId(db, owner.id);
|
|
47
|
+
}
|