@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.
- 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 +269 -239
- 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 +269 -239
- 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
|
@@ -51,7 +51,7 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
51
51
|
return null;
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
|
-
catch (
|
|
54
|
+
catch (_err) {
|
|
55
55
|
continue;
|
|
56
56
|
}
|
|
57
57
|
}
|
|
@@ -97,7 +97,7 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
97
97
|
return dataUrl;
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
|
-
catch (
|
|
100
|
+
catch (_err) {
|
|
101
101
|
// Continue to next path
|
|
102
102
|
continue;
|
|
103
103
|
}
|
|
@@ -321,7 +321,7 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
321
321
|
return obj;
|
|
322
322
|
}
|
|
323
323
|
}
|
|
324
|
-
catch (
|
|
324
|
+
catch (_error) {
|
|
325
325
|
// Log parsing errors for debugging but don't throw
|
|
326
326
|
}
|
|
327
327
|
return null;
|
|
@@ -728,7 +728,7 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
728
728
|
// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-unsafe-return
|
|
729
729
|
return require('../validation/obfValidator').ObfValidator;
|
|
730
730
|
}
|
|
731
|
-
catch (
|
|
731
|
+
catch (_error) {
|
|
732
732
|
throw new Error('Validation utilities are not available in this environment.');
|
|
733
733
|
}
|
|
734
734
|
}
|
|
@@ -37,7 +37,7 @@ async function collectFiles(root, matcher, maxDepth = 3, fileAdapter = io_1.defa
|
|
|
37
37
|
try {
|
|
38
38
|
entries = await listDir(current.dir);
|
|
39
39
|
}
|
|
40
|
-
catch (
|
|
40
|
+
catch (_error) {
|
|
41
41
|
continue;
|
|
42
42
|
}
|
|
43
43
|
for (const entry of entries) {
|
|
@@ -143,7 +143,7 @@ async function openImage(dbOrFile, entryPath, fileAdapter = io_1.defaultFileAdap
|
|
|
143
143
|
const dir = dirname(dbPath);
|
|
144
144
|
await removePath(dir);
|
|
145
145
|
}
|
|
146
|
-
catch (
|
|
146
|
+
catch (_e) {
|
|
147
147
|
// Ignore cleanup errors
|
|
148
148
|
}
|
|
149
149
|
}
|
|
@@ -187,7 +187,7 @@ async function findSnapPackages(packageNamePattern = 'TobiiDynavox', fileAdapter
|
|
|
187
187
|
}
|
|
188
188
|
}
|
|
189
189
|
}
|
|
190
|
-
catch (
|
|
190
|
+
catch (_error) {
|
|
191
191
|
// Silently fail if directory access fails
|
|
192
192
|
}
|
|
193
193
|
return results;
|
|
@@ -218,7 +218,7 @@ class SnapProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
218
218
|
try {
|
|
219
219
|
positions = JSON.parse(sg.SerializedGridPositions);
|
|
220
220
|
}
|
|
221
|
-
catch (
|
|
221
|
+
catch (_e) {
|
|
222
222
|
// Invalid JSON, skip this group
|
|
223
223
|
return;
|
|
224
224
|
}
|
|
@@ -439,7 +439,7 @@ class SnapProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
439
439
|
}
|
|
440
440
|
}
|
|
441
441
|
}
|
|
442
|
-
catch (
|
|
442
|
+
catch (_e) {
|
|
443
443
|
// Ignore JSON parse errors in commands
|
|
444
444
|
}
|
|
445
445
|
}
|
|
@@ -104,7 +104,7 @@ class TouchChatProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
104
104
|
idMappings.set(mapping.numeric_id, mapping.string_id);
|
|
105
105
|
});
|
|
106
106
|
}
|
|
107
|
-
catch (
|
|
107
|
+
catch (_e) {
|
|
108
108
|
// No mapping table, use numeric IDs as strings
|
|
109
109
|
}
|
|
110
110
|
// Load styles
|
|
@@ -122,7 +122,7 @@ class TouchChatProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
122
122
|
pageStyles.set(style.id, style);
|
|
123
123
|
});
|
|
124
124
|
}
|
|
125
|
-
catch (
|
|
125
|
+
catch (_e) {
|
|
126
126
|
// console.log('No styles found:', e);
|
|
127
127
|
}
|
|
128
128
|
// First, load all pages and get their names from resources
|
|
@@ -310,7 +310,7 @@ class TouchChatProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
310
310
|
}
|
|
311
311
|
});
|
|
312
312
|
}
|
|
313
|
-
catch (
|
|
313
|
+
catch (_e) {
|
|
314
314
|
// console.log('No button box cells found:', e);
|
|
315
315
|
}
|
|
316
316
|
// Load buttons directly linked to pages via resources
|
|
@@ -369,7 +369,7 @@ class TouchChatProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
369
369
|
page.addButton(button);
|
|
370
370
|
});
|
|
371
371
|
}
|
|
372
|
-
catch (
|
|
372
|
+
catch (_e) {
|
|
373
373
|
// console.log('No direct page buttons found:', e);
|
|
374
374
|
}
|
|
375
375
|
// Load navigation actions
|
|
@@ -416,7 +416,7 @@ class TouchChatProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
416
416
|
}
|
|
417
417
|
});
|
|
418
418
|
}
|
|
419
|
-
catch (
|
|
419
|
+
catch (_e) {
|
|
420
420
|
// console.log('No navigation actions found:', e);
|
|
421
421
|
}
|
|
422
422
|
// Try to load root ID from multiple sources in order of priority
|
|
@@ -457,7 +457,7 @@ class TouchChatProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
457
457
|
tree.metadata.defaultHomePageId = rootPageId;
|
|
458
458
|
}
|
|
459
459
|
}
|
|
460
|
-
catch (
|
|
460
|
+
catch (_e) {
|
|
461
461
|
// No metadata table or other error, use first page as root
|
|
462
462
|
if (rootPageId) {
|
|
463
463
|
tree.rootId = rootPageId;
|
|
@@ -39,6 +39,20 @@ export declare class MetricsCalculator {
|
|
|
39
39
|
* Calculate what percentage of links to this board match semantic_id/clone_id
|
|
40
40
|
*/
|
|
41
41
|
private calculateBoardLinkPercentages;
|
|
42
|
+
/**
|
|
43
|
+
* Quick check whether any button in the tree has a POS tag.
|
|
44
|
+
* Used to auto-enable smart grammar without requiring explicit opt-in.
|
|
45
|
+
*/
|
|
46
|
+
private treeHasPosTags;
|
|
47
|
+
/**
|
|
48
|
+
* Expand morphological predictions from POS tags on buttons
|
|
49
|
+
*
|
|
50
|
+
* For each button that has a POS tag (e.g., 'Verb', 'Noun'), use the
|
|
51
|
+
* MorphologyEngine to generate inflected word forms and populate the
|
|
52
|
+
* button's predictions array. This is done as a pre-processing step
|
|
53
|
+
* before calculateWordFormMetrics assigns effort to each form.
|
|
54
|
+
*/
|
|
55
|
+
private expandMorphologicalPredictions;
|
|
42
56
|
/**
|
|
43
57
|
* Calculate metrics for word forms (smart grammar predictions)
|
|
44
58
|
*
|
|
@@ -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
|
+
}
|