@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,25 @@
|
|
|
1
|
+
import { zValidator } from "@hono/zod-validator";
|
|
2
|
+
import { Hono } from "hono";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { projectExists } from "../../slices/admission/repo.js";
|
|
5
|
+
import { runCostOf } from "../../slices/run-cost/panel.js";
|
|
6
|
+
import { onInvalid, problem, titleOf } from "./problem.js";
|
|
7
|
+
const idParam = z.object({ id: z.string().min(1).max(64) });
|
|
8
|
+
// The project's Run cost tab and its "Waiting for … limits" line: what the run's provider
|
|
9
|
+
// calls used and cost, and any CLI plan limit a stage is waiting on. Read from the stored
|
|
10
|
+
// calls alone; nothing here reaches a provider.
|
|
11
|
+
//
|
|
12
|
+
// The return type is inferred so Hono keeps the route types the SPA's client is generated
|
|
13
|
+
// from; see stagingRoutes.
|
|
14
|
+
export function runCostRoutes(deps) {
|
|
15
|
+
return new Hono().get("/:id/run-cost", zValidator("param", idParam, onInvalid), (c) => {
|
|
16
|
+
const id = c.req.valid("param").id;
|
|
17
|
+
if (!projectExists(deps.db, id))
|
|
18
|
+
return problem(c, {
|
|
19
|
+
status: 404,
|
|
20
|
+
title: titleOf(404),
|
|
21
|
+
detail: "This project no longer exists. Go back to Projects to pick another.",
|
|
22
|
+
});
|
|
23
|
+
return c.json(runCostOf(deps.db, id));
|
|
24
|
+
});
|
|
25
|
+
}
|
|
@@ -2,14 +2,24 @@ import { zValidator } from "@hono/zod-validator";
|
|
|
2
2
|
import { Hono } from "hono";
|
|
3
3
|
import { HTTPException } from "hono/http-exception";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
-
import {
|
|
5
|
+
import { calendarRange } from "../../slices/schedules/agenda.js";
|
|
6
|
+
import { calendarMaxDays, calendarSchema, heldTopicSchema, queueMax, scheduleCreateSchema, scheduleRunSchema, scheduleSummarySchema, scheduleUpdateSchema, topicMoveSchema, topicTransferSchema, } from "../../slices/schedules/schema.js";
|
|
6
7
|
import { cancelSchedule, createSchedule, deleteSchedule, listScheduleRuns, listSchedules, pauseSchedule, resumeSchedule, updateSchedule, } from "../../slices/schedules/service.js";
|
|
8
|
+
import { approveHeldTopics, editHeldTopic, heldTopics, moveTopic, rejectHeldTopic, transferTopic, } from "../../slices/schedules/topics.js";
|
|
7
9
|
import { onInvalid, problem, titleOf } from "./problem.js";
|
|
8
10
|
const id = z.object({ id: z.uuid() });
|
|
11
|
+
const topicParam = z.object({ id: z.uuid(), topicId: z.string().min(1).max(100) });
|
|
9
12
|
function refused(c, result) {
|
|
10
|
-
const status = ["not-found", "missing-template"].includes(result.reason)
|
|
13
|
+
const status = ["not-found", "missing-template", "topic-not-found"].includes(result.reason)
|
|
11
14
|
? 404
|
|
12
|
-
: [
|
|
15
|
+
: [
|
|
16
|
+
"conflict",
|
|
17
|
+
"cancel-required",
|
|
18
|
+
"spend-limit",
|
|
19
|
+
"unsupported-media",
|
|
20
|
+
"queue-full",
|
|
21
|
+
"busy",
|
|
22
|
+
].includes(result.reason)
|
|
13
23
|
? 409
|
|
14
24
|
: 400;
|
|
15
25
|
const detail = {
|
|
@@ -18,15 +28,19 @@ function refused(c, result) {
|
|
|
18
28
|
conflict: "This schedule changed while you were editing, possibly because a run just started. Reload the page to see the latest, then make your change again.",
|
|
19
29
|
"cancel-required": "This schedule is still active. Cancel it first, then delete it. Completed schedules can be deleted straight away.",
|
|
20
30
|
"missing-template": "The template you picked was deleted or changed. Choose a template again, then save.",
|
|
21
|
-
"unsupported-media": "This template uses audio, images, a thumbnail
|
|
31
|
+
"unsupported-media": "This template uses audio, images, a thumbnail, shorts background music or an ambient sound file you supplied, so it cannot run on a schedule: a scheduled run has no one to attach the file again. Pick a template that generates these instead, or on Play remove the music under Outputs → Export → More shorts options (or pick Rain, Fireplace or Wind under Outputs → Export → Ambient sound) and save the template again.",
|
|
22
32
|
"not-due": "This one-off time has already passed. Choose a time in the future.",
|
|
23
33
|
"spend-limit": "The estimated cost is above this schedule's spend limit, or some prices are unknown. Raise the spend limit or choose models with known prices.",
|
|
24
34
|
readiness: "A provider this template needs is not ready, for example a missing API key, model or voice. Check Settings → Providers, then try again.",
|
|
35
|
+
"queue-full": `A schedule's topic queue holds at most ${String(queueMax)} topics. Remove some under Schedules → Edit, then try again.`,
|
|
36
|
+
"topic-not-found": "This topic was already approved, turned down or used. Reload the page to see the topics waiting now.",
|
|
37
|
+
busy: "Slopify is already generating topics for this schedule. Wait for it to finish, then try again.",
|
|
38
|
+
"invalid-topics": "Some topics name keywords this template does not use. Check each topic under Schedules → Edit and save again.",
|
|
25
39
|
};
|
|
26
40
|
return problem(c, {
|
|
27
41
|
status,
|
|
28
42
|
title: titleOf(status),
|
|
29
|
-
detail: detail[result.reason],
|
|
43
|
+
detail: result.message ?? detail[result.reason],
|
|
30
44
|
extensions: { reason: result.reason },
|
|
31
45
|
});
|
|
32
46
|
}
|
|
@@ -39,7 +53,7 @@ function routes(deps) {
|
|
|
39
53
|
throw new HTTPException(404);
|
|
40
54
|
return deps;
|
|
41
55
|
};
|
|
42
|
-
return new Hono()
|
|
56
|
+
return (new Hono()
|
|
43
57
|
.get("/", (c) => c.json({
|
|
44
58
|
schedules: listSchedules(service()).map((schedule) => scheduleSummarySchema.parse(schedule)),
|
|
45
59
|
}))
|
|
@@ -96,5 +110,96 @@ function routes(deps) {
|
|
|
96
110
|
id: c.req.valid("param").id,
|
|
97
111
|
});
|
|
98
112
|
return result.ok ? c.json(result.value) : refused(c, result);
|
|
113
|
+
})
|
|
114
|
+
.get("/:id/topics/held", zValidator("param", id, onInvalid), (c) => {
|
|
115
|
+
const scheduleId = c.req.valid("param").id;
|
|
116
|
+
if (listSchedules(service()).every((item) => item.id !== scheduleId))
|
|
117
|
+
return refused(c, { ok: false, reason: "not-found" });
|
|
118
|
+
return c.json({
|
|
119
|
+
topics: heldTopics(service(), scheduleId).map((topic) => heldTopicSchema.parse(topic)),
|
|
120
|
+
});
|
|
121
|
+
})
|
|
122
|
+
// Starts a generation in the background; the schedule's `topics` says when it is done.
|
|
123
|
+
.post("/:id/topics/generate", zValidator("param", id, onInvalid), (c) => {
|
|
124
|
+
const deps = service();
|
|
125
|
+
const scheduleId = c.req.valid("param").id;
|
|
126
|
+
const schedule = listSchedules(deps).find((item) => item.id === scheduleId);
|
|
127
|
+
if (schedule === undefined || schedule.deletedAt !== null)
|
|
128
|
+
return refused(c, { ok: false, reason: "not-found" });
|
|
129
|
+
const refusal = schedule.topicGeneration.mode === "off"
|
|
130
|
+
? "Topic generation is off for this schedule. Turn it on under Edit → Topic generation, then save."
|
|
131
|
+
: schedule.status === "canceled" || schedule.status === "completed"
|
|
132
|
+
? `This schedule is ${schedule.status}, so it needs no more topics.`
|
|
133
|
+
: schedule.topics.generatingSince !== null
|
|
134
|
+
? "Slopify is already generating topics for this schedule. They appear here when it finishes."
|
|
135
|
+
: deps.requestTopics === undefined
|
|
136
|
+
? "Topic generation is not available in this Slopify. Restart Slopify and try again."
|
|
137
|
+
: undefined;
|
|
138
|
+
if (refusal !== undefined)
|
|
139
|
+
return problem(c, {
|
|
140
|
+
status: 409,
|
|
141
|
+
title: titleOf(409),
|
|
142
|
+
detail: refusal,
|
|
143
|
+
extensions: { reason: "busy" },
|
|
144
|
+
});
|
|
145
|
+
deps.requestTopics?.(scheduleId);
|
|
146
|
+
return c.json({ started: true }, 202);
|
|
147
|
+
})
|
|
148
|
+
.post("/:id/topics/held/approve-all", zValidator("param", id, onInvalid), (c) => {
|
|
149
|
+
const result = approveHeldTopics(service(), c.req.valid("param").id, "all");
|
|
150
|
+
return result.ok ? c.json(scheduleSummarySchema.parse(result.value)) : refused(c, result);
|
|
151
|
+
})
|
|
152
|
+
.post("/:id/topics/held/:topicId/approve", zValidator("param", topicParam, onInvalid), zValidator("json", z.object({ title: z.string().optional() }).strict(), onInvalid), (c) => {
|
|
153
|
+
const { id: scheduleId, topicId } = c.req.valid("param");
|
|
154
|
+
const { title } = c.req.valid("json");
|
|
155
|
+
const result = approveHeldTopics(service(), scheduleId, [topicId], title === undefined ? {} : { [topicId]: title });
|
|
156
|
+
return result.ok ? c.json(scheduleSummarySchema.parse(result.value)) : refused(c, result);
|
|
157
|
+
})
|
|
158
|
+
.post("/:id/topics/held/:topicId/reject", zValidator("param", topicParam, onInvalid), (c) => {
|
|
159
|
+
const { id: scheduleId, topicId } = c.req.valid("param");
|
|
160
|
+
const result = rejectHeldTopic(service(), scheduleId, topicId);
|
|
161
|
+
return result.ok ? c.json(scheduleSummarySchema.parse(result.value)) : refused(c, result);
|
|
162
|
+
})
|
|
163
|
+
.put("/:id/topics/held/:topicId", zValidator("param", topicParam, onInvalid), zValidator("json", z.object({ title: z.string() }).strict(), onInvalid), (c) => {
|
|
164
|
+
const { id: scheduleId, topicId } = c.req.valid("param");
|
|
165
|
+
const result = editHeldTopic(service(), scheduleId, topicId, c.req.valid("json").title);
|
|
166
|
+
return result.ok ? c.json(heldTopicSchema.parse(result.value)) : refused(c, result);
|
|
167
|
+
})
|
|
168
|
+
.post("/:id/topics/move", zValidator("param", id, onInvalid), zValidator("json", topicMoveSchema, onInvalid), (c) => {
|
|
169
|
+
const result = moveTopic(service(), c.req.valid("param").id, c.req.valid("json"));
|
|
170
|
+
return result.ok ? c.json(scheduleSummarySchema.parse(result.value)) : refused(c, result);
|
|
171
|
+
})
|
|
172
|
+
.post("/:id/topics/transfer", zValidator("param", id, onInvalid), zValidator("json", topicTransferSchema, onInvalid), (c) => {
|
|
173
|
+
const result = transferTopic(service(), c.req.valid("param").id, c.req.valid("json"));
|
|
174
|
+
return result.ok
|
|
175
|
+
? c.json({
|
|
176
|
+
source: scheduleSummarySchema.parse(result.value.source),
|
|
177
|
+
target: scheduleSummarySchema.parse(result.value.target),
|
|
178
|
+
})
|
|
179
|
+
: refused(c, result);
|
|
180
|
+
}));
|
|
181
|
+
}
|
|
182
|
+
const calendarQuery = z
|
|
183
|
+
.object({ from: z.iso.datetime({ offset: true }), to: z.iso.datetime({ offset: true }) })
|
|
184
|
+
.partial();
|
|
185
|
+
// The coming weeks on one screen. Without `from`/`to`: now to four weeks from now.
|
|
186
|
+
export function calendarRoutes(deps) {
|
|
187
|
+
return new Hono().get("/", zValidator("query", calendarQuery, onInvalid), (c) => {
|
|
188
|
+
if (deps === undefined)
|
|
189
|
+
throw new HTTPException(404);
|
|
190
|
+
const query = c.req.valid("query");
|
|
191
|
+
const from = query.from === undefined ? deps.clock.now() : new Date(query.from);
|
|
192
|
+
const to = query.to === undefined
|
|
193
|
+
? new Date(from.valueOf() + 28 * 24 * 60 * 60_000)
|
|
194
|
+
: new Date(query.to);
|
|
195
|
+
const result = calendarRange(deps, from, to);
|
|
196
|
+
if (!result.ok)
|
|
197
|
+
return problem(c, {
|
|
198
|
+
status: 400,
|
|
199
|
+
title: titleOf(400),
|
|
200
|
+
detail: `Choose a range whose end is after its start and at most ${String(calendarMaxDays)} days long.`,
|
|
201
|
+
extensions: { reason: result.reason },
|
|
202
|
+
});
|
|
203
|
+
return c.json(calendarSchema.parse(result.value));
|
|
99
204
|
});
|
|
100
205
|
}
|
|
@@ -6,7 +6,9 @@ import { createNotificationSender, sendFailureText } from "../../slices/notifica
|
|
|
6
6
|
import { readNotificationUrl, saveNotificationUrl } from "../../slices/notifications/settings.js";
|
|
7
7
|
import { appearances, providerIds } from "../../slices/settings/model.js";
|
|
8
8
|
import { readSettings, saveSettings } from "../../slices/settings/playback.js";
|
|
9
|
-
import { addVoice, removeVoice, voiceIdMax, voiceNameMax, voices, } from "../../slices/settings/voices.js";
|
|
9
|
+
import { addVoice, removeVoice, setVoiceImitatesRealPerson, setVoiceLanguages, voiceIdMax, voiceNameMax, voices, } from "../../slices/settings/voices.js";
|
|
10
|
+
import { readChannelLinks, writeChannelLinks } from "../../slices/youtube/edits-repo.js";
|
|
11
|
+
import { channelLinkNameMax, channelLinksMax, channelLinksProblem, channelLinkUrlMax, } from "../../slices/youtube/placeholders.js";
|
|
10
12
|
import { onInvalid, problem, titleOf } from "./problem.js";
|
|
11
13
|
const idParam = z.object({ id: z.string().min(1).max(64) });
|
|
12
14
|
// Shape only. The rules - trimming, emptiness, length, the provider speaking at all -
|
|
@@ -15,13 +17,29 @@ const voiceBody = z.object({
|
|
|
15
17
|
provider: z.enum(providerIds),
|
|
16
18
|
name: z.string(),
|
|
17
19
|
voiceId: z.string(),
|
|
20
|
+
languages: z.array(z.string().max(20)).max(60).optional(),
|
|
18
21
|
});
|
|
22
|
+
const voiceLanguagesBody = z.object({ languages: z.array(z.string().max(20)).max(60) });
|
|
23
|
+
const voiceRealPersonBody = z.object({ imitatesRealPerson: z.boolean() });
|
|
19
24
|
// Length and scheme are the slice's rules, so their sentences reach the field; this bound only
|
|
20
25
|
// keeps a pasted novel off the parser.
|
|
21
26
|
const notificationBody = z.object({ url: z.string().max(notificationUrlMax * 2) });
|
|
27
|
+
// Shape and a generous bound only; `channelLinksProblem` says what is wrong in Settings' words.
|
|
28
|
+
export const channelLinksBody = z.object({
|
|
29
|
+
links: z
|
|
30
|
+
.array(z.object({
|
|
31
|
+
name: z.string().max(channelLinkNameMax * 4),
|
|
32
|
+
url: z.string().max(channelLinkUrlMax * 2),
|
|
33
|
+
}))
|
|
34
|
+
.max(channelLinksMax * 2),
|
|
35
|
+
});
|
|
22
36
|
const playbackBody = z.object({
|
|
23
37
|
silenceGapSeconds: z.number(),
|
|
24
38
|
appearance: z.enum(appearances),
|
|
39
|
+
// Absent keeps what is saved, so a client from before the setting still saves the others.
|
|
40
|
+
loudness: z
|
|
41
|
+
.object({ enabled: z.boolean(), videoLufs: z.number(), audioFilesLufs: z.number() })
|
|
42
|
+
.optional(),
|
|
25
43
|
});
|
|
26
44
|
// The return type is inferred so Hono keeps the route types the SPA's client is
|
|
27
45
|
// generated from; see stagingRoutes.
|
|
@@ -29,10 +47,14 @@ export function settingsRoutes(deps) {
|
|
|
29
47
|
const playback = { db: deps.db, log: deps.log };
|
|
30
48
|
const voiceDeps = { db: deps.db, ids: deps.ids };
|
|
31
49
|
const send = deps.sendNotification ?? createNotificationSender(globalThis.fetch);
|
|
32
|
-
return new Hono()
|
|
50
|
+
return (new Hono()
|
|
33
51
|
.get("/", (c) => c.json(readSettings(playback)))
|
|
34
52
|
.put("/", zValidator("json", playbackBody, onInvalid), (c) => {
|
|
35
|
-
const
|
|
53
|
+
const body = c.req.valid("json");
|
|
54
|
+
const result = saveSettings(playback, {
|
|
55
|
+
...body,
|
|
56
|
+
loudness: body.loudness ?? readSettings(playback).loudness,
|
|
57
|
+
});
|
|
36
58
|
if (!result.ok) {
|
|
37
59
|
return problem(c, {
|
|
38
60
|
status: 400,
|
|
@@ -42,6 +64,21 @@ export function settingsRoutes(deps) {
|
|
|
42
64
|
});
|
|
43
65
|
}
|
|
44
66
|
return c.json(result.settings);
|
|
67
|
+
})
|
|
68
|
+
// The named links `{{Name}}` placeholders in YouTube descriptions fill from
|
|
69
|
+
// (`slices/youtube/placeholders.ts`).
|
|
70
|
+
.get("/channel-links", (c) => c.json({ links: readChannelLinks(deps.db) }))
|
|
71
|
+
.put("/channel-links", zValidator("json", channelLinksBody, onInvalid), (c) => {
|
|
72
|
+
const { links } = c.req.valid("json");
|
|
73
|
+
const refused = channelLinksProblem(links);
|
|
74
|
+
if (refused !== undefined)
|
|
75
|
+
return problem(c, {
|
|
76
|
+
status: 400,
|
|
77
|
+
title: titleOf(400),
|
|
78
|
+
detail: `The channel links weren't saved: ${refused} Then press Save in Settings → Channel links.`,
|
|
79
|
+
});
|
|
80
|
+
writeChannelLinks(deps.db, links);
|
|
81
|
+
return c.json({ links: readChannelLinks(deps.db) });
|
|
45
82
|
})
|
|
46
83
|
.get("/notifications", (c) => c.json({ url: readNotificationUrl(deps.db) }))
|
|
47
84
|
.put("/notifications", zValidator("json", notificationBody, onInvalid), (c) => {
|
|
@@ -81,8 +118,16 @@ export function settingsRoutes(deps) {
|
|
|
81
118
|
return c.json({ sent: true });
|
|
82
119
|
})
|
|
83
120
|
.get("/voices", (c) => c.json({ voices: voices(voiceDeps) }))
|
|
84
|
-
.post("/voices", zValidator("json", voiceBody, onInvalid), (c) => {
|
|
85
|
-
const
|
|
121
|
+
.post("/voices", zValidator("json", voiceBody, onInvalid), async (c) => {
|
|
122
|
+
const body = c.req.valid("json");
|
|
123
|
+
// Languages left blank are asked of the provider, when it says; no answer is unknown.
|
|
124
|
+
const languages = body.languages !== undefined && body.languages.length > 0
|
|
125
|
+
? body.languages
|
|
126
|
+
: await deps.voiceLanguages?.(body.provider, body.voiceId.trim(), c.req.raw.signal);
|
|
127
|
+
const result = addVoice(voiceDeps, {
|
|
128
|
+
...body,
|
|
129
|
+
...(languages === undefined ? {} : { languages }),
|
|
130
|
+
});
|
|
86
131
|
if (!result.ok) {
|
|
87
132
|
// A voice ID already listed for its provider is a conflict with
|
|
88
133
|
// a row that exists, which the form shows under the Voice ID input.
|
|
@@ -100,6 +145,34 @@ export function settingsRoutes(deps) {
|
|
|
100
145
|
});
|
|
101
146
|
}
|
|
102
147
|
return c.json(result.voice, 201);
|
|
148
|
+
})
|
|
149
|
+
.put("/voices/:id/languages", zValidator("param", idParam, onInvalid), zValidator("json", voiceLanguagesBody, onInvalid), (c) => {
|
|
150
|
+
const result = setVoiceLanguages(voiceDeps, c.req.valid("param").id, c.req.valid("json").languages);
|
|
151
|
+
if (result.ok)
|
|
152
|
+
return c.body(null, 204);
|
|
153
|
+
return result.reason === "missing"
|
|
154
|
+
? problem(c, {
|
|
155
|
+
status: 404,
|
|
156
|
+
title: titleOf(404),
|
|
157
|
+
detail: "This voice no longer exists; it may have been deleted. Reload Settings → Voices to see your voices.",
|
|
158
|
+
})
|
|
159
|
+
: problem(c, {
|
|
160
|
+
status: 400,
|
|
161
|
+
title: titleOf(400),
|
|
162
|
+
detail: "The languages weren't saved. Fix the highlighted field and try again.",
|
|
163
|
+
extensions: { fields: [fieldOf("unknown-language")] },
|
|
164
|
+
});
|
|
165
|
+
})
|
|
166
|
+
// "Imitates a real person": the voice's narration answers Yes to YouTube's AI use.
|
|
167
|
+
.put("/voices/:id/real-person", zValidator("param", idParam, onInvalid), zValidator("json", voiceRealPersonBody, onInvalid), (c) => {
|
|
168
|
+
const result = setVoiceImitatesRealPerson(voiceDeps, c.req.valid("param").id, c.req.valid("json").imitatesRealPerson);
|
|
169
|
+
return result.ok
|
|
170
|
+
? c.body(null, 204)
|
|
171
|
+
: problem(c, {
|
|
172
|
+
status: 404,
|
|
173
|
+
title: titleOf(404),
|
|
174
|
+
detail: "This voice no longer exists; it may have been deleted. Reload Settings → Voices to see your voices.",
|
|
175
|
+
});
|
|
103
176
|
})
|
|
104
177
|
.delete("/voices/:id", zValidator("param", idParam, onInvalid), (c) => {
|
|
105
178
|
if (!removeVoice(voiceDeps, c.req.valid("param").id).ok) {
|
|
@@ -110,7 +183,7 @@ export function settingsRoutes(deps) {
|
|
|
110
183
|
});
|
|
111
184
|
}
|
|
112
185
|
return c.body(null, 204);
|
|
113
|
-
});
|
|
186
|
+
}));
|
|
114
187
|
}
|
|
115
188
|
function fieldOf(reason) {
|
|
116
189
|
switch (reason) {
|
|
@@ -131,6 +204,11 @@ function fieldOf(reason) {
|
|
|
131
204
|
field: "voiceId",
|
|
132
205
|
message: `A voice ID is at most ${String(voiceIdMax)} characters. Check you copied only the ID.`,
|
|
133
206
|
};
|
|
207
|
+
case "unknown-language":
|
|
208
|
+
return {
|
|
209
|
+
field: "languages",
|
|
210
|
+
message: "Enter language codes separated by commas, such as es, de or pt, or leave it blank to ask the provider.",
|
|
211
|
+
};
|
|
134
212
|
case "not-a-tts-provider":
|
|
135
213
|
return {
|
|
136
214
|
field: "provider",
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { zValidator } from "@hono/zod-validator";
|
|
2
|
+
import { Hono } from "hono";
|
|
3
|
+
import { HTTPException } from "hono/http-exception";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { onInvalid, problem, titleOf } from "./problem.js";
|
|
6
|
+
// Settings → Backup & storage → Your files: where projects and backups are, Open folder, and
|
|
7
|
+
// moving them. A move copies and checks every file, so it answers at once and the screen polls.
|
|
8
|
+
export function filesRoutes(deps) {
|
|
9
|
+
const files = () => {
|
|
10
|
+
if (deps.files === undefined)
|
|
11
|
+
throw new HTTPException(404);
|
|
12
|
+
return deps.files;
|
|
13
|
+
};
|
|
14
|
+
return new Hono()
|
|
15
|
+
.get("/", async (c) => {
|
|
16
|
+
c.header("Cache-Control", "no-store");
|
|
17
|
+
return c.json(await files().view());
|
|
18
|
+
})
|
|
19
|
+
.post("/move", zValidator("json", z.object({ target: z.string().min(1).max(4096) }).strict(), onInvalid), async (c) => {
|
|
20
|
+
const started = await files().move(c.req.valid("json").target);
|
|
21
|
+
if (!started.ok)
|
|
22
|
+
return problem(c, {
|
|
23
|
+
status: started.status,
|
|
24
|
+
title: titleOf(started.status),
|
|
25
|
+
detail: started.detail,
|
|
26
|
+
extensions: { field: "target" },
|
|
27
|
+
});
|
|
28
|
+
return c.json(await files().view(), 202);
|
|
29
|
+
})
|
|
30
|
+
.post("/open", async (c) => {
|
|
31
|
+
const origin = c.req.header("origin");
|
|
32
|
+
if (origin !== undefined && origin !== new URL(c.req.url).origin)
|
|
33
|
+
return problem(c, {
|
|
34
|
+
status: 403,
|
|
35
|
+
title: titleOf(403),
|
|
36
|
+
detail: "For your safety, folders can only be opened from the Slopify page itself. Open Slopify and try again there.",
|
|
37
|
+
});
|
|
38
|
+
try {
|
|
39
|
+
return c.json(await files().open());
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
deps.log.write("warn", "storage.open-files", { detail: String(error) });
|
|
43
|
+
return problem(c, {
|
|
44
|
+
status: 503,
|
|
45
|
+
title: titleOf(503),
|
|
46
|
+
detail: "Slopify could not open a file manager window on the computer it runs on. Make sure you are signed in to that computer's desktop, or open the folder shown in Settings → Backup & storage yourself.",
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
@@ -1,9 +1,19 @@
|
|
|
1
|
+
import { zValidator } from "@hono/zod-validator";
|
|
1
2
|
import { Hono } from "hono";
|
|
3
|
+
import { z } from "zod";
|
|
2
4
|
import { BackupBusyError, planBackup, streamBackup, } from "../../slices/storage/backup-export.js";
|
|
3
5
|
import { BackupImportRefused, importBackup } from "../../slices/storage/backup-import.js";
|
|
4
6
|
import { importPortable, portableMaxArchiveBytes, storageUsage, } from "../../slices/storage/portable.js";
|
|
5
7
|
import { reconcileStorage } from "../../slices/storage/reconcile.js";
|
|
6
|
-
import {
|
|
8
|
+
import { keepOutputsOnly } from "../../slices/storage/trim.js";
|
|
9
|
+
import { onInvalid, problem, titleOf } from "./problem.js";
|
|
10
|
+
const idParam = z.object({
|
|
11
|
+
id: z
|
|
12
|
+
.string()
|
|
13
|
+
.min(1)
|
|
14
|
+
.max(64)
|
|
15
|
+
.regex(/^[0-9A-Za-z_-]+$/),
|
|
16
|
+
});
|
|
7
17
|
const exportFailed = "Slopify could not create the backup. Check there is free disk space and try again; if it keeps failing, use Download diagnostics in Settings and report it.";
|
|
8
18
|
export function storageRoutes(deps) {
|
|
9
19
|
const backup = () => ({
|
|
@@ -15,7 +25,24 @@ export function storageRoutes(deps) {
|
|
|
15
25
|
hasInflight: deps.runner.hasInflight,
|
|
16
26
|
});
|
|
17
27
|
return (new Hono()
|
|
18
|
-
.get("/", (c) => c.json(storageUsage({ db: deps.db, paths: deps.paths })))
|
|
28
|
+
.get("/", (c) => c.json(storageUsage({ db: deps.db, paths: deps.paths, hasInflight: deps.runner.hasInflight })))
|
|
29
|
+
// Keep outputs only: a finished project drops its working files (`slices/storage/trim.ts`).
|
|
30
|
+
.post("/projects/:id/keep-outputs", zValidator("param", idParam, onInvalid), (c) => {
|
|
31
|
+
const result = keepOutputsOnly({ db: deps.db, paths: deps.paths, hasInflight: deps.runner.hasInflight }, c.req.valid("param").id);
|
|
32
|
+
if (result.ok)
|
|
33
|
+
return c.json(result);
|
|
34
|
+
return result.reason === "no-project"
|
|
35
|
+
? problem(c, {
|
|
36
|
+
status: 404,
|
|
37
|
+
title: titleOf(404),
|
|
38
|
+
detail: "This project no longer exists. Reload Settings → Storage to see the current list.",
|
|
39
|
+
})
|
|
40
|
+
: problem(c, {
|
|
41
|
+
status: 409,
|
|
42
|
+
title: titleOf(409),
|
|
43
|
+
detail: "Only a finished project can drop its working files, and this one is running, waiting or has unfinished steps. Let it finish (or cancel it on its project page), then use Keep outputs only again.",
|
|
44
|
+
});
|
|
45
|
+
})
|
|
19
46
|
// What Export everything would write, asked before the download starts: a download
|
|
20
47
|
// link cannot show a refusal, the browser would save it as the file.
|
|
21
48
|
.get("/export/summary", (c) => {
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { createReadStream } from "node:fs";
|
|
2
|
+
import { Readable } from "node:stream";
|
|
3
|
+
import { zValidator } from "@hono/zod-validator";
|
|
4
|
+
import { Hono } from "hono";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { findDownload } from "../../slices/storage/downloads.js";
|
|
7
|
+
import { studioPlaylistMax } from "../../slices/studio/model.js";
|
|
8
|
+
import { packItem, uploadPack } from "../../slices/studio/pack.js";
|
|
9
|
+
import { bearerToken, isExtensionOrigin, pairStudioExtension, readStudioPlaylist, resetStudioPairing, saveRealFootage, saveStudioPlaylist, studioPairing, studioPlaylistProblem, studioRequestAllowed, } from "../../slices/studio/settings.js";
|
|
10
|
+
import { onInvalid, problem, titleOf } from "./problem.js";
|
|
11
|
+
const id = z
|
|
12
|
+
.string()
|
|
13
|
+
.min(1)
|
|
14
|
+
.max(64)
|
|
15
|
+
.regex(/^[0-9A-Za-z_-]+$/);
|
|
16
|
+
const projectParam = z.object({ projectId: id });
|
|
17
|
+
const fileParam = z.object({
|
|
18
|
+
projectId: id,
|
|
19
|
+
asset: z
|
|
20
|
+
.string()
|
|
21
|
+
.max(64)
|
|
22
|
+
.regex(/^[a-z0-9-]+$/),
|
|
23
|
+
});
|
|
24
|
+
const chooseBody = z.object({ short: z.number().int().min(1).max(99).optional() });
|
|
25
|
+
const playlistBody = z.object({ playlist: z.string().max(studioPlaylistMax * 2) });
|
|
26
|
+
const realFootageBody = z.object({ realFootage: z.boolean() });
|
|
27
|
+
// How long a "Fill in YouTube Studio" choice stays the one the extension fills in.
|
|
28
|
+
const choiceMs = 6 * 60 * 60 * 1000;
|
|
29
|
+
// The Studio routes. The ones under `/ext` are the only routes of the app a page from another
|
|
30
|
+
// origin may read, and only the paired browser extension: every request carries the pairing
|
|
31
|
+
// token, and the CORS headers name the paired extension's origin, never a web page's and never
|
|
32
|
+
// `*`. The rest are the Slopify page's own, refused from any other origin.
|
|
33
|
+
export function studioRoutes(deps) {
|
|
34
|
+
// What the person chose to fill in last; kept in memory, so a restart asks them to choose
|
|
35
|
+
// again rather than filling in an old choice.
|
|
36
|
+
let chosen;
|
|
37
|
+
const samePage = (c) => {
|
|
38
|
+
const origin = c.req.header("origin");
|
|
39
|
+
if (origin === undefined || origin === new URL(c.req.url).origin)
|
|
40
|
+
return undefined;
|
|
41
|
+
return problem(c, {
|
|
42
|
+
status: 403,
|
|
43
|
+
title: titleOf(403),
|
|
44
|
+
detail: "For your safety, YouTube Studio settings can only be changed from the Slopify page itself. Open Slopify and try again there.",
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
// The CORS headers for the extension: its own origin, echoed only when it is allowed.
|
|
48
|
+
const allowOrigin = (c, pairing) => {
|
|
49
|
+
const origin = c.req.header("origin");
|
|
50
|
+
const paired = studioPairing(deps.db).origin;
|
|
51
|
+
c.header("Vary", "Origin");
|
|
52
|
+
if (!isExtensionOrigin(origin))
|
|
53
|
+
return;
|
|
54
|
+
if (origin === paired || pairing) {
|
|
55
|
+
c.header("Access-Control-Allow-Origin", origin);
|
|
56
|
+
c.header("Access-Control-Allow-Headers", "authorization, content-type");
|
|
57
|
+
c.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
58
|
+
c.header("Access-Control-Max-Age", "600");
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
const refused = (c) => problem(c, {
|
|
62
|
+
status: 401,
|
|
63
|
+
title: "Unauthorized",
|
|
64
|
+
detail: "The Slopify Studio extension isn't paired with this Slopify. Copy the pairing token from Slopify's Settings → YouTube Studio into the extension's options and press Pair.",
|
|
65
|
+
});
|
|
66
|
+
return (new Hono()
|
|
67
|
+
.get("/settings", (c) => {
|
|
68
|
+
// The token is read only by the Slopify page, never by another origin's.
|
|
69
|
+
const denied = samePage(c);
|
|
70
|
+
if (denied !== undefined)
|
|
71
|
+
return denied;
|
|
72
|
+
return c.json({ playlist: readStudioPlaylist(deps.db), pairing: studioPairing(deps.db) });
|
|
73
|
+
})
|
|
74
|
+
.put("/settings/playlist", zValidator("json", playlistBody, onInvalid), (c) => {
|
|
75
|
+
const denied = samePage(c);
|
|
76
|
+
if (denied !== undefined)
|
|
77
|
+
return denied;
|
|
78
|
+
const raw = c.req.valid("json").playlist;
|
|
79
|
+
const invalid = studioPlaylistProblem(raw);
|
|
80
|
+
if (invalid !== undefined)
|
|
81
|
+
return problem(c, {
|
|
82
|
+
status: 400,
|
|
83
|
+
title: titleOf(400),
|
|
84
|
+
detail: invalid,
|
|
85
|
+
extensions: { fields: [{ field: "playlist", message: invalid }] },
|
|
86
|
+
});
|
|
87
|
+
return c.json({ playlist: saveStudioPlaylist(deps.db, raw) });
|
|
88
|
+
})
|
|
89
|
+
.post("/settings/pairing", (c) => {
|
|
90
|
+
const denied = samePage(c);
|
|
91
|
+
if (denied !== undefined)
|
|
92
|
+
return denied;
|
|
93
|
+
return c.json({ pairing: resetStudioPairing(deps.db) });
|
|
94
|
+
})
|
|
95
|
+
.get("/packs/:projectId", zValidator("param", projectParam, onInvalid), (c) => {
|
|
96
|
+
const result = uploadPack(deps, c.req.valid("param").projectId);
|
|
97
|
+
if (!result.ok)
|
|
98
|
+
return unknownProject(c);
|
|
99
|
+
return c.json(result.pack);
|
|
100
|
+
})
|
|
101
|
+
// Prepare upload's AI use tick: the project's uploaded clips are real footage.
|
|
102
|
+
.put("/packs/:projectId/real-footage", zValidator("param", projectParam, onInvalid), zValidator("json", realFootageBody, onInvalid), (c) => {
|
|
103
|
+
const denied = samePage(c);
|
|
104
|
+
if (denied !== undefined)
|
|
105
|
+
return denied;
|
|
106
|
+
const { projectId } = c.req.valid("param");
|
|
107
|
+
if (!uploadPack(deps, projectId).ok)
|
|
108
|
+
return unknownProject(c);
|
|
109
|
+
saveRealFootage(deps.db, projectId, c.req.valid("json").realFootage);
|
|
110
|
+
const result = uploadPack(deps, projectId);
|
|
111
|
+
return result.ok ? c.json(result.pack) : unknownProject(c);
|
|
112
|
+
})
|
|
113
|
+
.post("/packs/:projectId/choose", zValidator("param", projectParam, onInvalid), zValidator("json", chooseBody, onInvalid), (c) => {
|
|
114
|
+
const denied = samePage(c);
|
|
115
|
+
if (denied !== undefined)
|
|
116
|
+
return denied;
|
|
117
|
+
const { projectId } = c.req.valid("param");
|
|
118
|
+
const { short } = c.req.valid("json");
|
|
119
|
+
const result = uploadPack(deps, projectId);
|
|
120
|
+
if (!result.ok)
|
|
121
|
+
return unknownProject(c);
|
|
122
|
+
if (packItem(result.pack, short) === undefined)
|
|
123
|
+
return problem(c, {
|
|
124
|
+
status: 404,
|
|
125
|
+
title: titleOf(404),
|
|
126
|
+
detail: `This project has no short ${String(short)}. Reload the project page and press Prepare upload again.`,
|
|
127
|
+
});
|
|
128
|
+
chosen = { projectId, short, at: deps.clock.now().getTime() };
|
|
129
|
+
return c.json({ chosen: { projectId, short: short ?? null } });
|
|
130
|
+
})
|
|
131
|
+
// The extension's routes.
|
|
132
|
+
.options("/ext/pair", (c) => {
|
|
133
|
+
allowOrigin(c, true);
|
|
134
|
+
return c.body(null, 204);
|
|
135
|
+
})
|
|
136
|
+
.options("/ext/*", (c) => {
|
|
137
|
+
allowOrigin(c, false);
|
|
138
|
+
return c.body(null, 204);
|
|
139
|
+
})
|
|
140
|
+
.post("/ext/pair", (c) => {
|
|
141
|
+
allowOrigin(c, true);
|
|
142
|
+
const result = pairStudioExtension(deps.db, bearerToken(c.req.header("authorization")), c.req.header("origin"), deps.clock.now());
|
|
143
|
+
if (!result.ok)
|
|
144
|
+
return result.reason === "bad-token"
|
|
145
|
+
? refused(c)
|
|
146
|
+
: problem(c, {
|
|
147
|
+
status: 403,
|
|
148
|
+
title: titleOf(403),
|
|
149
|
+
detail: "Only the Slopify Studio browser extension can pair with Slopify. Install it (see Settings → YouTube Studio) and pair from its options page.",
|
|
150
|
+
});
|
|
151
|
+
// The origin is paired now; the response carries its headers.
|
|
152
|
+
allowOrigin(c, false);
|
|
153
|
+
return c.json({ paired: true, origin: result.pairing.origin });
|
|
154
|
+
})
|
|
155
|
+
.get("/ext/pack", (c) => {
|
|
156
|
+
allowOrigin(c, false);
|
|
157
|
+
if (!studioRequestAllowed(deps.db, bearerToken(c.req.header("authorization")), c.req.header("origin")))
|
|
158
|
+
return refused(c);
|
|
159
|
+
const fresh = chosen !== undefined && deps.clock.now().getTime() - chosen.at <= choiceMs
|
|
160
|
+
? chosen
|
|
161
|
+
: undefined;
|
|
162
|
+
if (fresh === undefined)
|
|
163
|
+
return problem(c, {
|
|
164
|
+
status: 404,
|
|
165
|
+
title: titleOf(404),
|
|
166
|
+
detail: "Nothing is chosen to fill in. In Slopify, open the project, press Prepare upload, then Fill in YouTube Studio.",
|
|
167
|
+
});
|
|
168
|
+
const result = uploadPack(deps, fresh.projectId);
|
|
169
|
+
const item = result.ok ? packItem(result.pack, fresh.short) : undefined;
|
|
170
|
+
if (!result.ok || item === undefined)
|
|
171
|
+
return problem(c, {
|
|
172
|
+
status: 404,
|
|
173
|
+
title: titleOf(404),
|
|
174
|
+
detail: "The project chosen to fill in is gone. In Slopify, open the project again and press Fill in YouTube Studio.",
|
|
175
|
+
});
|
|
176
|
+
return c.json({ pack: result.pack, item });
|
|
177
|
+
})
|
|
178
|
+
.get("/ext/files/:projectId/:asset", zValidator("param", fileParam, onInvalid), (c) => {
|
|
179
|
+
allowOrigin(c, false);
|
|
180
|
+
if (!studioRequestAllowed(deps.db, bearerToken(c.req.header("authorization")), c.req.header("origin")))
|
|
181
|
+
return refused(c);
|
|
182
|
+
const { projectId, asset } = c.req.valid("param");
|
|
183
|
+
// Only the thumbnails of a pack: the extension needs nothing else from the disk.
|
|
184
|
+
const result = uploadPack(deps, projectId);
|
|
185
|
+
const listed = result.ok &&
|
|
186
|
+
result.pack.items.some((item) => item.thumbnails.some((thumbnail) => thumbnail.asset === asset));
|
|
187
|
+
const found = listed ? findDownload(deps, projectId, asset) : undefined;
|
|
188
|
+
if (found === undefined || !found.ok)
|
|
189
|
+
return problem(c, {
|
|
190
|
+
status: 404,
|
|
191
|
+
title: titleOf(404),
|
|
192
|
+
detail: "That thumbnail isn't in the project's upload pack any more. Reload the project in Slopify and press Fill in YouTube Studio again.",
|
|
193
|
+
});
|
|
194
|
+
return c.body(Readable.toWeb(createReadStream(found.download.path)), 200, {
|
|
195
|
+
"content-type": found.download.contentType,
|
|
196
|
+
"content-length": String(found.download.bytes),
|
|
197
|
+
"x-content-type-options": "nosniff",
|
|
198
|
+
});
|
|
199
|
+
}));
|
|
200
|
+
}
|
|
201
|
+
function unknownProject(c) {
|
|
202
|
+
return problem(c, {
|
|
203
|
+
status: 404,
|
|
204
|
+
title: titleOf(404),
|
|
205
|
+
detail: "This project no longer exists. Go back to Projects and open it again.",
|
|
206
|
+
});
|
|
207
|
+
}
|