@gentbajko/slopify 4.0.0 → 4.2.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 +5 -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/pronunciation-check.js +133 -0
- package/dist/edge/http/pronunciations.js +26 -0
- 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 +31 -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/4.2.0.md +28 -0
- package/dist/patch-notes/index.json +12 -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/schema.js +2 -0
- package/dist/slices/admission/start.js +7 -0
- package/dist/slices/admission/state-words.js +34 -5
- package/dist/slices/assistant/cost.js +1 -1
- package/dist/slices/assistant/gate.js +1 -0
- 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-check.js +226 -0
- package/dist/slices/narration/pronunciation.js +7 -2
- package/dist/slices/narration/pronunciations-library.js +94 -0
- 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-text.js +16 -4
- 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 +95 -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 +101 -19
- 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 +183 -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-CQASqwjT.js +460 -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,6 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { internalError } from "../../kernel/errors.js";
|
|
4
|
+
import { editSettings } from "../../kernel/ports/settings-places.js";
|
|
4
5
|
import { narrationAliasesOf, usesNarrationPreparation, usesPronunciationGlossary, } from "../admission/rules.js";
|
|
5
6
|
import { plainText } from "../article/plain.js";
|
|
6
7
|
import { splitEndMatter } from "../article/split.js";
|
|
@@ -52,18 +53,18 @@ export async function narrationAudio(deps, view, options = {}) {
|
|
|
52
53
|
? one.output.role === "audio_levelled" && one.workKey === levelKey(segment)
|
|
53
54
|
: one.output.role === role));
|
|
54
55
|
if (row === undefined && levelled)
|
|
55
|
-
throw new Error(`The levelled ${segment === "body" ? "narration" : segment} isn't finished yet, so the sound can't be exported at an even volume. Let the Narration stage finish (
|
|
56
|
+
throw new Error(`The levelled ${segment === "body" ? "narration" : segment} isn't finished yet, so the sound can't be exported at an even volume. Let the Narration stage finish (Continue the run, or Try again on Narration), then try this stage again; or turn off Level the volume in ${editSettings.sound}.`);
|
|
56
57
|
if (row === undefined)
|
|
57
58
|
return undefined;
|
|
58
59
|
const path = outputPath(deps.paths, view.revision.projectId, row.output.path);
|
|
59
60
|
const duration = row.output.durationMs ?? (await deps.measure(path));
|
|
60
61
|
if (!Number.isFinite(duration) || duration <= 0)
|
|
61
|
-
throw new Error(
|
|
62
|
+
throw new Error(`Slopify couldn't read the length of the narration audio, so the file may be damaged or empty. Press Make the narration again in the project's Narration section (or upload your audio file again in ${editSettings.narration}), then Try again.`);
|
|
62
63
|
return { path, seconds: duration / 1000 };
|
|
63
64
|
};
|
|
64
65
|
const body = await input("audio_body");
|
|
65
66
|
if (body === undefined)
|
|
66
|
-
throw new Error("The narration audio isn't finished yet. Let the Narration stage finish (
|
|
67
|
+
throw new Error("The narration audio isn't finished yet. Let the Narration stage finish (Continue the run, or Try again on Narration), then try this stage again.");
|
|
67
68
|
return narrationTimeline(config, {
|
|
68
69
|
body,
|
|
69
70
|
intro: speaksPart(config, "intro") ? await input("audio_intro") : undefined,
|
|
@@ -108,7 +109,7 @@ export function revisionTranscript(deps, snapshot, kind) {
|
|
|
108
109
|
Array.isArray(recipe.input.semantic) &&
|
|
109
110
|
typeof recipe.input.semantic[0] === "string")
|
|
110
111
|
return recipe.input.semantic[0];
|
|
111
|
-
throw new Error(
|
|
112
|
+
throw new Error(`Some narration chunks aren't finished, so the full narration text isn't available. Let the Narration stage finish (Continue the run, or Try again on Narration), or make the missing chunk again in ${editSettings.narration}.`);
|
|
112
113
|
});
|
|
113
114
|
if (texts.length > 0)
|
|
114
115
|
return texts.join("\n");
|
|
@@ -19,7 +19,7 @@ export async function executeShortExport(deps, context, piece, snapshot, timelin
|
|
|
19
19
|
const words = readTimingWords(deps, context, view, "the shorts are cut and captioned from").words;
|
|
20
20
|
const seconds = timeline.reduce((sum, segment) => sum + segment.seconds, 0);
|
|
21
21
|
if (seconds <= 0)
|
|
22
|
-
throw new Error("The short has no narration to play, so there is nothing to render. Let the Narration stage finish (
|
|
22
|
+
throw new Error("The short has no narration to play, so there is nothing to render. Let the Narration stage finish (Continue the run, or Try again on Narration), then try this stage again.");
|
|
23
23
|
const font = await resolveBoldFont(deps.paths, config.subtitles?.fontId ?? "default");
|
|
24
24
|
if (!context.maySubmit(piece.id))
|
|
25
25
|
return "held";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { copyFileSync, mkdirSync, mkdtempSync, rmSync } from "node:fs";
|
|
2
2
|
import { extname, join, relative, sep } from "node:path";
|
|
3
3
|
import { internalError } from "../../kernel/errors.js";
|
|
4
|
+
import { editSettings } from "../../kernel/ports/settings-places.js";
|
|
4
5
|
import { usesShortMode } from "../admission/short-mode.js";
|
|
5
6
|
import { masterFile, masterReport } from "../loudness/loudnorm.js";
|
|
6
7
|
import { masterGoal } from "../loudness/model.js";
|
|
@@ -46,7 +47,7 @@ export async function executeExportRecipe(deps, context, piece) {
|
|
|
46
47
|
if (!wav && piece.key !== "export:video")
|
|
47
48
|
throw new Error(internalError("unknown kind of export"));
|
|
48
49
|
if (wav && audio.length === 0)
|
|
49
|
-
throw new Error(
|
|
50
|
+
throw new Error(`The audio export has nothing to export because narration is turned off for this project. Turn narration on (Narration source in ${editSettings.narration}), then Try again.`);
|
|
50
51
|
if (!wav && usesShortMode(view.revision.config))
|
|
51
52
|
return executeShortExport(deps, context, piece, snapshot, audio);
|
|
52
53
|
const filename = wav ? "audio.wav" : "video.mp4";
|
|
@@ -240,7 +241,7 @@ export function slideshowImages(deps, context, view) {
|
|
|
240
241
|
return view.revision.content.imageOrder.map((key) => {
|
|
241
242
|
const row = readyOutput(view, { workKey: `image:${key}` });
|
|
242
243
|
if (row === undefined)
|
|
243
|
-
throw new Error(
|
|
244
|
+
throw new Error(`The video export can't find one of the slideshow images. Press Make this image again on it in the project's Images section (or replace it in ${editSettings.images}), then Try again.`);
|
|
244
245
|
return outputPath(deps.paths, context.work.projectId, row.output.path);
|
|
245
246
|
});
|
|
246
247
|
}
|
|
@@ -249,7 +250,7 @@ function captionDirectory(deps, context, snapshot) {
|
|
|
249
250
|
const ass = find("subtitle_ass");
|
|
250
251
|
const font = find("subtitle_font");
|
|
251
252
|
if (ass === undefined || font === undefined)
|
|
252
|
-
throw new Error(
|
|
253
|
+
throw new Error(`Burned-in captions are missing their caption file or font. Press Make the video again in the project's Video section (or choose the caption font again in ${editSettings.captions}), then Try again.`);
|
|
253
254
|
const directory = mkdtempSync(join(projectDir(deps.paths, context.work.projectId), "render-"));
|
|
254
255
|
try {
|
|
255
256
|
mkdirSync(join(directory, "fonts"), { mode: 0o700 });
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
+
import { editSettings } from "../../kernel/ports/settings-places.js";
|
|
2
3
|
import { imageBlob } from "../channels/repo.js";
|
|
3
4
|
import { assetPathOrUndefined } from "../revisions/mutation-assets.js";
|
|
4
5
|
import { outputPath } from "../storage/layout.js";
|
|
@@ -22,7 +23,7 @@ function referenceImage(deps, projectId, input) {
|
|
|
22
23
|
if (input.reference === undefined)
|
|
23
24
|
return undefined;
|
|
24
25
|
// The step waits for the establishing image, so a missing one went away since.
|
|
25
|
-
const missing =
|
|
26
|
+
const missing = `The establishing image this image is drawn from is missing. Press Make the establishing image again in the project's Images section (or upload it again in ${editSettings.images}), then use Try again.`;
|
|
26
27
|
if (input.reference.assetId === null)
|
|
27
28
|
throw new Error(missing);
|
|
28
29
|
const saved = assetPathOrUndefined(deps, projectId, input.reference.assetId);
|
|
@@ -37,7 +38,7 @@ function referenceImage(deps, projectId, input) {
|
|
|
37
38
|
}
|
|
38
39
|
const mime = imageMime(bytes);
|
|
39
40
|
if (mime === undefined)
|
|
40
|
-
throw new Error(
|
|
41
|
+
throw new Error(`The establishing image is not a PNG or JPEG file, so it can't be sent as a reference. Upload a PNG or JPEG under Establishing image in ${editSettings.images}, then use Try again.`);
|
|
41
42
|
return { bytes, mime };
|
|
42
43
|
}
|
|
43
44
|
// The cast pictures the request names, read by hash from the database. They are never removed
|
|
@@ -51,7 +52,7 @@ function castImages(deps, input) {
|
|
|
51
52
|
images: member.images.map((sha256) => {
|
|
52
53
|
const blob = imageBlob(deps.db, sha256);
|
|
53
54
|
if (blob === undefined)
|
|
54
|
-
throw new Error(`A picture of ${member.name} that this project was started with is not in this Slopify's database, so it can't be sent as a reference. Add the picture again in Channels → Cast, then start the video again from
|
|
55
|
+
throw new Error(`A picture of ${member.name} that this project was started with is not in this Slopify's database, so it can't be sent as a reference. Add the picture again in Channels → Cast, then start the video again from Create.`);
|
|
55
56
|
return blob;
|
|
56
57
|
}),
|
|
57
58
|
}));
|
|
@@ -2,6 +2,7 @@ import { rmSync, statSync } from "node:fs";
|
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { internalError } from "../../kernel/errors.js";
|
|
5
|
+
import { editSettings } from "../../kernel/ports/settings-places.js";
|
|
5
6
|
import { referenceKey, thumbnailVariant } from "../admission/model.js";
|
|
6
7
|
import { plainText } from "../article/plain.js";
|
|
7
8
|
import { splitEndMatter } from "../article/split.js";
|
|
@@ -31,7 +32,7 @@ export async function executeLocalRecipe(deps, context, piece) {
|
|
|
31
32
|
const input = piece.input;
|
|
32
33
|
if (input.kind === "provided") {
|
|
33
34
|
if (input.assetId === null)
|
|
34
|
-
throw new Error("A file you uploaded for this project is missing. Upload it again in Edit
|
|
35
|
+
throw new Error("A file you uploaded for this project is missing. Upload it again in Edit settings, then Try again.");
|
|
35
36
|
const row = z
|
|
36
37
|
.object({ id: z.string(), project_id: z.string(), path: z.string(), created_at: z.string() })
|
|
37
38
|
.parse(deps.db
|
|
@@ -115,7 +116,7 @@ async function concatenate(deps, context, piece) {
|
|
|
115
116
|
isDonePiece(candidate) &&
|
|
116
117
|
candidate.fingerprint === plan.recipes.find((value) => value.key === key)?.fingerprint);
|
|
117
118
|
if (input?.assetId === null || input === undefined)
|
|
118
|
-
throw new Error(
|
|
119
|
+
throw new Error(`One narration chunk has no saved audio, so the narration can't be joined together. Make the missing chunk again in ${editSettings.narration}, then Try again.`);
|
|
119
120
|
return outputPath(deps.paths, context.work.projectId, assetPath(deps, context.work.projectId, input.assetId));
|
|
120
121
|
});
|
|
121
122
|
// Level the volume repeats the join its recipe names, from the same pieces, each levelled
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { internalError } from "../../kernel/errors.js";
|
|
3
|
+
import { editSettings } from "../../kernel/ports/settings-places.js";
|
|
3
4
|
import { isDonePiece } from "../revisions/view.js";
|
|
4
5
|
import { executionPlan, executionView, savedCatalogue, workCatalogue } from "./runtime-plan.js";
|
|
5
6
|
import { preparedTexts, publishResult } from "./runtime-publication.js";
|
|
@@ -16,7 +17,7 @@ export function narrationTextParts(view, plan, segment) {
|
|
|
16
17
|
const recipe = plan.recipes.find((row) => row.key === key);
|
|
17
18
|
const selected = view.pieces.find((row) => row.key === key && isDonePiece(row) && row.fingerprint === recipe?.fingerprint);
|
|
18
19
|
if (selected === undefined || recipe === undefined)
|
|
19
|
-
throw new Error(
|
|
20
|
+
throw new Error(`Some narration chunks aren't finished, so the full narration text isn't available. Let the Narration stage finish (Continue the run, or Try again on Narration), or make the missing chunk again in ${editSettings.narration}.`);
|
|
20
21
|
const input = recipe.input;
|
|
21
22
|
if (input.kind === "provided" &&
|
|
22
23
|
Array.isArray(input.semantic) &&
|
|
@@ -33,7 +34,7 @@ export function narrationTextParts(view, plan, segment) {
|
|
|
33
34
|
.parse(JSON.parse(selected.piece.payload ?? "{}"));
|
|
34
35
|
if (input.spokenText !== undefined &&
|
|
35
36
|
(payload.spokenText !== input.spokenText || payload.text !== input.text))
|
|
36
|
-
throw new Error(
|
|
37
|
+
throw new Error(`A narration chunk's audio no longer matches its prepared text. Make that chunk again in ${editSettings.narration}, then Try again.`);
|
|
37
38
|
return {
|
|
38
39
|
logicalKey: input.logicalKey,
|
|
39
40
|
spokenText: input.spokenText === undefined ? (payload.text ?? input.text) : (payload.spokenText ?? ""),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { basename } from "node:path";
|
|
2
2
|
import { internalError } from "../../kernel/errors.js";
|
|
3
|
+
import { editSettings } from "../../kernel/ports/settings-places.js";
|
|
3
4
|
import { referenceKey, thumbnailVariant } from "../admission/model.js";
|
|
4
5
|
import { plainText } from "../article/plain.js";
|
|
5
6
|
import { splitEndMatter } from "../article/split.js";
|
|
@@ -189,17 +190,17 @@ export async function softenIfAsked(deps, context, providers, piece, prompt, lab
|
|
|
189
190
|
messages: softenMessages(prompt),
|
|
190
191
|
previewLabel: `${label}: softened prompt`,
|
|
191
192
|
check: (value) => softenedPrompt(value.text) === ""
|
|
192
|
-
?
|
|
193
|
+
? `The AI model sent back an empty prompt while softening it. Use Soften and retry again, or reword the prompt yourself in ${editSettings.images}.`
|
|
193
194
|
: undefined,
|
|
194
195
|
});
|
|
195
196
|
return answer.ok ? softenedPrompt(answer.value.text) : "held";
|
|
196
197
|
}
|
|
197
198
|
function checkAnswer(piece, answer) {
|
|
198
199
|
if (answer.text.trim() === "")
|
|
199
|
-
return
|
|
200
|
+
return `The AI model sent back an empty answer. Use Try again; if it keeps happening, choose another model in ${editSettings.writer}.`;
|
|
200
201
|
if (piece.input.kind === "llm" && piece.input.describe !== undefined)
|
|
201
202
|
return spokenPassage(answer.text) === ""
|
|
202
|
-
?
|
|
203
|
+
? `The AI model's description had no words a narrator could say. Use Try again; if it keeps happening, choose another model in ${editSettings.writer}, or turn off Describe tables and figures in ${editSettings.narration}.`
|
|
203
204
|
: undefined;
|
|
204
205
|
// Scenes from the article: one scene per image, as many as the request lists.
|
|
205
206
|
if ((piece.key === imageScenesKey || piece.key === thumbnailScenesKey) &&
|
|
@@ -222,7 +223,7 @@ function checkAnswer(piece, answer) {
|
|
|
222
223
|
return checkPost(answer.text, piece.input.social);
|
|
223
224
|
if (piece.key === "research:planner")
|
|
224
225
|
return chaptersFrom(answer.text).length === 0
|
|
225
|
-
?
|
|
226
|
+
? `The AI model's research plan listed no chapters, so research could not go on. Use Try again; if it keeps happening, choose another model in ${editSettings.writer}.`
|
|
226
227
|
: undefined;
|
|
227
228
|
if (piece.key === "research:notes" || piece.key.startsWith("research:chapter:"))
|
|
228
229
|
return sourcedAnswer(piece.key, answer.text);
|
|
@@ -236,7 +237,7 @@ function checkAnswer(piece, answer) {
|
|
|
236
237
|
const checked = parseScript(answer.text, piece.input.script.speakers);
|
|
237
238
|
return checked.ok
|
|
238
239
|
? undefined
|
|
239
|
-
: `The conversation the
|
|
240
|
+
: `The conversation the AI model wrote can't be read as a script: ${checked.reason}`;
|
|
240
241
|
}
|
|
241
242
|
return undefined;
|
|
242
243
|
}
|
|
@@ -322,7 +323,7 @@ async function publishText(deps, context, piece, answer) {
|
|
|
322
323
|
if (piece.input.kind === "llm" && piece.input.script !== undefined) {
|
|
323
324
|
const checked = parseScript(parts.body, piece.input.script.speakers);
|
|
324
325
|
if (!checked.ok)
|
|
325
|
-
throw new Error(`The
|
|
326
|
+
throw new Error(`The AI model's script can't be read: ${checked.reason} Try again to have it written again, or fix it in ${editSettings.article}.`);
|
|
326
327
|
}
|
|
327
328
|
await publishResult(deps, context, piece, preparedTexts(deps, context, piece, [
|
|
328
329
|
["instructions", "instructions.md", frozenInstructions(deps, context, piece)],
|
|
@@ -2,6 +2,7 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { internalError } from "../../kernel/errors.js";
|
|
4
4
|
import { thinkingModes } from "../../kernel/ports/llm.js";
|
|
5
|
+
import { editSettings } from "../../kernel/ports/settings-places.js";
|
|
5
6
|
import { plainText } from "../article/plain.js";
|
|
6
7
|
import { defaultReviewPrompts, narrationItemKey, reviewStages, } from "../reviews/model.js";
|
|
7
8
|
import { nextAttempt, reviewOutcome } from "../reviews/outcome.js";
|
|
@@ -191,8 +192,8 @@ export function reviewMaterial(deps, context, snapshot, stage, itemKey, prompt)
|
|
|
191
192
|
const image = output(itemKey);
|
|
192
193
|
if (image === undefined)
|
|
193
194
|
throw missing(stage === "images" ? "image" : "thumbnail", stage === "images"
|
|
194
|
-
? "
|
|
195
|
-
: "
|
|
195
|
+
? "Press Make this image again on it in the project's Images section"
|
|
196
|
+
: "Press Make the thumbnail again in the project's Images section");
|
|
196
197
|
const recipe = recipes.find((one) => one.key === itemKey);
|
|
197
198
|
const brief = recipe?.input.kind === "image" ? recipe.input.prompt : "";
|
|
198
199
|
const reference = stage === "images" ? output("reference:image") : undefined;
|
|
@@ -226,7 +227,7 @@ export function reviewMaterial(deps, context, snapshot, stage, itemKey, prompt)
|
|
|
226
227
|
case "narration": {
|
|
227
228
|
const timing = output("subtitles:timing", "subtitle_words");
|
|
228
229
|
if (timing === undefined || itemKey !== narrationItemKey)
|
|
229
|
-
throw missing("narration's word timing", "
|
|
230
|
+
throw missing("narration's word timing", "Press Make the video again in the project's Video section");
|
|
230
231
|
const { words, omissions } = wordsSchema.parse(JSON.parse(readFileSync(path(timing), "utf8")));
|
|
231
232
|
const unsure = words.filter((word) => (word.confidence ?? 1) < 0.3).length;
|
|
232
233
|
return {
|
|
@@ -254,7 +255,7 @@ export function reviewMaterial(deps, context, snapshot, stage, itemKey, prompt)
|
|
|
254
255
|
case "shorts": {
|
|
255
256
|
const render = output(`${itemKey}:render`);
|
|
256
257
|
if (render === undefined)
|
|
257
|
-
throw missing("short",
|
|
258
|
+
throw missing("short", `Use Make this short again in ${editSettings.shorts}`);
|
|
258
259
|
const number = Number(itemKey.slice("shorts:".length));
|
|
259
260
|
const pick = view.pieces.find((row) => row.key === "shorts:pick" && row.selected && row.piece.state === "done");
|
|
260
261
|
const title = pickedShortsOf(pick?.piece.payload)?.shorts.find((clip) => clip.number === number)?.title ??
|
|
@@ -263,7 +264,7 @@ export function reviewMaterial(deps, context, snapshot, stage, itemKey, prompt)
|
|
|
263
264
|
.filter((row) => row.workKey.startsWith(`${itemKey}:image:`) && isReadyOutput(row))
|
|
264
265
|
.slice(0, 6);
|
|
265
266
|
if (stills.length === 0)
|
|
266
|
-
throw missing("short's images",
|
|
267
|
+
throw missing("short's images", `Use Make this short again in ${editSettings.shorts}`);
|
|
267
268
|
const names = stills.map((_, index) => `image ${String(index + 1)} of the short`);
|
|
268
269
|
return {
|
|
269
270
|
request: request([{ label: "Title of the short", text: title }], names),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { errorText, internalError } from "../../kernel/errors.js";
|
|
2
2
|
import { withLanguage, wordTimingUnavailable } from "../../kernel/ports/languages.js";
|
|
3
|
+
import { editSettings } from "../../kernel/ports/settings-places.js";
|
|
3
4
|
import { usesFigureCards, usesShorts } from "../admission/rules.js";
|
|
4
5
|
import { captionBoldFont } from "../fonts/coverage.js";
|
|
5
6
|
import { masterGoal } from "../loudness/model.js";
|
|
@@ -48,7 +49,7 @@ export async function executeShortsRecipe(deps, context, providers, piece) {
|
|
|
48
49
|
throw setupError();
|
|
49
50
|
const clip = savedClips(snapshot).find((one) => one.number === number);
|
|
50
51
|
if (clip === undefined)
|
|
51
|
-
throw new Error(
|
|
52
|
+
throw new Error(`The clip this short was cut from is no longer among the picked shorts. Use Pick different moments in ${editSettings.shorts}, then Try again.`);
|
|
52
53
|
if (matched[2] === "prompts")
|
|
53
54
|
return prompts(deps, context, providers, piece, snapshot, clip);
|
|
54
55
|
if (matched[2] === "render")
|
|
@@ -66,10 +67,10 @@ async function pick(deps, context, providers, piece, snapshot) {
|
|
|
66
67
|
throw setupError();
|
|
67
68
|
const llm = config.llm;
|
|
68
69
|
if (llm === undefined || llm.provider.trim() === "" || llm.model.trim() === "")
|
|
69
|
-
throw new Error(
|
|
70
|
+
throw new Error(`Shorts need an AI model to pick the clips, and none is chosen. Choose a Service and a Model in ${editSettings.writer}, then Try again.`);
|
|
70
71
|
const sentences = transcriptSentences(readTimingWords(deps, context, view, shortsTimingPurpose).words);
|
|
71
72
|
if (sentences.length === 0)
|
|
72
|
-
throw new Error("The narration's word timing holds no words, so there is nothing to cut shorts from.
|
|
73
|
+
throw new Error("The narration's word timing holds no words, so there is nothing to cut shorts from. Press Make the video again in the project's Video section, then Try again.");
|
|
73
74
|
const audio = await revisionAudio(deps, context, view);
|
|
74
75
|
const durationSeconds = audio.reduce((sum, segment) => sum + segment.seconds, 0);
|
|
75
76
|
// Kept shorts are found again in this timing; the model is asked only for the rest.
|
|
@@ -238,7 +239,7 @@ async function render(deps, context, piece, snapshot, clip) {
|
|
|
238
239
|
.map((recipe) => {
|
|
239
240
|
const row = readyOutput(view, { workKey: recipe.key });
|
|
240
241
|
if (row === undefined)
|
|
241
|
-
throw new Error(`Short ${String(clip.number)} is missing one of its images. Try again to make it again; if it keeps happening, use Make this short again in
|
|
242
|
+
throw new Error(`Short ${String(clip.number)} is missing one of its images. Try again to make it again; if it keeps happening, use Make this short again in ${editSettings.shorts}.`);
|
|
242
243
|
return outputPath(deps.paths, context.work.projectId, row.output.path);
|
|
243
244
|
});
|
|
244
245
|
const allWords = readTimingWords(deps, context, view, shortsTimingPurpose).words;
|
|
@@ -344,7 +345,7 @@ async function backgroundMusic(deps, context, view) {
|
|
|
344
345
|
const shorts = view.revision.config.shorts;
|
|
345
346
|
if (assetId === undefined || shorts === undefined)
|
|
346
347
|
return undefined;
|
|
347
|
-
const fix =
|
|
348
|
+
const fix = `Choose another file under Background music in ${editSettings.shorts}, or remove the music, then Try again.`;
|
|
348
349
|
const saved = assetPathOrUndefined(deps, context.work.projectId, assetId);
|
|
349
350
|
if (saved === undefined)
|
|
350
351
|
throw new Error(`The shorts' background music file is no longer in the project. ${fix}`);
|
|
@@ -223,7 +223,13 @@ export function projectStandings(deps, projectId) {
|
|
|
223
223
|
}, 0);
|
|
224
224
|
// A failed step, or else one waiting to run again, says why on the stage row. A failed
|
|
225
225
|
// stage keeps the moment it failed: needs_attention tells a narration retry started after
|
|
226
|
-
// the failure (still going) from one started before it (spent) by it.
|
|
226
|
+
// the failure (still going) from one started before it (spent) by it. A stage's start is
|
|
227
|
+
// the moment it went running, kept through a wait to retry, between its steps (one done,
|
|
228
|
+
// the next not claimed yet) and once it ends; a pending start is kept only then, so a
|
|
229
|
+
// stage made again after it was done takes a new one when it runs. A done
|
|
230
|
+
// stage keeps the moment it finished: the project's run length, the time left on a running
|
|
231
|
+
// step and the usual time a step takes (`eta/history.ts`) read them, and a later save that
|
|
232
|
+
// changes nothing in the stage moves neither.
|
|
227
233
|
const failed = all.find((work) => work.state === "failed") ??
|
|
228
234
|
(stage.retryAt === undefined || stage.retryAt === null
|
|
229
235
|
? undefined
|
|
@@ -236,7 +242,16 @@ export function projectStandings(deps, projectId) {
|
|
|
236
242
|
deps.db
|
|
237
243
|
.prepare(`UPDATE stages SET state=:state,failure_reason=:reason,failure_kind=:kind,retry_at=:retryAt,
|
|
238
244
|
progress_current=:current,progress_total=:total,
|
|
239
|
-
|
|
245
|
+
started_at=CASE
|
|
246
|
+
WHEN :state='running' THEN CASE
|
|
247
|
+
WHEN started_at IS NOT NULL AND state IN ('running','pending') THEN started_at
|
|
248
|
+
ELSE :now END
|
|
249
|
+
WHEN :state IN ('done','failed') THEN started_at
|
|
250
|
+
WHEN :state='pending' AND :retryAt IS NOT NULL THEN started_at
|
|
251
|
+
WHEN :state='pending' AND state IN ('running','pending') AND :current > 0 THEN started_at
|
|
252
|
+
ELSE NULL END,
|
|
253
|
+
finished_at=CASE
|
|
254
|
+
WHEN :state='done' THEN CASE WHEN state='done' AND finished_at IS NOT NULL THEN finished_at ELSE :now END
|
|
240
255
|
WHEN :state='failed' THEN CASE WHEN state='failed' AND finished_at IS NOT NULL THEN finished_at ELSE :now END
|
|
241
256
|
ELSE NULL END
|
|
242
257
|
WHERE project_id=:projectId AND kind=:stage`)
|
|
@@ -3,6 +3,7 @@ import { z } from "zod";
|
|
|
3
3
|
import { internalError } from "../../kernel/errors.js";
|
|
4
4
|
import { subtitleModelDir } from "../../kernel/paths.js";
|
|
5
5
|
import { projectLanguage } from "../../kernel/ports/languages.js";
|
|
6
|
+
import { editSettings } from "../../kernel/ports/settings-places.js";
|
|
6
7
|
import { SubtitleMismatch } from "../../kernel/ports/subtitles.js";
|
|
7
8
|
import { narrationAliasesOf } from "../admission/rules.js";
|
|
8
9
|
import { captionFont, captionFontDeps } from "../fonts/coverage.js";
|
|
@@ -54,7 +55,7 @@ const cuesSchema = z.object({
|
|
|
54
55
|
export function readTimingWords(deps, context, view, purpose) {
|
|
55
56
|
const timing = readyOutput(view, { workKey: "subtitles:timing", role: "subtitle_words" });
|
|
56
57
|
if (timing === undefined)
|
|
57
|
-
throw new Error(`The narration's word timing, which ${purpose}, is missing.
|
|
58
|
+
throw new Error(`The narration's word timing, which ${purpose}, is missing. Press Make the video again in the project's Video section, then Try again.`);
|
|
58
59
|
return wordsSchema.parse(JSON.parse(readFileSync(outputPath(deps.paths, context.work.projectId, timing.output.path), "utf8")));
|
|
59
60
|
}
|
|
60
61
|
export async function executeSubtitleRecipe(deps, context, piece) {
|
|
@@ -73,7 +74,7 @@ export async function executeSubtitleRecipe(deps, context, piece) {
|
|
|
73
74
|
async function timing(deps, context, piece, snapshot) {
|
|
74
75
|
const audio = await revisionAudio(deps, context, snapshot.view);
|
|
75
76
|
if (audio.length === 0)
|
|
76
|
-
throw new Error(
|
|
77
|
+
throw new Error(`Captions and the YouTube description need narration audio, but this project has none. Turn captions off in ${editSettings.captions} and the YouTube description off in ${editSettings.extras}, or turn narration on, then Try again.`);
|
|
77
78
|
const alignSubtitles = deps.alignSubtitles;
|
|
78
79
|
if (alignSubtitles === undefined)
|
|
79
80
|
throw new Error(internalError("the caption timing tool is missing from this build"));
|
|
@@ -121,7 +122,7 @@ async function timing(deps, context, piece, snapshot) {
|
|
|
121
122
|
offset += segment.seconds;
|
|
122
123
|
}
|
|
123
124
|
if (captionCues(words).length === 0)
|
|
124
|
-
throw new Error(
|
|
125
|
+
throw new Error(`None of the narration could be matched to the article text, so captions can't be timed. The narration must be in the project's language (${editSettings.language}); if you uploaded your own audio, make sure it reads the article text, then Try again.`);
|
|
125
126
|
context.signal.throwIfAborted();
|
|
126
127
|
const output = preparedText(deps, context, piece, "subtitle_words", "subtitles.json", JSON.stringify({ key: piece.logicalFingerprint ?? piece.fingerprint, words, omissions }));
|
|
127
128
|
await publishResult(deps, context, piece, [output], { words, omissions }, output.asset);
|
|
@@ -133,7 +134,7 @@ async function cues(deps, context, piece, snapshot) {
|
|
|
133
134
|
let value;
|
|
134
135
|
if (manual !== undefined) {
|
|
135
136
|
if (plan.work.find((one) => one.key === piece.key)?.disposition === "review")
|
|
136
|
-
throw new Error(
|
|
137
|
+
throw new Error(`Your edited captions were written for an older version of the narration. Look them over in ${editSettings.captions} and save, then Try again.`);
|
|
137
138
|
const audio = await revisionAudio(deps, context, view);
|
|
138
139
|
const errors = validateCues(manual.cues, audio.reduce((sum, segment) => sum + segment.seconds, 0));
|
|
139
140
|
if (errors.length > 0)
|
|
@@ -143,7 +144,7 @@ async function cues(deps, context, piece, snapshot) {
|
|
|
143
144
|
else {
|
|
144
145
|
const output = readyOutput(view, { workKey: "subtitles:timing" });
|
|
145
146
|
if (output === undefined)
|
|
146
|
-
throw new Error("Caption timing is missing.
|
|
147
|
+
throw new Error("Caption timing is missing. Press Make the video again in the project's Video section, then Try again.");
|
|
147
148
|
const timed = wordsSchema.parse(JSON.parse(readFileSync(outputPath(deps.paths, context.work.projectId, output.output.path), "utf8")));
|
|
148
149
|
value = { cues: [...captionCues(timed.words)], omissions: timed.omissions };
|
|
149
150
|
}
|
|
@@ -160,7 +161,7 @@ async function files(deps, context, piece, snapshot) {
|
|
|
160
161
|
throw new Error(internalError("captions are off for this version of the project"));
|
|
161
162
|
const row = donePiece(view, "subtitles:cues");
|
|
162
163
|
if (row?.piece.payload === null || row?.piece.payload === undefined)
|
|
163
|
-
throw new Error("The captions haven't been prepared yet.
|
|
164
|
+
throw new Error("The captions haven't been prepared yet. Press Make the video again in the project's Video section, then Try again.");
|
|
164
165
|
const value = cuesSchema.parse(JSON.parse(row.piece.payload));
|
|
165
166
|
const previous = view.outputs.find((one) => one.output.role === "subtitle_font" && one.selected && one.available);
|
|
166
167
|
const previousFiles = view.pieces.find((one) => one.key === "subtitles:files" &&
|
|
@@ -318,19 +319,19 @@ retry) {
|
|
|
318
319
|
: `${clock(mismatch.at)} into the ${segment} narration, in narration chunk ${String(at + 1)} of ${String(chunks.length)} (it starts "${opening(chunk.spokenText)}")`;
|
|
319
320
|
const expected = mismatch.expected === "" ? "the end of the text" : `"${mismatch.expected}…"`;
|
|
320
321
|
const heard = mismatch.heard === "" ? "no more speech" : `"${mismatch.heard.toLowerCase()}…"`;
|
|
321
|
-
const stopped = `
|
|
322
|
+
const stopped = `Captions stopped matching the audio at ${where}. The text expected ${expected} but the audio has ${heard}`;
|
|
322
323
|
if (retry !== undefined) {
|
|
323
324
|
const n = String(retry.chunk);
|
|
324
325
|
return retry.try === undefined
|
|
325
|
-
? `${stopped} Slopify already recorded narration chunk ${n} again ${String(narrationRetryLimit)} times and the audio still doesn't match there, so the voice probably reads something its own way (a year, an abbreviation or a name). Listen at ${clock(mismatch.at)}: if words are missing or wrong, reword that sentence in
|
|
326
|
-
: `${stopped} The voice probably skipped or garbled words there, so Slopify is recording narration chunk ${n} again by itself (try ${String(retry.try)} of ${String(narrationRetryLimit)}) and carries on with the video when it's done. If this message is still here in a few minutes,
|
|
326
|
+
? `${stopped} Slopify already recorded narration chunk ${n} again ${String(narrationRetryLimit)} times and the audio still doesn't match there, so the voice probably reads something its own way (a year, an abbreviation or a name). Listen at ${clock(mismatch.at)}: if words are missing or wrong, reword that sentence in narration chunk ${n} in ${editSettings.narration}, save, then Continue the run. If it says them right, use Download diagnostics in Settings and report it.`
|
|
327
|
+
: `${stopped} The voice probably skipped or garbled words there, so Slopify is recording narration chunk ${n} again by itself (try ${String(retry.try)} of ${String(narrationRetryLimit)}) and carries on with the video when it's done. If this message is still here in a few minutes, make narration chunk ${n} again in ${editSettings.narration}, then Continue the run.`;
|
|
327
328
|
}
|
|
328
329
|
const fix = chunk === undefined
|
|
329
|
-
? `Listen at ${clock(mismatch.at)}: if words are missing or wrong,
|
|
330
|
-
: `Listen at ${clock(mismatch.at)}: if words are missing or wrong,
|
|
330
|
+
? `Listen at ${clock(mismatch.at)}: if words are missing or wrong, make that part again in ${editSettings.narration}, then Continue the run.`
|
|
331
|
+
: `Listen at ${clock(mismatch.at)}: if words are missing or wrong, make narration chunk ${String(at + 1)} again in ${editSettings.narration}, then Continue the run.`;
|
|
331
332
|
// A voice that reads a year, an abbreviation or a name its own way reads it the same way
|
|
332
333
|
// again, so a remake would cost a chunk and fail here once more.
|
|
333
|
-
const same = "If it says them right, only another way (a year, an abbreviation or a name read its own way),
|
|
334
|
+
const same = "If it says them right, only another way (a year, an abbreviation or a name read its own way), making it again gives the same reading: use Download diagnostics in Settings and report it.";
|
|
334
335
|
return `${stopped} The recording there probably skips or changes words. ${fix} ${same}`;
|
|
335
336
|
}
|
|
336
337
|
function comparable(text) {
|
|
@@ -2,6 +2,7 @@ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { internalError } from "../../kernel/errors.js";
|
|
5
|
+
import { createRows, editSettings } from "../../kernel/ports/settings-places.js";
|
|
5
6
|
import { masterFile, masterReport } from "../loudness/loudnorm.js";
|
|
6
7
|
import { masterGoal } from "../loudness/model.js";
|
|
7
8
|
import { allocateAsset, discardPreparedAssets, sealAsset } from "../storage/assets.js";
|
|
@@ -35,7 +36,7 @@ export async function executeVoicesRecipe(deps, context, piece) {
|
|
|
35
36
|
const { words } = readTimingWords(deps, context, view, "the chapter markers come from");
|
|
36
37
|
const audio = await revisionAudio(deps, context, view, { levelled: true });
|
|
37
38
|
if (audio.length === 0)
|
|
38
|
-
throw new Error(
|
|
39
|
+
throw new Error(`The MP3 and M4B files need narration audio, but this project has none. Turn narration on in ${editSettings.narration}, or untick Also make MP3 and M4B files under Speakers (${createRows.narration}, or ${editSettings.narration}), then Try again.`);
|
|
39
40
|
const totalSeconds = audio.reduce((sum, segment) => sum + segment.seconds, 0);
|
|
40
41
|
const chapters = audioChapters({
|
|
41
42
|
title: view.revision.config.title,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { internalError } from "../../kernel/errors.js";
|
|
2
2
|
import { withLanguage } from "../../kernel/ports/languages.js";
|
|
3
|
+
import { editSettings } from "../../kernel/ports/settings-places.js";
|
|
3
4
|
import { assembleDescription, checkDescriptionAnswer, descriptionMessages, tagsText, } from "../youtube/answer.js";
|
|
4
5
|
import { defaultDescriptionPrompt } from "../youtube/model.js";
|
|
5
6
|
import { keepsShape, titleShape } from "../youtube/titles.js";
|
|
@@ -26,11 +27,11 @@ export async function executeYoutubeRecipe(deps, context, providers, piece) {
|
|
|
26
27
|
const config = view.revision.config;
|
|
27
28
|
const llm = config.llm;
|
|
28
29
|
if (llm === undefined || llm.provider.trim() === "" || llm.model.trim() === "")
|
|
29
|
-
throw new Error(
|
|
30
|
+
throw new Error(`The YouTube description needs an AI model, and none is chosen. Choose a Service and a Model in ${editSettings.writer}, then Try again.`);
|
|
30
31
|
const { words } = readTimingWords(deps, context, view, "the YouTube description's chapters come from");
|
|
31
32
|
const passages = transcriptPassages(words);
|
|
32
33
|
if (passages.length === 0)
|
|
33
|
-
throw new Error("The narration's word timing holds no words, so there is nothing to write the YouTube description from.
|
|
34
|
+
throw new Error("The narration's word timing holds no words, so there is nothing to write the YouTube description from. Press Make the video again in the project's Video section, then Try again.");
|
|
34
35
|
// The same timeline the timing walked: silence at the start, intro, gaps, body, outro and
|
|
35
36
|
// silence at the end.
|
|
36
37
|
const audio = await revisionAudio(deps, context, view);
|
|
@@ -33,9 +33,14 @@ export async function checkReadiness(deps, snapshot, view) {
|
|
|
33
33
|
if (provider === undefined || !readinessIsUsable(provider.readiness)) {
|
|
34
34
|
fields.push({
|
|
35
35
|
field: choice.family,
|
|
36
|
+
// Said under the step's name on the project page ("Images: …"), in the person's words:
|
|
37
|
+
// a service Slopify does not offer here (a sample's own) is changed in the project's
|
|
38
|
+
// settings, one it offers is set up under Keys & services.
|
|
36
39
|
message: provider !== undefined && provider.readiness.kind !== "keyed" && provider.readiness.issue
|
|
37
40
|
? provider.readiness.issue
|
|
38
|
-
:
|
|
41
|
+
: provider === undefined
|
|
42
|
+
? "This project's service isn't one Slopify can use here. Choose another in Edit settings, then try again."
|
|
43
|
+
: "Its service isn't set up yet. Add it in Settings → Keys & services, then try again.",
|
|
39
44
|
});
|
|
40
45
|
continue;
|
|
41
46
|
}
|
|
@@ -72,7 +77,10 @@ export function localReadiness(deps, snapshot, view, providers, catalogue) {
|
|
|
72
77
|
message: "The configured CLI command changed. Check readiness again before rebuilding.",
|
|
73
78
|
});
|
|
74
79
|
if (provider?.readiness.kind === "keyed" && !hasKey(deps.db, provider.id))
|
|
75
|
-
fields.push({
|
|
80
|
+
fields.push({
|
|
81
|
+
field: choice.family,
|
|
82
|
+
message: "Its key isn't saved yet. Add it in Settings → Keys & services, then try again.",
|
|
83
|
+
});
|
|
76
84
|
if (choice.voice !== undefined &&
|
|
77
85
|
!listVoices(deps.db).some((row) => row.provider === choice.provider && row.voiceId === choice.voice))
|
|
78
86
|
fields.push({ field: "audio.voice", message: "Choose a saved voice before rebuilding." });
|
|
@@ -88,7 +96,7 @@ export function localReadiness(deps, snapshot, view, providers, catalogue) {
|
|
|
88
96
|
if (maximum !== undefined && input.text.length > maximum)
|
|
89
97
|
fields.push({
|
|
90
98
|
field: `work.${recipe.key}.text`,
|
|
91
|
-
message: "This saved
|
|
99
|
+
message: "This saved request is longer than the voice model takes now. Make this part of the narration again, so it is split into parts that fit.",
|
|
92
100
|
});
|
|
93
101
|
}
|
|
94
102
|
else if (input.kind === "llm") {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { documentIndex } from "../../kernel/ports/llm-documents.js";
|
|
2
|
+
import { editSettings } from "../../kernel/ports/settings-places.js";
|
|
2
3
|
import { researchDocuments } from "./documents.js";
|
|
3
4
|
import { briefText } from "./planner.js";
|
|
4
5
|
// An editorial pass over every sub-agent's output that selects and organizes the findings; it
|
|
@@ -41,9 +42,9 @@ export function endsWithSources(text) {
|
|
|
41
42
|
// but cannot be used is a failed attempt, so the wrapper retries it.
|
|
42
43
|
export function sourcedAnswer(who, text) {
|
|
43
44
|
if (text.trim() === "") {
|
|
44
|
-
return `The AI model returned an empty answer for ${who}. Try again; if it keeps happening, choose a different model in
|
|
45
|
+
return `The AI model returned an empty answer for ${who}. Try again; if it keeps happening, choose a different model in ${editSettings.writer}.`;
|
|
45
46
|
}
|
|
46
47
|
return endsWithSources(text)
|
|
47
48
|
? undefined
|
|
48
|
-
: `The AI model's answer for ${who} had no sources. Research needs web sources: Try again, or choose a model with web search in
|
|
49
|
+
: `The AI model's answer for ${who} had no sources. Research needs web sources: Try again, or choose a model with web search in ${editSettings.writer}.`;
|
|
49
50
|
}
|
|
@@ -20,5 +20,5 @@ export const closedReviewSentences = {
|
|
|
20
20
|
superseded: "This review was replaced: its item was made again or edited, and the new one waits for its own verdict. Call needs_attention to see it.",
|
|
21
21
|
"replaced-pending": "This review was replaced: its item was made again or edited, so it takes no verdict, and no new review waits for it yet. One opens once the new item is made; an edit saved but not continued makes none until the run is continued. There is nothing to answer here: call needs_attention to see what is waiting now.",
|
|
22
22
|
canceled: "This review's run was canceled, so nothing waits on it and it takes no verdict: a canceled run's reviews never pass. Call needs_attention to see what is waiting now.",
|
|
23
|
-
trashed: "This review's project is in the Trash, so nothing waits on it and it takes no verdict. Restore the project in Settings →
|
|
23
|
+
trashed: "This review's project is in the Trash, so nothing waits on it and it takes no verdict. Restore the project in Settings → Trash; the review then waits for its verdict again.",
|
|
24
24
|
};
|
|
@@ -4,7 +4,7 @@ import { pendingAdmitted } from "../admission/admitted-work.js";
|
|
|
4
4
|
import { liveProject, oneProject, stageStandingsByProject } from "../admission/repo.js";
|
|
5
5
|
import { checkpointStageSchema } from "../checkpoints/schema.js";
|
|
6
6
|
import { heldForItsDay } from "../schedules/prepare.js";
|
|
7
|
-
import {
|
|
7
|
+
import { settledProjects } from "../uploads/repo.js";
|
|
8
8
|
import { projectsWaitingOnAgents } from "./external-waiting.js";
|
|
9
9
|
const gateRow = z.object({
|
|
10
10
|
project_id: z.string(),
|
|
@@ -18,7 +18,7 @@ const gateRow = z.object({
|
|
|
18
18
|
const projectRow = z.object({ project_id: z.string() });
|
|
19
19
|
// Every project's holds, or one project's alone when `projectId` is given. A caller that has
|
|
20
20
|
// already read the stage standings (`stageStandingsByProject`) or the uploaded projects that
|
|
21
|
-
// read Done (`
|
|
21
|
+
// read Done (`settledProjects`) passes them; one that lists what it can read passes `skip`,
|
|
22
22
|
// given each checkpoint row that can't be read instead of failing them all.
|
|
23
23
|
export function projectHolds(db, scope = {}) {
|
|
24
24
|
const { projectId, skip } = scope;
|
|
@@ -74,8 +74,9 @@ export function projectHolds(db, scope = {}) {
|
|
|
74
74
|
.map((row) => projectRow.parse(row).project_id);
|
|
75
75
|
const agents = projectsWaitingOnAgents(db, projectId);
|
|
76
76
|
const runs = heldRuns(db, projectId);
|
|
77
|
-
// An uploaded project with nothing admitted
|
|
78
|
-
|
|
77
|
+
// An uploaded project with nothing admitted, or a finished one an edit left only outdated,
|
|
78
|
+
// reads Done (`edge/http/projects.ts`).
|
|
79
|
+
const settled = scope.settled ?? settledProjects(db, projectId);
|
|
79
80
|
const waitingForAgent = new Set([...agents].filter((id) => !reachedTheirs.has(id) && runs.get(id)?.held !== true));
|
|
80
81
|
const stopped = new Set([...runs]
|
|
81
82
|
.filter(([, run]) => run.stuck)
|