@gentbajko/slopify 4.0.0 → 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 +1 -1
- package/dist/adapters/alignment/audio.js +1 -1
- package/dist/adapters/alignment/cache.js +1 -1
- package/dist/adapters/alignment/multilingual.js +2 -1
- package/dist/adapters/alignment/runner.js +1 -1
- package/dist/adapters/alignment/worker.js +1 -1
- package/dist/adapters/explain.js +10 -9
- package/dist/adapters/host-cli/index.js +2 -1
- package/dist/adapters/host-cli/transport.js +2 -1
- package/dist/adapters/image/bytes.js +2 -1
- package/dist/adapters/image/codex.js +6 -5
- package/dist/adapters/image/fal.js +4 -3
- package/dist/adapters/image/google.js +3 -2
- package/dist/adapters/image/models.js +2 -2
- package/dist/adapters/image/openai.js +2 -1
- package/dist/adapters/image/replicate.js +3 -2
- package/dist/adapters/image/video.js +2 -1
- package/dist/adapters/key-probes.js +1 -1
- package/dist/adapters/llm/antigravity-models.js +1 -1
- package/dist/adapters/llm/antigravity.js +7 -6
- package/dist/adapters/llm/assistant-cli.js +1 -1
- package/dist/adapters/llm/claude-code-models.js +1 -1
- package/dist/adapters/llm/claude-code.js +3 -2
- package/dist/adapters/llm/codex-models.js +1 -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 +2 -1
- package/dist/adapters/llm/run-cli.js +3 -2
- package/dist/adapters/tts/gemini.js +2 -1
- package/dist/adapters/tts/inworld-async.js +2 -1
- package/dist/adapters/tts/inworld.js +3 -2
- package/dist/adapters/tts/system.js +2 -1
- package/dist/catalog/registry.js +20 -13
- package/dist/catalog/schema.js +2 -1
- package/dist/catalog/validate.js +5 -2
- package/dist/edge/http/actions.js +7 -7
- package/dist/edge/http/app.js +3 -3
- package/dist/edge/http/assistant.js +3 -1
- package/dist/edge/http/backups.js +1 -1
- package/dist/edge/http/channel-memory.js +2 -2
- package/dist/edge/http/channels.js +1 -1
- package/dist/edge/http/checkpoints.js +7 -5
- package/dist/edge/http/design-templates.js +3 -3
- package/dist/edge/http/designs.js +1 -1
- package/dist/edge/http/document-themes.js +3 -3
- package/dist/edge/http/draft-problem.js +1 -1
- package/dist/edge/http/extension-auth.js +1 -1
- package/dist/edge/http/home.js +9 -1
- package/dist/edge/http/model-checks.js +4 -4
- package/dist/edge/http/onboarding.js +29 -12
- package/dist/edge/http/outputs.js +3 -2
- package/dist/edge/http/patch-notes.js +2 -2
- package/dist/edge/http/planning.js +1 -1
- package/dist/edge/http/project-create.js +1 -1
- package/dist/edge/http/project-templates.js +2 -2
- package/dist/edge/http/projects.js +22 -4
- package/dist/edge/http/prompts.js +2 -2
- package/dist/edge/http/providers.js +9 -10
- package/dist/edge/http/refusal.js +1 -1
- package/dist/edge/http/remote-access.js +4 -4
- package/dist/edge/http/remote-guard.js +2 -2
- package/dist/edge/http/reviews.js +1 -1
- package/dist/edge/http/revision-files.js +1 -1
- package/dist/edge/http/revisions.js +4 -4
- package/dist/edge/http/schedules.js +13 -11
- package/dist/edge/http/settings.js +6 -2
- package/dist/edge/http/social.js +5 -4
- package/dist/edge/http/staging.js +1 -1
- package/dist/edge/http/storage.js +12 -4
- package/dist/edge/http/studio.js +71 -19
- package/dist/edge/http/style-preview.js +2 -2
- package/dist/edge/http/template-studio.js +12 -9
- package/dist/edge/http/trash.js +1 -1
- package/dist/edge/http/youtube-edits.js +1 -1
- package/dist/edge/mcp/excluded-routes.js +4 -3
- package/dist/edge/mcp/instructions.js +3 -3
- package/dist/edge/mcp/library-tools.js +8 -5
- package/dist/edge/mcp/remake-tools.js +7 -4
- package/dist/edge/mcp/server.js +1 -1
- package/dist/edge/mcp/tools-channels.js +9 -9
- package/dist/edge/mcp/tools-designs.js +9 -9
- package/dist/edge/mcp/tools-library.js +7 -7
- package/dist/edge/mcp/tools-planning.js +18 -18
- package/dist/edge/mcp/tools-projects.js +10 -10
- package/dist/edge/mcp/tools-publishing.js +9 -9
- package/dist/edge/mcp/tools-runs.js +9 -9
- package/dist/edge/mcp/tools-schedules.js +6 -6
- package/dist/edge/mcp/tools-setup.js +20 -14
- package/dist/edge/mcp/tools-social.js +5 -4
- package/dist/edge/mcp/tools-storage.js +3 -3
- package/dist/edge/mcp/tools-template-studio.js +18 -18
- package/dist/edge/remote-access/model.js +1 -1
- package/dist/extension/slopify-studio-chrome.zip +0 -0
- package/dist/extension/slopify-studio-firefox.zip +0 -0
- package/dist/kernel/db/migrations/0078-uploaded-from-links.sql +7 -0
- package/dist/kernel/pipeline.js +12 -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 +11 -3
- package/dist/kernel/runner/providers.js +3 -2
- package/dist/kernel/runner/standalone.js +2 -2
- package/dist/main.js +5 -3
- package/dist/model-catalog.js +20 -0
- package/dist/patch-notes/4.0.0.md +13 -13
- package/dist/patch-notes/4.1.0.md +42 -0
- package/dist/patch-notes/index.json +6 -0
- package/dist/slices/admission/listing-facts.js +95 -0
- package/dist/slices/admission/repo.js +9 -6
- package/dist/slices/admission/rules.js +7 -7
- package/dist/slices/admission/state-words.js +34 -5
- package/dist/slices/assistant/cost.js +1 -1
- package/dist/slices/assistant/model.js +10 -3
- package/dist/slices/assistant/turn.js +5 -3
- package/dist/slices/autopilot/warnings.js +1 -1
- package/dist/slices/backups/service.js +5 -5
- package/dist/slices/channels/cast-images.js +1 -1
- package/dist/slices/channels/schema.js +1 -1
- package/dist/slices/channels/videos.js +1 -1
- package/dist/slices/control/providers.js +2 -2
- package/dist/slices/designs/budget.js +1 -1
- package/dist/slices/designs/calls.js +2 -2
- package/dist/slices/designs/check-context.js +4 -4
- package/dist/slices/designs/fields.js +4 -4
- package/dist/slices/designs/gate.js +7 -5
- package/dist/slices/designs/render-input.js +1 -1
- package/dist/slices/designs/requests.js +1 -1
- package/dist/slices/designs/runner.js +1 -1
- package/dist/slices/designs/sample.js +2 -1
- package/dist/slices/designs/service.js +1 -1
- package/dist/slices/designs/template-checks.js +2 -2
- package/dist/slices/designs/template-make.js +3 -3
- package/dist/slices/designs/templates.js +3 -3
- package/dist/slices/document/fonts.js +1 -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 +2 -2
- package/dist/slices/estimate/index.js +28 -22
- package/dist/slices/estimate/requests.js +4 -4
- package/dist/slices/fixes/rules.js +3 -1
- package/dist/slices/fonts/catalog.js +2 -1
- package/dist/slices/fonts/coverage.js +2 -1
- package/dist/slices/images/appearance.js +2 -1
- package/dist/slices/images/scenes.js +2 -1
- package/dist/slices/keywords/digest.js +2 -1
- package/dist/slices/keywords/rules.js +2 -2
- package/dist/slices/keywords/specs.js +5 -5
- package/dist/slices/keywords/values.js +1 -1
- package/dist/slices/library/transfer.js +3 -3
- package/dist/slices/loudness/loudnorm.js +2 -1
- package/dist/slices/loudness/model.js +3 -2
- package/dist/slices/loudness/sentence-level.js +2 -1
- package/dist/slices/model-checks/rules.js +13 -0
- package/dist/slices/model-upkeep/model.js +1 -1
- package/dist/slices/model-upkeep/switch.js +3 -3
- package/dist/slices/model-upkeep/usage.js +13 -2
- package/dist/slices/narration/breaths.js +2 -1
- package/dist/slices/narration/clean-pieces.js +2 -1
- package/dist/slices/narration/pauses-model.js +3 -2
- package/dist/slices/narration/pronunciation.js +3 -2
- package/dist/slices/notifications/subject.js +2 -2
- package/dist/slices/onboarding/copy.js +19 -2
- package/dist/slices/onboarding/first-run.js +13 -38
- package/dist/slices/onboarding/install.js +2 -1
- package/dist/slices/onboarding/model.js +26 -21
- package/dist/slices/onboarding/preferred-model.js +14 -0
- package/dist/slices/onboarding/quick-short.js +103 -64
- package/dist/slices/onboarding/sample.js +4 -1
- package/dist/slices/onboarding/state.js +7 -0
- package/dist/slices/onboarding/template.js +6 -5
- package/dist/slices/outputs/add.js +3 -2
- package/dist/slices/outputs/service.js +2 -2
- package/dist/slices/patch-notes/library.js +1 -1
- package/dist/slices/play-drafts/readiness.js +3 -3
- package/dist/slices/play-drafts/review-inputs.js +1 -1
- package/dist/slices/play-drafts/service.js +17 -7
- 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/setup-sections.js +26 -0
- package/dist/slices/project-templates/setup.js +6 -1
- package/dist/slices/rebuild/preview-details.js +5 -5
- package/dist/slices/rebuild/recipe-preparation.js +1 -1
- package/dist/slices/rebuild/recipe-save.js +2 -2
- package/dist/slices/rebuild/recipe-validation.js +5 -4
- package/dist/slices/rebuild/recipe-voices.js +3 -2
- package/dist/slices/rebuild/recipe-work.js +4 -4
- package/dist/slices/rebuild/recovery.js +6 -6
- package/dist/slices/rebuild/remake-outdated.js +5 -5
- package/dist/slices/rebuild/review-redo.js +1 -1
- package/dist/slices/rebuild/runtime-animate.js +2 -2
- package/dist/slices/rebuild/runtime-article.js +4 -3
- package/dist/slices/rebuild/runtime-document.js +5 -4
- package/dist/slices/rebuild/runtime-export-bed.js +2 -1
- package/dist/slices/rebuild/runtime-export-edit.js +2 -1
- package/dist/slices/rebuild/runtime-export-inputs.js +5 -4
- package/dist/slices/rebuild/runtime-export-short.js +1 -1
- package/dist/slices/rebuild/runtime-export.js +4 -3
- package/dist/slices/rebuild/runtime-image.js +4 -3
- package/dist/slices/rebuild/runtime-local.js +3 -2
- package/dist/slices/rebuild/runtime-narration-text.js +3 -2
- package/dist/slices/rebuild/runtime-provider.js +7 -6
- package/dist/slices/rebuild/runtime-review.js +6 -5
- package/dist/slices/rebuild/runtime-shorts.js +6 -5
- package/dist/slices/rebuild/runtime-store.js +17 -2
- package/dist/slices/rebuild/runtime-subtitles.js +13 -12
- package/dist/slices/rebuild/runtime-voices.js +2 -1
- package/dist/slices/rebuild/runtime-youtube.js +3 -2
- package/dist/slices/rebuild/service-readiness.js +11 -3
- package/dist/slices/research/synthesis.js +3 -2
- package/dist/slices/reviews/external-model.js +1 -1
- package/dist/slices/reviews/holds.js +5 -4
- package/dist/slices/reviews/model.js +8 -5
- package/dist/slices/reviews/rules.js +3 -3
- package/dist/slices/reviews/verdict.js +2 -1
- package/dist/slices/revisions/image-plan.js +1 -1
- package/dist/slices/revisions/mutation-assets.js +2 -1
- package/dist/slices/revisions/mutations.js +3 -2
- package/dist/slices/schedules/agenda.js +7 -5
- package/dist/slices/schedules/design-check.js +4 -4
- package/dist/slices/schedules/design-run.js +3 -3
- package/dist/slices/schedules/repo.js +6 -5
- package/dist/slices/schedules/schema.js +3 -0
- package/dist/slices/schedules/topics.js +5 -5
- package/dist/slices/settings/first-run.js +6 -32
- package/dist/slices/settings/health.js +30 -15
- package/dist/slices/settings/key-test.js +1 -1
- package/dist/slices/settings/model.js +4 -2
- package/dist/slices/settings/playback.js +2 -2
- package/dist/slices/shorts/keep.js +2 -1
- package/dist/slices/shorts/pick.js +4 -3
- package/dist/slices/shorts/prompts.js +2 -1
- package/dist/slices/shorts/render.js +3 -2
- package/dist/slices/social/media.js +5 -4
- package/dist/slices/social/pack.js +1 -1
- package/dist/slices/social/post-prompt.js +2 -1
- package/dist/slices/social/reshape.js +2 -2
- package/dist/slices/social/serve.js +1 -1
- package/dist/slices/social/service.js +3 -2
- package/dist/slices/social/state.js +1 -1
- package/dist/slices/storage/backup-import.js +2 -2
- package/dist/slices/storage/portable.js +1 -1
- package/dist/slices/storage/trim.js +23 -13
- package/dist/slices/studio/calendar.js +3 -1
- 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/plan.js +1 -1
- package/dist/slices/studio/settings.js +6 -6
- package/dist/slices/studio/videos.js +23 -4
- package/dist/slices/style-preview/render.js +3 -3
- package/dist/slices/style-preview/schema.js +1 -1
- package/dist/slices/subtitles/captions.js +1 -1
- package/dist/slices/template-studio/anatomy-view.js +46 -3
- package/dist/slices/template-studio/bakeoff.js +3 -3
- package/dist/slices/template-studio/budget.js +6 -3
- package/dist/slices/template-studio/cards.js +78 -0
- package/dist/slices/template-studio/checks.js +99 -50
- package/dist/slices/template-studio/for-all.js +5 -5
- package/dist/slices/template-studio/free-video.js +1 -1
- package/dist/slices/template-studio/free.js +5 -5
- package/dist/slices/template-studio/model.js +19 -15
- package/dist/slices/template-studio/pack-import.js +14 -14
- package/dist/slices/template-studio/pack.js +9 -7
- package/dist/slices/template-studio/paid.js +12 -5
- package/dist/slices/template-studio/quote.js +18 -11
- package/dist/slices/template-studio/ready.js +7 -7
- package/dist/slices/template-studio/runner.js +1 -1
- package/dist/slices/template-studio/save.js +12 -12
- package/dist/slices/template-studio/say-it-as.js +3 -3
- package/dist/slices/template-studio/schedule-gate.js +1 -1
- package/dist/slices/template-studio/schedule-price.js +3 -1
- package/dist/slices/template-studio/schedule-sample.js +5 -1
- package/dist/slices/template-studio/schema.js +25 -0
- package/dist/slices/template-studio/service.js +3 -1
- package/dist/slices/template-studio/status.js +26 -9
- package/dist/slices/template-studio/topic.js +3 -3
- package/dist/slices/thumbnail/by-llm.js +1 -1
- package/dist/slices/uploads/repo.js +37 -1
- package/dist/slices/uploads/rules.js +3 -2
- package/dist/slices/video/edit-list.js +2 -2
- package/dist/slices/video/edit-settings.js +1 -1
- package/dist/slices/video/encoding.js +14 -5
- package/dist/slices/video/ffmpeg.js +2 -2
- package/dist/slices/video/plan.js +2 -1
- package/dist/slices/video/slideshow.js +1 -1
- package/dist/slices/voices/attribution.js +3 -2
- package/dist/slices/voices/model.js +9 -8
- package/dist/slices/voices/script.js +2 -1
- package/dist/slices/youtube/answer.js +2 -1
- package/dist/slices/youtube/edits-repo.js +1 -1
- package/dist/tutorials/AI-CLIs.md +25 -25
- package/dist/tutorials/Agents-MCP.md +32 -32
- package/dist/tutorials/Assistant.md +5 -5
- 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 +40 -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 +101 -76
- package/dist/tutorials/Home.md +33 -33
- package/dist/tutorials/Install.md +13 -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 +21 -21
- package/dist/tutorials/Models.md +27 -27
- package/dist/tutorials/Multiple-Voices.md +41 -40
- package/dist/tutorials/Narration-Aliases-and-Glossary.md +14 -14
- package/dist/tutorials/Notifications.md +21 -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 +12 -12
- package/dist/tutorials/Project-Page.md +118 -120
- package/dist/tutorials/Prompts-and-Templates.md +84 -0
- package/dist/tutorials/Prompts.md +45 -45
- 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 +2 -2
- package/dist/tutorials/Reviews-and-Checkpoints.md +90 -93
- package/dist/tutorials/Schedules.md +27 -28
- package/dist/tutorials/Settings-Reference.md +182 -204
- 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 +45 -42
- package/dist/tutorials/Templates.md +50 -79
- 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 +15 -15
- 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 +2 -2
- package/dist/tutorials/Library-Overview.md +0 -76
- package/dist/tutorials/Play-Images.md +0 -165
- 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-CkAI_2E7.css +0 -1
- package/dist/web/assets/index-DcD0cdfo.js +0 -334
- package/dist/web/assets/pdf-TODRwF31.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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { jsonBetween } from "../../kernel/json-answer.js";
|
|
3
|
+
import { editSettings } from "../../kernel/ports/settings-places.js";
|
|
3
4
|
export function imagePromptMessages(brief) {
|
|
4
5
|
const plural = brief.count === 1 ? "prompt" : "prompts";
|
|
5
6
|
return [
|
|
@@ -32,7 +33,7 @@ export function imagePromptMessages(brief) {
|
|
|
32
33
|
},
|
|
33
34
|
];
|
|
34
35
|
}
|
|
35
|
-
const fix =
|
|
36
|
+
const fix = `Try again, or choose another model in ${editSettings.writer}.`;
|
|
36
37
|
// The reason is the sentence the stage shows; the provider wrapper's `check` asks the model
|
|
37
38
|
// again while attempts remain.
|
|
38
39
|
export function checkImagePrompts(text, count) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { copyFileSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import { editSettings } from "../../kernel/ports/settings-places.js";
|
|
3
4
|
import { editListVersion, } from "../video/edit-list.js";
|
|
4
5
|
import { runFfmpeg } from "../video/ffmpeg.js";
|
|
5
6
|
import { motionFor } from "../video/motion.js";
|
|
@@ -46,7 +47,7 @@ export function shortAudioArgs(timeline, start, end, output, options = {}) {
|
|
|
46
47
|
count += 1;
|
|
47
48
|
}
|
|
48
49
|
if (count === 0 || first === undefined)
|
|
49
|
-
throw new Error(
|
|
50
|
+
throw new Error(`A short's clip falls outside the narration, so it has no sound to cut. Use Pick different moments in ${editSettings.shorts}, then Try again.`);
|
|
50
51
|
const joined = Array.from({ length: count }, (_value, at) => `[a${String(at)}]`).join("");
|
|
51
52
|
chains.push(`${joined}concat=n=${String(count)}:v=0:a=1[joined]`);
|
|
52
53
|
const speed = options.speed ?? 1;
|
|
@@ -89,7 +90,7 @@ export function shortAudioArgs(timeline, start, end, output, options = {}) {
|
|
|
89
90
|
// moving as the video's shots move by their place.
|
|
90
91
|
export function shortEditList(input) {
|
|
91
92
|
if (input.images.length === 0)
|
|
92
|
-
throw new Error(
|
|
93
|
+
throw new Error(`A short has no images to show. Use Make this short again in ${editSettings.shorts}, then Try again.`);
|
|
93
94
|
const total = Math.max(1, Math.round(input.seconds * fps));
|
|
94
95
|
const each = Math.max(1, Math.round(input.imageSeconds * fps));
|
|
95
96
|
const figures = figureFrames(input.figures ?? [], total);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Which files a post carries, in the shape its site wants them. Pure: the sources come from
|
|
2
2
|
// the project's outputs (`sources.ts`); a reshaped picture or a design's render is made the
|
|
3
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";
|
|
4
5
|
import { limitsFor } from "./limits.js";
|
|
5
6
|
import { platformLabels, } from "./model.js";
|
|
6
7
|
const shapeSlugs = {
|
|
@@ -65,7 +66,7 @@ export function mediaFor(platform, choice, sources) {
|
|
|
65
66
|
const shape = limitsFor(platform).pictureShape;
|
|
66
67
|
if (choice.video === "long") {
|
|
67
68
|
if (sources.long === null)
|
|
68
|
-
missing("The video isn't made yet. Let the Video stage finish (or use Continue the run on the project page), then open
|
|
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.");
|
|
69
70
|
else
|
|
70
71
|
media.push({
|
|
71
72
|
kind: "video",
|
|
@@ -83,8 +84,8 @@ export function mediaFor(platform, choice, sources) {
|
|
|
83
84
|
const short = sources.shorts.find((one) => one.number === number);
|
|
84
85
|
if (short?.file == null)
|
|
85
86
|
missing(short === undefined
|
|
86
|
-
? `This project has no short ${String(number)}. Pick another short for ${site} in the post's media, or turn on Shorts in
|
|
87
|
-
: `Short ${String(number)} isn't rendered yet. Let the Video stage finish, then open
|
|
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.`);
|
|
88
89
|
else
|
|
89
90
|
media.push({
|
|
90
91
|
kind: "short",
|
|
@@ -99,7 +100,7 @@ export function mediaFor(platform, choice, sources) {
|
|
|
99
100
|
}
|
|
100
101
|
if (choice.thumbnail === true) {
|
|
101
102
|
if (sources.thumbnail === null)
|
|
102
|
-
missing(
|
|
103
|
+
missing(`The thumbnail isn't made yet. Let the Thumbnail stage finish, or set Thumbnail source in ${editSettings.extras}.`);
|
|
103
104
|
else
|
|
104
105
|
media.push(still(sources, "thumbnail", sources.thumbnail, shape));
|
|
105
106
|
}
|
|
@@ -94,7 +94,7 @@ function buildPack(sources, platform, record, now) {
|
|
|
94
94
|
if (choice.link && link === null)
|
|
95
95
|
warnings.push({
|
|
96
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 (
|
|
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
98
|
});
|
|
99
99
|
const hashtags = hashtagsFor([sources.tags, sources.shortHashtags], Math.min(choice.hashtags, limits.hashtags));
|
|
100
100
|
const fitted = fitText({ body, link, hashtags }, limits, site);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// The request that writes a post for another site with a Library prompt of kind "post" (or the
|
|
2
2
|
// built-in one), and the check its answer must pass. Pure.
|
|
3
|
+
import { editSettings } from "../../kernel/ports/settings-places.js";
|
|
3
4
|
import { limitsFor } from "./limits.js";
|
|
4
5
|
import { platformLabels } from "./model.js";
|
|
5
6
|
import { countFor } from "./text.js";
|
|
@@ -45,7 +46,7 @@ export function postText(answer) {
|
|
|
45
46
|
export function checkPost(answer, target) {
|
|
46
47
|
const site = platformLabels[target.platform];
|
|
47
48
|
const text = postText(answer);
|
|
48
|
-
const fix =
|
|
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}.`;
|
|
49
50
|
if (text === "")
|
|
50
51
|
return `The AI model sent back an empty ${site} post. ${fix}`;
|
|
51
52
|
const used = countFor(text, target.unit);
|
|
@@ -46,7 +46,7 @@ export function createReshaper(deps) {
|
|
|
46
46
|
if (stat === undefined || !stat.isFile())
|
|
47
47
|
return {
|
|
48
48
|
ok: false,
|
|
49
|
-
sentence: "The picture isn't on disk any more.
|
|
49
|
+
sentence: "The picture isn't on disk any more. Close Copy for YouTube Studio and open it again; if it is still missing, make the project's images again.",
|
|
50
50
|
};
|
|
51
51
|
const key = createHash("sha256")
|
|
52
52
|
.update(JSON.stringify([reshapeVersion, source, stat.size, stat.mtimeMs, shape]))
|
|
@@ -82,7 +82,7 @@ export function createReshaper(deps) {
|
|
|
82
82
|
});
|
|
83
83
|
return {
|
|
84
84
|
ok: false,
|
|
85
|
-
sentence: `The picture couldn't be cut to ${shape} because ffmpeg stopped with an error.
|
|
85
|
+
sentence: `The picture couldn't be cut to ${shape} because ffmpeg stopped with an error. Close Copy for YouTube Studio and open it again to try again; if it fails again, use Download diagnostics in Settings and report it.`,
|
|
86
86
|
};
|
|
87
87
|
}
|
|
88
88
|
finally {
|
|
@@ -2,7 +2,7 @@ import { statSync } from "node:fs";
|
|
|
2
2
|
import { findDownload } from "../storage/downloads.js";
|
|
3
3
|
import { parseMediaFile } from "./media.js";
|
|
4
4
|
import { postPacks } from "./pack.js";
|
|
5
|
-
const gone = "That file isn't in the project's posts any more.
|
|
5
|
+
const gone = "That file isn't in the project's posts any more. Close Copy for YouTube Studio, open it again and press Fill again.";
|
|
6
6
|
// One file of a project's posts, only when one of its posts lists it: the asset as it is, a
|
|
7
7
|
// picture cut to its site's shape (made now the first time), or a design drawn in its format.
|
|
8
8
|
export async function serveMedia(deps, projectId, file, now, signal) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { projectById } from "../admission/repo.js";
|
|
2
2
|
import { projectChannelId } from "../channels/repo.js";
|
|
3
|
+
import { setupSectionNames } from "../project-templates/setup-sections.js";
|
|
3
4
|
import { channelSocial } from "./channel.js";
|
|
4
5
|
import { dueAt, readyNow, tooLate, waitsForRelease } from "./due.js";
|
|
5
6
|
import { profileUrl } from "./limits.js";
|
|
@@ -9,10 +10,10 @@ import { packFor } from "./pack.js";
|
|
|
9
10
|
import { expireWaiting, takeNext } from "./queue.js";
|
|
10
11
|
import { postsIn, readPost, writeProgress } from "./repo.js";
|
|
11
12
|
import { askFill, pasteLink, reportFill } from "./state.js";
|
|
12
|
-
//
|
|
13
|
+
// A project's settings have no posting editor: its sites come from its template (or Create).
|
|
13
14
|
export function notPostingSentence(platform) {
|
|
14
15
|
const site = platformLabels[platform];
|
|
15
|
-
return `This project doesn't post to ${site}. A project's sites come from its template:
|
|
16
|
+
return `This project doesn't post to ${site}. A project's sites come from its template: tick ${site} under Post to in Edit template → ${setupSectionNames.youtube}, or under Post to other sites in Create, for the projects it makes next.`;
|
|
16
17
|
}
|
|
17
18
|
const notPosting = (platform) => ({
|
|
18
19
|
ok: false,
|
|
@@ -31,7 +31,7 @@ export function reportFill(current, platform, report, now) {
|
|
|
31
31
|
return {
|
|
32
32
|
ok: false,
|
|
33
33
|
reason: "not-asked",
|
|
34
|
-
sentence: `Nothing was waiting to be filled in ${site} for this project. In Slopify, open the project, press
|
|
34
|
+
sentence: `Nothing was waiting to be filled in ${site} for this project. In Slopify, open the project, press Copy for YouTube Studio, then Fill in ${site}.`,
|
|
35
35
|
};
|
|
36
36
|
const url = report.url === undefined || report.url === "" ? null : report.url;
|
|
37
37
|
if (url !== null && !linkPatterns[platform].test(url))
|
|
@@ -436,7 +436,7 @@ function checkScratch(deps, scratch, library, parts) {
|
|
|
436
436
|
}
|
|
437
437
|
}
|
|
438
438
|
catch {
|
|
439
|
-
throw unreadable("a PDF
|
|
439
|
+
throw unreadable("a PDF look");
|
|
440
440
|
}
|
|
441
441
|
try {
|
|
442
442
|
for (const row of rowsOf(scratch, "SELECT id FROM channels")) {
|
|
@@ -465,7 +465,7 @@ function checkScratch(deps, scratch, library, parts) {
|
|
|
465
465
|
throw new Error("staged files");
|
|
466
466
|
}
|
|
467
467
|
catch {
|
|
468
|
-
throw unreadable("a
|
|
468
|
+
throw unreadable("a draft on Create");
|
|
469
469
|
}
|
|
470
470
|
try {
|
|
471
471
|
allTelemetryEvents(scratch);
|
|
@@ -362,7 +362,7 @@ const storedAppearance = z.enum(appearances);
|
|
|
362
362
|
const storedLoudness = z
|
|
363
363
|
.object({ enabled: z.boolean(), videoLufs: z.number(), audioFilesLufs: z.number() })
|
|
364
364
|
.strict();
|
|
365
|
-
// Settings →
|
|
365
|
+
// Settings → YouTube Studio and posting, and the providers a fresh Play form starts with.
|
|
366
366
|
// The AI use marks: ticked voices, Image prompts and projects, by id.
|
|
367
367
|
const storedIds = z.array(z.string().max(200)).max(100_000);
|
|
368
368
|
// The channel page's "Keep only titles containing…" text, one per channel.
|
|
@@ -2,6 +2,7 @@ import { existsSync, readdirSync, rmdirSync, statSync, unlinkSync } from "node:f
|
|
|
2
2
|
import { join, relative, sep } from "node:path";
|
|
3
3
|
import { derive } from "../../kernel/runner/graph.js";
|
|
4
4
|
import { liveProject, projectExists, projectPaused, stagesOf } from "../admission/repo.js";
|
|
5
|
+
import { bundledSampleProjects } from "../onboarding/state.js";
|
|
5
6
|
import { notTemplateSample } from "../template-studio/repo.js";
|
|
6
7
|
import { projectSettled } from "./arrange.js";
|
|
7
8
|
import { backupsFolderName, outputPath, projectDir } from "./layout.js";
|
|
@@ -11,7 +12,9 @@ import { pieceFile } from "./reconcile.js";
|
|
|
11
12
|
// video and shorts show included: making them again costs money and never gives the same ones. Two
|
|
12
13
|
// rules keep it safe. A file the user supplied is never removed, because nothing could make
|
|
13
14
|
// it again. And only files a project record names are removed: anything else in the folder
|
|
14
|
-
// is reconcile's business (`reconcile.ts`), which leaves the Backups folder alone.
|
|
15
|
+
// is reconcile's business (`reconcile.ts`), which leaves the Backups folder alone. A bundled
|
|
16
|
+
// sample is read-only and keeps every file: nothing on it counts as removable, and no clean-up
|
|
17
|
+
// touches it.
|
|
15
18
|
// What a finished project is for: the files that get uploaded or read, from every revision,
|
|
16
19
|
// so an older video in History stays too.
|
|
17
20
|
export const outputRoles = [
|
|
@@ -37,14 +40,16 @@ export const outputRoles = [
|
|
|
37
40
|
];
|
|
38
41
|
export function projectStorage(deps, projectId) {
|
|
39
42
|
const files = filesOf(deps.paths, projectId);
|
|
40
|
-
const kept = keptPaths(deps.db, projectId);
|
|
41
43
|
const outputs = outputPaths(deps.db, projectId);
|
|
42
|
-
const
|
|
44
|
+
const sample = bundledSampleProjects(deps.db).has(projectId);
|
|
45
|
+
const removable = sample
|
|
46
|
+
? new Set()
|
|
47
|
+
: removablePaths(deps.db, projectId, keptPaths(deps.db, projectId));
|
|
43
48
|
let outputsBytes = 0;
|
|
44
49
|
let workingBytes = 0;
|
|
45
50
|
let removableFiles = 0;
|
|
46
51
|
let removableBytes = 0;
|
|
47
|
-
const history = historyPaths(deps, projectId);
|
|
52
|
+
const history = sample ? new Set() : historyPaths(deps, projectId);
|
|
48
53
|
let historyFiles = 0;
|
|
49
54
|
let historyBytes = 0;
|
|
50
55
|
for (const [path, bytes] of files) {
|
|
@@ -108,17 +113,15 @@ export function deleteOldVersions(deps, projectId) {
|
|
|
108
113
|
return { ok: true, files: count, bytesFreed };
|
|
109
114
|
}
|
|
110
115
|
// Every project's old versions, skipping any that runs or waits to run. A template studio
|
|
111
|
-
// sample keeps its files (its bake-offs and resamples reuse them), and is no project to count
|
|
116
|
+
// sample keeps its files (its bake-offs and resamples reuse them), and is no project to count;
|
|
117
|
+
// nor is a bundled sample, which is read-only.
|
|
112
118
|
export function deleteAllOldVersions(deps) {
|
|
113
119
|
const busyIds = [];
|
|
114
120
|
let projects = 0;
|
|
115
121
|
let files = 0;
|
|
116
122
|
let bytesFreed = 0;
|
|
117
123
|
let busy = 0;
|
|
118
|
-
for (const
|
|
119
|
-
.prepare(`SELECT id FROM projects WHERE ${liveProject()} AND ${notTemplateSample()}`)
|
|
120
|
-
.all()) {
|
|
121
|
-
const id = String(row.id);
|
|
124
|
+
for (const id of cleanable(deps.db)) {
|
|
122
125
|
if (placedFiles(deps.paths, id).size === 0)
|
|
123
126
|
continue;
|
|
124
127
|
const result = deleteOldVersions(deps, id);
|
|
@@ -143,10 +146,7 @@ export function cleanUpAll(deps) {
|
|
|
143
146
|
let files = old.files;
|
|
144
147
|
let bytesFreed = old.bytesFreed;
|
|
145
148
|
let skipped = old.busy;
|
|
146
|
-
for (const
|
|
147
|
-
.prepare(`SELECT id FROM projects WHERE ${liveProject()} AND ${notTemplateSample()}`)
|
|
148
|
-
.all()) {
|
|
149
|
-
const id = String(row.id);
|
|
149
|
+
for (const id of cleanable(deps.db)) {
|
|
150
150
|
const result = keepOutputsOnly(deps, id);
|
|
151
151
|
if (result.ok) {
|
|
152
152
|
files += result.files;
|
|
@@ -157,6 +157,16 @@ export function cleanUpAll(deps) {
|
|
|
157
157
|
}
|
|
158
158
|
return { files, bytesFreed, skipped };
|
|
159
159
|
}
|
|
160
|
+
// The projects the clean-ups for every project go through: neither a template studio sample nor
|
|
161
|
+
// a bundled one.
|
|
162
|
+
function cleanable(db) {
|
|
163
|
+
const samples = bundledSampleProjects(db);
|
|
164
|
+
return db
|
|
165
|
+
.prepare(`SELECT id FROM projects WHERE ${liveProject()} AND ${notTemplateSample()}`)
|
|
166
|
+
.all()
|
|
167
|
+
.map((row) => String(row.id))
|
|
168
|
+
.filter((id) => !samples.has(id));
|
|
169
|
+
}
|
|
160
170
|
// The folders under `root` left empty, deepest first, and `root` itself if it is empty then.
|
|
161
171
|
function removeEmptyUnder(root) {
|
|
162
172
|
if (!existsSync(root))
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { listProjects } from "../admission/repo.js";
|
|
2
|
+
import { uploadedProjects } from "../uploads/repo.js";
|
|
2
3
|
import { uploadPack } from "./pack.js";
|
|
3
4
|
import { readPlan } from "./plan.js";
|
|
4
5
|
import { seriesOf } from "./plan-model.js";
|
|
@@ -16,6 +17,7 @@ export function releaseCalendar(deps, options) {
|
|
|
16
17
|
return at >= from.getTime() && at < until.getTime();
|
|
17
18
|
};
|
|
18
19
|
const projects = new Map(listProjects(deps.db).map((project) => [project.id, project]));
|
|
20
|
+
const uploaded = uploadedProjects(deps.db);
|
|
19
21
|
const longs = allReleases(deps.db).filter((release) => release.short === 0 && projects.has(release.projectId) && inWindow(release.at));
|
|
20
22
|
// A schedule's line shows the schedule's name; a stored line its series.
|
|
21
23
|
const seriesOfLine = (line) => {
|
|
@@ -39,7 +41,7 @@ export function releaseCalendar(deps, options) {
|
|
|
39
41
|
const release = at === undefined || at === "" ? null : at;
|
|
40
42
|
const uploadBy = release === null ? null : new Date(Date.parse(release) - leadMs);
|
|
41
43
|
const video = videoOf(deps.db, long.projectId, item.short ?? null);
|
|
42
|
-
const state = video?.uploadState === "done"
|
|
44
|
+
const state = video?.uploadState === "done" || (short === 0 && uploaded.has(long.projectId))
|
|
43
45
|
? "scheduled"
|
|
44
46
|
: video?.uploadState === "filled"
|
|
45
47
|
? "filled"
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// So the automatic answer is Yes only when one of the three cases applies, and No otherwise:
|
|
17
17
|
// 1. a saved voice marked "Imitates a real person" (Settings → Voices) narrates it: an AI
|
|
18
18
|
// voice cloned from, or made to sound like, a real person, so that person seems to say it;
|
|
19
|
-
// 2. uploaded clips the person marked as real footage (the project's
|
|
19
|
+
// 2. uploaded clips the person marked as real footage (the project's Copy for YouTube Studio) get the
|
|
20
20
|
// Look's atmosphere overlay (embers, dust or fog), which adds to a real scene what wasn't
|
|
21
21
|
// there. A colour grade, vignette or grain alone is one of YouTube's minor edits;
|
|
22
22
|
// 3. an AI image model draws its pictures from an Image prompt marked "Draws photorealistic
|
|
@@ -49,18 +49,18 @@ export function aiDisclosureOf(input) {
|
|
|
49
49
|
if (input.realFootage && input.footageOverlay !== undefined)
|
|
50
50
|
reasons.push(`the Look's ${input.footageOverlay} overlay is laid over uploaded clips marked as real footage (YouTube's case 2: "${aiUseCases[1]}")`);
|
|
51
51
|
if (input.photorealistic)
|
|
52
|
-
reasons.push(`an AI image model draws its pictures from an Image prompt marked photorealistic in
|
|
52
|
+
reasons.push(`an AI image model draws its pictures from an Image prompt marked photorealistic in Prompts (YouTube's case 3: "${aiUseCases[2]}")`);
|
|
53
53
|
if (reasons.length > 0)
|
|
54
54
|
return {
|
|
55
55
|
altered: true,
|
|
56
56
|
why: `Yes because ${list(reasons)}. Selecting Yes adds YouTube's AI label.`,
|
|
57
57
|
};
|
|
58
58
|
const footage = input.realFootage && input.kind === "video"
|
|
59
|
-
? " The uploaded real footage only gets
|
|
59
|
+
? " The uploaded real footage only gets color, vignette or grain, which YouTube counts as minor edits."
|
|
60
60
|
: "";
|
|
61
61
|
return {
|
|
62
62
|
altered: false,
|
|
63
|
-
why: `No because none of YouTube's three AI use cases applies: no narrating voice is marked as imitating a real person (Settings → Voices), no real footage is altered, and no AI pictures come from an Image prompt marked photorealistic (
|
|
63
|
+
why: `No because none of YouTube's three AI use cases applies: no narrating voice is marked as imitating a real person (Settings → Voices), no real footage is altered, and no AI pictures come from an Image prompt marked photorealistic (Prompts). Stylized AI pictures and an AI narrator that doesn't pose as a real person don't need the label.${footage}`,
|
|
64
64
|
};
|
|
65
65
|
}
|
|
66
66
|
function quotedList(names) {
|
|
@@ -13,7 +13,7 @@ export const studioAudience = "not_made_for_kids";
|
|
|
13
13
|
export const studioUploadUrl = "https://www.youtube.com/upload";
|
|
14
14
|
// What a playlist takes: every upload, only long videos, or only shorts.
|
|
15
15
|
export const playlistUses = ["all", "long", "shorts"];
|
|
16
|
-
// The steps in Studio's order, as
|
|
16
|
+
// The steps in Studio's order, as Copy for YouTube Studio lists them.
|
|
17
17
|
export const studioSteps = [
|
|
18
18
|
"video",
|
|
19
19
|
"title",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFileSync, statSync } from "node:fs";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import { editSettings } from "../../kernel/ports/settings-places.js";
|
|
3
4
|
import { thumbnailCountOf } from "../admission/model.js";
|
|
4
5
|
import { projectById } from "../admission/repo.js";
|
|
5
6
|
import { channelById, projectChannelId } from "../channels/repo.js";
|
|
@@ -55,7 +56,7 @@ export function uploadPack(deps, projectId) {
|
|
|
55
56
|
? undefined
|
|
56
57
|
: readFileSync(outputPath(deps.paths, projectId, output.path), "utf8").trim();
|
|
57
58
|
const channelId = projectChannelId(deps.db, projectId);
|
|
58
|
-
// The channel's playlists (Settings → YouTube Studio), ticked as this project chose.
|
|
59
|
+
// The channel's playlists (Settings → YouTube Studio and posting), ticked as this project chose.
|
|
59
60
|
const playlistChoices = projectPlaylists(deps.db, projectId, channelId);
|
|
60
61
|
// The long video goes into the chosen playlists that take long videos, each short into those
|
|
61
62
|
// that take shorts.
|
|
@@ -88,7 +89,7 @@ export function uploadPack(deps, projectId) {
|
|
|
88
89
|
});
|
|
89
90
|
const video = outputs.find((output) => output.role === "video");
|
|
90
91
|
if (video === undefined)
|
|
91
|
-
missing.push("The video isn't made yet. Let the Video stage finish (or use Continue the run on the project page), then open
|
|
92
|
+
missing.push("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.");
|
|
92
93
|
const written = text(outputs.find((output) => output.role === "youtube_description"));
|
|
93
94
|
const writtenTags = text(outputs.find((output) => output.role === "youtube_tags"));
|
|
94
95
|
// As the project page shows it: the user's edits, and channel links filled in.
|
|
@@ -109,7 +110,7 @@ export function uploadPack(deps, projectId) {
|
|
|
109
110
|
if (description === undefined)
|
|
110
111
|
missing.push(config.youtubeDescription === true
|
|
111
112
|
? "The YouTube description isn't written yet. It is written after the video's subtitle timing; wait for the Video stage to finish."
|
|
112
|
-
:
|
|
113
|
+
: `No YouTube description is written for this project. Turn on YouTube description in ${editSettings.extras} to have one written with chapters and tags.`);
|
|
113
114
|
const count = thumbnailCountOf(config);
|
|
114
115
|
// The first thumbnail, then the second and third, and never one left from when the project
|
|
115
116
|
// made three and now makes one.
|
|
@@ -118,12 +119,12 @@ export function uploadPack(deps, projectId) {
|
|
|
118
119
|
.sort((a, b) => (a.meta.index ?? 1) - (b.meta.index ?? 1));
|
|
119
120
|
if (thumbnails.length === 0)
|
|
120
121
|
missing.push(config.sources.thumbnail === "off"
|
|
121
|
-
?
|
|
122
|
-
: "The thumbnail isn't made yet. Let the Thumbnail stage finish, then open
|
|
122
|
+
? `This project makes no thumbnail, so YouTube will pick a frame. Set Thumbnail source in ${editSettings.extras} to make or upload one.`
|
|
123
|
+
: "The thumbnail isn't made yet. Let the Thumbnail stage finish, then open Copy for YouTube Studio again.");
|
|
123
124
|
else if (thumbnails.length < count)
|
|
124
|
-
missing.push(`Only ${String(thumbnails.length)} of the ${String(count)} thumbnails are made.
|
|
125
|
+
missing.push(`Only ${String(thumbnails.length)} of the ${String(count)} thumbnails are made. Make the missing ones in the project's Images section.`);
|
|
125
126
|
if (playlistChoices.length === 0)
|
|
126
|
-
missing.push("No playlist is set for this project's channel, so the playlist step is left to you. Set one in Settings → YouTube Studio → Playlists.");
|
|
127
|
+
missing.push("No playlist is set for this project's channel, so the playlist step is left to you. Set one in Settings → YouTube Studio and posting → Playlists.");
|
|
127
128
|
// The video's titles and thumbnails in the project's order, and the ones the person picked
|
|
128
129
|
// for the upload first (`pick.ts`); the A/B test later tries the rest beside them.
|
|
129
130
|
const ownTitle = project.title.slice(0, studioTitleMax);
|
|
@@ -189,7 +190,7 @@ export function uploadPack(deps, projectId) {
|
|
|
189
190
|
output.meta.sentences?.[1] === clip.last);
|
|
190
191
|
const render = exact ?? mine.at(-1);
|
|
191
192
|
if (render === undefined)
|
|
192
|
-
missing.push(`Short ${String(clip.number)} isn't rendered yet. Let the Video stage finish, then open
|
|
193
|
+
missing.push(`Short ${String(clip.number)} isn't rendered yet. Let the Video stage finish, then open Copy for YouTube Studio again.`);
|
|
193
194
|
items.push({
|
|
194
195
|
kind: "short",
|
|
195
196
|
short: clip.number,
|
|
@@ -11,7 +11,7 @@ export { postingPlanSchema, weekdays, } from "./plan-model.js";
|
|
|
11
11
|
import { postingPlanSchema, seriesOf } from "./plan-model.js";
|
|
12
12
|
export const postingPlanKey = "studio.postingPlan";
|
|
13
13
|
const key = postingPlanKey;
|
|
14
|
-
// No plan until one is made: nothing is scheduled, and
|
|
14
|
+
// No plan until one is made: nothing is scheduled, and Copy for YouTube Studio says so.
|
|
15
15
|
export function emptyPlan(timeZone) {
|
|
16
16
|
return { timeZone, rows: [] };
|
|
17
17
|
}
|
|
@@ -14,7 +14,7 @@ const pairingSchema = z.object({
|
|
|
14
14
|
});
|
|
15
15
|
// A channel's own playlists are a row of their own, `studio.playlist.<channelId>`; a channel
|
|
16
16
|
// without one uses the default list above. Each playlist is ticked by default or not; a
|
|
17
|
-
// project's
|
|
17
|
+
// project's Copy for YouTube Studio can change which it goes into. A row saved before lists existed
|
|
18
18
|
// holds one name, read as one playlist ticked by default.
|
|
19
19
|
export const studioChannelPlaylistPrefix = `${studioPlaylistKey}.`;
|
|
20
20
|
function playlistKey(channelId) {
|
|
@@ -65,16 +65,16 @@ function storedPlaylists(db, key) {
|
|
|
65
65
|
return stored.trim() === "" ? null : [{ name: stored.trim(), byDefault: true }];
|
|
66
66
|
return stored.length === 0 ? null : stored;
|
|
67
67
|
}
|
|
68
|
-
// Why a list can't be saved, in words for Settings → YouTube Studio; undefined when it can.
|
|
68
|
+
// Why a list can't be saved, in words for Settings → YouTube Studio and posting; undefined when it can.
|
|
69
69
|
export function studioPlaylistsProblem(list) {
|
|
70
70
|
const long = list.find((one) => one.name.trim().length > studioPlaylistMax);
|
|
71
71
|
if (long !== undefined)
|
|
72
|
-
return `The playlist name "${long.name.slice(0, 40)}…" is longer than YouTube allows (${String(studioPlaylistMax)} characters). Shorten it in Settings → YouTube Studio.`;
|
|
72
|
+
return `The playlist name "${long.name.slice(0, 40)}…" is longer than YouTube allows (${String(studioPlaylistMax)} characters). Shorten it in Settings → YouTube Studio and posting.`;
|
|
73
73
|
const seen = new Set();
|
|
74
74
|
for (const one of list) {
|
|
75
75
|
const key = one.name.trim().toLowerCase();
|
|
76
76
|
if (seen.has(key))
|
|
77
|
-
return `"${one.name.trim()}" is listed twice. Remove one in Settings → YouTube Studio.`;
|
|
77
|
+
return `"${one.name.trim()}" is listed twice. Remove one in Settings → YouTube Studio and posting.`;
|
|
78
78
|
seen.add(key);
|
|
79
79
|
}
|
|
80
80
|
return undefined;
|
|
@@ -98,7 +98,7 @@ export function saveStudioPlaylists(db, list, channelId) {
|
|
|
98
98
|
writeSetting(db, key, JSON.stringify(value));
|
|
99
99
|
return value;
|
|
100
100
|
}
|
|
101
|
-
// The playlists a project's uploads go into, when its
|
|
101
|
+
// The playlists a project's uploads go into, when its Copy for YouTube Studio changed them from the
|
|
102
102
|
// channel's defaults: one row, project id to names.
|
|
103
103
|
export const studioProjectPlaylistsKey = "studio.projectPlaylists";
|
|
104
104
|
export const storedProjectPlaylistsSchema = z.record(z.string().max(64), z.array(z.string().max(studioPlaylistMax)).max(studioPlaylistsMax));
|
|
@@ -127,7 +127,7 @@ export function saveProjectPlaylists(db, projectId, names) {
|
|
|
127
127
|
writeSetting(db, studioProjectPlaylistsKey, JSON.stringify(next));
|
|
128
128
|
}
|
|
129
129
|
// The projects whose uploaded clips are real footage (filmed, not generated): ticked in the
|
|
130
|
-
// project's
|
|
130
|
+
// project's Copy for YouTube Studio, under AI use. One row holding their ids; YouTube's second AI use
|
|
131
131
|
// case is footage of a real event or place altered by AI (`disclosure.ts`).
|
|
132
132
|
export const studioRealFootageKey = "studio.realFootage";
|
|
133
133
|
// "on" or "off": whether the extension posts and pins each video's comment once it is public.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { markUploaded } from "../uploads/repo.js";
|
|
1
2
|
const taskState = (value) => value === "waiting" || value === "done" || value === "failed" ? value : "none";
|
|
2
3
|
// A YouTube video id: 11 letters, digits, `-` and `_`.
|
|
3
4
|
export const videoIdPattern = /^[A-Za-z0-9_-]{11}$/;
|
|
@@ -41,15 +42,33 @@ export function recordVideo(db, projectId, short, videoId, at, state = "done") {
|
|
|
41
42
|
upload_state=CASE WHEN video_id=excluded.video_id AND upload_state='done' THEN 'done'
|
|
42
43
|
ELSE excluded.upload_state END,
|
|
43
44
|
video_id=excluded.video_id, recorded_at=excluded.recorded_at`).run(projectId, slot(short), videoId, at, state);
|
|
45
|
+
if (short === null && videoOf(db, projectId, null)?.uploadState === "done")
|
|
46
|
+
markUploaded(db, projectId, true, at);
|
|
44
47
|
}
|
|
45
48
|
// Studio said the upload was scheduled or published. False when no such upload was filled.
|
|
46
|
-
export function confirmUpload(db, projectId, short, videoId) {
|
|
47
|
-
|
|
49
|
+
export function confirmUpload(db, projectId, short, videoId, at) {
|
|
50
|
+
const confirmed = db
|
|
48
51
|
.prepare("UPDATE youtube_videos SET upload_state='done' WHERE project_id=? AND short=? AND video_id=?")
|
|
49
|
-
.run(projectId, slot(short), videoId).changes > 0
|
|
52
|
+
.run(projectId, slot(short), videoId).changes > 0;
|
|
53
|
+
if (confirmed && short === null)
|
|
54
|
+
markUploaded(db, projectId, true, at);
|
|
55
|
+
return confirmed;
|
|
50
56
|
}
|
|
51
|
-
|
|
57
|
+
// Forgets the video an upload became (deleted on YouTube, or a wrong link). Forgetting a long
|
|
58
|
+
// video that was on YouTube takes the project's uploaded mark off with it; an upload only
|
|
59
|
+
// filled in Studio never set the mark, so forgetting it leaves a mark made by hand.
|
|
60
|
+
export function forgetVideo(db, projectId, short, at) {
|
|
61
|
+
const was = videoOf(db, projectId, short);
|
|
52
62
|
db.prepare("DELETE FROM youtube_videos WHERE project_id=? AND short=?").run(projectId, slot(short));
|
|
63
|
+
if (short === null && was?.uploadState === "done")
|
|
64
|
+
markUploaded(db, projectId, false, at);
|
|
65
|
+
}
|
|
66
|
+
// "Mark not uploaded" (Home, Projects): the long video's link goes with the mark, so the two
|
|
67
|
+
// never disagree. An upload only filled in Studio is not on YouTube and stays.
|
|
68
|
+
export function markNotUploaded(db, projectId, at) {
|
|
69
|
+
if (videoOf(db, projectId, null)?.uploadState === "done")
|
|
70
|
+
forgetVideo(db, projectId, null, at);
|
|
71
|
+
return markUploaded(db, projectId, false, at);
|
|
53
72
|
}
|
|
54
73
|
export function projectVideos(db, projectId) {
|
|
55
74
|
return db
|
|
@@ -162,9 +162,9 @@ function explained(log, error) {
|
|
|
162
162
|
return error;
|
|
163
163
|
const message = errorText(error);
|
|
164
164
|
if (isMissingFont(error))
|
|
165
|
-
return new StylePreviewError("font-missing", "The style preview couldn't render because the chosen caption font is no longer available; it may have been uninstalled or deleted. Choose another font
|
|
165
|
+
return new StylePreviewError("font-missing", "The style preview couldn't render because the chosen caption font is no longer available; it may have been uninstalled or deleted. Choose another caption font, then press Make the preview again.");
|
|
166
166
|
if (message.startsWith("Slopify could not start ffmpeg") || /\bENOENT\b/.test(message))
|
|
167
|
-
return new StylePreviewError("ffmpeg-missing", "The style preview couldn't render because ffmpeg wasn't found. Install ffmpeg, or use the Docker image, then press
|
|
167
|
+
return new StylePreviewError("ffmpeg-missing", "The style preview couldn't render because ffmpeg wasn't found. Install ffmpeg, or use the Docker image, then press Make the preview again.");
|
|
168
168
|
log.write("error", "style-preview.render", { detail: message });
|
|
169
|
-
return new StylePreviewError("render-failed", "The style preview couldn't render because ffmpeg stopped with an error. Press
|
|
169
|
+
return new StylePreviewError("render-failed", "The style preview couldn't render because ffmpeg stopped with an error. Press Make the preview again; if it fails again, use Download diagnostics in Settings and report it.");
|
|
170
170
|
}
|
|
@@ -60,7 +60,7 @@ export const stylePreviewRequestSchema = z.object({
|
|
|
60
60
|
motionStyle: z.enum(motionStyles),
|
|
61
61
|
})
|
|
62
62
|
.optional(),
|
|
63
|
-
//
|
|
63
|
+
// Make the preview again even when this exact preview is already saved.
|
|
64
64
|
force: z.boolean().optional(),
|
|
65
65
|
});
|
|
66
66
|
export const stylePreviewReplySchema = z.object({
|
|
@@ -24,7 +24,7 @@ export function captionCues(words) {
|
|
|
24
24
|
!Number.isFinite(word.end) ||
|
|
25
25
|
word.start < previousEnd - 0.001 ||
|
|
26
26
|
word.end <= word.start) {
|
|
27
|
-
throw new Error("The saved caption timing is damaged.
|
|
27
|
+
throw new Error("The saved caption timing is damaged. Press Make the video again in the project's Video section to time them again, then Try again.");
|
|
28
28
|
}
|
|
29
29
|
const first = pending[0];
|
|
30
30
|
if (first !== undefined &&
|
|
@@ -58,14 +58,24 @@ export async function getAnatomy(deps, id, section, item) {
|
|
|
58
58
|
return {
|
|
59
59
|
ok: false,
|
|
60
60
|
reason: "not-found",
|
|
61
|
-
message: "This template no longer exists; it may have been deleted. Go to
|
|
61
|
+
message: "This template no longer exists; it may have been deleted. Go to Templates to see your templates.",
|
|
62
62
|
};
|
|
63
63
|
const record = sampleOf(deps.db, id);
|
|
64
64
|
const fullScript = record?.fullScript ?? false;
|
|
65
|
-
const
|
|
65
|
+
const sampled = sampleDocument(template.document, {
|
|
66
66
|
topic: record?.topic ?? defaultSampleTopic(deps.db),
|
|
67
67
|
fullScript,
|
|
68
|
-
})
|
|
68
|
+
});
|
|
69
|
+
let resolved = await resolveSampleRun(deps.review, sampled, fullScript);
|
|
70
|
+
// A refusal about another section (no image service on a fresh install) never hides this
|
|
71
|
+
// section's prompt: the setup is read again with only what this section's prompt needs.
|
|
72
|
+
for (const focused of resolved.ok ? [] : focusedDocuments(sampled, section)) {
|
|
73
|
+
const retry = await resolveSampleRun(deps.review, focused, fullScript);
|
|
74
|
+
if (retry.ok) {
|
|
75
|
+
resolved = retry;
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
69
79
|
const empty = (problem) => ({
|
|
70
80
|
ok: true,
|
|
71
81
|
value: { section, items: [], item: null, segments: [], fromSample: false, problem },
|
|
@@ -116,3 +126,36 @@ export async function getAnatomy(deps, id, section, item) {
|
|
|
116
126
|
},
|
|
117
127
|
};
|
|
118
128
|
}
|
|
129
|
+
// The setup with every stage this section's prompt doesn't need turned off: first keeping the
|
|
130
|
+
// narration, which shapes the article's prompt (a script for speakers), then without it.
|
|
131
|
+
export function focusedDocuments(document, section) {
|
|
132
|
+
const { form } = document;
|
|
133
|
+
const keep = (own, source) => section === own ? source : "off";
|
|
134
|
+
const focused = {
|
|
135
|
+
...document,
|
|
136
|
+
form: {
|
|
137
|
+
...form,
|
|
138
|
+
sources: {
|
|
139
|
+
...form.sources,
|
|
140
|
+
images: section === "shorts" ? form.sources.images : keep("pictures", form.sources.images),
|
|
141
|
+
thumbnail: keep("thumbnail", form.sources.thumbnail),
|
|
142
|
+
video: "off",
|
|
143
|
+
...(form.sources.document === undefined ? {} : { document: "off" }),
|
|
144
|
+
},
|
|
145
|
+
youtubeDescription: section === "upload" ? form.youtubeDescription : false,
|
|
146
|
+
...(form.shorts === undefined || section === "shorts"
|
|
147
|
+
? {}
|
|
148
|
+
: { shorts: { ...form.shorts, enabled: false } }),
|
|
149
|
+
...(section === "upload" ? {} : { social: undefined }),
|
|
150
|
+
...(form.reviews === undefined || section === "reviews"
|
|
151
|
+
? {}
|
|
152
|
+
: { reviews: { ...form.reviews, stages: {} } }),
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
if (section === "voice" || focused.form.sources.audio === "off")
|
|
156
|
+
return [focused];
|
|
157
|
+
return [
|
|
158
|
+
focused,
|
|
159
|
+
{ ...focused, form: { ...focused.form, sources: { ...focused.form.sources, audio: "off" } } },
|
|
160
|
+
];
|
|
161
|
+
}
|