@gentbajko/slopify 3.21.0 → 3.22.1
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/alignment/text.js +7 -1
- package/dist/extension/slopify-studio-chrome.zip +0 -0
- package/dist/extension/slopify-studio-firefox.zip +0 -0
- package/dist/patch-notes/3.22.0.md +12 -0
- package/dist/patch-notes/3.22.1.md +9 -0
- package/dist/patch-notes/index.json +12 -0
- package/dist/slices/admission/schema.js +1 -0
- package/dist/slices/admission/start.js +2 -0
- package/dist/slices/narration/pronunciation-chunks.js +2 -2
- package/dist/slices/narration/pronunciation.js +95 -25
- package/dist/slices/rebuild/recipe-audio-parts.js +3 -3
- package/dist/slices/rebuild/recipe-audio.js +2 -1
- package/dist/slices/rebuild/recipe-text.js +2 -2
- package/dist/slices/rebuild/recipe-voices.js +3 -3
- package/dist/slices/rebuild/recipe-work.js +4 -4
- package/dist/web/assets/{index-BKbZIxAq.css → index-BadsoORk.css} +1 -1
- package/dist/web/assets/index-X6JyTe2r.js +326 -0
- package/dist/web/assets/{pdf-DH2f4NNx.js → pdf-B00spAHg.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-wwRT6LH3.js +0 -326
|
@@ -7,7 +7,10 @@ import { cardinal, numberForms } from "../../kernel/ports/number-words.js";
|
|
|
7
7
|
// `formsOf` is the language's normaliser (`multilingual.ts` passes its own); the default is
|
|
8
8
|
// English, and `empty` is the sentence for a text with nothing to say.
|
|
9
9
|
export function speechWords(text, observed = "", aliases = [], formsOf = wordForms, empty = "Local subtitles currently require an English transcript with spoken words.") {
|
|
10
|
-
|
|
10
|
+
// An alias spoken as IPA ("Gygax's" said /ˈɡaɪɡæksɪz/) is a pronunciation, not other
|
|
11
|
+
// words: like a glossary entry, the recording is matched against the written word.
|
|
12
|
+
const worded = aliases.filter((alias) => !spokenAsIpa(alias.spoken));
|
|
13
|
+
const matches = worded.length === 0 ? [] : aliasMatches(text, worded);
|
|
11
14
|
const words = [];
|
|
12
15
|
for (const token of text.matchAll(/\S+/g)) {
|
|
13
16
|
const textWord = token[0];
|
|
@@ -57,6 +60,9 @@ export function speechWords(text, observed = "", aliases = [], formsOf = wordFor
|
|
|
57
60
|
// the written text again without the aliases, and "&" alone says nothing.
|
|
58
61
|
return words.map(({ text: shown, spoken, forms, aliased }) => forms.length > 1 || aliased ? { text: shown, spoken, forms } : { text: shown, spoken });
|
|
59
62
|
}
|
|
63
|
+
function spokenAsIpa(spoken) {
|
|
64
|
+
return /^\/[^/]+\/(?:\s+\/[^/]+\/)*$/u.test(spoken.trim());
|
|
65
|
+
}
|
|
60
66
|
// The form of a word the recording seems to say, else its default.
|
|
61
67
|
export function respoken(word, observed, formsOf = wordForms) {
|
|
62
68
|
if (word.forms === undefined)
|
|
Binary file
|
|
Binary file
|
|
@@ -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.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Slopify 3.22.1
|
|
2
|
+
|
|
3
|
+
Released 13 October 2026.
|
|
4
|
+
|
|
5
|
+
- **Subtitles work with a pronunciation alias.** A narration alias spoken as IPA (for example "Gygax's" said /ˈɡaɪɡæksɪz/) stopped the subtitles with "Local subtitles currently support English speech and Latin-script names only." Subtitles now time that word from its written form, the same way they do for a Pronunciation Glossary entry.
|
|
6
|
+
|
|
7
|
+
## Upgrading from 3.22.0
|
|
8
|
+
|
|
9
|
+
- A run stopped by that message goes on with Try again; nothing is voiced again.
|
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"id": "3.22.1",
|
|
4
|
+
"title": "Slopify 3.22.1",
|
|
5
|
+
"version": "3.22.1",
|
|
6
|
+
"date": "2026-10-13"
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"id": "3.22.0",
|
|
10
|
+
"title": "Slopify 3.22.0",
|
|
11
|
+
"version": "3.22.0",
|
|
12
|
+
"date": "2026-10-13"
|
|
13
|
+
},
|
|
2
14
|
{
|
|
3
15
|
"id": "3.21.0",
|
|
4
16
|
"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
|
-
|
|
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
|
-
|
|
91
|
-
|
|
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(" ") !==
|
|
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
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
109
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
};
|