narrator-ts 0.1.1 → 0.1.2
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 +11 -1
- package/dist/aros-resource/index.d.ts +16 -1
- package/dist/aros-resource/index.js +71 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -87,7 +87,9 @@ Create the versioned IFF resource consumed by AROS `translator.library`,
|
|
|
87
87
|
npm run export:aros -- -o speech.iff
|
|
88
88
|
npm run export:aros -- -o speech.iff \
|
|
89
89
|
--translator data/translator-33.2.json \
|
|
90
|
-
--voice data/narrator-33.2.json
|
|
90
|
+
--voice data/narrator-33.2.json \
|
|
91
|
+
--translator-license "User supplied; redistribution not granted" \
|
|
92
|
+
--voice-license "User supplied; redistribution not granted"
|
|
91
93
|
npm run export:aros -- -o speech-resource.c --format c
|
|
92
94
|
```
|
|
93
95
|
|
|
@@ -96,6 +98,14 @@ locally extracted authentic tables; the resulting file has the same legal
|
|
|
96
98
|
status as those inputs and should not be redistributed.
|
|
97
99
|
The C form contains the identical IFF payload as a byte array for ROM builds.
|
|
98
100
|
|
|
101
|
+
`FORM NARR` uses ordinary length-delimited Latin-1 IFF chunks rather than an
|
|
102
|
+
embedded serialization format. `VERS` is the binary resource schema version;
|
|
103
|
+
`FVER` records the narrator-ts package version. `TVER`/`TSRC`/`TLIC` describe
|
|
104
|
+
the translator and `VVER`/`VSRC`/`VLIC` describe the voice, followed by their
|
|
105
|
+
`LTRS` and `NVOI` table chunks. The built-in free inputs are labelled public
|
|
106
|
+
domain. Explicit table paths conservatively default to `User supplied;
|
|
107
|
+
redistribution not granted`; the two `--*-license` options override that text.
|
|
108
|
+
|
|
99
109
|
## Status
|
|
100
110
|
|
|
101
111
|
| | |
|
|
@@ -14,6 +14,21 @@ declare const ALT_VOICE_COLUMNS: readonly ["f1", "f2", "f3"];
|
|
|
14
14
|
export interface ArosResourceInput {
|
|
15
15
|
translator?: TranslatorTables;
|
|
16
16
|
voice?: VoiceData;
|
|
17
|
+
metadata: {
|
|
18
|
+
generator: string;
|
|
19
|
+
translatorLicense?: string;
|
|
20
|
+
voiceLicense?: string;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export interface ArosComponentMetadata {
|
|
24
|
+
version?: string;
|
|
25
|
+
source?: string;
|
|
26
|
+
license?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface ArosResourceMetadata {
|
|
29
|
+
generator?: string;
|
|
30
|
+
translator?: ArosComponentMetadata;
|
|
31
|
+
voice?: ArosComponentMetadata;
|
|
17
32
|
}
|
|
18
33
|
export interface ArosVoiceTables {
|
|
19
34
|
names: string[];
|
|
@@ -26,7 +41,7 @@ export interface ArosVoiceTables {
|
|
|
26
41
|
}
|
|
27
42
|
export interface DecodedArosResource {
|
|
28
43
|
version: number;
|
|
29
|
-
metadata:
|
|
44
|
+
metadata: ArosResourceMetadata;
|
|
30
45
|
translator?: TranslatorTables;
|
|
31
46
|
voice?: ArosVoiceTables;
|
|
32
47
|
}
|
|
@@ -49,6 +49,16 @@ class Writer {
|
|
|
49
49
|
this.u8(code);
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
|
+
text(value, label) {
|
|
53
|
+
if (value.length === 0 || value.length > 0xff)
|
|
54
|
+
throw new RangeError(`${label} must be 1..255 bytes`);
|
|
55
|
+
for (const c of value) {
|
|
56
|
+
const code = c.charCodeAt(0);
|
|
57
|
+
if (code === 0 || code > 0xff)
|
|
58
|
+
throw new RangeError(`${label} is not non-NUL Latin-1 text`);
|
|
59
|
+
this.u8(code);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
52
62
|
finish() { return Uint8Array.from(this.bytes); }
|
|
53
63
|
}
|
|
54
64
|
class Reader {
|
|
@@ -98,6 +108,11 @@ function chunk(id, payload) {
|
|
|
98
108
|
w.u8(0);
|
|
99
109
|
return w.finish();
|
|
100
110
|
}
|
|
111
|
+
function textChunk(id, value) {
|
|
112
|
+
const w = new Writer();
|
|
113
|
+
w.text(value, id);
|
|
114
|
+
return chunk(id, w.finish());
|
|
115
|
+
}
|
|
101
116
|
function translatorChunk(data) {
|
|
102
117
|
if (data.classes.length !== 128 || data.buckets.length !== 28)
|
|
103
118
|
throw new RangeError('invalid translator table shape');
|
|
@@ -167,18 +182,27 @@ function voiceChunk(data) {
|
|
|
167
182
|
export function encodeArosResource(input) {
|
|
168
183
|
if (!input.translator && !input.voice)
|
|
169
184
|
throw new RangeError('resource contains no tables');
|
|
185
|
+
if (!input.metadata?.generator)
|
|
186
|
+
throw new RangeError('resource generator is required');
|
|
187
|
+
if (input.translator && !input.metadata.translatorLicense)
|
|
188
|
+
throw new RangeError('translator license is required');
|
|
189
|
+
if (input.voice && !input.metadata.voiceLicense)
|
|
190
|
+
throw new RangeError('voice license is required');
|
|
170
191
|
const version = new Writer();
|
|
171
192
|
version.u32(AROS_RESOURCE_VERSION);
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const chunks = [chunk('VERS', version.finish()), chunk('META', meta)];
|
|
178
|
-
if (input.translator)
|
|
193
|
+
const chunks = [chunk('VERS', version.finish()), textChunk('FVER', input.metadata.generator)];
|
|
194
|
+
if (input.translator) {
|
|
195
|
+
chunks.push(textChunk('TVER', input.translator.version));
|
|
196
|
+
chunks.push(textChunk('TSRC', input.translator.source));
|
|
197
|
+
chunks.push(textChunk('TLIC', input.metadata.translatorLicense));
|
|
179
198
|
chunks.push(chunk('LTRS', translatorChunk(input.translator)));
|
|
180
|
-
|
|
199
|
+
}
|
|
200
|
+
if (input.voice) {
|
|
201
|
+
chunks.push(textChunk('VVER', input.voice.version));
|
|
202
|
+
chunks.push(textChunk('VSRC', input.voice.source));
|
|
203
|
+
chunks.push(textChunk('VLIC', input.metadata.voiceLicense));
|
|
181
204
|
chunks.push(chunk('NVOI', voiceChunk(input.voice)));
|
|
205
|
+
}
|
|
182
206
|
const body = concat(chunks);
|
|
183
207
|
const head = new Writer();
|
|
184
208
|
head.id('FORM');
|
|
@@ -215,18 +239,31 @@ function chunksFrom(bytes) {
|
|
|
215
239
|
if (ascii(0) !== 'FORM' || ascii(8) !== AROS_RESOURCE_FORM || u32(4) + 8 !== bytes.length)
|
|
216
240
|
throw new RangeError('not an AROS Narrator IFF resource');
|
|
217
241
|
const chunks = new Map();
|
|
218
|
-
|
|
242
|
+
const singletons = new Set(['VERS', 'FVER', 'TVER', 'TSRC', 'TLIC', 'LTRS', 'VVER', 'VSRC', 'VLIC', 'NVOI']);
|
|
243
|
+
let at = 12;
|
|
244
|
+
while (at < bytes.length) {
|
|
219
245
|
if (at + 8 > bytes.length)
|
|
220
246
|
throw new RangeError('truncated IFF chunk');
|
|
221
247
|
const id = ascii(at), size = u32(at + 4), start = at + 8, end = start + size;
|
|
222
248
|
if (end > bytes.length)
|
|
223
249
|
throw new RangeError('truncated IFF chunk payload');
|
|
250
|
+
if (singletons.has(id) && chunks.has(id))
|
|
251
|
+
throw new RangeError(`duplicate ${id} chunk`);
|
|
224
252
|
chunks.set(id, bytes.subarray(start, end));
|
|
225
253
|
at = end + (size & 1);
|
|
226
254
|
}
|
|
255
|
+
if (at !== bytes.length)
|
|
256
|
+
throw new RangeError('missing IFF pad byte');
|
|
227
257
|
return chunks;
|
|
228
258
|
}
|
|
229
|
-
function
|
|
259
|
+
function decodeText(bytes, id) {
|
|
260
|
+
if (!bytes)
|
|
261
|
+
return undefined;
|
|
262
|
+
if (bytes.length === 0 || bytes.length > 0xff || bytes.includes(0))
|
|
263
|
+
throw new RangeError(`invalid ${id} text chunk`);
|
|
264
|
+
return String.fromCharCode(...bytes);
|
|
265
|
+
}
|
|
266
|
+
function decodeTranslator(bytes, metadata) {
|
|
230
267
|
const r = new Reader(bytes);
|
|
231
268
|
const classes = Array.from({ length: r.u16() }, () => r.u16());
|
|
232
269
|
const wildcards = r.latin8();
|
|
@@ -242,7 +279,11 @@ function decodeTranslator(bytes) {
|
|
|
242
279
|
}
|
|
243
280
|
if (!r.done())
|
|
244
281
|
throw new RangeError('trailing translator data');
|
|
245
|
-
return {
|
|
282
|
+
return {
|
|
283
|
+
version: metadata?.version ?? 'resource',
|
|
284
|
+
source: metadata?.source ?? 'IFF LTRS',
|
|
285
|
+
classes, wildcards, vowels, buckets,
|
|
286
|
+
};
|
|
246
287
|
}
|
|
247
288
|
function decodeVoice(bytes) {
|
|
248
289
|
const r = new Reader(bytes), count = r.u16();
|
|
@@ -275,8 +316,25 @@ export function decodeArosResource(bytes) {
|
|
|
275
316
|
const version = new DataView(versionBytes.buffer, versionBytes.byteOffset, 4).getUint32(0);
|
|
276
317
|
if (version !== AROS_RESOURCE_VERSION)
|
|
277
318
|
throw new RangeError(`unsupported resource version ${version}`);
|
|
278
|
-
const
|
|
279
|
-
|
|
319
|
+
const translatorMetadata = {
|
|
320
|
+
version: decodeText(chunks.get('TVER'), 'TVER'),
|
|
321
|
+
source: decodeText(chunks.get('TSRC'), 'TSRC'),
|
|
322
|
+
license: decodeText(chunks.get('TLIC'), 'TLIC'),
|
|
323
|
+
};
|
|
324
|
+
const voiceMetadata = {
|
|
325
|
+
version: decodeText(chunks.get('VVER'), 'VVER'),
|
|
326
|
+
source: decodeText(chunks.get('VSRC'), 'VSRC'),
|
|
327
|
+
license: decodeText(chunks.get('VLIC'), 'VLIC'),
|
|
328
|
+
};
|
|
329
|
+
const metadata = {};
|
|
330
|
+
const generator = decodeText(chunks.get('FVER'), 'FVER');
|
|
331
|
+
if (generator !== undefined)
|
|
332
|
+
metadata.generator = generator;
|
|
333
|
+
if (Object.values(translatorMetadata).some((value) => value !== undefined))
|
|
334
|
+
metadata.translator = translatorMetadata;
|
|
335
|
+
if (Object.values(voiceMetadata).some((value) => value !== undefined))
|
|
336
|
+
metadata.voice = voiceMetadata;
|
|
337
|
+
const translator = chunks.has('LTRS') ? decodeTranslator(chunks.get('LTRS'), metadata.translator) : undefined;
|
|
280
338
|
const voice = chunks.has('NVOI') ? decodeVoice(chunks.get('NVOI')) : undefined;
|
|
281
339
|
return { version, metadata, translator, voice };
|
|
282
340
|
}
|