@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.
Files changed (50) hide show
  1. package/README.md +3 -1
  2. package/dist/browser/core/treeStructure.js +3 -1
  3. package/dist/browser/metrics.js +2 -0
  4. package/dist/browser/processors/astericsGridProcessor.js +21 -12
  5. package/dist/browser/processors/gridset/helpers.js +3 -3
  6. package/dist/browser/processors/gridsetProcessor.js +16 -8
  7. package/dist/browser/processors/obfProcessor.js +4 -4
  8. package/dist/browser/processors/snap/helpers.js +3 -3
  9. package/dist/browser/processors/snapProcessor.js +2 -2
  10. package/dist/browser/processors/touchchatProcessor.js +6 -6
  11. package/dist/browser/utilities/analytics/metrics/core.js +213 -8
  12. package/dist/browser/utilities/analytics/metrics/vocabulary.js +13 -1
  13. package/dist/browser/utilities/analytics/morphology/engine.js +910 -0
  14. package/dist/browser/utilities/analytics/morphology/grid3VerbsParser.js +455 -0
  15. package/dist/browser/utilities/analytics/morphology/index.js +3 -0
  16. package/dist/browser/utilities/analytics/morphology/types.js +1 -0
  17. package/dist/browser/utilities/analytics/morphology/wordFormGenerator.js +74 -0
  18. package/dist/browser/utils/sqlite.js +10 -6
  19. package/dist/core/treeStructure.d.ts +17 -1
  20. package/dist/core/treeStructure.js +3 -1
  21. package/dist/index.node.d.ts +2 -0
  22. package/dist/index.node.js +6 -1
  23. package/dist/metrics.d.ts +3 -0
  24. package/dist/metrics.js +5 -1
  25. package/dist/processors/astericsGridProcessor.js +21 -12
  26. package/dist/processors/excelProcessor.js +5 -1
  27. package/dist/processors/gridset/helpers.js +3 -3
  28. package/dist/processors/gridset/imageDebug.js +2 -2
  29. package/dist/processors/gridsetProcessor.js +16 -8
  30. package/dist/processors/obfProcessor.js +4 -4
  31. package/dist/processors/snap/helpers.js +3 -3
  32. package/dist/processors/snapProcessor.js +2 -2
  33. package/dist/processors/touchchatProcessor.js +6 -6
  34. package/dist/utilities/analytics/metrics/core.d.ts +14 -0
  35. package/dist/utilities/analytics/metrics/core.js +213 -8
  36. package/dist/utilities/analytics/metrics/types.d.ts +18 -3
  37. package/dist/utilities/analytics/metrics/vocabulary.d.ts +3 -0
  38. package/dist/utilities/analytics/metrics/vocabulary.js +13 -1
  39. package/dist/utilities/analytics/morphology/engine.d.ts +30 -0
  40. package/dist/utilities/analytics/morphology/engine.js +914 -0
  41. package/dist/utilities/analytics/morphology/grid3VerbsParser.d.ts +36 -0
  42. package/dist/utilities/analytics/morphology/grid3VerbsParser.js +485 -0
  43. package/dist/utilities/analytics/morphology/index.d.ts +5 -0
  44. package/dist/utilities/analytics/morphology/index.js +9 -0
  45. package/dist/utilities/analytics/morphology/types.d.ts +40 -0
  46. package/dist/utilities/analytics/morphology/types.js +2 -0
  47. package/dist/utilities/analytics/morphology/wordFormGenerator.d.ts +10 -0
  48. package/dist/utilities/analytics/morphology/wordFormGenerator.js +78 -0
  49. package/dist/utils/sqlite.js +10 -29
  50. package/package.json +13 -12
@@ -12,6 +12,7 @@ exports.MetricsCalculator = void 0;
12
12
  const treeStructure_1 = require("../../../core/treeStructure");
13
13
  const aac_1 = require("../../../types/aac");
14
14
  const effort_1 = require("./effort");
15
+ const morphology_1 = require("../morphology");
15
16
  class MetricsCalculator {
16
17
  constructor() {
17
18
  this.locale = 'en';
@@ -83,9 +84,11 @@ class MetricsCalculator {
83
84
  });
84
85
  // Update buttons using dynamic spelling effort if applicable
85
86
  const buttons = Array.from(knownButtons.values()).sort((a, b) => a.effort - b.effort);
86
- // Calculate metrics for word forms (smart grammar predictions) if enabled
87
- // Default to true if not specified
88
- const useSmartGrammar = options.useSmartGrammar !== false;
87
+ // Expand morphological predictions from POS tags if enabled or auto-detected
88
+ const useSmartGrammar = options.useSmartGrammar === true || this.treeHasPosTags(tree);
89
+ if (useSmartGrammar) {
90
+ this.expandMorphologicalPredictions(tree, options);
91
+ }
89
92
  if (useSmartGrammar) {
90
93
  const { wordFormMetrics, replacedLabels } = this.calculateWordFormMetrics(tree, buttons, options);
91
94
  // Remove buttons that were replaced by lower-effort word forms
@@ -154,13 +157,21 @@ class MetricsCalculator {
154
157
  }) || null;
155
158
  }
156
159
  if (!spellingPage)
157
- return { spellingPage: null, spellingBaseEffort: 10, spellingAvgLetterEffort: 2.5 };
160
+ return {
161
+ spellingPage: null,
162
+ spellingBaseEffort: 10,
163
+ spellingAvgLetterEffort: 2.5,
164
+ };
158
165
  // Calculate effort to reach this page from root
159
166
  const rootBoard = tree.rootId
160
167
  ? tree.pages[tree.rootId]
161
168
  : Object.values(tree.pages).find((p) => !p.parentId);
162
169
  if (!rootBoard)
163
- return { spellingPage, spellingBaseEffort: 10, spellingAvgLetterEffort: 2.5 };
170
+ return {
171
+ spellingPage,
172
+ spellingBaseEffort: 10,
173
+ spellingAvgLetterEffort: 2.5,
174
+ };
164
175
  // Analyze specifically to find the lowest effort path to the spelling page
165
176
  const result = this.analyzeFrom(tree, rootBoard, setPcts, true, options);
166
177
  const spellingBaseEffort = result.visitedBoardEfforts.get(spellingPage.id) ?? 10;
@@ -574,6 +585,157 @@ class MetricsCalculator {
574
585
  boardPcts['all'] = totalLinks;
575
586
  return boardPcts;
576
587
  }
588
+ /**
589
+ * Quick check whether any button in the tree has a POS tag.
590
+ * Used to auto-enable smart grammar without requiring explicit opt-in.
591
+ */
592
+ treeHasPosTags(tree) {
593
+ for (const page of Object.values(tree.pages)) {
594
+ for (const row of page.grid) {
595
+ for (const btn of row) {
596
+ if (btn?.pos && btn.pos !== 'Unknown' && btn.pos !== 'Ignore') {
597
+ return true;
598
+ }
599
+ }
600
+ }
601
+ }
602
+ return false;
603
+ }
604
+ /**
605
+ * Expand morphological predictions from POS tags on buttons
606
+ *
607
+ * For each button that has a POS tag (e.g., 'Verb', 'Noun'), use the
608
+ * MorphologyEngine to generate inflected word forms and populate the
609
+ * button's predictions array. This is done as a pre-processing step
610
+ * before calculateWordFormMetrics assigns effort to each form.
611
+ */
612
+ expandMorphologicalPredictions(tree, options) {
613
+ const locale = options.morphologyLocale || 'en-gb';
614
+ const morph = new morphology_1.MorphologyEngine(locale);
615
+ // Words that should never be POS-inferred (function words, determiners, etc.)
616
+ const skipInference = new Set([
617
+ 'a',
618
+ 'an',
619
+ 'the',
620
+ 'to',
621
+ 'in',
622
+ 'on',
623
+ 'at',
624
+ 'of',
625
+ 'for',
626
+ 'and',
627
+ 'or',
628
+ 'but',
629
+ 'not',
630
+ 'no',
631
+ 'yes',
632
+ 'is',
633
+ 'am',
634
+ 'are',
635
+ 'was',
636
+ 'were',
637
+ 'be',
638
+ 'been',
639
+ 'being',
640
+ 'has',
641
+ 'have',
642
+ 'had',
643
+ 'do',
644
+ 'does',
645
+ 'did',
646
+ 'will',
647
+ 'would',
648
+ 'could',
649
+ 'should',
650
+ 'shall',
651
+ 'may',
652
+ 'might',
653
+ 'can',
654
+ 'must',
655
+ 'with',
656
+ 'from',
657
+ 'by',
658
+ 'up',
659
+ 'down',
660
+ 'out',
661
+ 'off',
662
+ 'over',
663
+ 'under',
664
+ 'again',
665
+ 'then',
666
+ 'than',
667
+ 'so',
668
+ 'if',
669
+ 'when',
670
+ 'where',
671
+ 'how',
672
+ 'what',
673
+ 'who',
674
+ 'which',
675
+ 'that',
676
+ 'this',
677
+ 'these',
678
+ 'those',
679
+ 'here',
680
+ 'there',
681
+ 'now',
682
+ 'very',
683
+ 'just',
684
+ 'more',
685
+ 'also',
686
+ 'too',
687
+ 'please',
688
+ 'thank',
689
+ 'hi',
690
+ 'hello',
691
+ 'bye',
692
+ 'goodbye',
693
+ 'okay',
694
+ 'oh',
695
+ 'wow',
696
+ 'sorry',
697
+ ]);
698
+ for (const page of Object.values(tree.pages)) {
699
+ for (const row of page.grid) {
700
+ for (const btn of row) {
701
+ if (!btn || !btn.label)
702
+ continue;
703
+ let pos = btn.pos;
704
+ // If no POS tag (or Unknown/Ignore), attempt POS inference.
705
+ // Many content words on topic pages lack POS tags even though
706
+ // they are clearly nouns (e.g., "bird", "tree", "cloud").
707
+ // Strategy: check irregular tables first for confident POS,
708
+ // then fall back to Noun for single-word content labels.
709
+ if (!pos || pos === 'Unknown' || pos === 'Ignore') {
710
+ const lower = btn.label.toLowerCase();
711
+ // Skip function words and multi-word labels
712
+ if (!skipInference.has(lower) && !lower.includes(' ') && lower.length > 1) {
713
+ // Check irregular tables for confident POS assignment
714
+ const inferredPOS = morph.inferPOS(lower);
715
+ if (inferredPOS) {
716
+ pos = inferredPOS;
717
+ btn.pos = inferredPOS;
718
+ }
719
+ else {
720
+ // Default to Noun for untagged content words.
721
+ // This generates plurals (e.g., bird → birds, tree → trees).
722
+ pos = 'Noun';
723
+ btn.pos = 'Noun';
724
+ }
725
+ }
726
+ }
727
+ if (!pos || pos === 'Unknown' || pos === 'Ignore')
728
+ continue;
729
+ const forms = morph.inflect(btn.label, pos);
730
+ if (forms.length > 0) {
731
+ const existing = btn.predictions || [];
732
+ const merged = new Set([...existing, ...forms]);
733
+ btn.predictions = Array.from(merged);
734
+ }
735
+ }
736
+ }
737
+ }
738
+ }
577
739
  /**
578
740
  * Calculate metrics for word forms (smart grammar predictions)
579
741
  *
@@ -596,14 +758,53 @@ class MetricsCalculator {
596
758
  // Track buttons by label to compare efforts
597
759
  const existingLabels = new Map();
598
760
  buttons.forEach((btn) => existingLabels.set(btn.label.toLowerCase(), btn));
761
+ // Build a map of POS tags from ALL tree buttons, keyed by lowercase label.
762
+ // This ensures words on BFS-unreachable pages still contribute POS data.
763
+ const treePosMap = new Map();
764
+ const treePredictionsMap = new Map();
765
+ Object.values(tree.pages).forEach((page) => {
766
+ page.grid.forEach((row) => {
767
+ row.forEach((btn) => {
768
+ if (!btn || !btn.label)
769
+ return;
770
+ const lower = btn.label.toLowerCase();
771
+ if (btn.pos && btn.pos !== 'Unknown' && btn.pos !== 'Ignore') {
772
+ treePosMap.set(lower, btn.pos);
773
+ }
774
+ if (btn.predictions && btn.predictions.length > 0) {
775
+ const existing = treePredictionsMap.get(lower);
776
+ if (!existing || btn.predictions.length > existing.length) {
777
+ treePredictionsMap.set(lower, btn.predictions);
778
+ }
779
+ }
780
+ });
781
+ });
782
+ });
783
+ // For metrics buttons that lack POS but have a tree counterpart with POS,
784
+ // propagate the POS tag so it's available in the output.
785
+ buttons.forEach((btn) => {
786
+ const lower = btn.label.toLowerCase();
787
+ if (!btn.pos || btn.pos === 'Unknown' || btn.pos === 'Ignore') {
788
+ const treePos = treePosMap.get(lower);
789
+ if (treePos)
790
+ btn.pos = treePos;
791
+ }
792
+ });
793
+ // Note: buttons on pages unreachable via BFS from the root page are
794
+ // intentionally excluded. If there is no navigation path to a page,
795
+ // those buttons are not accessible to the user and should not count
796
+ // as available vocabulary.
599
797
  // Iterate through all pages to find buttons with predictions
600
798
  Object.values(tree.pages).forEach((page) => {
601
799
  page.grid.forEach((row) => {
602
800
  row.forEach((btn) => {
603
801
  if (!btn || !btn.predictions || btn.predictions.length === 0)
604
802
  return;
605
- // Find the parent button's metrics
606
- const parentMetrics = buttons.find((b) => b.id === btn.id);
803
+ // Find the parent button's metrics (by id first, then by label)
804
+ let parentMetrics = buttons.find((b) => b.id === btn.id);
805
+ if (!parentMetrics && btn.label) {
806
+ parentMetrics = existingLabels.get(btn.label.toLowerCase());
807
+ }
607
808
  if (!parentMetrics)
608
809
  return;
609
810
  // Calculate effort for each word form
@@ -701,7 +902,11 @@ class MetricsCalculator {
701
902
  // If no block assigned, treat as its own block at the end (fallback)
702
903
  if (blockId === null) {
703
904
  const loop = board.grid.length + (board.grid[0]?.length || 0);
704
- return { steps: rowIndex + colIndex + 1, selections: 1, loopSteps: loop };
905
+ return {
906
+ steps: rowIndex + colIndex + 1,
907
+ selections: 1,
908
+ loopSteps: loop,
909
+ };
705
910
  }
706
911
  const blockConfig = board.scanBlocksConfig?.find((c) => c.id === blockId);
707
912
  const blockOrder = blockConfig?.order ?? blockId;
@@ -21,6 +21,7 @@ export interface ButtonMetrics {
21
21
  is_word_form?: boolean;
22
22
  parent_button_id?: string;
23
23
  parent_button_label?: string;
24
+ pos?: string;
24
25
  }
25
26
  /**
26
27
  * Board/page level analysis result
@@ -120,17 +121,31 @@ export interface MetricsOptions {
120
121
  /**
121
122
  * Whether to include smart grammar word forms in metrics
122
123
  *
123
- * When true (default): Word forms from smart grammar predictions are included
124
+ * When true: Word forms from smart grammar predictions are included
124
125
  * in the metrics. If a word exists as both a regular button and a word form,
125
126
  * the version with lower effort is used.
126
127
  *
127
128
  * When false: Smart grammar word forms are excluded from metrics. Only actual
128
129
  * buttons in the tree are analyzed.
129
130
  *
130
- * Only applicable to systems that support smart grammar (e.g., Grid 3).
131
- * Default is true.
131
+ * Auto-detected by default: if any button in the tree has a POS tag (e.g.,
132
+ * from Grid 3's Action.InsertText), smart grammar is enabled automatically.
133
+ * For non-Grid-3 formats (TD Snap, TouchChat, OBF), no buttons have POS tags,
134
+ * so smart grammar is automatically disabled with zero overhead.
135
+ *
136
+ * Set explicitly to `true` to force-enable, or `false` to force-disable.
132
137
  */
133
138
  useSmartGrammar?: boolean;
139
+ /**
140
+ * Locale for morphological inflection rules
141
+ *
142
+ * When provided, the MorphologyEngine will generate inflected word forms
143
+ * (e.g., "going", "went" for "go") based on the POS tags extracted from
144
+ * the gridset. Defaults to 'en-gb'.
145
+ *
146
+ * Only used when useSmartGrammar is true.
147
+ */
148
+ morphologyLocale?: string;
134
149
  }
135
150
  /**
136
151
  * Comparison result between two board sets
@@ -60,6 +60,9 @@ export declare class VocabularyAnalyzer {
60
60
  getWordEffort(word: string, metrics: MetricsResult): number;
61
61
  /**
62
62
  * Check if a word is in the board set
63
+ *
64
+ * Checks both direct button labels and smart grammar word forms
65
+ * (morphological inflections stored as predictions).
63
66
  */
64
67
  hasWord(word: string, metrics: MetricsResult): boolean;
65
68
  }
@@ -134,9 +134,21 @@ class VocabularyAnalyzer {
134
134
  }
135
135
  /**
136
136
  * Check if a word is in the board set
137
+ *
138
+ * Checks both direct button labels and smart grammar word forms
139
+ * (morphological inflections stored as predictions).
137
140
  */
138
141
  hasWord(word, metrics) {
139
- return metrics.buttons.some((b) => b.label.toLowerCase() === word.toLowerCase());
142
+ const lower = word.toLowerCase();
143
+ return metrics.buttons.some((b) => {
144
+ if (b.label.toLowerCase() === lower)
145
+ return true;
146
+ if (b.is_word_form && b.parent_button_label) {
147
+ if (b.label.toLowerCase() === lower)
148
+ return true;
149
+ }
150
+ return false;
151
+ });
140
152
  }
141
153
  }
142
154
  exports.VocabularyAnalyzer = VocabularyAnalyzer;
@@ -0,0 +1,30 @@
1
+ import { MorphRuleSet } from './types';
2
+ import type { Grid3VerbForms } from './grid3VerbsParser';
3
+ export declare class MorphologyEngine {
4
+ private ruleSet;
5
+ private grid3Verbs?;
6
+ private cache;
7
+ constructor(ruleSetOrLocale: string | MorphRuleSet);
8
+ static fromGrid3Verbs(verbForms: Grid3VerbForms): MorphologyEngine;
9
+ get locale(): string;
10
+ inflect(base: string, pos: string): string[];
11
+ isFormOf(word: string, base: string, pos: string): boolean;
12
+ expandVocabulary(buttons: Array<{
13
+ label: string;
14
+ pos?: string;
15
+ predictions?: string[];
16
+ }>): Map<string, string[]>;
17
+ inflectWithSlots(base: string, pos: string): Array<{
18
+ slot: string;
19
+ form: string;
20
+ }>;
21
+ private computeForms;
22
+ private applyRules;
23
+ /**
24
+ * Infer the most likely POS for a word by checking the irregular tables.
25
+ * Returns the POS if found in any irregular table, or null if not found.
26
+ * Priority: Verb > Noun > Adjective > Pronoun
27
+ */
28
+ inferPOS(word: string): string | null;
29
+ private loadBundled;
30
+ }