@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,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { editSettings } from "../../kernel/ports/settings-places.js";
|
|
2
3
|
import { keywordSpecs } from "./specs.js";
|
|
3
4
|
// The keyword digest: one call to the project's AI model reads the article once and writes every
|
|
4
5
|
// digest keyword (`{{Slopify_Summary}}`, `{{Slopify_Hook}}`…), so a run that uses several pays for
|
|
@@ -39,7 +40,7 @@ const digestSchema = z.object({
|
|
|
39
40
|
tags: listOrText,
|
|
40
41
|
});
|
|
41
42
|
export function checkDigest(text) {
|
|
42
|
-
const fix =
|
|
43
|
+
const fix = `Use Try again; if it keeps happening, choose another model in ${editSettings.writer}.`;
|
|
43
44
|
const parsed = digestSchema.safeParse(jsonObjectOf(text));
|
|
44
45
|
if (!parsed.success)
|
|
45
46
|
return {
|
|
@@ -167,7 +167,7 @@ export function placedProblems(bodies, values) {
|
|
|
167
167
|
field,
|
|
168
168
|
message: place === "title"
|
|
169
169
|
? problem.message
|
|
170
|
-
: `${problem.message} Change the prompt in
|
|
170
|
+
: `${problem.message} Change the prompt in Prompts, or pick another one.`,
|
|
171
171
|
});
|
|
172
172
|
}
|
|
173
173
|
return problems;
|
|
@@ -180,7 +180,7 @@ export function textEntryProblems(body, category) {
|
|
|
180
180
|
? [
|
|
181
181
|
{
|
|
182
182
|
field: category,
|
|
183
|
-
message: `{{Slopify_Release_Date}} can't be used in
|
|
183
|
+
message: `{{Slopify_Release_Date}} can't be used in an ${category} that says these exact words: those are narrated as written, and the release date is only filled in when a step asks the AI model. In Prompts → Intros and outros, set this ${category}'s What the narrator reads to Let the AI write it from these instructions, or remove the keyword.`,
|
|
184
184
|
},
|
|
185
185
|
]
|
|
186
186
|
: [];
|
|
@@ -38,7 +38,7 @@ export const keywordSpecs = [
|
|
|
38
38
|
{
|
|
39
39
|
name: "Slopify_Series",
|
|
40
40
|
filler: "facts",
|
|
41
|
-
fills: "The channel's series brief from
|
|
41
|
+
fills: "The channel's series brief, from the channel's page under Name and look. A line with an empty brief is left out.",
|
|
42
42
|
example: "Calm histories of how things were made, one invention at a time.",
|
|
43
43
|
available: atStart,
|
|
44
44
|
places: everywhere,
|
|
@@ -88,7 +88,7 @@ export const keywordSpecs = [
|
|
|
88
88
|
{
|
|
89
89
|
name: "Slopify_Release_Date",
|
|
90
90
|
filler: "release",
|
|
91
|
-
fills: "The release date from the posting plan. A line with no date is left out; moving the date later
|
|
91
|
+
fills: "The release date from the posting plan. A line with no date is left out; moving the date later makes nothing again.",
|
|
92
92
|
example: "Thursday 22 October 2026",
|
|
93
93
|
available: "When the step runs.",
|
|
94
94
|
places: everywhere,
|
|
@@ -138,8 +138,8 @@ export const keywordSpecs = [
|
|
|
138
138
|
{
|
|
139
139
|
name: "Slopify_Brand",
|
|
140
140
|
filler: "facts",
|
|
141
|
-
fills: "The brand kit's caption and title
|
|
142
|
-
example: "
|
|
141
|
+
fills: "The brand kit's caption and title colors and font names. A line with an empty kit is left out.",
|
|
142
|
+
example: "Colors: #f5c542, #101010. Fonts: Barlow Condensed",
|
|
143
143
|
available: atStart,
|
|
144
144
|
places: pictures,
|
|
145
145
|
cost: "free",
|
|
@@ -229,7 +229,7 @@ export const keywordSpecs = [
|
|
|
229
229
|
{
|
|
230
230
|
name: "Slopify_Mood",
|
|
231
231
|
filler: "scenes",
|
|
232
|
-
fills: "The feeling of this picture's moment, for its light and
|
|
232
|
+
fills: "The feeling of this picture's moment, for its light and color.",
|
|
233
233
|
example: "tense",
|
|
234
234
|
available: sceneAvailable,
|
|
235
235
|
places: pictures,
|
|
@@ -37,7 +37,7 @@ function brandText(brand) {
|
|
|
37
37
|
if (brand === undefined)
|
|
38
38
|
return "";
|
|
39
39
|
const parts = [
|
|
40
|
-
...(brand.colours.length === 0 ? [] : [`
|
|
40
|
+
...(brand.colours.length === 0 ? [] : [`Colors: ${brand.colours.join(", ")}.`]),
|
|
41
41
|
...(brand.fonts.length === 0 ? [] : [`Fonts: ${brand.fonts.join(", ")}.`]),
|
|
42
42
|
];
|
|
43
43
|
return parts.join(" ");
|
|
@@ -17,7 +17,7 @@ const itemsMax = 10_000;
|
|
|
17
17
|
const sectionWords = {
|
|
18
18
|
prompts: "prompts",
|
|
19
19
|
entries: "intros or outros",
|
|
20
|
-
documentThemes: "PDF
|
|
20
|
+
documentThemes: "PDF looks",
|
|
21
21
|
aliases: "narration aliases",
|
|
22
22
|
};
|
|
23
23
|
// Only what the editor saves travels: ids, keywords and dates are the install's own.
|
|
@@ -93,7 +93,7 @@ export function readLibraryFile(text, section) {
|
|
|
93
93
|
if (!top.success)
|
|
94
94
|
return {
|
|
95
95
|
ok: false,
|
|
96
|
-
message: `it isn't a Slopify library file. Pick a .json file made by Export on
|
|
96
|
+
message: `it isn't a Slopify library file. Pick a .json file made by Export on Prompts, the PDF looks or Pronunciation.`,
|
|
97
97
|
};
|
|
98
98
|
if (top.data.version > libraryFileVersion)
|
|
99
99
|
return {
|
|
@@ -104,7 +104,7 @@ export function readLibraryFile(text, section) {
|
|
|
104
104
|
if (!Array.isArray(list) || list.length === 0)
|
|
105
105
|
return {
|
|
106
106
|
ok: false,
|
|
107
|
-
message: `it holds no ${what}. Import it on the
|
|
107
|
+
message: `it holds no ${what}. Import it on the page it was exported from.`,
|
|
108
108
|
};
|
|
109
109
|
if (list.length > itemsMax)
|
|
110
110
|
return {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { renameSync, rmSync } from "node:fs";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import { editSettings } from "../../kernel/ports/settings-places.js";
|
|
3
4
|
import { runFfmpeg } from "../video/ffmpeg.js";
|
|
4
5
|
// ceiling: below -70 LUFS (the filter's absolute gate) or under 0.4 s (one measuring block)
|
|
5
6
|
// there is nothing to measure: a piece that quiet or short is left as it is.
|
|
@@ -97,7 +98,7 @@ export async function measure(run, input, goal, chain = "") {
|
|
|
97
98
|
});
|
|
98
99
|
const measured = parseMeasured(stderr);
|
|
99
100
|
if (measured === undefined)
|
|
100
|
-
throw new Error(
|
|
101
|
+
throw new Error(`Slopify couldn't measure the loudness of the narration (ffmpeg printed no measurement). Try again; if it happens again, turn off Level the volume in ${editSettings.sound}, then use Download diagnostics in Settings and report it.`);
|
|
101
102
|
return measured;
|
|
102
103
|
}
|
|
103
104
|
export async function measureFile(run, path, goal) {
|
|
@@ -9,10 +9,11 @@
|
|
|
9
9
|
// The ambient bed and the shorts' music are mixed before the master and ducked by the levelled
|
|
10
10
|
// voice, so they keep their place under it.
|
|
11
11
|
//
|
|
12
|
-
// Browser-safe:
|
|
12
|
+
// Browser-safe: Create, Edit settings and Settings convert the same numbers.
|
|
13
13
|
//
|
|
14
14
|
// A run without the setting has no `loudness` at all, which is what every project saved before
|
|
15
15
|
// it was, and every recipe leaves it out of its fingerprint then.
|
|
16
|
+
import { createRows, editSettings } from "../../kernel/ports/settings-places.js";
|
|
16
17
|
// YouTube and Spotify play everything back at about -14 LUFS, so a video mastered there is
|
|
17
18
|
// neither turned down nor left quiet. Audiobook shops ask for about -18 to -20 LUFS with more
|
|
18
19
|
// headroom, so the listening files sit lower.
|
|
@@ -87,7 +88,7 @@ export function loudnessProblem(lufs, target) {
|
|
|
87
88
|
db >= loudnessDbMin - 1e-9 &&
|
|
88
89
|
db <= loudnessDbMax + 1e-9
|
|
89
90
|
? undefined
|
|
90
|
-
: `The volume must be between ${String(loudnessDbMin)} dB and +${String(loudnessDbMax)} dB of the recommended level, in steps of ${String(loudnessDbStep)} dB. Change it under Level the volume (
|
|
91
|
+
: `The volume must be between ${String(loudnessDbMin)} dB and +${String(loudnessDbMax)} dB of the recommended level, in steps of ${String(loudnessDbStep)} dB. Change it under Level the volume (${createRows.video}, ${editSettings.sound}, or Settings → Production defaults).`;
|
|
91
92
|
}
|
|
92
93
|
export function loudnessFields(settings) {
|
|
93
94
|
if (settings === undefined)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { mkdirSync } from "node:fs";
|
|
3
3
|
import { isAbsolute, join, resolve as resolvePath } from "node:path";
|
|
4
|
+
import { editSettings } from "../../kernel/ports/settings-places.js";
|
|
4
5
|
import { measurePiece } from "../narration/pauses.js";
|
|
5
6
|
import { runFfmpeg } from "../video/ffmpeg.js";
|
|
6
7
|
import { sentenceLevel } from "./sentence-level-model.js";
|
|
@@ -243,7 +244,7 @@ export async function meterFile(run, file) {
|
|
|
243
244
|
else if (code === 0)
|
|
244
245
|
resolve();
|
|
245
246
|
else
|
|
246
|
-
reject(new Error(`Slopify couldn't measure a narration piece's sentences (ffmpeg said: ${errors.trim().split("\n").at(-1) ?? "nothing"}). Try again; if it happens again, turn off Level each sentence in
|
|
247
|
+
reject(new Error(`Slopify couldn't measure a narration piece's sentences (ffmpeg said: ${errors.trim().split("\n").at(-1) ?? "nothing"}). Try again; if it happens again, turn off Level each sentence in ${editSettings.sound}, then use Download diagnostics in Settings and report it.`));
|
|
247
248
|
});
|
|
248
249
|
});
|
|
249
250
|
return meter.readings();
|
|
@@ -110,3 +110,16 @@ export function dollarsText(dollars, unpriced = 0) {
|
|
|
110
110
|
const more = `${String(unpriced)} model${unpriced === 1 ? "" : "s"} with no listed price`;
|
|
111
111
|
return dollars <= 0 ? `${more} (price unknown)` : `${base}, plus ${more}`;
|
|
112
112
|
}
|
|
113
|
+
// The same estimate as the end of a sentence, "this costs …": "about $0.04 at list price",
|
|
114
|
+
// and for models with no listed price "an unknown amount: 3 models have no listed price".
|
|
115
|
+
export function costPhrase(dollars, unpriced = 0) {
|
|
116
|
+
const base = dollars <= 0
|
|
117
|
+
? "nothing"
|
|
118
|
+
: `${dollars < 0.01 ? "less than a cent" : `about $${dollars.toFixed(2)}`} at list price`;
|
|
119
|
+
if (unpriced === 0)
|
|
120
|
+
return base;
|
|
121
|
+
const models = `${String(unpriced)} model${unpriced === 1 ? "" : "s"}`;
|
|
122
|
+
return dollars <= 0
|
|
123
|
+
? `an unknown amount: ${models} ${unpriced === 1 ? "has" : "have"} no listed price`
|
|
124
|
+
: `${base}, plus an unknown amount for ${models} with no listed price`;
|
|
125
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
export const usageSlots = ["llm", "audio", "images", "animate"];
|
|
4
4
|
export const usageKinds = ["template", "schedule", "draft", "project"];
|
|
5
5
|
export const slotLabels = {
|
|
6
|
-
llm: "
|
|
6
|
+
llm: "Writing model",
|
|
7
7
|
audio: "Voice model",
|
|
8
8
|
images: "Image model",
|
|
9
9
|
animate: "Animation model",
|
|
@@ -94,7 +94,7 @@ export async function switchRetiredModel(deps, catalogue, request) {
|
|
|
94
94
|
if (row.version !== row.head)
|
|
95
95
|
return {
|
|
96
96
|
ok: false,
|
|
97
|
-
message: "This schedule runs an older version of its template. In
|
|
97
|
+
message: "This schedule runs an older version of its template. In Calendar → Schedules, choose Edit, pick the template again so it runs the latest version, then save.",
|
|
98
98
|
};
|
|
99
99
|
return switchTemplate(deps, catalogue, request, String(row.templateId));
|
|
100
100
|
}
|
|
@@ -117,7 +117,7 @@ function switchTemplate(deps, catalogue, request, templateId) {
|
|
|
117
117
|
return {
|
|
118
118
|
ok: false,
|
|
119
119
|
message: error.reason === "missing-prompt"
|
|
120
|
-
? `The template "${template.name}" uses a prompt or intro/outro that was deleted, so it cannot be saved. Open it in
|
|
120
|
+
? `The template "${template.name}" uses a prompt or intro/outro that was deleted, so it cannot be saved. Open it in Templates → Edit template, choose another, and save.`
|
|
121
121
|
: changedMeanwhile,
|
|
122
122
|
};
|
|
123
123
|
}
|
|
@@ -230,7 +230,7 @@ async function switchProject(deps, catalogue, request) {
|
|
|
230
230
|
? " because the project changed meanwhile"
|
|
231
231
|
: "fields" in saved && saved.fields?.[0] !== undefined
|
|
232
232
|
? ` (${saved.fields[0].message})`
|
|
233
|
-
: ""}. Open the project
|
|
233
|
+
: ""}. Open the project and change it in Edit settings.`,
|
|
234
234
|
};
|
|
235
235
|
}
|
|
236
236
|
// "Switch all": every listed use that has a suggestion and no reason to hold back, one by one.
|
|
@@ -13,6 +13,17 @@ export { slotLabels, usageKinds, usageSlots } from "./model.js";
|
|
|
13
13
|
function familyOf(slot) {
|
|
14
14
|
return slot === "llm" ? "llm" : slot === "audio" ? "tts" : "image";
|
|
15
15
|
}
|
|
16
|
+
// A Create draft's name as Create's header says it: its title once every keyword in it is
|
|
17
|
+
// filled, else "Untitled draft". Never a raw {{Topic}}.
|
|
18
|
+
function draftName(title, values) {
|
|
19
|
+
const filled = title
|
|
20
|
+
.replace(/\{\{\s*([^{}]+?)\s*\}\}/g, (keyword, name) => {
|
|
21
|
+
const value = values[name]?.trim() ?? "";
|
|
22
|
+
return value === "" ? keyword : value;
|
|
23
|
+
})
|
|
24
|
+
.trim();
|
|
25
|
+
return filled === "" || filled.includes("{{") ? "Untitled draft" : filled;
|
|
26
|
+
}
|
|
16
27
|
// The model choices a Play form (a draft or a template) makes.
|
|
17
28
|
export function formChoices(form) {
|
|
18
29
|
const edit = form.videoEdit;
|
|
@@ -155,7 +166,7 @@ export function choiceSites(db) {
|
|
|
155
166
|
choices: formChoices(template.document.form),
|
|
156
167
|
blocked: head === row.template_version
|
|
157
168
|
? null
|
|
158
|
-
: `This schedule runs an older version of the template "${template.name}". In
|
|
169
|
+
: `This schedule runs an older version of the template "${template.name}". In Calendar → Schedules, choose Edit, pick the template again so it runs the latest version, then save.`,
|
|
159
170
|
});
|
|
160
171
|
}
|
|
161
172
|
for (const row of draftRows(db)) {
|
|
@@ -167,7 +178,7 @@ export function choiceSites(db) {
|
|
|
167
178
|
sites.push({
|
|
168
179
|
kind: "draft",
|
|
169
180
|
id: row.id,
|
|
170
|
-
name: row.title.
|
|
181
|
+
name: draftName(row.title, document.form.values),
|
|
171
182
|
choices: formChoices(document.form),
|
|
172
183
|
blocked: null,
|
|
173
184
|
});
|
|
@@ -2,6 +2,7 @@ import { mkdirSync, rmSync } from "node:fs";
|
|
|
2
2
|
import { open, stat } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { errorText } from "../../kernel/errors.js";
|
|
5
|
+
import { editSettings } from "../../kernel/ports/settings-places.js";
|
|
5
6
|
import { runFfmpeg } from "../video/ffmpeg.js";
|
|
6
7
|
import { breathRate, scanBreaths, softenChunk } from "./breaths-detect.js";
|
|
7
8
|
// Soften breaths, made in the join (`breaths-model.ts` says why there). Each narration piece is
|
|
@@ -39,7 +40,7 @@ export async function softenBreaths(run, files, directory) {
|
|
|
39
40
|
catch (error) {
|
|
40
41
|
if (run.signal.aborted)
|
|
41
42
|
throw error;
|
|
42
|
-
throw new Error(`Soften breaths could not process narration piece ${String(index + 1)} (${errorText(error)}), so the narration can't be joined together. Use Try again; if it fails again, turn off Soften breaths in
|
|
43
|
+
throw new Error(`Soften breaths could not process narration piece ${String(index + 1)} (${errorText(error)}), so the narration can't be joined together. Use Try again; if it fails again, turn off Soften breaths in ${editSettings.sound} and use Save changes, then use Download diagnostics in Settings and report it.`, { cause: error });
|
|
43
44
|
}
|
|
44
45
|
finally {
|
|
45
46
|
rmSync(raw, { force: true });
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mkdirSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { errorText } from "../../kernel/errors.js";
|
|
4
|
+
import { editSettings } from "../../kernel/ports/settings-places.js";
|
|
4
5
|
import { runFfmpeg } from "../video/ffmpeg.js";
|
|
5
6
|
import { softenBreaths } from "./breaths.js";
|
|
6
7
|
import { deEsser } from "./breaths-model.js";
|
|
@@ -37,7 +38,7 @@ export async function deEssPieces(run, files, directory) {
|
|
|
37
38
|
catch (error) {
|
|
38
39
|
if (run.signal.aborted)
|
|
39
40
|
throw error;
|
|
40
|
-
throw new Error(`Soften S sounds could not process narration piece ${String(index + 1)} (${errorText(error)}), so the narration can't be joined together. Use Try again; if it fails again, turn off Soften S sounds in
|
|
41
|
+
throw new Error(`Soften S sounds could not process narration piece ${String(index + 1)} (${errorText(error)}), so the narration can't be joined together. Use Try again; if it fails again, turn off Soften S sounds in ${editSettings.sound} and use Save changes, then use Download diagnostics in Settings and report it.`, { cause: error });
|
|
41
42
|
}
|
|
42
43
|
written.push(output);
|
|
43
44
|
}
|
|
@@ -9,9 +9,10 @@
|
|
|
9
9
|
// A paragraph's minimum is separate and 0 (the sentence minimum only) unless set: the pause
|
|
10
10
|
// between paragraphs is mostly the narration prompt's and the voice's to make.
|
|
11
11
|
//
|
|
12
|
-
// Browser-safe:
|
|
12
|
+
// Browser-safe: Create and Edit settings read these limits. A run without either has neither on
|
|
13
13
|
// its config, which is what every project saved before them was, and the joins keep their
|
|
14
14
|
// fingerprints then.
|
|
15
|
+
import { createRows, editSettings } from "../../kernel/ports/settings-places.js";
|
|
15
16
|
// ceiling: a quarter second reads brisk, 0.35 to 0.45 natural or documentary, 0.5 to 0.8 an
|
|
16
17
|
// audiobook's relaxed pace, and above 0.8 sleep content; beyond 2 s it stops sounding like a
|
|
17
18
|
// pause between sentences.
|
|
@@ -33,7 +34,7 @@ export function pauseProblem(value, name) {
|
|
|
33
34
|
value <= pauseSecondsMax &&
|
|
34
35
|
Math.abs(steps - Math.round(steps)) < 1e-6
|
|
35
36
|
? undefined
|
|
36
|
-
: `The pause between ${name} must be between 0 and ${String(pauseSecondsMax)} seconds, in steps of ${String(pauseSecondsStep)}. Change it under
|
|
37
|
+
: `The pause between ${name} must be between 0 and ${String(pauseSecondsMax)} seconds, in steps of ${String(pauseSecondsStep)}. Change it under ${createRows.video}, or ${editSettings.sound}.`;
|
|
37
38
|
}
|
|
38
39
|
export function pauseFields(config) {
|
|
39
40
|
const fields = [];
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { parsePronunciationGlossary, pronunciationMatches, pronunciationSpans, } from "./pronunciation.js";
|
|
2
|
+
import { libraryGlossary } from "./pronunciations-library.js";
|
|
3
|
+
import { withSharedGlossary } from "./shared-glossary.js";
|
|
4
|
+
// Measured over the 30 finished videos in this install: "Do'Urden" spreads 0.36 in both videos
|
|
5
|
+
// that say it and is heard several ways, while "Gygax" — a rare name the aligner always scores
|
|
6
|
+
// low — never spreads past 0.29 in the 26 videos that say it, so a low score on its own never
|
|
7
|
+
// flags. Eight mentions is the fewest from which the 10th and 90th percentiles mean anything.
|
|
8
|
+
export const heardMinMentions = 8;
|
|
9
|
+
export const heardSpreadFlag = 0.3;
|
|
10
|
+
// The entries narration pronounces with, assembled the way `recipe-text.ts` assembles them.
|
|
11
|
+
// Parsed twice: once under the rules the project narrates with, which is what actually guides
|
|
12
|
+
// the voice, and once under the current rules, which is the fullest list of names the sources
|
|
13
|
+
// name at all — a hyphenated term rules 1 had to skip is still a name this video says.
|
|
14
|
+
export function pronunciationEntries(sources) {
|
|
15
|
+
const under = (rules) => {
|
|
16
|
+
const own = parsePronunciationGlossary(sources.glossary, sources.language, rules);
|
|
17
|
+
const list = libraryGlossary(sources.list, sources.language, rules);
|
|
18
|
+
return withSharedGlossary(list.ok ? list.entries : [], withSharedGlossary(own.ok ? own.entries : [], sources.shared));
|
|
19
|
+
};
|
|
20
|
+
return { guiding: under(sources.rules), listed: under(2) };
|
|
21
|
+
}
|
|
22
|
+
// Delivery tags are the preparation's instructions to the voice, not words it reads, so a name
|
|
23
|
+
// inside one is not a mention. Blanked, not removed, so every offset still points at the text.
|
|
24
|
+
function withoutTags(source) {
|
|
25
|
+
return source.replace(/\[[^\]\n]*\]/gu, (tag) => " ".repeat(tag.length));
|
|
26
|
+
}
|
|
27
|
+
// Everything between a pair of slashes already reached the voice as IPA. Whitespace inside is
|
|
28
|
+
// never IPA, which keeps an "and/or … his/her" in ordinary prose from reading as a span.
|
|
29
|
+
function ipaRanges(source) {
|
|
30
|
+
return Array.from(source.matchAll(/\/[^/\s]+\//gu), (match) => [match.index, match.index + match[0].length]);
|
|
31
|
+
}
|
|
32
|
+
// The names to look for: every term the sources name, and — because a two-word term never
|
|
33
|
+
// matches one of its words said alone ("Belwar Dissengulp" listed, "Belwar" spoken) — each
|
|
34
|
+
// capitalised word of a longer term as a name of its own.
|
|
35
|
+
//
|
|
36
|
+
// A one-word name of four letters or more that the text also writes in lower case is an
|
|
37
|
+
// ordinary English word here, not a name: the pooled copy of the other projects' glossaries
|
|
38
|
+
// carries "Hall" (of "Mithral Hall"), "Quick" and "Claw", and an article that also says "halls"
|
|
39
|
+
// or "quickly" is not mispronouncing them. An ending of one or two letters counts as the same
|
|
40
|
+
// word, which is why "quickly" speaks for "Quick"; more than that does not, so "torment" leaves
|
|
41
|
+
// the god Torm alone. A term of several words, or a short one like "Bel", is never read this way.
|
|
42
|
+
function lowercaseWords(source) {
|
|
43
|
+
const words = new Set();
|
|
44
|
+
for (const match of source.matchAll(/(?<![\p{L}\p{N}'’-])\p{Ll}[\p{L}'’-]*/gu)) {
|
|
45
|
+
const word = match[0];
|
|
46
|
+
words.add(word);
|
|
47
|
+
if (word.length > 1)
|
|
48
|
+
words.add(word.slice(0, -1));
|
|
49
|
+
if (word.length > 2)
|
|
50
|
+
words.add(word.slice(0, -2));
|
|
51
|
+
}
|
|
52
|
+
return words;
|
|
53
|
+
}
|
|
54
|
+
function ordinaryWord(term, lowercased) {
|
|
55
|
+
return (term.length >= 4 &&
|
|
56
|
+
!term.includes(" ") &&
|
|
57
|
+
// Only a name written with a capital can be mistaken for one: a term the glossary itself
|
|
58
|
+
// writes in lower case ("drow", "lich", "tiefling") is a common word by its own spelling,
|
|
59
|
+
// and is told apart by `proper` rather than left out.
|
|
60
|
+
/^\p{Lu}/u.test(term) &&
|
|
61
|
+
lowercased.has(term.toLowerCase()));
|
|
62
|
+
}
|
|
63
|
+
function nameCandidates(entries, source) {
|
|
64
|
+
const lowercased = lowercaseWords(source);
|
|
65
|
+
const candidates = new Map();
|
|
66
|
+
for (const entry of entries) {
|
|
67
|
+
if (ordinaryWord(entry.term, lowercased))
|
|
68
|
+
continue;
|
|
69
|
+
candidates.set(entry.term.toLowerCase(), entry);
|
|
70
|
+
}
|
|
71
|
+
for (const entry of entries) {
|
|
72
|
+
const words = entry.term.split(" ");
|
|
73
|
+
if (words.length < 2)
|
|
74
|
+
continue;
|
|
75
|
+
for (const [index, word] of words.entries()) {
|
|
76
|
+
const key = word.toLowerCase();
|
|
77
|
+
if (word.length < 3 || !/^\p{Lu}/u.test(word))
|
|
78
|
+
continue;
|
|
79
|
+
if (candidates.has(key) || ordinaryWord(word, lowercased))
|
|
80
|
+
continue;
|
|
81
|
+
candidates.set(key, { term: word, ipa: [entry.ipa[index] ?? ""] });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return [...candidates.values()];
|
|
85
|
+
}
|
|
86
|
+
function countUnguided(source, entries, guided) {
|
|
87
|
+
const text = withoutTags(source);
|
|
88
|
+
const rows = new Map();
|
|
89
|
+
// Possessives are always matched here, whatever the project's rules: a possessive the rules
|
|
90
|
+
// never wrapped is exactly the mention that reached the voice as written.
|
|
91
|
+
for (const match of pronunciationMatches(text, nameCandidates(entries, text), true)) {
|
|
92
|
+
if (guided.some(([from, to]) => match.start < to && match.end > from))
|
|
93
|
+
continue;
|
|
94
|
+
// Named by what the text says, not by the glossary's spelling of the term.
|
|
95
|
+
const said = text.slice(match.start, match.end);
|
|
96
|
+
const name = said.replace(/['’]s$/u, "");
|
|
97
|
+
const row = rows.get(name) ?? { plain: 0, possessive: 0, proper: /^\p{Lu}/u.test(name) };
|
|
98
|
+
if (match.possessive === undefined)
|
|
99
|
+
row.plain++;
|
|
100
|
+
else
|
|
101
|
+
row.possessive++;
|
|
102
|
+
rows.set(name, row);
|
|
103
|
+
}
|
|
104
|
+
const total = (row) => row.plain + row.possessive;
|
|
105
|
+
const names = [...rows.entries()]
|
|
106
|
+
.map(([name, row]) => ({ name, ...row }))
|
|
107
|
+
.sort((a, b) => Number(b.proper) - Number(a.proper) ||
|
|
108
|
+
total(b) - total(a) ||
|
|
109
|
+
a.name.localeCompare(b.name, "en"));
|
|
110
|
+
return {
|
|
111
|
+
names,
|
|
112
|
+
mentions: names.reduce((sum, row) => sum + total(row), 0),
|
|
113
|
+
proper: names.filter((row) => row.proper).reduce((sum, row) => sum + total(row), 0),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
// Before the narration runs: the names this text says that the voice will have to invent. What
|
|
117
|
+
// narration will wrap is `pronunciationSpans` itself, under the project's own rules, so a
|
|
118
|
+
// project that narrates without possessives is told about its possessives and a newer one is not.
|
|
119
|
+
export function namesAhead(text, sources, possessives) {
|
|
120
|
+
const { guiding, listed } = pronunciationEntries(sources);
|
|
121
|
+
const plain = withoutTags(text);
|
|
122
|
+
const guided = pronunciationSpans(plain, guiding, possessives).map((span) => [span.start, span.end]);
|
|
123
|
+
return countUnguided(plain, listed, guided);
|
|
124
|
+
}
|
|
125
|
+
// After the render: the same names, counted from the voice script, which is exactly the text the
|
|
126
|
+
// voice was sent. A name with no IPA in front of it there was guessed, whatever the glossary says.
|
|
127
|
+
export function namesNarrated(script, sources) {
|
|
128
|
+
const { listed } = pronunciationEntries(sources);
|
|
129
|
+
return countUnguided(script, listed, ipaRanges(withoutTags(script)));
|
|
130
|
+
}
|
|
131
|
+
// A word as the aligner read it: "Do'Urden," is the name, "Drizzt's" is the name too.
|
|
132
|
+
function spokenName(text) {
|
|
133
|
+
return text
|
|
134
|
+
.replace(/^[^\p{L}\p{N}]+/u, "")
|
|
135
|
+
.replace(/[^\p{L}\p{N}'’-]+$/u, "")
|
|
136
|
+
.replace(/['’]s$/u, "");
|
|
137
|
+
}
|
|
138
|
+
function percentile(sorted, fraction) {
|
|
139
|
+
const at = Math.round(fraction * (sorted.length - 1));
|
|
140
|
+
return sorted[Math.min(sorted.length - 1, Math.max(0, at))] ?? 0;
|
|
141
|
+
}
|
|
142
|
+
// How the finished narration was heard saying each name, from the confidence the subtitle timing
|
|
143
|
+
// already carries. Nothing here is listened to again and nothing is re-aligned: the numbers are
|
|
144
|
+
// the ones written when the captions were timed.
|
|
145
|
+
export function heardNames(words, sources,
|
|
146
|
+
// The narration as it was written: the corpus the candidates are judged against, so a pooled
|
|
147
|
+
// "Hall" is left alone in a video that also says "hall" (see `nameCandidates`). The words as
|
|
148
|
+
// the aligner wrote them down stand in when the narration itself is not at hand.
|
|
149
|
+
corpus) {
|
|
150
|
+
const { listed } = pronunciationEntries(sources);
|
|
151
|
+
const heardText = corpus ?? words.map((word) => word.text).join(" ");
|
|
152
|
+
const lowercased = lowercaseWords(heardText);
|
|
153
|
+
const wanted = new Map();
|
|
154
|
+
// A word of the timing is one word, so a term of several reaches it a part at a time, and a
|
|
155
|
+
// hyphenated name may arrive whole or in parts. A part that is an ordinary word of this text
|
|
156
|
+
// ("Hall" of "Mithral Hall", in a video that also says "hall") is left out, as it is above.
|
|
157
|
+
for (const entry of nameCandidates(listed, heardText))
|
|
158
|
+
for (const part of [entry.term, ...entry.term.split(/[\s‐‑-]+/u)])
|
|
159
|
+
if (part.length >= 3 && !wanted.has(part.toLowerCase()) && !ordinaryWord(part, lowercased))
|
|
160
|
+
wanted.set(part.toLowerCase(), part);
|
|
161
|
+
const scored = words.flatMap((word) => typeof word.confidence === "number" ? [word.confidence] : []);
|
|
162
|
+
const by = new Map();
|
|
163
|
+
for (const word of words) {
|
|
164
|
+
if (typeof word.confidence !== "number")
|
|
165
|
+
continue;
|
|
166
|
+
const name = wanted.get(spokenName(word.text).toLowerCase());
|
|
167
|
+
if (name === undefined)
|
|
168
|
+
continue;
|
|
169
|
+
by.set(name, [...(by.get(name) ?? []), word.confidence]);
|
|
170
|
+
}
|
|
171
|
+
const names = [...by.entries()]
|
|
172
|
+
.map(([name, values]) => {
|
|
173
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
174
|
+
const low = percentile(sorted, 0.1);
|
|
175
|
+
const high = percentile(sorted, 0.9);
|
|
176
|
+
return {
|
|
177
|
+
name,
|
|
178
|
+
mentions: sorted.length,
|
|
179
|
+
mean: sorted.reduce((sum, value) => sum + value, 0) / sorted.length,
|
|
180
|
+
low,
|
|
181
|
+
high,
|
|
182
|
+
spread: high - low,
|
|
183
|
+
uneven: sorted.length >= heardMinMentions && high - low >= heardSpreadFlag,
|
|
184
|
+
};
|
|
185
|
+
})
|
|
186
|
+
.sort((a, b) => b.spread - a.spread || a.name.localeCompare(b.name, "en"));
|
|
187
|
+
return {
|
|
188
|
+
names,
|
|
189
|
+
uneven: names.filter((row) => row.uneven),
|
|
190
|
+
words: scored.length,
|
|
191
|
+
confidence: scored.length === 0 ? 0 : scored.reduce((sum, value) => sum + value, 0) / scored.length,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
const list = (names, most = 6) => {
|
|
195
|
+
const shown = names.slice(0, most).map((row) => row.name);
|
|
196
|
+
const rest = names.length - shown.length;
|
|
197
|
+
return rest === 0 ? shown.join(", ") : `${shown.join(", ")} and ${String(rest)} more`;
|
|
198
|
+
};
|
|
199
|
+
// The one line before the narration is paid for. Names the names, says what will happen to them,
|
|
200
|
+
// and says the one place that fixes them for every video at once.
|
|
201
|
+
export function namesAheadNotice(result) {
|
|
202
|
+
const proper = result.names.filter((row) => row.proper);
|
|
203
|
+
if (proper.length === 0)
|
|
204
|
+
return null;
|
|
205
|
+
return (`${proper.length === 1 ? "1 name" : `${String(proper.length)} names`} in this article ` +
|
|
206
|
+
`${proper.length === 1 ? "has" : "have"} no pronunciation anywhere, so the voice will guess ` +
|
|
207
|
+
`${proper.length === 1 ? "it" : "them"}: ${list(proper)}. ` +
|
|
208
|
+
`To decide how they are said, add them to Settings → Voices → Name pronunciations, then make ` +
|
|
209
|
+
`the narration.`);
|
|
210
|
+
}
|
|
211
|
+
// The one line on the upload section, after the render: what the voice guessed, and what it was
|
|
212
|
+
// heard saying more than one way. Null when there is nothing to say.
|
|
213
|
+
export function narratedNotice(narrated, heard) {
|
|
214
|
+
const guessed = narrated?.names.filter((row) => row.proper) ?? [];
|
|
215
|
+
const uneven = heard?.uneven ?? [];
|
|
216
|
+
if (guessed.length === 0 && uneven.length === 0)
|
|
217
|
+
return null;
|
|
218
|
+
const parts = [];
|
|
219
|
+
if (guessed.length > 0)
|
|
220
|
+
parts.push(`the voice guessed ${guessed.length === 1 ? "1 name" : `${String(guessed.length)} names`} ` +
|
|
221
|
+
`(${list(guessed)})`);
|
|
222
|
+
if (uneven.length > 0)
|
|
223
|
+
parts.push(`${uneven.length === 1 ? "1 name was" : `${String(uneven.length)} names were`} heard more ` +
|
|
224
|
+
`than one way (${list(uneven)})`);
|
|
225
|
+
return `Names in this narration: ${parts.join(", and ")}. Open the check to see every mention.`;
|
|
226
|
+
}
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { remark } from "remark";
|
|
2
2
|
import remarkGfm from "remark-gfm";
|
|
3
3
|
import { internalError } from "../../kernel/errors.js";
|
|
4
|
+
import { createRows, editSettings } from "../../kernel/ports/settings-places.js";
|
|
4
5
|
import { remembered } from "../../kernel/remembered.js";
|
|
6
|
+
// How long Settings → Voices → Pronunciation's own list may be, and with it the copy a project
|
|
7
|
+
// carries (`slices/narration/pronunciations-library.ts`). Long enough for a channel's whole cast
|
|
8
|
+
// of names.
|
|
9
|
+
export const libraryPronunciationsMax = 100_000;
|
|
5
10
|
export const currentPronunciationRules = 2;
|
|
6
11
|
export function pronunciationRulesOf(config) {
|
|
7
12
|
return config.pronunciationRules ?? 1;
|
|
@@ -154,7 +159,7 @@ function parseGlossary(markdown, language, rules = currentPronunciationRules) {
|
|
|
154
159
|
export function skippedGlossaryNotice(skipped, unpronounced = 0) {
|
|
155
160
|
if (skipped.length === 0 && unpronounced === 0)
|
|
156
161
|
return null;
|
|
157
|
-
const where =
|
|
162
|
+
const where = `in the Pronunciation Glossary at the end of the article in ${editSettings.article}`;
|
|
158
163
|
const blank = unpronounced === 0
|
|
159
164
|
? ""
|
|
160
165
|
: `${unpronounced === 1 ? "1 name has" : `${unpronounced} names have`} no pronunciation given and ${unpronounced === 1 ? "is" : "are"} read as written. To give ${unpronounced === 1 ? "it one" : "them one"}, fill in its IPA ${where}.`;
|
|
@@ -179,7 +184,7 @@ export function skippedSpeakerPronunciationsNotice(rows) {
|
|
|
179
184
|
return null;
|
|
180
185
|
const parts = rows.map((row) => `${row.speaker}: ${row.skipped.map((one) => `entry ${one.row}: ${one.reason}`).join("; ")}`);
|
|
181
186
|
return (`Speaker pronunciations: some entries are skipped and read as ordinary text (${parts.join(". ")}). ` +
|
|
182
|
-
|
|
187
|
+
`The rest are used. To use them, fix those entries under Speakers → Pronunciations for that speaker (${createRows.narration}, or ${editSettings.narration}).`);
|
|
183
188
|
}
|
|
184
189
|
// Remembered (`kernel/remembered.ts`): every plan rebuild matches the whole narration again.
|
|
185
190
|
export const pronunciationMatches = remembered(1024, (source, entries, possessives) => `${possessives === true ? "p" : "-"}\u0000${source}\u0000${JSON.stringify(entries)}`, matchPronunciations);
|