@gentbajko/slopify 2.4.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -21
- package/NOTICE +12 -0
- package/README.md +21 -40
- package/SUBTITLES.md +1 -1
- package/dist/adapter-registry.js +11 -1
- package/dist/adapters/alignment/cache.js +4 -1
- package/dist/adapters/alignment/ctc.js +25 -23
- package/dist/adapters/alignment/index.js +37 -6
- package/dist/adapters/alignment/multilingual.js +267 -0
- package/dist/adapters/alignment/protocol.js +11 -0
- package/dist/adapters/alignment/quality.js +4 -3
- package/dist/adapters/alignment/sentences.js +95 -0
- package/dist/adapters/alignment/spec.js +24 -0
- package/dist/adapters/alignment/spell.js +769 -0
- package/dist/adapters/alignment/text.js +56 -9
- package/dist/adapters/alignment/window.js +34 -30
- package/dist/adapters/alignment/worker.js +23 -21
- package/dist/adapters/explain.js +11 -11
- package/dist/adapters/host-cli/index.js +55 -5
- package/dist/adapters/host-cli/transport.js +13 -6
- package/dist/adapters/image/bytes.js +2 -2
- package/dist/adapters/image/codex-output.js +60 -18
- package/dist/adapters/image/codex.js +166 -25
- package/dist/adapters/image/fal.js +31 -3
- package/dist/adapters/image/google.js +17 -2
- package/dist/adapters/image/openai.js +45 -16
- package/dist/adapters/image/reference.js +41 -0
- package/dist/adapters/image/replicate.js +14 -2
- package/dist/adapters/image/video.js +2 -2
- package/dist/adapters/key-probes.js +36 -0
- package/dist/adapters/llm/claude-code.js +168 -32
- package/dist/adapters/llm/cli-login-error.js +1 -1
- package/dist/adapters/llm/codex-limits.js +157 -0
- package/dist/adapters/llm/codex-models.js +2 -1
- package/dist/adapters/llm/codex-server-models.js +2 -1
- package/dist/adapters/llm/codex.js +99 -12
- package/dist/adapters/llm/document-workspace.js +2 -2
- package/dist/adapters/llm/gemini-workspace.js +1 -1
- package/dist/adapters/llm/gemini.js +16 -3
- package/dist/adapters/llm/image-workspace.js +58 -0
- package/dist/adapters/llm/openrouter.js +11 -3
- package/dist/adapters/llm/run-cli.js +26 -9
- package/dist/adapters/tts/cartesia.js +10 -0
- package/dist/adapters/tts/elevenlabs.js +31 -9
- package/dist/adapters/tts/inworld-async.js +9 -5
- package/dist/adapters/tts/inworld.js +17 -3
- package/dist/adapters/tts/openai.js +3 -0
- package/dist/adapters/tts/voice-languages.js +80 -0
- package/dist/assets/fonts/noto/NotoSans-Regular.ttf +0 -0
- package/dist/assets/fonts/noto/NotoSansArabic-Regular.ttf +0 -0
- package/dist/assets/fonts/noto/NotoSansDevanagari-Regular.ttf +0 -0
- package/dist/assets/fonts/noto/NotoSansHebrew-Regular.ttf +0 -0
- package/dist/assets/fonts/noto/NotoSansThai-Regular.ttf +0 -0
- package/dist/assets/fonts/noto/OFL.txt +93 -0
- package/dist/assets/fonts/noto/SOURCE.txt +18 -0
- package/dist/assets/models.yaml +7 -0
- package/dist/assets/sample/sample-audiobook.tar +0 -0
- package/dist/assets/sample/sample-podcast.tar +0 -0
- package/dist/assets/sample/sample-project.tar +0 -0
- package/dist/catalog/merge.js +132 -0
- package/dist/catalog/registry.js +33 -12
- package/dist/catalog/runtime-models.js +1 -1
- package/dist/catalog/schema.js +12 -1
- package/dist/catalog/store.js +147 -8
- package/dist/catalog/validate.js +17 -2
- package/dist/compose.yaml +71 -0
- package/dist/edge/autostart/docker-record.js +32 -0
- package/dist/edge/autostart/docker.js +50 -0
- package/dist/edge/autostart/index.js +78 -0
- package/dist/edge/autostart/launcher.js +176 -0
- package/dist/edge/autostart/model.js +3 -0
- package/dist/edge/autostart/native.js +210 -0
- package/dist/edge/autostart/prompt.js +66 -0
- package/dist/edge/autostart/service.js +81 -0
- package/dist/edge/cli.js +57 -51
- package/dist/edge/docker-install/activation.js +56 -0
- package/dist/edge/docker-install/apply.js +458 -0
- package/dist/edge/docker-install/engine.js +362 -0
- package/dist/edge/docker-install/run.js +154 -0
- package/dist/edge/docker-install/state.js +146 -0
- package/dist/edge/{docker-projects → docker-install}/volume.js +1 -5
- package/dist/edge/docker.js +14 -22
- package/dist/edge/events/hub.js +11 -0
- package/dist/edge/host-cli.js +1 -1
- package/dist/edge/http/actions.js +32 -10
- package/dist/edge/http/app.js +60 -2
- package/dist/edge/http/auditions.js +70 -0
- package/dist/edge/http/autostart.js +37 -0
- package/dist/edge/http/backups.js +40 -0
- package/dist/edge/http/channel-memory.js +92 -0
- package/dist/edge/http/channels.js +128 -0
- package/dist/edge/http/entries.js +22 -3
- package/dist/edge/http/files.js +1 -1
- package/dist/edge/http/folder-location.js +1 -1
- package/dist/edge/http/home.js +39 -0
- package/dist/edge/http/host-cli.js +45 -6
- package/dist/edge/http/narration-peaks.js +76 -0
- package/dist/edge/http/onboarding.js +152 -0
- package/dist/edge/http/open-folder.js +1 -1
- package/dist/edge/http/patch-notes.js +45 -0
- package/dist/edge/http/planning.js +4 -2
- package/dist/edge/http/problem.js +1 -0
- package/dist/edge/http/project-create.js +87 -0
- package/dist/edge/http/projects.js +31 -99
- package/dist/edge/http/prompts.js +40 -3
- package/dist/edge/http/pronunciations.js +23 -4
- package/dist/edge/http/providers.js +86 -0
- package/dist/edge/http/reviews.js +121 -0
- package/dist/edge/http/revision-files.js +1 -1
- package/dist/edge/http/revisions.js +1 -1
- package/dist/edge/http/run-cost.js +25 -0
- package/dist/edge/http/schedules.js +111 -6
- package/dist/edge/http/settings.js +128 -6
- package/dist/edge/http/storage-files.js +50 -0
- package/dist/edge/http/storage.js +29 -2
- package/dist/edge/http/studio.js +207 -0
- package/dist/edge/http/style-preview.js +100 -0
- package/dist/edge/http/subtitles.js +1 -1
- package/dist/edge/http/trash.js +69 -0
- package/dist/edge/http/update.js +24 -3
- package/dist/edge/http/whats-new.js +15 -0
- package/dist/edge/http/youtube-edits.js +83 -0
- package/dist/edge/native-update.js +62 -0
- package/dist/host-cli/open-folder.js +10 -10
- package/dist/host-cli/runtime.js +12 -3
- package/dist/kernel/db/migrations/0022-drop-duplicate-held-work.sql +56 -0
- package/dist/kernel/db/migrations/0024-reference-attachments.sql +18 -0
- package/dist/kernel/db/migrations/0025-automatic-reviews.sql +40 -0
- package/dist/kernel/db/migrations/0026-schedule-topic-generation.sql +23 -0
- package/dist/kernel/db/migrations/0027-run-cost.sql +58 -0
- package/dist/kernel/db/migrations/0030-automatic-retries.sql +18 -0
- package/dist/kernel/db/migrations/0031-prompt-history-and-description-edits.sql +33 -0
- package/dist/kernel/db/migrations/0032-channels.sql +65 -0
- package/dist/kernel/db/migrations/0034-script-prompts.sql +23 -0
- package/dist/kernel/db/migrations/0035-narration-aliases.sql +13 -0
- package/dist/kernel/db/migrations/0037-project-uploads.sql +8 -0
- package/dist/kernel/db/migrations/0038-voice-languages.sql +7 -0
- package/dist/kernel/db/migrations/0039-channel-essentials.sql +64 -0
- package/dist/kernel/db/migrations/0040-standalone-usage.sql +35 -0
- package/dist/kernel/paths.js +27 -6
- package/dist/kernel/pipeline.js +4 -1
- package/dist/kernel/ports/host-cli.js +139 -16
- package/dist/kernel/ports/image.js +3 -1
- package/dist/kernel/ports/key-probe.js +1 -0
- package/dist/kernel/ports/languages.js +157 -0
- package/dist/kernel/ports/llm.js +3 -1
- package/dist/kernel/ports/model.js +5 -2
- package/dist/kernel/ports/narration-aliases.js +60 -0
- package/dist/kernel/ports/plan-limits.js +36 -0
- package/dist/kernel/runner/attempt.js +18 -9
- package/dist/kernel/runner/audition.js +36 -0
- package/dist/kernel/runner/graph.js +24 -5
- package/dist/kernel/runner/index.js +35 -5
- package/dist/kernel/runner/meter.js +1 -0
- package/dist/kernel/runner/providers.js +164 -9
- package/dist/kernel/runner/retry-policy.js +43 -0
- package/dist/kernel/runner/standalone.js +134 -0
- package/dist/kernel/runner/work-authority.js +28 -2
- package/dist/main.js +321 -20
- package/dist/patch-notes/0.4.2-to-1.0.md +93 -0
- package/dist/patch-notes/1.0-to-2.0.md +74 -0
- package/dist/patch-notes/2.0-to-3.0.md +257 -0
- package/dist/patch-notes/3.0.0.md +70 -0
- package/dist/patch-notes/index.json +26 -0
- package/dist/slices/admission/model.js +35 -0
- package/dist/slices/admission/repo.js +24 -4
- package/dist/slices/admission/rules.js +126 -1
- package/dist/slices/admission/schema.js +68 -1
- package/dist/slices/admission/short-mode.js +47 -0
- package/dist/slices/admission/start.js +90 -16
- package/dist/slices/article/continuation.js +2 -2
- package/dist/slices/article/provided-entries.js +2 -2
- package/dist/slices/article/run.js +6 -2
- package/dist/slices/article/segments.js +3 -3
- package/dist/slices/backups/files.js +135 -0
- package/dist/slices/backups/folder.js +43 -0
- package/dist/slices/backups/model.js +65 -0
- package/dist/slices/backups/repo.js +28 -0
- package/dist/slices/backups/schedule.js +60 -0
- package/dist/slices/backups/service.js +311 -0
- package/dist/slices/batch/index.js +9 -2
- package/dist/slices/cancel/index.js +1 -1
- package/dist/slices/channels/cast-images.js +110 -0
- package/dist/slices/channels/cast-match.js +32 -0
- package/dist/slices/channels/images.js +19 -0
- package/dist/slices/channels/model.js +5 -0
- package/dist/slices/channels/rebrand.js +100 -0
- package/dist/slices/channels/repo.js +218 -0
- package/dist/slices/channels/runs.js +129 -0
- package/dist/slices/channels/schema.js +129 -0
- package/dist/slices/channels/service.js +180 -0
- package/dist/slices/channels/studio-csv.js +110 -0
- package/dist/slices/channels/videos.js +120 -0
- package/dist/slices/checkpoints/recovery.js +10 -0
- package/dist/slices/control/index.js +2 -1
- package/dist/slices/document/fonts.js +1 -1
- package/dist/slices/document/render.js +1 -1
- package/dist/slices/episodes/related.js +90 -0
- package/dist/slices/episodes/repo.js +60 -0
- package/dist/slices/episodes/service.js +58 -0
- package/dist/slices/episodes/summarize.js +170 -0
- package/dist/slices/estimate/index.js +113 -24
- package/dist/slices/estimate/requests.js +60 -2
- package/dist/slices/fixes/rules.js +73 -0
- package/dist/slices/fonts/coverage.js +82 -0
- package/dist/slices/fonts/sfnt.js +74 -0
- package/dist/slices/images/run.js +8 -5
- package/dist/slices/images/scale.js +84 -0
- package/dist/slices/library/diff.js +122 -0
- package/dist/slices/library/history.js +100 -0
- package/dist/slices/library/model.js +3 -0
- package/dist/slices/library/photorealistic.js +49 -0
- package/dist/slices/library/repo.js +19 -14
- package/dist/slices/library/save.js +62 -12
- package/dist/slices/library/slots.js +15 -3
- package/dist/slices/library/snapshot.js +8 -2
- package/dist/slices/library/used-by.js +153 -0
- package/dist/slices/loudness/level-pieces.js +41 -0
- package/dist/slices/loudness/loudnorm.js +164 -0
- package/dist/slices/loudness/model.js +153 -0
- package/dist/slices/model-upkeep/model.js +10 -0
- package/dist/slices/model-upkeep/switch.js +217 -0
- package/dist/slices/model-upkeep/usage.js +183 -0
- package/dist/slices/narration/aliases-library.js +31 -0
- package/dist/slices/narration/aliases-schema.js +53 -0
- package/dist/slices/narration/aliases.js +49 -0
- package/dist/slices/narration/blocks.js +352 -0
- package/dist/slices/narration/chunk.js +19 -7
- package/dist/slices/narration/concat.js +2 -2
- package/dist/slices/narration/describe.js +45 -0
- package/dist/slices/narration/pauses-model.js +63 -0
- package/dist/slices/narration/pauses.js +269 -0
- package/dist/slices/narration/peaks-model.js +1 -0
- package/dist/slices/narration/peaks.js +94 -0
- package/dist/slices/narration/preparation.js +21 -3
- package/dist/slices/narration/pronunciation-chunks.js +3 -3
- package/dist/slices/narration/pronunciation.js +72 -25
- package/dist/slices/narration/run.js +6 -6
- package/dist/slices/narration/shared-glossary.js +1 -1
- package/dist/slices/narration/steering.js +15 -4
- package/dist/slices/notifications/notifier.js +94 -0
- package/dist/slices/notifications/rules.js +102 -0
- package/dist/slices/notifications/send.js +40 -0
- package/dist/slices/notifications/settings.js +34 -0
- package/dist/slices/onboarding/copy.js +143 -0
- package/dist/slices/onboarding/first-run.js +46 -0
- package/dist/slices/onboarding/install.js +119 -0
- package/dist/slices/onboarding/model.js +64 -0
- package/dist/slices/onboarding/packs.js +491 -0
- package/dist/slices/onboarding/quick-short.js +129 -0
- package/dist/slices/onboarding/sample.js +132 -0
- package/dist/slices/onboarding/state.js +68 -0
- package/dist/slices/onboarding/template.js +73 -0
- package/dist/slices/patch-notes/library.js +73 -0
- package/dist/slices/patch-notes/seen.js +77 -0
- package/dist/slices/play-drafts/convert.js +160 -9
- package/dist/slices/play-drafts/readiness.js +3 -0
- package/dist/slices/play-drafts/repo.js +9 -1
- package/dist/slices/play-drafts/review-inputs.js +22 -3
- package/dist/slices/play-drafts/schema.js +109 -2
- package/dist/slices/play-drafts/service.js +10 -1
- package/dist/slices/play-drafts/start.js +43 -18
- package/dist/slices/play-drafts/uploads.js +2 -1
- package/dist/slices/project-templates/from-project.js +70 -4
- package/dist/slices/project-templates/one-off.js +26 -0
- package/dist/slices/project-templates/repo.js +2 -1
- package/dist/slices/project-templates/service.js +15 -4
- package/dist/slices/project-templates/setup.js +29 -2
- package/dist/slices/rebuild/admission-repo.js +1 -1
- package/dist/slices/rebuild/model.js +6 -0
- package/dist/slices/rebuild/preview-details.js +67 -2
- package/dist/slices/rebuild/preview-plan.js +13 -6
- package/dist/slices/rebuild/recipe-audio-parts.js +40 -7
- package/dist/slices/rebuild/recipe-audio.js +66 -12
- package/dist/slices/rebuild/recipe-build.js +15 -5
- package/dist/slices/rebuild/recipe-cast.js +18 -0
- package/dist/slices/rebuild/recipe-checkpoints.js +11 -9
- package/dist/slices/rebuild/recipe-describe.js +93 -0
- package/dist/slices/rebuild/recipe-edit.js +13 -1
- package/dist/slices/rebuild/recipe-exports.js +62 -8
- package/dist/slices/rebuild/recipe-input-schema.js +43 -1
- package/dist/slices/rebuild/recipe-legacy.js +12 -0
- package/dist/slices/rebuild/recipe-loudness.js +45 -0
- package/dist/slices/rebuild/recipe-model.js +25 -6
- package/dist/slices/rebuild/recipe-pauses.js +7 -0
- package/dist/slices/rebuild/recipe-preparation.js +3 -3
- package/dist/slices/rebuild/recipe-provider-choice.js +11 -1
- package/dist/slices/rebuild/recipe-reference.js +58 -0
- package/dist/slices/rebuild/recipe-reviews.js +117 -0
- package/dist/slices/rebuild/recipe-save.js +4 -1
- package/dist/slices/rebuild/recipe-shorts.js +32 -5
- package/dist/slices/rebuild/recipe-text.js +114 -19
- package/dist/slices/rebuild/recipe-validation.js +29 -3
- package/dist/slices/rebuild/recipe-visual.js +104 -29
- package/dist/slices/rebuild/recipe-voices.js +286 -0
- package/dist/slices/rebuild/recipe-work.js +77 -6
- package/dist/slices/rebuild/recipe-youtube.js +2 -0
- package/dist/slices/rebuild/recovery-conflict.js +7 -3
- package/dist/slices/rebuild/recovery-model.js +3 -0
- package/dist/slices/rebuild/recovery-selection.js +28 -2
- package/dist/slices/rebuild/recovery.js +35 -17
- package/dist/slices/rebuild/repo.js +3 -3
- package/dist/slices/rebuild/retry.js +18 -0
- package/dist/slices/rebuild/review-redo.js +110 -0
- package/dist/slices/rebuild/runtime-actions.js +14 -2
- package/dist/slices/rebuild/runtime-admission.js +4 -1
- package/dist/slices/rebuild/runtime-animate.js +4 -4
- package/dist/slices/rebuild/runtime-article.js +5 -5
- package/dist/slices/rebuild/runtime-document.js +4 -4
- package/dist/slices/rebuild/runtime-export-bed.js +43 -0
- package/dist/slices/rebuild/runtime-export-edit.js +70 -11
- package/dist/slices/rebuild/runtime-export-inputs.js +52 -10
- package/dist/slices/rebuild/runtime-export-short.js +85 -0
- package/dist/slices/rebuild/runtime-export.js +84 -19
- package/dist/slices/rebuild/runtime-figure-card.js +87 -0
- package/dist/slices/rebuild/runtime-image.js +69 -0
- package/dist/slices/rebuild/runtime-local.js +154 -27
- package/dist/slices/rebuild/runtime-materialize.js +8 -0
- package/dist/slices/rebuild/runtime-narration-reuse.js +2 -1
- package/dist/slices/rebuild/runtime-narration-text.js +7 -7
- package/dist/slices/rebuild/runtime-pictures.js +28 -0
- package/dist/slices/rebuild/runtime-piece-label.js +29 -0
- package/dist/slices/rebuild/runtime-provider.js +134 -19
- package/dist/slices/rebuild/runtime-publication.js +21 -5
- package/dist/slices/rebuild/runtime-review.js +237 -0
- package/dist/slices/rebuild/runtime-run.js +17 -11
- package/dist/slices/rebuild/runtime-shorts.js +48 -23
- package/dist/slices/rebuild/runtime-store.js +39 -10
- package/dist/slices/rebuild/runtime-subtitles.js +72 -19
- package/dist/slices/rebuild/runtime-voices.js +103 -0
- package/dist/slices/rebuild/runtime-youtube.js +7 -6
- package/dist/slices/rebuild/service-readiness.js +15 -1
- package/dist/slices/rebuild/soften.js +49 -0
- package/dist/slices/rebuild/transition-repo.js +10 -3
- package/dist/slices/research/run.js +4 -4
- package/dist/slices/research/synthesis.js +2 -2
- package/dist/slices/reviews/model.js +105 -0
- package/dist/slices/reviews/outcome.js +22 -0
- package/dist/slices/reviews/repo.js +106 -0
- package/dist/slices/reviews/rules.js +63 -0
- package/dist/slices/reviews/verdict.js +71 -0
- package/dist/slices/reviews/view.js +30 -0
- package/dist/slices/revisions/adopt-content.js +12 -2
- package/dist/slices/revisions/adopt.js +9 -2
- package/dist/slices/revisions/downloads.js +7 -1
- package/dist/slices/revisions/image-plan.js +183 -0
- package/dist/slices/revisions/mutation-assets.js +43 -9
- package/dist/slices/revisions/mutation-cues.js +71 -0
- package/dist/slices/revisions/mutation-prepare.js +8 -10
- package/dist/slices/revisions/mutation-request.js +2 -1
- package/dist/slices/revisions/mutations.js +97 -3
- package/dist/slices/revisions/schema.js +16 -2
- package/dist/slices/run-cost/limits.js +128 -0
- package/dist/slices/run-cost/meter.js +47 -0
- package/dist/slices/run-cost/panel.js +234 -0
- package/dist/slices/run-cost/pricing.js +92 -0
- package/dist/slices/run-cost/week.js +108 -0
- package/dist/slices/schedules/agenda.js +158 -0
- package/dist/slices/schedules/repo.js +58 -18
- package/dist/slices/schedules/scheduler.js +67 -16
- package/dist/slices/schedules/schema.js +138 -1
- package/dist/slices/schedules/service.js +48 -4
- package/dist/slices/schedules/similar.js +90 -0
- package/dist/slices/schedules/topic-list.js +163 -0
- package/dist/slices/schedules/topics.js +422 -0
- package/dist/slices/settings/first-run.js +102 -0
- package/dist/slices/settings/health.js +190 -0
- package/dist/slices/settings/key-guides.js +128 -0
- package/dist/slices/settings/key-test.js +85 -0
- package/dist/slices/settings/model.js +11 -1
- package/dist/slices/settings/playback.js +15 -0
- package/dist/slices/settings/repo.js +17 -3
- package/dist/slices/settings/voices.js +64 -4
- package/dist/slices/settings/whats-new.js +64 -0
- package/dist/slices/shorts/captions.js +9 -0
- package/dist/slices/shorts/pick.js +3 -3
- package/dist/slices/shorts/prompts.js +1 -1
- package/dist/slices/shorts/render.js +54 -5
- package/dist/slices/storage/asset-name.js +3 -0
- package/dist/slices/storage/backup-export.js +48 -9
- package/dist/slices/storage/backup-format.js +38 -1
- package/dist/slices/storage/backup-import.js +191 -19
- package/dist/slices/storage/delete-project.js +7 -4
- package/dist/slices/storage/documents.js +79 -0
- package/dist/slices/storage/files-location.js +531 -0
- package/dist/slices/storage/layout.js +20 -0
- package/dist/slices/storage/model.js +14 -0
- package/dist/slices/storage/portable.js +95 -12
- package/dist/slices/storage/reconcile.js +19 -2
- package/dist/slices/storage/repo.js +4 -1
- package/dist/slices/storage/schema.js +11 -0
- package/dist/slices/storage/trim.js +146 -0
- package/dist/slices/studio/disclosure.js +74 -0
- package/dist/slices/studio/model.js +28 -0
- package/dist/slices/studio/pack.js +209 -0
- package/dist/slices/studio/settings.js +133 -0
- package/dist/slices/style-preview/images.js +61 -0
- package/dist/slices/style-preview/render.js +161 -0
- package/dist/slices/style-preview/schema.js +45 -0
- package/dist/slices/style-preview/service.js +93 -0
- package/dist/slices/style-preview/settings.js +89 -0
- package/dist/slices/subtitles/captions.js +42 -9
- package/dist/slices/subtitles/model.js +22 -0
- package/dist/slices/subtitles/prepare.js +27 -16
- package/dist/slices/subtitles/transcript.js +1 -1
- package/dist/slices/thumbnail/run.js +7 -7
- package/dist/slices/trash/model.js +25 -0
- package/dist/slices/trash/service.js +273 -0
- package/dist/slices/uploads/repo.js +20 -0
- package/dist/slices/video/ambient-bed-schema.js +15 -0
- package/dist/slices/video/ambient-bed.js +95 -0
- package/dist/slices/video/ambient-mix.js +72 -0
- package/dist/slices/video/audio-inputs.js +2 -2
- package/dist/slices/video/cards.js +5 -2
- package/dist/slices/video/edit-list.js +23 -2
- package/dist/slices/video/edit-settings.js +13 -3
- package/dist/slices/video/ffmpeg.js +63 -9
- package/dist/slices/video/figure-card.js +492 -0
- package/dist/slices/video/figure-spans.js +51 -0
- package/dist/slices/video/plan.js +125 -13
- package/dist/slices/video/run.js +14 -12
- package/dist/slices/video/slideshow.js +39 -4
- package/dist/slices/voices/attribution.js +69 -0
- package/dist/slices/voices/audio-files.js +82 -0
- package/dist/slices/voices/audition.js +25 -0
- package/dist/slices/voices/cast.js +56 -0
- package/dist/slices/voices/grouping.js +49 -0
- package/dist/slices/voices/join.js +35 -0
- package/dist/slices/voices/languages.js +25 -0
- package/dist/slices/voices/model.js +179 -0
- package/dist/slices/voices/palette.js +26 -0
- package/dist/slices/voices/panel.js +91 -0
- package/dist/slices/voices/portraits.js +55 -0
- package/dist/slices/voices/script.js +136 -0
- package/dist/slices/voices/timing.js +58 -0
- package/dist/slices/youtube/answer.js +1 -1
- package/dist/slices/youtube/chapters.js +126 -0
- package/dist/slices/youtube/edits-repo.js +69 -0
- package/dist/slices/youtube/edits.js +89 -0
- package/dist/slices/youtube/placeholders.js +98 -0
- package/dist/slices/youtube/transcript.js +11 -4
- package/dist/updater/service.js +66 -21
- package/dist/web/assets/index-CUA2sUxU.css +1 -0
- package/dist/web/assets/index-CX590kE-.js +303 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
- package/dist/web/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
- package/dist/web/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
- package/dist/web/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
- package/dist/web/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
- package/dist/web/assets/{pdf-CAJNT254.js → pdf-BG8n_uc5.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +4 -4
- package/dist/edge/docker-launch.js +0 -81
- package/dist/edge/docker-projects/activation.js +0 -35
- package/dist/edge/docker-projects/claims.js +0 -85
- package/dist/edge/docker-projects/committed.js +0 -40
- package/dist/edge/docker-projects/engine.js +0 -461
- package/dist/edge/docker-projects/install.js +0 -254
- package/dist/edge/docker-projects/prune.js +0 -66
- package/dist/edge/docker-projects/recover.js +0 -100
- package/dist/edge/docker-projects/state.js +0 -225
- package/dist/web/assets/index-BGgYMm2u.css +0 -1
- package/dist/web/assets/index-BVVgtntL.js +0 -171
- package/scripts/docker-run.sh +0 -3
- /package/dist/edge/{docker-projects → docker-install}/tree.js +0 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { languageSchema } from "../../kernel/ports/languages.js";
|
|
3
|
+
import { aiDisclosureSettings } from "../studio/disclosure.js";
|
|
4
|
+
import { ambientBedProblems } from "../video/ambient-bed.js";
|
|
5
|
+
import { channelAmbientBedSchema } from "../video/ambient-bed-schema.js";
|
|
6
|
+
import { paceSteps } from "../voices/model.js";
|
|
7
|
+
import { castKinds } from "./model.js";
|
|
8
|
+
const id = z.uuid();
|
|
9
|
+
const colour = z
|
|
10
|
+
.string()
|
|
11
|
+
.regex(/^#[0-9a-fA-F]{6}$/, "Use a colour like #FFD700.")
|
|
12
|
+
.transform((value) => value.toUpperCase());
|
|
13
|
+
const optionalText = (max) => z.string().trim().max(max).optional();
|
|
14
|
+
const fontId = z
|
|
15
|
+
.string()
|
|
16
|
+
.max(160)
|
|
17
|
+
.regex(/^[A-Za-z0-9_-]+$/, "Choose a font from the list.")
|
|
18
|
+
.optional();
|
|
19
|
+
// A blank field is left out rather than kept as "", so it inherits instead of overriding.
|
|
20
|
+
export const brandKitSchema = z
|
|
21
|
+
.object({
|
|
22
|
+
captionFontId: fontId,
|
|
23
|
+
captionColor: colour.optional(),
|
|
24
|
+
captionOutlineColor: colour.optional(),
|
|
25
|
+
titleFontId: fontId,
|
|
26
|
+
titleColor: colour.optional(),
|
|
27
|
+
intro: optionalText(200),
|
|
28
|
+
outro: optionalText(200),
|
|
29
|
+
endScreenText: optionalText(200),
|
|
30
|
+
documentTheme: optionalText(200),
|
|
31
|
+
// The built-in beds only; the ranges are `video/ambient-bed.ts`'s, said in its words.
|
|
32
|
+
ambientBed: channelAmbientBedSchema
|
|
33
|
+
.superRefine((bed, context) => {
|
|
34
|
+
for (const problem of ambientBedProblems(bed))
|
|
35
|
+
context.addIssue({
|
|
36
|
+
code: "custom",
|
|
37
|
+
path: [problem.field],
|
|
38
|
+
message: `${problem.message} Change it under Brand kit → Ambient sound on the channel page.`,
|
|
39
|
+
});
|
|
40
|
+
})
|
|
41
|
+
.optional(),
|
|
42
|
+
language: languageSchema.optional(),
|
|
43
|
+
})
|
|
44
|
+
.strict()
|
|
45
|
+
.transform((kit) => Object.fromEntries(Object.entries(kit).filter(([, value]) => value !== undefined && value !== "")));
|
|
46
|
+
const name = z
|
|
47
|
+
.string()
|
|
48
|
+
.trim()
|
|
49
|
+
.min(1, "Enter a name.")
|
|
50
|
+
.max(200, "Keep the name to 200 characters or fewer.");
|
|
51
|
+
export const channelCreateSchema = z.object({ id, name }).strict().readonly();
|
|
52
|
+
export const channelUpdateSchema = z
|
|
53
|
+
.object({
|
|
54
|
+
name,
|
|
55
|
+
brand: brandKitSchema,
|
|
56
|
+
seriesBrief: z.string().max(10000, "Keep the series brief to 10,000 characters or fewer."),
|
|
57
|
+
baseVersion: z.number().int().positive(),
|
|
58
|
+
})
|
|
59
|
+
.strict()
|
|
60
|
+
.readonly();
|
|
61
|
+
// Names and aliases are matched against titles and image briefs as whole words; one of at
|
|
62
|
+
// most 200 characters, and at most 20 aliases, keeps that cheap.
|
|
63
|
+
const term = z
|
|
64
|
+
.string()
|
|
65
|
+
.trim()
|
|
66
|
+
.min(1, "Enter a name, and remove empty aliases.")
|
|
67
|
+
.max(200, "Keep names and aliases to 200 characters or fewer.");
|
|
68
|
+
// A cast member's voice. The pace is one of the Speakers list's steps; the rest is checked
|
|
69
|
+
// when a run uses it, like every other voice choice.
|
|
70
|
+
export const castVoiceSchema = z
|
|
71
|
+
.object({
|
|
72
|
+
provider: z.string().trim().min(1, "Choose a voice provider.").max(100),
|
|
73
|
+
model: z.string().trim().min(1, "Choose a voice model.").max(200),
|
|
74
|
+
voice: z.string().trim().min(1, "Choose a voice.").max(200),
|
|
75
|
+
pace: z
|
|
76
|
+
.number()
|
|
77
|
+
.refine((value) => paceSteps.includes(value), "Pick a pace from the list.")
|
|
78
|
+
.optional(),
|
|
79
|
+
pronunciations: z
|
|
80
|
+
.string()
|
|
81
|
+
.max(20_000, "Keep the pronunciations to 20,000 characters or fewer.")
|
|
82
|
+
.optional(),
|
|
83
|
+
})
|
|
84
|
+
.strict();
|
|
85
|
+
export const castMemberInputSchema = z
|
|
86
|
+
.object({
|
|
87
|
+
kind: z.enum(castKinds),
|
|
88
|
+
name: term,
|
|
89
|
+
aliases: z.array(term).max(20, "Keep to 20 aliases or fewer.").default([]),
|
|
90
|
+
description: z
|
|
91
|
+
.string()
|
|
92
|
+
.trim()
|
|
93
|
+
.max(2000, "Keep the description to 2,000 characters or fewer.")
|
|
94
|
+
.default(""),
|
|
95
|
+
// Absent keeps the saved voice; null removes it.
|
|
96
|
+
voice: castVoiceSchema.nullable().optional(),
|
|
97
|
+
})
|
|
98
|
+
.strict();
|
|
99
|
+
export const castMemberCreateSchema = castMemberInputSchema.extend({ id }).strict();
|
|
100
|
+
export const castMemberUpdateSchema = castMemberInputSchema
|
|
101
|
+
.extend({ baseVersion: z.number().int().positive() })
|
|
102
|
+
.strict();
|
|
103
|
+
export const castGenerateSchema = z
|
|
104
|
+
.object({
|
|
105
|
+
prompt: z
|
|
106
|
+
.string()
|
|
107
|
+
.trim()
|
|
108
|
+
.min(1, "Describe the picture to make.")
|
|
109
|
+
.max(4000, "Keep the picture's description to 4,000 characters or fewer."),
|
|
110
|
+
provider: z.string().min(1).max(100),
|
|
111
|
+
model: z.string().min(1).max(200),
|
|
112
|
+
})
|
|
113
|
+
.strict();
|
|
114
|
+
export const templateChannelSchema = z.object({ channelId: id }).strict();
|
|
115
|
+
// The channel's YouTube AI disclosure, saved on its own so it never races the Brand form.
|
|
116
|
+
export const channelAiDisclosureSchema = z
|
|
117
|
+
.object({
|
|
118
|
+
aiDisclosure: z.enum(aiDisclosureSettings, "Choose Automatic, Always Yes or Always No."),
|
|
119
|
+
})
|
|
120
|
+
.strict();
|
|
121
|
+
// A cast member as a run's config holds it (`CastSnapshot`).
|
|
122
|
+
export const castSnapshotSchema = z
|
|
123
|
+
.array(z.object({
|
|
124
|
+
name: z.string(),
|
|
125
|
+
aliases: z.array(z.string()).readonly(),
|
|
126
|
+
description: z.string(),
|
|
127
|
+
images: z.array(z.string()).readonly(),
|
|
128
|
+
}))
|
|
129
|
+
.readonly();
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { transact } from "../../kernel/db/tx.js";
|
|
2
|
+
import { defaultChannelId } from "./model.js";
|
|
3
|
+
import { castMemberById, castOfChannel, channelById, channelSummaries } from "./repo.js";
|
|
4
|
+
import { castMemberCreateSchema, castMemberUpdateSchema, channelAiDisclosureSchema, channelCreateSchema, channelUpdateSchema, templateChannelSchema, } from "./schema.js";
|
|
5
|
+
export function listChannels(deps) {
|
|
6
|
+
return channelSummaries(deps.db);
|
|
7
|
+
}
|
|
8
|
+
export function readChannel(deps, id) {
|
|
9
|
+
const channel = channelById(deps.db, id);
|
|
10
|
+
if (channel === undefined)
|
|
11
|
+
return { ok: false, reason: "not-found" };
|
|
12
|
+
return { ok: true, value: { channel, cast: castOfChannel(deps.db, id) } };
|
|
13
|
+
}
|
|
14
|
+
export function createChannel(deps, input) {
|
|
15
|
+
const parsed = channelCreateSchema.safeParse(input);
|
|
16
|
+
if (!parsed.success)
|
|
17
|
+
return { ok: false, reason: "invalid-input" };
|
|
18
|
+
const { id, name } = parsed.data;
|
|
19
|
+
return transact(deps.db, () => {
|
|
20
|
+
const existing = channelById(deps.db, id);
|
|
21
|
+
// The same create sent twice (a retried request) answers with what the first made.
|
|
22
|
+
if (existing !== undefined)
|
|
23
|
+
return existing.name === name
|
|
24
|
+
? { ok: true, value: existing }
|
|
25
|
+
: { ok: false, reason: "conflict" };
|
|
26
|
+
const at = deps.clock.now().toISOString();
|
|
27
|
+
// A new channel starts with episode memory on (`slices/episodes`); the column's default
|
|
28
|
+
// is off only for the channels that existed before it.
|
|
29
|
+
deps.db
|
|
30
|
+
.prepare("INSERT INTO channels(id,name,episode_memory,created_at,updated_at) VALUES (?,?,1,?,?)")
|
|
31
|
+
.run(id, name, at, at);
|
|
32
|
+
const created = channelById(deps.db, id);
|
|
33
|
+
if (created === undefined)
|
|
34
|
+
throw new Error("The new channel could not be read back");
|
|
35
|
+
return { ok: true, value: created };
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
export function updateChannel(deps, id, input) {
|
|
39
|
+
const parsed = channelUpdateSchema.safeParse(input);
|
|
40
|
+
if (!parsed.success)
|
|
41
|
+
return {
|
|
42
|
+
ok: false,
|
|
43
|
+
reason: "invalid-input",
|
|
44
|
+
message: parsed.error.issues[0]?.message,
|
|
45
|
+
};
|
|
46
|
+
return transact(deps.db, () => {
|
|
47
|
+
const previous = channelById(deps.db, id);
|
|
48
|
+
if (previous === undefined)
|
|
49
|
+
return { ok: false, reason: "not-found" };
|
|
50
|
+
if (previous.version !== parsed.data.baseVersion)
|
|
51
|
+
return { ok: false, reason: "conflict" };
|
|
52
|
+
const brand = parsed.data.brand;
|
|
53
|
+
deps.db
|
|
54
|
+
.prepare("UPDATE channels SET name=?,brand_json=?,series_brief=?,version=version+1,updated_at=? WHERE id=? AND version=?")
|
|
55
|
+
.run(parsed.data.name, JSON.stringify(brand), parsed.data.seriesBrief, deps.clock.now().toISOString(), id, parsed.data.baseVersion);
|
|
56
|
+
const saved = channelById(deps.db, id);
|
|
57
|
+
if (saved === undefined)
|
|
58
|
+
throw new Error("The saved channel could not be read back");
|
|
59
|
+
return { ok: true, value: saved };
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
// Automatic, Always Yes or Always No for YouTube's AI disclosure. It leaves the channel's
|
|
63
|
+
// version alone, so a Brand form open in another tab still saves.
|
|
64
|
+
export function setChannelAiDisclosure(deps, id, input) {
|
|
65
|
+
const parsed = channelAiDisclosureSchema.safeParse(input);
|
|
66
|
+
if (!parsed.success)
|
|
67
|
+
return { ok: false, reason: "invalid-input", message: parsed.error.issues[0]?.message };
|
|
68
|
+
const changed = deps.db
|
|
69
|
+
.prepare("UPDATE channels SET ai_disclosure=?,updated_at=? WHERE id=?")
|
|
70
|
+
.run(parsed.data.aiDisclosure, deps.clock.now().toISOString(), id);
|
|
71
|
+
if (Number(changed.changes) === 0)
|
|
72
|
+
return { ok: false, reason: "not-found" };
|
|
73
|
+
const saved = channelById(deps.db, id);
|
|
74
|
+
if (saved === undefined)
|
|
75
|
+
throw new Error("The saved channel could not be read back");
|
|
76
|
+
return { ok: true, value: saved };
|
|
77
|
+
}
|
|
78
|
+
// Its cast goes with it; its projects move to the default channel. A channel with templates
|
|
79
|
+
// is refused, since its schedules would silently change channel.
|
|
80
|
+
export function deleteChannel(deps, id) {
|
|
81
|
+
return transact(deps.db, () => {
|
|
82
|
+
const channel = channelById(deps.db, id);
|
|
83
|
+
if (channel === undefined)
|
|
84
|
+
return { ok: false, reason: "not-found" };
|
|
85
|
+
if (channel.isDefault)
|
|
86
|
+
return { ok: false, reason: "default-channel" };
|
|
87
|
+
// A template in the trash does not hold its channel: restored, it falls back to the
|
|
88
|
+
// default channel (`resolveChannelId`).
|
|
89
|
+
if (deps.db
|
|
90
|
+
.prepare("SELECT 1 FROM project_templates WHERE channel_id=? AND deleted_at IS NULL LIMIT 1")
|
|
91
|
+
.get(id))
|
|
92
|
+
return { ok: false, reason: "has-templates" };
|
|
93
|
+
deps.db
|
|
94
|
+
.prepare("UPDATE project_channels SET channel_id=? WHERE channel_id=?")
|
|
95
|
+
.run(defaultChannelId, id);
|
|
96
|
+
deps.db.prepare("DELETE FROM channels WHERE id=?").run(id);
|
|
97
|
+
return { ok: true, value: { deleted: true } };
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
export function moveTemplate(deps, templateId, input) {
|
|
101
|
+
const parsed = templateChannelSchema.safeParse(input);
|
|
102
|
+
if (!parsed.success)
|
|
103
|
+
return { ok: false, reason: "invalid-input" };
|
|
104
|
+
return transact(deps.db, () => {
|
|
105
|
+
if (channelById(deps.db, parsed.data.channelId) === undefined)
|
|
106
|
+
return { ok: false, reason: "not-found" };
|
|
107
|
+
const changed = deps.db
|
|
108
|
+
.prepare("UPDATE project_templates SET channel_id=? WHERE id=?")
|
|
109
|
+
.run(parsed.data.channelId, templateId);
|
|
110
|
+
if (Number(changed.changes) === 0)
|
|
111
|
+
return { ok: false, reason: "not-found" };
|
|
112
|
+
return { ok: true, value: { templateId, channelId: parsed.data.channelId } };
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
export function createCastMember(deps, channelId, input) {
|
|
116
|
+
const parsed = castMemberCreateSchema.safeParse(input);
|
|
117
|
+
if (!parsed.success)
|
|
118
|
+
return { ok: false, reason: "invalid-input", message: parsed.error.issues[0]?.message };
|
|
119
|
+
const { id, kind, name, aliases, description, voice } = parsed.data;
|
|
120
|
+
return transact(deps.db, () => {
|
|
121
|
+
if (channelById(deps.db, channelId) === undefined)
|
|
122
|
+
return { ok: false, reason: "not-found" };
|
|
123
|
+
const existing = castMemberById(deps.db, id);
|
|
124
|
+
if (existing !== undefined)
|
|
125
|
+
return existing.channelId === channelId && existing.name === name
|
|
126
|
+
? { ok: true, value: existing }
|
|
127
|
+
: { ok: false, reason: "conflict" };
|
|
128
|
+
const at = deps.clock.now().toISOString();
|
|
129
|
+
deps.db
|
|
130
|
+
.prepare("INSERT INTO cast_members(id,channel_id,kind,name,aliases_json,description,voice_json,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?,?)")
|
|
131
|
+
.run(id, channelId, kind, name, JSON.stringify(unique(aliases, name)), description, voice === undefined || voice === null ? null : JSON.stringify(voice), at, at);
|
|
132
|
+
const created = castMemberById(deps.db, id);
|
|
133
|
+
if (created === undefined)
|
|
134
|
+
throw new Error("The new cast member could not be read back");
|
|
135
|
+
return { ok: true, value: created };
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
export function updateCastMember(deps, id, input) {
|
|
139
|
+
const parsed = castMemberUpdateSchema.safeParse(input);
|
|
140
|
+
if (!parsed.success)
|
|
141
|
+
return { ok: false, reason: "invalid-input", message: parsed.error.issues[0]?.message };
|
|
142
|
+
const { kind, name, aliases, description, voice, baseVersion } = parsed.data;
|
|
143
|
+
return transact(deps.db, () => {
|
|
144
|
+
const previous = castMemberById(deps.db, id);
|
|
145
|
+
if (previous === undefined)
|
|
146
|
+
return { ok: false, reason: "not-found" };
|
|
147
|
+
if (previous.version !== baseVersion)
|
|
148
|
+
return { ok: false, reason: "conflict" };
|
|
149
|
+
deps.db
|
|
150
|
+
.prepare("UPDATE cast_members SET kind=?,name=?,aliases_json=?,description=?,voice_json=?,version=version+1,updated_at=? WHERE id=? AND version=?")
|
|
151
|
+
.run(kind, name, JSON.stringify(unique(aliases, name)), description, voice === undefined
|
|
152
|
+
? previous.voice === undefined
|
|
153
|
+
? null
|
|
154
|
+
: JSON.stringify(previous.voice)
|
|
155
|
+
: voice === null
|
|
156
|
+
? null
|
|
157
|
+
: JSON.stringify(voice), deps.clock.now().toISOString(), id, baseVersion);
|
|
158
|
+
const saved = castMemberById(deps.db, id);
|
|
159
|
+
if (saved === undefined)
|
|
160
|
+
throw new Error("The saved cast member could not be read back");
|
|
161
|
+
return { ok: true, value: saved };
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
export function deleteCastMember(deps, id) {
|
|
165
|
+
const changed = deps.db.prepare("DELETE FROM cast_members WHERE id=?").run(id);
|
|
166
|
+
return Number(changed.changes) === 0
|
|
167
|
+
? { ok: false, reason: "not-found" }
|
|
168
|
+
: { ok: true, value: { deleted: true } };
|
|
169
|
+
}
|
|
170
|
+
// Aliases compared without case, the name itself left out: matching ignores case anyway.
|
|
171
|
+
function unique(aliases, name) {
|
|
172
|
+
const seen = new Set([name.toLowerCase()]);
|
|
173
|
+
return aliases.filter((alias) => {
|
|
174
|
+
const key = alias.toLowerCase();
|
|
175
|
+
if (seen.has(key))
|
|
176
|
+
return false;
|
|
177
|
+
seen.add(key);
|
|
178
|
+
return true;
|
|
179
|
+
});
|
|
180
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// The video titles in a CSV file, such as YouTube Studio's Analytics → Content export. Pure.
|
|
2
|
+
//
|
|
3
|
+
// The title column is the one whose header reads "Video title", else "Title", else "Content"
|
|
4
|
+
// (any case); failing that, the first column holding text that is not a number, a date or an id.
|
|
5
|
+
// Quoted fields may hold commas, doubled quotes and line breaks. A leading byte-order mark is
|
|
6
|
+
// dropped, and a file with more semicolons or tabs than commas in its first line is read with
|
|
7
|
+
// that separator. Studio's "Total" row is skipped.
|
|
8
|
+
const titleHeaders = ["video title", "title", "content"];
|
|
9
|
+
export function parseCsv(text) {
|
|
10
|
+
const source = text.replace(/^/, "");
|
|
11
|
+
const firstLine = source.slice(0, source.search(/\r?\n|$/));
|
|
12
|
+
const separator = pickSeparator(firstLine);
|
|
13
|
+
const rows = [];
|
|
14
|
+
let row = [];
|
|
15
|
+
let field = "";
|
|
16
|
+
let quoted = false;
|
|
17
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
18
|
+
const char = source[index];
|
|
19
|
+
if (quoted) {
|
|
20
|
+
if (char === '"') {
|
|
21
|
+
if (source[index + 1] === '"') {
|
|
22
|
+
field += '"';
|
|
23
|
+
index += 1;
|
|
24
|
+
}
|
|
25
|
+
else
|
|
26
|
+
quoted = false;
|
|
27
|
+
}
|
|
28
|
+
else
|
|
29
|
+
field += char;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (char === '"' && field === "")
|
|
33
|
+
quoted = true;
|
|
34
|
+
else if (char === separator) {
|
|
35
|
+
row.push(field);
|
|
36
|
+
field = "";
|
|
37
|
+
}
|
|
38
|
+
else if (char === "\n" || char === "\r") {
|
|
39
|
+
if (char === "\r" && source[index + 1] === "\n")
|
|
40
|
+
index += 1;
|
|
41
|
+
row.push(field);
|
|
42
|
+
rows.push(row);
|
|
43
|
+
row = [];
|
|
44
|
+
field = "";
|
|
45
|
+
}
|
|
46
|
+
else
|
|
47
|
+
field += char;
|
|
48
|
+
}
|
|
49
|
+
if (field !== "" || row.length > 0) {
|
|
50
|
+
row.push(field);
|
|
51
|
+
rows.push(row);
|
|
52
|
+
}
|
|
53
|
+
return rows.filter((cells) => cells.some((cell) => cell.trim() !== ""));
|
|
54
|
+
}
|
|
55
|
+
function pickSeparator(line) {
|
|
56
|
+
const count = (char) => line.split(char).length - 1;
|
|
57
|
+
const candidates = [",", ";", "\t"].map((char) => ({ char, n: count(char) }));
|
|
58
|
+
return candidates.reduce((best, next) => (next.n > best.n ? next : best)).char;
|
|
59
|
+
}
|
|
60
|
+
export function titlesFromCsv(text) {
|
|
61
|
+
const rows = parseCsv(text);
|
|
62
|
+
const header = rows[0];
|
|
63
|
+
if (header === undefined)
|
|
64
|
+
return [];
|
|
65
|
+
// In that order: Studio's export has both "Content" (the video id) and "Video title".
|
|
66
|
+
const names = header.map((cell) => cell.trim().toLowerCase());
|
|
67
|
+
const named = titleHeaders.map((name) => names.indexOf(name)).find((index) => index !== -1) ?? -1;
|
|
68
|
+
const column = named !== -1 ? named : textColumn(rows);
|
|
69
|
+
if (column === -1)
|
|
70
|
+
return [];
|
|
71
|
+
const body = named !== -1 || looksLikeHeader(header, column) ? rows.slice(1) : rows;
|
|
72
|
+
return body
|
|
73
|
+
.map((cells) => (cells[column] ?? "").replace(/\s+/g, " ").trim())
|
|
74
|
+
.filter((title) => title !== "" && title.toLowerCase() !== "total");
|
|
75
|
+
}
|
|
76
|
+
// A cell that is only a number, duration, date or a YouTube video id is no title.
|
|
77
|
+
function plainValue(cell) {
|
|
78
|
+
const value = cell.trim();
|
|
79
|
+
return (value === "" ||
|
|
80
|
+
/^[-+]?[\d.,:%\s]+$/.test(value) ||
|
|
81
|
+
/^\d{4}-\d{2}-\d{2}/.test(value) ||
|
|
82
|
+
/^[A-Za-z0-9_-]{11}$/.test(value));
|
|
83
|
+
}
|
|
84
|
+
function textColumn(rows) {
|
|
85
|
+
const width = Math.max(...rows.map((cells) => cells.length));
|
|
86
|
+
const sample = rows.slice(1, 21).length > 0 ? rows.slice(1, 21) : rows;
|
|
87
|
+
const text = (column, cell) => sample.some((cells) => !plainValue(cells[column] ?? "") && cell(cells[column] ?? ""));
|
|
88
|
+
// Titles are words: a column with spaces in it wins over a column of single tokens (ids,
|
|
89
|
+
// handles), which is taken only when there is nothing else.
|
|
90
|
+
for (let column = 0; column < width; column += 1)
|
|
91
|
+
if (text(column, (value) => /\S\s+\S/.test(value.trim())))
|
|
92
|
+
return column;
|
|
93
|
+
for (let column = 0; column < width; column += 1)
|
|
94
|
+
if (text(column, () => true))
|
|
95
|
+
return column;
|
|
96
|
+
return -1;
|
|
97
|
+
}
|
|
98
|
+
// Without a known header name, the first row is a header when the other columns of it are
|
|
99
|
+
// words while the rows below hold numbers there.
|
|
100
|
+
function looksLikeHeader(header, column) {
|
|
101
|
+
return header.some((cell, index) => index !== column && !plainValue(cell) && cell.trim() !== "");
|
|
102
|
+
}
|
|
103
|
+
// Pasted titles: one per line, blank lines and surrounding spaces dropped.
|
|
104
|
+
export function titlesFromLines(text) {
|
|
105
|
+
return text
|
|
106
|
+
.replace(/^/, "")
|
|
107
|
+
.split(/\r?\n/)
|
|
108
|
+
.map((line) => line.replace(/\s+/g, " ").trim())
|
|
109
|
+
.filter((line) => line !== "");
|
|
110
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { transact } from "../../kernel/db/tx.js";
|
|
3
|
+
import { readSetting, writeSetting } from "../settings/repo.js";
|
|
4
|
+
import { channelById } from "./repo.js";
|
|
5
|
+
import { titlesFromCsv, titlesFromLines } from "./studio-csv.js";
|
|
6
|
+
export const videoTitleMax = 500;
|
|
7
|
+
// ceiling: a channel with more uploads than this imports the first ones; the topic prompt
|
|
8
|
+
// only lists 5,000 titles anyway.
|
|
9
|
+
export const importMax = 10_000;
|
|
10
|
+
export const importFilterMax = 200;
|
|
11
|
+
const filterKey = (channelId) => `channels.importFilter.${channelId}`;
|
|
12
|
+
const importSchema = z
|
|
13
|
+
.object({
|
|
14
|
+
// "lines": one title per line; "csv": a CSV export, the title column found by its header.
|
|
15
|
+
format: z.enum(["lines", "csv"]),
|
|
16
|
+
text: z.string().max(20_000_000, "This file is too large. Import at most 20 MB at a time."),
|
|
17
|
+
// The preview's "Keep only titles containing…" text, remembered for the next import.
|
|
18
|
+
filter: z
|
|
19
|
+
.string()
|
|
20
|
+
.max(importFilterMax, `The filter is too long to remember. Shorten "Keep only titles containing…" on the channel's Existing videos tab to at most ${importFilterMax} characters.`)
|
|
21
|
+
.optional(),
|
|
22
|
+
})
|
|
23
|
+
.strict();
|
|
24
|
+
const row = z.object({ id: z.string(), title: z.string(), created_at: z.string() });
|
|
25
|
+
export function channelVideos(db, channelId) {
|
|
26
|
+
return db
|
|
27
|
+
.prepare("SELECT id,title,created_at FROM channel_videos WHERE channel_id=? ORDER BY lower(title), id")
|
|
28
|
+
.all(channelId)
|
|
29
|
+
.map((value) => {
|
|
30
|
+
const parsed = row.parse(value);
|
|
31
|
+
return { id: parsed.id, title: parsed.title, createdAt: parsed.created_at };
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
export function channelVideoTitles(db, channelId) {
|
|
35
|
+
return channelVideos(db, channelId).map((video) => video.title);
|
|
36
|
+
}
|
|
37
|
+
export function listChannelVideos(deps, channelId) {
|
|
38
|
+
if (channelById(deps.db, channelId) === undefined)
|
|
39
|
+
return { ok: false, reason: "not-found" };
|
|
40
|
+
return { ok: true, value: channelVideos(deps.db, channelId) };
|
|
41
|
+
}
|
|
42
|
+
// The titles an import would add and the channel's last "Keep only titles containing…" text
|
|
43
|
+
// ("" when none); nothing is saved.
|
|
44
|
+
export function previewChannelVideos(deps, channelId, input) {
|
|
45
|
+
if (channelById(deps.db, channelId) === undefined)
|
|
46
|
+
return { ok: false, reason: "not-found" };
|
|
47
|
+
const read = importOf(input);
|
|
48
|
+
if (!read.ok)
|
|
49
|
+
return read;
|
|
50
|
+
const filter = storedFilter(readSetting(deps.db, filterKey(channelId)));
|
|
51
|
+
return { ok: true, value: { titles: read.value.titles, filter } };
|
|
52
|
+
}
|
|
53
|
+
// Titles already on the channel (any case) and repeats within the import are skipped.
|
|
54
|
+
export function importChannelVideos(deps, channelId, input) {
|
|
55
|
+
const read = importOf(input);
|
|
56
|
+
if (!read.ok)
|
|
57
|
+
return read;
|
|
58
|
+
const { titles, filter } = read.value;
|
|
59
|
+
return transact(deps.db, () => {
|
|
60
|
+
if (channelById(deps.db, channelId) === undefined)
|
|
61
|
+
return { ok: false, reason: "not-found" };
|
|
62
|
+
const at = deps.clock.now().toISOString();
|
|
63
|
+
const insert = deps.db.prepare("INSERT INTO channel_videos(id,channel_id,title,created_at) VALUES (?,?,?,?) ON CONFLICT DO NOTHING");
|
|
64
|
+
let added = 0;
|
|
65
|
+
for (const title of titles)
|
|
66
|
+
added += Number(insert.run(deps.uuid(), channelId, title, at).changes);
|
|
67
|
+
if (filter !== undefined)
|
|
68
|
+
writeSetting(deps.db, filterKey(channelId), JSON.stringify(filter.trim()));
|
|
69
|
+
return { ok: true, value: { added, skipped: titles.length - added } };
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
// Stored as JSON, like every row of the `settings` table a backup carries.
|
|
73
|
+
function storedFilter(stored) {
|
|
74
|
+
if (stored === undefined)
|
|
75
|
+
return "";
|
|
76
|
+
try {
|
|
77
|
+
const value = JSON.parse(stored);
|
|
78
|
+
return typeof value === "string" ? value : "";
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return "";
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function importOf(input) {
|
|
85
|
+
const parsed = importSchema.safeParse(input);
|
|
86
|
+
if (!parsed.success)
|
|
87
|
+
return { ok: false, reason: "invalid-input", message: parsed.error.issues[0]?.message };
|
|
88
|
+
const titles = (parsed.data.format === "csv"
|
|
89
|
+
? titlesFromCsv(parsed.data.text)
|
|
90
|
+
: titlesFromLines(parsed.data.text)).map((title) => title.slice(0, videoTitleMax));
|
|
91
|
+
if (titles.length === 0)
|
|
92
|
+
return {
|
|
93
|
+
ok: false,
|
|
94
|
+
reason: "invalid-input",
|
|
95
|
+
message: parsed.data.format === "csv"
|
|
96
|
+
? "No video titles were found in this file. Export it from YouTube Studio → Analytics → Content (Export current view → Comma-separated values), or paste the titles one per line instead."
|
|
97
|
+
: "Paste at least one video title, one per line.",
|
|
98
|
+
};
|
|
99
|
+
if (titles.length > importMax)
|
|
100
|
+
return {
|
|
101
|
+
ok: false,
|
|
102
|
+
reason: "invalid-input",
|
|
103
|
+
message: `This holds ${titles.length.toLocaleString("en")} titles; import at most ${importMax.toLocaleString("en")} at a time.`,
|
|
104
|
+
};
|
|
105
|
+
return { ok: true, value: { titles, filter: parsed.data.filter } };
|
|
106
|
+
}
|
|
107
|
+
export function deleteChannelVideo(deps, channelId, videoId) {
|
|
108
|
+
const changed = deps.db
|
|
109
|
+
.prepare("DELETE FROM channel_videos WHERE id=? AND channel_id=?")
|
|
110
|
+
.run(videoId, channelId);
|
|
111
|
+
return Number(changed.changes) === 0
|
|
112
|
+
? { ok: false, reason: "not-found" }
|
|
113
|
+
: { ok: true, value: { deleted: true } };
|
|
114
|
+
}
|
|
115
|
+
export function clearChannelVideos(deps, channelId) {
|
|
116
|
+
if (channelById(deps.db, channelId) === undefined)
|
|
117
|
+
return { ok: false, reason: "not-found" };
|
|
118
|
+
const changed = deps.db.prepare("DELETE FROM channel_videos WHERE channel_id=?").run(channelId);
|
|
119
|
+
return { ok: true, value: { deleted: Number(changed.changes) } };
|
|
120
|
+
}
|
|
@@ -107,7 +107,17 @@ export function recoverCheckpointWork(db) {
|
|
|
107
107
|
AND (p.submitted_at IS NOT NULL OR p.continuation IS NOT NULL OR p.state IN ('running','done')))`)
|
|
108
108
|
.all()
|
|
109
109
|
.map((row) => z.string().parse(row.id));
|
|
110
|
+
// A step waiting out a rate limit or a timeout keeps its wait across a restart: it was not
|
|
111
|
+
// running when the process stopped, and the failure that parked it lost no submission.
|
|
112
|
+
const waiting = db
|
|
113
|
+
.prepare("SELECT id FROM revision_work WHERE state='pending' AND dispatch_state='allowed' AND retry_at IS NOT NULL")
|
|
114
|
+
.all()
|
|
115
|
+
.map((row) => z.string().parse(row.id));
|
|
110
116
|
recoverWork(db);
|
|
117
|
+
for (const id of waiting) {
|
|
118
|
+
db.prepare("UPDATE revision_work SET dispatch_state='allowed' WHERE id=? AND state='pending'").run(id);
|
|
119
|
+
db.prepare("UPDATE revision_work_pieces SET state='pending',dispatch_state='allowed' WHERE work_id=? AND state!='done'").run(id);
|
|
120
|
+
}
|
|
111
121
|
for (const id of retained) {
|
|
112
122
|
db.prepare("UPDATE revision_work SET dispatch_state='allowed' WHERE id=? AND state='pending'").run(id);
|
|
113
123
|
db.prepare("UPDATE revision_work_pieces SET state='pending',dispatch_state='allowed' WHERE work_id=? AND submitted_at IS NULL AND continuation IS NULL").run(id);
|
|
@@ -129,7 +129,8 @@ function editable(db, id, runner) {
|
|
|
129
129
|
const stages = stagesOf(db, id);
|
|
130
130
|
if (stages.some((stage) => stage.state === "running") || runner.hasInflight?.(id))
|
|
131
131
|
return { ok: false, reason: "running" };
|
|
132
|
-
|
|
132
|
+
const status = derive(stages);
|
|
133
|
+
if (project.paused !== true && status !== "failed" && status !== "partial")
|
|
133
134
|
return { ok: false, reason: "not-editable" };
|
|
134
135
|
return { ok: true };
|
|
135
136
|
}
|
|
@@ -18,7 +18,7 @@ export function readDocumentAssets() {
|
|
|
18
18
|
return { cinzel, parchment: new Uint8Array(readFileSync(new URL("background.jpg", assets))) };
|
|
19
19
|
}
|
|
20
20
|
catch (error) {
|
|
21
|
-
throw new Error(`Slopify couldn't read the fonts and page texture it makes documents with (${error instanceof Error ? error.message : String(error)}). Its installation is incomplete: reinstall or update Slopify, then use
|
|
21
|
+
throw new Error(`Slopify couldn't read the fonts and page texture it makes documents with (${error instanceof Error ? error.message : String(error)}). Its installation is incomplete: reinstall or update Slopify, then use Try again on Document.`, { cause: error });
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
export function registerFonts(doc, fonts) {
|
|
@@ -10,7 +10,7 @@ export function renderDocument(input) {
|
|
|
10
10
|
const text = documentText(input.articleMarkdown, input.researchNotes);
|
|
11
11
|
const blocks = withoutTitle(text.blocks, input.title);
|
|
12
12
|
if (blocks.length === 0)
|
|
13
|
-
throw new Error("The article has no text to put in the document. Write or regenerate the article (Edit project → Article), then use
|
|
13
|
+
throw new Error("The article has no text to put in the document. Write or regenerate the article (Edit project → Article), then use Try again on Document.");
|
|
14
14
|
const words = blocks
|
|
15
15
|
.map((block) => block.kind === "heading" ? block.text : "runs" in block ? runsText(block.runs) : "")
|
|
16
16
|
.join(" ")
|