@willwade/aac-processors 0.2.20 → 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 +1 -0
- package/dist/browser/index.browser.js +17 -1
- package/dist/browser/processors/gotalkNow/nsKeyed.js +108 -0
- package/dist/browser/processors/gotalkNowProcessor.js +538 -0
- package/dist/browser/processors/gridsetProcessor.js +2 -2
- package/dist/browser/processors/obfProcessor.js +17 -6
- package/dist/browser/validation/gridsetValidator.js +54 -198
- package/dist/cli/index.js +3 -0
- package/dist/core/analyze.js +5 -0
- package/dist/core/treeStructure.d.ts +12 -0
- package/dist/gotalkNow.d.ts +6 -0
- package/dist/gotalkNow.js +11 -0
- package/dist/index.browser.d.ts +1 -0
- package/dist/index.browser.js +19 -2
- package/dist/index.node.d.ts +1 -0
- package/dist/index.node.js +6 -1
- package/dist/processors/gotalkNow/nsKeyed.d.ts +27 -0
- package/dist/processors/gotalkNow/nsKeyed.js +112 -0
- package/dist/processors/gotalkNowProcessor.d.ts +51 -0
- package/dist/processors/gotalkNowProcessor.js +544 -0
- package/dist/processors/gridsetProcessor.js +2 -2
- package/dist/processors/index.d.ts +1 -0
- package/dist/processors/index.js +3 -1
- package/dist/processors/obfProcessor.js +17 -6
- package/dist/validation/gridsetValidator.d.ts +8 -15
- package/dist/validation/gridsetValidator.js +54 -198
- package/package.json +5 -1
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GoTalk NOW Processor
|
|
3
|
+
*
|
|
4
|
+
* Reads, translates, and converts Attainment Company's GoTalk NOW
|
|
5
|
+
* communication-board files (`.gtbz`).
|
|
6
|
+
*
|
|
7
|
+
* A `.gtbz` file is a ZIP archive containing Apple property lists:
|
|
8
|
+
* - BookInfo.plist – format version (GoTalkBookFormatVersion)
|
|
9
|
+
* - PageData.plist – pages dict keyed by page id (positive = content,
|
|
10
|
+
* negative = system/express-bar pages)
|
|
11
|
+
* - PageOrder.plist – ordered array of page ids
|
|
12
|
+
* - PageHistory.plist – navigation history (usually empty)
|
|
13
|
+
* - SettingsWriteOut.plist– app-level settings (TTS, scanning, …)
|
|
14
|
+
* - IconSource-<id>.png – page icons
|
|
15
|
+
* - Source-<page>-<btn>-<guid>-…png – button images
|
|
16
|
+
* - <page>-<btn>.caf – audio recordings (Core Audio Format)
|
|
17
|
+
* - Backup-<n>.zip – rolling backups
|
|
18
|
+
* - REQUIRESREGEN – marker file
|
|
19
|
+
*
|
|
20
|
+
* Colours and fonts in PageData are NSKeyedArchiver-encoded binary plists
|
|
21
|
+
* carried inside `<data>` tags. They are treated as opaque blobs and preserved
|
|
22
|
+
* verbatim so that only text fields are touched during translation.
|
|
23
|
+
*/
|
|
24
|
+
import { BaseProcessor, ProcessorOptions, ExtractStringsResult, TranslatedString, SourceString } from '../core/baseProcessor';
|
|
25
|
+
import { AACTree } from '../core/treeStructure';
|
|
26
|
+
import { ProcessorInput } from '../utils/io';
|
|
27
|
+
declare class GotalkNowProcessor extends BaseProcessor {
|
|
28
|
+
readonly capabilities: {
|
|
29
|
+
wordList: "none";
|
|
30
|
+
preservesAssetsOnSave: boolean;
|
|
31
|
+
newCellCreation: "allowed";
|
|
32
|
+
};
|
|
33
|
+
constructor(options?: ProcessorOptions);
|
|
34
|
+
private openZip;
|
|
35
|
+
private readPlistFromZip;
|
|
36
|
+
extractTexts(filePathOrBuffer: ProcessorInput): Promise<string[]>;
|
|
37
|
+
loadIntoTree(filePathOrBuffer: ProcessorInput): Promise<AACTree>;
|
|
38
|
+
processTexts(filePathOrBuffer: ProcessorInput, translations: Map<string, string>, outputPath: string): Promise<Uint8Array>;
|
|
39
|
+
saveFromTree(tree: AACTree, outputPath: string): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Extract strings with metadata for aac-tools-platform compatibility.
|
|
42
|
+
* Uses the generic implementation from BaseProcessor.
|
|
43
|
+
*/
|
|
44
|
+
extractStringsWithMetadata(filePath: string): Promise<ExtractStringsResult>;
|
|
45
|
+
/**
|
|
46
|
+
* Generate translated download for aac-tools-platform compatibility.
|
|
47
|
+
* Uses the generic implementation from BaseProcessor.
|
|
48
|
+
*/
|
|
49
|
+
generateTranslatedDownload(filePath: string, translatedStrings: TranslatedString[], sourceStrings: SourceString[]): Promise<string>;
|
|
50
|
+
}
|
|
51
|
+
export { GotalkNowProcessor };
|
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* GoTalk NOW Processor
|
|
4
|
+
*
|
|
5
|
+
* Reads, translates, and converts Attainment Company's GoTalk NOW
|
|
6
|
+
* communication-board files (`.gtbz`).
|
|
7
|
+
*
|
|
8
|
+
* A `.gtbz` file is a ZIP archive containing Apple property lists:
|
|
9
|
+
* - BookInfo.plist – format version (GoTalkBookFormatVersion)
|
|
10
|
+
* - PageData.plist – pages dict keyed by page id (positive = content,
|
|
11
|
+
* negative = system/express-bar pages)
|
|
12
|
+
* - PageOrder.plist – ordered array of page ids
|
|
13
|
+
* - PageHistory.plist – navigation history (usually empty)
|
|
14
|
+
* - SettingsWriteOut.plist– app-level settings (TTS, scanning, …)
|
|
15
|
+
* - IconSource-<id>.png – page icons
|
|
16
|
+
* - Source-<page>-<btn>-<guid>-…png – button images
|
|
17
|
+
* - <page>-<btn>.caf – audio recordings (Core Audio Format)
|
|
18
|
+
* - Backup-<n>.zip – rolling backups
|
|
19
|
+
* - REQUIRESREGEN – marker file
|
|
20
|
+
*
|
|
21
|
+
* Colours and fonts in PageData are NSKeyedArchiver-encoded binary plists
|
|
22
|
+
* carried inside `<data>` tags. They are treated as opaque blobs and preserved
|
|
23
|
+
* verbatim so that only text fields are touched during translation.
|
|
24
|
+
*/
|
|
25
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
26
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
27
|
+
};
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.GotalkNowProcessor = void 0;
|
|
30
|
+
const baseProcessor_1 = require("../core/baseProcessor");
|
|
31
|
+
const treeStructure_1 = require("../core/treeStructure");
|
|
32
|
+
const plist_1 = __importDefault(require("plist"));
|
|
33
|
+
const validationTypes_1 = require("../validation/validationTypes");
|
|
34
|
+
const io_1 = require("../utils/io");
|
|
35
|
+
const nsKeyed_1 = require("./gotalkNow/nsKeyed");
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// GoTalk NOW button type → semantic-action mapping
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
function buildSemanticAction(rawButton, pageId, buttonIndex) {
|
|
40
|
+
const buttonType = rawButton.ButtonType || 'TTS';
|
|
41
|
+
const actionData = rawButton.ActionData || {};
|
|
42
|
+
const label = rawButton.ButtonText ?? '';
|
|
43
|
+
const ttsText = actionData.TTSText;
|
|
44
|
+
const spoken = ttsText && ttsText.length > 0 ? ttsText : label;
|
|
45
|
+
switch (buttonType) {
|
|
46
|
+
case 'Jump': {
|
|
47
|
+
const jumpTo = actionData.JumpTo;
|
|
48
|
+
const target = jumpTo !== undefined ? String(jumpTo) : undefined;
|
|
49
|
+
return {
|
|
50
|
+
semanticAction: {
|
|
51
|
+
category: treeStructure_1.AACSemanticCategory.NAVIGATION,
|
|
52
|
+
intent: treeStructure_1.AACSemanticIntent.NAVIGATE_TO,
|
|
53
|
+
targetId: target,
|
|
54
|
+
platformData: {
|
|
55
|
+
gotalkNow: { buttonType: 'Jump', jumpTo, jumpBook: actionData.JumpBook },
|
|
56
|
+
},
|
|
57
|
+
fallback: { type: 'NAVIGATE', targetPageId: target },
|
|
58
|
+
},
|
|
59
|
+
message: '',
|
|
60
|
+
targetPageId: target,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
case 'Audio':
|
|
64
|
+
case 'Recorded': {
|
|
65
|
+
// Recorded audio: plays a .caf file named <pageId>-<buttonIndex>.caf.
|
|
66
|
+
// Recorded buttons may also have TrimStart/TrimEnd for audio trimming.
|
|
67
|
+
const audioLocation = `${pageId}-${buttonIndex}.caf`;
|
|
68
|
+
return {
|
|
69
|
+
semanticAction: {
|
|
70
|
+
category: treeStructure_1.AACSemanticCategory.MEDIA,
|
|
71
|
+
intent: treeStructure_1.AACSemanticIntent.PLAY_SOUND,
|
|
72
|
+
platformData: {
|
|
73
|
+
gotalkNow: {
|
|
74
|
+
buttonType,
|
|
75
|
+
audioLocation,
|
|
76
|
+
trimStart: actionData.TrimStart,
|
|
77
|
+
trimEnd: actionData.TrimEnd,
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
fallback: { type: 'ACTION' },
|
|
81
|
+
},
|
|
82
|
+
message: spoken,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
case 'URL': {
|
|
86
|
+
return {
|
|
87
|
+
semanticAction: {
|
|
88
|
+
category: treeStructure_1.AACSemanticCategory.SYSTEM_CONTROL,
|
|
89
|
+
intent: treeStructure_1.AACSemanticIntent.WEB_NAVIGATE,
|
|
90
|
+
text: actionData.URL,
|
|
91
|
+
platformData: {
|
|
92
|
+
gotalkNow: {
|
|
93
|
+
buttonType: 'URL',
|
|
94
|
+
url: actionData.URL,
|
|
95
|
+
openInternally: actionData.OpenInternally,
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
fallback: { type: 'ACTION' },
|
|
99
|
+
},
|
|
100
|
+
message: spoken,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
case 'GoVisual': {
|
|
104
|
+
return {
|
|
105
|
+
semanticAction: {
|
|
106
|
+
category: treeStructure_1.AACSemanticCategory.SYSTEM_CONTROL,
|
|
107
|
+
intent: treeStructure_1.AACSemanticIntent.PLATFORM_SPECIFIC,
|
|
108
|
+
platformData: {
|
|
109
|
+
gotalkNow: {
|
|
110
|
+
buttonType: 'GoVisual',
|
|
111
|
+
goVisual: actionData.GoVisual,
|
|
112
|
+
goVisualLink: actionData.GoVisualLink,
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
fallback: { type: 'ACTION' },
|
|
116
|
+
},
|
|
117
|
+
message: spoken,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
case 'Video': {
|
|
121
|
+
return {
|
|
122
|
+
semanticAction: {
|
|
123
|
+
category: treeStructure_1.AACSemanticCategory.MEDIA,
|
|
124
|
+
intent: treeStructure_1.AACSemanticIntent.PLAY_VIDEO,
|
|
125
|
+
platformData: {
|
|
126
|
+
gotalkNow: { buttonType: 'Video' },
|
|
127
|
+
},
|
|
128
|
+
fallback: { type: 'ACTION' },
|
|
129
|
+
},
|
|
130
|
+
message: spoken,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
case 'Express':
|
|
134
|
+
case 'Bookmark': {
|
|
135
|
+
return {
|
|
136
|
+
semanticAction: {
|
|
137
|
+
category: treeStructure_1.AACSemanticCategory.SYSTEM_CONTROL,
|
|
138
|
+
intent: treeStructure_1.AACSemanticIntent.PLATFORM_SPECIFIC,
|
|
139
|
+
platformData: {
|
|
140
|
+
gotalkNow: { buttonType },
|
|
141
|
+
},
|
|
142
|
+
fallback: { type: 'ACTION' },
|
|
143
|
+
},
|
|
144
|
+
message: spoken,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
case 'TTS':
|
|
148
|
+
default: {
|
|
149
|
+
// Empty/placeholder cells (no ButtonType or no label) get a SPEAK with empty text.
|
|
150
|
+
return {
|
|
151
|
+
semanticAction: {
|
|
152
|
+
category: treeStructure_1.AACSemanticCategory.COMMUNICATION,
|
|
153
|
+
intent: treeStructure_1.AACSemanticIntent.SPEAK_TEXT,
|
|
154
|
+
text: spoken,
|
|
155
|
+
platformData: {
|
|
156
|
+
gotalkNow: { buttonType: buttonType === 'TTS' ? 'TTS' : 'Empty' },
|
|
157
|
+
},
|
|
158
|
+
fallback: { type: 'SPEAK', message: spoken },
|
|
159
|
+
},
|
|
160
|
+
message: spoken,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Derive a square-ish grid layout from the declared button count.
|
|
167
|
+
* GoTalk NOW uses fixed layouts (1, 4, 9, 16, 25, 36 …).
|
|
168
|
+
*/
|
|
169
|
+
function gridDimensionsFromButtonCount(count) {
|
|
170
|
+
const side = Math.max(1, Math.ceil(Math.sqrt(Math.max(count, 1))));
|
|
171
|
+
return { rows: side, cols: side };
|
|
172
|
+
}
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
// Processor
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
class GotalkNowProcessor extends baseProcessor_1.BaseProcessor {
|
|
177
|
+
constructor(options) {
|
|
178
|
+
super(options);
|
|
179
|
+
this.capabilities = {
|
|
180
|
+
wordList: 'none',
|
|
181
|
+
preservesAssetsOnSave: true,
|
|
182
|
+
newCellCreation: 'allowed',
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
// ---- Zip access ---------------------------------------------------------
|
|
186
|
+
async openZip(filePathOrBuffer) {
|
|
187
|
+
const zip = await this.options.zipAdapter(filePathOrBuffer, this.options.fileAdapter);
|
|
188
|
+
return zip;
|
|
189
|
+
}
|
|
190
|
+
async readPlistFromZip(zip, name, fallback) {
|
|
191
|
+
const files = zip.listFiles();
|
|
192
|
+
if (!files.includes(name)) {
|
|
193
|
+
if (fallback)
|
|
194
|
+
return fallback();
|
|
195
|
+
throw new Error(`GoTalk NOW archive is missing ${name}`);
|
|
196
|
+
}
|
|
197
|
+
const bytes = await zip.readFile(name);
|
|
198
|
+
const text = typeof Buffer !== 'undefined' && Buffer.isBuffer(bytes)
|
|
199
|
+
? bytes.toString('utf8')
|
|
200
|
+
: new TextDecoder().decode(bytes);
|
|
201
|
+
return plist_1.default.parse(text);
|
|
202
|
+
}
|
|
203
|
+
// ---- extractTexts -------------------------------------------------------
|
|
204
|
+
async extractTexts(filePathOrBuffer) {
|
|
205
|
+
const tree = await this.loadIntoTree(filePathOrBuffer);
|
|
206
|
+
const texts = [];
|
|
207
|
+
for (const pageId in tree.pages) {
|
|
208
|
+
const page = tree.pages[pageId];
|
|
209
|
+
if (page.name)
|
|
210
|
+
texts.push(page.name);
|
|
211
|
+
page.buttons.forEach((btn) => {
|
|
212
|
+
if (btn.label)
|
|
213
|
+
texts.push(btn.label);
|
|
214
|
+
if (btn.message && btn.message !== btn.label)
|
|
215
|
+
texts.push(btn.message);
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
return texts;
|
|
219
|
+
}
|
|
220
|
+
// ---- loadIntoTree -------------------------------------------------------
|
|
221
|
+
async loadIntoTree(filePathOrBuffer) {
|
|
222
|
+
const { getFileSize, pathExists } = this.options.fileAdapter;
|
|
223
|
+
const filename = typeof filePathOrBuffer === 'string' ? (0, io_1.getBasename)(filePathOrBuffer) : 'upload.gtbz';
|
|
224
|
+
let zip;
|
|
225
|
+
let pageDataValue;
|
|
226
|
+
let pageOrderValue;
|
|
227
|
+
let bookInfoValue;
|
|
228
|
+
try {
|
|
229
|
+
zip = await this.openZip(filePathOrBuffer);
|
|
230
|
+
pageDataValue = await this.readPlistFromZip(zip, 'PageData.plist');
|
|
231
|
+
pageOrderValue = await this.readPlistFromZip(zip, 'PageOrder.plist', () => []);
|
|
232
|
+
bookInfoValue = await this.readPlistFromZip(zip, 'BookInfo.plist', () => ({}));
|
|
233
|
+
}
|
|
234
|
+
catch (err) {
|
|
235
|
+
if (err instanceof validationTypes_1.ValidationFailureError)
|
|
236
|
+
throw err;
|
|
237
|
+
const validation = (0, validationTypes_1.buildValidationResultFromMessage)({
|
|
238
|
+
filename,
|
|
239
|
+
filesize: typeof filePathOrBuffer === 'string'
|
|
240
|
+
? (await pathExists(filePathOrBuffer))
|
|
241
|
+
? await getFileSize(filePathOrBuffer)
|
|
242
|
+
: 0
|
|
243
|
+
: 0,
|
|
244
|
+
format: 'gotalknow',
|
|
245
|
+
message: err?.message || 'Failed to open GoTalk NOW archive',
|
|
246
|
+
type: 'structure',
|
|
247
|
+
description: 'gtbz archive',
|
|
248
|
+
});
|
|
249
|
+
throw new validationTypes_1.ValidationFailureError('Failed to load GoTalk NOW file', validation, err);
|
|
250
|
+
}
|
|
251
|
+
const pageData = pageDataValue || {};
|
|
252
|
+
const pageOrderRaw = Array.isArray(pageOrderValue) ? pageOrderValue : [];
|
|
253
|
+
const tree = new treeStructure_1.AACTree();
|
|
254
|
+
tree.metadata.format = 'gotalknow';
|
|
255
|
+
// Format version
|
|
256
|
+
const bookInfo = bookInfoValue || {};
|
|
257
|
+
if (bookInfo.GoTalkBookFormatVersion !== undefined) {
|
|
258
|
+
const version = bookInfo.GoTalkBookFormatVersion;
|
|
259
|
+
tree.metadata.version =
|
|
260
|
+
typeof version === 'number' || typeof version === 'string' ? String(version) : '1';
|
|
261
|
+
}
|
|
262
|
+
// Preserve the ordered list of page ids for round-trip fidelity.
|
|
263
|
+
const orderedPageIds = pageOrderRaw.map((id) => String(id));
|
|
264
|
+
tree.metadata.pageOrder = orderedPageIds;
|
|
265
|
+
// The first page in PageOrder (if any) is treated as the home page.
|
|
266
|
+
if (orderedPageIds.length > 0) {
|
|
267
|
+
tree.metadata.defaultHomePageId = orderedPageIds[0];
|
|
268
|
+
}
|
|
269
|
+
// Track available image files so we can resolve ButtonImages locations.
|
|
270
|
+
const zipFiles = new Set(zip.listFiles());
|
|
271
|
+
// Build pages. Use the union of PageData keys and PageOrder ids.
|
|
272
|
+
const allPageIds = new Set([...Object.keys(pageData), ...orderedPageIds]);
|
|
273
|
+
// Deterministic ordering: ordered pages first (in order), then any extras sorted numerically.
|
|
274
|
+
const sortedPageIds = [
|
|
275
|
+
...orderedPageIds.filter((id) => allPageIds.has(id)),
|
|
276
|
+
...[...allPageIds]
|
|
277
|
+
.filter((id) => !orderedPageIds.includes(id))
|
|
278
|
+
.sort((a, b) => Number(a) - Number(b)),
|
|
279
|
+
];
|
|
280
|
+
if (sortedPageIds.length === 0) {
|
|
281
|
+
const validation = (0, validationTypes_1.buildValidationResultFromMessage)({
|
|
282
|
+
filename,
|
|
283
|
+
filesize: 0,
|
|
284
|
+
format: 'gotalknow',
|
|
285
|
+
message: 'No pages found in GoTalk NOW PageData.plist',
|
|
286
|
+
type: 'structure',
|
|
287
|
+
description: 'PageData.plist',
|
|
288
|
+
});
|
|
289
|
+
throw new validationTypes_1.ValidationFailureError('GoTalk NOW file has no pages', validation);
|
|
290
|
+
}
|
|
291
|
+
for (const pageId of sortedPageIds) {
|
|
292
|
+
const rawPage = pageData[pageId] || { Buttons: {} };
|
|
293
|
+
const pageTitle = typeof rawPage.PageTitle === 'string' && rawPage.PageTitle.trim().length > 0
|
|
294
|
+
? rawPage.PageTitle
|
|
295
|
+
: `Page ${pageId}`;
|
|
296
|
+
const page = new treeStructure_1.AACPage({
|
|
297
|
+
id: pageId,
|
|
298
|
+
name: pageTitle,
|
|
299
|
+
grid: [],
|
|
300
|
+
buttons: [],
|
|
301
|
+
parentId: null,
|
|
302
|
+
});
|
|
303
|
+
// Page-level style: background colour and title font.
|
|
304
|
+
const pageBg = (0, nsKeyed_1.decodeNsColor)(rawPage.BackgroundColor);
|
|
305
|
+
const pageFont = (0, nsKeyed_1.decodeNsFont)(rawPage.TitleFont);
|
|
306
|
+
if (pageBg || pageFont.fontFamily) {
|
|
307
|
+
page.style = {
|
|
308
|
+
...(page.style || {}),
|
|
309
|
+
...(pageBg ? { backgroundColor: pageBg } : {}),
|
|
310
|
+
...(pageFont.fontFamily ? { fontFamily: pageFont.fontFamily } : {}),
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
if (rawPage.Enabled === false) {
|
|
314
|
+
// Disabled pages are retained but flagged for fidelity.
|
|
315
|
+
page.enabled = false;
|
|
316
|
+
}
|
|
317
|
+
const buttonsById = rawPage.Buttons || {};
|
|
318
|
+
// Numeric sort of button indices ("0","1",…,"10",…)
|
|
319
|
+
const buttonIndices = Object.keys(buttonsById).sort((a, b) => Number(a) - Number(b));
|
|
320
|
+
const buttonCount = rawPage.ButtonCount ?? buttonIndices.length;
|
|
321
|
+
const { rows, cols } = gridDimensionsFromButtonCount(buttonCount);
|
|
322
|
+
const gridLayout = Array.from({ length: rows }, () => Array.from({ length: cols }, () => null));
|
|
323
|
+
buttonIndices.forEach((btnIndex, i) => {
|
|
324
|
+
const rawButton = buttonsById[btnIndex];
|
|
325
|
+
if (!rawButton)
|
|
326
|
+
return;
|
|
327
|
+
const { semanticAction, message, targetPageId } = buildSemanticAction(rawButton, pageId, btnIndex);
|
|
328
|
+
const button = new treeStructure_1.AACButton({
|
|
329
|
+
id: `${pageId}_btn_${btnIndex}`,
|
|
330
|
+
label: rawButton.ButtonText ?? '',
|
|
331
|
+
message,
|
|
332
|
+
targetPageId,
|
|
333
|
+
semanticAction,
|
|
334
|
+
});
|
|
335
|
+
// Decode style from NSKeyedArchiver blobs.
|
|
336
|
+
const backgroundColor = (0, nsKeyed_1.decodeNsColor)(rawButton.BackgroundColor);
|
|
337
|
+
const borderColor = (0, nsKeyed_1.decodeNsColor)(rawButton.BorderColor);
|
|
338
|
+
const fontColor = (0, nsKeyed_1.decodeNsColor)(rawButton.TextColor);
|
|
339
|
+
const font = (0, nsKeyed_1.decodeNsFont)(rawButton.TextFont);
|
|
340
|
+
if (backgroundColor || borderColor || fontColor || font.fontFamily) {
|
|
341
|
+
button.style = {
|
|
342
|
+
...(button.style || {}),
|
|
343
|
+
...(backgroundColor ? { backgroundColor } : {}),
|
|
344
|
+
...(borderColor ? { borderColor } : {}),
|
|
345
|
+
...(fontColor ? { fontColor } : {}),
|
|
346
|
+
...(font.fontFamily ? { fontFamily: font.fontFamily } : {}),
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
// Resolve image metadata.
|
|
350
|
+
const images = rawButton.ButtonImages;
|
|
351
|
+
if (Array.isArray(images) && images.length > 0) {
|
|
352
|
+
const first = images[0];
|
|
353
|
+
if (first?.Location) {
|
|
354
|
+
button.image = first.Location;
|
|
355
|
+
if (zipFiles.has(first.Location)) {
|
|
356
|
+
button.resolvedImageEntry = first.Location;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
if (first?.SourceLibrary)
|
|
360
|
+
button.symbolLibrary = first.SourceLibrary;
|
|
361
|
+
if (first?.SourceImageName)
|
|
362
|
+
button.symbolPath = first.SourceImageName;
|
|
363
|
+
}
|
|
364
|
+
// Visibility: GoTalk buttons can be disabled via either field.
|
|
365
|
+
if (rawButton.Enabled === false || rawButton.Disabled === true) {
|
|
366
|
+
button.visibility = 'Disabled';
|
|
367
|
+
}
|
|
368
|
+
// Auditory cue (played when scanning/highlighting).
|
|
369
|
+
if (rawButton.CueType && rawButton.CueType !== 'None') {
|
|
370
|
+
const cueText = rawButton.CueData?.TTSText;
|
|
371
|
+
if (cueText && cueText.length > 0) {
|
|
372
|
+
button.audioDescription = cueText;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
// Recorded audio reference.
|
|
376
|
+
const btnType = rawButton.ButtonType;
|
|
377
|
+
if (btnType === 'Audio' || btnType === 'Recorded') {
|
|
378
|
+
button.audioRecording = {
|
|
379
|
+
identifier: `${pageId}-${btnIndex}.caf`,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
// AfterAction: motor-planning "speak and return" behaviour.
|
|
383
|
+
// AfterAction is an enum (0=stay, 2=..., 50=jump-back) and
|
|
384
|
+
// AfterActionData has the same shape as ActionData (e.g. {JumpTo: 0}).
|
|
385
|
+
const afterActionJumpTo = rawButton.AfterActionData?.JumpTo;
|
|
386
|
+
// Preserve raw style + extended data opaquely for round-trip styling.
|
|
387
|
+
button.parameters = {
|
|
388
|
+
...(button.parameters || {}),
|
|
389
|
+
__gotalkNow: {
|
|
390
|
+
buttonType: rawButton.ButtonType,
|
|
391
|
+
textShadow: rawButton.TextShadow,
|
|
392
|
+
textCenter: rawButton.TextCenter,
|
|
393
|
+
cueType: rawButton.CueType,
|
|
394
|
+
afterAction: rawButton.AfterAction,
|
|
395
|
+
afterActionJumpTo: afterActionJumpTo !== undefined ? String(afterActionJumpTo) : undefined,
|
|
396
|
+
jumpBook: rawButton.ActionData?.JumpBook,
|
|
397
|
+
},
|
|
398
|
+
};
|
|
399
|
+
page._loadButton(button);
|
|
400
|
+
// Place into grid by index (row-major).
|
|
401
|
+
const row = Math.floor(i / cols);
|
|
402
|
+
const col = i % cols;
|
|
403
|
+
if (row < rows && col < cols) {
|
|
404
|
+
gridLayout[row][col] = button;
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
page.grid = gridLayout;
|
|
408
|
+
tree.addPage(page);
|
|
409
|
+
}
|
|
410
|
+
return tree;
|
|
411
|
+
}
|
|
412
|
+
// ---- processTexts (asset-preserving round-trip) -------------------------
|
|
413
|
+
async processTexts(filePathOrBuffer, translations, outputPath) {
|
|
414
|
+
const { writeBinaryToPath } = this.options.fileAdapter;
|
|
415
|
+
const originalZip = await this.openZip(filePathOrBuffer);
|
|
416
|
+
// Parse PageData and apply translations in place.
|
|
417
|
+
const pageDataBytes = await originalZip.readFile('PageData.plist');
|
|
418
|
+
const pageDataText = typeof Buffer !== 'undefined' && Buffer.isBuffer(pageDataBytes)
|
|
419
|
+
? pageDataBytes.toString('utf8')
|
|
420
|
+
: new TextDecoder().decode(pageDataBytes);
|
|
421
|
+
const pageData = plist_1.default.parse(pageDataText);
|
|
422
|
+
for (const pageId of Object.keys(pageData)) {
|
|
423
|
+
const rawPage = pageData[pageId];
|
|
424
|
+
if (!rawPage || !rawPage.Buttons)
|
|
425
|
+
continue;
|
|
426
|
+
for (const btnIndex of Object.keys(rawPage.Buttons)) {
|
|
427
|
+
const rawButton = rawPage.Buttons[btnIndex];
|
|
428
|
+
if (!rawButton)
|
|
429
|
+
continue;
|
|
430
|
+
// Translate the visible label.
|
|
431
|
+
if (rawButton.ButtonText && translations.has(rawButton.ButtonText)) {
|
|
432
|
+
rawButton.ButtonText = translations.get(rawButton.ButtonText);
|
|
433
|
+
}
|
|
434
|
+
// Translate the spoken text (only when explicitly set).
|
|
435
|
+
const actionData = rawButton.ActionData;
|
|
436
|
+
if (actionData && actionData.TTSText && translations.has(actionData.TTSText)) {
|
|
437
|
+
actionData.TTSText = translations.get(actionData.TTSText);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
const rebuiltPageData = plist_1.default.build(pageData);
|
|
442
|
+
// Repack: copy every original entry verbatim, replacing only PageData.plist.
|
|
443
|
+
const outputZip = await this.options.zipAdapter(undefined, this.options.fileAdapter);
|
|
444
|
+
const files = [];
|
|
445
|
+
for (const name of originalZip.listFiles()) {
|
|
446
|
+
if (name === 'PageData.plist')
|
|
447
|
+
continue;
|
|
448
|
+
files.push({ name, data: await originalZip.readFile(name) });
|
|
449
|
+
}
|
|
450
|
+
files.push({ name: 'PageData.plist', data: rebuiltPageData });
|
|
451
|
+
const outputBuffer = await outputZip.writeFiles(files);
|
|
452
|
+
await writeBinaryToPath(outputPath, outputBuffer);
|
|
453
|
+
return outputBuffer;
|
|
454
|
+
}
|
|
455
|
+
// ---- saveFromTree (build from tree) -------------------------------------
|
|
456
|
+
async saveFromTree(tree, outputPath) {
|
|
457
|
+
const { writeBinaryToPath } = this.options.fileAdapter;
|
|
458
|
+
const pageData = {};
|
|
459
|
+
const pageOrder = [];
|
|
460
|
+
const imageFiles = [];
|
|
461
|
+
const pageIds = Object.keys(tree.pages);
|
|
462
|
+
pageIds.forEach((pageId, pageIdx) => {
|
|
463
|
+
const page = tree.pages[pageId];
|
|
464
|
+
const numericId = Number(pageId);
|
|
465
|
+
if (!Number.isNaN(numericId))
|
|
466
|
+
pageOrder.push(numericId);
|
|
467
|
+
const buttons = {};
|
|
468
|
+
const buttonList = page.buttons;
|
|
469
|
+
buttonList.forEach((btn, i) => {
|
|
470
|
+
const btnIndex = String(i);
|
|
471
|
+
const buttonType = btn.parameters?.__gotalkNow?.buttonType ||
|
|
472
|
+
(btn.targetPageId ? 'Jump' : 'TTS');
|
|
473
|
+
const actionData = {};
|
|
474
|
+
if (buttonType === 'Jump' && btn.targetPageId) {
|
|
475
|
+
actionData.JumpTo = Number(btn.targetPageId);
|
|
476
|
+
actionData.TTSText = '';
|
|
477
|
+
}
|
|
478
|
+
else if (buttonType === 'TTS') {
|
|
479
|
+
// Only store TTSText when it differs from the label (matches GoTalk's convention).
|
|
480
|
+
actionData.TTSText = btn.message && btn.message !== btn.label ? btn.message : '';
|
|
481
|
+
}
|
|
482
|
+
else {
|
|
483
|
+
actionData.TTSText = btn.message || '';
|
|
484
|
+
}
|
|
485
|
+
const rawButton = {
|
|
486
|
+
ActionData: actionData,
|
|
487
|
+
ButtonType: buttonType,
|
|
488
|
+
ButtonText: btn.label || '',
|
|
489
|
+
};
|
|
490
|
+
if (btn.resolvedImageEntry || btn.image) {
|
|
491
|
+
rawButton.ButtonImages = [
|
|
492
|
+
{
|
|
493
|
+
Location: btn.resolvedImageEntry || btn.image,
|
|
494
|
+
Type: 'Saved',
|
|
495
|
+
},
|
|
496
|
+
];
|
|
497
|
+
}
|
|
498
|
+
buttons[btnIndex] = rawButton;
|
|
499
|
+
});
|
|
500
|
+
pageData[pageId] = {
|
|
501
|
+
ButtonCount: buttonList.length,
|
|
502
|
+
Buttons: buttons,
|
|
503
|
+
};
|
|
504
|
+
// Collect referenced images so we can include them in the archive.
|
|
505
|
+
buttonList.forEach((btn) => {
|
|
506
|
+
if (btn.resolvedImageEntry) {
|
|
507
|
+
// Image bytes are not available from the tree alone; skip if absent.
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
void pageIdx;
|
|
511
|
+
});
|
|
512
|
+
const bookInfo = {
|
|
513
|
+
GoTalkBookFormatVersion: Number(tree.metadata.version) || 1,
|
|
514
|
+
};
|
|
515
|
+
const zip = await this.options.zipAdapter(undefined, this.options.fileAdapter);
|
|
516
|
+
const files = [
|
|
517
|
+
{ name: 'BookInfo.plist', data: plist_1.default.build(bookInfo) },
|
|
518
|
+
{ name: 'PageData.plist', data: plist_1.default.build(pageData) },
|
|
519
|
+
{
|
|
520
|
+
name: 'PageOrder.plist',
|
|
521
|
+
data: plist_1.default.build(pageOrder),
|
|
522
|
+
},
|
|
523
|
+
{ name: 'PageHistory.plist', data: plist_1.default.build([]) },
|
|
524
|
+
...imageFiles,
|
|
525
|
+
];
|
|
526
|
+
const outputBuffer = await zip.writeFiles(files);
|
|
527
|
+
await writeBinaryToPath(outputPath, outputBuffer);
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Extract strings with metadata for aac-tools-platform compatibility.
|
|
531
|
+
* Uses the generic implementation from BaseProcessor.
|
|
532
|
+
*/
|
|
533
|
+
extractStringsWithMetadata(filePath) {
|
|
534
|
+
return this.extractStringsWithMetadataGeneric(filePath);
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Generate translated download for aac-tools-platform compatibility.
|
|
538
|
+
* Uses the generic implementation from BaseProcessor.
|
|
539
|
+
*/
|
|
540
|
+
generateTranslatedDownload(filePath, translatedStrings, sourceStrings) {
|
|
541
|
+
return this.generateTranslatedDownloadGeneric(filePath, translatedStrings, sourceStrings);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
exports.GotalkNowProcessor = GotalkNowProcessor;
|
|
@@ -2963,8 +2963,8 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
2963
2963
|
cellContent.CaptionAndImage = captionAndImage;
|
|
2964
2964
|
}
|
|
2965
2965
|
const cellData = {
|
|
2966
|
-
'@_X': position.x
|
|
2967
|
-
'@_Y': position.y + yOffset
|
|
2966
|
+
'@_X': position.x, // Grid 3 coordinates are 0-based (matching loader)
|
|
2967
|
+
'@_Y': position.y + yOffset,
|
|
2968
2968
|
'@_ColumnSpan': position.columnSpan,
|
|
2969
2969
|
'@_RowSpan': position.rowSpan,
|
|
2970
2970
|
Content: cellContent,
|
|
@@ -16,3 +16,4 @@ export { SnapProcessor } from './snapProcessor';
|
|
|
16
16
|
export { TouchChatProcessor } from './touchchatProcessor';
|
|
17
17
|
export { AstericsGridProcessor } from './astericsGridProcessor';
|
|
18
18
|
export { ObfsetProcessor } from './obfsetProcessor';
|
|
19
|
+
export { GotalkNowProcessor } from './gotalkNowProcessor';
|
package/dist/processors/index.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* - import { Snap } from 'aac-processors/snap';
|
|
9
9
|
*/
|
|
10
10
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
-
exports.ObfsetProcessor = exports.AstericsGridProcessor = exports.TouchChatProcessor = exports.SnapProcessor = exports.OpmlProcessor = exports.ObfProcessor = exports.GridsetProcessor = exports.ExcelProcessor = exports.DotProcessor = exports.ApplePanelsProcessor = void 0;
|
|
11
|
+
exports.GotalkNowProcessor = exports.ObfsetProcessor = exports.AstericsGridProcessor = exports.TouchChatProcessor = exports.SnapProcessor = exports.OpmlProcessor = exports.ObfProcessor = exports.GridsetProcessor = exports.ExcelProcessor = exports.DotProcessor = exports.ApplePanelsProcessor = void 0;
|
|
12
12
|
var applePanelsProcessor_1 = require("./applePanelsProcessor");
|
|
13
13
|
Object.defineProperty(exports, "ApplePanelsProcessor", { enumerable: true, get: function () { return applePanelsProcessor_1.ApplePanelsProcessor; } });
|
|
14
14
|
var dotProcessor_1 = require("./dotProcessor");
|
|
@@ -29,3 +29,5 @@ var astericsGridProcessor_1 = require("./astericsGridProcessor");
|
|
|
29
29
|
Object.defineProperty(exports, "AstericsGridProcessor", { enumerable: true, get: function () { return astericsGridProcessor_1.AstericsGridProcessor; } });
|
|
30
30
|
var obfsetProcessor_1 = require("./obfsetProcessor");
|
|
31
31
|
Object.defineProperty(exports, "ObfsetProcessor", { enumerable: true, get: function () { return obfsetProcessor_1.ObfsetProcessor; } });
|
|
32
|
+
var gotalkNowProcessor_1 = require("./gotalkNowProcessor");
|
|
33
|
+
Object.defineProperty(exports, "GotalkNowProcessor", { enumerable: true, get: function () { return gotalkNowProcessor_1.GotalkNowProcessor; } });
|