@willwade/aac-processors 0.2.3 → 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/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/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/package.json +12 -11
|
@@ -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
|
-
//
|
|
87
|
-
|
|
88
|
-
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
-
*
|
|
131
|
-
*
|
|
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
|
-
|
|
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
|
+
}
|