@gentbajko/slopify 3.25.9 → 4.1.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/dist/adapters/account-limits.js +30 -0
- package/dist/adapters/alignment/audio.js +1 -1
- package/dist/adapters/alignment/cache.js +4 -6
- package/dist/adapters/alignment/ctc.js +1 -2
- package/dist/adapters/alignment/lock.js +4 -3
- package/dist/adapters/alignment/multilingual.js +2 -4
- package/dist/adapters/alignment/prefetch.js +2 -1
- package/dist/adapters/alignment/runner.js +1 -1
- package/dist/adapters/alignment/worker.js +3 -2
- package/dist/adapters/explain.js +10 -12
- package/dist/adapters/ffmpeg.js +19 -1
- package/dist/adapters/host-cli/index.js +21 -4
- package/dist/adapters/host-cli/transport.js +2 -1
- package/dist/adapters/http-failure.js +53 -0
- package/dist/adapters/image/bytes.js +2 -1
- package/dist/adapters/image/codex.js +42 -9
- package/dist/adapters/image/fal.js +21 -53
- package/dist/adapters/image/google.js +33 -56
- package/dist/adapters/image/models.js +2 -2
- package/dist/adapters/image/openai.js +22 -48
- package/dist/adapters/image/reference.js +36 -0
- package/dist/adapters/image/replicate.js +21 -49
- package/dist/adapters/image/video.js +2 -1
- package/dist/adapters/interest/search-interest.js +113 -0
- package/dist/adapters/key-probes.js +54 -3
- package/dist/adapters/llm/antigravity-models.js +1 -1
- package/dist/adapters/llm/antigravity.js +11 -6
- package/dist/adapters/llm/assistant-cli.js +388 -0
- package/dist/adapters/llm/child-session.js +83 -0
- package/dist/adapters/llm/claude-code-models.js +52 -99
- package/dist/adapters/llm/claude-code.js +16 -4
- package/dist/adapters/llm/codex-limits.js +27 -68
- package/dist/adapters/llm/codex-models.js +1 -1
- package/dist/adapters/llm/codex-server-models.js +61 -109
- package/dist/adapters/llm/codex.js +12 -1
- package/dist/adapters/llm/document-workspace.js +3 -2
- package/dist/adapters/llm/image-workspace.js +3 -2
- package/dist/adapters/llm/openrouter.js +12 -48
- package/dist/adapters/llm/run-cli.js +9 -24
- package/dist/adapters/llm/sse-lines.js +5 -1
- package/dist/adapters/model-access.js +37 -0
- package/dist/adapters/tts/cartesia.js +15 -46
- package/dist/adapters/tts/elevenlabs.js +20 -52
- package/dist/adapters/tts/gemini.js +21 -41
- package/dist/adapters/tts/inworld-async.js +6 -2
- package/dist/adapters/tts/inworld.js +6 -2
- package/dist/adapters/tts/openai.js +20 -50
- package/dist/adapters/tts/system.js +2 -1
- package/dist/assets/models-lifecycle.yaml +6 -0
- package/dist/assistant-wiring.js +60 -0
- package/dist/autopilot-timers.js +59 -0
- package/dist/catalog/lifecycle.js +52 -0
- package/dist/catalog/propose.js +204 -0
- package/dist/catalog/registry.js +23 -14
- package/dist/catalog/schema.js +4 -1
- package/dist/catalog/store.js +39 -0
- package/dist/catalog/validate.js +5 -2
- package/dist/edge/autostart/native.js +3 -5
- package/dist/edge/autostart/prompt.js +2 -1
- package/dist/edge/autostart/service.js +3 -4
- package/dist/edge/cli-args.js +3 -4
- package/dist/edge/cli.js +2 -1
- package/dist/edge/docker-install/apply.js +11 -11
- package/dist/edge/docker-install/run.js +2 -1
- package/dist/edge/docker-install/state.js +4 -3
- package/dist/edge/docker-install/tree.js +2 -4
- package/dist/edge/docker-install/volume.js +3 -2
- package/dist/edge/docker.js +5 -4
- package/dist/edge/events/hub.js +5 -5
- package/dist/edge/http/actions.js +11 -32
- package/dist/edge/http/actor.js +5 -3
- package/dist/edge/http/app.js +86 -11
- package/dist/edge/http/assistant.js +129 -0
- package/dist/edge/http/audio-preview.js +4 -11
- package/dist/edge/http/auditions.js +1 -1
- package/dist/edge/http/autostart.js +5 -5
- package/dist/edge/http/backups.js +2 -4
- package/dist/edge/http/channel-memory.js +9 -22
- package/dist/edge/http/channels.js +14 -27
- package/dist/edge/http/checkpoints.js +39 -21
- package/dist/edge/http/design-templates.js +167 -0
- package/dist/edge/http/designs.js +178 -0
- package/dist/edge/http/diagnostics.js +5 -1
- package/dist/edge/http/document-themes.js +5 -14
- package/dist/edge/http/draft-files.js +1 -3
- package/dist/edge/http/draft-problem.js +5 -10
- package/dist/edge/http/drafts.js +3 -3
- package/dist/edge/http/entries.js +6 -9
- package/dist/edge/http/extension-auth.js +27 -0
- package/dist/edge/http/external-reviews.js +12 -11
- package/dist/edge/http/files.js +5 -11
- package/dist/edge/http/folder-location.js +1 -4
- package/dist/edge/http/fonts.js +0 -7
- package/dist/edge/http/home.js +47 -14
- package/dist/edge/http/host-cli.js +18 -8
- package/dist/edge/http/ids.js +8 -0
- package/dist/edge/http/keywords.js +35 -0
- package/dist/edge/http/model-checks.js +132 -0
- package/dist/edge/http/narration-peaks.js +5 -13
- package/dist/edge/http/onboarding.js +30 -26
- package/dist/edge/http/open-folder.js +5 -10
- package/dist/edge/http/outputs.js +7 -14
- package/dist/edge/http/patch-notes.js +6 -12
- package/dist/edge/http/planning.js +3 -7
- package/dist/edge/http/problem.js +2 -4
- package/dist/edge/http/project-create.js +2 -1
- package/dist/edge/http/project-templates.js +31 -19
- package/dist/edge/http/projects.js +24 -16
- package/dist/edge/http/prompts.js +9 -16
- package/dist/edge/http/pronunciations.js +1 -2
- package/dist/edge/http/providers.js +48 -34
- package/dist/edge/http/refusal.js +29 -0
- package/dist/edge/http/remote-access.js +40 -0
- package/dist/edge/http/remote-guard.js +71 -0
- package/dist/edge/http/request-guard.js +91 -0
- package/dist/edge/http/reviews.js +12 -25
- package/dist/edge/http/revision-files.js +6 -11
- package/dist/edge/http/revisions.js +14 -19
- package/dist/edge/http/run-cost.js +1 -2
- package/dist/edge/http/same-page.js +80 -0
- package/dist/edge/http/schedules.js +69 -40
- package/dist/edge/http/settings.js +27 -14
- package/dist/edge/http/social.js +283 -0
- package/dist/edge/http/staging.js +3 -5
- package/dist/edge/http/storage-files.js +5 -7
- package/dist/edge/http/storage.js +33 -25
- package/dist/edge/http/studio.js +94 -69
- package/dist/edge/http/style-preview.js +3 -6
- package/dist/edge/http/subtitles.js +3 -17
- package/dist/edge/http/template-studio.js +436 -0
- package/dist/edge/http/trash.js +11 -12
- package/dist/edge/http/tutorial.js +1 -2
- package/dist/edge/http/tutorials.js +5 -11
- package/dist/edge/http/update.js +7 -13
- package/dist/edge/http/waveform.js +3 -6
- package/dist/edge/http/youtube-edits.js +3 -4
- package/dist/edge/mcp/assistant.js +279 -0
- package/dist/edge/mcp/attribution.js +8 -2
- package/dist/edge/mcp/call-route.js +11 -2
- package/dist/edge/mcp/excluded-routes.js +29 -3
- package/dist/edge/mcp/http.js +11 -2
- package/dist/edge/mcp/instructions.js +6 -4
- package/dist/edge/mcp/library-tools.js +9 -5
- package/dist/edge/mcp/remake-tools.js +7 -4
- package/dist/edge/mcp/route-tool.js +35 -15
- package/dist/edge/mcp/server.js +13 -8
- package/dist/edge/mcp/tools-channels.js +11 -9
- package/dist/edge/mcp/tools-designs.js +165 -0
- package/dist/edge/mcp/tools-library.js +19 -7
- package/dist/edge/mcp/tools-planning.js +40 -21
- package/dist/edge/mcp/tools-projects.js +38 -11
- package/dist/edge/mcp/tools-publishing.js +9 -9
- package/dist/edge/mcp/tools-runs.js +23 -10
- package/dist/edge/mcp/tools-schedules.js +32 -8
- package/dist/edge/mcp/tools-setup.js +75 -11
- package/dist/edge/mcp/tools-social.js +47 -0
- package/dist/edge/mcp/tools-storage.js +11 -3
- package/dist/edge/mcp/tools-template-studio.js +166 -0
- package/dist/edge/mcp/tools.js +6 -0
- package/dist/edge/remote-access/cli.js +71 -0
- package/dist/edge/remote-access/model.js +32 -0
- package/dist/edge/remote-access/rules.js +207 -0
- package/dist/edge/remote-access/service.js +203 -0
- package/dist/edge/remote-access/store.js +44 -0
- package/dist/edge/remote-access/views.js +43 -0
- package/dist/extension/slopify-studio-chrome.zip +0 -0
- package/dist/extension/slopify-studio-firefox.zip +0 -0
- package/dist/host-cli/install.js +2 -2
- package/dist/host-cli/open-folder.js +2 -2
- package/dist/host-cli/paths.js +2 -4
- package/dist/host-cli/server.js +4 -4
- package/dist/host-cli/service.js +3 -3
- package/dist/host-cli/status.js +7 -7
- package/dist/kernel/db/migrations/0063-model-checks.sql +20 -0
- package/dist/kernel/db/migrations/0064-template-studio.sql +46 -0
- package/dist/kernel/db/migrations/0065-promise-and-assistant-reviews.sql +58 -0
- package/dist/kernel/db/migrations/0066-assistant.sql +44 -0
- package/dist/kernel/db/migrations/0067-autopilot.sql +6 -0
- package/dist/kernel/db/migrations/0068-topic-scores.sql +17 -0
- package/dist/kernel/db/migrations/0070-designs.sql +53 -0
- package/dist/kernel/db/migrations/0071-design-usage.sql +33 -0
- package/dist/kernel/db/migrations/0072-design-templates.sql +33 -0
- package/dist/kernel/db/migrations/0073-schedule-design-kind.sql +5 -0
- package/dist/kernel/db/migrations/0074-template-bakeoffs.sql +44 -0
- package/dist/kernel/db/migrations/0075-social-posts.sql +25 -0
- package/dist/kernel/db/migrations/0076-channel-social.sql +5 -0
- package/dist/kernel/db/migrations/0077-post-prompts.sql +17 -0
- package/dist/kernel/db/migrations/0078-uploaded-from-links.sql +7 -0
- package/dist/kernel/db/tx.js +2 -4
- package/dist/kernel/errors.js +27 -1
- package/dist/kernel/json-answer.js +15 -0
- package/dist/kernel/lock.js +4 -9
- package/dist/kernel/paths.js +8 -0
- package/dist/kernel/pipeline.js +12 -0
- package/dist/kernel/ports/assistant-cli.js +4 -0
- package/dist/kernel/ports/host-cli.js +3 -0
- package/dist/kernel/ports/interest.js +4 -0
- package/dist/kernel/ports/languages.js +0 -3
- package/dist/kernel/ports/model.js +2 -0
- package/dist/kernel/ports/settings-places.js +31 -0
- package/dist/kernel/ports/system-speech.js +31 -3
- package/dist/kernel/runner/attempt.js +14 -4
- package/dist/kernel/runner/audition.js +3 -1
- package/dist/kernel/runner/checkpoint-authority.js +2 -1
- package/dist/kernel/runner/index.js +8 -10
- package/dist/kernel/runner/model-access.js +40 -0
- package/dist/kernel/runner/model-check.js +153 -0
- package/dist/kernel/runner/providers.js +18 -12
- package/dist/kernel/runner/standalone.js +11 -7
- package/dist/main.js +338 -21
- package/dist/model-catalog.js +20 -0
- package/dist/model-checks.js +166 -0
- package/dist/patch-notes/4.0.0.md +51 -0
- package/dist/patch-notes/4.1.0.md +42 -0
- package/dist/patch-notes/index.json +12 -0
- package/dist/slices/admission/heads.js +2 -1
- package/dist/slices/admission/listing-facts.js +95 -0
- package/dist/slices/admission/repo.js +11 -7
- package/dist/slices/admission/rules.js +50 -15
- package/dist/slices/admission/schema.js +8 -0
- package/dist/slices/admission/stage-writes.js +0 -3
- package/dist/slices/admission/start.js +48 -31
- package/dist/slices/admission/state-words.js +34 -5
- package/dist/slices/admission/substitute.js +7 -4
- package/dist/slices/agents/attention-posts.js +31 -0
- package/dist/slices/agents/attention-templates.js +33 -0
- package/dist/slices/agents/attention.js +10 -1
- package/dist/slices/agents/connection.js +2 -6
- package/dist/slices/agents/model.js +6 -3
- package/dist/slices/agents/repo.js +15 -7
- package/dist/slices/article/continuation.js +0 -63
- package/dist/slices/article/plain.js +3 -2
- package/dist/slices/article/segments.js +2 -62
- package/dist/slices/assistant/access.js +1 -0
- package/dist/slices/assistant/asks.js +80 -0
- package/dist/slices/assistant/cost.js +54 -0
- package/dist/slices/assistant/gate.js +222 -0
- package/dist/slices/assistant/model.js +86 -0
- package/dist/slices/assistant/names.js +23 -0
- package/dist/slices/assistant/prompt.js +74 -0
- package/dist/slices/assistant/repo.js +74 -0
- package/dist/slices/assistant/tokens.js +18 -0
- package/dist/slices/assistant/turn.js +344 -0
- package/dist/slices/autopilot/buffer.js +76 -0
- package/dist/slices/autopilot/model.js +14 -0
- package/dist/slices/autopilot/notify.js +27 -0
- package/dist/slices/autopilot/repo.js +34 -0
- package/dist/slices/autopilot/signals.js +194 -0
- package/dist/slices/autopilot/warnings.js +85 -0
- package/dist/slices/autopilot/weekly.js +100 -0
- package/dist/slices/backups/files.js +2 -6
- package/dist/slices/backups/repo.js +3 -15
- package/dist/slices/backups/service.js +8 -7
- package/dist/slices/channels/cast-images.js +3 -2
- package/dist/slices/channels/model.js +0 -1
- package/dist/slices/channels/rebrand.js +0 -3
- package/dist/slices/channels/repo.js +4 -0
- package/dist/slices/channels/schema.js +7 -1
- package/dist/slices/channels/service.js +10 -0
- package/dist/slices/channels/videos.js +3 -15
- package/dist/slices/checkpoints/index.js +0 -1
- package/dist/slices/control/providers.js +2 -28
- package/dist/slices/designs/artwork.js +66 -0
- package/dist/slices/designs/budget.js +23 -0
- package/dist/slices/designs/calls.js +124 -0
- package/dist/slices/designs/check-context.js +38 -0
- package/dist/slices/designs/check-text.js +43 -0
- package/dist/slices/designs/fields.js +88 -0
- package/dist/slices/designs/frames.js +34 -0
- package/dist/slices/designs/gate.js +130 -0
- package/dist/slices/designs/layout.js +293 -0
- package/dist/slices/designs/model.js +35 -0
- package/dist/slices/designs/pdf-job.js +23 -0
- package/dist/slices/designs/pdf-pages.js +30 -0
- package/dist/slices/designs/pdf-runner.js +68 -0
- package/dist/slices/designs/pdf-worker.js +24 -0
- package/dist/slices/designs/picture-size.js +79 -0
- package/dist/slices/designs/pictures.js +55 -0
- package/dist/slices/designs/quote.js +24 -0
- package/dist/slices/designs/render-args.js +36 -0
- package/dist/slices/designs/render-input.js +59 -0
- package/dist/slices/designs/render.js +215 -0
- package/dist/slices/designs/repo.js +101 -0
- package/dist/slices/designs/requests.js +80 -0
- package/dist/slices/designs/runner.js +239 -0
- package/dist/slices/designs/sample-repo.js +91 -0
- package/dist/slices/designs/sample.js +223 -0
- package/dist/slices/designs/schema.js +87 -0
- package/dist/slices/designs/sections.js +44 -0
- package/dist/slices/designs/service.js +233 -0
- package/dist/slices/designs/steps-repo.js +102 -0
- package/dist/slices/designs/studio-kind.js +39 -0
- package/dist/slices/designs/template-checks.js +44 -0
- package/dist/slices/designs/template-make.js +118 -0
- package/dist/slices/designs/templates.js +218 -0
- package/dist/slices/document/fonts.js +2 -1
- package/dist/slices/document/model.js +7 -4
- package/dist/slices/document/render.js +2 -1
- package/dist/slices/document/theme-schema.js +1 -1
- package/dist/slices/document/theme.js +49 -3
- package/dist/slices/episodes/summarize.js +6 -2
- package/dist/slices/estimate/index.js +72 -28
- package/dist/slices/estimate/requests.js +4 -4
- package/dist/slices/fixes/rules.js +3 -1
- package/dist/slices/fonts/catalog.js +5 -3
- package/dist/slices/fonts/coverage.js +2 -1
- package/dist/slices/fonts/discovery.js +3 -2
- package/dist/slices/fonts/files.js +2 -6
- package/dist/slices/fonts/index.js +0 -1
- package/dist/slices/fonts/sfnt.js +45 -10
- package/dist/slices/images/appearance.js +8 -16
- package/dist/slices/images/scenes.js +102 -28
- package/dist/slices/keywords/digest.js +85 -0
- package/dist/slices/keywords/facts.js +92 -0
- package/dist/slices/keywords/fill.js +80 -0
- package/dist/slices/keywords/model.js +75 -0
- package/dist/slices/keywords/rules.js +187 -0
- package/dist/slices/keywords/schema.js +28 -0
- package/dist/slices/keywords/specs.js +260 -0
- package/dist/slices/keywords/values.js +74 -0
- package/dist/slices/library/lint.js +29 -12
- package/dist/slices/library/model.js +2 -0
- package/dist/slices/library/photorealistic.js +4 -12
- package/dist/slices/library/rename.js +9 -7
- package/dist/slices/library/save.js +14 -10
- package/dist/slices/library/slots.js +37 -11
- package/dist/slices/library/transfer.js +5 -4
- package/dist/slices/library/used-by.js +25 -10
- package/dist/slices/loudness/loudnorm.js +53 -12
- package/dist/slices/loudness/model.js +3 -2
- package/dist/slices/loudness/sentence-level.js +2 -1
- package/dist/slices/model-checks/cost.js +61 -0
- package/dist/slices/model-checks/listing.js +124 -0
- package/dist/slices/model-checks/model.js +20 -0
- package/dist/slices/model-checks/record.js +32 -0
- package/dist/slices/model-checks/repo.js +120 -0
- package/dist/slices/model-checks/rules.js +125 -0
- package/dist/slices/model-checks/run.js +143 -0
- package/dist/slices/model-checks/service.js +198 -0
- package/dist/slices/model-upkeep/model.js +1 -1
- package/dist/slices/model-upkeep/switch.js +72 -27
- package/dist/slices/model-upkeep/usage.js +39 -13
- package/dist/slices/narration/breaths.js +3 -1
- package/dist/slices/narration/clean-pieces.js +3 -1
- package/dist/slices/narration/concat.js +2 -1
- package/dist/slices/narration/pauses-model.js +3 -2
- package/dist/slices/narration/peaks.js +3 -4
- package/dist/slices/narration/pronunciation-chunks.js +4 -3
- package/dist/slices/narration/pronunciation.js +6 -4
- package/dist/slices/narration/steering.js +4 -3
- package/dist/slices/notifications/notifier.js +4 -3
- package/dist/slices/notifications/rules.js +5 -2
- package/dist/slices/notifications/settings.js +4 -12
- package/dist/slices/notifications/subject.js +4 -3
- package/dist/slices/onboarding/copy.js +21 -3
- package/dist/slices/onboarding/first-run.js +16 -40
- package/dist/slices/onboarding/full-video.js +3 -2
- package/dist/slices/onboarding/install.js +6 -4
- package/dist/slices/onboarding/model.js +26 -37
- package/dist/slices/onboarding/packs.js +22 -22
- package/dist/slices/onboarding/preferred-model.js +14 -0
- package/dist/slices/onboarding/quick-short.js +105 -65
- package/dist/slices/onboarding/sample.js +4 -2
- package/dist/slices/onboarding/state.js +9 -11
- package/dist/slices/onboarding/template.js +8 -6
- package/dist/slices/outputs/add.js +3 -2
- package/dist/slices/outputs/service.js +2 -2
- package/dist/slices/patch-notes/library.js +4 -3
- package/dist/slices/play-drafts/convert.js +4 -0
- package/dist/slices/play-drafts/readiness.js +3 -3
- package/dist/slices/play-drafts/review-inputs.js +9 -3
- package/dist/slices/play-drafts/review.js +5 -4
- package/dist/slices/play-drafts/schema.js +4 -0
- package/dist/slices/play-drafts/service.js +17 -7
- package/dist/slices/project-templates/from-project.js +9 -2
- package/dist/slices/project-templates/next-chapter.js +2 -1
- package/dist/slices/project-templates/provider-defaults.js +22 -0
- package/dist/slices/project-templates/repo.js +6 -1
- package/dist/slices/project-templates/setup-sections.js +26 -0
- package/dist/slices/project-templates/setup.js +14 -1
- package/dist/slices/project-templates/stand-ins.js +22 -0
- package/dist/slices/rebuild/activity.js +2 -0
- package/dist/slices/rebuild/admission-repo.js +3 -2
- package/dist/slices/rebuild/narration-history.js +4 -5
- package/dist/slices/rebuild/narration-retry.js +2 -1
- package/dist/slices/rebuild/preview-details.js +32 -14
- package/dist/slices/rebuild/preview-plan.js +3 -8
- package/dist/slices/rebuild/recipe-build.js +8 -3
- package/dist/slices/rebuild/recipe-describe.js +3 -3
- package/dist/slices/rebuild/recipe-digest.js +58 -0
- package/dist/slices/rebuild/recipe-input-schema.js +10 -0
- package/dist/slices/rebuild/recipe-keywords.js +71 -0
- package/dist/slices/rebuild/recipe-legacy.js +5 -1
- package/dist/slices/rebuild/recipe-model.js +4 -0
- package/dist/slices/rebuild/recipe-narration-text.js +2 -1
- package/dist/slices/rebuild/recipe-preparation.js +4 -4
- package/dist/slices/rebuild/recipe-reference.js +8 -4
- package/dist/slices/rebuild/recipe-reviews.js +27 -7
- package/dist/slices/rebuild/recipe-save.js +8 -5
- package/dist/slices/rebuild/recipe-scenes.js +40 -11
- package/dist/slices/rebuild/recipe-shorts.js +8 -8
- package/dist/slices/rebuild/recipe-social.js +46 -0
- package/dist/slices/rebuild/recipe-text.js +64 -20
- package/dist/slices/rebuild/recipe-validation.js +102 -31
- package/dist/slices/rebuild/recipe-visual.js +55 -16
- package/dist/slices/rebuild/recipe-voices.js +3 -2
- package/dist/slices/rebuild/recipe-work.js +19 -7
- package/dist/slices/rebuild/recipe-youtube.js +4 -3
- package/dist/slices/rebuild/recovery-selection.js +3 -1
- package/dist/slices/rebuild/recovery.js +9 -8
- package/dist/slices/rebuild/remake-outdated.js +5 -5
- package/dist/slices/rebuild/repo.js +4 -3
- package/dist/slices/rebuild/review-promise.js +25 -0
- package/dist/slices/rebuild/review-redo.js +3 -2
- package/dist/slices/rebuild/runtime-admission.js +4 -3
- package/dist/slices/rebuild/runtime-agent-review.js +11 -5
- package/dist/slices/rebuild/runtime-animate.js +10 -9
- package/dist/slices/rebuild/runtime-article.js +7 -5
- package/dist/slices/rebuild/runtime-checkpoints.js +5 -4
- package/dist/slices/rebuild/runtime-document.js +11 -13
- package/dist/slices/rebuild/runtime-export-bed.js +8 -12
- package/dist/slices/rebuild/runtime-export-edit.js +8 -16
- package/dist/slices/rebuild/runtime-export-inputs.js +12 -11
- package/dist/slices/rebuild/runtime-export-short.js +9 -5
- package/dist/slices/rebuild/runtime-export.js +38 -15
- package/dist/slices/rebuild/runtime-figure-card.js +2 -1
- package/dist/slices/rebuild/runtime-image.js +8 -12
- package/dist/slices/rebuild/runtime-instructions.js +2 -0
- package/dist/slices/rebuild/runtime-keywords.js +59 -0
- package/dist/slices/rebuild/runtime-lines.js +2 -11
- package/dist/slices/rebuild/runtime-local.js +17 -22
- package/dist/slices/rebuild/runtime-narration-publication.js +2 -5
- package/dist/slices/rebuild/runtime-narration-reuse.js +9 -13
- package/dist/slices/rebuild/runtime-narration-text.js +13 -17
- package/dist/slices/rebuild/runtime-pictures.js +2 -4
- package/dist/slices/rebuild/runtime-piece-label.js +10 -6
- package/dist/slices/rebuild/runtime-plan.js +31 -5
- package/dist/slices/rebuild/runtime-provider.js +67 -25
- package/dist/slices/rebuild/runtime-publication.js +10 -9
- package/dist/slices/rebuild/runtime-review.js +39 -33
- package/dist/slices/rebuild/runtime-run.js +4 -1
- package/dist/slices/rebuild/runtime-shorts.js +31 -50
- package/dist/slices/rebuild/runtime-store.js +20 -10
- package/dist/slices/rebuild/runtime-subtitles.js +28 -25
- package/dist/slices/rebuild/runtime-voices.js +10 -16
- package/dist/slices/rebuild/runtime-youtube.js +14 -31
- package/dist/slices/rebuild/service-readiness.js +11 -3
- package/dist/slices/rebuild/transition-repo.js +6 -5
- package/dist/slices/rebuild/work-records.js +6 -0
- package/dist/slices/reruns/cascade.js +1 -2
- package/dist/slices/reruns/index.js +6 -9
- package/dist/slices/research/synthesis.js +3 -2
- package/dist/slices/reviews/external-model.js +4 -1
- package/dist/slices/reviews/external-repo.js +2 -1
- package/dist/slices/reviews/external-rows.js +2 -3
- package/dist/slices/reviews/holds.js +5 -4
- package/dist/slices/reviews/model.js +32 -11
- package/dist/slices/reviews/repo.js +2 -1
- package/dist/slices/reviews/rules.js +4 -3
- package/dist/slices/reviews/verdict.js +2 -1
- package/dist/slices/reviews/view.js +5 -2
- package/dist/slices/revisions/downloads.js +4 -5
- package/dist/slices/revisions/image-plan.js +1 -1
- package/dist/slices/revisions/mutation-assets.js +9 -5
- package/dist/slices/revisions/mutation-cues.js +2 -5
- package/dist/slices/revisions/mutations.js +3 -3
- package/dist/slices/revisions/repo.js +3 -1
- package/dist/slices/revisions/schema.js +1 -0
- package/dist/slices/revisions/view.js +20 -0
- package/dist/slices/run-cost/limits.js +5 -2
- package/dist/slices/run-cost/meter.js +12 -5
- package/dist/slices/run-cost/panel.js +8 -74
- package/dist/slices/run-cost/plan-readings.js +127 -0
- package/dist/slices/run-cost/plan-use.js +169 -0
- package/dist/slices/run-cost/pricing.js +3 -3
- package/dist/slices/run-cost/week.js +32 -4
- package/dist/slices/schedules/agenda.js +18 -7
- package/dist/slices/schedules/by-date.js +42 -0
- package/dist/slices/schedules/calendar.js +5 -0
- package/dist/slices/schedules/design-agenda.js +36 -0
- package/dist/slices/schedules/design-check.js +41 -0
- package/dist/slices/schedules/design-run.js +126 -0
- package/dist/slices/schedules/next-run.js +62 -0
- package/dist/slices/schedules/prepare.js +18 -2
- package/dist/slices/schedules/repo.js +30 -21
- package/dist/slices/schedules/rescore.js +35 -0
- package/dist/slices/schedules/scheduler.js +13 -3
- package/dist/slices/schedules/schema.js +57 -3
- package/dist/slices/schedules/score.js +107 -0
- package/dist/slices/schedules/service.js +57 -24
- package/dist/slices/schedules/topic-list.js +15 -0
- package/dist/slices/schedules/topic-scores.js +104 -0
- package/dist/slices/schedules/topics.js +53 -41
- package/dist/slices/settings/cli-paths.js +4 -6
- package/dist/slices/settings/first-run.js +8 -43
- package/dist/slices/settings/health.js +30 -15
- package/dist/slices/settings/key-test.js +1 -1
- package/dist/slices/settings/model-reach.js +3 -17
- package/dist/slices/settings/model.js +14 -2
- package/dist/slices/settings/playback.js +34 -23
- package/dist/slices/settings/probe-read.js +16 -0
- package/dist/slices/settings/repo.js +33 -0
- package/dist/slices/settings/tutorial.js +2 -2
- package/dist/slices/settings/voices.js +4 -12
- package/dist/slices/shorts/keep.js +2 -1
- package/dist/slices/shorts/pick.js +6 -18
- package/dist/slices/shorts/prompts.js +4 -14
- package/dist/slices/shorts/render.js +6 -2
- package/dist/slices/social/channel.js +44 -0
- package/dist/slices/social/defaults.js +17 -0
- package/dist/slices/social/due.js +36 -0
- package/dist/slices/social/extension.js +18 -0
- package/dist/slices/social/fit.js +115 -0
- package/dist/slices/social/limits.js +186 -0
- package/dist/slices/social/links.js +21 -0
- package/dist/slices/social/media.js +132 -0
- package/dist/slices/social/model.js +75 -0
- package/dist/slices/social/notice.js +19 -0
- package/dist/slices/social/pack.js +176 -0
- package/dist/slices/social/post-prompt.js +56 -0
- package/dist/slices/social/queue.js +31 -0
- package/dist/slices/social/repo.js +133 -0
- package/dist/slices/social/reshape.js +97 -0
- package/dist/slices/social/rules.js +5 -0
- package/dist/slices/social/schema.js +70 -0
- package/dist/slices/social/serve.js +51 -0
- package/dist/slices/social/service.js +129 -0
- package/dist/slices/social/settings.js +21 -0
- package/dist/slices/social/sources.js +114 -0
- package/dist/slices/social/state.js +110 -0
- package/dist/slices/social/text.js +149 -0
- package/dist/slices/storage/arrange.js +3 -1
- package/dist/slices/storage/assets.js +8 -10
- package/dist/slices/storage/backup-designs.js +74 -0
- package/dist/slices/storage/backup-export.js +8 -3
- package/dist/slices/storage/backup-format.js +10 -0
- package/dist/slices/storage/backup-import.js +22 -4
- package/dist/slices/storage/delete-project.js +6 -5
- package/dist/slices/storage/files-location.js +6 -8
- package/dist/slices/storage/layout.js +13 -2
- package/dist/slices/storage/model.js +3 -0
- package/dist/slices/storage/portable.js +17 -12
- package/dist/slices/storage/schema.js +2 -0
- package/dist/slices/storage/staging-refs.js +2 -1
- package/dist/slices/storage/staging.js +6 -9
- package/dist/slices/storage/trim.js +26 -9
- package/dist/slices/studio/calendar.js +7 -2
- package/dist/slices/studio/disclosure.js +4 -4
- package/dist/slices/studio/model.js +1 -1
- package/dist/slices/studio/pack.js +9 -8
- package/dist/slices/studio/pick.js +2 -11
- package/dist/slices/studio/plan.js +7 -16
- package/dist/slices/studio/queue.js +3 -14
- package/dist/slices/studio/releases.js +26 -0
- package/dist/slices/studio/settings.js +20 -57
- package/dist/slices/studio/videos.js +35 -4
- package/dist/slices/style-preview/render.js +37 -16
- package/dist/slices/style-preview/schema.js +14 -1
- package/dist/slices/style-preview/service.js +2 -1
- package/dist/slices/style-preview/settings.js +33 -5
- package/dist/slices/subtitles/captions.js +1 -1
- package/dist/slices/telemetry/collector-client.js +2 -4
- package/dist/slices/telemetry/flush.js +2 -4
- package/dist/slices/telemetry/model.js +0 -7
- package/dist/slices/telemetry/record.js +10 -3
- package/dist/slices/template-studio/anatomy-view.js +161 -0
- package/dist/slices/template-studio/anatomy.js +73 -0
- package/dist/slices/template-studio/bakeoff-make.js +225 -0
- package/dist/slices/template-studio/bakeoff-repo.js +83 -0
- package/dist/slices/template-studio/bakeoff.js +333 -0
- package/dist/slices/template-studio/budget.js +106 -0
- package/dist/slices/template-studio/cards.js +78 -0
- package/dist/slices/template-studio/checks.js +322 -0
- package/dist/slices/template-studio/document-sample.js +56 -0
- package/dist/slices/template-studio/edit.js +15 -0
- package/dist/slices/template-studio/for-all.js +161 -0
- package/dist/slices/template-studio/free-video.js +31 -0
- package/dist/slices/template-studio/free.js +176 -0
- package/dist/slices/template-studio/gate.js +40 -0
- package/dist/slices/template-studio/hidden.js +20 -0
- package/dist/slices/template-studio/jobs.js +79 -0
- package/dist/slices/template-studio/loudness.js +111 -0
- package/dist/slices/template-studio/make.js +40 -0
- package/dist/slices/template-studio/model.js +58 -0
- package/dist/slices/template-studio/pack-import.js +253 -0
- package/dist/slices/template-studio/pack-samples.js +60 -0
- package/dist/slices/template-studio/pack.js +207 -0
- package/dist/slices/template-studio/paid-view.js +66 -0
- package/dist/slices/template-studio/paid.js +355 -0
- package/dist/slices/template-studio/quote.js +155 -0
- package/dist/slices/template-studio/ready.js +104 -0
- package/dist/slices/template-studio/repo.js +134 -0
- package/dist/slices/template-studio/reviews-path.js +58 -0
- package/dist/slices/template-studio/runner.js +84 -0
- package/dist/slices/template-studio/save.js +236 -0
- package/dist/slices/template-studio/say-it-as.js +248 -0
- package/dist/slices/template-studio/schedule-gate.js +34 -0
- package/dist/slices/template-studio/schedule-price.js +32 -0
- package/dist/slices/template-studio/schedule-sample.js +91 -0
- package/dist/slices/template-studio/schema.js +243 -0
- package/dist/slices/template-studio/scope.js +124 -0
- package/dist/slices/template-studio/sections.js +176 -0
- package/dist/slices/template-studio/service.js +175 -0
- package/dist/slices/template-studio/status.js +191 -0
- package/dist/slices/template-studio/topic.js +223 -0
- package/dist/slices/thumbnail/by-llm.js +1 -1
- package/dist/slices/trash/designs.js +76 -0
- package/dist/slices/trash/model.js +13 -4
- package/dist/slices/trash/service.js +52 -2
- package/dist/slices/tutorials/library.js +3 -2
- package/dist/slices/uploads/repo.js +37 -1
- package/dist/slices/uploads/rules.js +3 -2
- package/dist/slices/video/ass-text.js +17 -0
- package/dist/slices/video/audio-export.js +2 -64
- package/dist/slices/video/edit-list.js +2 -2
- package/dist/slices/video/edit-settings.js +1 -1
- package/dist/slices/video/encoding.js +169 -0
- package/dist/slices/video/ffmpeg.js +23 -21
- package/dist/slices/video/figure-card.js +1 -15
- package/dist/slices/video/plan.js +2 -1
- package/dist/slices/video/slideshow.js +43 -18
- package/dist/slices/voices/attribution.js +3 -2
- package/dist/slices/voices/model.js +9 -8
- package/dist/slices/voices/palette.js +0 -3
- package/dist/slices/voices/script.js +2 -1
- package/dist/slices/youtube/answer.js +4 -16
- package/dist/slices/youtube/edits-repo.js +3 -12
- package/dist/tutorials/AI-CLIs.md +25 -25
- package/dist/tutorials/Agents-MCP.md +34 -33
- package/dist/tutorials/Assistant.md +42 -0
- package/dist/tutorials/Backups.md +7 -7
- package/dist/tutorials/Calendar.md +17 -25
- package/dist/tutorials/Cast-Library.md +19 -19
- package/dist/tutorials/Channels.md +42 -35
- package/dist/tutorials/Costs-and-Run-Cost.md +38 -40
- package/dist/tutorials/Create-Article.md +114 -0
- package/dist/tutorials/Create-Extras.md +112 -0
- package/dist/tutorials/Create-Images.md +165 -0
- package/dist/tutorials/Create-Narration.md +178 -0
- package/dist/tutorials/Create-Overview.md +244 -0
- package/dist/tutorials/Create-Video-and-Style.md +144 -0
- package/dist/tutorials/Docker.md +3 -3
- package/dist/tutorials/Document-Themes.md +32 -32
- package/dist/tutorials/Editing-a-Project.md +94 -94
- package/dist/tutorials/FAQ.md +18 -18
- package/dist/tutorials/First-Launch-and-Welcome.md +50 -64
- package/dist/tutorials/Home-and-Projects.md +115 -71
- package/dist/tutorials/Home.md +33 -33
- package/dist/tutorials/Install.md +14 -13
- package/dist/tutorials/Intros-and-Outros.md +33 -34
- package/dist/tutorials/Keyboard-Shortcuts-and-Command-Palette.md +62 -58
- package/dist/tutorials/Keywords-Slopify-Fills.md +87 -0
- package/dist/tutorials/Models.md +53 -21
- package/dist/tutorials/Multiple-Voices.md +41 -40
- package/dist/tutorials/Narration-Aliases-and-Glossary.md +14 -14
- package/dist/tutorials/Notifications.md +27 -17
- package/dist/tutorials/Other-Languages.md +15 -15
- package/dist/tutorials/PDF-Documents.md +38 -29
- package/dist/tutorials/Posting-to-Social-Sites.md +121 -0
- package/dist/tutorials/Project-Page.md +118 -120
- package/dist/tutorials/Prompts-and-Templates.md +84 -0
- package/dist/tutorials/Prompts.md +48 -44
- package/dist/tutorials/Providers-and-Keys.md +105 -98
- package/dist/tutorials/Publishing-to-YouTube.md +31 -31
- package/dist/tutorials/Recovery-and-Retries.md +31 -31
- package/dist/tutorials/Remote-Access.md +63 -0
- package/dist/tutorials/Reviews-and-Checkpoints.md +98 -90
- package/dist/tutorials/Schedules.md +46 -27
- package/dist/tutorials/Settings-Reference.md +194 -174
- package/dist/tutorials/Shorts.md +34 -33
- package/dist/tutorials/Start-at-Login.md +3 -3
- package/dist/tutorials/Studio-Extension.md +22 -24
- package/dist/tutorials/Template-Studio.md +128 -0
- package/dist/tutorials/Templates.md +51 -77
- package/dist/tutorials/Trash.md +4 -4
- package/dist/tutorials/Troubleshooting.md +44 -44
- package/dist/tutorials/Uninstalling-and-Moving.md +5 -5
- package/dist/tutorials/Updating-and-Patch-Notes.md +18 -18
- package/dist/tutorials/Video-Editing.md +34 -34
- package/dist/tutorials/Where-Your-Files-Live.md +10 -10
- package/dist/tutorials/YouTube-Description.md +26 -26
- package/dist/tutorials/Your-First-Short.md +35 -35
- package/dist/tutorials/_Sidebar.md +19 -14
- package/dist/updater/work-in-progress.js +42 -12
- package/dist/web/assets/index-BNftKQrA.css +1 -0
- package/dist/web/assets/index-Cpa58woP.js +459 -0
- package/dist/web/assets/pdf-BKh8f7R6.js +56 -0
- package/dist/web/assets/pdf.worker.min-BmVo14Nb.mjs +28 -0
- package/dist/web/assets/pdf.worker.min-D7i8o3wR.js +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +6 -2
- package/dist/slices/article/provided-entries.js +0 -66
- package/dist/slices/article/run.js +0 -145
- package/dist/slices/images/run.js +0 -234
- package/dist/slices/narration/legacy-plan.js +0 -58
- package/dist/slices/narration/run.js +0 -270
- package/dist/slices/rebuild/runtime-actions.js +0 -90
- package/dist/slices/research/run.js +0 -224
- package/dist/slices/subtitles/prepare.js +0 -194
- package/dist/slices/subtitles/transcript.js +0 -34
- package/dist/slices/thumbnail/run.js +0 -219
- package/dist/slices/video/audio-inputs.js +0 -33
- package/dist/slices/video/reuse-audio.js +0 -62
- package/dist/slices/video/run.js +0 -78
- package/dist/slices/video/write-export.js +0 -158
- package/dist/slices/video/write-subtitles.js +0 -59
- package/dist/tutorials/Library-Overview.md +0 -76
- package/dist/tutorials/Play-Images.md +0 -161
- package/dist/tutorials/Play-Narration.md +0 -167
- package/dist/tutorials/Play-Outputs.md +0 -109
- package/dist/tutorials/Play-Overview.md +0 -227
- package/dist/tutorials/Play-Title-and-Article.md +0 -136
- package/dist/tutorials/Play-Video-and-Style.md +0 -143
- package/dist/web/assets/index-BadsoORk.css +0 -1
- package/dist/web/assets/index-D8LqNsCl.js +0 -326
- package/dist/web/assets/pdf-T7MCwjIv.js +0 -54
- package/dist/web/assets/pdf.worker.min-Dswkl-cV.mjs +0 -29
- package/dist/web/assets/pdf.worker.min-zC6nfbxG.js +0 -1
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// How long after a post went out the extension looks it up on the person's profile, and for
|
|
2
|
+
// how long it keeps looking.
|
|
3
|
+
export const lookupAfterMs = 10 * 60 * 1000;
|
|
4
|
+
export const lookupForMs = 7 * 24 * 60 * 60 * 1000;
|
|
5
|
+
export const lookupStartLength = 40;
|
|
6
|
+
// A filled or scheduled post without its link, once it should be on the site.
|
|
7
|
+
export function lookupDue(progress, releaseAt, now) {
|
|
8
|
+
if (progress.url !== null)
|
|
9
|
+
return false;
|
|
10
|
+
if (progress.state !== "filled" && progress.state !== "scheduled")
|
|
11
|
+
return false;
|
|
12
|
+
const from = Date.parse(progress.state === "scheduled" && releaseAt !== null ? releaseAt : (progress.filledAt ?? ""));
|
|
13
|
+
if (!Number.isFinite(from))
|
|
14
|
+
return false;
|
|
15
|
+
const since = now.getTime() - from;
|
|
16
|
+
return since >= lookupAfterMs && since <= lookupForMs;
|
|
17
|
+
}
|
|
18
|
+
// The post's first characters, as the profile page shows them, to find it among its posts.
|
|
19
|
+
export function lookupStart(text) {
|
|
20
|
+
return [...text.replace(/\s+/gu, " ").trim()].slice(0, lookupStartLength).join("");
|
|
21
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// Which files a post carries, in the shape its site wants them. Pure: the sources come from
|
|
2
|
+
// the project's outputs (`sources.ts`); a reshaped picture or a design's render is made the
|
|
3
|
+
// first time it is asked for (`reshape.ts`), so listing a pack never draws anything.
|
|
4
|
+
import { editSettings } from "../../kernel/ports/settings-places.js";
|
|
5
|
+
import { limitsFor } from "./limits.js";
|
|
6
|
+
import { platformLabels, } from "./model.js";
|
|
7
|
+
const shapeSlugs = {
|
|
8
|
+
"1:1": "1x1",
|
|
9
|
+
"4:5": "4x5",
|
|
10
|
+
"9:16": "9x16",
|
|
11
|
+
"16:9": "16x9",
|
|
12
|
+
};
|
|
13
|
+
// A design's format on each site: portrait on Instagram, a story on TikTok, square elsewhere.
|
|
14
|
+
export function designFormatFor(platform) {
|
|
15
|
+
return platform === "instagram" ? "portrait" : platform === "tiktok" ? "story" : "square";
|
|
16
|
+
}
|
|
17
|
+
const designShapes = {
|
|
18
|
+
portrait: "4:5",
|
|
19
|
+
story: "9:16",
|
|
20
|
+
square: "1:1",
|
|
21
|
+
};
|
|
22
|
+
// The file names the media routes take: the asset itself, `<asset>--4x5` for a reshaped copy,
|
|
23
|
+
// and `design--<id>--<format>` for a design's render.
|
|
24
|
+
export function mediaFile(asset, shape) {
|
|
25
|
+
return shape === "original" ? asset : `${asset}--${shapeSlugs[shape]}`;
|
|
26
|
+
}
|
|
27
|
+
export function parseMediaFile(file) {
|
|
28
|
+
const design = /^design--([A-Za-z0-9_-]{1,64})--([a-z0-9]+)$/u.exec(file);
|
|
29
|
+
if (design !== null) {
|
|
30
|
+
const format = Object.keys(designShapes).find((one) => one === design[2]);
|
|
31
|
+
return format === undefined || design[1] === undefined
|
|
32
|
+
? undefined
|
|
33
|
+
: { kind: "design", id: design[1], format: format };
|
|
34
|
+
}
|
|
35
|
+
const match = /^([a-z0-9]+(?:-[a-z0-9]+)*)(?:--([0-9]+x[0-9]+))?$/u.exec(file);
|
|
36
|
+
if (match === null || match[1] === undefined)
|
|
37
|
+
return undefined;
|
|
38
|
+
if (match[2] === undefined)
|
|
39
|
+
return { kind: "asset", asset: match[1], shape: "original" };
|
|
40
|
+
const shape = Object.keys(shapeSlugs).find((one) => shapeSlugs[one] === match[2]);
|
|
41
|
+
return shape === undefined ? undefined : { kind: "asset", asset: match[1], shape };
|
|
42
|
+
}
|
|
43
|
+
const url = (projectId, file) => `/api/social/media/${projectId}/${file}`;
|
|
44
|
+
function still(sources, kind, source, shape) {
|
|
45
|
+
const file = mediaFile(source.asset, shape);
|
|
46
|
+
const reshaped = shape !== "original";
|
|
47
|
+
return {
|
|
48
|
+
kind,
|
|
49
|
+
file,
|
|
50
|
+
url: url(sources.projectId, file),
|
|
51
|
+
filename: reshaped
|
|
52
|
+
? `${source.filename.replace(/\.[^.]+$/u, "")}-${shapeSlugs[shape]}.jpg`
|
|
53
|
+
: source.filename,
|
|
54
|
+
contentType: reshaped ? "image/jpeg" : source.contentType,
|
|
55
|
+
bytes: reshaped ? null : source.bytes,
|
|
56
|
+
shape,
|
|
57
|
+
...(source.alt === undefined ? {} : { alt: source.alt }),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
// The post's files in the order the site shows them: the video first, then the pictures.
|
|
61
|
+
export function mediaFor(platform, choice, sources) {
|
|
62
|
+
const media = [];
|
|
63
|
+
const warnings = [];
|
|
64
|
+
const site = platformLabels[platform];
|
|
65
|
+
const missing = (sentence) => warnings.push({ code: "media-missing", sentence });
|
|
66
|
+
const shape = limitsFor(platform).pictureShape;
|
|
67
|
+
if (choice.video === "long") {
|
|
68
|
+
if (sources.long === null)
|
|
69
|
+
missing("The video isn't made yet. Let the Video stage finish (or use Continue the run on the project page), then open Copy for YouTube Studio again.");
|
|
70
|
+
else
|
|
71
|
+
media.push({
|
|
72
|
+
kind: "video",
|
|
73
|
+
file: sources.long.asset,
|
|
74
|
+
url: url(sources.projectId, sources.long.asset),
|
|
75
|
+
filename: sources.long.filename,
|
|
76
|
+
contentType: sources.long.contentType,
|
|
77
|
+
bytes: sources.long.bytes,
|
|
78
|
+
shape: sources.format,
|
|
79
|
+
...(sources.long.seconds === null ? {} : { seconds: sources.long.seconds }),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
else if (choice.video === "short") {
|
|
83
|
+
const number = choice.short ?? 1;
|
|
84
|
+
const short = sources.shorts.find((one) => one.number === number);
|
|
85
|
+
if (short?.file == null)
|
|
86
|
+
missing(short === undefined
|
|
87
|
+
? `This project has no short ${String(number)}. Pick another short for ${site} in the post's media, or turn on Shorts in ${editSettings.shorts}.`
|
|
88
|
+
: `Short ${String(number)} isn't rendered yet. Let the Video stage finish, then open Copy for YouTube Studio again.`);
|
|
89
|
+
else
|
|
90
|
+
media.push({
|
|
91
|
+
kind: "short",
|
|
92
|
+
file: short.file.asset,
|
|
93
|
+
url: url(sources.projectId, short.file.asset),
|
|
94
|
+
filename: short.file.filename,
|
|
95
|
+
contentType: short.file.contentType,
|
|
96
|
+
bytes: short.file.bytes,
|
|
97
|
+
shape: "9:16",
|
|
98
|
+
...(short.file.seconds === null ? {} : { seconds: short.file.seconds }),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
if (choice.thumbnail === true) {
|
|
102
|
+
if (sources.thumbnail === null)
|
|
103
|
+
missing(`The thumbnail isn't made yet. Let the Thumbnail stage finish, or set Thumbnail source in ${editSettings.extras}.`);
|
|
104
|
+
else
|
|
105
|
+
media.push(still(sources, "thumbnail", sources.thumbnail, shape));
|
|
106
|
+
}
|
|
107
|
+
const wanted = choice.pictures ?? 0;
|
|
108
|
+
if (wanted > 0) {
|
|
109
|
+
if (sources.pictures.length === 0)
|
|
110
|
+
missing("This project has no pictures yet. Let the Images stage finish, or pick other media for this post.");
|
|
111
|
+
for (const picture of sources.pictures.slice(0, wanted))
|
|
112
|
+
media.push(still(sources, "picture", picture, shape));
|
|
113
|
+
}
|
|
114
|
+
const format = designFormatFor(platform);
|
|
115
|
+
for (const id of choice.designs ?? []) {
|
|
116
|
+
if (!sources.designs.some((design) => design.id === id)) {
|
|
117
|
+
missing("A design picked for this post is gone or belongs to another project. Pick the post's designs again.");
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const file = `design--${id}--${format}`;
|
|
121
|
+
media.push({
|
|
122
|
+
kind: "design",
|
|
123
|
+
file,
|
|
124
|
+
url: url(sources.projectId, file),
|
|
125
|
+
filename: `design-${id}-${format}.png`,
|
|
126
|
+
contentType: "image/png",
|
|
127
|
+
bytes: null,
|
|
128
|
+
shape: designShapes[format] ?? "original",
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
return { media, warnings };
|
|
132
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Posting a project's work to X, LinkedIn, Instagram, TikTok, Reddit and Bluesky. Slopify never
|
|
2
|
+
// posts: the Slopify Studio extension fills each site's own post form and the person presses
|
|
3
|
+
// Post. Browser-safe: the project page, Template Studio and the extension read these names.
|
|
4
|
+
export const socialPlatforms = [
|
|
5
|
+
"x",
|
|
6
|
+
"linkedin",
|
|
7
|
+
"instagram",
|
|
8
|
+
"tiktok",
|
|
9
|
+
"reddit",
|
|
10
|
+
"bluesky",
|
|
11
|
+
];
|
|
12
|
+
export const platformLabels = {
|
|
13
|
+
x: "X",
|
|
14
|
+
linkedin: "LinkedIn",
|
|
15
|
+
instagram: "Instagram",
|
|
16
|
+
tiktok: "TikTok",
|
|
17
|
+
reddit: "Reddit",
|
|
18
|
+
bluesky: "Bluesky",
|
|
19
|
+
};
|
|
20
|
+
// Where a post's words come from. "description" is the YouTube description without its
|
|
21
|
+
// chapters, "summary" the article's digest summary, "prompt" a Library prompt of kind "post"
|
|
22
|
+
// (a paid AI call), "custom" a short text with {{Slopify_…}} keywords, "none" no words.
|
|
23
|
+
export const postTextSources = [
|
|
24
|
+
"description",
|
|
25
|
+
"summary",
|
|
26
|
+
"title",
|
|
27
|
+
"prompt",
|
|
28
|
+
"custom",
|
|
29
|
+
"none",
|
|
30
|
+
];
|
|
31
|
+
// The X subscription of the channel's account: it only changes which warnings a post gets.
|
|
32
|
+
export const xTiers = ["free", "premium", "premium_plus"];
|
|
33
|
+
export const xTierLabels = {
|
|
34
|
+
free: "Free",
|
|
35
|
+
premium: "Premium",
|
|
36
|
+
premium_plus: "Premium+",
|
|
37
|
+
};
|
|
38
|
+
export const redditPostKinds = ["link", "text", "media"];
|
|
39
|
+
// Where a post stands. "waiting": the person pressed Fill in <site>; "filling": the extension
|
|
40
|
+
// took it; "filled": the form is filled and waits for the person's Post; "scheduled": the site
|
|
41
|
+
// will post it at the release time; "posted": it is on the site; "failed": the fill stopped.
|
|
42
|
+
export const postStates = [
|
|
43
|
+
"none",
|
|
44
|
+
"waiting",
|
|
45
|
+
"filling",
|
|
46
|
+
"filled",
|
|
47
|
+
"scheduled",
|
|
48
|
+
"posted",
|
|
49
|
+
"failed",
|
|
50
|
+
];
|
|
51
|
+
// What the extension reports after a fill (`POST /api/social/ext/result`).
|
|
52
|
+
export const reportedStates = ["filling", "filled", "scheduled", "posted", "failed"];
|
|
53
|
+
export const postMediaKinds = ["video", "short", "thumbnail", "picture", "design"];
|
|
54
|
+
export const mediaShapes = ["original", "1:1", "4:5", "9:16", "16:9"];
|
|
55
|
+
// How the post meets the release time: the site's own scheduler set to it, the extension
|
|
56
|
+
// filling it when it is due (the person presses Post), or posted when the person presses it.
|
|
57
|
+
export const scheduleModes = ["native", "at-release", "now"];
|
|
58
|
+
export const warningCodes = [
|
|
59
|
+
"text-cut",
|
|
60
|
+
"too-many-pictures",
|
|
61
|
+
"video-too-long",
|
|
62
|
+
"video-and-pictures",
|
|
63
|
+
"no-media",
|
|
64
|
+
"subreddit-missing",
|
|
65
|
+
"title-missing",
|
|
66
|
+
"release-past",
|
|
67
|
+
"release-outside-window",
|
|
68
|
+
"no-release",
|
|
69
|
+
"media-missing",
|
|
70
|
+
"text-missing",
|
|
71
|
+
"full-video",
|
|
72
|
+
"too-many-videos",
|
|
73
|
+
"text-too-long",
|
|
74
|
+
"link-missing",
|
|
75
|
+
];
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { platformLabels } from "./model.js";
|
|
2
|
+
// The button the person presses on each site to post.
|
|
3
|
+
const postButton = {
|
|
4
|
+
x: "Post",
|
|
5
|
+
linkedin: "Post",
|
|
6
|
+
instagram: "Share",
|
|
7
|
+
tiktok: "Post",
|
|
8
|
+
reddit: "Post",
|
|
9
|
+
bluesky: "Publish",
|
|
10
|
+
};
|
|
11
|
+
// The Notification URL's message when the extension filled a post at its release time on a site
|
|
12
|
+
// without a scheduler: the person presses the button, Slopify never does.
|
|
13
|
+
export function postReadyNotice(pack) {
|
|
14
|
+
const site = platformLabels[pack.platform];
|
|
15
|
+
return {
|
|
16
|
+
headline: `Ready to post on ${site}: ${pack.projectTitle}`,
|
|
17
|
+
detail: `The Slopify Studio extension filled the post in your browser. Open the ${site} tab and press ${postButton[pack.platform]}; Slopify never posts by itself.`,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { projectById } from "../admission/repo.js";
|
|
2
|
+
import { render } from "../admission/substitute.js";
|
|
3
|
+
import { fillWith } from "../keywords/fill.js";
|
|
4
|
+
import { keywordOf } from "../keywords/specs.js";
|
|
5
|
+
import { factValues } from "../keywords/values.js";
|
|
6
|
+
import { defaultReddit } from "./defaults.js";
|
|
7
|
+
import { mediaWarnings, redditWarnings, scheduleFor } from "./fit.js";
|
|
8
|
+
import { composeUrls, limitsFor, redditSubmitUrl } from "./limits.js";
|
|
9
|
+
import { mediaFor } from "./media.js";
|
|
10
|
+
import { platformLabels, socialPlatforms, } from "./model.js";
|
|
11
|
+
import { postsOf } from "./repo.js";
|
|
12
|
+
import { postSources } from "./sources.js";
|
|
13
|
+
import { countFor, fitText, hashtagsFor } from "./text.js";
|
|
14
|
+
// The settings a site's post is made with: the project's own pick, else its template's. Off
|
|
15
|
+
// for this project, or a site neither turned on, has none.
|
|
16
|
+
export function choiceOf(settings, record, platform) {
|
|
17
|
+
const own = record?.choice ?? null;
|
|
18
|
+
if (own !== null)
|
|
19
|
+
return "off" in own ? undefined : { choice: own, chosenHere: true };
|
|
20
|
+
const base = settings?.[platform];
|
|
21
|
+
return base === undefined ? undefined : { choice: base, chosenHere: false };
|
|
22
|
+
}
|
|
23
|
+
// Every site the project posts to, each with its post as Prepare upload shows it and the
|
|
24
|
+
// extension fills it. A project with no posting settings and no picks of its own has none.
|
|
25
|
+
export function postPacks(deps, projectId, now) {
|
|
26
|
+
const project = projectById(deps.db, projectId);
|
|
27
|
+
if (project === undefined)
|
|
28
|
+
return { ok: false, reason: "unknown-project" };
|
|
29
|
+
const records = postsOf(deps.db, projectId);
|
|
30
|
+
const settings = project.config.social;
|
|
31
|
+
const wanted = socialPlatforms.filter((platform) => choiceOf(settings, records.find((one) => one.platform === platform), platform) !== undefined);
|
|
32
|
+
if (wanted.length === 0)
|
|
33
|
+
return { ok: true, packs: { projectId, packs: [] } };
|
|
34
|
+
const sources = postSources(deps, projectId);
|
|
35
|
+
if (sources === undefined)
|
|
36
|
+
return { ok: false, reason: "unknown-project" };
|
|
37
|
+
const packs = wanted.flatMap((platform) => {
|
|
38
|
+
const record = records.find((one) => one.platform === platform && one.item === 0);
|
|
39
|
+
const pack = buildPack(sources, platform, record, now);
|
|
40
|
+
return pack === undefined ? [] : [pack];
|
|
41
|
+
});
|
|
42
|
+
return { ok: true, packs: { projectId, packs } };
|
|
43
|
+
}
|
|
44
|
+
export function packFor(deps, projectId, platform, now) {
|
|
45
|
+
const result = postPacks(deps, projectId, now);
|
|
46
|
+
return result.ok ? result.packs.packs.find((pack) => pack.platform === platform) : undefined;
|
|
47
|
+
}
|
|
48
|
+
function bodyFor(sources, choice, platform) {
|
|
49
|
+
switch (choice.text) {
|
|
50
|
+
case "description":
|
|
51
|
+
return sources.description;
|
|
52
|
+
case "summary":
|
|
53
|
+
return sources.summary;
|
|
54
|
+
case "title":
|
|
55
|
+
return sources.title;
|
|
56
|
+
case "prompt":
|
|
57
|
+
return sources.written[platform] ?? "";
|
|
58
|
+
case "custom":
|
|
59
|
+
return customText(sources, choice.custom ?? "");
|
|
60
|
+
case "none":
|
|
61
|
+
return "";
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// The person's own words with their fields and the keywords Slopify fills: the facts copied
|
|
65
|
+
// when the run started, and the summary. A keyword with no value leaves its line out.
|
|
66
|
+
function customText(sources, custom) {
|
|
67
|
+
const own = render(custom, sources.config.values);
|
|
68
|
+
const values = {
|
|
69
|
+
...factValues(sources.config.keywordFacts, sources.title, "description"),
|
|
70
|
+
Slopify_Summary: sources.summary,
|
|
71
|
+
};
|
|
72
|
+
const filled = fillWith(own, (name) => keywordOf(name)?.name === name
|
|
73
|
+
? Object.hasOwn(values, name)
|
|
74
|
+
? (values[name] ?? "")
|
|
75
|
+
: ""
|
|
76
|
+
: undefined);
|
|
77
|
+
return filled.ok ? filled.text.trim() : own.trim();
|
|
78
|
+
}
|
|
79
|
+
function buildPack(sources, platform, record, now) {
|
|
80
|
+
const picked = choiceOf(sources.config.social, record, platform);
|
|
81
|
+
if (picked === undefined)
|
|
82
|
+
return undefined;
|
|
83
|
+
const { choice } = picked;
|
|
84
|
+
const site = platformLabels[platform];
|
|
85
|
+
const tier = sources.channel.xTier;
|
|
86
|
+
const limits = limitsFor(platform, tier);
|
|
87
|
+
const warnings = [];
|
|
88
|
+
const { media, warnings: mediaMissing } = mediaFor(platform, choice.media, sources.media);
|
|
89
|
+
warnings.push(...mediaMissing, ...mediaWarnings(platform, media, tier));
|
|
90
|
+
const body = bodyFor(sources, choice, platform);
|
|
91
|
+
if (choice.text !== "none" && body === "")
|
|
92
|
+
warnings.push({ code: "text-missing", sentence: missingText(choice, site) });
|
|
93
|
+
const link = choice.link ? sources.youtubeLink : null;
|
|
94
|
+
if (choice.link && link === null)
|
|
95
|
+
warnings.push({
|
|
96
|
+
code: "link-missing",
|
|
97
|
+
sentence: "The YouTube link isn't known yet, so the post goes without it. It is added once the video is on YouTube (Copy for YouTube Studio → Fill in YouTube Studio, or paste the video's link there).",
|
|
98
|
+
});
|
|
99
|
+
const hashtags = hashtagsFor([sources.tags, sources.shortHashtags], Math.min(choice.hashtags, limits.hashtags));
|
|
100
|
+
const fitted = fitText({ body, link, hashtags }, limits, site);
|
|
101
|
+
if (fitted.notice !== undefined)
|
|
102
|
+
warnings.push({ code: "text-cut", sentence: fitted.notice });
|
|
103
|
+
const edit = record?.textEdit ?? null;
|
|
104
|
+
const text = edit ?? fitted.text;
|
|
105
|
+
const used = countFor(text, limits.unit);
|
|
106
|
+
if (edit !== null && used > limits.text)
|
|
107
|
+
warnings.push({
|
|
108
|
+
code: "text-too-long",
|
|
109
|
+
sentence: `The text is ${String(used)} characters; ${site} takes ${String(limits.text)}. Shorten it in the post's text box, or press Reset to go back to the written text.`,
|
|
110
|
+
});
|
|
111
|
+
const main = media.find((one) => one.kind === "short" || one.kind === "video");
|
|
112
|
+
const shortNumber = main?.kind === "short" ? (choice.media.short ?? 1) : undefined;
|
|
113
|
+
const item = shortNumber === undefined
|
|
114
|
+
? sources.youtube.items[0]
|
|
115
|
+
: sources.youtube.items.find((one) => one.kind === "short" && one.short === shortNumber);
|
|
116
|
+
const release = scheduleFor(platform, item?.scheduleAt ?? null, now);
|
|
117
|
+
warnings.push(...release.warnings);
|
|
118
|
+
const reddit = platform === "reddit"
|
|
119
|
+
? (record?.reddit ?? defaultReddit(sources.title.slice(0, 300)))
|
|
120
|
+
: undefined;
|
|
121
|
+
if (reddit !== undefined)
|
|
122
|
+
warnings.push(...redditWarnings(reddit));
|
|
123
|
+
return {
|
|
124
|
+
projectId: sources.projectId,
|
|
125
|
+
projectTitle: sources.title,
|
|
126
|
+
platform,
|
|
127
|
+
item: 0,
|
|
128
|
+
choice,
|
|
129
|
+
chosenHere: picked.chosenHere,
|
|
130
|
+
text,
|
|
131
|
+
generated: fitted.text,
|
|
132
|
+
edited: edit !== null,
|
|
133
|
+
count: { used, max: limits.text, unit: limits.unit },
|
|
134
|
+
hashtags,
|
|
135
|
+
link,
|
|
136
|
+
media,
|
|
137
|
+
aiLabel: limits.aiLabel && media.length > 0,
|
|
138
|
+
schedule: release.schedule,
|
|
139
|
+
...(reddit === undefined ? {} : { reddit }),
|
|
140
|
+
...(platform === "x" ? { xTier: tier } : {}),
|
|
141
|
+
composeUrl: reddit === undefined
|
|
142
|
+
? composeUrls[platform]
|
|
143
|
+
: redditSubmitUrl({
|
|
144
|
+
subreddit: reddit.subreddit,
|
|
145
|
+
title: reddit.title,
|
|
146
|
+
...(reddit.kind === "text" ? { text } : {}),
|
|
147
|
+
...(reddit.kind === "link" && sources.youtubeLink !== null
|
|
148
|
+
? { url: sources.youtubeLink }
|
|
149
|
+
: {}),
|
|
150
|
+
}),
|
|
151
|
+
warnings,
|
|
152
|
+
status: {
|
|
153
|
+
state: record?.state ?? "none",
|
|
154
|
+
url: record?.url ?? null,
|
|
155
|
+
message: record?.message ?? null,
|
|
156
|
+
at: record === undefined
|
|
157
|
+
? null
|
|
158
|
+
: (record.postedAt ?? record.scheduledAt ?? record.filledAt ?? record.waitingAt),
|
|
159
|
+
by: record?.by ?? null,
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
function missingText(choice, site) {
|
|
164
|
+
switch (choice.text) {
|
|
165
|
+
case "prompt":
|
|
166
|
+
return `The ${site} post isn't written yet. It is written by the post prompt after the video's description; let the run finish, or use Continue the run on the project page.`;
|
|
167
|
+
case "description":
|
|
168
|
+
case "summary":
|
|
169
|
+
return `There is no description to post yet. Let the Video stage write the YouTube description, or pick another text source for ${site}.`;
|
|
170
|
+
case "custom":
|
|
171
|
+
return `The post's own text is empty. Type it in the post's settings, or pick another text source for ${site}.`;
|
|
172
|
+
case "title":
|
|
173
|
+
case "none":
|
|
174
|
+
return `The post has no text. Pick a text source for ${site}.`;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// The request that writes a post for another site with a Library prompt of kind "post" (or the
|
|
2
|
+
// built-in one), and the check its answer must pass. Pure.
|
|
3
|
+
import { editSettings } from "../../kernel/ports/settings-places.js";
|
|
4
|
+
import { limitsFor } from "./limits.js";
|
|
5
|
+
import { platformLabels } from "./model.js";
|
|
6
|
+
import { countFor } from "./text.js";
|
|
7
|
+
// Changing it changes every post written with the built-in prompt, so it ships with a new
|
|
8
|
+
// recipe operation version.
|
|
9
|
+
export const defaultPostPrompt = "Write one post that makes people want to watch the video. Use the article's own facts, open with its most striking one, and write the way people write on this site.";
|
|
10
|
+
// A post written by a prompt is held to the site's limit, and never longer than this: a post,
|
|
11
|
+
// not an article.
|
|
12
|
+
const writtenMax = 3000;
|
|
13
|
+
// The limit a written post must keep: the site's free limit (X Premium's longer posts are the
|
|
14
|
+
// person's to write), at most `writtenMax`.
|
|
15
|
+
export function postTarget(platform) {
|
|
16
|
+
const limits = limitsFor(platform);
|
|
17
|
+
return { platform, max: Math.min(limits.text, writtenMax), unit: limits.unit };
|
|
18
|
+
}
|
|
19
|
+
export function postMessages(input) {
|
|
20
|
+
const site = platformLabels[input.target.platform];
|
|
21
|
+
return [
|
|
22
|
+
{
|
|
23
|
+
role: "system",
|
|
24
|
+
content: [
|
|
25
|
+
`You write one ${site} post about a narrated video, from its article.`,
|
|
26
|
+
input.instruction.trim() === "" ? defaultPostPrompt : input.instruction.trim(),
|
|
27
|
+
`Keep it within ${String(input.target.max)} characters as ${site} counts them.`,
|
|
28
|
+
"Leave out hashtags and links: they are added after you.",
|
|
29
|
+
"Answer with the post's text and nothing else.",
|
|
30
|
+
].join("\n\n"),
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
role: "user",
|
|
34
|
+
content: [`Video: ${input.title}`, `Article:\n\n${input.article}`].join("\n\n"),
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
}
|
|
38
|
+
// The post as it is kept: without quotes around it.
|
|
39
|
+
export function postText(answer) {
|
|
40
|
+
const trimmed = answer.trim();
|
|
41
|
+
const quoted = /^(["“'])([\s\S]*)(["”'])$/u.exec(trimmed);
|
|
42
|
+
return (quoted?.[2] ?? trimmed).trim();
|
|
43
|
+
}
|
|
44
|
+
// Why an answer can't be kept, or undefined when it can. A failed check is a failed attempt,
|
|
45
|
+
// which the provider wrapper asks again while attempts remain.
|
|
46
|
+
export function checkPost(answer, target) {
|
|
47
|
+
const site = platformLabels[target.platform];
|
|
48
|
+
const text = postText(answer);
|
|
49
|
+
const fix = `Use Try again; if it keeps happening, shorten what the post prompt asks for in Prompts → Post, or choose another model in ${editSettings.writer}.`;
|
|
50
|
+
if (text === "")
|
|
51
|
+
return `The AI model sent back an empty ${site} post. ${fix}`;
|
|
52
|
+
const used = countFor(text, target.unit);
|
|
53
|
+
if (used > target.max)
|
|
54
|
+
return `The AI model's ${site} post is ${String(used)} characters; it may be at most ${String(target.max)}. ${fix}`;
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { platformLabels } from "./model.js";
|
|
2
|
+
import { postsIn, writeProgress } from "./repo.js";
|
|
3
|
+
// A post waits for the extension at most this long after Fill in <site> was pressed, unless
|
|
4
|
+
// its site has no scheduler and it waits for its release time (`due.ts`).
|
|
5
|
+
export const waitingMs = 24 * 60 * 60 * 1000;
|
|
6
|
+
// Posts left waiting longer than a day stop waiting, with the sentence that says why.
|
|
7
|
+
export function expireWaiting(db, now, waitsForRelease) {
|
|
8
|
+
for (const post of postsIn(db, ["waiting"])) {
|
|
9
|
+
if (waitsForRelease(post))
|
|
10
|
+
continue;
|
|
11
|
+
const age = now.getTime() - Date.parse(post.waitingAt ?? "");
|
|
12
|
+
if (Number.isFinite(age) && age <= waitingMs)
|
|
13
|
+
continue;
|
|
14
|
+
const site = platformLabels[post.platform];
|
|
15
|
+
writeProgress(db, post, {
|
|
16
|
+
...post,
|
|
17
|
+
state: "failed",
|
|
18
|
+
message: `Fill in ${site} waited a day without the Slopify Studio extension taking it. Open ${site} in the browser where the extension is paired and allowed to post there, then press Fill in ${site} again.`,
|
|
19
|
+
}, now);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
// The oldest waiting post for a site that can be filled now (of `projectId` when the tab Slopify
|
|
23
|
+
// opened names one); it becomes "filling" so another tab of the site doesn't take it too.
|
|
24
|
+
export function takeNext(db, platform, now, readyNow, projectId) {
|
|
25
|
+
const waiting = postsIn(db, ["waiting"], platform).filter((post) => (projectId === undefined || post.projectId === projectId) && readyNow(post));
|
|
26
|
+
const [first] = waiting;
|
|
27
|
+
if (first === undefined)
|
|
28
|
+
return undefined;
|
|
29
|
+
writeProgress(db, first, { ...first, state: "filling", message: null }, now);
|
|
30
|
+
return { post: first, waiting: waiting.length - 1 };
|
|
31
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { postStates, socialPlatforms, } from "./model.js";
|
|
2
|
+
import { projectPostChoiceSchema, redditFieldsSchema } from "./schema.js";
|
|
3
|
+
import { noProgress } from "./state.js";
|
|
4
|
+
const columns = "project_id,platform,item,choice_json,text_edit,text_base,reddit_json,state,waiting_at,filled_at,scheduled_at,posted_at,url,message,by,updated_at";
|
|
5
|
+
const text = (value) => (typeof value === "string" ? value : null);
|
|
6
|
+
function json(value, parse) {
|
|
7
|
+
if (typeof value !== "string")
|
|
8
|
+
return null;
|
|
9
|
+
try {
|
|
10
|
+
return parse(JSON.parse(value)) ?? null;
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function toRecord(row) {
|
|
17
|
+
const platform = socialPlatforms.find((one) => one === row.platform);
|
|
18
|
+
const state = postStates.find((one) => one === row.state) ?? "none";
|
|
19
|
+
if (platform === undefined || typeof row.project_id !== "string")
|
|
20
|
+
return undefined;
|
|
21
|
+
const by = row.by === "extension" || row.by === "person" ? row.by : null;
|
|
22
|
+
return {
|
|
23
|
+
projectId: row.project_id,
|
|
24
|
+
platform,
|
|
25
|
+
item: typeof row.item === "number" ? row.item : 0,
|
|
26
|
+
choice: json(row.choice_json, (raw) => {
|
|
27
|
+
const parsed = projectPostChoiceSchema.safeParse(raw);
|
|
28
|
+
return parsed.success ? parsed.data : undefined;
|
|
29
|
+
}),
|
|
30
|
+
textEdit: text(row.text_edit),
|
|
31
|
+
textBase: text(row.text_base),
|
|
32
|
+
reddit: json(row.reddit_json, (raw) => {
|
|
33
|
+
const parsed = redditFieldsSchema.safeParse(raw);
|
|
34
|
+
return parsed.success ? parsed.data : undefined;
|
|
35
|
+
}),
|
|
36
|
+
state,
|
|
37
|
+
waitingAt: text(row.waiting_at),
|
|
38
|
+
filledAt: text(row.filled_at),
|
|
39
|
+
scheduledAt: text(row.scheduled_at),
|
|
40
|
+
postedAt: text(row.posted_at),
|
|
41
|
+
url: text(row.url),
|
|
42
|
+
message: text(row.message),
|
|
43
|
+
by,
|
|
44
|
+
updatedAt: text(row.updated_at),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function blank(key) {
|
|
48
|
+
return {
|
|
49
|
+
...noProgress,
|
|
50
|
+
projectId: key.projectId,
|
|
51
|
+
platform: key.platform,
|
|
52
|
+
item: key.item ?? 0,
|
|
53
|
+
choice: null,
|
|
54
|
+
textEdit: null,
|
|
55
|
+
textBase: null,
|
|
56
|
+
reddit: null,
|
|
57
|
+
updatedAt: null,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
export function readPost(db, key) {
|
|
61
|
+
const row = db
|
|
62
|
+
.prepare(`SELECT ${columns} FROM social_posts WHERE project_id=? AND platform=? AND item=?`)
|
|
63
|
+
.get(key.projectId, key.platform, key.item ?? 0);
|
|
64
|
+
return (row === undefined ? undefined : toRecord(row)) ?? blank(key);
|
|
65
|
+
}
|
|
66
|
+
export function postsOf(db, projectId) {
|
|
67
|
+
return db
|
|
68
|
+
.prepare(`SELECT ${columns} FROM social_posts WHERE project_id=? ORDER BY platform, item`)
|
|
69
|
+
.all(projectId)
|
|
70
|
+
.flatMap((row) => toRecord(row) ?? []);
|
|
71
|
+
}
|
|
72
|
+
// Every post in one of these states, oldest first (by when it was asked to fill).
|
|
73
|
+
export function postsIn(db, states, platform) {
|
|
74
|
+
const marks = states.map(() => "?").join(",");
|
|
75
|
+
const params = [...states];
|
|
76
|
+
let where = `state IN (${marks})`;
|
|
77
|
+
if (platform !== undefined) {
|
|
78
|
+
where += " AND platform=?";
|
|
79
|
+
params.push(platform);
|
|
80
|
+
}
|
|
81
|
+
return db
|
|
82
|
+
.prepare(`SELECT ${columns} FROM social_posts WHERE ${where} ORDER BY coalesce(waiting_at, updated_at), rowid`)
|
|
83
|
+
.all(...params)
|
|
84
|
+
.flatMap((row) => toRecord(row) ?? []);
|
|
85
|
+
}
|
|
86
|
+
const writable = [
|
|
87
|
+
"choice_json",
|
|
88
|
+
"text_edit",
|
|
89
|
+
"text_base",
|
|
90
|
+
"reddit_json",
|
|
91
|
+
"state",
|
|
92
|
+
"waiting_at",
|
|
93
|
+
"filled_at",
|
|
94
|
+
"scheduled_at",
|
|
95
|
+
"posted_at",
|
|
96
|
+
"url",
|
|
97
|
+
"message",
|
|
98
|
+
"by",
|
|
99
|
+
];
|
|
100
|
+
function upsert(db, key, fields, now) {
|
|
101
|
+
const names = writable.filter((name) => Object.hasOwn(fields, name));
|
|
102
|
+
const values = names.map((name) => fields[name] ?? null);
|
|
103
|
+
const updates = [
|
|
104
|
+
...names.map((name) => `${name}=excluded.${name}`),
|
|
105
|
+
"updated_at=excluded.updated_at",
|
|
106
|
+
];
|
|
107
|
+
db.prepare(`INSERT INTO social_posts (project_id,platform,item,${[...names, "updated_at"].join(",")})
|
|
108
|
+
VALUES (?,?,?,${[...names, "updated_at"].map(() => "?").join(",")})
|
|
109
|
+
ON CONFLICT(project_id,platform,item) DO UPDATE SET ${updates.join(",")}`).run(key.projectId, key.platform, key.item ?? 0, ...values, now.toISOString());
|
|
110
|
+
}
|
|
111
|
+
export function writeChoice(db, key, choice, now) {
|
|
112
|
+
upsert(db, key, { choice_json: choice === null ? null : JSON.stringify(choice) }, now);
|
|
113
|
+
}
|
|
114
|
+
// The person's text and the generated text it was edited from; null goes back to the
|
|
115
|
+
// generated text.
|
|
116
|
+
export function writeTextEdit(db, key, edit, now) {
|
|
117
|
+
upsert(db, key, { text_edit: edit?.text ?? null, text_base: edit?.base ?? null }, now);
|
|
118
|
+
}
|
|
119
|
+
export function writeReddit(db, key, fields, now) {
|
|
120
|
+
upsert(db, key, { reddit_json: JSON.stringify(fields) }, now);
|
|
121
|
+
}
|
|
122
|
+
export function writeProgress(db, key, progress, now) {
|
|
123
|
+
upsert(db, key, {
|
|
124
|
+
state: progress.state,
|
|
125
|
+
waiting_at: progress.waitingAt,
|
|
126
|
+
filled_at: progress.filledAt,
|
|
127
|
+
scheduled_at: progress.scheduledAt,
|
|
128
|
+
posted_at: progress.postedAt,
|
|
129
|
+
url: progress.url,
|
|
130
|
+
message: progress.message,
|
|
131
|
+
by: progress.by,
|
|
132
|
+
}, now);
|
|
133
|
+
}
|