@willwade/aac-processors 0.2.2 → 0.2.4
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 -1
- package/dist/browser/core/treeStructure.js +3 -1
- package/dist/browser/metrics.js +2 -0
- package/dist/browser/processors/astericsGridProcessor.js +21 -12
- package/dist/browser/processors/gridset/helpers.js +3 -3
- package/dist/browser/processors/gridsetProcessor.js +16 -8
- package/dist/browser/processors/obfProcessor.js +4 -4
- package/dist/browser/processors/snap/helpers.js +3 -3
- package/dist/browser/processors/snapProcessor.js +2 -2
- package/dist/browser/processors/touchchatProcessor.js +6 -6
- package/dist/browser/utilities/analytics/metrics/core.js +213 -8
- package/dist/browser/utilities/analytics/metrics/vocabulary.js +13 -1
- package/dist/browser/utilities/analytics/morphology/engine.js +910 -0
- package/dist/browser/utilities/analytics/morphology/grid3VerbsParser.js +455 -0
- package/dist/browser/utilities/analytics/morphology/index.js +3 -0
- package/dist/browser/utilities/analytics/morphology/types.js +1 -0
- package/dist/browser/utilities/analytics/morphology/wordFormGenerator.js +74 -0
- package/dist/browser/utils/sqlite.js +10 -6
- package/dist/core/treeStructure.d.ts +17 -1
- package/dist/core/treeStructure.js +3 -1
- package/dist/index.node.d.ts +2 -0
- package/dist/index.node.js +6 -1
- package/dist/metrics.d.ts +3 -0
- package/dist/metrics.js +5 -1
- package/dist/processors/astericsGridProcessor.js +21 -12
- package/dist/processors/excelProcessor.js +5 -1
- package/dist/processors/gridset/helpers.js +3 -3
- package/dist/processors/gridset/imageDebug.js +2 -2
- package/dist/processors/gridsetProcessor.js +16 -8
- package/dist/processors/obfProcessor.js +4 -4
- package/dist/processors/snap/helpers.js +3 -3
- package/dist/processors/snapProcessor.js +2 -2
- package/dist/processors/touchchatProcessor.js +6 -6
- package/dist/utilities/analytics/metrics/core.d.ts +14 -0
- package/dist/utilities/analytics/metrics/core.js +213 -8
- package/dist/utilities/analytics/metrics/types.d.ts +18 -3
- package/dist/utilities/analytics/metrics/vocabulary.d.ts +3 -0
- package/dist/utilities/analytics/metrics/vocabulary.js +13 -1
- package/dist/utilities/analytics/morphology/engine.d.ts +30 -0
- package/dist/utilities/analytics/morphology/engine.js +914 -0
- package/dist/utilities/analytics/morphology/grid3VerbsParser.d.ts +36 -0
- package/dist/utilities/analytics/morphology/grid3VerbsParser.js +485 -0
- package/dist/utilities/analytics/morphology/index.d.ts +5 -0
- package/dist/utilities/analytics/morphology/index.js +9 -0
- package/dist/utilities/analytics/morphology/types.d.ts +40 -0
- package/dist/utilities/analytics/morphology/types.js +2 -0
- package/dist/utilities/analytics/morphology/wordFormGenerator.d.ts +10 -0
- package/dist/utilities/analytics/morphology/wordFormGenerator.js +78 -0
- package/dist/utils/sqlite.js +10 -29
- package/package.json +13 -12
package/README.md
CHANGED
|
@@ -31,7 +31,9 @@ Browser-safe entry that avoids Node-only dependencies. It expects `Buffer`,
|
|
|
31
31
|
`Uint8Array`, or `ArrayBuffer` inputs rather than file paths.
|
|
32
32
|
|
|
33
33
|
SQLite-backed formats (Snap `.sps/.spb` and TouchChat `.ce`) require a WASM
|
|
34
|
-
SQLite engine.
|
|
34
|
+
SQLite engine. To support these, configure `sql.js` in your bundler
|
|
35
|
+
and either include `<script src="sql-wasm.js"></script>` in your HTML, or
|
|
36
|
+
`window.initSqlJs = require('sql.js');` in your app.
|
|
35
37
|
|
|
36
38
|
```ts
|
|
37
39
|
import { configureSqlJs, SnapProcessor } from '@willwade/aac-processors/browser';
|
|
@@ -51,7 +51,7 @@ export var AACScanType;
|
|
|
51
51
|
AACScanType["BLOCK_COLUMN_ROW"] = "block-column-row";
|
|
52
52
|
})(AACScanType || (AACScanType = {}));
|
|
53
53
|
export class AACButton {
|
|
54
|
-
constructor({ id, label = '', message = '', targetPageId, semanticAction, audioRecording, style, contentType, contentSubType, image, resolvedImageEntry, symbolLibrary, symbolPath, x, y, columnSpan, rowSpan, scanBlocks, scanBlock, visibility, directActivate, parameters, predictions, semantic_id, clone_id,
|
|
54
|
+
constructor({ id, label = '', message = '', targetPageId, semanticAction, audioRecording, style, contentType, contentSubType, image, resolvedImageEntry, symbolLibrary, symbolPath, x, y, columnSpan, rowSpan, scanBlocks, scanBlock, visibility, directActivate, parameters, predictions, pos, wordForms, semantic_id, clone_id,
|
|
55
55
|
// Legacy input support
|
|
56
56
|
type, action, }) {
|
|
57
57
|
this.id = id;
|
|
@@ -77,6 +77,8 @@ export class AACButton {
|
|
|
77
77
|
this.directActivate = directActivate;
|
|
78
78
|
this.parameters = parameters;
|
|
79
79
|
this.predictions = predictions;
|
|
80
|
+
this.pos = pos;
|
|
81
|
+
this.wordForms = wordForms;
|
|
80
82
|
this.semantic_id = semantic_id;
|
|
81
83
|
this.clone_id = clone_id;
|
|
82
84
|
// Legacy mapping: if no semanticAction provided, derive from legacy `action` first
|
package/dist/browser/metrics.js
CHANGED
|
@@ -12,6 +12,8 @@ export { MetricsCalculator } from './utilities/analytics/metrics/core';
|
|
|
12
12
|
export { VocabularyAnalyzer } from './utilities/analytics/metrics/vocabulary';
|
|
13
13
|
export { SentenceAnalyzer } from './utilities/analytics/metrics/sentence';
|
|
14
14
|
export { ComparisonAnalyzer } from './utilities/analytics/metrics/comparison';
|
|
15
|
+
export { MorphologyEngine } from './utilities/analytics/morphology';
|
|
16
|
+
export { WordFormGenerator } from './utilities/analytics/morphology';
|
|
15
17
|
export { ReferenceLoader } from './utilities/analytics/reference';
|
|
16
18
|
export { InMemoryReferenceLoader, createBrowserReferenceLoader, loadReferenceDataFromUrl, } from './utilities/analytics/reference/browser';
|
|
17
19
|
export * from './utilities/analytics/utils/idGenerator';
|
|
@@ -605,7 +605,7 @@ class AstericsGridProcessor extends BaseProcessor {
|
|
|
605
605
|
});
|
|
606
606
|
});
|
|
607
607
|
}
|
|
608
|
-
catch (
|
|
608
|
+
catch (_error) {
|
|
609
609
|
// If JSON parsing fails, return empty array
|
|
610
610
|
}
|
|
611
611
|
return texts;
|
|
@@ -1021,7 +1021,7 @@ class AstericsGridProcessor extends BaseProcessor {
|
|
|
1021
1021
|
// Use detected format for filename
|
|
1022
1022
|
imageName = element.image.id || `image.${imageFormat}`;
|
|
1023
1023
|
}
|
|
1024
|
-
catch (
|
|
1024
|
+
catch (_e) {
|
|
1025
1025
|
// Invalid base64 data, skip image
|
|
1026
1026
|
}
|
|
1027
1027
|
}
|
|
@@ -1034,19 +1034,23 @@ class AstericsGridProcessor extends BaseProcessor {
|
|
|
1034
1034
|
audioRecording: audioRecording,
|
|
1035
1035
|
visibility: mapAstericsVisibility(element.hidden),
|
|
1036
1036
|
image: imageName, // Store image filename/reference
|
|
1037
|
-
parameters: imageData
|
|
1038
|
-
? {
|
|
1039
|
-
...{ imageData: imageData }, // Store actual image data in parameters for conversion
|
|
1040
|
-
}
|
|
1041
|
-
: undefined,
|
|
1042
1037
|
style: {
|
|
1043
1038
|
backgroundColor: finalBackgroundColor,
|
|
1044
1039
|
borderColor: colorStyles.borderColor || colorConfig?.elementBorderColor || '#CCCCCC',
|
|
1045
1040
|
borderWidth: colorConfig?.borderWidth || 1,
|
|
1046
1041
|
fontFamily: colorConfig?.fontFamily || 'Arial',
|
|
1047
|
-
fontSize: colorConfig?.fontSizePct ? colorConfig.fontSizePct * 16 : 16,
|
|
1042
|
+
fontSize: colorConfig?.fontSizePct ? colorConfig.fontSizePct * 16 : 16,
|
|
1048
1043
|
fontColor: fontColor,
|
|
1049
1044
|
},
|
|
1045
|
+
wordForms: element.wordForms && element.wordForms.length > 0 ? element.wordForms : undefined,
|
|
1046
|
+
parameters: {
|
|
1047
|
+
...(imageData ? { imageData: imageData } : {}),
|
|
1048
|
+
...(element.actions?.some((a) => a.modelName === 'GridActionWordForm')
|
|
1049
|
+
? {
|
|
1050
|
+
wordFormActions: element.actions.filter((a) => a.modelName === 'GridActionWordForm'),
|
|
1051
|
+
}
|
|
1052
|
+
: {}),
|
|
1053
|
+
},
|
|
1050
1054
|
});
|
|
1051
1055
|
}
|
|
1052
1056
|
async processTexts(filePathOrBuffer, translations, outputPath) {
|
|
@@ -1313,6 +1317,11 @@ class AstericsGridProcessor extends BaseProcessor {
|
|
|
1313
1317
|
});
|
|
1314
1318
|
}
|
|
1315
1319
|
const locale = tree.metadata?.locale || 'en';
|
|
1320
|
+
if (button.parameters?.wordFormActions &&
|
|
1321
|
+
Array.isArray(button.parameters.wordFormActions)) {
|
|
1322
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
1323
|
+
actions.push(...button.parameters.wordFormActions);
|
|
1324
|
+
}
|
|
1316
1325
|
return {
|
|
1317
1326
|
id: button.id,
|
|
1318
1327
|
modelName: 'GridElement',
|
|
@@ -1322,7 +1331,7 @@ class AstericsGridProcessor extends BaseProcessor {
|
|
|
1322
1331
|
x: calculatedX,
|
|
1323
1332
|
y: calculatedY,
|
|
1324
1333
|
label: { [locale]: button.label },
|
|
1325
|
-
wordForms: [],
|
|
1334
|
+
wordForms: button.wordForms || [],
|
|
1326
1335
|
image: {
|
|
1327
1336
|
data: null,
|
|
1328
1337
|
author: undefined,
|
|
@@ -1414,7 +1423,7 @@ class AstericsGridProcessor extends BaseProcessor {
|
|
|
1414
1423
|
audioAction.durationMs = parsedMetadata.durationMs || audioAction.durationMs;
|
|
1415
1424
|
audioAction.filename = parsedMetadata.filename || audioAction.filename;
|
|
1416
1425
|
}
|
|
1417
|
-
catch (
|
|
1426
|
+
catch (_e) {
|
|
1418
1427
|
// Use defaults if metadata parsing fails
|
|
1419
1428
|
}
|
|
1420
1429
|
}
|
|
@@ -1466,7 +1475,7 @@ class AstericsGridProcessor extends BaseProcessor {
|
|
|
1466
1475
|
});
|
|
1467
1476
|
});
|
|
1468
1477
|
}
|
|
1469
|
-
catch (
|
|
1478
|
+
catch (_error) {
|
|
1470
1479
|
// If JSON parsing fails, return empty array
|
|
1471
1480
|
}
|
|
1472
1481
|
return elementIds;
|
|
@@ -1491,7 +1500,7 @@ class AstericsGridProcessor extends BaseProcessor {
|
|
|
1491
1500
|
}
|
|
1492
1501
|
}
|
|
1493
1502
|
}
|
|
1494
|
-
catch (
|
|
1503
|
+
catch (_error) {
|
|
1495
1504
|
// If JSON parsing fails, return false
|
|
1496
1505
|
}
|
|
1497
1506
|
return false;
|
|
@@ -66,7 +66,7 @@ export async function openImage(gridsetBuffer, entryPath, password = resolveGrid
|
|
|
66
66
|
}
|
|
67
67
|
return data;
|
|
68
68
|
}
|
|
69
|
-
catch (
|
|
69
|
+
catch (_error) {
|
|
70
70
|
return null;
|
|
71
71
|
}
|
|
72
72
|
}
|
|
@@ -156,7 +156,7 @@ export function getCommonDocumentsPath() {
|
|
|
156
156
|
return match[1].trim();
|
|
157
157
|
}
|
|
158
158
|
}
|
|
159
|
-
catch (
|
|
159
|
+
catch (_error) {
|
|
160
160
|
// Registry access failed, fall back to default
|
|
161
161
|
}
|
|
162
162
|
// Default fallback path
|
|
@@ -212,7 +212,7 @@ export async function findGrid3UserPaths(fileAdapter = defaultFileAdapter) {
|
|
|
212
212
|
}
|
|
213
213
|
}
|
|
214
214
|
}
|
|
215
|
-
catch (
|
|
215
|
+
catch (_error) {
|
|
216
216
|
// Silently fail if directory access fails
|
|
217
217
|
}
|
|
218
218
|
return results;
|
|
@@ -462,7 +462,7 @@ class GridsetProcessor extends BaseProcessor {
|
|
|
462
462
|
}
|
|
463
463
|
}
|
|
464
464
|
}
|
|
465
|
-
catch (
|
|
465
|
+
catch (_e) {
|
|
466
466
|
/* ignore: optional FileMap.xml may be missing or malformed */
|
|
467
467
|
}
|
|
468
468
|
// First, load styles from Settings0/Styles/styles.xml (Grid3 format)
|
|
@@ -533,7 +533,7 @@ class GridsetProcessor extends BaseProcessor {
|
|
|
533
533
|
const normalizedEntry = imageEntry.entryName.replace(/\\/g, '/');
|
|
534
534
|
imageDataCache.set(normalizedEntry, data);
|
|
535
535
|
}
|
|
536
|
-
catch (
|
|
536
|
+
catch (_err) {
|
|
537
537
|
// Silently fail - individual image loading failures shouldn't break the entire load
|
|
538
538
|
}
|
|
539
539
|
}
|
|
@@ -566,7 +566,7 @@ class GridsetProcessor extends BaseProcessor {
|
|
|
566
566
|
}
|
|
567
567
|
}
|
|
568
568
|
}
|
|
569
|
-
catch (
|
|
569
|
+
catch (_e) {
|
|
570
570
|
// Skip errors in first pass
|
|
571
571
|
}
|
|
572
572
|
}
|
|
@@ -581,7 +581,7 @@ class GridsetProcessor extends BaseProcessor {
|
|
|
581
581
|
xmlContent = decodeText(buffer);
|
|
582
582
|
console.log(`[Gridset] Raw XML content (first 200 chars) for ${entry.entryName}:`, xmlContent.substring(0, 200));
|
|
583
583
|
}
|
|
584
|
-
catch (
|
|
584
|
+
catch (_e) {
|
|
585
585
|
// Skip unreadable files
|
|
586
586
|
continue;
|
|
587
587
|
}
|
|
@@ -940,6 +940,7 @@ class GridsetProcessor extends BaseProcessor {
|
|
|
940
940
|
// infer action type implicitly from commands; no explicit enum needed
|
|
941
941
|
let navigationTarget;
|
|
942
942
|
let detectedCommands = []; // Store detected command metadata
|
|
943
|
+
let buttonPos; // Part-of-speech from Action.InsertText
|
|
943
944
|
const commands = content.Commands?.Command || content.commands?.command;
|
|
944
945
|
let predictionWords;
|
|
945
946
|
// Resolve image for this cell using FileMap and coordinate heuristics
|
|
@@ -1226,8 +1227,11 @@ class GridsetProcessor extends BaseProcessor {
|
|
|
1226
1227
|
break;
|
|
1227
1228
|
}
|
|
1228
1229
|
case 'Action.InsertText': {
|
|
1229
|
-
// speak
|
|
1230
1230
|
const insertText = getParam('text');
|
|
1231
|
+
const posParam = getParam('pos');
|
|
1232
|
+
if (posParam) {
|
|
1233
|
+
buttonPos = posParam;
|
|
1234
|
+
}
|
|
1231
1235
|
semanticAction = {
|
|
1232
1236
|
category: AACSemanticCategory.COMMUNICATION,
|
|
1233
1237
|
intent: AACSemanticIntent.INSERT_TEXT,
|
|
@@ -1235,7 +1239,7 @@ class GridsetProcessor extends BaseProcessor {
|
|
|
1235
1239
|
platformData: {
|
|
1236
1240
|
grid3: {
|
|
1237
1241
|
commandId,
|
|
1238
|
-
parameters: { text: insertText },
|
|
1242
|
+
parameters: { text: insertText, pos: posParam },
|
|
1239
1243
|
},
|
|
1240
1244
|
},
|
|
1241
1245
|
fallback: {
|
|
@@ -1454,8 +1458,10 @@ class GridsetProcessor extends BaseProcessor {
|
|
|
1454
1458
|
}
|
|
1455
1459
|
// Extract grammar tags from commands (Smart Grammar)
|
|
1456
1460
|
const grammar = {};
|
|
1461
|
+
if (buttonPos)
|
|
1462
|
+
grammar.pos = buttonPos;
|
|
1457
1463
|
detectedCommands.forEach((cmd) => {
|
|
1458
|
-
if (cmd.parameters.pos)
|
|
1464
|
+
if (!grammar.pos && cmd.parameters.pos)
|
|
1459
1465
|
grammar.pos = cmd.parameters.pos;
|
|
1460
1466
|
if (cmd.parameters.person)
|
|
1461
1467
|
grammar.person = cmd.parameters.person;
|
|
@@ -1465,6 +1471,7 @@ class GridsetProcessor extends BaseProcessor {
|
|
|
1465
1471
|
grammar.feature = cmd.parameters.feature;
|
|
1466
1472
|
});
|
|
1467
1473
|
const isSmartGrammarCell = Object.keys(grammar).length > 0;
|
|
1474
|
+
const effectivePos = buttonPos || grammar.pos || undefined;
|
|
1468
1475
|
const button = new AACButton({
|
|
1469
1476
|
id: `${gridId}_btn_${idx}`,
|
|
1470
1477
|
label: String(label),
|
|
@@ -1502,6 +1509,7 @@ class GridsetProcessor extends BaseProcessor {
|
|
|
1502
1509
|
: gridPredictionWords.length > 0
|
|
1503
1510
|
? [...gridPredictionWords]
|
|
1504
1511
|
: undefined,
|
|
1512
|
+
pos: effectivePos,
|
|
1505
1513
|
parameters: {
|
|
1506
1514
|
pluginMetadata: pluginMetadata, // Store full plugin metadata for future use
|
|
1507
1515
|
grid3Commands: detectedCommands, // Store detected command metadata
|
|
@@ -1673,7 +1681,7 @@ class GridsetProcessor extends BaseProcessor {
|
|
|
1673
1681
|
}
|
|
1674
1682
|
}
|
|
1675
1683
|
}
|
|
1676
|
-
catch (
|
|
1684
|
+
catch (_e) {
|
|
1677
1685
|
// If settings.xml parsing fails, tree.rootId will default to first page
|
|
1678
1686
|
}
|
|
1679
1687
|
// Set metadata on tree
|
|
@@ -48,7 +48,7 @@ class ObfProcessor extends BaseProcessor {
|
|
|
48
48
|
return null;
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
-
catch (
|
|
51
|
+
catch (_err) {
|
|
52
52
|
continue;
|
|
53
53
|
}
|
|
54
54
|
}
|
|
@@ -94,7 +94,7 @@ class ObfProcessor extends BaseProcessor {
|
|
|
94
94
|
return dataUrl;
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
|
-
catch (
|
|
97
|
+
catch (_err) {
|
|
98
98
|
// Continue to next path
|
|
99
99
|
continue;
|
|
100
100
|
}
|
|
@@ -318,7 +318,7 @@ class ObfProcessor extends BaseProcessor {
|
|
|
318
318
|
return obj;
|
|
319
319
|
}
|
|
320
320
|
}
|
|
321
|
-
catch (
|
|
321
|
+
catch (_error) {
|
|
322
322
|
// Log parsing errors for debugging but don't throw
|
|
323
323
|
}
|
|
324
324
|
return null;
|
|
@@ -725,7 +725,7 @@ class ObfProcessor extends BaseProcessor {
|
|
|
725
725
|
// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-unsafe-return
|
|
726
726
|
return require('../validation/obfValidator').ObfValidator;
|
|
727
727
|
}
|
|
728
|
-
catch (
|
|
728
|
+
catch (_error) {
|
|
729
729
|
throw new Error('Validation utilities are not available in this environment.');
|
|
730
730
|
}
|
|
731
731
|
}
|
|
@@ -24,7 +24,7 @@ async function collectFiles(root, matcher, maxDepth = 3, fileAdapter = defaultFi
|
|
|
24
24
|
try {
|
|
25
25
|
entries = await listDir(current.dir);
|
|
26
26
|
}
|
|
27
|
-
catch (
|
|
27
|
+
catch (_error) {
|
|
28
28
|
continue;
|
|
29
29
|
}
|
|
30
30
|
for (const entry of entries) {
|
|
@@ -130,7 +130,7 @@ export async function openImage(dbOrFile, entryPath, fileAdapter = defaultFileAd
|
|
|
130
130
|
const dir = dirname(dbPath);
|
|
131
131
|
await removePath(dir);
|
|
132
132
|
}
|
|
133
|
-
catch (
|
|
133
|
+
catch (_e) {
|
|
134
134
|
// Ignore cleanup errors
|
|
135
135
|
}
|
|
136
136
|
}
|
|
@@ -174,7 +174,7 @@ export async function findSnapPackages(packageNamePattern = 'TobiiDynavox', file
|
|
|
174
174
|
}
|
|
175
175
|
}
|
|
176
176
|
}
|
|
177
|
-
catch (
|
|
177
|
+
catch (_error) {
|
|
178
178
|
// Silently fail if directory access fails
|
|
179
179
|
}
|
|
180
180
|
return results;
|
|
@@ -215,7 +215,7 @@ class SnapProcessor extends BaseProcessor {
|
|
|
215
215
|
try {
|
|
216
216
|
positions = JSON.parse(sg.SerializedGridPositions);
|
|
217
217
|
}
|
|
218
|
-
catch (
|
|
218
|
+
catch (_e) {
|
|
219
219
|
// Invalid JSON, skip this group
|
|
220
220
|
return;
|
|
221
221
|
}
|
|
@@ -436,7 +436,7 @@ class SnapProcessor extends BaseProcessor {
|
|
|
436
436
|
}
|
|
437
437
|
}
|
|
438
438
|
}
|
|
439
|
-
catch (
|
|
439
|
+
catch (_e) {
|
|
440
440
|
// Ignore JSON parse errors in commands
|
|
441
441
|
}
|
|
442
442
|
}
|
|
@@ -101,7 +101,7 @@ class TouchChatProcessor extends BaseProcessor {
|
|
|
101
101
|
idMappings.set(mapping.numeric_id, mapping.string_id);
|
|
102
102
|
});
|
|
103
103
|
}
|
|
104
|
-
catch (
|
|
104
|
+
catch (_e) {
|
|
105
105
|
// No mapping table, use numeric IDs as strings
|
|
106
106
|
}
|
|
107
107
|
// Load styles
|
|
@@ -119,7 +119,7 @@ class TouchChatProcessor extends BaseProcessor {
|
|
|
119
119
|
pageStyles.set(style.id, style);
|
|
120
120
|
});
|
|
121
121
|
}
|
|
122
|
-
catch (
|
|
122
|
+
catch (_e) {
|
|
123
123
|
// console.log('No styles found:', e);
|
|
124
124
|
}
|
|
125
125
|
// First, load all pages and get their names from resources
|
|
@@ -307,7 +307,7 @@ class TouchChatProcessor extends BaseProcessor {
|
|
|
307
307
|
}
|
|
308
308
|
});
|
|
309
309
|
}
|
|
310
|
-
catch (
|
|
310
|
+
catch (_e) {
|
|
311
311
|
// console.log('No button box cells found:', e);
|
|
312
312
|
}
|
|
313
313
|
// Load buttons directly linked to pages via resources
|
|
@@ -366,7 +366,7 @@ class TouchChatProcessor extends BaseProcessor {
|
|
|
366
366
|
page.addButton(button);
|
|
367
367
|
});
|
|
368
368
|
}
|
|
369
|
-
catch (
|
|
369
|
+
catch (_e) {
|
|
370
370
|
// console.log('No direct page buttons found:', e);
|
|
371
371
|
}
|
|
372
372
|
// Load navigation actions
|
|
@@ -413,7 +413,7 @@ class TouchChatProcessor extends BaseProcessor {
|
|
|
413
413
|
}
|
|
414
414
|
});
|
|
415
415
|
}
|
|
416
|
-
catch (
|
|
416
|
+
catch (_e) {
|
|
417
417
|
// console.log('No navigation actions found:', e);
|
|
418
418
|
}
|
|
419
419
|
// Try to load root ID from multiple sources in order of priority
|
|
@@ -454,7 +454,7 @@ class TouchChatProcessor extends BaseProcessor {
|
|
|
454
454
|
tree.metadata.defaultHomePageId = rootPageId;
|
|
455
455
|
}
|
|
456
456
|
}
|
|
457
|
-
catch (
|
|
457
|
+
catch (_e) {
|
|
458
458
|
// No metadata table or other error, use first page as root
|
|
459
459
|
if (rootPageId) {
|
|
460
460
|
tree.rootId = rootPageId;
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { AACSemanticCategory, AACScanType, } from '../../../core/treeStructure';
|
|
10
10
|
import { CellScanningOrder, ScanningSelectionMethod } from '../../../types/aac';
|
|
11
11
|
import { baseBoardEffort, distanceEffort, visualScanEffort, EFFORT_CONSTANTS, localScanEffort, scanningEffort, } from './effort';
|
|
12
|
+
import { MorphologyEngine } from '../morphology';
|
|
12
13
|
export class MetricsCalculator {
|
|
13
14
|
constructor() {
|
|
14
15
|
this.locale = 'en';
|
|
@@ -80,9 +81,11 @@ export class MetricsCalculator {
|
|
|
80
81
|
});
|
|
81
82
|
// Update buttons using dynamic spelling effort if applicable
|
|
82
83
|
const buttons = Array.from(knownButtons.values()).sort((a, b) => a.effort - b.effort);
|
|
83
|
-
//
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
// Expand morphological predictions from POS tags if enabled or auto-detected
|
|
85
|
+
const useSmartGrammar = options.useSmartGrammar === true || this.treeHasPosTags(tree);
|
|
86
|
+
if (useSmartGrammar) {
|
|
87
|
+
this.expandMorphologicalPredictions(tree, options);
|
|
88
|
+
}
|
|
86
89
|
if (useSmartGrammar) {
|
|
87
90
|
const { wordFormMetrics, replacedLabels } = this.calculateWordFormMetrics(tree, buttons, options);
|
|
88
91
|
// Remove buttons that were replaced by lower-effort word forms
|
|
@@ -151,13 +154,21 @@ export class MetricsCalculator {
|
|
|
151
154
|
}) || null;
|
|
152
155
|
}
|
|
153
156
|
if (!spellingPage)
|
|
154
|
-
return {
|
|
157
|
+
return {
|
|
158
|
+
spellingPage: null,
|
|
159
|
+
spellingBaseEffort: 10,
|
|
160
|
+
spellingAvgLetterEffort: 2.5,
|
|
161
|
+
};
|
|
155
162
|
// Calculate effort to reach this page from root
|
|
156
163
|
const rootBoard = tree.rootId
|
|
157
164
|
? tree.pages[tree.rootId]
|
|
158
165
|
: Object.values(tree.pages).find((p) => !p.parentId);
|
|
159
166
|
if (!rootBoard)
|
|
160
|
-
return {
|
|
167
|
+
return {
|
|
168
|
+
spellingPage,
|
|
169
|
+
spellingBaseEffort: 10,
|
|
170
|
+
spellingAvgLetterEffort: 2.5,
|
|
171
|
+
};
|
|
161
172
|
// Analyze specifically to find the lowest effort path to the spelling page
|
|
162
173
|
const result = this.analyzeFrom(tree, rootBoard, setPcts, true, options);
|
|
163
174
|
const spellingBaseEffort = result.visitedBoardEfforts.get(spellingPage.id) ?? 10;
|
|
@@ -571,6 +582,157 @@ export class MetricsCalculator {
|
|
|
571
582
|
boardPcts['all'] = totalLinks;
|
|
572
583
|
return boardPcts;
|
|
573
584
|
}
|
|
585
|
+
/**
|
|
586
|
+
* Quick check whether any button in the tree has a POS tag.
|
|
587
|
+
* Used to auto-enable smart grammar without requiring explicit opt-in.
|
|
588
|
+
*/
|
|
589
|
+
treeHasPosTags(tree) {
|
|
590
|
+
for (const page of Object.values(tree.pages)) {
|
|
591
|
+
for (const row of page.grid) {
|
|
592
|
+
for (const btn of row) {
|
|
593
|
+
if (btn?.pos && btn.pos !== 'Unknown' && btn.pos !== 'Ignore') {
|
|
594
|
+
return true;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
return false;
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Expand morphological predictions from POS tags on buttons
|
|
603
|
+
*
|
|
604
|
+
* For each button that has a POS tag (e.g., 'Verb', 'Noun'), use the
|
|
605
|
+
* MorphologyEngine to generate inflected word forms and populate the
|
|
606
|
+
* button's predictions array. This is done as a pre-processing step
|
|
607
|
+
* before calculateWordFormMetrics assigns effort to each form.
|
|
608
|
+
*/
|
|
609
|
+
expandMorphologicalPredictions(tree, options) {
|
|
610
|
+
const locale = options.morphologyLocale || 'en-gb';
|
|
611
|
+
const morph = new MorphologyEngine(locale);
|
|
612
|
+
// Words that should never be POS-inferred (function words, determiners, etc.)
|
|
613
|
+
const skipInference = new Set([
|
|
614
|
+
'a',
|
|
615
|
+
'an',
|
|
616
|
+
'the',
|
|
617
|
+
'to',
|
|
618
|
+
'in',
|
|
619
|
+
'on',
|
|
620
|
+
'at',
|
|
621
|
+
'of',
|
|
622
|
+
'for',
|
|
623
|
+
'and',
|
|
624
|
+
'or',
|
|
625
|
+
'but',
|
|
626
|
+
'not',
|
|
627
|
+
'no',
|
|
628
|
+
'yes',
|
|
629
|
+
'is',
|
|
630
|
+
'am',
|
|
631
|
+
'are',
|
|
632
|
+
'was',
|
|
633
|
+
'were',
|
|
634
|
+
'be',
|
|
635
|
+
'been',
|
|
636
|
+
'being',
|
|
637
|
+
'has',
|
|
638
|
+
'have',
|
|
639
|
+
'had',
|
|
640
|
+
'do',
|
|
641
|
+
'does',
|
|
642
|
+
'did',
|
|
643
|
+
'will',
|
|
644
|
+
'would',
|
|
645
|
+
'could',
|
|
646
|
+
'should',
|
|
647
|
+
'shall',
|
|
648
|
+
'may',
|
|
649
|
+
'might',
|
|
650
|
+
'can',
|
|
651
|
+
'must',
|
|
652
|
+
'with',
|
|
653
|
+
'from',
|
|
654
|
+
'by',
|
|
655
|
+
'up',
|
|
656
|
+
'down',
|
|
657
|
+
'out',
|
|
658
|
+
'off',
|
|
659
|
+
'over',
|
|
660
|
+
'under',
|
|
661
|
+
'again',
|
|
662
|
+
'then',
|
|
663
|
+
'than',
|
|
664
|
+
'so',
|
|
665
|
+
'if',
|
|
666
|
+
'when',
|
|
667
|
+
'where',
|
|
668
|
+
'how',
|
|
669
|
+
'what',
|
|
670
|
+
'who',
|
|
671
|
+
'which',
|
|
672
|
+
'that',
|
|
673
|
+
'this',
|
|
674
|
+
'these',
|
|
675
|
+
'those',
|
|
676
|
+
'here',
|
|
677
|
+
'there',
|
|
678
|
+
'now',
|
|
679
|
+
'very',
|
|
680
|
+
'just',
|
|
681
|
+
'more',
|
|
682
|
+
'also',
|
|
683
|
+
'too',
|
|
684
|
+
'please',
|
|
685
|
+
'thank',
|
|
686
|
+
'hi',
|
|
687
|
+
'hello',
|
|
688
|
+
'bye',
|
|
689
|
+
'goodbye',
|
|
690
|
+
'okay',
|
|
691
|
+
'oh',
|
|
692
|
+
'wow',
|
|
693
|
+
'sorry',
|
|
694
|
+
]);
|
|
695
|
+
for (const page of Object.values(tree.pages)) {
|
|
696
|
+
for (const row of page.grid) {
|
|
697
|
+
for (const btn of row) {
|
|
698
|
+
if (!btn || !btn.label)
|
|
699
|
+
continue;
|
|
700
|
+
let pos = btn.pos;
|
|
701
|
+
// If no POS tag (or Unknown/Ignore), attempt POS inference.
|
|
702
|
+
// Many content words on topic pages lack POS tags even though
|
|
703
|
+
// they are clearly nouns (e.g., "bird", "tree", "cloud").
|
|
704
|
+
// Strategy: check irregular tables first for confident POS,
|
|
705
|
+
// then fall back to Noun for single-word content labels.
|
|
706
|
+
if (!pos || pos === 'Unknown' || pos === 'Ignore') {
|
|
707
|
+
const lower = btn.label.toLowerCase();
|
|
708
|
+
// Skip function words and multi-word labels
|
|
709
|
+
if (!skipInference.has(lower) && !lower.includes(' ') && lower.length > 1) {
|
|
710
|
+
// Check irregular tables for confident POS assignment
|
|
711
|
+
const inferredPOS = morph.inferPOS(lower);
|
|
712
|
+
if (inferredPOS) {
|
|
713
|
+
pos = inferredPOS;
|
|
714
|
+
btn.pos = inferredPOS;
|
|
715
|
+
}
|
|
716
|
+
else {
|
|
717
|
+
// Default to Noun for untagged content words.
|
|
718
|
+
// This generates plurals (e.g., bird → birds, tree → trees).
|
|
719
|
+
pos = 'Noun';
|
|
720
|
+
btn.pos = 'Noun';
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
if (!pos || pos === 'Unknown' || pos === 'Ignore')
|
|
725
|
+
continue;
|
|
726
|
+
const forms = morph.inflect(btn.label, pos);
|
|
727
|
+
if (forms.length > 0) {
|
|
728
|
+
const existing = btn.predictions || [];
|
|
729
|
+
const merged = new Set([...existing, ...forms]);
|
|
730
|
+
btn.predictions = Array.from(merged);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
574
736
|
/**
|
|
575
737
|
* Calculate metrics for word forms (smart grammar predictions)
|
|
576
738
|
*
|
|
@@ -593,14 +755,53 @@ export class MetricsCalculator {
|
|
|
593
755
|
// Track buttons by label to compare efforts
|
|
594
756
|
const existingLabels = new Map();
|
|
595
757
|
buttons.forEach((btn) => existingLabels.set(btn.label.toLowerCase(), btn));
|
|
758
|
+
// Build a map of POS tags from ALL tree buttons, keyed by lowercase label.
|
|
759
|
+
// This ensures words on BFS-unreachable pages still contribute POS data.
|
|
760
|
+
const treePosMap = new Map();
|
|
761
|
+
const treePredictionsMap = new Map();
|
|
762
|
+
Object.values(tree.pages).forEach((page) => {
|
|
763
|
+
page.grid.forEach((row) => {
|
|
764
|
+
row.forEach((btn) => {
|
|
765
|
+
if (!btn || !btn.label)
|
|
766
|
+
return;
|
|
767
|
+
const lower = btn.label.toLowerCase();
|
|
768
|
+
if (btn.pos && btn.pos !== 'Unknown' && btn.pos !== 'Ignore') {
|
|
769
|
+
treePosMap.set(lower, btn.pos);
|
|
770
|
+
}
|
|
771
|
+
if (btn.predictions && btn.predictions.length > 0) {
|
|
772
|
+
const existing = treePredictionsMap.get(lower);
|
|
773
|
+
if (!existing || btn.predictions.length > existing.length) {
|
|
774
|
+
treePredictionsMap.set(lower, btn.predictions);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
});
|
|
778
|
+
});
|
|
779
|
+
});
|
|
780
|
+
// For metrics buttons that lack POS but have a tree counterpart with POS,
|
|
781
|
+
// propagate the POS tag so it's available in the output.
|
|
782
|
+
buttons.forEach((btn) => {
|
|
783
|
+
const lower = btn.label.toLowerCase();
|
|
784
|
+
if (!btn.pos || btn.pos === 'Unknown' || btn.pos === 'Ignore') {
|
|
785
|
+
const treePos = treePosMap.get(lower);
|
|
786
|
+
if (treePos)
|
|
787
|
+
btn.pos = treePos;
|
|
788
|
+
}
|
|
789
|
+
});
|
|
790
|
+
// Note: buttons on pages unreachable via BFS from the root page are
|
|
791
|
+
// intentionally excluded. If there is no navigation path to a page,
|
|
792
|
+
// those buttons are not accessible to the user and should not count
|
|
793
|
+
// as available vocabulary.
|
|
596
794
|
// Iterate through all pages to find buttons with predictions
|
|
597
795
|
Object.values(tree.pages).forEach((page) => {
|
|
598
796
|
page.grid.forEach((row) => {
|
|
599
797
|
row.forEach((btn) => {
|
|
600
798
|
if (!btn || !btn.predictions || btn.predictions.length === 0)
|
|
601
799
|
return;
|
|
602
|
-
// Find the parent button's metrics
|
|
603
|
-
|
|
800
|
+
// Find the parent button's metrics (by id first, then by label)
|
|
801
|
+
let parentMetrics = buttons.find((b) => b.id === btn.id);
|
|
802
|
+
if (!parentMetrics && btn.label) {
|
|
803
|
+
parentMetrics = existingLabels.get(btn.label.toLowerCase());
|
|
804
|
+
}
|
|
604
805
|
if (!parentMetrics)
|
|
605
806
|
return;
|
|
606
807
|
// Calculate effort for each word form
|
|
@@ -698,7 +899,11 @@ export class MetricsCalculator {
|
|
|
698
899
|
// If no block assigned, treat as its own block at the end (fallback)
|
|
699
900
|
if (blockId === null) {
|
|
700
901
|
const loop = board.grid.length + (board.grid[0]?.length || 0);
|
|
701
|
-
return {
|
|
902
|
+
return {
|
|
903
|
+
steps: rowIndex + colIndex + 1,
|
|
904
|
+
selections: 1,
|
|
905
|
+
loopSteps: loop,
|
|
906
|
+
};
|
|
702
907
|
}
|
|
703
908
|
const blockConfig = board.scanBlocksConfig?.find((c) => c.id === blockId);
|
|
704
909
|
const blockOrder = blockConfig?.order ?? blockId;
|