@willwade/aac-processors 0.1.21 → 0.2.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 +3 -2
- package/dist/browser/processors/applePanelsProcessor.js +24 -24
- package/dist/browser/processors/astericsGridProcessor.js +22 -24
- package/dist/browser/processors/dotProcessor.js +6 -10
- package/dist/browser/processors/gridset/helpers.js +33 -30
- package/dist/browser/processors/gridset/symbolExtractor.js +2 -2
- package/dist/browser/processors/gridset/symbolSearch.js +22 -22
- package/dist/browser/processors/gridset/symbols.js +14 -14
- package/dist/browser/processors/gridsetProcessor.js +7 -7
- package/dist/browser/processors/obfProcessor.js +54 -47
- package/dist/browser/processors/opmlProcessor.js +6 -10
- package/dist/browser/processors/snap/helpers.js +34 -30
- package/dist/browser/processors/snapProcessor.js +28 -28
- package/dist/browser/processors/touchchatProcessor.js +24 -25
- package/dist/browser/utilities/analytics/history.js +24 -18
- package/dist/browser/utilities/analytics/metrics/comparison.js +16 -16
- package/dist/browser/utilities/analytics/metrics/vocabulary.js +2 -2
- package/dist/browser/utilities/analytics/reference/browser.js +16 -16
- package/dist/browser/utilities/analytics/reference/index.js +25 -24
- package/dist/browser/utils/io.js +29 -25
- package/dist/browser/utils/sqlite.js +5 -5
- package/dist/browser/utils/zip.js +2 -4
- package/dist/browser/validation/gridsetValidator.js +2 -2
- package/dist/browser/validation/obfValidator.js +2 -2
- package/dist/browser/validation/snapValidator.js +3 -3
- package/dist/browser/validation/touchChatValidator.js +3 -3
- package/dist/cli/index.js +19 -16
- package/dist/core/baseProcessor.d.ts +1 -1
- package/dist/processors/applePanelsProcessor.js +24 -24
- package/dist/processors/astericsGridProcessor.d.ts +4 -4
- package/dist/processors/astericsGridProcessor.js +22 -24
- package/dist/processors/dotProcessor.js +6 -10
- package/dist/processors/excelProcessor.d.ts +3 -3
- package/dist/processors/excelProcessor.js +10 -13
- package/dist/processors/gridset/helpers.d.ts +9 -9
- package/dist/processors/gridset/helpers.js +33 -30
- package/dist/processors/gridset/symbolExtractor.d.ts +1 -1
- package/dist/processors/gridset/symbolExtractor.js +2 -2
- package/dist/processors/gridset/symbolSearch.d.ts +10 -10
- package/dist/processors/gridset/symbolSearch.js +22 -22
- package/dist/processors/gridset/symbols.d.ts +3 -3
- package/dist/processors/gridset/symbols.js +14 -14
- package/dist/processors/gridsetProcessor.d.ts +2 -2
- package/dist/processors/gridsetProcessor.js +7 -7
- package/dist/processors/obfProcessor.d.ts +2 -2
- package/dist/processors/obfProcessor.js +54 -47
- package/dist/processors/obfsetProcessor.js +1 -2
- package/dist/processors/opmlProcessor.js +6 -10
- package/dist/processors/snap/helpers.d.ts +8 -8
- package/dist/processors/snap/helpers.js +34 -30
- package/dist/processors/snapProcessor.d.ts +2 -2
- package/dist/processors/snapProcessor.js +28 -28
- package/dist/processors/touchchatProcessor.d.ts +2 -2
- package/dist/processors/touchchatProcessor.js +24 -25
- package/dist/types/aac.d.ts +2 -2
- package/dist/utilities/analytics/history.d.ts +8 -8
- package/dist/utilities/analytics/history.js +24 -18
- package/dist/utilities/analytics/index.d.ts +1 -1
- package/dist/utilities/analytics/index.js +3 -2
- package/dist/utilities/analytics/metrics/comparison.d.ts +1 -1
- package/dist/utilities/analytics/metrics/comparison.js +16 -16
- package/dist/utilities/analytics/metrics/vocabulary.d.ts +1 -1
- package/dist/utilities/analytics/metrics/vocabulary.js +2 -2
- package/dist/utilities/analytics/reference/browser.d.ts +9 -9
- package/dist/utilities/analytics/reference/browser.js +16 -16
- package/dist/utilities/analytics/reference/index.d.ts +21 -21
- package/dist/utilities/analytics/reference/index.js +25 -24
- package/dist/utilities/symbolTools.d.ts +5 -5
- package/dist/utilities/symbolTools.js +10 -8
- package/dist/utils/io.d.ts +11 -11
- package/dist/utils/io.js +29 -25
- package/dist/utils/sqlite.d.ts +1 -1
- package/dist/utils/sqlite.js +5 -5
- package/dist/utils/zip.js +2 -4
- package/dist/validation/applePanelsValidator.js +7 -6
- package/dist/validation/astericsValidator.js +2 -2
- package/dist/validation/dotValidator.js +2 -2
- package/dist/validation/excelValidator.js +2 -2
- package/dist/validation/gridsetValidator.js +2 -2
- package/dist/validation/index.js +2 -2
- package/dist/validation/obfValidator.js +2 -2
- package/dist/validation/obfsetValidator.js +2 -2
- package/dist/validation/opmlValidator.js +2 -2
- package/dist/validation/snapValidator.js +3 -3
- package/dist/validation/touchChatValidator.js +3 -3
- package/docs/BROWSER_USAGE.md +0 -40
- package/package.json +1 -1
|
@@ -93,12 +93,12 @@ export function isSymbolReference(reference) {
|
|
|
93
93
|
* Get the default Grid 3 installation path for the current platform
|
|
94
94
|
* @returns Default Grid 3 path or empty string if not found
|
|
95
95
|
*/
|
|
96
|
-
export function getDefaultGrid3Path(fileAdapter) {
|
|
96
|
+
export async function getDefaultGrid3Path(fileAdapter) {
|
|
97
97
|
const { pathExists } = fileAdapter ?? defaultFileAdapter;
|
|
98
98
|
const platform = (typeof process !== 'undefined' && process.platform ? process.platform : 'unknown');
|
|
99
99
|
const defaultPath = DEFAULT_GRID3_PATHS[platform] || '';
|
|
100
100
|
try {
|
|
101
|
-
if (defaultPath && pathExists(defaultPath)) {
|
|
101
|
+
if (defaultPath && (await pathExists(defaultPath))) {
|
|
102
102
|
return defaultPath;
|
|
103
103
|
}
|
|
104
104
|
// Try to find Grid 3 in common locations
|
|
@@ -110,7 +110,7 @@ export function getDefaultGrid3Path(fileAdapter) {
|
|
|
110
110
|
'/opt/smartbox/grid3',
|
|
111
111
|
];
|
|
112
112
|
for (const testPath of commonPaths) {
|
|
113
|
-
if (pathExists(testPath)) {
|
|
113
|
+
if (await pathExists(testPath)) {
|
|
114
114
|
return testPath;
|
|
115
115
|
}
|
|
116
116
|
}
|
|
@@ -146,22 +146,22 @@ export function getSymbolSearchIndexesDir(grid3Path, locale = DEFAULT_LOCALE, fi
|
|
|
146
146
|
* @param options - Resolution options
|
|
147
147
|
* @returns Array of symbol library information
|
|
148
148
|
*/
|
|
149
|
-
export function getAvailableSymbolLibraries(options = {}, fileAdapter) {
|
|
149
|
+
export async function getAvailableSymbolLibraries(options = {}, fileAdapter) {
|
|
150
150
|
const { pathExists, getFileSize, listDir, join, basename } = fileAdapter ?? defaultFileAdapter;
|
|
151
|
-
const grid3Path = options.grid3Path || options.symbolDir || getDefaultGrid3Path();
|
|
151
|
+
const grid3Path = options.grid3Path || options.symbolDir || (await getDefaultGrid3Path());
|
|
152
152
|
if (!grid3Path) {
|
|
153
153
|
return [];
|
|
154
154
|
}
|
|
155
155
|
const symbolsDir = getSymbolLibrariesDir(grid3Path, fileAdapter);
|
|
156
|
-
if (!pathExists(symbolsDir)) {
|
|
156
|
+
if (!(await pathExists(symbolsDir))) {
|
|
157
157
|
return [];
|
|
158
158
|
}
|
|
159
159
|
const libraries = [];
|
|
160
|
-
const files = listDir(symbolsDir);
|
|
160
|
+
const files = await listDir(symbolsDir);
|
|
161
161
|
for (const file of files) {
|
|
162
162
|
if (file.endsWith('.symbols')) {
|
|
163
163
|
const fullPath = join(symbolsDir, file);
|
|
164
|
-
const size = getFileSize(fullPath);
|
|
164
|
+
const size = await getFileSize(fullPath);
|
|
165
165
|
const libraryName = basename(file, '.symbols');
|
|
166
166
|
libraries.push({
|
|
167
167
|
name: libraryName,
|
|
@@ -180,9 +180,9 @@ export function getAvailableSymbolLibraries(options = {}, fileAdapter) {
|
|
|
180
180
|
* @param options - Resolution options
|
|
181
181
|
* @returns Symbol library info or undefined if not found
|
|
182
182
|
*/
|
|
183
|
-
export function getSymbolLibraryInfo(libraryName, options = {}, fileAdapter) {
|
|
183
|
+
export async function getSymbolLibraryInfo(libraryName, options = {}, fileAdapter) {
|
|
184
184
|
const { pathExists, getFileSize, join } = fileAdapter ?? defaultFileAdapter;
|
|
185
|
-
const grid3Path = options.grid3Path || options.symbolDir || getDefaultGrid3Path();
|
|
185
|
+
const grid3Path = options.grid3Path || options.symbolDir || (await getDefaultGrid3Path());
|
|
186
186
|
if (!grid3Path) {
|
|
187
187
|
return undefined;
|
|
188
188
|
}
|
|
@@ -196,8 +196,8 @@ export function getSymbolLibraryInfo(libraryName, options = {}, fileAdapter) {
|
|
|
196
196
|
];
|
|
197
197
|
for (const file of variations) {
|
|
198
198
|
const fullPath = join(symbolsDir, file);
|
|
199
|
-
if (pathExists(fullPath)) {
|
|
200
|
-
const size = getFileSize(fullPath);
|
|
199
|
+
if (await pathExists(fullPath)) {
|
|
200
|
+
const size = await getFileSize(fullPath);
|
|
201
201
|
return {
|
|
202
202
|
name: libraryName,
|
|
203
203
|
pixFile: fullPath,
|
|
@@ -224,7 +224,7 @@ export async function resolveSymbolReference(reference, options = {}, fileAdapte
|
|
|
224
224
|
error: 'Invalid symbol reference format',
|
|
225
225
|
};
|
|
226
226
|
}
|
|
227
|
-
const grid3Path = options.grid3Path || getDefaultGrid3Path();
|
|
227
|
+
const grid3Path = options.grid3Path || (await getDefaultGrid3Path());
|
|
228
228
|
if (!grid3Path) {
|
|
229
229
|
return {
|
|
230
230
|
reference: parsed,
|
|
@@ -232,7 +232,7 @@ export async function resolveSymbolReference(reference, options = {}, fileAdapte
|
|
|
232
232
|
error: 'Grid 3 installation not found. Please specify grid3Path.',
|
|
233
233
|
};
|
|
234
234
|
}
|
|
235
|
-
const libraryInfo = getSymbolLibraryInfo(parsed.library, { grid3Path });
|
|
235
|
+
const libraryInfo = await getSymbolLibraryInfo(parsed.library, { grid3Path });
|
|
236
236
|
if (!libraryInfo || !libraryInfo.exists) {
|
|
237
237
|
return {
|
|
238
238
|
reference: parsed,
|
|
@@ -398,7 +398,7 @@ class GridsetProcessor extends BaseProcessor {
|
|
|
398
398
|
const tree = new AACTree();
|
|
399
399
|
let zipResult;
|
|
400
400
|
try {
|
|
401
|
-
const zipInput = readBinaryFromInput(filePathOrBuffer);
|
|
401
|
+
const zipInput = await readBinaryFromInput(filePathOrBuffer);
|
|
402
402
|
zipResult = await this.options.zipAdapter(zipInput);
|
|
403
403
|
}
|
|
404
404
|
catch (error) {
|
|
@@ -1742,7 +1742,7 @@ class GridsetProcessor extends BaseProcessor {
|
|
|
1742
1742
|
});
|
|
1743
1743
|
// Save the translated tree and return its content
|
|
1744
1744
|
await this.saveFromTree(tree, outputPath);
|
|
1745
|
-
return readBinaryFromInput(outputPath);
|
|
1745
|
+
return await readBinaryFromInput(outputPath);
|
|
1746
1746
|
}
|
|
1747
1747
|
/**
|
|
1748
1748
|
* Extract symbol information from a gridset for LLM-based translation.
|
|
@@ -1751,7 +1751,7 @@ class GridsetProcessor extends BaseProcessor {
|
|
|
1751
1751
|
* This method uses shared translation utilities that work across all AAC formats.
|
|
1752
1752
|
*
|
|
1753
1753
|
* @param filePathOrBuffer - Path to gridset file or buffer
|
|
1754
|
-
* @returns
|
|
1754
|
+
* @returns Promise resolving to symbol information for LLM processing
|
|
1755
1755
|
*/
|
|
1756
1756
|
async extractSymbolsForLLM(filePathOrBuffer) {
|
|
1757
1757
|
const tree = await this.loadIntoTree(filePathOrBuffer);
|
|
@@ -1781,7 +1781,7 @@ class GridsetProcessor extends BaseProcessor {
|
|
|
1781
1781
|
* @param llmTranslations - Array of LLM translations with symbol info
|
|
1782
1782
|
* @param outputPath - Where to save the translated gridset
|
|
1783
1783
|
* @param options - Translation options (e.g., allowPartial for testing)
|
|
1784
|
-
* @returns
|
|
1784
|
+
* @returns Promise resolving to a buffer of the translated gridset
|
|
1785
1785
|
*/
|
|
1786
1786
|
async processLLMTranslations(filePathOrBuffer, llmTranslations, outputPath, options) {
|
|
1787
1787
|
const { readBinaryFromInput } = this.options.fileAdapter;
|
|
@@ -1823,7 +1823,7 @@ class GridsetProcessor extends BaseProcessor {
|
|
|
1823
1823
|
});
|
|
1824
1824
|
// Save and return
|
|
1825
1825
|
await this.saveFromTree(tree, outputPath);
|
|
1826
|
-
return readBinaryFromInput(outputPath);
|
|
1826
|
+
return await readBinaryFromInput(outputPath);
|
|
1827
1827
|
}
|
|
1828
1828
|
async saveFromTree(tree, outputPath) {
|
|
1829
1829
|
const files = [];
|
|
@@ -1832,7 +1832,7 @@ class GridsetProcessor extends BaseProcessor {
|
|
|
1832
1832
|
if (Object.keys(tree.pages).length === 0) {
|
|
1833
1833
|
// Create empty zip for empty tree
|
|
1834
1834
|
const zipBuffer = await zip.writeFiles([]);
|
|
1835
|
-
writeBinaryToPath(outputPath, zipBuffer);
|
|
1835
|
+
await writeBinaryToPath(outputPath, zipBuffer);
|
|
1836
1836
|
return;
|
|
1837
1837
|
}
|
|
1838
1838
|
// Collect all unique styles from pages and buttons
|
|
@@ -2142,7 +2142,7 @@ class GridsetProcessor extends BaseProcessor {
|
|
|
2142
2142
|
});
|
|
2143
2143
|
// Write the zip file
|
|
2144
2144
|
const zipBuffer = await zip.writeFiles(files);
|
|
2145
|
-
writeBinaryToPath(outputPath, zipBuffer);
|
|
2145
|
+
await writeBinaryToPath(outputPath, zipBuffer);
|
|
2146
2146
|
}
|
|
2147
2147
|
// Helper method to calculate column definitions based on page layout
|
|
2148
2148
|
calculateColumnDefinitions(page) {
|
|
@@ -3,7 +3,6 @@ import { AACTree, AACPage, AACButton, AACSemanticCategory, AACSemanticIntent, }
|
|
|
3
3
|
import { generateCloneId } from '../utilities/analytics/utils/idGenerator';
|
|
4
4
|
import { extractAllButtonsForTranslation, validateTranslationResults, } from '../utilities/translation/translationProcessor';
|
|
5
5
|
import { encodeBase64, decodeText } from '../utils/io';
|
|
6
|
-
import { getZipAdapter } from '../utils/zip';
|
|
7
6
|
const OBF_FORMAT_VERSION = 'open-board-0.1';
|
|
8
7
|
/**
|
|
9
8
|
* Map OBF hidden value to AAC standard visibility
|
|
@@ -63,35 +62,42 @@ class ObfProcessor extends BaseProcessor {
|
|
|
63
62
|
if (this.imageCache.has(imageId)) {
|
|
64
63
|
return this.imageCache.get(imageId) ?? null;
|
|
65
64
|
}
|
|
66
|
-
if (!
|
|
65
|
+
if (!images)
|
|
67
66
|
return null;
|
|
68
|
-
}
|
|
69
67
|
// Find the image metadata
|
|
70
68
|
const imageData = images.find((img) => img.id === imageId);
|
|
71
69
|
if (!imageData) {
|
|
72
70
|
return null;
|
|
73
71
|
}
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
if
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
72
|
+
// If image has data property, use that
|
|
73
|
+
if (imageData.data) {
|
|
74
|
+
const dataUrl = imageData.data;
|
|
75
|
+
this.imageCache.set(imageId, dataUrl);
|
|
76
|
+
return dataUrl;
|
|
77
|
+
}
|
|
78
|
+
if (this.zipFile) {
|
|
79
|
+
// Try to get the image file from the ZIP
|
|
80
|
+
// Images are typically stored in an 'images' folder or root
|
|
81
|
+
const possiblePaths = [
|
|
82
|
+
imageData.path, // Explicit path if provided
|
|
83
|
+
`images/${imageData.filename || imageId}`, // Standard images folder
|
|
84
|
+
imageData.id, // Just the ID
|
|
85
|
+
].filter(Boolean);
|
|
86
|
+
for (const imagePath of possiblePaths) {
|
|
87
|
+
try {
|
|
88
|
+
const buffer = await this.zipFile.readFile(imagePath);
|
|
89
|
+
if (buffer) {
|
|
90
|
+
const contentType = imageData.content_type ||
|
|
91
|
+
this.getMimeTypeFromFilename(imagePath);
|
|
92
|
+
const dataUrl = `data:${contentType};base64,${encodeBase64(buffer)}`;
|
|
93
|
+
this.imageCache.set(imageId, dataUrl);
|
|
94
|
+
return dataUrl;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
// Continue to next path
|
|
99
|
+
continue;
|
|
90
100
|
}
|
|
91
|
-
}
|
|
92
|
-
catch (err) {
|
|
93
|
-
// Continue to next path
|
|
94
|
-
continue;
|
|
95
101
|
}
|
|
96
102
|
}
|
|
97
103
|
// If image has a URL, use that as fallback
|
|
@@ -167,8 +173,7 @@ class ObfProcessor extends BaseProcessor {
|
|
|
167
173
|
buttonParameters.image_id = btn.image_id;
|
|
168
174
|
}
|
|
169
175
|
return new AACButton({
|
|
170
|
-
|
|
171
|
-
id: `${pageId}::${btn?.id || ''}`,
|
|
176
|
+
id: String(btn.id),
|
|
172
177
|
label: String(btn?.label || ''),
|
|
173
178
|
message: String(btn?.vocalization || btn?.label || ''),
|
|
174
179
|
visibility: mapObfVisibility(btn.hidden),
|
|
@@ -217,7 +222,7 @@ class ObfProcessor extends BaseProcessor {
|
|
|
217
222
|
return;
|
|
218
223
|
if (rowIndex >= rows || colIndex >= cols)
|
|
219
224
|
return;
|
|
220
|
-
const aacBtn = buttonMap.get(
|
|
225
|
+
const aacBtn = buttonMap.get(String(cellId));
|
|
221
226
|
if (aacBtn) {
|
|
222
227
|
grid[rowIndex][colIndex] = aacBtn;
|
|
223
228
|
}
|
|
@@ -230,7 +235,7 @@ class ObfProcessor extends BaseProcessor {
|
|
|
230
235
|
const row = Math.floor(btn.box_id / cols);
|
|
231
236
|
const col = btn.box_id % cols;
|
|
232
237
|
if (row < rows && col < cols) {
|
|
233
|
-
const aacBtn = buttonMap.get(
|
|
238
|
+
const aacBtn = buttonMap.get(String(btn.id));
|
|
234
239
|
if (aacBtn) {
|
|
235
240
|
grid[row][col] = aacBtn;
|
|
236
241
|
}
|
|
@@ -287,7 +292,7 @@ class ObfProcessor extends BaseProcessor {
|
|
|
287
292
|
// Detailed logging for debugging input
|
|
288
293
|
const bufferLength = typeof filePathOrBuffer === 'string'
|
|
289
294
|
? null
|
|
290
|
-
: readBinaryFromInput(filePathOrBuffer).byteLength;
|
|
295
|
+
: (await readBinaryFromInput(filePathOrBuffer)).byteLength;
|
|
291
296
|
console.log('[OBF] loadIntoTree called with:', {
|
|
292
297
|
type: typeof filePathOrBuffer,
|
|
293
298
|
isBuffer: typeof Buffer !== 'undefined' && Buffer.isBuffer(filePathOrBuffer),
|
|
@@ -297,9 +302,9 @@ class ObfProcessor extends BaseProcessor {
|
|
|
297
302
|
});
|
|
298
303
|
const tree = new AACTree();
|
|
299
304
|
// Helper: try to parse JSON OBF
|
|
300
|
-
function tryParseObfJson(data) {
|
|
305
|
+
async function tryParseObfJson(data) {
|
|
301
306
|
try {
|
|
302
|
-
const str = typeof data === 'string' ? data : readTextFromInput(data);
|
|
307
|
+
const str = typeof data === 'string' ? data : await readTextFromInput(data);
|
|
303
308
|
// Check for empty or whitespace-only content
|
|
304
309
|
if (!str.trim()) {
|
|
305
310
|
return null;
|
|
@@ -321,8 +326,8 @@ class ObfProcessor extends BaseProcessor {
|
|
|
321
326
|
// If input is a string path and ends with .obf, treat as JSON
|
|
322
327
|
if (typeof filePathOrBuffer === 'string' && filePathOrBuffer.toLowerCase().endsWith('.obf')) {
|
|
323
328
|
try {
|
|
324
|
-
const content = readTextFromInput(filePathOrBuffer);
|
|
325
|
-
const boardData = tryParseObfJson(content);
|
|
329
|
+
const content = await readTextFromInput(filePathOrBuffer);
|
|
330
|
+
const boardData = await tryParseObfJson(content);
|
|
326
331
|
if (boardData) {
|
|
327
332
|
console.log('[OBF] Detected .obf file, parsed as JSON');
|
|
328
333
|
const page = await this.processBoard(boardData, filePathOrBuffer, false);
|
|
@@ -350,17 +355,17 @@ class ObfProcessor extends BaseProcessor {
|
|
|
350
355
|
}
|
|
351
356
|
}
|
|
352
357
|
// Detect likely zip signature first
|
|
353
|
-
function isLikelyZip(input) {
|
|
358
|
+
async function isLikelyZip(input) {
|
|
354
359
|
if (typeof input === 'string') {
|
|
355
360
|
const lowered = input.toLowerCase();
|
|
356
361
|
return lowered.endsWith('.zip') || lowered.endsWith('.obz');
|
|
357
362
|
}
|
|
358
|
-
const bytes = readBinaryFromInput(input);
|
|
363
|
+
const bytes = await readBinaryFromInput(input);
|
|
359
364
|
return bytes.length >= 2 && bytes[0] === 0x50 && bytes[1] === 0x4b;
|
|
360
365
|
}
|
|
361
366
|
// Check if input is a buffer or string that parses as OBF JSON; throw if neither JSON nor ZIP
|
|
362
|
-
if (!isLikelyZip(filePathOrBuffer)) {
|
|
363
|
-
const asJson = tryParseObfJson(filePathOrBuffer);
|
|
367
|
+
if (!(await isLikelyZip(filePathOrBuffer))) {
|
|
368
|
+
const asJson = await tryParseObfJson(filePathOrBuffer);
|
|
364
369
|
if (!asJson)
|
|
365
370
|
throw new Error('Invalid OBF content: not JSON and not ZIP');
|
|
366
371
|
console.log('[OBF] Detected buffer/string as OBF JSON');
|
|
@@ -399,7 +404,7 @@ class ObfProcessor extends BaseProcessor {
|
|
|
399
404
|
try {
|
|
400
405
|
const content = await this.zipFile.readFile(manifestFile[0]);
|
|
401
406
|
const data = decodeText(content);
|
|
402
|
-
const str = typeof data === 'string' ? data : readTextFromInput(data);
|
|
407
|
+
const str = typeof data === 'string' ? data : await readTextFromInput(data);
|
|
403
408
|
if (!str.trim())
|
|
404
409
|
throw new Error('Manifest object missing');
|
|
405
410
|
const manifestObject = JSON.parse(str);
|
|
@@ -423,7 +428,7 @@ class ObfProcessor extends BaseProcessor {
|
|
|
423
428
|
for (const entryName of obfEntries) {
|
|
424
429
|
try {
|
|
425
430
|
const content = await this.zipFile.readFile(entryName);
|
|
426
|
-
const boardData = tryParseObfJson(decodeText(content));
|
|
431
|
+
const boardData = await tryParseObfJson(decodeText(content));
|
|
427
432
|
if (boardData) {
|
|
428
433
|
const page = await this.processBoard(boardData, entryName, true);
|
|
429
434
|
tree.addPage(page);
|
|
@@ -530,6 +535,7 @@ class ObfProcessor extends BaseProcessor {
|
|
|
530
535
|
border_color: button.style?.borderColor,
|
|
531
536
|
box_id: buttonPositions.get(String(button.id ?? '')),
|
|
532
537
|
image_id: imageId,
|
|
538
|
+
hidden: button.visibility === 'Hidden' || false,
|
|
533
539
|
};
|
|
534
540
|
}),
|
|
535
541
|
images: Array.isArray(page.images) ? page.images : [],
|
|
@@ -567,10 +573,10 @@ class ObfProcessor extends BaseProcessor {
|
|
|
567
573
|
});
|
|
568
574
|
// Save the translated tree and return its content
|
|
569
575
|
await this.saveFromTree(tree, outputPath);
|
|
570
|
-
return readBinaryFromInput(outputPath);
|
|
576
|
+
return await readBinaryFromInput(outputPath);
|
|
571
577
|
}
|
|
572
578
|
async saveFromTree(tree, outputPath) {
|
|
573
|
-
const { writeTextToPath, writeBinaryToPath } = this.options.fileAdapter;
|
|
579
|
+
const { writeTextToPath, writeBinaryToPath, pathExists } = this.options.fileAdapter;
|
|
574
580
|
if (outputPath.endsWith('.obf')) {
|
|
575
581
|
// Save as single OBF JSON file
|
|
576
582
|
const rootPage = tree.rootId ? tree.getPage(tree.rootId) : Object.values(tree.pages)[0];
|
|
@@ -578,7 +584,7 @@ class ObfProcessor extends BaseProcessor {
|
|
|
578
584
|
throw new Error('No pages to save');
|
|
579
585
|
}
|
|
580
586
|
const obfBoard = this.createObfBoardFromPage(rootPage, 'Exported Board', tree.metadata);
|
|
581
|
-
writeTextToPath(outputPath, JSON.stringify(obfBoard, null, 2));
|
|
587
|
+
await writeTextToPath(outputPath, JSON.stringify(obfBoard, null, 2));
|
|
582
588
|
}
|
|
583
589
|
else {
|
|
584
590
|
const files = Object.values(tree.pages).map((page) => {
|
|
@@ -590,9 +596,10 @@ class ObfProcessor extends BaseProcessor {
|
|
|
590
596
|
data: new TextEncoder().encode(obfContent),
|
|
591
597
|
};
|
|
592
598
|
});
|
|
593
|
-
const
|
|
594
|
-
|
|
595
|
-
|
|
599
|
+
const fileExists = await pathExists(outputPath);
|
|
600
|
+
this.zipFile = await this.options.zipAdapter(fileExists ? outputPath : undefined, this.options.fileAdapter);
|
|
601
|
+
const zipData = await this.zipFile.writeFiles(files);
|
|
602
|
+
await writeBinaryToPath(outputPath, zipData);
|
|
596
603
|
}
|
|
597
604
|
}
|
|
598
605
|
/**
|
|
@@ -625,7 +632,7 @@ class ObfProcessor extends BaseProcessor {
|
|
|
625
632
|
* This method uses shared translation utilities that work across all AAC formats.
|
|
626
633
|
*
|
|
627
634
|
* @param filePathOrBuffer - Path to OBF/OBZ file or buffer
|
|
628
|
-
* @returns
|
|
635
|
+
* @returns Promise resolving to symbol information for LLM processing
|
|
629
636
|
*/
|
|
630
637
|
async extractSymbolsForLLM(filePathOrBuffer) {
|
|
631
638
|
const tree = await this.loadIntoTree(filePathOrBuffer);
|
|
@@ -655,7 +662,7 @@ class ObfProcessor extends BaseProcessor {
|
|
|
655
662
|
* @param llmTranslations - Array of LLM translations with symbol info
|
|
656
663
|
* @param outputPath - Where to save the translated OBF/OBZ file
|
|
657
664
|
* @param options - Translation options (e.g., allowPartial for testing)
|
|
658
|
-
* @returns
|
|
665
|
+
* @returns Promise resolving to a buffer of the translated OBF/OBZ file
|
|
659
666
|
*/
|
|
660
667
|
async processLLMTranslations(filePathOrBuffer, llmTranslations, outputPath, options) {
|
|
661
668
|
const { readBinaryFromInput } = this.options.fileAdapter;
|
|
@@ -697,7 +704,7 @@ class ObfProcessor extends BaseProcessor {
|
|
|
697
704
|
});
|
|
698
705
|
// Save and return
|
|
699
706
|
await this.saveFromTree(tree, outputPath);
|
|
700
|
-
return readBinaryFromInput(outputPath);
|
|
707
|
+
return await readBinaryFromInput(outputPath);
|
|
701
708
|
}
|
|
702
709
|
getObfValidator() {
|
|
703
710
|
try {
|
|
@@ -50,8 +50,7 @@ class OpmlProcessor extends BaseProcessor {
|
|
|
50
50
|
}
|
|
51
51
|
async extractTexts(filePathOrBuffer) {
|
|
52
52
|
const { readTextFromInput } = this.options.fileAdapter;
|
|
53
|
-
await
|
|
54
|
-
const content = readTextFromInput(filePathOrBuffer);
|
|
53
|
+
const content = await readTextFromInput(filePathOrBuffer);
|
|
55
54
|
const parser = new XMLParser({ ignoreAttributes: false });
|
|
56
55
|
const data = parser.parse(content);
|
|
57
56
|
const texts = [];
|
|
@@ -83,10 +82,9 @@ class OpmlProcessor extends BaseProcessor {
|
|
|
83
82
|
}
|
|
84
83
|
async loadIntoTree(filePathOrBuffer) {
|
|
85
84
|
const { readBinaryFromInput, readTextFromInput } = this.options.fileAdapter;
|
|
86
|
-
await Promise.resolve();
|
|
87
85
|
const filename = typeof filePathOrBuffer === 'string' ? getBasename(filePathOrBuffer) : 'upload.opml';
|
|
88
|
-
const buffer = readBinaryFromInput(filePathOrBuffer);
|
|
89
|
-
const content = readTextFromInput(buffer);
|
|
86
|
+
const buffer = await readBinaryFromInput(filePathOrBuffer);
|
|
87
|
+
const content = await readTextFromInput(buffer);
|
|
90
88
|
try {
|
|
91
89
|
if (!content || !content.trim()) {
|
|
92
90
|
const validationResult = buildValidationResultFromMessage({
|
|
@@ -170,8 +168,7 @@ class OpmlProcessor extends BaseProcessor {
|
|
|
170
168
|
}
|
|
171
169
|
async processTexts(filePathOrBuffer, translations, outputPath) {
|
|
172
170
|
const { writeBinaryToPath, readTextFromInput } = this.options.fileAdapter;
|
|
173
|
-
await
|
|
174
|
-
const content = readTextFromInput(filePathOrBuffer);
|
|
171
|
+
const content = await readTextFromInput(filePathOrBuffer);
|
|
175
172
|
let translatedContent = content;
|
|
176
173
|
// Apply translations to text attributes in OPML outline elements
|
|
177
174
|
translations.forEach((translation, originalText) => {
|
|
@@ -182,12 +179,11 @@ class OpmlProcessor extends BaseProcessor {
|
|
|
182
179
|
}
|
|
183
180
|
});
|
|
184
181
|
const resultBuffer = encodeText(translatedContent);
|
|
185
|
-
writeBinaryToPath(outputPath, resultBuffer);
|
|
182
|
+
await writeBinaryToPath(outputPath, resultBuffer);
|
|
186
183
|
return resultBuffer;
|
|
187
184
|
}
|
|
188
185
|
async saveFromTree(tree, outputPath) {
|
|
189
186
|
const { writeTextToPath } = this.options.fileAdapter;
|
|
190
|
-
await Promise.resolve();
|
|
191
187
|
// Helper to recursively build outline nodes with cycle detection
|
|
192
188
|
function buildOutline(page, visited = new Set()) {
|
|
193
189
|
// Prevent infinite recursion by tracking visited pages
|
|
@@ -257,7 +253,7 @@ class OpmlProcessor extends BaseProcessor {
|
|
|
257
253
|
attributeNamePrefix: '@_',
|
|
258
254
|
});
|
|
259
255
|
const xml = '<?xml version="1.0" encoding="UTF-8"?>\n' + builder.build(opmlObj);
|
|
260
|
-
writeTextToPath(outputPath, xml);
|
|
256
|
+
await writeTextToPath(outputPath, xml);
|
|
261
257
|
}
|
|
262
258
|
/**
|
|
263
259
|
* Extract strings with metadata for aac-tools-platform compatibility
|
|
@@ -10,7 +10,7 @@ import { requireBetterSqlite3 } from '../../utils/sqlite';
|
|
|
10
10
|
// We extract PNG/JPEG images but skip vector graphics (requires renderer).
|
|
11
11
|
// NOTE: Snap buttons currently do not populate resolvedImageEntry; these helpers
|
|
12
12
|
// therefore return empty collections until image resolution is implemented.
|
|
13
|
-
function collectFiles(root, matcher, maxDepth = 3, fileAdapter = defaultFileAdapter) {
|
|
13
|
+
async function collectFiles(root, matcher, maxDepth = 3, fileAdapter = defaultFileAdapter) {
|
|
14
14
|
const { listDir, join, isDirectory } = fileAdapter;
|
|
15
15
|
const results = new Set();
|
|
16
16
|
const stack = [{ dir: root, depth: 0 }];
|
|
@@ -22,14 +22,14 @@ function collectFiles(root, matcher, maxDepth = 3, fileAdapter = defaultFileAdap
|
|
|
22
22
|
continue;
|
|
23
23
|
let entries;
|
|
24
24
|
try {
|
|
25
|
-
entries = listDir(current.dir);
|
|
25
|
+
entries = await listDir(current.dir);
|
|
26
26
|
}
|
|
27
27
|
catch (error) {
|
|
28
28
|
continue;
|
|
29
29
|
}
|
|
30
30
|
for (const entry of entries) {
|
|
31
31
|
const fullPath = join(current.dir, entry);
|
|
32
|
-
if (isDirectory(entry)) {
|
|
32
|
+
if (await isDirectory(entry)) {
|
|
33
33
|
stack.push({ dir: fullPath, depth: current.depth + 1 });
|
|
34
34
|
}
|
|
35
35
|
else if (matcher(fullPath)) {
|
|
@@ -83,15 +83,15 @@ export function getAllowedImageEntries(tree) {
|
|
|
83
83
|
* @param entryPath Symbol identifier (e.g., "SYM:12345")
|
|
84
84
|
* @returns Image data buffer or null if not found
|
|
85
85
|
*/
|
|
86
|
-
export function openImage(dbOrFile, entryPath, fileAdapter = defaultFileAdapter) {
|
|
86
|
+
export async function openImage(dbOrFile, entryPath, fileAdapter = defaultFileAdapter) {
|
|
87
87
|
const { mkTempDir, join, writeBinaryToPath, removePath, dirname } = fileAdapter;
|
|
88
88
|
let dbPath;
|
|
89
89
|
let cleanupNeeded = false;
|
|
90
90
|
// Handle Buffer input by writing to temp file
|
|
91
91
|
if (Buffer.isBuffer(dbOrFile)) {
|
|
92
|
-
const tempDir = mkTempDir(join(process.cwd(), 'snap-'));
|
|
92
|
+
const tempDir = await mkTempDir(join(process.cwd(), 'snap-'));
|
|
93
93
|
dbPath = join(tempDir, 'temp.sps');
|
|
94
|
-
writeBinaryToPath(dbPath, dbOrFile);
|
|
94
|
+
await writeBinaryToPath(dbPath, dbOrFile);
|
|
95
95
|
cleanupNeeded = true;
|
|
96
96
|
}
|
|
97
97
|
else if (typeof dbOrFile === 'string') {
|
|
@@ -126,9 +126,9 @@ export function openImage(dbOrFile, entryPath, fileAdapter = defaultFileAdapter)
|
|
|
126
126
|
}
|
|
127
127
|
if (cleanupNeeded && dbPath) {
|
|
128
128
|
try {
|
|
129
|
-
removePath(dbPath);
|
|
129
|
+
await removePath(dbPath);
|
|
130
130
|
const dir = dirname(dbPath);
|
|
131
|
-
removePath(dir);
|
|
131
|
+
await removePath(dir);
|
|
132
132
|
}
|
|
133
133
|
catch (e) {
|
|
134
134
|
// Ignore cleanup errors
|
|
@@ -142,7 +142,7 @@ export function openImage(dbOrFile, entryPath, fileAdapter = defaultFileAdapter)
|
|
|
142
142
|
* @param packageNamePattern Optional pattern to filter package names (default: 'TobiiDynavox')
|
|
143
143
|
* @returns Array of Snap package path information
|
|
144
144
|
*/
|
|
145
|
-
export function findSnapPackages(packageNamePattern = 'TobiiDynavox', fileAdapter = defaultFileAdapter) {
|
|
145
|
+
export async function findSnapPackages(packageNamePattern = 'TobiiDynavox', fileAdapter = defaultFileAdapter) {
|
|
146
146
|
const { join, listDir, isDirectory, pathExists } = fileAdapter;
|
|
147
147
|
const results = [];
|
|
148
148
|
// Only works on Windows
|
|
@@ -156,13 +156,13 @@ export function findSnapPackages(packageNamePattern = 'TobiiDynavox', fileAdapte
|
|
|
156
156
|
}
|
|
157
157
|
const packagesPath = join(localAppData, 'Packages');
|
|
158
158
|
// Check if Packages directory exists
|
|
159
|
-
if (!pathExists(packagesPath)) {
|
|
159
|
+
if (!(await pathExists(packagesPath))) {
|
|
160
160
|
return results;
|
|
161
161
|
}
|
|
162
162
|
// Enumerate packages
|
|
163
|
-
const packages = listDir(packagesPath);
|
|
163
|
+
const packages = await listDir(packagesPath);
|
|
164
164
|
for (const packageDir of packages) {
|
|
165
|
-
if (!isDirectory(packageDir))
|
|
165
|
+
if (!(await isDirectory(packageDir)))
|
|
166
166
|
continue;
|
|
167
167
|
const packageName = packageDir;
|
|
168
168
|
// Filter by pattern
|
|
@@ -185,8 +185,8 @@ export function findSnapPackages(packageNamePattern = 'TobiiDynavox', fileAdapte
|
|
|
185
185
|
* @param packageNamePattern Optional pattern to filter package names (default: 'TobiiDynavox')
|
|
186
186
|
* @returns Path to the first matching Snap package, or null if not found
|
|
187
187
|
*/
|
|
188
|
-
export function findSnapPackagePath(packageNamePattern = 'TobiiDynavox', fileAdapter) {
|
|
189
|
-
const packages = findSnapPackages(packageNamePattern, fileAdapter);
|
|
188
|
+
export async function findSnapPackagePath(packageNamePattern = 'TobiiDynavox', fileAdapter) {
|
|
189
|
+
const packages = await findSnapPackages(packageNamePattern, fileAdapter);
|
|
190
190
|
return packages.length > 0 ? packages[0].packagePath : null;
|
|
191
191
|
}
|
|
192
192
|
/**
|
|
@@ -196,28 +196,28 @@ export function findSnapPackagePath(packageNamePattern = 'TobiiDynavox', fileAda
|
|
|
196
196
|
* @param packageNamePattern Optional package filter (default TobiiDynavox)
|
|
197
197
|
* @returns Array of user info with vocab paths
|
|
198
198
|
*/
|
|
199
|
-
export function findSnapUsers(packageNamePattern = 'TobiiDynavox', fileAdapter = defaultFileAdapter) {
|
|
199
|
+
export async function findSnapUsers(packageNamePattern = 'TobiiDynavox', fileAdapter = defaultFileAdapter) {
|
|
200
200
|
const { join, listDir, isDirectory, pathExists } = fileAdapter;
|
|
201
201
|
const results = [];
|
|
202
202
|
if (process.platform !== 'win32') {
|
|
203
203
|
return results;
|
|
204
204
|
}
|
|
205
|
-
const packagePath = findSnapPackagePath(packageNamePattern, fileAdapter);
|
|
205
|
+
const packagePath = await findSnapPackagePath(packageNamePattern, fileAdapter);
|
|
206
206
|
if (!packagePath) {
|
|
207
207
|
return results;
|
|
208
208
|
}
|
|
209
209
|
const usersRoot = join(packagePath, 'LocalState', 'Users');
|
|
210
|
-
if (!pathExists(usersRoot)) {
|
|
210
|
+
if (!(await pathExists(usersRoot))) {
|
|
211
211
|
return results;
|
|
212
212
|
}
|
|
213
|
-
const entries = listDir(usersRoot);
|
|
213
|
+
const entries = await listDir(usersRoot);
|
|
214
214
|
for (const entry of entries) {
|
|
215
|
-
if (!isDirectory(entry))
|
|
215
|
+
if (!(await isDirectory(entry)))
|
|
216
216
|
continue;
|
|
217
217
|
if (entry.toLowerCase().startsWith('swiftkey'))
|
|
218
218
|
continue;
|
|
219
219
|
const userPath = join(usersRoot, entry);
|
|
220
|
-
const vocabPaths = collectFiles(userPath, (full) => {
|
|
220
|
+
const vocabPaths = await collectFiles(userPath, (full) => {
|
|
221
221
|
const ext = extname(full).toLowerCase();
|
|
222
222
|
return ext === '.sps' || ext === '.spb';
|
|
223
223
|
}, 2, fileAdapter);
|
|
@@ -235,8 +235,9 @@ export function findSnapUsers(packageNamePattern = 'TobiiDynavox', fileAdapter =
|
|
|
235
235
|
* @param packageNamePattern Optional package filter
|
|
236
236
|
* @returns Array of vocab file paths
|
|
237
237
|
*/
|
|
238
|
-
export function findSnapUserVocabularies(userId, packageNamePattern = 'TobiiDynavox', fileAdapter) {
|
|
239
|
-
const
|
|
238
|
+
export async function findSnapUserVocabularies(userId, packageNamePattern = 'TobiiDynavox', fileAdapter) {
|
|
239
|
+
const allUsers = await findSnapUsers(packageNamePattern, fileAdapter);
|
|
240
|
+
const users = allUsers.filter((u) => !userId || u.userId === userId);
|
|
240
241
|
return users.flatMap((u) => u.vocabPaths);
|
|
241
242
|
}
|
|
242
243
|
/**
|
|
@@ -246,12 +247,13 @@ export function findSnapUserVocabularies(userId, packageNamePattern = 'TobiiDyna
|
|
|
246
247
|
* @param packageNamePattern Optional package filter
|
|
247
248
|
* @returns Array of history file paths (may be empty if not found)
|
|
248
249
|
*/
|
|
249
|
-
export function findSnapUserHistory(userId, packageNamePattern = 'TobiiDynavox', fileAdapter = defaultFileAdapter) {
|
|
250
|
+
export async function findSnapUserHistory(userId, packageNamePattern = 'TobiiDynavox', fileAdapter = defaultFileAdapter) {
|
|
250
251
|
const { basename } = fileAdapter;
|
|
251
|
-
const
|
|
252
|
+
const allUsers = await findSnapUsers(packageNamePattern, fileAdapter);
|
|
253
|
+
const user = allUsers.find((u) => u.userId === userId);
|
|
252
254
|
if (!user)
|
|
253
255
|
return [];
|
|
254
|
-
return collectFiles(user.userPath, (full) => basename(full).toLowerCase().includes('history'), 2, fileAdapter);
|
|
256
|
+
return await collectFiles(user.userPath, (full) => basename(full).toLowerCase().includes('history'), 2, fileAdapter);
|
|
255
257
|
}
|
|
256
258
|
/**
|
|
257
259
|
* Check whether TD Snap appears to be installed (Windows only)
|
|
@@ -264,9 +266,9 @@ export function isSnapInstalled(packageNamePattern = 'TobiiDynavox') {
|
|
|
264
266
|
/**
|
|
265
267
|
* Read Snap usage history from a pageset file (.sps/.spb)
|
|
266
268
|
*/
|
|
267
|
-
export function readSnapUsage(pagesetPath, fileAdapter = defaultFileAdapter) {
|
|
269
|
+
export async function readSnapUsage(pagesetPath, fileAdapter = defaultFileAdapter) {
|
|
268
270
|
const { pathExists } = fileAdapter;
|
|
269
|
-
if (!pathExists(pagesetPath))
|
|
271
|
+
if (!(await pathExists(pagesetPath)))
|
|
270
272
|
return [];
|
|
271
273
|
const Database = requireBetterSqlite3();
|
|
272
274
|
const db = new Database(pagesetPath, { readonly: true });
|
|
@@ -323,8 +325,10 @@ export function readSnapUsage(pagesetPath, fileAdapter = defaultFileAdapter) {
|
|
|
323
325
|
/**
|
|
324
326
|
* Read Snap usage history for a user (all pagesets)
|
|
325
327
|
*/
|
|
326
|
-
export function readSnapUsageForUser(userId, packageNamePattern = 'TobiiDynavox') {
|
|
327
|
-
const
|
|
328
|
+
export async function readSnapUsageForUser(userId, packageNamePattern = 'TobiiDynavox') {
|
|
329
|
+
const allUsers = await findSnapUsers(packageNamePattern);
|
|
330
|
+
const users = allUsers.filter((u) => !userId || u.userId === userId);
|
|
328
331
|
const pagesets = users.flatMap((u) => u.vocabPaths);
|
|
329
|
-
|
|
332
|
+
const usage = await Promise.all(pagesets.map(async (p) => await readSnapUsage(p)));
|
|
333
|
+
return usage.flat();
|
|
330
334
|
}
|