speechready 0.0.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +101 -3
- package/dist/changes.d.ts +14 -0
- package/dist/changes.js +10 -0
- package/dist/chunk.d.ts +28 -0
- package/dist/chunk.js +217 -0
- package/dist/clean.d.ts +41 -0
- package/dist/clean.js +323 -0
- package/dist/compile.d.ts +5 -0
- package/dist/compile.js +116 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +15 -0
- package/dist/integrations/livekit.d.ts +19 -0
- package/dist/integrations/livekit.js +35 -0
- package/dist/intents.d.ts +34 -0
- package/dist/intents.js +92 -0
- package/dist/lexicon.d.ts +53 -0
- package/dist/lexicon.js +131 -0
- package/dist/pipeline.d.ts +34 -0
- package/dist/pipeline.js +49 -0
- package/dist/profiles/data.d.ts +1 -0
- package/dist/profiles/data.js +411 -0
- package/dist/profiles/index.d.ts +35 -0
- package/dist/profiles/index.js +72 -0
- package/dist/util.d.ts +53 -0
- package/dist/util.js +146 -0
- package/dist/verbalize/en.d.ts +3 -0
- package/dist/verbalize/en.js +219 -0
- package/dist/verbalize/index.d.ts +22 -0
- package/dist/verbalize/index.js +75 -0
- package/dist/verbalize/numbers_en.d.ts +8 -0
- package/dist/verbalize/numbers_en.js +83 -0
- package/dist/verbalize/numbers_tr.d.ts +24 -0
- package/dist/verbalize/numbers_tr.js +134 -0
- package/dist/verbalize/tr.d.ts +9 -0
- package/dist/verbalize/tr.js +204 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +2 -0
- package/package.json +45 -5
- package/index.js +0 -2
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/** One pronunciation lexicon, applied in the pipeline and exported to each vendor's format.
|
|
2
|
+
*
|
|
3
|
+
* An entry says how a word is pronounced in one of three ways:
|
|
4
|
+
* - `ipa`: an IPA transcription, rendered as a phoneme tag where the provider
|
|
5
|
+
* accepts one (Azure, Google, Polly) and exported to lexicon files;
|
|
6
|
+
* - `say`: a respelling or alias ("engine X", "sequel") that replaces the word in
|
|
7
|
+
* the text, which works on every provider;
|
|
8
|
+
* - `spell: true`: the word is read letter by letter.
|
|
9
|
+
*
|
|
10
|
+
* When a provider cannot take a phoneme tag, an entry falls back to `say`, then
|
|
11
|
+
* to `spell`, then leaves the word alone.
|
|
12
|
+
*/
|
|
13
|
+
import { Intent } from "./intents.js";
|
|
14
|
+
import { Profile } from "./profiles/index.js";
|
|
15
|
+
export interface Entry {
|
|
16
|
+
word: string;
|
|
17
|
+
ipa: string;
|
|
18
|
+
say: string;
|
|
19
|
+
spell: boolean;
|
|
20
|
+
case_sensitive: boolean;
|
|
21
|
+
}
|
|
22
|
+
export type EntryInput = Partial<Entry> & {
|
|
23
|
+
word: string;
|
|
24
|
+
};
|
|
25
|
+
export declare function entry(e: EntryInput): Entry;
|
|
26
|
+
export declare class Lexicon {
|
|
27
|
+
entries: Entry[];
|
|
28
|
+
locale: string;
|
|
29
|
+
constructor(entries?: EntryInput[], locale?: string);
|
|
30
|
+
static fromEntries(entries: EntryInput[], locale?: string): Lexicon;
|
|
31
|
+
/** Build from a parsed lexicon document (`{locale, entries}`), e.g. `JSON.parse` of a lexicon file. */
|
|
32
|
+
static fromObject(data: {
|
|
33
|
+
locale?: string;
|
|
34
|
+
entries?: EntryInput[];
|
|
35
|
+
}): Lexicon;
|
|
36
|
+
/** Read a `.json` lexicon file, or a `.yaml` one when the optional `yaml` package is installed. Node only. */
|
|
37
|
+
static load(path: string): Promise<Lexicon>;
|
|
38
|
+
toObject(): {
|
|
39
|
+
locale: string;
|
|
40
|
+
entries: Record<string, unknown>[];
|
|
41
|
+
};
|
|
42
|
+
/** Replace every lexicon word in `text` with an intent sentinel; appends to `intents`. */
|
|
43
|
+
apply(text: string, intents: Intent[]): string;
|
|
44
|
+
/** Pronunciation Lexicon Specification XML, accepted by ElevenLabs, Azure and Amazon Polly. */
|
|
45
|
+
toPls(alphabet?: string): string;
|
|
46
|
+
/** Google Cloud TTS custom pronunciations (v1beta1 `customPronunciations`). IPA entries only. */
|
|
47
|
+
toGoogleJson(): string;
|
|
48
|
+
toJson(): string;
|
|
49
|
+
/** The upload file for a provider profile, or null when the provider takes no lexicon. */
|
|
50
|
+
exportFor(profile: Profile): string | null;
|
|
51
|
+
/** `pls`, `google`, or `json`. */
|
|
52
|
+
export(fmt: string, alphabet?: string): string;
|
|
53
|
+
}
|
package/dist/lexicon.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/** One pronunciation lexicon, applied in the pipeline and exported to each vendor's format.
|
|
2
|
+
*
|
|
3
|
+
* An entry says how a word is pronounced in one of three ways:
|
|
4
|
+
* - `ipa`: an IPA transcription, rendered as a phoneme tag where the provider
|
|
5
|
+
* accepts one (Azure, Google, Polly) and exported to lexicon files;
|
|
6
|
+
* - `say`: a respelling or alias ("engine X", "sequel") that replaces the word in
|
|
7
|
+
* the text, which works on every provider;
|
|
8
|
+
* - `spell: true`: the word is read letter by letter.
|
|
9
|
+
*
|
|
10
|
+
* When a provider cannot take a phoneme tag, an entry falls back to `say`, then
|
|
11
|
+
* to `spell`, then leaves the word alone.
|
|
12
|
+
*/
|
|
13
|
+
import { SENTINEL_CLOSE, SENTINEL_OPEN, intent } from "./intents.js";
|
|
14
|
+
import { W, escapeRe, escapeXml, re, sub } from "./util.js";
|
|
15
|
+
export function entry(e) {
|
|
16
|
+
return { ipa: "", say: "", spell: false, case_sensitive: false, ...e };
|
|
17
|
+
}
|
|
18
|
+
function entryIntent(e, matched) {
|
|
19
|
+
if (e.spell && !e.ipa && !e.say)
|
|
20
|
+
return intent("spell", { text: matched });
|
|
21
|
+
return intent("phoneme", { text: matched, ipa: e.ipa, say: e.say });
|
|
22
|
+
}
|
|
23
|
+
export class Lexicon {
|
|
24
|
+
entries;
|
|
25
|
+
locale;
|
|
26
|
+
constructor(entries = [], locale = "en-US") {
|
|
27
|
+
this.entries = entries.map(entry);
|
|
28
|
+
this.locale = locale;
|
|
29
|
+
}
|
|
30
|
+
// --- construction ---------------------------------------------------------------
|
|
31
|
+
static fromEntries(entries, locale = "en-US") {
|
|
32
|
+
return new Lexicon(entries, locale);
|
|
33
|
+
}
|
|
34
|
+
/** Build from a parsed lexicon document (`{locale, entries}`), e.g. `JSON.parse` of a lexicon file. */
|
|
35
|
+
static fromObject(data) {
|
|
36
|
+
return new Lexicon(data.entries ?? [], data.locale ?? "en-US");
|
|
37
|
+
}
|
|
38
|
+
/** Read a `.json` lexicon file, or a `.yaml` one when the optional `yaml` package is installed. Node only. */
|
|
39
|
+
static async load(path) {
|
|
40
|
+
const fs = await import("node:fs/promises");
|
|
41
|
+
const text = await fs.readFile(path, "utf-8");
|
|
42
|
+
if (/\.ya?ml$/i.test(path)) {
|
|
43
|
+
let yaml;
|
|
44
|
+
try {
|
|
45
|
+
yaml = (await import("yaml"));
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
throw new Error("reading a YAML lexicon needs the `yaml` package (npm install yaml); JSON lexicons need nothing");
|
|
49
|
+
}
|
|
50
|
+
return Lexicon.fromObject(yaml.parse(text));
|
|
51
|
+
}
|
|
52
|
+
return Lexicon.fromObject(JSON.parse(text));
|
|
53
|
+
}
|
|
54
|
+
toObject() {
|
|
55
|
+
return {
|
|
56
|
+
locale: this.locale,
|
|
57
|
+
entries: this.entries.map((e) => {
|
|
58
|
+
const out = {};
|
|
59
|
+
for (const [k, v] of Object.entries({ word: e.word, ipa: e.ipa, say: e.say, spell: e.spell, case_sensitive: e.case_sensitive })) {
|
|
60
|
+
if (v)
|
|
61
|
+
out[k] = v;
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
// --- applying to text ------------------------------------------------------------
|
|
68
|
+
/** Replace every lexicon word in `text` with an intent sentinel; appends to `intents`. */
|
|
69
|
+
apply(text, intents) {
|
|
70
|
+
if (!this.entries.length)
|
|
71
|
+
return text;
|
|
72
|
+
let work = text;
|
|
73
|
+
const ordered = [...this.entries].sort((a, b) => b.word.length - a.word.length);
|
|
74
|
+
for (const e of ordered) {
|
|
75
|
+
const flags = e.case_sensitive ? "" : "i";
|
|
76
|
+
const pattern = re(`(?<![${W}${SENTINEL_OPEN}${SENTINEL_CLOSE}])${escapeRe(e.word)}(?![${W}])`, flags);
|
|
77
|
+
work = sub(pattern, work, (m) => {
|
|
78
|
+
intents.push(entryIntent(e, m[0]));
|
|
79
|
+
return `${SENTINEL_OPEN}${intents.length - 1}${SENTINEL_CLOSE}`;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return work;
|
|
83
|
+
}
|
|
84
|
+
// --- export ----------------------------------------------------------------------------
|
|
85
|
+
/** Pronunciation Lexicon Specification XML, accepted by ElevenLabs, Azure and Amazon Polly. */
|
|
86
|
+
toPls(alphabet = "ipa") {
|
|
87
|
+
const lines = [
|
|
88
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
89
|
+
`<lexicon version="1.0" xmlns="http://www.w3.org/2005/01/pronunciation-lexicon" alphabet="${alphabet}" xml:lang="${this.locale}">`,
|
|
90
|
+
];
|
|
91
|
+
for (const e of this.entries) {
|
|
92
|
+
lines.push(" <lexeme>");
|
|
93
|
+
lines.push(` <grapheme>${escapeXml(e.word)}</grapheme>`);
|
|
94
|
+
if (e.ipa)
|
|
95
|
+
lines.push(` <phoneme>${escapeXml(e.ipa)}</phoneme>`);
|
|
96
|
+
else if (e.say)
|
|
97
|
+
lines.push(` <alias>${escapeXml(e.say)}</alias>`);
|
|
98
|
+
else if (e.spell)
|
|
99
|
+
lines.push(` <alias>${escapeXml(Array.from(e.word).join(" "))}</alias>`);
|
|
100
|
+
lines.push(" </lexeme>");
|
|
101
|
+
}
|
|
102
|
+
lines.push("</lexicon>");
|
|
103
|
+
return lines.join("\n") + "\n";
|
|
104
|
+
}
|
|
105
|
+
/** Google Cloud TTS custom pronunciations (v1beta1 `customPronunciations`). IPA entries only. */
|
|
106
|
+
toGoogleJson() {
|
|
107
|
+
const items = this.entries
|
|
108
|
+
.filter((e) => e.ipa)
|
|
109
|
+
.map((e) => ({ phrase: e.word, phoneticEncoding: "PHONETIC_ENCODING_IPA", pronunciation: e.ipa }));
|
|
110
|
+
return JSON.stringify({ pronunciations: items }, null, 2) + "\n";
|
|
111
|
+
}
|
|
112
|
+
toJson() {
|
|
113
|
+
return JSON.stringify(this.toObject(), null, 2) + "\n";
|
|
114
|
+
}
|
|
115
|
+
/** The upload file for a provider profile, or null when the provider takes no lexicon. */
|
|
116
|
+
exportFor(profile) {
|
|
117
|
+
if (profile.lexiconFormat === "none")
|
|
118
|
+
return null;
|
|
119
|
+
return this.export(profile.lexiconFormat, profile.lexiconAlphabet);
|
|
120
|
+
}
|
|
121
|
+
/** `pls`, `google`, or `json`. */
|
|
122
|
+
export(fmt, alphabet = "ipa") {
|
|
123
|
+
if (fmt === "pls")
|
|
124
|
+
return this.toPls(alphabet);
|
|
125
|
+
if (fmt === "google")
|
|
126
|
+
return this.toGoogleJson();
|
|
127
|
+
if (fmt === "json")
|
|
128
|
+
return this.toJson();
|
|
129
|
+
throw new Error(`unknown lexicon format ${JSON.stringify(fmt)}; use pls, google or json`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** The pipeline: clean -> verbalize -> compile -> chunk, with a change log. */
|
|
2
|
+
import { Change } from "./changes.js";
|
|
3
|
+
import { Options } from "./clean.js";
|
|
4
|
+
import { EntryInput, Lexicon } from "./lexicon.js";
|
|
5
|
+
import { Profile } from "./profiles/index.js";
|
|
6
|
+
import "./verbalize/en.js";
|
|
7
|
+
import "./verbalize/tr.js";
|
|
8
|
+
export interface Prepared {
|
|
9
|
+
/** The full compiled text. */
|
|
10
|
+
text: string;
|
|
11
|
+
/** The same text split under the model's request limit. */
|
|
12
|
+
chunks: string[];
|
|
13
|
+
/** Every rewrite: rule, before, after, note. */
|
|
14
|
+
changes: Change[];
|
|
15
|
+
profile: Profile;
|
|
16
|
+
locale: string;
|
|
17
|
+
}
|
|
18
|
+
export interface PrepareInit {
|
|
19
|
+
provider?: string | null;
|
|
20
|
+
model?: string | null;
|
|
21
|
+
/** A profile picked directly; overrides provider and model. */
|
|
22
|
+
profile?: Profile | null;
|
|
23
|
+
locale?: string;
|
|
24
|
+
/** Overrides the profile's request limit (null = no limit, undefined = the profile's). */
|
|
25
|
+
limit?: number | null;
|
|
26
|
+
options?: Partial<Options> | null;
|
|
27
|
+
/** A Lexicon, or its entries; its words are pronounced as it says. */
|
|
28
|
+
lexicon?: Lexicon | EntryInput[] | null;
|
|
29
|
+
}
|
|
30
|
+
/** Prepare `text` for the given provider and model.
|
|
31
|
+
*
|
|
32
|
+
* Returns the full compiled text, the request-sized chunks, and every change made.
|
|
33
|
+
*/
|
|
34
|
+
export declare function prepare(text: string, init?: PrepareInit): Prepared;
|
package/dist/pipeline.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** The pipeline: clean -> verbalize -> compile -> chunk, with a change log. */
|
|
2
|
+
import { Recorder } from "./changes.js";
|
|
3
|
+
import { pack, splitSentences } from "./chunk.js";
|
|
4
|
+
import { clean } from "./clean.js";
|
|
5
|
+
import { compileText } from "./compile.js";
|
|
6
|
+
import { extract, sentinelPattern } from "./intents.js";
|
|
7
|
+
import { Lexicon } from "./lexicon.js";
|
|
8
|
+
import { resolveProfile } from "./profiles/index.js";
|
|
9
|
+
import { sub } from "./util.js";
|
|
10
|
+
import { verbalize } from "./verbalize/index.js";
|
|
11
|
+
import "./verbalize/en.js";
|
|
12
|
+
import "./verbalize/tr.js";
|
|
13
|
+
/** Prepare `text` for the given provider and model.
|
|
14
|
+
*
|
|
15
|
+
* Returns the full compiled text, the request-sized chunks, and every change made.
|
|
16
|
+
*/
|
|
17
|
+
export function prepare(text, init = {}) {
|
|
18
|
+
const locale = init.locale ?? "en-US";
|
|
19
|
+
const lex = Array.isArray(init.lexicon) ? Lexicon.fromEntries(init.lexicon, locale) : init.lexicon ?? null;
|
|
20
|
+
const prof = init.profile ?? resolveProfile(init.provider ?? null, init.model ?? null);
|
|
21
|
+
const rec = new Recorder();
|
|
22
|
+
let [work, intents] = extract(text);
|
|
23
|
+
work = clean(work, init.options, rec, locale);
|
|
24
|
+
[work, intents] = extract(work, intents);
|
|
25
|
+
if (lex) {
|
|
26
|
+
const before = work;
|
|
27
|
+
work = lex.apply(work, intents);
|
|
28
|
+
rec.add("lexicon", "lexicon.apply", before, work, "lexicon words marked for pronunciation");
|
|
29
|
+
}
|
|
30
|
+
work = verbalize(work, locale, rec, init.options);
|
|
31
|
+
work = compileText(work, intents, prof, rec);
|
|
32
|
+
const maxSize = init.limit !== undefined ? init.limit : prof.limitFor(init.model ?? null);
|
|
33
|
+
const sentences = splitSentences(work, locale);
|
|
34
|
+
const chunks = pack(sentences, maxSize, prof.limitUnit, " ");
|
|
35
|
+
return { text: work, chunks, changes: readable(rec.changes, intents), profile: prof, locale };
|
|
36
|
+
}
|
|
37
|
+
function intentTag(intent) {
|
|
38
|
+
if (intent.kind === "pause")
|
|
39
|
+
return `[pause ${intent.ms}ms]`;
|
|
40
|
+
if (intent.kind === "phoneme")
|
|
41
|
+
return `[ipa ${intent.text} ${intent.ipa || intent.say}]`;
|
|
42
|
+
return `[${intent.kind} ${intent.text}]`;
|
|
43
|
+
}
|
|
44
|
+
/** Show intents in the change log as the tags the developer wrote, not as sentinels. */
|
|
45
|
+
function readable(changes, intents) {
|
|
46
|
+
const pattern = sentinelPattern();
|
|
47
|
+
const fix = (text) => sub(pattern, text, (m) => intentTag(intents[parseInt(m[1], 10)]));
|
|
48
|
+
return changes.map((c) => ({ ...c, before: fix(c.before), after: fix(c.after) }));
|
|
49
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const PROFILE_DATA: Record<string, unknown>[];
|