narrator-ts 0.0.0 → 0.1.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/LICENSE +21 -0
- package/README.md +113 -3
- package/dist/narrator/contour.d.ts +90 -0
- package/dist/narrator/contour.js +235 -0
- package/dist/narrator/duration.d.ts +39 -0
- package/dist/narrator/duration.js +259 -0
- package/dist/narrator/frames.d.ts +140 -0
- package/dist/narrator/frames.js +428 -0
- package/dist/narrator/index.d.ts +15 -0
- package/dist/narrator/index.js +13 -0
- package/dist/narrator/interpolate.d.ts +183 -0
- package/dist/narrator/interpolate.js +467 -0
- package/dist/narrator/onset.d.ts +25 -0
- package/dist/narrator/onset.js +73 -0
- package/dist/narrator/parse.d.ts +67 -0
- package/dist/narrator/parse.js +184 -0
- package/dist/narrator/prosody.d.ts +355 -0
- package/dist/narrator/prosody.js +1121 -0
- package/dist/narrator/render.d.ts +64 -0
- package/dist/narrator/render.js +273 -0
- package/dist/narrator/rewrite.d.ts +55 -0
- package/dist/narrator/rewrite.js +155 -0
- package/dist/narrator/speak.d.ts +120 -0
- package/dist/narrator/speak.js +206 -0
- package/dist/narrator/stress.d.ts +36 -0
- package/dist/narrator/stress.js +147 -0
- package/dist/narrator/voice.d.ts +66 -0
- package/dist/narrator/voice.js +78 -0
- package/dist/translator/engines.d.ts +37 -0
- package/dist/translator/engines.js +26 -0
- package/dist/translator/index.d.ts +3 -0
- package/dist/translator/index.js +2 -0
- package/dist/translator/translate.d.ts +10 -0
- package/dist/translator/translate.js +363 -0
- package/dist/translator/types.d.ts +70 -0
- package/dist/translator/types.js +31 -0
- package/package.json +57 -4
- package/reference/README.md +168 -0
- package/reference/formants.json +18 -0
- package/reference/nrl-7948.json +434 -0
- package/reference/nrl-table.json +1 -0
- package/reference/voice-free.json +11775 -0
- package/reference/wordlist.txt +10 -0
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import { engineFor } from './engines.js';
|
|
2
|
+
import { CLASS } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* English text to narrator phonemes — a port of translator.library's
|
|
5
|
+
* Translate(), following the 33.2 disassembly rather than the published
|
|
6
|
+
* description of the NRL rules, because the two differ in places that matter.
|
|
7
|
+
*
|
|
8
|
+
* Addresses in comments are hunk offsets into translator.library 33.2
|
|
9
|
+
* (`11997e3c`); see research/01-translator.md.
|
|
10
|
+
*/
|
|
11
|
+
const SPACE = 0x20;
|
|
12
|
+
const HASH = 0x23;
|
|
13
|
+
const DEL = 0x7f;
|
|
14
|
+
const RBRACKET = 0x5d;
|
|
15
|
+
/** Longest word the library will buffer, from `cmpi.w #$64,D3` at 0x552. */
|
|
16
|
+
const MAX_WORD = 100;
|
|
17
|
+
/** The word buffer is 27 longwords of spaces pushed at 0x13e. */
|
|
18
|
+
const WORD_BUF = 108;
|
|
19
|
+
/** Returned when a word exceeds MAX_WORD (`moveq #-3,D1` at 0x590). */
|
|
20
|
+
const ERR_WORD_TOO_LONG = -3;
|
|
21
|
+
/** Letters the suffix and long-U wildcards compare against by name. */
|
|
22
|
+
const CH = { D: 0x44, E: 0x45, G: 0x47, I: 0x49, L: 0x4c, N: 0x4e, R: 0x52, S: 0x53, Y: 0x59 };
|
|
23
|
+
/**
|
|
24
|
+
* Fold one input character the way the buffer filler does (0x522-0x54c).
|
|
25
|
+
*
|
|
26
|
+
* The range test is `cmpi.b #$20,D1` followed by **bge** — a *signed* branch.
|
|
27
|
+
* A byte of 0x80 or more is negative as a signed byte, so it fails the test
|
|
28
|
+
* and becomes a space, exactly like a control character. That is why the
|
|
29
|
+
* class table only ever needs its 128 real entries: no high byte reaches it.
|
|
30
|
+
*/
|
|
31
|
+
function normalise(c) {
|
|
32
|
+
if (c >= 0x80)
|
|
33
|
+
return SPACE; // signed bge at 0x526
|
|
34
|
+
if (c < 0x20)
|
|
35
|
+
return SPACE;
|
|
36
|
+
if (c === DEL)
|
|
37
|
+
return SPACE;
|
|
38
|
+
if (c >= 0x61 && c <= 0x7a)
|
|
39
|
+
return c - 0x20; // to upper
|
|
40
|
+
if (c === RBRACKET)
|
|
41
|
+
return SPACE; // ']' can't appear in a rule
|
|
42
|
+
return c;
|
|
43
|
+
}
|
|
44
|
+
export function translate(text, tables, outSize = 4096) {
|
|
45
|
+
const traits = engineFor(tables.version);
|
|
46
|
+
const classOf = (c) => c >= 0 && c < tables.classes.length ? tables.classes[c] : 0;
|
|
47
|
+
const input = [];
|
|
48
|
+
for (let i = 0; i < text.length; i++)
|
|
49
|
+
input.push(text.charCodeAt(i) & 0xff);
|
|
50
|
+
let inPos = 0;
|
|
51
|
+
let remaining = input.length; // frame+0x82
|
|
52
|
+
const out = [];
|
|
53
|
+
let wordStart = 0; // frame+0x10, start of this word's output
|
|
54
|
+
let stressPending = false; // frame+0x14 bit 0
|
|
55
|
+
let overflowed = false;
|
|
56
|
+
const word = new Uint8Array(WORD_BUF).fill(SPACE);
|
|
57
|
+
let pos = 0; // A2, index into `word`
|
|
58
|
+
let written = 0; // D3
|
|
59
|
+
const emit = (c) => {
|
|
60
|
+
if (out.length >= outSize - 1) {
|
|
61
|
+
overflowed = true;
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
out.push(c);
|
|
65
|
+
};
|
|
66
|
+
// ---------------------------------------------------------------- 0x4fc
|
|
67
|
+
/**
|
|
68
|
+
* Fill the buffer with the next word. Returns an error code, or 0.
|
|
69
|
+
*
|
|
70
|
+
* The buffer is laid out as [carried char][space][word...]: matching starts
|
|
71
|
+
* at index 2, so a rule's left context can see a space at word start. The
|
|
72
|
+
* filler deliberately over-reads one character past the trailing space and
|
|
73
|
+
* un-reads it from the input (0x58a), carrying it into the next buffer.
|
|
74
|
+
*/
|
|
75
|
+
const fillWord = () => {
|
|
76
|
+
// 0x50e, and only here: the loop's back-edges all target 0x514, so the
|
|
77
|
+
// remaining-input test is not repeated. Re-testing it each pass emits the
|
|
78
|
+
// end sentinel in the middle of a word that consumes the last character.
|
|
79
|
+
if (remaining <= 0) {
|
|
80
|
+
emit(HASH);
|
|
81
|
+
written = 0;
|
|
82
|
+
return 0;
|
|
83
|
+
}
|
|
84
|
+
let a2 = 0;
|
|
85
|
+
word[a2] = word[a2 + written];
|
|
86
|
+
a2++;
|
|
87
|
+
word[a2++] = SPACE;
|
|
88
|
+
let d0 = 0;
|
|
89
|
+
let d1 = 0;
|
|
90
|
+
written = 0;
|
|
91
|
+
for (;;) {
|
|
92
|
+
d0 = d1;
|
|
93
|
+
d1 = input[inPos++] ?? 0;
|
|
94
|
+
if (d1 === 0)
|
|
95
|
+
remaining = 1; // 0x51a: NUL forces the last pass
|
|
96
|
+
d1 = normalise(d1);
|
|
97
|
+
written++;
|
|
98
|
+
// 0x590 puts -3 in D1 and returns. The main loop branches on D3, not
|
|
99
|
+
// D1, so an over-long word does not abort: the buffer simply keeps the
|
|
100
|
+
// first 100 characters and matching carries on. D1 is then overwritten
|
|
101
|
+
// with 0 on the normal exit path (0x1a8), so the code rarely surfaces.
|
|
102
|
+
if (written > MAX_WORD)
|
|
103
|
+
return ERR_WORD_TOO_LONG;
|
|
104
|
+
word[a2++] = d1;
|
|
105
|
+
if (d0 === SPACE) { // 0x55a: stop one past the space...
|
|
106
|
+
inPos--; // 0x58a: ...and un-read it
|
|
107
|
+
written--;
|
|
108
|
+
return 0;
|
|
109
|
+
}
|
|
110
|
+
remaining--;
|
|
111
|
+
if (remaining === 0) { // 0x564 -> 0x594
|
|
112
|
+
word[a2++] = SPACE;
|
|
113
|
+
word[a2++] = SPACE;
|
|
114
|
+
return 0;
|
|
115
|
+
}
|
|
116
|
+
if (d1 === HASH && d0 === HASH) { // 0x566: '##' ends translation
|
|
117
|
+
if (written === 2) { // 0x572
|
|
118
|
+
emit(HASH);
|
|
119
|
+
written = 0;
|
|
120
|
+
return 0;
|
|
121
|
+
}
|
|
122
|
+
inPos--; // 0x580
|
|
123
|
+
word[a2 - 2] = SPACE;
|
|
124
|
+
written--;
|
|
125
|
+
inPos--;
|
|
126
|
+
written--;
|
|
127
|
+
return 0;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
// ---------------------------------------------------------------- 0x59e
|
|
132
|
+
/**
|
|
133
|
+
* Insert a stress mark into the word just emitted.
|
|
134
|
+
*
|
|
135
|
+
* Only when the previous rule left a mark pending, the word is at least
|
|
136
|
+
* three characters, and it contains no stress digit already — a rule that
|
|
137
|
+
* supplied its own digit wins, which is why `Amiga` keeps its `IY3` rather
|
|
138
|
+
* than gaining a `4` on the leading `AH`.
|
|
139
|
+
*/
|
|
140
|
+
const stressPass = () => {
|
|
141
|
+
// Every exit path runs 0x608, which advances the word-start marker —
|
|
142
|
+
// including the not-pending branch at 0x5a4. Returning early without it
|
|
143
|
+
// makes the next word's stress land on this one.
|
|
144
|
+
if (!stressPending) {
|
|
145
|
+
wordStart = out.length;
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (out.length - wordStart < 3) {
|
|
149
|
+
wordStart = out.length;
|
|
150
|
+
return;
|
|
151
|
+
} // 0x5b2
|
|
152
|
+
for (let i = wordStart; i < out.length; i++) { // 0x5bc
|
|
153
|
+
if (classOf(out[i]) & CLASS.DIGIT) {
|
|
154
|
+
wordStart = out.length;
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
for (let i = wordStart; i + 1 < out.length; i++) { // 0x5da
|
|
159
|
+
const pair = String.fromCharCode(out[i], out[i + 1]);
|
|
160
|
+
if (tables.vowels.includes(pair)) {
|
|
161
|
+
out.splice(i + 2, 0, 0x34); // '4'
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
wordStart = out.length;
|
|
166
|
+
};
|
|
167
|
+
// ---------------------------------------------------------------- 0x3be
|
|
168
|
+
/**
|
|
169
|
+
* Read the next character of the word buffer and advance.
|
|
170
|
+
* `dir` +1 walks left (predecrement), -1 walks right (postincrement).
|
|
171
|
+
*/
|
|
172
|
+
const step = (dir) => (dir < 0 ? word[pos++] : word[--pos]) ?? SPACE;
|
|
173
|
+
/** The same, when only the character's class matters. */
|
|
174
|
+
const stepClass = (dir) => classOf(step(dir));
|
|
175
|
+
// ---------------------------------------------------------------- 0x398
|
|
176
|
+
/**
|
|
177
|
+
* Apply one wildcard. Returns true on match.
|
|
178
|
+
*
|
|
179
|
+
* The handlers are in jump-table order (hunk 0x60e). Two asymmetries are
|
|
180
|
+
* deliberate and reproduced: `*` does not step back after over-consuming,
|
|
181
|
+
* while `:` and `_` do; and `%` never checks that its `?NG` branch actually
|
|
182
|
+
* begins with an I, so any letter followed by NG at a word end counts as
|
|
183
|
+
* the suffix.
|
|
184
|
+
*/
|
|
185
|
+
const wildcard = (wc, dir) => {
|
|
186
|
+
switch (wc) {
|
|
187
|
+
case '#': return (stepClass(dir) & CLASS.VOWEL) !== 0;
|
|
188
|
+
case '*': {
|
|
189
|
+
if (!(stepClass(dir) & CLASS.CONSONANT))
|
|
190
|
+
return false;
|
|
191
|
+
while (stepClass(dir) & CLASS.CONSONANT) { /* consume; no step back */ }
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
case '.': {
|
|
195
|
+
const c = stepClass(dir);
|
|
196
|
+
return (c & CLASS.VOICED) !== 0 && (c & CLASS.CONSONANT) !== 0;
|
|
197
|
+
}
|
|
198
|
+
case '$': {
|
|
199
|
+
if (!(stepClass(dir) & CLASS.CONSONANT))
|
|
200
|
+
return false;
|
|
201
|
+
const c = step(dir);
|
|
202
|
+
return c === CH.I || c === CH.E;
|
|
203
|
+
}
|
|
204
|
+
case '%': { // 0x422
|
|
205
|
+
// A suffix only counts when it ends the word. `-ES`/`-ED`/`-ELY`
|
|
206
|
+
// check that directly (0x47a); `-ER` and `-ING` allow a trailing S
|
|
207
|
+
// first (0x46a), and ING falls through into the very same code.
|
|
208
|
+
const endsWord = () => (stepClass(dir) & CLASS.LETTER) === 0; // 0x47a
|
|
209
|
+
// 0x46a — the trailing-S branch exists only from 31.7 on; engines.ts.
|
|
210
|
+
const afterRorIng = () => {
|
|
211
|
+
if (!traits.suffixAllowsTrailingS)
|
|
212
|
+
return endsWord();
|
|
213
|
+
const c = step(dir);
|
|
214
|
+
if (!(classOf(c) & CLASS.LETTER))
|
|
215
|
+
return true;
|
|
216
|
+
if (c !== CH.S)
|
|
217
|
+
return false;
|
|
218
|
+
return endsWord();
|
|
219
|
+
};
|
|
220
|
+
if (step(dir) === CH.E) {
|
|
221
|
+
const c = step(dir);
|
|
222
|
+
if (!(classOf(c) & CLASS.LETTER))
|
|
223
|
+
return true; // bare E at word end
|
|
224
|
+
if (c === CH.S || c === CH.D)
|
|
225
|
+
return endsWord();
|
|
226
|
+
if (c === CH.R)
|
|
227
|
+
return afterRorIng();
|
|
228
|
+
if (c === CH.L)
|
|
229
|
+
return step(dir) === CH.Y ? endsWord() : false;
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
// 0x456: the leading letter is never checked, so anything followed by
|
|
233
|
+
// NG at a word end counts as -ING. Reproduced deliberately.
|
|
234
|
+
if (step(dir) !== CH.N)
|
|
235
|
+
return false;
|
|
236
|
+
if (step(dir) !== CH.G)
|
|
237
|
+
return false;
|
|
238
|
+
return afterRorIng();
|
|
239
|
+
}
|
|
240
|
+
case '&': return (stepClass(dir) & CLASS.SIBILANT) !== 0;
|
|
241
|
+
case '@': return (stepClass(dir) & CLASS.AFFECTS_U) !== 0;
|
|
242
|
+
case '^': return (stepClass(dir) & CLASS.CONSONANT) !== 0;
|
|
243
|
+
case '+': return (stepClass(dir) & CLASS.FRONT_VOWEL) !== 0;
|
|
244
|
+
case ':': {
|
|
245
|
+
while (stepClass(dir) & CLASS.CONSONANT) { /* consume */ }
|
|
246
|
+
pos += dir; // 0x4c2 step back
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
case '?': return (stepClass(dir) & CLASS.DIGIT) !== 0;
|
|
250
|
+
case '_': {
|
|
251
|
+
while (stepClass(dir) & CLASS.DIGIT) { /* consume */ }
|
|
252
|
+
pos += dir; // 0x4de step back
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
default: { // ' ' — 0x4e6
|
|
256
|
+
return (stepClass(dir) & CLASS.LETTER) === 0;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
/** Match a context pattern. Left runs right-to-left from `pos`. */
|
|
261
|
+
const matchContext = (pattern, dir) => {
|
|
262
|
+
const order = dir > 0
|
|
263
|
+
? [...pattern].reverse() // left context is scanned backwards
|
|
264
|
+
: [...pattern];
|
|
265
|
+
for (const ch of order) {
|
|
266
|
+
if (classOf(ch.charCodeAt(0)) & CLASS.WILDCARD) {
|
|
267
|
+
if (!wildcard(ch, dir))
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
if (dir > 0) {
|
|
272
|
+
if (word[--pos] !== ch.charCodeAt(0))
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
if (word[pos++] !== ch.charCodeAt(0))
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return true;
|
|
282
|
+
};
|
|
283
|
+
// ---------------------------------------------------------------- 0x23a
|
|
284
|
+
/** Bucket 0-25 for A-Z, 26 for digits, 27 for anything else. */
|
|
285
|
+
const bucketFor = (c) => {
|
|
286
|
+
const cl = classOf(c);
|
|
287
|
+
let k = c;
|
|
288
|
+
if (!(cl & CLASS.LETTER))
|
|
289
|
+
k = cl & CLASS.DIGIT ? 0x5b : 0x5c;
|
|
290
|
+
return k - 0x41;
|
|
291
|
+
};
|
|
292
|
+
const tryRules = () => {
|
|
293
|
+
const bucket = tables.buckets[bucketFor(word[pos])];
|
|
294
|
+
if (!bucket)
|
|
295
|
+
return false;
|
|
296
|
+
for (const [left, match, right, outStr, term] of bucket) {
|
|
297
|
+
const save = pos;
|
|
298
|
+
let ok = true;
|
|
299
|
+
for (let i = 0; i < match.length; i++) { // 0x266, literal
|
|
300
|
+
if (word[pos + i] !== match.charCodeAt(i)) {
|
|
301
|
+
ok = false;
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (!ok)
|
|
306
|
+
continue;
|
|
307
|
+
pos = save;
|
|
308
|
+
if (!matchContext(left, +1)) {
|
|
309
|
+
pos = save;
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
pos = save + match.length;
|
|
313
|
+
if (!matchContext(right, -1)) {
|
|
314
|
+
pos = save;
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
pos = save + match.length;
|
|
318
|
+
for (let i = 0; i < outStr.length; i++)
|
|
319
|
+
emit(outStr.charCodeAt(i));
|
|
320
|
+
// 0x334: a rule whose output ends in a letter leaves a stress mark
|
|
321
|
+
// pending, unless the rule was stored with a backtick terminator.
|
|
322
|
+
const last = out.length ? out[out.length - 1] : 0;
|
|
323
|
+
if (classOf(last) & CLASS.LETTER)
|
|
324
|
+
stressPending = term !== '`';
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
327
|
+
return false;
|
|
328
|
+
};
|
|
329
|
+
// ---------------------------------------------------------------- 0x17a
|
|
330
|
+
let rc = 0;
|
|
331
|
+
for (;;) {
|
|
332
|
+
if (out.length && out[out.length - 1] === HASH)
|
|
333
|
+
break; // 0x17a
|
|
334
|
+
if (pos === 0 || word[pos - 1] === SPACE) { // 0x184
|
|
335
|
+
const err = fillWord();
|
|
336
|
+
stressPass();
|
|
337
|
+
if (written < 0) {
|
|
338
|
+
rc = err;
|
|
339
|
+
break;
|
|
340
|
+
} // 0x19a tests D3, and returns D1
|
|
341
|
+
if (written === 0)
|
|
342
|
+
break; // 0x19c
|
|
343
|
+
pos = 2;
|
|
344
|
+
}
|
|
345
|
+
if (!tryRules())
|
|
346
|
+
pos++; // no rule matched: don't spin on this character
|
|
347
|
+
if (overflowed) {
|
|
348
|
+
rc = -(input.length - inPos);
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
stressPass(); // 0x1aa
|
|
353
|
+
if (out.length && out[out.length - 1] === HASH)
|
|
354
|
+
out.pop(); // 0x1ae
|
|
355
|
+
return { phonemes: String.fromCharCode(...out), rc };
|
|
356
|
+
}
|
|
357
|
+
/** Convenience wrapper around a fixed table set. */
|
|
358
|
+
export function createTranslator(tables) {
|
|
359
|
+
return {
|
|
360
|
+
version: tables.version,
|
|
361
|
+
translate: (text, outSize) => translate(text, tables, outSize),
|
|
362
|
+
};
|
|
363
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A rule from the letter-to-sound table: `left[match]right=out<term>`.
|
|
3
|
+
*
|
|
4
|
+
* `term` is the raw terminator byte the rule was stored with, `\` or a
|
|
5
|
+
* backtick. It is not decoration — see CLASS.LETTER and the stress pass in
|
|
6
|
+
* translate.ts.
|
|
7
|
+
*/
|
|
8
|
+
export interface Rule {
|
|
9
|
+
left: string;
|
|
10
|
+
match: string;
|
|
11
|
+
right: string;
|
|
12
|
+
out: string;
|
|
13
|
+
term: string;
|
|
14
|
+
}
|
|
15
|
+
/** Everything the matcher needs, extracted from one translator.library build. */
|
|
16
|
+
export interface TranslatorTables {
|
|
17
|
+
version: string;
|
|
18
|
+
source: string;
|
|
19
|
+
/** 128 entries, one per character code, of CLASS bit flags. */
|
|
20
|
+
classes: number[];
|
|
21
|
+
/** The pattern metacharacters, in dispatch order. */
|
|
22
|
+
wildcards: string;
|
|
23
|
+
/** The fifteen two-character vowel phonemes the stress pass looks for. */
|
|
24
|
+
vowels: string[];
|
|
25
|
+
/**
|
|
26
|
+
* 28 buckets: 0-25 for A-Z, 26 for digits, 27 for everything else.
|
|
27
|
+
* Order within a bucket decides which rule wins, so it is load-bearing.
|
|
28
|
+
*/
|
|
29
|
+
buckets: Array<Array<[string, string, string, string, string]>>;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Character class bits, read from the table at hunk 0x642.
|
|
33
|
+
*
|
|
34
|
+
* Bits 9 and 12-15 are never set in any shipped build. The names come from
|
|
35
|
+
* which wildcard handler tests each bit (jump table at hunk 0x60e), not from
|
|
36
|
+
* guesswork about what the letters have in common.
|
|
37
|
+
*/
|
|
38
|
+
export declare const CLASS: {
|
|
39
|
+
/** Not alphanumeric. */
|
|
40
|
+
readonly PUNCT: number;
|
|
41
|
+
/** `0`-`9`; tested by `?` and `_`, and by the stress pass. */
|
|
42
|
+
readonly DIGIT: number;
|
|
43
|
+
/** `@` — a consonant that changes a following long U: D J L N R S T Z. */
|
|
44
|
+
readonly AFFECTS_U: number;
|
|
45
|
+
/** `.` — voiced consonant: B D G J L M N R V W Z. */
|
|
46
|
+
readonly VOICED: number;
|
|
47
|
+
/** `&` — sibilant: C G J S X Z. */
|
|
48
|
+
readonly SIBILANT: number;
|
|
49
|
+
/** `^`, `*` and `:` — a consonant. */
|
|
50
|
+
readonly CONSONANT: number;
|
|
51
|
+
/** `#` — a vowel: A E I O U Y. */
|
|
52
|
+
readonly VOWEL: number;
|
|
53
|
+
/** Any letter. Gates whether a rule's output can take a stress mark. */
|
|
54
|
+
readonly LETTER: number;
|
|
55
|
+
/** `+` — a front vowel: E I Y. */
|
|
56
|
+
readonly FRONT_VOWEL: number;
|
|
57
|
+
/** The character is itself a pattern metacharacter. */
|
|
58
|
+
readonly WILDCARD: number;
|
|
59
|
+
/** A word or sentence delimiter. */
|
|
60
|
+
readonly DELIMITER: number;
|
|
61
|
+
};
|
|
62
|
+
export interface TranslateResult {
|
|
63
|
+
/** The phoneme string, as narrator.device expects it. */
|
|
64
|
+
phonemes: string;
|
|
65
|
+
/**
|
|
66
|
+
* 0 on success, or minus the number of input characters not consumed when
|
|
67
|
+
* the output buffer ran out — the library's own convention.
|
|
68
|
+
*/
|
|
69
|
+
rc: number;
|
|
70
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Character class bits, read from the table at hunk 0x642.
|
|
3
|
+
*
|
|
4
|
+
* Bits 9 and 12-15 are never set in any shipped build. The names come from
|
|
5
|
+
* which wildcard handler tests each bit (jump table at hunk 0x60e), not from
|
|
6
|
+
* guesswork about what the letters have in common.
|
|
7
|
+
*/
|
|
8
|
+
export const CLASS = {
|
|
9
|
+
/** Not alphanumeric. */
|
|
10
|
+
PUNCT: 1 << 0,
|
|
11
|
+
/** `0`-`9`; tested by `?` and `_`, and by the stress pass. */
|
|
12
|
+
DIGIT: 1 << 1,
|
|
13
|
+
/** `@` — a consonant that changes a following long U: D J L N R S T Z. */
|
|
14
|
+
AFFECTS_U: 1 << 2,
|
|
15
|
+
/** `.` — voiced consonant: B D G J L M N R V W Z. */
|
|
16
|
+
VOICED: 1 << 3,
|
|
17
|
+
/** `&` — sibilant: C G J S X Z. */
|
|
18
|
+
SIBILANT: 1 << 4,
|
|
19
|
+
/** `^`, `*` and `:` — a consonant. */
|
|
20
|
+
CONSONANT: 1 << 5,
|
|
21
|
+
/** `#` — a vowel: A E I O U Y. */
|
|
22
|
+
VOWEL: 1 << 6,
|
|
23
|
+
/** Any letter. Gates whether a rule's output can take a stress mark. */
|
|
24
|
+
LETTER: 1 << 7,
|
|
25
|
+
/** `+` — a front vowel: E I Y. */
|
|
26
|
+
FRONT_VOWEL: 1 << 8,
|
|
27
|
+
/** The character is itself a pattern metacharacter. */
|
|
28
|
+
WILDCARD: 1 << 10,
|
|
29
|
+
/** A word or sentence delimiter. */
|
|
30
|
+
DELIMITER: 1 << 11,
|
|
31
|
+
};
|
package/package.json
CHANGED
|
@@ -1,9 +1,62 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "narrator-ts",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "TypeScript reimplementation of the Amiga narrator.device and translator.library",
|
|
5
|
+
"type": "module",
|
|
5
6
|
"license": "MIT",
|
|
6
|
-
"
|
|
7
|
+
"author": "Gareth Davidson <gaz@bitplane.net>",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/bitplane/narrator-ts.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": "https://github.com/bitplane/narrator-ts/issues",
|
|
7
13
|
"homepage": "https://github.com/bitplane/narrator-ts#readme",
|
|
8
|
-
"
|
|
14
|
+
"keywords": [
|
|
15
|
+
"amiga",
|
|
16
|
+
"speech-synthesis",
|
|
17
|
+
"text-to-speech",
|
|
18
|
+
"tts",
|
|
19
|
+
"narrator",
|
|
20
|
+
"translator",
|
|
21
|
+
"formant",
|
|
22
|
+
"phonemes",
|
|
23
|
+
"retrocomputing"
|
|
24
|
+
],
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=18"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "tsc -p tsconfig.json",
|
|
30
|
+
"prepublishOnly": "npm run build && npm test",
|
|
31
|
+
"test": "vitest run",
|
|
32
|
+
"test:watch": "vitest",
|
|
33
|
+
"say": "vite-node tools/say.ts --",
|
|
34
|
+
"oracle:build": "bash tools/fetch-musashi.sh && make -C tools/oracle"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"typescript": "^5.7.2",
|
|
38
|
+
"vitest": "^2.1.8",
|
|
39
|
+
"@types/node": "^22.10.2"
|
|
40
|
+
},
|
|
41
|
+
"main": "./dist/narrator/index.js",
|
|
42
|
+
"types": "./dist/narrator/index.d.ts",
|
|
43
|
+
"exports": {
|
|
44
|
+
".": {
|
|
45
|
+
"types": "./dist/narrator/index.d.ts",
|
|
46
|
+
"default": "./dist/narrator/index.js"
|
|
47
|
+
},
|
|
48
|
+
"./translator": {
|
|
49
|
+
"types": "./dist/translator/index.d.ts",
|
|
50
|
+
"default": "./dist/translator/index.js"
|
|
51
|
+
},
|
|
52
|
+
"./reference/nrl-table.json": "./reference/nrl-table.json",
|
|
53
|
+
"./reference/voice-free.json": "./reference/voice-free.json",
|
|
54
|
+
"./package.json": "./package.json"
|
|
55
|
+
},
|
|
56
|
+
"files": [
|
|
57
|
+
"dist",
|
|
58
|
+
"reference",
|
|
59
|
+
"README.md",
|
|
60
|
+
"LICENSE"
|
|
61
|
+
]
|
|
9
62
|
}
|