@gentbajko/slopify 3.21.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.
@@ -0,0 +1,12 @@
1
+ # Slopify 3.22.0
2
+
3
+ Released 13 October 2026.
4
+
5
+ - **Names keep their pronunciation in possessives.** "Gygax's", "Lolth's" and every other possessive of a name in the Pronunciation Glossary were read without the glossary's pronunciation. They now get it, with the right ending sound added.
6
+ - **Hyphenated names work in the glossary.** Names like "Arach-Tinilith" or "Catti-brie" with one pronunciation per part were refused; they are now used.
7
+ - **Blank glossary entries are no longer reported as errors.** When the article leaves a name's pronunciation blank ("—"), the Pronunciation tab now says how many names are read as written, instead of listing them as broken entries.
8
+ - **A project's page no longer scrolls into an empty screen.** On wide screens, scrolling outside the columns of a project (most of all on Video) could slide the whole page up past its content.
9
+
10
+ ## Upgrading from 3.21.0
11
+
12
+ - The new glossary rules apply to projects made from now on. Projects already made keep the old rules, so nothing already voiced becomes outdated.
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "id": "3.22.0",
4
+ "title": "Slopify 3.22.0",
5
+ "version": "3.22.0",
6
+ "date": "2026-10-13"
7
+ },
2
8
  {
3
9
  "id": "3.21.0",
4
10
  "title": "Slopify 3.21.0",
@@ -70,6 +70,7 @@ export const runDraftSchema = z.object({
70
70
  })
71
71
  .optional(),
72
72
  narrationAliases: narrationAliasesSchema.optional(),
73
+ pronunciationRules: z.union([z.literal(1), z.literal(2)]).optional(),
73
74
  sharedGlossary: z
74
75
  .array(z.object({
75
76
  term: z.string().min(1).max(200),
@@ -5,6 +5,7 @@ import { resolveChannelId, setProjectChannel } from "../channels/repo.js";
5
5
  import { defaultDocumentTheme } from "../document/model.js";
6
6
  import { earlierEpisodesFor } from "../episodes/related.js";
7
7
  import { listNarrationAliases } from "../narration/aliases-library.js";
8
+ import { currentPronunciationRules } from "../narration/pronunciation.js";
8
9
  import { collectSharedGlossary } from "../narration/shared-glossary.js";
9
10
  import { admitInitialRevision } from "../rebuild/runtime-admission.js";
10
11
  import { adoptBaseline } from "../revisions/adopt.js";
@@ -60,6 +61,7 @@ export function startRun(deps, draft, rendered, retainStaged = false, templates)
60
61
  rendered,
61
62
  ...(shared.length === 0 ? {} : { sharedGlossary: shared }),
62
63
  ...(aliases.length === 0 ? {} : { narrationAliases: aliases }),
64
+ pronunciationRules: currentPronunciationRules,
63
65
  ...(earlier === undefined ? {} : { earlierEpisodes: earlier }),
64
66
  };
65
67
  const project = {
@@ -15,7 +15,7 @@ export function chunkingFingerprintOf(chunking) {
15
15
  export function narrationChunkTexts(source, chunking, anchors) {
16
16
  return anchoredChunks(source, chunking, anchors?.chunkingFingerprint === chunkingFingerprintOf(chunking) ? anchors.texts : undefined);
17
17
  }
18
- export function pronunciationChunks(source, chunking, entries, overriddenKeys = new Set(), sources = {}, anchors) {
18
+ export function pronunciationChunks(source, chunking, entries, overriddenKeys = new Set(), sources = {}, anchors, possessives = false) {
19
19
  const occurrences = new Map();
20
20
  const usedKeys = new Set();
21
21
  const nextKey = (text) => {
@@ -67,7 +67,7 @@ export function pronunciationChunks(source, chunking, entries, overriddenKeys =
67
67
  })),
68
68
  ].sort((a, b) => a.start - b.start);
69
69
  let blockedIndex = 0;
70
- const matches = pronunciationMatches(source, entries).filter((match) => {
70
+ const matches = pronunciationMatches(source, entries, possessives).filter((match) => {
71
71
  while ((blocked[blockedIndex]?.end ?? Infinity) <= match.start)
72
72
  blockedIndex++;
73
73
  return (blocked[blockedIndex]?.start ?? Infinity) >= match.end;
@@ -1,6 +1,15 @@
1
1
  import { remark } from "remark";
2
2
  import remarkGfm from "remark-gfm";
3
3
  import { remembered } from "../../kernel/remembered.js";
4
+ export const currentPronunciationRules = 2;
5
+ export function pronunciationRulesOf(config) {
6
+ return config.pronunciationRules ?? 1;
7
+ }
8
+ // "'s" after a name is English; another language's glossary has no such ending to add.
9
+ export function readsPossessives(config) {
10
+ return (pronunciationRulesOf(config) === 2 &&
11
+ (config.language === undefined || config.language === "en"));
12
+ }
4
13
  // Inworld requires English IPA, not every symbol in Unicode's IPA Extensions block.
5
14
  // https://docs.inworld.ai/tts/capabilities/custom-pronunciation
6
15
  const ipaAtom = "[ˈˌ]?[abdefghijklmnoprstuvwxzæðŋθɑɒɔəɚɛɜɝɡɪɹʃʊʌʒʔɫɾ][\\u0303\\u031a\\u0325\\u0329\\u032a\\u032c\\u032f\\u035c\\u0361ʰʲʷ]*[ːˑ]?";
@@ -50,8 +59,29 @@ function termIdentity(term) {
50
59
  }
51
60
  // `language` is the project's (`kernel/ports/languages.ts`): absent or English accepts the
52
61
  // standard-English IPA Inworld asks for, as it always did; any other accepts full IPA.
53
- export const parsePronunciationGlossary = remembered(8, (markdown, language) => `${language ?? ""}\u0000${markdown}`, parseGlossary);
54
- function parseGlossary(markdown, language) {
62
+ export const parsePronunciationGlossary = remembered(8, (markdown, language, rules) => `${rules ?? currentPronunciationRules}\u0000${language ?? ""}\u0000${markdown}`, parseGlossary);
63
+ // A cell the writer left out on purpose: empty, a dash, or "n/a", with or without slashes.
64
+ const blankIpa = /^(?:\/\s*(?:[—–-]+|n\/a)\s*\/|[—–-]+|n\/a)?$/iu;
65
+ const hyphens = /[-\u2010\u2011]/u;
66
+ // One IPA word per written word. A hyphenated name is one written word, so its parts' IPA words
67
+ // are joined with "." (a syllable break) into the one word Inworld wants between a pair of slashes.
68
+ function pairWords(term, ipa) {
69
+ const words = term.split(" ");
70
+ if (words.length === ipa.length)
71
+ return ipa;
72
+ const parts = words.map((word) => word.split(hyphens).filter((part) => part !== "").length || 1);
73
+ if (parts.reduce((sum, count) => sum + count, 0) !== ipa.length)
74
+ return null;
75
+ let next = 0;
76
+ return parts.map((count) => {
77
+ const joined = ipa.slice(next, next + count).join(".");
78
+ next += count;
79
+ return joined;
80
+ });
81
+ }
82
+ function parseGlossary(markdown, language, rules = currentPronunciationRules) {
83
+ const modern = rules === 2;
84
+ let unpronounced = 0;
55
85
  const english = language === undefined || language === "en";
56
86
  const symbols = english ? ipaSymbols : worldSymbols;
57
87
  const kind = english ? "standard-English IPA" : "IPA";
@@ -65,7 +95,7 @@ function parseGlossary(markdown, language) {
65
95
  const trimmed = row.trim();
66
96
  if (trimmed === "" || /^pronunciation glossary:?\s*$/iu.test(trimmed))
67
97
  continue;
68
- const pair = /^([^:]+):\s*(.+)$/u.exec(trimmed);
98
+ const pair = (modern ? /^([^:]+):\s*(.*)$/u : /^([^:]+):\s*(.+)$/u).exec(trimmed);
69
99
  const term = pair?.[1]?.trim().replace(/\s+/gu, " ");
70
100
  const pronunciation = pair?.[2];
71
101
  if (term === undefined ||
@@ -75,6 +105,10 @@ function parseGlossary(markdown, language) {
75
105
  skip("use Term: /IPA/ or a Term | IPA table");
76
106
  continue;
77
107
  }
108
+ if (modern && blankIpa.test(pronunciation.trim())) {
109
+ unpronounced++;
110
+ continue;
111
+ }
78
112
  const notation = /^(\/[^/]+\/(?:\s+\/[^/]+\/)*)(?:\s+[^/]+)?$/u.exec(pronunciation);
79
113
  if (notation?.[1] === undefined) {
80
114
  skip(`use slash-delimited ${kind}`);
@@ -87,37 +121,54 @@ function parseGlossary(markdown, language) {
87
121
  : "use IPA, not ARPAbet or delivery tags");
88
122
  continue;
89
123
  }
90
- if (term.split(" ").length !== ipa.length) {
91
- skip("supply one IPA word for each written word");
124
+ const paired = modern
125
+ ? pairWords(term, ipa)
126
+ : term.split(" ").length === ipa.length
127
+ ? ipa
128
+ : null;
129
+ if (paired === null) {
130
+ skip(modern
131
+ ? "supply one IPA word for each written word, or for each hyphen-separated part of it"
132
+ : "supply one IPA word for each written word");
92
133
  continue;
93
134
  }
94
135
  const key = termIdentity(term);
95
136
  const previous = entries.get(key);
96
- if (previous !== undefined && previous.ipa.join(" ") !== ipa.join(" ")) {
137
+ if (previous !== undefined && previous.ipa.join(" ") !== paired.join(" ")) {
97
138
  skip("an earlier entry already gives this term a different pronunciation");
98
139
  continue;
99
140
  }
100
141
  if (previous === undefined)
101
- entries.set(key, { term, ipa });
142
+ entries.set(key, { term, ipa: paired });
102
143
  }
103
- return skipped.length === 0
104
- ? { ok: true, entries: [...entries.values()] }
105
- : { ok: true, entries: [...entries.values()], skipped };
144
+ return {
145
+ ok: true,
146
+ entries: [...entries.values()],
147
+ ...(unpronounced === 0 ? {} : { unpronounced }),
148
+ ...(skipped.length === 0 ? {} : { skipped }),
149
+ };
106
150
  }
107
- // What a person reads about the skipped rows: where they are and why, never their text.
108
- export function skippedGlossaryNotice(skipped) {
109
- if (skipped.length === 0)
151
+ // What a person reads about the skipped rows: where they are and why, never their text. Names
152
+ // left without a pronunciation on purpose are not errors, so they get one calm line of their own.
153
+ export function skippedGlossaryNotice(skipped, unpronounced = 0) {
154
+ if (skipped.length === 0 && unpronounced === 0)
110
155
  return null;
156
+ const where = "in the Pronunciation Glossary at the end of the article in Edit project → Article";
157
+ const blank = unpronounced === 0
158
+ ? ""
159
+ : `${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}.`;
160
+ if (skipped.length === 0)
161
+ return `Pronunciation Glossary: ${blank}`;
111
162
  const rows = skipped.map((one) => `entry ${one.row}: ${one.reason}`).join("; ");
112
163
  return (`Pronunciation Glossary: ${skipped.length === 1 ? "1 entry is" : `${skipped.length} entries are`} ` +
113
164
  `skipped and read as ordinary text (${rows}). The rest of the glossary is used. ` +
114
- "To use them, fix those entries in the Pronunciation Glossary at the end of the article in Edit project → Article.");
165
+ `To use them, fix those entries ${where}.${blank === "" ? "" : ` ${blank}`}`);
115
166
  }
116
- export function skippedSpeakerPronunciations(speakers, language) {
167
+ export function skippedSpeakerPronunciations(speakers, language, rules) {
117
168
  return speakers.flatMap((speaker) => {
118
169
  if (!speaker.pronunciations?.trim())
119
170
  return [];
120
- const parsed = parsePronunciationGlossary(speaker.pronunciations, language);
171
+ const parsed = parsePronunciationGlossary(speaker.pronunciations, language, rules);
121
172
  const skipped = parsed.ok ? (parsed.skipped ?? []) : [];
122
173
  return skipped.length === 0 ? [] : [{ speaker: speaker.name.trim() || "A speaker", skipped }];
123
174
  });
@@ -130,8 +181,8 @@ export function skippedSpeakerPronunciationsNotice(rows) {
130
181
  "The rest are used. To use them, fix those entries under Speakers → Pronunciations for that speaker (Play → Narration, or Edit project → Providers).");
131
182
  }
132
183
  // Remembered (`kernel/remembered.ts`): every plan rebuild matches the whole narration again.
133
- export const pronunciationMatches = remembered(1024, (source, entries) => `${source}\u0000${JSON.stringify(entries)}`, matchPronunciations);
134
- function matchPronunciations(source, entries) {
184
+ export const pronunciationMatches = remembered(1024, (source, entries, possessives) => `${possessives === true ? "p" : "-"}\u0000${source}\u0000${JSON.stringify(entries)}`, matchPronunciations);
185
+ function matchPronunciations(source, entries, possessives = false) {
135
186
  if (entries.length === 0)
136
187
  return [];
137
188
  const ordered = [...entries].sort((a, b) => b.term.length - a.term.length);
@@ -140,11 +191,15 @@ function matchPronunciations(source, entries) {
140
191
  .map((word) => word.replace(/[|\\{}()[\]^$+*?.]/gu, "\\$&"))
141
192
  .join("\\s+"));
142
193
  const boundary = "[\\p{L}\\p{M}\\p{N}_\\-\u00ad\u2010\u2011\ufe63\uff0d]";
194
+ // A bare trailing apostrophe ("Iuz' henchmen") is never a possessive here: it can't be told
195
+ // from a closing quote, and a wrong "z" is worse than a name read as written.
143
196
  const expression = new RegExp("(?<!" +
144
197
  boundary +
145
198
  "['’]?)(?:" +
146
199
  patterns.map((pattern) => `(${pattern})`).join("|") +
147
- ")(?!['’]?" +
200
+ ")" +
201
+ (possessives ? "(['’]s)?" : "") +
202
+ "(?!['’]?" +
148
203
  boundary +
149
204
  ")", "giu");
150
205
  const matches = [];
@@ -157,20 +212,35 @@ function matchPronunciations(source, entries) {
157
212
  const entry = ordered.find((_, index) => match[index + 1] !== undefined);
158
213
  if (entry === undefined)
159
214
  throw new Error("Slopify hit an internal error (a pronunciation glossary term has no pronunciation). Try again; if it happens again, use Download diagnostics in Settings and report it.");
160
- matches.push({ start: match.index, end, entry });
215
+ const possessive = possessives ? match[ordered.length + 1] : undefined;
216
+ matches.push({
217
+ start: match.index,
218
+ end,
219
+ entry,
220
+ ...(possessive === undefined ? {} : { possessive }),
221
+ });
161
222
  }
162
223
  return matches;
163
224
  }
164
- export function pronunciationSpans(source, entries) {
225
+ // The sound a possessive adds to the last sound of the name: "iz" after s z sh zh ch j,
226
+ // "s" after the other voiceless sounds, "z" after everything else.
227
+ function withPossessive(ipa) {
228
+ const last = ipa.replace(/[\u0300-\u036fʰʲʷːˑ]+$/u, "").slice(-1);
229
+ if ("sz\u0283\u0292".includes(last))
230
+ return `${ipa}\u026az`;
231
+ return `${ipa}${"ptkfθ".includes(last) ? "s" : "z"}`;
232
+ }
233
+ export function pronunciationSpans(source, entries, possessives = false) {
165
234
  const spans = [];
166
- for (const match of pronunciationMatches(source, entries)) {
167
- const words = source.slice(match.start, match.end).matchAll(/\S+/gu);
168
- for (const [index, word] of Array.from(words).entries()) {
235
+ for (const match of pronunciationMatches(source, entries, possessives)) {
236
+ const words = Array.from(source.slice(match.start, match.end).matchAll(/\S+/gu));
237
+ for (const [index, word] of words.entries()) {
169
238
  const ipa = match.entry.ipa[index];
170
239
  if (ipa === undefined)
171
240
  throw new Error("Slopify hit an internal error (a pronunciation glossary term is missing a word). Try again; if it happens again, use Download diagnostics in Settings and report it.");
172
241
  const start = match.start + word.index;
173
- spans.push({ start, end: start + word[0].length, text: `/${ipa}/` });
242
+ const spoken = match.possessive !== undefined && index === words.length - 1 ? withPossessive(ipa) : ipa;
243
+ spans.push({ start, end: start + word[0].length, text: `/${spoken}/` });
174
244
  }
175
245
  }
176
246
  return spans;
@@ -2,7 +2,7 @@ import { aliasMatches } from "../../kernel/ports/narration-aliases.js";
2
2
  import { narrationAliasesOf, usesNarrationPreparation, usesPronunciationGlossary, } from "../admission/rules.js";
3
3
  import { aliasSpans, withAliasSpans } from "../narration/aliases.js";
4
4
  import { narrationRegenerationToken, normalizeNarrationText, planNarration, } from "../narration/plan.js";
5
- import { pronunciationSpans } from "../narration/pronunciation.js";
5
+ import { pronunciationSpans, readsPossessives, } from "../narration/pronunciation.js";
6
6
  import { prepareRequests } from "../narration/steering.js";
7
7
  import { recipe } from "./recipe-model.js";
8
8
  import { preparationForGroup } from "./recipe-preparation.js";
@@ -25,7 +25,7 @@ export function narrationParts(context, logicalKey, originalText, segment, depen
25
25
  // Aliases first: where one applies, the glossary does not.
26
26
  const aliases = narrationAliasesOf(context.config);
27
27
  const matches = aliasMatches(logicalText, aliases);
28
- const spans = withAliasSpans(pronunciationSpans(logicalText, glossary.entries), aliasSpans(logicalText, aliases));
28
+ const spans = withAliasSpans(pronunciationSpans(logicalText, glossary.entries, readsPossessives(context.config)), aliasSpans(logicalText, aliases));
29
29
  const wholeRequest = segment !== "body" || (context.config.chunking?.mode ?? "whole") === "whole";
30
30
  const choice = context.config.audio;
31
31
  // A retired model's character limit is still its limit: the automatic catalogue check marks
@@ -128,7 +128,7 @@ export function pronunciationFutureValues(context, glossary, article, logicalKey
128
128
  ],
129
129
  ];
130
130
  const clean = normalizeNarrationText(override?.kind === "text" ? override.text : source);
131
- const spans = pronunciationSpans(clean, glossary.entries);
131
+ const spans = pronunciationSpans(clean, glossary.entries, readsPossessives(context.config));
132
132
  return spans.length === 0
133
133
  ? []
134
134
  : [
@@ -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
  };