@gentbajko/slopify 4.1.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.
@@ -0,0 +1,94 @@
1
+ import { z } from "zod";
2
+ import { fingerprint } from "../../kernel/runner/work.js";
3
+ import { actors } from "../agents/model.js";
4
+ import { deleteSetting, readJsonSetting, readSetting, writeSetting } from "../settings/repo.js";
5
+ import { libraryPronunciationsMax, parsePronunciationGlossary, } from "./pronunciation.js";
6
+ // Settings → Voices → Name pronunciations: one Pronunciation Glossary of your own, in the same
7
+ // `Term: /ipa/` lines an article's own glossary uses. Its entries come before the article's, so
8
+ // a name keeps the same pronunciation in every video however each article spells it out. A
9
+ // project takes a copy when it starts (and again when Edit project refreshes it), so a change
10
+ // here never alters a project's narration behind its back. Aliases on the Pronunciation page are
11
+ // the other half: they change the words, this changes the sounds.
12
+ export const libraryPronunciationsKey = "narration.pronunciations";
13
+ const savedKey = "narration.pronunciations.saved";
14
+ const savedRow = z
15
+ .object({ at: z.string().min(1).max(40), by: z.enum(actors) })
16
+ .strict()
17
+ .readonly();
18
+ export function readLibraryPronunciations(db) {
19
+ const saved = readJsonSetting(db, savedKey, savedRow, undefined);
20
+ return {
21
+ ...describeLibraryPronunciations(readSetting(db, libraryPronunciationsKey) ?? ""),
22
+ ...(saved === undefined ? {} : { savedAt: saved.at, savedBy: saved.by }),
23
+ };
24
+ }
25
+ export function describeLibraryPronunciations(text) {
26
+ const parsed = parsePronunciationGlossary(text);
27
+ return {
28
+ text,
29
+ terms: parsed.ok ? parsed.entries.length : 0,
30
+ version: versionOf(text),
31
+ ...(parsed.ok && parsed.skipped !== undefined ? { skipped: parsed.skipped } : {}),
32
+ ...(parsed.ok && parsed.unpronounced !== undefined
33
+ ? { unpronounced: parsed.unpronounced }
34
+ : {}),
35
+ };
36
+ }
37
+ // Short enough to pass around and read in a refusal, long enough never to collide.
38
+ export function versionOf(text) {
39
+ return fingerprint({ pronunciations: text.trim() }).slice(0, 16);
40
+ }
41
+ // `version` is the list the save was made from. It is required once a list exists: a save
42
+ // without it, or from an older one, is refused rather than replacing what somebody else wrote
43
+ // in another window or through an agent.
44
+ export function saveLibraryPronunciations(deps, text, version, by = "person") {
45
+ if (typeof text !== "string")
46
+ return {
47
+ ok: false,
48
+ reason: "refused",
49
+ detail: "Write the list as Term: /ipa/ lines, one name per line.",
50
+ };
51
+ if (text.length > libraryPronunciationsMax)
52
+ return {
53
+ ok: false,
54
+ reason: "refused",
55
+ detail: `Keep the list under ${String(libraryPronunciationsMax)} characters; it is ${String(text.length)} now. Remove the names you no longer narrate, then Save.`,
56
+ };
57
+ const current = readLibraryPronunciations(deps.db);
58
+ if (current.text !== "" && version !== current.version)
59
+ return {
60
+ ok: false,
61
+ reason: "conflict",
62
+ detail: conflictDetail(current, version),
63
+ current,
64
+ };
65
+ const trimmed = text.trim();
66
+ if (trimmed === "")
67
+ deleteSetting(deps.db, libraryPronunciationsKey);
68
+ else
69
+ writeSetting(deps.db, libraryPronunciationsKey, trimmed);
70
+ writeSetting(deps.db, savedKey, JSON.stringify({ at: deps.clock.now().toISOString(), by }));
71
+ return { ok: true, list: readLibraryPronunciations(deps.db) };
72
+ }
73
+ // Said to whoever sent the save: the person at the page, or an agent reading a refusal. It
74
+ // names what the list is now and the one step that keeps their own work.
75
+ function conflictDetail(current, version) {
76
+ const who = current.savedBy === "agent"
77
+ ? " by an agent"
78
+ : current.savedBy === "assistant"
79
+ ? " by the assistant"
80
+ : "";
81
+ const when = current.savedAt === undefined ? "" : ` at ${current.savedAt}`;
82
+ const names = `${String(current.terms)} ${current.terms === 1 ? "name" : "names"}`;
83
+ return version === undefined
84
+ ? `The list has ${names} already, saved${who}${when}. Read it first and send your save with the version it gives, so this one does not replace what is there.`
85
+ : `The list changed since this save was prepared: it has ${names} now, saved${who}${when}. Read it again and save with the version it gives; on the page, Save mine anyway keeps what is on screen.`;
86
+ }
87
+ // The entries a project's copy pronounces, under the project's own language and rules. An
88
+ // unparseable list is never fatal: its bad rows are skipped, and the editor is where they are
89
+ // named (the run's notices number the rows of the article's glossary, not of this list).
90
+ export function libraryGlossary(text, language, rules) {
91
+ if (text === undefined || text.trim() === "")
92
+ return { ok: true, entries: [] };
93
+ return parsePronunciationGlossary(text, language, rules);
94
+ }
@@ -12,6 +12,7 @@ import { splitEndMatter } from "../article/split.js";
12
12
  import { withEarlierEpisodes } from "../episodes/related.js";
13
13
  import { withoutSlopifyScene } from "../images/scenes.js";
14
14
  import { parsePronunciationGlossary, pronunciationRulesOf, } from "../narration/pronunciation.js";
15
+ import { libraryGlossary } from "../narration/pronunciations-library.js";
15
16
  import { withSharedGlossary } from "../narration/shared-glossary.js";
16
17
  import { researchDocuments } from "../research/documents.js";
17
18
  import { plannerMessages, subAgentMessages } from "../research/planner.js";
@@ -333,12 +334,23 @@ export function narrationGlossary(context) {
333
334
  const markdown = articleMarkdownOf(context);
334
335
  return glossaryOf(context, markdown === null ? null : splitEndMatter(markdown));
335
336
  }
336
- // The project's own glossary, then the other projects' pronunciations it copied, when it
337
- // shares them. Rows the own glossary had to skip are still reported.
337
+ // Your own pronunciation list first, then the project's own glossary, then the other projects'
338
+ // pronunciations it copied, when it shares them. The list wins so a name is said the same way in
339
+ // every video however this article spelled it out. Rows the own glossary had to skip are still
340
+ // reported; rows the list had to skip are named in its editor, where they can be fixed.
338
341
  function withShared(own, config) {
339
- if (!own.ok || config.audio?.shareGlossary !== true)
342
+ if (!own.ok)
340
343
  return own;
341
- return { ...own, entries: withSharedGlossary(own.entries, config.sharedGlossary) };
344
+ const list = libraryGlossary(config.libraryPronunciations, config.language, pronunciationRulesOf(config));
345
+ const shared = config.audio?.shareGlossary === true ? config.sharedGlossary : undefined;
346
+ if (!list.ok)
347
+ return list;
348
+ if (list.entries.length === 0 && shared === undefined)
349
+ return own;
350
+ return {
351
+ ...own,
352
+ entries: withSharedGlossary(list.entries, withSharedGlossary(own.entries, shared)),
353
+ };
342
354
  }
343
355
  function withSampleScope(prompt, sample) {
344
356
  return sample === undefined || sample.fullScript
@@ -83,7 +83,7 @@ It needs the Inworld TTS-2 model. With several speakers it works per speaker: ea
83
83
 
84
84
  | Option | What it does | Default |
85
85
  | --- | --- | --- |
86
- | **Use the article's pronunciation glossary** | Shown only for voices that read it. Reads names with the IPA in the article's Pronunciation Glossary, such as `Arda: /ˈɑɹdə/`, with no extra AI call. Works with Inworld TTS-2 and TTS-2 Flash. The written text stays unchanged. | On |
86
+ | **Use pronunciations** | Shown only for voices that read IPA. Reads names with the IPA they are given, with no extra AI call: your **Name pronunciations** list first, so a listed name sounds the same in every video, then the article's own Pronunciation Glossary, such as `Arda: /ˈɑɹdə/`. Works with Inworld TTS-2 and TTS-2 Flash. The written text stays unchanged. | On |
87
87
  | **Also use pronunciations from my other projects** | Adds every term from your other projects' glossaries, copied when this project starts. This project's own glossary wins where they differ. Turn it off to use only this article's glossary. | On |
88
88
 
89
89
  With another service or model the tick is not shown; your choice is kept for when you pick an Inworld TTS-2 voice.
@@ -62,6 +62,7 @@ When the narration was spoken as one whole request, the section says so: editing
62
62
 
63
63
  | Control | What it does |
64
64
  | --- | --- |
65
+ | **Use pronunciations** → **Copy the list again** | Copies your **Name pronunciations** (Settings → Voices) into this project again, so a name you corrected there is said the new way here too. A change to the list reaches the project only when you press this. After you save, only the chunks where a changed name appears are spoken again, paid per character. |
65
66
  | **Also use pronunciations from my other projects** → **Update from other projects** | Copies your other projects' Pronunciation Glossary terms into this project again, picking up any added since the last copy. After you save, only the chunks whose words those terms change are spoken again, paid per character. |
66
67
  | **Use your pronunciation list** → **Copy the list again** | Copies your Pronunciation list (Settings → Voices → **Pronunciation**) into this project again. A change to the list reaches the project only when you press this. After you save, only the chunks the changed aliases touch are spoken again. |
67
68
  | **Describe tables and figures in the narration** | The AI writes a short spoken passage for each table, figure, equation and code block. Turning it on costs one AI call per block, and after you save the chunks that contain a block are spoken again (the whole narration when it is spoken as one request). **Leave code out** skips code blocks. |
@@ -1,8 +1,55 @@
1
1
  # Narration Aliases and Glossary
2
2
 
3
- Two tools control how the narrator says words without changing what is written. **Narration aliases** swap a written form for a spoken one, such as `Dr.` said as "Doctor", with any generated voice. The **Pronunciation Glossary** gives exact IPA pronunciations for names, such as `Arda: /ˈɑɹdə/`, with Inworld's TTS-2 voices. In both cases the article, the transcript and the captions keep the written words; only the audio changes.
3
+ Three tools control how the narrator says words without changing what is written. **Name pronunciations** is your own list of names with their IPA, used before anything an article says, so a name sounds the same in every video. **Narration aliases** swap a written form for a spoken one, such as `Dr.` said as "Doctor", with any generated voice. The **Pronunciation Glossary** is the list each article writes for itself, such as `Arda: /ˈɑɹdə/`. IPA is read by Inworld's TTS-2 voices; aliases work with every voice. In all three the article, the transcript and the captions keep the written words; only the audio changes.
4
4
 
5
- **Where to find it:** aliases in **Settings → Voices → Pronunciation**. Both switches are on Create under **Narration → Change → More narration settings**, and in a project's **Edit settings** → **Narration**. A project's glossary is shown on its **Article** section, in the **Pronunciation** tab.
5
+ **Where to find it:** your list in **Settings → Voices → Name pronunciations**, aliases in **Settings → Voices → Pronunciation**. The switches are on Create under **Narration → Change → More narration settings**, and in a project's **Edit settings** → **Narration**. A project's own glossary is shown on its **Article** section, in the **Pronunciation** tab.
6
+
7
+ ## Name pronunciations
8
+
9
+ Your list settles a name once. Every project copies it when it starts, and its entries come **before** the glossary that project's article wrote, so the name is said your way whatever the article spelled out.
10
+
11
+ 1. Open **Settings → Voices → Name pronunciations**.
12
+ 2. Write one `Term: /IPA/` per line, in standard-English IPA between slashes:
13
+
14
+ ```
15
+ Gygax: /ˈɡaɪɡæks/
16
+ Faerûn: /feɪˈruːn/
17
+ Mordenkainen: /ˌmɔːrdənˈkeɪnən/
18
+ ```
19
+
20
+ 3. Press **Save pronunciations**. The line under the box says how many names the list pronounces, and a note names any line narration has to skip, by its number and reason, the way the article's glossary does.
21
+
22
+ - One IPA word per written word. A hyphenated name's parts each get one.
23
+ - A possessive follows its name: `Gygax's` is said from the `Gygax` entry.
24
+ - A dash, `n/a` or nothing leaves the name read as written: useful for a name you want plain.
25
+ - Up to 100,000 characters, which is thousands of names.
26
+ - Only Inworld **TTS-2** and **TTS-2 Flash** read IPA. For any other voice, use an alias instead.
27
+ - **Use pronunciations** must be on for the project (the default) — the same switch that reads the article's glossary.
28
+
29
+ A project keeps the copy it started with. Editing the list later changes new projects only, until you open a project's **Edit settings** → **Narration** and press **Copy the list again** under **Use pronunciations**; only the narration where a changed name appears is made again.
30
+
31
+ ### Two people, or an agent, at the same list
32
+
33
+ The whole list is saved at once, so Slopify makes sure one save never quietly replaces another. Every save carries the version of the list it was made from. If something else saved meanwhile — a second window, or an agent — the save is refused and the page says so:
34
+
35
+ > **The list changed somewhere else.** An agent saved it: it has 3 names you don't have, 1 said differently.
36
+
37
+ Nothing is lost. What you typed stays in the box, and you choose:
38
+
39
+ - **Save mine anyway** keeps what is on screen, over theirs.
40
+ - **Load theirs instead** puts their list in the box, so you can add your change to it and save.
41
+
42
+ An agent gets the same refusal as a sentence, with the list as it stands: it reads the version, folds its change into that text, and saves again (`get_library_pronunciations`, then `set_library_pronunciations` with `{text, version}`).
43
+
44
+ ### Which list wins
45
+
46
+ | Where a pronunciation comes from | Order |
47
+ |---|---|
48
+ | **Name pronunciations** (your list) | First: it wins everywhere |
49
+ | The article's own **Pronunciation Glossary** | Next, for names your list does not have |
50
+ | **Other projects' pronunciations**, while shared | Last, for names neither of the above has |
51
+
52
+ An alias beats all three, because it changes the words before the sounds are decided.
6
53
 
7
54
  ## Narration aliases
8
55
 
@@ -39,7 +86,7 @@ Each row's title line sums it up, for example "Says Dr. as Doctor · whole word"
39
86
  - Two aliases can't share a written form.
40
87
  - Where two overlap in the text, the one starting first wins, then the longer.
41
88
  - A multi-word alias is read with single spaces around it: "Grey et al. wrote" goes to the voice as "Grey and others wrote".
42
- - An alias wins over a Pronunciation Glossary entry for the same words.
89
+ - An alias wins over a Name pronunciations entry and a Pronunciation Glossary entry for the same words.
43
90
  - Aliases work with every generated voice, and in multi-voice runs they apply to each speaker's turn.
44
91
  - Aliases also change the sentences **Narration preparation** reads, so its cues match what is said.
45
92
 
@@ -87,7 +134,7 @@ A table with `Term | IPA` columns works too. If you paste your own article, add
87
134
  ### Turn it on
88
135
 
89
136
  1. On Create, open **Narration → Change → More narration settings**.
90
- 2. Keep **Use the article's pronunciation glossary** on (the default). It shows only while the voice is one that reads it.
137
+ 2. Keep **Use pronunciations** on (the default). It shows only while the voice is one that reads IPA, and it covers both your Name pronunciations and this article's glossary.
91
138
  3. Optional: keep **Also use pronunciations from my other projects** on (the default). It adds every term from your other projects' glossaries, copied when this project starts. This project's own glossary wins where they differ. Turn it off to use only this article's glossary.
92
139
 
93
140
  ### Entries that are skipped
@@ -104,21 +151,56 @@ An entry narration can't use is skipped and read as ordinary text. It never bloc
104
151
 
105
152
  To fix skipped entries, edit the Pronunciation Glossary at the end of the article in **Edit settings** → **Article**, then save. See [Editing a project](Editing-a-Project).
106
153
 
107
- ## Aliases or glossary?
154
+ ## Does this video say the names right?
155
+
156
+ Two checks on the project page answer that from files the project already has. Neither one blocks the run or the upload, neither costs anything, and neither changes a thing.
157
+
158
+ ### Before the narration is paid for
159
+
160
+ The **Narration** section says, in one line, which names this article uses that nothing pronounces: not its own Pronunciation Glossary, not your **Name pronunciations** list, not the pooled copy of the other projects' glossaries. **Every mention** opens the table — each name, how often it is said as written and how often as a possessive, proper names first.
161
+
162
+ This is the cheapest moment to settle a name, because adding it to **Name pronunciations** costs nothing and the voice has not been paid yet.
163
+
164
+ It also catches the names a glossary looks like it covers and doesn't:
165
+
166
+ | What the glossary has | What the article says | Why the voice guesses |
167
+ |---|---|---|
168
+ | `Belwar Dissengulp` | "Belwar" on its own | A two-word term never matches one of its words said alone. |
169
+ | `Drizzt` | "Drizzt's" | A project narrated under the old pronunciation rules wrapped no possessive. |
170
+ | `Catti-brie` | "Catti-brie" | The old rules skipped a hyphenated term, so it was read as ordinary text. |
171
+
172
+ A project is judged by the rules it was voiced under, so a video made before those rules changed is told what actually happened to it, not what would happen today.
173
+
174
+ ### After the render, before the upload
175
+
176
+ The **YouTube** section carries the same line, saying two things.
177
+
178
+ **What the voice was sent.** Counted from the voice script itself, which is exactly the text the voice received: a name with no IPA in front of it there was guessed, whatever the glossary says.
179
+
180
+ **How each name was heard.** Read from the per-word confidence the subtitle timing already wrote when the captions were made: each name's mean, and the spread across its mentions. Nothing is listened to again and nothing is re-aligned.
181
+
182
+ The spread is the signal, not a low score. A rare name is heard poorly even when it is said perfectly, so only a name whose own mentions disagree with each other is flagged: eight mentions or more, and a spread of 0.30 or more between its 10th and 90th percentile.
183
+
184
+ A name read as an ordinary English word is left out of both checks: a one-word name that the same text also writes in lower case is a word here and not a name, which is why "Hall" stays quiet in a video that also says "hall".
185
+
186
+ ## Which one do I use?
108
187
 
109
188
  | You want | Use |
110
189
  |---|---|
111
190
  | An abbreviation read in full (Dr., St., vs.) | Alias |
112
191
  | A word read as a different word or spelling | Alias |
113
192
  | Any voice service | Alias |
114
- | A made-up or foreign name said exactly right, on Inworld TTS-2 | Glossary |
115
- | Pronunciations that come with each article automatically | Glossary |
193
+ | A name said the same way in every video | Name pronunciations |
194
+ | To correct a name once, for everything you have made since | Name pronunciations |
195
+ | A made-up or foreign name said exactly right, on Inworld TTS-2 | Name pronunciations, or the article's glossary |
196
+ | Pronunciations that come with each article automatically | The article's glossary |
116
197
 
117
198
  ## Tips
118
199
 
119
200
  - Spell **Say it as** the way it sounds, not the way it is written: "Shiv-awn" rather than "Siobhan".
120
201
  - Keep **Whole word** on unless you really want to match inside other words. Otherwise `St.` could fire inside unrelated text.
121
202
  - Listen to the first minute of a new run before a long batch, and add aliases for anything the voice stumbles on.
203
+ - A name that comes back video after video belongs in **Name pronunciations**, not in each article's prompt: one line there holds for every project that follows.
122
204
 
123
205
  ## Related pages
124
206
 
@@ -58,6 +58,7 @@ Full details: [Keys and Services](Providers-and-Keys) and [AI CLIs](AI-CLIs).
58
58
  | **Sounds like a real person** | On for a voice cloned from, or made to sound like, someone real. Copy for YouTube Studio then answers Yes to YouTube's question for videos it narrates. | Default: off. |
59
59
  | **Listen** | Reads one sentence aloud: free with the system voice, one short paid request with a service that has a key. | |
60
60
  | **Remove** | Removes the voice after you confirm. Projects keep the audio they made with it. | |
61
+ | **Name pronunciations** | Opens your own list of names with their IPA, said that way in every video, before any article's own glossary. | |
61
62
  | **Pronunciation** | Opens the narration aliases: words the narrator says differently from how they are written. | |
62
63
 
63
64
  A line above the table says when a voice's service has no key yet, with a button to add it. See [Keys and Services](Providers-and-Keys#add-a-voice) and [Narration Aliases and Glossary](Narration-Aliases-and-Glossary).