@willwade/aac-processors 0.2.3 → 0.2.5

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.
Files changed (47) hide show
  1. package/dist/browser/core/treeStructure.js +3 -1
  2. package/dist/browser/metrics.js +2 -0
  3. package/dist/browser/processors/astericsGridProcessor.js +21 -12
  4. package/dist/browser/processors/gridset/helpers.js +3 -3
  5. package/dist/browser/processors/gridsetProcessor.js +269 -239
  6. package/dist/browser/processors/obfProcessor.js +4 -4
  7. package/dist/browser/processors/snap/helpers.js +3 -3
  8. package/dist/browser/processors/snapProcessor.js +2 -2
  9. package/dist/browser/processors/touchchatProcessor.js +6 -6
  10. package/dist/browser/utilities/analytics/metrics/core.js +213 -8
  11. package/dist/browser/utilities/analytics/metrics/vocabulary.js +13 -1
  12. package/dist/browser/utilities/analytics/morphology/engine.js +910 -0
  13. package/dist/browser/utilities/analytics/morphology/grid3VerbsParser.js +455 -0
  14. package/dist/browser/utilities/analytics/morphology/index.js +3 -0
  15. package/dist/browser/utilities/analytics/morphology/types.js +1 -0
  16. package/dist/browser/utilities/analytics/morphology/wordFormGenerator.js +74 -0
  17. package/dist/core/treeStructure.d.ts +17 -1
  18. package/dist/core/treeStructure.js +3 -1
  19. package/dist/index.node.d.ts +2 -0
  20. package/dist/index.node.js +6 -1
  21. package/dist/metrics.d.ts +3 -0
  22. package/dist/metrics.js +5 -1
  23. package/dist/processors/astericsGridProcessor.js +21 -12
  24. package/dist/processors/excelProcessor.js +5 -1
  25. package/dist/processors/gridset/helpers.js +3 -3
  26. package/dist/processors/gridset/imageDebug.js +2 -2
  27. package/dist/processors/gridsetProcessor.js +269 -239
  28. package/dist/processors/obfProcessor.js +4 -4
  29. package/dist/processors/snap/helpers.js +3 -3
  30. package/dist/processors/snapProcessor.js +2 -2
  31. package/dist/processors/touchchatProcessor.js +6 -6
  32. package/dist/utilities/analytics/metrics/core.d.ts +14 -0
  33. package/dist/utilities/analytics/metrics/core.js +213 -8
  34. package/dist/utilities/analytics/metrics/types.d.ts +18 -3
  35. package/dist/utilities/analytics/metrics/vocabulary.d.ts +3 -0
  36. package/dist/utilities/analytics/metrics/vocabulary.js +13 -1
  37. package/dist/utilities/analytics/morphology/engine.d.ts +30 -0
  38. package/dist/utilities/analytics/morphology/engine.js +914 -0
  39. package/dist/utilities/analytics/morphology/grid3VerbsParser.d.ts +36 -0
  40. package/dist/utilities/analytics/morphology/grid3VerbsParser.js +485 -0
  41. package/dist/utilities/analytics/morphology/index.d.ts +5 -0
  42. package/dist/utilities/analytics/morphology/index.js +9 -0
  43. package/dist/utilities/analytics/morphology/types.d.ts +40 -0
  44. package/dist/utilities/analytics/morphology/types.js +2 -0
  45. package/dist/utilities/analytics/morphology/wordFormGenerator.d.ts +10 -0
  46. package/dist/utilities/analytics/morphology/wordFormGenerator.js +78 -0
  47. package/package.json +12 -11
@@ -48,7 +48,7 @@ class ObfProcessor extends BaseProcessor {
48
48
  return null;
49
49
  }
50
50
  }
51
- catch (err) {
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 (err) {
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 (error) {
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 (error) {
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 (error) {
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 (e) {
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 (error) {
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 (e) {
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 (e) {
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 (e) {
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 (e) {
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 (e) {
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 (e) {
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 (e) {
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 (e) {
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
- // Calculate metrics for word forms (smart grammar predictions) if enabled
84
- // Default to true if not specified
85
- const useSmartGrammar = options.useSmartGrammar !== false;
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 { spellingPage: null, spellingBaseEffort: 10, spellingAvgLetterEffort: 2.5 };
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 { spellingPage, spellingBaseEffort: 10, spellingAvgLetterEffort: 2.5 };
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
- const parentMetrics = buttons.find((b) => b.id === btn.id);
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 { steps: rowIndex + colIndex + 1, selections: 1, loopSteps: loop };
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;
@@ -131,8 +131,20 @@ export class VocabularyAnalyzer {
131
131
  }
132
132
  /**
133
133
  * Check if a word is in the board set
134
+ *
135
+ * Checks both direct button labels and smart grammar word forms
136
+ * (morphological inflections stored as predictions).
134
137
  */
135
138
  hasWord(word, metrics) {
136
- return metrics.buttons.some((b) => b.label.toLowerCase() === word.toLowerCase());
139
+ const lower = word.toLowerCase();
140
+ return metrics.buttons.some((b) => {
141
+ if (b.label.toLowerCase() === lower)
142
+ return true;
143
+ if (b.is_word_form && b.parent_button_label) {
144
+ if (b.label.toLowerCase() === lower)
145
+ return true;
146
+ }
147
+ return false;
148
+ });
137
149
  }
138
150
  }