speechready 0.2.0 → 0.3.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/README.md +10 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/integrations/livekit.d.ts +7 -4
- package/dist/integrations/livekit.js +41 -5
- 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/en.js +2 -0
- package/dist/verbalize/numbers_tr.d.ts +6 -0
- package/dist/verbalize/numbers_tr.js +19 -0
- package/dist/verbalize/tr.js +11 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -57,7 +57,7 @@ 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; sentences are released as they are confirmed, and a fenced code block is handled as one piece
|
|
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
61
|
|
|
62
62
|
```js
|
|
63
63
|
import { textTransform } from "speechready/livekit";
|
|
@@ -88,6 +88,15 @@ lex.toGoogleJson(); // Google Cloud TTS custom pronunciations
|
|
|
88
88
|
await Lexicon.load("lexicon.json"); // Node only; .yaml files need the optional `yaml` package
|
|
89
89
|
```
|
|
90
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
|
+
|
|
91
100
|
## Differences from the Python package
|
|
92
101
|
|
|
93
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/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";
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/** speechready as a LiveKit Agents text transform.
|
|
2
2
|
*
|
|
3
3
|
* LiveKit Agents for Node passes the LLM's text stream through `ttsTextTransforms`
|
|
4
|
-
* before synthesis. Each transform is `(
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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.
|
|
7
9
|
*
|
|
8
10
|
* import { textTransform } from "speechready/livekit";
|
|
9
11
|
* const session = new voice.AgentSession({
|
|
@@ -16,4 +18,5 @@ export interface TextTransformInit extends Omit<PrepareInit, "limit"> {
|
|
|
16
18
|
/** Release a long sentence at a clause boundary once the buffer holds this many characters. */
|
|
17
19
|
maxBuffer?: number | null;
|
|
18
20
|
}
|
|
19
|
-
export
|
|
21
|
+
export type TextInput = ReadableStream<string> | AsyncIterable<string>;
|
|
22
|
+
export declare function textTransform(init?: TextTransformInit): (text: TextInput) => ReadableStream<string>;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/** speechready as a LiveKit Agents text transform.
|
|
2
2
|
*
|
|
3
3
|
* LiveKit Agents for Node passes the LLM's text stream through `ttsTextTransforms`
|
|
4
|
-
* before synthesis. Each transform is `(
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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.
|
|
7
9
|
*
|
|
8
10
|
* import { textTransform } from "speechready/livekit";
|
|
9
11
|
* const session = new voice.AgentSession({
|
|
@@ -17,9 +19,9 @@ export function textTransform(init = {}) {
|
|
|
17
19
|
const { maxBuffer = null, ...prepareInit } = init;
|
|
18
20
|
const locale = prepareInit.locale ?? "en-US";
|
|
19
21
|
const spoken = (piece) => prepare(piece, prepareInit).text;
|
|
20
|
-
|
|
22
|
+
async function* run(text) {
|
|
21
23
|
const seg = new Segmenter({ locale, maxBuffer });
|
|
22
|
-
for await (const chunk of text) {
|
|
24
|
+
for await (const chunk of iterate(text)) {
|
|
23
25
|
for (const piece of seg.push(chunk)) {
|
|
24
26
|
const out = spoken(piece);
|
|
25
27
|
if (out)
|
|
@@ -31,5 +33,39 @@ export function textTransform(init = {}) {
|
|
|
31
33
|
if (out)
|
|
32
34
|
yield out + " ";
|
|
33
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
|
+
});
|
|
34
51
|
};
|
|
35
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);
|
package/dist/verbalize/en.js
CHANGED
|
@@ -149,6 +149,7 @@ function fraction(m) {
|
|
|
149
149
|
return `${cardinal(num)} ${num === 1 ? one : many}`;
|
|
150
150
|
}
|
|
151
151
|
const YEAR_LIKE = re(String.raw `(19|20)\d\d`);
|
|
152
|
+
const minus = (m) => `${m[1]} minus ${m[2]}`;
|
|
152
153
|
function range(m) {
|
|
153
154
|
const a = m[1];
|
|
154
155
|
const b = m[2];
|
|
@@ -209,6 +210,7 @@ export const RULES = [
|
|
|
209
210
|
rule("verbalize.percent", re(String.raw `(?<![${W}.])(\d+(?:\.\d+)?)\s?%|%\s?(\d+(?:\.\d+)?)`), (m) => `${group(m, 1) ?? group(m, 2)} percent`, "percent sign read as a word"),
|
|
210
211
|
rule("verbalize.unit", re(String.raw `(?<![${W}.])(\d+(?:\.\d+)?)\s?(${UNIT_ALTERNATION})(?![${W}/])`), unit, "unit abbreviation read as a word"),
|
|
211
212
|
rule("verbalize.fraction", re(String.raw `(?<![\d/])([1-9])/([2-9]|10)(?![\d/])`), fraction, "fraction read as words"),
|
|
213
|
+
rule("verbalize.minus", re(String.raw `(?<![${W}.,\-/:])(\d+(?:\.\d+)?) - (\d+(?:\.\d+)?)(?=\s*=)`), minus, "spaced minus in an equation read as 'minus'"),
|
|
212
214
|
rule("verbalize.range", re(String.raw `(?<![${W}.,\-/:])(\d+(?:\.\d+)?)\s?[-–]\s?(\d+(?:\.\d+)?)(?![${W}\-/:]|[.,]\d)`), range, "number range read with 'to'"),
|
|
213
215
|
rule("verbalize.year", re(String.raw `${B}(in|In|since|Since|by|from|until|till|before|after|during|circa|year|est\.|©)\s+(1[1-9]\d\d|20\d\d)${B}(?![${W}.,]?\d)`), yearInContext, "year read as a year"),
|
|
214
216
|
rule("verbalize.decade", re(String.raw `${B}(1[1-9]\d0|20\d0)s${B}`), decade, "decade read as a year"),
|
|
@@ -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/verbalize/tr.js
CHANGED
|
@@ -66,7 +66,13 @@ function timeWords(hh, mm) {
|
|
|
66
66
|
return `${hour} sıfır sıfır`;
|
|
67
67
|
return `${hour} ${mm[0] !== "0" ? cardinal(minute) : "sıfır " + cardinal(minute)}`;
|
|
68
68
|
}
|
|
69
|
-
|
|
69
|
+
function time(m) {
|
|
70
|
+
const suffix = group(m, 3) ?? "";
|
|
71
|
+
if (suffix && parseInt(m[2], 10) === 0)
|
|
72
|
+
return attach(cardinal(parseInt(m[1], 10)), suffix); // 14.00'teki -> on dörtteki
|
|
73
|
+
return attach(timeWords(m[1], m[2]), suffix);
|
|
74
|
+
}
|
|
75
|
+
const minus = (m) => `${m[1]} eksi ${m[2]}`;
|
|
70
76
|
const timeRange = (m) => `${timeWords(m[1], m[2])} ile ${timeWords(m[3], m[4])}`;
|
|
71
77
|
const CURRENCY = {
|
|
72
78
|
"₺": ["lira", "kuruş"], TL: ["lira", "kuruş"], TRY: ["lira", "kuruş"],
|
|
@@ -181,12 +187,15 @@ export const RULES = [
|
|
|
181
187
|
rule("verbalize.abbreviation", re(String.raw `(?<![${W}.])(${ABBR_ALTERNATION})\.(?![${LOWER}${UPPER}])`), abbreviation, "abbreviation read in full"),
|
|
182
188
|
rule("verbalize.date.dmy", re(String.raw `${B}(\d{1,2})[./](\d{1,2})[./](\d{4}|\d{2})${B}${SUFFIX}`), dateDmy, "date read as day, month name, year"),
|
|
183
189
|
rule("verbalize.date.iso", re(String.raw `${B}(\d{4})-(\d{2})-(\d{2})${B}${SUFFIX}`), dateIso, "ISO date read as day, month name, year"),
|
|
190
|
+
rule("verbalize.time.range", re(String.raw `${B}([01]?\d|2[0-3])[.:]([0-5]\d)\s?[-–]\s?([01]?\d|2[0-3])[.:]([0-5]\d)${B}(?![.,]\d)`), timeRange, "time range read with 'ile'"),
|
|
184
191
|
rule("verbalize.phone", re(String.raw `(?<![${W}.])((?:\+?\d{1,3}${SEP})?(?:\(\d{2,4}\)[ \-]?|\d{2,4}${SEP})(?:\d{2,4}${SEP}){1,4}\d{2,4})${SUFFIX}(?![${W}\-]|[.,]\d)`), digitGroups, "digit groups read digit by digit with a pause between groups"),
|
|
185
192
|
rule("verbalize.number_sign", re(String.raw `#(?=\d)`), () => "numara ", "number sign read as a word"),
|
|
186
193
|
rule("verbalize.acronym.foreign", re(String.raw `(?<![${W}'’.])[bcdfghjklmnpqrstvwxyz]{2,5}(?![${W}'’])`), foreignAcronym, "lowercase foreign acronym read with English letter names"),
|
|
187
194
|
rule("verbalize.digits.long", re(String.raw `(?<![${W}.,])\d{7,}(?![${W}]|[.,]\d)`), (m) => digits(m[0]), "long digit string read digit by digit"),
|
|
188
|
-
rule("verbalize.time.range", re(String.raw `${B}([01]?\d|2[0-3]):([0-5]\d)\s?[-–]\s?([01]?\d|2[0-3]):([0-5]\d)${B}`), timeRange, "time range read with 'ile'"),
|
|
189
195
|
rule("verbalize.time", re(String.raw `${B}([01]?\d|2[0-3]):([0-5]\d)${B}(?!:)${SUFFIX}`), time, "clock time read as hours and minutes"),
|
|
196
|
+
// Turkish also writes times with a dot (saat 14.00); taken as a time only after "saat" or with a suffix, since 22.22 may be a decimal
|
|
197
|
+
rule("verbalize.time.dotted", re(String.raw `(?:(?<=[Ss]aat )|(?=[0-2]?\d\.[0-5]\d${APOS}))${B}([01]?\d|2[0-3])\.([0-5]\d)${B}(?![.,]\d)${SUFFIX}`), time, "dotted clock time read as hours and minutes"),
|
|
198
|
+
rule("verbalize.minus", re(String.raw `(?<![${W}.,\-/:])(\d+(?:,\d+)?) - (\d+(?:,\d+)?)(?=\s*=)`), minus, "spaced minus in an equation read as 'eksi'"),
|
|
190
199
|
rule("verbalize.currency.postfix", re(String.raw `(?<![${W}.,])(\d+(?:,\d+)?)\s?(bin|milyon|milyar)?\s?([₺$€£])${SUFFIX}(?![${W}])`), currencyPostfix, "currency symbol after the amount read as words"),
|
|
191
200
|
rule("verbalize.currency", re(String.raw `([₺$€£])\s?(\d{1,3}(?:\.\d{3})*|\d+)(?:,(\d{1,2}))?${SUFFIX}(?![${W}]|[.,]\d)`), currencyPrefix, "currency amount read as words"),
|
|
192
201
|
rule("verbalize.currency.code", re(String.raw `(?<![${W}.,])(\d{1,3}(?:\.\d{3})*|\d+)(?:,(\d{1,2}))?\s?(TL|TRY|USD|EUR|GBP)${B}${SUFFIX}`), currencySuffix, "currency code read as words"),
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const version = "0.
|
|
1
|
+
export declare const version = "0.3.1";
|
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.1";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "speechready",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
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",
|