speechready 0.1.0 → 0.3.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 +20 -0
- package/dist/chunk.d.ts +2 -6
- package/dist/chunk.js +23 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/integrations/livekit.d.ts +22 -0
- package/dist/integrations/livekit.js +71 -0
- package/dist/lexicon.js +15 -3
- package/dist/lexicons/data.d.ts +4 -0
- package/dist/lexicons/data.js +293 -0
- package/dist/lexicons/index.d.ts +10 -0
- package/dist/lexicons/index.js +18 -0
- package/dist/pipeline.d.ts +2 -2
- package/dist/pipeline.js +4 -1
- package/dist/verbalize/numbers_tr.d.ts +6 -0
- package/dist/verbalize/numbers_tr.js +19 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +26 -6
package/README.md
CHANGED
|
@@ -57,6 +57,17 @@ for (const token of llmStream) {
|
|
|
57
57
|
for (const sentence of seg.flush()) speak(sentence);
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
+
For LiveKit Agents (Node), the same thing as a `ttsTextTransforms` entry. It returns the `(ReadableStream<string>) => ReadableStream<string>` the SDK chains; sentences are released as they are confirmed, and a fenced code block is handled as one piece. A complete agent is in [`examples/livekit_agent.mjs`](../examples/livekit_agent.mjs).
|
|
61
|
+
|
|
62
|
+
```js
|
|
63
|
+
import { textTransform } from "speechready/livekit";
|
|
64
|
+
|
|
65
|
+
const session = new voice.AgentSession({
|
|
66
|
+
...,
|
|
67
|
+
ttsTextTransforms: [textTransform({ provider: "elevenlabs", model: "eleven_turbo_v2_5" })],
|
|
68
|
+
});
|
|
69
|
+
```
|
|
70
|
+
|
|
60
71
|
Pronunciation lexicon, one file for every vendor:
|
|
61
72
|
|
|
62
73
|
```js
|
|
@@ -77,6 +88,15 @@ lex.toGoogleJson(); // Google Cloud TTS custom pronunciations
|
|
|
77
88
|
await Lexicon.load("lexicon.json"); // Node only; .yaml files need the optional `yaml` package
|
|
78
89
|
```
|
|
79
90
|
|
|
91
|
+
One lexicon is bundled: `tr-tech`, foreign tech acronyms and names as Turkish developers say them (`API` → ey pi ay, `JSON` → ceyson). In Turkish an apostrophe suffix is refitted to the spoken form, so `API'yi` becomes `ey pi ay'ı`.
|
|
92
|
+
|
|
93
|
+
```js
|
|
94
|
+
import { loadLexicon, prepare } from "speechready";
|
|
95
|
+
|
|
96
|
+
prepare("API'yi JSON ile çağır.", { locale: "tr-TR", lexicon: "tr-tech" }).text; // "ey pi ay'ı ceyson ile çağır."
|
|
97
|
+
const lex = loadLexicon("tr-tech"); // a Lexicon you can extend before passing it
|
|
98
|
+
```
|
|
99
|
+
|
|
80
100
|
## Differences from the Python package
|
|
81
101
|
|
|
82
102
|
The two packages produce the same text for every fixture in `spec/fixtures`. The API differs only where the languages do: keyword arguments are one options object, methods are camelCase (`limitFor`, `toPls`, `splitSentences`), `Lexicon.load` is async, and `prepare` takes a `Lexicon` or an entries array rather than a file path.
|
package/dist/chunk.d.ts
CHANGED
|
@@ -11,16 +11,12 @@
|
|
|
11
11
|
*/
|
|
12
12
|
/** Split text into sentences. Newlines are hard boundaries. Empty sentences are dropped. */
|
|
13
13
|
export declare function splitSentences(text: string, locale?: string): string[];
|
|
14
|
-
/** Streaming sentence segmenter for LLM token feeds.
|
|
15
|
-
*
|
|
16
|
-
* `push(text)` returns the sentences that are now complete; `flush()` returns
|
|
17
|
-
* whatever is left. A boundary is only confirmed once text after it arrives,
|
|
18
|
-
* so a sentence is never cut at "Dr." or "3.14" because the stream paused there.
|
|
19
|
-
*/
|
|
20
14
|
export declare class Segmenter {
|
|
21
15
|
locale: string;
|
|
22
16
|
maxBuffer: number | null;
|
|
23
17
|
private buffer;
|
|
18
|
+
/** Lines of an open fenced code block; released whole once the fence closes. */
|
|
19
|
+
private fence;
|
|
24
20
|
constructor(init?: {
|
|
25
21
|
locale?: string;
|
|
26
22
|
maxBuffer?: number | null;
|
package/dist/chunk.js
CHANGED
|
@@ -93,10 +93,13 @@ export function splitSentences(text, locale = "en-US") {
|
|
|
93
93
|
* whatever is left. A boundary is only confirmed once text after it arrives,
|
|
94
94
|
* so a sentence is never cut at "Dr." or "3.14" because the stream paused there.
|
|
95
95
|
*/
|
|
96
|
+
const FENCE = /^\s*(```|~~~)/u;
|
|
96
97
|
export class Segmenter {
|
|
97
98
|
locale;
|
|
98
99
|
maxBuffer;
|
|
99
100
|
buffer = "";
|
|
101
|
+
/** Lines of an open fenced code block; released whole once the fence closes. */
|
|
102
|
+
fence = null;
|
|
100
103
|
constructor(init = {}) {
|
|
101
104
|
this.locale = init.locale ?? "en-US";
|
|
102
105
|
this.maxBuffer = init.maxBuffer ?? null;
|
|
@@ -110,8 +113,22 @@ export class Segmenter {
|
|
|
110
113
|
break;
|
|
111
114
|
const head = this.buffer.slice(0, nl);
|
|
112
115
|
this.buffer = this.buffer.slice(nl + 1);
|
|
113
|
-
|
|
116
|
+
if (this.fence !== null) {
|
|
117
|
+
this.fence.push(head);
|
|
118
|
+
if (FENCE.test(head)) {
|
|
119
|
+
done.push(this.fence.join("\n"));
|
|
120
|
+
this.fence = null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
else if (FENCE.test(head)) {
|
|
124
|
+
this.fence = [head];
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
done.push(...splitSentences(head, this.locale));
|
|
128
|
+
}
|
|
114
129
|
}
|
|
130
|
+
if (this.fence !== null)
|
|
131
|
+
return done.filter((s) => s); // inside a fence: nothing is sentence-split
|
|
115
132
|
const sentences = splitSentences(this.buffer, this.locale);
|
|
116
133
|
if (sentences.length > 1) {
|
|
117
134
|
done.push(...sentences.slice(0, -1));
|
|
@@ -131,6 +148,11 @@ export class Segmenter {
|
|
|
131
148
|
flush() {
|
|
132
149
|
const rest = this.buffer;
|
|
133
150
|
this.buffer = "";
|
|
151
|
+
if (this.fence !== null) {
|
|
152
|
+
const block = [...this.fence, ...(rest ? [rest] : [])].join("\n");
|
|
153
|
+
this.fence = null;
|
|
154
|
+
return [block];
|
|
155
|
+
}
|
|
134
156
|
return splitSentences(rest, this.locale);
|
|
135
157
|
}
|
|
136
158
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export { DEFAULT_OPTIONS, clean, options } from "./clean.js";
|
|
|
10
10
|
export type { Options } from "./clean.js";
|
|
11
11
|
export { Lexicon } from "./lexicon.js";
|
|
12
12
|
export type { Entry, EntryInput } from "./lexicon.js";
|
|
13
|
+
export { listLexicons, loadLexicon } from "./lexicons/index.js";
|
|
13
14
|
export { prepare } from "./pipeline.js";
|
|
14
15
|
export type { Prepared, PrepareInit } from "./pipeline.js";
|
|
15
16
|
export { listProfiles, loadProfile, resolveProfile } from "./profiles/index.js";
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
export { Segmenter, pack, splitSentences } from "./chunk.js";
|
|
8
8
|
export { DEFAULT_OPTIONS, clean, options } from "./clean.js";
|
|
9
9
|
export { Lexicon } from "./lexicon.js";
|
|
10
|
+
export { listLexicons, loadLexicon } from "./lexicons/index.js";
|
|
10
11
|
export { prepare } from "./pipeline.js";
|
|
11
12
|
export { listProfiles, loadProfile, resolveProfile } from "./profiles/index.js";
|
|
12
13
|
export { verbalize } from "./verbalize/index.js";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** speechready as a LiveKit Agents text transform.
|
|
2
|
+
*
|
|
3
|
+
* LiveKit Agents for Node passes the LLM's text stream through `ttsTextTransforms`
|
|
4
|
+
* before synthesis. Each transform is `(text: ReadableStream<string>) => ReadableStream<string>`
|
|
5
|
+
* (`TextTransform` in `@livekit/agents`), and the transforms are chained, so the
|
|
6
|
+
* return value has to be a real `ReadableStream`. This one segments the stream into
|
|
7
|
+
* sentences as they are confirmed, runs each through `prepare`, and streams the result.
|
|
8
|
+
* It also accepts any async iterable of strings as input.
|
|
9
|
+
*
|
|
10
|
+
* import { textTransform } from "speechready/livekit";
|
|
11
|
+
* const session = new voice.AgentSession({
|
|
12
|
+
* ...,
|
|
13
|
+
* ttsTextTransforms: [textTransform({ provider: "elevenlabs", model: "eleven_turbo_v2_5" })],
|
|
14
|
+
* });
|
|
15
|
+
*/
|
|
16
|
+
import type { PrepareInit } from "../pipeline.js";
|
|
17
|
+
export interface TextTransformInit extends Omit<PrepareInit, "limit"> {
|
|
18
|
+
/** Release a long sentence at a clause boundary once the buffer holds this many characters. */
|
|
19
|
+
maxBuffer?: number | null;
|
|
20
|
+
}
|
|
21
|
+
export type TextInput = ReadableStream<string> | AsyncIterable<string>;
|
|
22
|
+
export declare function textTransform(init?: TextTransformInit): (text: TextInput) => ReadableStream<string>;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/** speechready as a LiveKit Agents text transform.
|
|
2
|
+
*
|
|
3
|
+
* LiveKit Agents for Node passes the LLM's text stream through `ttsTextTransforms`
|
|
4
|
+
* before synthesis. Each transform is `(text: ReadableStream<string>) => ReadableStream<string>`
|
|
5
|
+
* (`TextTransform` in `@livekit/agents`), and the transforms are chained, so the
|
|
6
|
+
* return value has to be a real `ReadableStream`. This one segments the stream into
|
|
7
|
+
* sentences as they are confirmed, runs each through `prepare`, and streams the result.
|
|
8
|
+
* It also accepts any async iterable of strings as input.
|
|
9
|
+
*
|
|
10
|
+
* import { textTransform } from "speechready/livekit";
|
|
11
|
+
* const session = new voice.AgentSession({
|
|
12
|
+
* ...,
|
|
13
|
+
* ttsTextTransforms: [textTransform({ provider: "elevenlabs", model: "eleven_turbo_v2_5" })],
|
|
14
|
+
* });
|
|
15
|
+
*/
|
|
16
|
+
import { Segmenter } from "../chunk.js";
|
|
17
|
+
import { prepare } from "../pipeline.js";
|
|
18
|
+
export function textTransform(init = {}) {
|
|
19
|
+
const { maxBuffer = null, ...prepareInit } = init;
|
|
20
|
+
const locale = prepareInit.locale ?? "en-US";
|
|
21
|
+
const spoken = (piece) => prepare(piece, prepareInit).text;
|
|
22
|
+
async function* run(text) {
|
|
23
|
+
const seg = new Segmenter({ locale, maxBuffer });
|
|
24
|
+
for await (const chunk of iterate(text)) {
|
|
25
|
+
for (const piece of seg.push(chunk)) {
|
|
26
|
+
const out = spoken(piece);
|
|
27
|
+
if (out)
|
|
28
|
+
yield out + " ";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
for (const piece of seg.flush()) {
|
|
32
|
+
const out = spoken(piece);
|
|
33
|
+
if (out)
|
|
34
|
+
yield out + " ";
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return function transform(text) {
|
|
38
|
+
const gen = run(text);
|
|
39
|
+
return new ReadableStream({
|
|
40
|
+
async pull(controller) {
|
|
41
|
+
const { value, done } = await gen.next();
|
|
42
|
+
if (done)
|
|
43
|
+
controller.close();
|
|
44
|
+
else
|
|
45
|
+
controller.enqueue(value);
|
|
46
|
+
},
|
|
47
|
+
async cancel() {
|
|
48
|
+
await gen.return(undefined);
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/** Read a ReadableStream (which browsers do not make async-iterable) or any async iterable. */
|
|
54
|
+
async function* iterate(text) {
|
|
55
|
+
if (typeof text[Symbol.asyncIterator] === "function") {
|
|
56
|
+
yield* text;
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const reader = text.getReader();
|
|
60
|
+
try {
|
|
61
|
+
for (;;) {
|
|
62
|
+
const { value, done } = await reader.read();
|
|
63
|
+
if (done)
|
|
64
|
+
return;
|
|
65
|
+
yield value;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
reader.releaseLock();
|
|
70
|
+
}
|
|
71
|
+
}
|
package/dist/lexicon.js
CHANGED
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { SENTINEL_CLOSE, SENTINEL_OPEN, intent } from "./intents.js";
|
|
14
14
|
import { W, escapeRe, escapeXml, re, sub } from "./util.js";
|
|
15
|
+
import { respellSuffix } from "./verbalize/numbers_tr.js";
|
|
16
|
+
const TR_SUFFIX = "(?:(['’])([a-zçğıöşü]+))?";
|
|
15
17
|
export function entry(e) {
|
|
16
18
|
return { ipa: "", say: "", spell: false, case_sensitive: false, ...e };
|
|
17
19
|
}
|
|
@@ -71,12 +73,22 @@ export class Lexicon {
|
|
|
71
73
|
return text;
|
|
72
74
|
let work = text;
|
|
73
75
|
const ordered = [...this.entries].sort((a, b) => b.word.length - a.word.length);
|
|
76
|
+
const turkish = this.locale.split("-")[0].toLowerCase() === "tr";
|
|
74
77
|
for (const e of ordered) {
|
|
75
78
|
const flags = e.case_sensitive ? "" : "i";
|
|
76
|
-
|
|
79
|
+
// In Turkish a respelled word's apostrophe suffix (API'yi) is refitted to the spoken form (ey pi ay'ı).
|
|
80
|
+
const refit = turkish && !!e.say && !e.ipa;
|
|
81
|
+
const suffix = refit ? TR_SUFFIX : "";
|
|
82
|
+
const pattern = re(`(?<![${W}${SENTINEL_OPEN}${SENTINEL_CLOSE}])${escapeRe(e.word)}${suffix}(?![${W}])`, flags);
|
|
77
83
|
work = sub(pattern, work, (m) => {
|
|
78
|
-
|
|
79
|
-
|
|
84
|
+
let word = m[0];
|
|
85
|
+
let tail = "";
|
|
86
|
+
if (refit && m[2]) {
|
|
87
|
+
word = word.slice(0, word.length - m[1].length - m[2].length);
|
|
88
|
+
tail = m[1] + respellSuffix(e.say, m[2]);
|
|
89
|
+
}
|
|
90
|
+
intents.push(entryIntent(e, word));
|
|
91
|
+
return `${SENTINEL_OPEN}${intents.length - 1}${SENTINEL_CLOSE}${tail}`;
|
|
80
92
|
});
|
|
81
93
|
}
|
|
82
94
|
return work;
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
// Generated by scripts/build-profiles.mjs from spec/lexicons. Do not edit.
|
|
2
|
+
export const LEXICON_DATA = {
|
|
3
|
+
"tr-tech": {
|
|
4
|
+
"locale": "tr-TR",
|
|
5
|
+
"entries": [
|
|
6
|
+
{
|
|
7
|
+
"word": "API",
|
|
8
|
+
"say": "ey pi ay"
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"word": "URL",
|
|
12
|
+
"say": "yu ar el"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"word": "URI",
|
|
16
|
+
"say": "yu ar ay"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"word": "AWS",
|
|
20
|
+
"say": "ey dabılyu es"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"word": "GCP",
|
|
24
|
+
"say": "ci si pi"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"word": "CPU",
|
|
28
|
+
"say": "si pi yu"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"word": "GPU",
|
|
32
|
+
"say": "ci pi yu"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"word": "SSD",
|
|
36
|
+
"say": "es es di"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"word": "HTML",
|
|
40
|
+
"say": "eyç ti em el"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"word": "CSS",
|
|
44
|
+
"say": "si es es"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"word": "SQL",
|
|
48
|
+
"say": "es kyu el"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"word": "PHP",
|
|
52
|
+
"say": "pi eyç pi"
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"word": "HTTP",
|
|
56
|
+
"say": "eyç ti ti pi"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"word": "HTTPS",
|
|
60
|
+
"say": "eyç ti ti pi es"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"word": "JS",
|
|
64
|
+
"say": "cey es"
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"word": "TS",
|
|
68
|
+
"say": "ti es"
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"word": "UI",
|
|
72
|
+
"say": "yu ay"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"word": "UX",
|
|
76
|
+
"say": "yu eks"
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"word": "AI",
|
|
80
|
+
"say": "ey ay"
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"word": "ML",
|
|
84
|
+
"say": "em el"
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"word": "LLM",
|
|
88
|
+
"say": "el el em"
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"word": "GPT",
|
|
92
|
+
"say": "ci pi ti"
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"word": "OCR",
|
|
96
|
+
"say": "o si ar"
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"word": "PDF",
|
|
100
|
+
"say": "pi di ef"
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"word": "CSV",
|
|
104
|
+
"say": "si es vi"
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"word": "XML",
|
|
108
|
+
"say": "eks em el"
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"word": "SDK",
|
|
112
|
+
"say": "es di key"
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
"word": "CLI",
|
|
116
|
+
"say": "si el ay"
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
"word": "IDE",
|
|
120
|
+
"say": "ay di i"
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
"word": "DNS",
|
|
124
|
+
"say": "di en es"
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
"word": "IP",
|
|
128
|
+
"say": "ay pi"
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"word": "TCP",
|
|
132
|
+
"say": "ti si pi"
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
"word": "UDP",
|
|
136
|
+
"say": "yu di pi"
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
"word": "SSH",
|
|
140
|
+
"say": "es es eyç"
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
"word": "SSL",
|
|
144
|
+
"say": "es es el"
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
"word": "TLS",
|
|
148
|
+
"say": "ti el es"
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
"word": "VPN",
|
|
152
|
+
"say": "vi pi en"
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
"word": "VM",
|
|
156
|
+
"say": "vi em"
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
"word": "DB",
|
|
160
|
+
"say": "di bi"
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
"word": "ORM",
|
|
164
|
+
"say": "o ar em"
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
"word": "JWT",
|
|
168
|
+
"say": "cey dabılyu ti"
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
"word": "NPM",
|
|
172
|
+
"say": "en pi em"
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
"word": "UUID",
|
|
176
|
+
"say": "yu yu ay di"
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
"word": "CDN",
|
|
180
|
+
"say": "si di en"
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
"word": "TTS",
|
|
184
|
+
"say": "ti ti es"
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
"word": "STT",
|
|
188
|
+
"say": "es ti ti"
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
"word": "ASR",
|
|
192
|
+
"say": "ey es ar"
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
"word": "NLP",
|
|
196
|
+
"say": "en el pi"
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
"word": "MCP",
|
|
200
|
+
"say": "em si pi"
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
"word": "QA",
|
|
204
|
+
"say": "kyu ey"
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
"word": "PR",
|
|
208
|
+
"say": "pi ar"
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
"word": "CI",
|
|
212
|
+
"say": "si ay"
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
"word": "CD",
|
|
216
|
+
"say": "si di"
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
"word": "CRM",
|
|
220
|
+
"say": "si ar em"
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
"word": "ERP",
|
|
224
|
+
"say": "i ar pi"
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
"word": "SEO",
|
|
228
|
+
"say": "es i o"
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
"word": "KPI",
|
|
232
|
+
"say": "key pi ay"
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
"word": "CEO",
|
|
236
|
+
"say": "si i o"
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
"word": "CTO",
|
|
240
|
+
"say": "si ti o"
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
"word": "OS",
|
|
244
|
+
"say": "o es"
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
"word": "PC",
|
|
248
|
+
"say": "pi si"
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
"word": "iOS",
|
|
252
|
+
"say": "ay o es"
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
"word": "IT",
|
|
256
|
+
"say": "ay ti",
|
|
257
|
+
"case_sensitive": true
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
"word": "JSON",
|
|
261
|
+
"say": "ceyson"
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
"word": "GitHub",
|
|
265
|
+
"say": "githab"
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
"word": "Python",
|
|
269
|
+
"say": "paytın"
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
"word": "WiFi",
|
|
273
|
+
"say": "vayfay"
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
"word": "Wi-Fi",
|
|
277
|
+
"say": "vayfay"
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
"word": "Java",
|
|
281
|
+
"say": "cava"
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
"word": "Excel",
|
|
285
|
+
"say": "eksel"
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
"word": "Chrome",
|
|
289
|
+
"say": "krom"
|
|
290
|
+
}
|
|
291
|
+
]
|
|
292
|
+
}
|
|
293
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Lexicons bundled with the package, from the repository's `spec/lexicons`.
|
|
2
|
+
*
|
|
3
|
+
* prepare("API'yi çağır.", { locale: "tr-TR", lexicon: loadLexicon("tr-tech") }).text
|
|
4
|
+
* prepare("API'yi çağır.", { locale: "tr-TR", lexicon: "tr-tech" }).text // the same, by name
|
|
5
|
+
*/
|
|
6
|
+
import { Lexicon } from "../lexicon.js";
|
|
7
|
+
/** Names of the bundled lexicons, e.g. ["tr-tech"]. */
|
|
8
|
+
export declare function listLexicons(): string[];
|
|
9
|
+
/** A bundled lexicon by name; a new object each call, so it can be extended. */
|
|
10
|
+
export declare function loadLexicon(name: string): Lexicon;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Lexicons bundled with the package, from the repository's `spec/lexicons`.
|
|
2
|
+
*
|
|
3
|
+
* prepare("API'yi çağır.", { locale: "tr-TR", lexicon: loadLexicon("tr-tech") }).text
|
|
4
|
+
* prepare("API'yi çağır.", { locale: "tr-TR", lexicon: "tr-tech" }).text // the same, by name
|
|
5
|
+
*/
|
|
6
|
+
import { Lexicon } from "../lexicon.js";
|
|
7
|
+
import { LEXICON_DATA } from "./data.js";
|
|
8
|
+
/** Names of the bundled lexicons, e.g. ["tr-tech"]. */
|
|
9
|
+
export function listLexicons() {
|
|
10
|
+
return Object.keys(LEXICON_DATA).sort();
|
|
11
|
+
}
|
|
12
|
+
/** A bundled lexicon by name; a new object each call, so it can be extended. */
|
|
13
|
+
export function loadLexicon(name) {
|
|
14
|
+
const data = LEXICON_DATA[name];
|
|
15
|
+
if (!data)
|
|
16
|
+
throw new Error(`unknown bundled lexicon ${JSON.stringify(name)}; known: ${listLexicons().join(", ")}`);
|
|
17
|
+
return Lexicon.fromObject({ locale: data.locale, entries: (data.entries ?? []) });
|
|
18
|
+
}
|
package/dist/pipeline.d.ts
CHANGED
|
@@ -24,8 +24,8 @@ export interface PrepareInit {
|
|
|
24
24
|
/** Overrides the profile's request limit (null = no limit, undefined = the profile's). */
|
|
25
25
|
limit?: number | null;
|
|
26
26
|
options?: Partial<Options> | null;
|
|
27
|
-
/** A Lexicon, or
|
|
28
|
-
lexicon?: Lexicon | EntryInput[] | null;
|
|
27
|
+
/** A Lexicon, its entries, or the name of a bundled lexicon ("tr-tech"); its words are pronounced as it says. */
|
|
28
|
+
lexicon?: Lexicon | EntryInput[] | string | null;
|
|
29
29
|
}
|
|
30
30
|
/** Prepare `text` for the given provider and model.
|
|
31
31
|
*
|
package/dist/pipeline.js
CHANGED
|
@@ -5,6 +5,7 @@ import { clean } from "./clean.js";
|
|
|
5
5
|
import { compileText } from "./compile.js";
|
|
6
6
|
import { extract, sentinelPattern } from "./intents.js";
|
|
7
7
|
import { Lexicon } from "./lexicon.js";
|
|
8
|
+
import { loadLexicon } from "./lexicons/index.js";
|
|
8
9
|
import { resolveProfile } from "./profiles/index.js";
|
|
9
10
|
import { sub } from "./util.js";
|
|
10
11
|
import { verbalize } from "./verbalize/index.js";
|
|
@@ -16,7 +17,9 @@ import "./verbalize/tr.js";
|
|
|
16
17
|
*/
|
|
17
18
|
export function prepare(text, init = {}) {
|
|
18
19
|
const locale = init.locale ?? "en-US";
|
|
19
|
-
const lex =
|
|
20
|
+
const lex = typeof init.lexicon === "string" ? loadLexicon(init.lexicon)
|
|
21
|
+
: Array.isArray(init.lexicon) ? Lexicon.fromEntries(init.lexicon, locale)
|
|
22
|
+
: init.lexicon ?? null;
|
|
20
23
|
const prof = init.profile ?? resolveProfile(init.provider ?? null, init.model ?? null);
|
|
21
24
|
const rec = new Recorder();
|
|
22
25
|
let [work, intents] = extract(text);
|
|
@@ -16,6 +16,12 @@ export declare function soften(word: string): string;
|
|
|
16
16
|
export declare function attach(word: string, suffix: string): string;
|
|
17
17
|
/** Re-harmonise a suffix written against an abbreviation (TL'ye, 1/4'ü) for the spoken word (liraya, çeyreği). */
|
|
18
18
|
export declare function reharmonize(word: string, suffix: string): string;
|
|
19
|
+
/** The suffix a writer attached to an abbreviation (API'yi), refitted to how the word is said (ey pi ay -> 'ı).
|
|
20
|
+
*
|
|
21
|
+
* Harmony follows the last spoken word, and the buffer consonant follows whether it
|
|
22
|
+
* ends in a vowel: GPU'yu -> ci pi yu'yu, API'si -> ey pi ay'ı.
|
|
23
|
+
*/
|
|
24
|
+
export declare function respellSuffix(word: string, suffix: string): string;
|
|
19
25
|
/** üçüncü, dördüncü, yirmi birinci. */
|
|
20
26
|
export declare function ordinal(n: number): string;
|
|
21
27
|
/** üç -> üçte, dört -> dörtte, on -> onda, beş -> beşte. */
|
|
@@ -99,6 +99,25 @@ export function reharmonize(word, suffix) {
|
|
|
99
99
|
const four = harmonyI(word);
|
|
100
100
|
return Array.from(suffix).map((ch) => ("ae".includes(ch) ? two : "ıiuü".includes(ch) ? four : ch)).join("");
|
|
101
101
|
}
|
|
102
|
+
/** The suffix a writer attached to an abbreviation (API'yi), refitted to how the word is said (ey pi ay -> 'ı).
|
|
103
|
+
*
|
|
104
|
+
* Harmony follows the last spoken word, and the buffer consonant follows whether it
|
|
105
|
+
* ends in a vowel: GPU'yu -> ci pi yu'yu, API'si -> ey pi ay'ı.
|
|
106
|
+
*/
|
|
107
|
+
export function respellSuffix(word, suffix) {
|
|
108
|
+
if (!suffix)
|
|
109
|
+
return suffix;
|
|
110
|
+
const last = word.slice(word.lastIndexOf(" ") + 1);
|
|
111
|
+
let s = reharmonize(last, suffix);
|
|
112
|
+
const endsInVowel = VOWELS.includes(last[last.length - 1]);
|
|
113
|
+
if ("sy".includes(s[0]) && s.length > 1 && VOWELS.includes(s[1]) && !endsInVowel) {
|
|
114
|
+
s = s.slice(1);
|
|
115
|
+
}
|
|
116
|
+
else if (VOWELS.includes(s[0]) && endsInVowel) {
|
|
117
|
+
s = "y" + s;
|
|
118
|
+
}
|
|
119
|
+
return s;
|
|
120
|
+
}
|
|
102
121
|
/** üçüncü, dördüncü, yirmi birinci. */
|
|
103
122
|
export function ordinal(n) {
|
|
104
123
|
const words = cardinal(n);
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const version = "0.
|
|
1
|
+
export declare const version = "0.3.0";
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated by scripts/build-profiles.mjs from package.json. Do not edit.
|
|
2
|
-
export const version = "0.
|
|
2
|
+
export const version = "0.3.0";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "speechready",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Prepare text for text-to-speech: clean LLM output, verbalize ambiguous tokens, compile pauses to each provider's syntax, chunk under model limits.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -9,18 +9,38 @@
|
|
|
9
9
|
".": {
|
|
10
10
|
"types": "./dist/index.d.ts",
|
|
11
11
|
"import": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./livekit": {
|
|
14
|
+
"types": "./dist/integrations/livekit.d.ts",
|
|
15
|
+
"import": "./dist/integrations/livekit.js"
|
|
12
16
|
}
|
|
13
17
|
},
|
|
14
18
|
"license": "MIT",
|
|
15
19
|
"author": "Berker Batur",
|
|
16
|
-
"repository": {
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/berkerbatur/speechready.git",
|
|
23
|
+
"directory": "js"
|
|
24
|
+
},
|
|
17
25
|
"homepage": "https://github.com/berkerbatur/speechready",
|
|
18
|
-
"keywords": [
|
|
19
|
-
|
|
20
|
-
|
|
26
|
+
"keywords": [
|
|
27
|
+
"tts",
|
|
28
|
+
"text-to-speech",
|
|
29
|
+
"text-normalization",
|
|
30
|
+
"ssml",
|
|
31
|
+
"voice-agents"
|
|
32
|
+
],
|
|
33
|
+
"files": [
|
|
34
|
+
"dist",
|
|
35
|
+
"README.md",
|
|
36
|
+
"LICENSE"
|
|
37
|
+
],
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=20"
|
|
40
|
+
},
|
|
21
41
|
"scripts": {
|
|
22
42
|
"build": "node scripts/build-profiles.mjs && tsc -p tsconfig.json",
|
|
23
|
-
"test": "npm run build && node --test test/spec.test.mjs test/package.test.mjs",
|
|
43
|
+
"test": "npm run build && node --test test/spec.test.mjs test/package.test.mjs test/livekit.test.mjs",
|
|
24
44
|
"prepack": "npm run build"
|
|
25
45
|
},
|
|
26
46
|
"devDependencies": {
|