@gentbajko/slopify 3.20.0 → 3.22.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.
Files changed (37) hide show
  1. package/SUBTITLES.md +10 -2
  2. package/dist/adapters/alignment/cache.js +36 -9
  3. package/dist/adapters/alignment/index.js +16 -4
  4. package/dist/adapters/alignment/prefetch.js +11 -6
  5. package/dist/edge/http/settings.js +6 -1
  6. package/dist/edge/mcp/tools-setup.js +2 -2
  7. package/dist/extension/slopify-studio-chrome.zip +0 -0
  8. package/dist/extension/slopify-studio-firefox.zip +0 -0
  9. package/dist/kernel/ports/subtitles.js +6 -0
  10. package/dist/main.js +6 -2
  11. package/dist/patch-notes/3.21.0.md +11 -0
  12. package/dist/patch-notes/3.22.0.md +12 -0
  13. package/dist/patch-notes/index.json +12 -0
  14. package/dist/slices/admission/schema.js +1 -0
  15. package/dist/slices/admission/start.js +2 -0
  16. package/dist/slices/narration/pronunciation-chunks.js +2 -2
  17. package/dist/slices/narration/pronunciation.js +95 -25
  18. package/dist/slices/rebuild/recipe-audio-parts.js +3 -3
  19. package/dist/slices/rebuild/recipe-audio.js +2 -1
  20. package/dist/slices/rebuild/recipe-text.js +2 -2
  21. package/dist/slices/rebuild/recipe-voices.js +3 -3
  22. package/dist/slices/rebuild/recipe-work.js +4 -4
  23. package/dist/slices/settings/model.js +2 -0
  24. package/dist/slices/settings/playback.js +11 -0
  25. package/dist/tutorials/FAQ.md +1 -1
  26. package/dist/tutorials/Install.md +3 -3
  27. package/dist/tutorials/Other-Languages.md +1 -1
  28. package/dist/tutorials/Play-Video-and-Style.md +1 -1
  29. package/dist/tutorials/Settings-Reference.md +9 -1
  30. package/dist/tutorials/Troubleshooting.md +1 -1
  31. package/dist/tutorials/Video-Editing.md +1 -1
  32. package/dist/web/assets/{index-BKbZIxAq.css → index-BadsoORk.css} +1 -1
  33. package/dist/web/assets/index-X6JyTe2r.js +326 -0
  34. package/dist/web/assets/{pdf-tyJWWiKA.js → pdf-B00spAHg.js} +1 -1
  35. package/dist/web/index.html +2 -2
  36. package/package.json +1 -1
  37. package/dist/web/assets/index-DNPct8pl.js +0 -326
@@ -3,6 +3,7 @@ import { usesNarrationPreparation, usesPronunciationGlossary } from "../admissio
3
3
  import { defaultChunking } from "../narration/chunk.js";
4
4
  import { concatArgs } from "../narration/concat.js";
5
5
  import { normalizeNarrationText } from "../narration/plan.js";
6
+ import { readsPossessives } from "../narration/pronunciation.js";
6
7
  import { pronunciationChunks } from "../narration/pronunciation-chunks.js";
7
8
  import { aliasFutureValues, narrationParts, pronunciationFutureValues, voiceValues, } from "./recipe-audio-parts.js";
8
9
  import { breathValues } from "./recipe-breaths.js";
@@ -15,7 +16,7 @@ import { voiceBodyRecipes } from "./recipe-voices.js";
15
16
  export function bodyNarrationGroups(context, text) {
16
17
  if (text.narrationText === null)
17
18
  return [];
18
- const groups = pronunciationChunks(normalizeNarrationText(text.narrationText), context.config.chunking ?? defaultChunking, usesPronunciationGlossary(context.config) && text.glossary?.ok ? text.glossary.entries : [], new Set(Object.keys(context.content.narrationOverrides)), context.content.narrationSources, context.content.narrationAnchors);
19
+ const groups = pronunciationChunks(normalizeNarrationText(text.narrationText), context.config.chunking ?? defaultChunking, usesPronunciationGlossary(context.config) && text.glossary?.ok ? text.glossary.entries : [], new Set(Object.keys(context.content.narrationOverrides)), context.content.narrationSources, context.content.narrationAnchors, readsPossessives(context.config));
19
20
  // A chunk's key is its text's, so an article edit that makes a chunk say what its narration
20
21
  // edit already said (a line dropped from both) would make a new chunk to voice again. It
21
22
  // keeps the edited chunk's key instead, and with it the narration already made.
@@ -10,7 +10,7 @@ import { plainText } from "../article/plain.js";
10
10
  import { segmentMessages } from "../article/segments.js";
11
11
  import { splitEndMatter } from "../article/split.js";
12
12
  import { withEarlierEpisodes } from "../episodes/related.js";
13
- import { parsePronunciationGlossary } from "../narration/pronunciation.js";
13
+ import { parsePronunciationGlossary, pronunciationRulesOf, } from "../narration/pronunciation.js";
14
14
  import { withSharedGlossary } from "../narration/shared-glossary.js";
15
15
  import { researchDocuments } from "../research/documents.js";
16
16
  import { plannerMessages, subAgentMessages } from "../research/planner.js";
@@ -287,7 +287,7 @@ function glossaryOf(context, endMatter) {
287
287
  return { ok: true, entries: [] };
288
288
  if (endMatter === null)
289
289
  return null;
290
- return withShared(parsePronunciationGlossary(endMatter.glossary, context.config.language), context.config);
290
+ return withShared(parsePronunciationGlossary(endMatter.glossary, context.config.language, pronunciationRulesOf(context.config)), context.config);
291
291
  }
292
292
  // The narration glossary this revision uses, or null while its article is still unwritten.
293
293
  export function narrationGlossary(context) {
@@ -3,7 +3,7 @@ import { splitText } from "../../kernel/ports/text.js";
3
3
  import { narrationAliasesOf, usesNarrationPreparation } from "../admission/rules.js";
4
4
  import { aliasSpans, withAliasSpans } from "../narration/aliases.js";
5
5
  import { normalizeNarrationText } from "../narration/plan.js";
6
- import { parsePronunciationGlossary, pronunciationSpans, } from "../narration/pronunciation.js";
6
+ import { parsePronunciationGlossary, pronunciationRulesOf, pronunciationSpans, readsPossessives, } from "../narration/pronunciation.js";
7
7
  import { withSharedGlossary } from "../narration/shared-glossary.js";
8
8
  import { prepareRequests } from "../narration/steering.js";
9
9
  import { groupTurns } from "../voices/grouping.js";
@@ -193,7 +193,7 @@ function groupParts(context, voices, native, turns, speaker, glossary, script, p
193
193
  row.enabled &&
194
194
  !row.deprecated)?.tts.maxCharacters ?? Math.max(2, logicalText.length * 4);
195
195
  // Narration aliases apply to every voice, the speaker's pronunciations to IPA voices only.
196
- const spans = withAliasSpans(pronunciationSpans(logicalText, entries.entries), aliasSpans(logicalText, narrationAliasesOf(context.config)));
196
+ const spans = withAliasSpans(pronunciationSpans(logicalText, entries.entries, readsPossessives(context.config)), aliasSpans(logicalText, narrationAliasesOf(context.config)));
197
197
  if (preparesTurns(context.config, speaker)) {
198
198
  const prepared = preparationForGroup(context, logicalKey, logicalText, "body", script.dependsOn, maxCharacters, spans, aliasMatches(logicalText, narrationAliasesOf(context.config)), { name: speaker.name.trim(), role: speaker.role });
199
199
  preparations.push(prepared.preparation);
@@ -246,7 +246,7 @@ function speakerGlossary(context, speaker, glossary) {
246
246
  if (!readsIpa(speaker))
247
247
  return { ok: true, entries: [] };
248
248
  const own = speaker.pronunciations?.trim()
249
- ? parsePronunciationGlossary(speaker.pronunciations, context.config.language)
249
+ ? parsePronunciationGlossary(speaker.pronunciations, context.config.language, pronunciationRulesOf(context.config))
250
250
  : { ok: true, entries: [] };
251
251
  if (!own.ok)
252
252
  return {
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { estimateRequests } from "../estimate/index.js";
3
- import { skippedGlossaryNotice, skippedSpeakerPronunciations, skippedSpeakerPronunciationsNotice, } from "../narration/pronunciation.js";
3
+ import { pronunciationRulesOf, skippedGlossaryNotice, skippedSpeakerPronunciations, skippedSpeakerPronunciationsNotice, } from "../narration/pronunciation.js";
4
4
  import { isLocalCliProvider } from "../settings/model.js";
5
5
  import { defaultShortsPrompt, shortsImageUpperBound } from "../shorts/model.js";
6
6
  import { usesVoices } from "../voices/model.js";
@@ -82,12 +82,12 @@ export function planRevisionWork(revision, manifest, catalogue, availableAssetId
82
82
  (revision.config.chunking?.mode ?? "whole") === "whole"
83
83
  ? "Whole-text narration is one logical request. Editing its text rebuilds every provider part of that request."
84
84
  : null,
85
- glossaryNotice: glossary?.ok === true && glossary.skipped !== undefined
86
- ? skippedGlossaryNotice(glossary.skipped)
85
+ glossaryNotice: glossary?.ok === true
86
+ ? skippedGlossaryNotice(glossary.skipped ?? [], glossary.unpronounced)
87
87
  : null,
88
88
  // Only speakers whose voice reads IPA use their pronunciations at all.
89
89
  speakerPronunciationNotice: usesVoices(revision.config) && revision.config.voices !== undefined
90
- ? skippedSpeakerPronunciationsNotice(skippedSpeakerPronunciations(revision.config.voices.speakers.filter(readsIpa), revision.config.language))
90
+ ? skippedSpeakerPronunciationsNotice(skippedSpeakerPronunciations(revision.config.voices.speakers.filter(readsIpa), revision.config.language, pronunciationRulesOf(revision.config)))
91
91
  : null,
92
92
  costs: estimateRequests(priced, catalogue),
93
93
  };
@@ -1,4 +1,5 @@
1
1
  // The provider catalogue and the settings domain types.
2
+ import { defaultEnglishSubtitleModel, } from "../../kernel/ports/subtitles.js";
2
3
  import { defaultLoudness } from "../loudness/model.js";
3
4
  // The three families are the three ports, so the set is named beside them.
4
5
  export { providerFamilies } from "../../kernel/ports/model.js";
@@ -135,4 +136,5 @@ export const defaultSettings = {
135
136
  silenceGapSeconds: 3,
136
137
  appearance: "system",
137
138
  loudness: defaultLoudness,
139
+ subtitleModel: defaultEnglishSubtitleModel,
138
140
  };
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { englishSubtitleModels } from "../../kernel/ports/subtitles.js";
2
3
  import { silenceGapSecondsMax } from "../admission/rules.js";
3
4
  import { loudnessFields } from "../loudness/model.js";
4
5
  import { appearances, defaultSettings } from "./model.js";
@@ -8,8 +9,10 @@ import { readSetting, writeSetting } from "./repo.js";
8
9
  const silenceGapKey = "silenceGapSeconds";
9
10
  const appearanceKey = "appearance";
10
11
  const loudnessKey = "loudness";
12
+ const subtitleModelKey = "subtitleModel";
11
13
  const silenceGapValue = z.number().int().min(0).max(silenceGapSecondsMax);
12
14
  const appearanceValue = z.enum(appearances);
15
+ const subtitleModelValue = z.enum(englishSubtitleModels);
13
16
  const loudnessValue = z.object({
14
17
  enabled: z.boolean(),
15
18
  videoLufs: z.number(),
@@ -20,6 +23,7 @@ export function readSettings(deps) {
20
23
  silenceGapSeconds: parsed(deps, silenceGapKey, silenceGapValue, defaultSettings.silenceGapSeconds),
21
24
  appearance: parsed(deps, appearanceKey, appearanceValue, defaultSettings.appearance),
22
25
  loudness: parsed(deps, loudnessKey, loudnessValue, defaultSettings.loudness),
26
+ subtitleModel: parsed(deps, subtitleModelKey, subtitleModelValue, defaultSettings.subtitleModel),
23
27
  };
24
28
  }
25
29
  export function saveSettings(deps, settings) {
@@ -39,12 +43,19 @@ export function saveSettings(deps, settings) {
39
43
  else {
40
44
  fields.push(...loudnessFields(settings.loudness));
41
45
  }
46
+ if (!subtitleModelValue.safeParse(settings.subtitleModel).success) {
47
+ fields.push({
48
+ field: "subtitleModel",
49
+ message: "Choose Most accurate, Balanced or Smallest in Settings → Production defaults → Subtitle timing model.",
50
+ });
51
+ }
42
52
  if (fields.length > 0) {
43
53
  return { ok: false, fields };
44
54
  }
45
55
  writeSetting(deps.db, silenceGapKey, JSON.stringify(settings.silenceGapSeconds));
46
56
  writeSetting(deps.db, appearanceKey, JSON.stringify(settings.appearance));
47
57
  writeSetting(deps.db, loudnessKey, JSON.stringify(settings.loudness));
58
+ writeSetting(deps.db, subtitleModelKey, JSON.stringify(settings.subtitleModel));
48
59
  return { ok: true, settings };
49
60
  }
50
61
  // A stored value is JSON, and a hand-edited database is the only way it can be anything
@@ -48,7 +48,7 @@ What you write is sent only to the AI providers you pick, because they make the
48
48
 
49
49
  ### Does it work offline?
50
50
 
51
- Partly. The AI steps (article, research, narration, images, thumbnails, descriptions) call providers over the internet, or CLIs that do. Rendering the video, levelling the volume, captions, the style preview and the System voice run on your computer. Captions need a speech model of about 95 MB that is downloaded once, on first use. Patch notes ship with the app and open offline.
51
+ Partly. The AI steps (article, research, narration, images, thumbnails, descriptions) call providers over the internet, or CLIs that do. Rendering the video, levelling the volume, captions, the style preview and the System voice run on your computer. Captions need a speech model of about 378 MB (smaller ones can be chosen in Settings → Production defaults) that is downloaded once, on first use. Patch notes ship with the app and open offline.
52
52
 
53
53
  ## Making and editing videos
54
54
 
@@ -11,7 +11,7 @@ Slopify is a local web app you start from a terminal. It runs on your own comput
11
11
  | Node.js | Version 26 or newer, with npm (it comes with Node). Get it from nodejs.org. |
12
12
  | Operating system | Windows, macOS or Linux for the normal install. The managed Docker install is Linux only (see [Docker](Docker)). |
13
13
  | ffmpeg | Nothing to install: Slopify brings its own copy. See [ffmpeg](#ffmpeg) below. |
14
- | Disk space | Enough for your videos. Every project keeps its video, images, narration and working files. The caption model is a 95 MB download. |
14
+ | Disk space | Enough for your videos. Every project keeps its video, images, narration and working files. The caption model is a 378 MB download (95 MB if you pick Smallest in Settings → Production defaults). |
15
15
  | Keys or CLIs | Text and images can come from Claude Code, Codex or Gemini CLI, or from API keys. Narration can use a voice provider key, or your computer's own speech for free (the System voice; on Linux, install `espeak-ng`). See [Providers and Keys](Providers-and-Keys) and [AI CLIs](AI-CLIs). |
16
16
 
17
17
  ## Choose how to run it
@@ -71,7 +71,7 @@ A flag beats the environment variable, and the variable beats the default.
71
71
  | `--no-open` | `SLOPIFY_NO_OPEN` | opens a browser | Doesn't open a browser tab. For the variable, any value except empty, `0` or `false` counts as on. |
72
72
  | `--autostart` / `--no-autostart` | | asks once | Turns [Start at Login](Start-at-Login) on or off without asking. Useful in scripts. |
73
73
  | | `SLOPIFY_FFMPEG` | the bundled ffmpeg | The full path of another ffmpeg to use. |
74
- | | `SLOPIFY_NO_MODEL_PREFETCH` | downloads at start | Set to `1` to skip fetching the 95 MB caption model when Slopify starts. It is then fetched the first time a video needs captions. |
74
+ | | `SLOPIFY_NO_MODEL_PREFETCH` | downloads at start | Set to `1` to skip fetching the caption model (the one chosen in Settings → Production defaults, 378 MB by default) when Slopify starts. It is then fetched the first time a video needs captions. |
75
75
 
76
76
  Options that only work with `--docker`: `--projects-dir <folder>`, `--host-cli=off` and `--accept-host-cli`. See [Docker](Docker). With `--docker`, `--host` and `--data-dir` are refused, because the Docker version always listens on localhost and keeps its data in a Docker volume.
77
77
 
@@ -112,7 +112,7 @@ The bundled ffmpeg is a separate program under the GPL-3.0-or-later licence. Slo
112
112
 
113
113
  ## The caption model
114
114
 
115
- Captions are timed against the narration by a speech model that runs on your computer, at no cost. The English model (95 MB) is downloaded when Slopify starts, in the background, so a run never waits for it later. It is kept in `<data folder>/models/`. Set `SLOPIFY_NO_MODEL_PREFETCH=1` to skip the download at start. Other languages use a separate model that is downloaded the first time a project in that language needs it.
115
+ Captions are timed against the narration by a speech model that runs on your computer, at no cost. The English model is downloaded when Slopify starts, in the background, so a run never waits for it later: the one chosen in Settings → Production defaults → **Subtitle timing model**, Most accurate (378 MB) unless changed. The Docker image already carries it. It is kept in `<data folder>/models/`. Set `SLOPIFY_NO_MODEL_PREFETCH=1` to skip the download at start. Other languages use a separate model that is downloaded the first time a project in that language needs it.
116
116
 
117
117
  ## Anonymous usage stats
118
118
 
@@ -32,7 +32,7 @@ This applies with the brand kit off too, because language is not styling. Projec
32
32
  | | English | Spanish, German, French, Italian, Portuguese, Dutch, Catalan, Polish, Czech | All other languages |
33
33
  |---|---|---|---|
34
34
  | Article, narration preparation, YouTube description, chapters, tags, shorts titles and hashtags, multi-voice scripts | English | Written in the language | Written in the language |
35
- | Caption timing | Word by word, English model (95 MB) | Word by word, multilingual model (248 MB, downloaded once, on first use) | Sentence by sentence, no model |
35
+ | Caption timing | Word by word, English model (378 MB; 189 MB or 95 MB in Settings → Production defaults) | Word by word, multilingual model (248 MB, downloaded once, on first use) | Sentence by sentence, no model |
36
36
  | Word-by-word Shorts captions | On | On | Off: shorts show whole caption groups |
37
37
  | Cuts that follow the narration | Available | Available | Off: cuts fall every N seconds |
38
38
  | Pronunciation Glossary | Standard-English IPA | Full IPA (ç, ʁ, ø, y, ɲ, x and so on) | Full IPA |
@@ -84,7 +84,7 @@ Below the ambient sound are **Pause between sentences**, **Pause between paragra
84
84
 
85
85
  ## Captions
86
86
 
87
- Captions are timed from your narration on this computer, with no paid API. The first use downloads a speech model of about 95 MB. They need narration: with narration Off, the section says "Turn Audio on to add subtitles."
87
+ Captions are timed from your narration on this computer, with no paid API. The first use downloads a speech model of about 378 MB (Settings → Production defaults → **Subtitle timing model** offers smaller ones). They need narration: with narration Off, the section says "Turn Audio on to add subtitles."
88
88
 
89
89
  | Option | What it does | Default / range |
90
90
  | --- | --- | --- |
@@ -116,7 +116,7 @@ There is nothing to turn on: the address works whenever Slopify runs. See [Agent
116
116
 
117
117
  ## Production defaults
118
118
 
119
- "How new runs pace the narration and level the volume."
119
+ "How new runs pace the narration and level the volume, and which model times English captions."
120
120
 
121
121
  ### Silence between segments
122
122
 
@@ -136,6 +136,14 @@ Videos already started keep their own gap. A value that is not a whole number fr
136
136
 
137
137
  The volume fields show only while the switch is on. A project keeps what it started with; change it there in **Edit project**. Levelling runs on this computer and makes no API calls. Changes save as you make them.
138
138
 
139
+ ### Subtitle timing model
140
+
141
+ | Control | What it does | Default / range |
142
+ |---|---|---|
143
+ | **Subtitle timing model** | The speech model that times English captions on this computer. The same model in three sizes: **Most accurate (378 MB, recommended)**, **Balanced (189 MB)** and **Smallest (95 MB, less accurate)**. Bigger hears the narration more accurately and, on most computers, also runs faster. | Most accurate |
144
+
145
+ The chosen model downloads once, the first time captions are timed (or when Slopify starts, unless `SLOPIFY_NO_MODEL_PREFETCH` is set), and is kept in `<data folder>/models/english-subtitles/`. A model already downloaded stays there when you switch, so switching back costs nothing. Smallest can mishear a line and stop a run with "Subtitles stopped matching the audio" where the narration is fine. Changing the model never marks finished subtitles outdated, and a timing that is already running finishes with the model it started with; the next one uses the new choice. It is a setting of this computer, not of a project. Other languages use their own model and are not affected. Changes save as you make them.
146
+
139
147
  ## Notifications
140
148
 
141
149
  "When a run finishes, fails, or waits for you."
@@ -67,7 +67,7 @@ Slopify ships its own ffmpeg; you do not need to install one. In Docker it is in
67
67
  | "The audio/video export failed (ffmpeg exited with code ...)" | Open **Error details**: it quotes what ffmpeg could not do, for example an input file it could not open. Replace that file (an uploaded image or clip in **Edit project → Images**), or try again. |
68
68
  | "Slopify couldn't measure the loudness of the narration" | Try again. If it happens again, turn off **Level the volume** in **Edit project → Inputs → Pauses and volume**, then download diagnostics and report it. |
69
69
  | "The style preview couldn't render ... Press Render again." | Press **Render again**. If it fails again, download diagnostics and report it. |
70
- | "The free subtitle model could not be downloaded" / "download was interrupted" / "failed download verification" | The 95 MB caption model downloads on first use. Check your connection and try again. `SLOPIFY_NO_MODEL_PREFETCH` only skips the download at start; captions still need it. |
70
+ | "The free subtitle model could not be downloaded" / "download was interrupted" / "failed download verification" | The caption model (378 MB by default, the size chosen in Settings → Production defaults → **Subtitle timing model**) downloads on first use. Check your connection, then **Continue** the run, or pick a smaller model there. `SLOPIFY_NO_MODEL_PREFETCH` only skips the download at start; captions still need it. |
71
71
  | "Captions can't be edited until the current narration has finished and its subtitle timing is ready." | Let the narration and subtitle timing finish, then edit captions. |
72
72
  | "The saved subtitle timing doesn't match the current narration." | Do what the message says: make the subtitle timing again (More → make it again), then edit the captions. |
73
73
  | "Subtitles recovered after missing narration (N)" under the video | Some transcript passages could not be matched to the audio and were left out of the captions. The audio is fine. Review those passages before sharing. |
@@ -90,7 +90,7 @@ With **Level the volume** on, the bed is mixed and ducked first, then the whole
90
90
  | **Subtitle font size** | How big burned-in captions are. Larger is easier to read on phones but covers more of the picture. | 48; 16 to 120 |
91
91
  | **Subtitle position** | Where burned-in captions sit: **Top**, **Upper-middle**, **Center**, **Lower-middle** or **Bottom**. Move them up when pictures have detail near the bottom. | Bottom |
92
92
 
93
- Captions are timed from your narration on your computer, with no paid API. The first use downloads a speech model of about 95 MB. Subtitle files carry no font, size or position, because players style them. Changing the font, size or position remakes only the video.
93
+ Captions are timed from your narration on your computer, with no paid API. The first use downloads a speech model of about 378 MB (Settings → Production defaults → **Subtitle timing model** offers smaller ones). Subtitle files carry no font, size or position, because players style them. Changing the font, size or position remakes only the video.
94
94
 
95
95
  Subtitles need narration: with Audio off the section says "Turn Audio on to add subtitles." With Video off (an audio export), only subtitle files are offered.
96
96