@willwade/aac-processors 0.0.15 → 0.0.17

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.
@@ -29,6 +29,8 @@ export declare const EFFORT_CONSTANTS: {
29
29
  readonly REUSED_CLONE_FROM_OTHER_BONUS: 0.005;
30
30
  readonly SCAN_STEP_COST: 0.015;
31
31
  readonly SCAN_SELECTION_COST: 0.1;
32
+ readonly DEFAULT_SCAN_ERROR_RATE: 0.1;
33
+ readonly SCAN_RETRY_PENALTY: 1;
32
34
  };
33
35
  /**
34
36
  * Calculate button size effort based on grid dimensions
@@ -68,12 +70,28 @@ export declare function visualScanEffort(priorButtons: number): number;
68
70
  export declare function distanceEffort(x: number, y: number, entryX?: number, entryY?: number): number;
69
71
  /**
70
72
  * Calculate spelling effort for words not available in the board set
71
- * Base cost + per-letter cost
72
73
  *
73
74
  * @param word - The word to spell
75
+ * @param entryEffort - Effort to reach the spelling/keyboard page
76
+ * @param perLetterEffort - Average effort per letter on the keyboard
74
77
  * @returns Spelling effort score
75
78
  */
76
- export declare function spellingEffort(word: string): number;
79
+ export declare function spellingEffort(word: string, entryEffort?: number, perLetterEffort?: number): number;
80
+ /**
81
+ * Calculate effort to access a word via prediction
82
+ *
83
+ * When prediction is available, the user:
84
+ * 1. Navigates to the spelling/keyboard page (entryEffort)
85
+ * 2. Types first 1-3 letters to trigger predictions
86
+ * 3. Selects from 1-3 predictions (average selections)
87
+ *
88
+ * @param entryEffort - Effort to reach the spelling/keyboard page
89
+ * @param perLetterEffort - Average effort per letter on the keyboard
90
+ * @param avgSelections - Average number of predictions to check (default 1.5)
91
+ * @param lettersToType - Letters to type before prediction appears (default 2)
92
+ * @returns Prediction effort score
93
+ */
94
+ export declare function predictionEffort(entryEffort?: number, perLetterEffort?: number, avgSelections?: number, lettersToType?: number): number;
77
95
  /**
78
96
  * Calculate base board effort
79
97
  * Combines button size and field size efforts
@@ -142,6 +160,8 @@ export declare function localScanEffort(distance: number): number;
142
160
  *
143
161
  * @param steps - Number of scan steps to reach target
144
162
  * @param selections - Number of switch selections required
163
+ * @param stepCost - Optional override for scan step cost
164
+ * @param selectionCost - Optional override for scan selection cost
145
165
  * @returns Scanning effort score
146
166
  */
147
- export declare function scanningEffort(steps: number, selections: number): number;
167
+ export declare function scanningEffort(steps: number, selections: number, stepCost?: number, selectionCost?: number): number;
@@ -13,6 +13,7 @@ exports.fieldSizeEffort = fieldSizeEffort;
13
13
  exports.visualScanEffort = visualScanEffort;
14
14
  exports.distanceEffort = distanceEffort;
15
15
  exports.spellingEffort = spellingEffort;
16
+ exports.predictionEffort = predictionEffort;
16
17
  exports.baseBoardEffort = baseBoardEffort;
17
18
  exports.applyReuseDiscount = applyReuseDiscount;
18
19
  exports.calculateButtonEffort = calculateButtonEffort;
@@ -44,6 +45,8 @@ exports.EFFORT_CONSTANTS = {
44
45
  REUSED_CLONE_FROM_OTHER_BONUS: 0.005,
45
46
  SCAN_STEP_COST: 0.015, // Matches visual scan multiplier
46
47
  SCAN_SELECTION_COST: 0.1, // Cost of a switch selection
48
+ DEFAULT_SCAN_ERROR_RATE: 0.1, // 10% chance of missing a selection
49
+ SCAN_RETRY_PENALTY: 1.0, // Cost multiplier for a full loop retry
47
50
  };
48
51
  /**
49
52
  * Calculate button size effort based on grid dimensions
@@ -92,13 +95,34 @@ function distanceEffort(x, y, entryX = 1.0, entryY = 1.0) {
92
95
  }
93
96
  /**
94
97
  * Calculate spelling effort for words not available in the board set
95
- * Base cost + per-letter cost
96
98
  *
97
99
  * @param word - The word to spell
100
+ * @param entryEffort - Effort to reach the spelling/keyboard page
101
+ * @param perLetterEffort - Average effort per letter on the keyboard
98
102
  * @returns Spelling effort score
99
103
  */
100
- function spellingEffort(word) {
101
- return 10 + word.length * 2.5;
104
+ function spellingEffort(word, entryEffort = 10, perLetterEffort = 2.5) {
105
+ return entryEffort + word.length * perLetterEffort;
106
+ }
107
+ /**
108
+ * Calculate effort to access a word via prediction
109
+ *
110
+ * When prediction is available, the user:
111
+ * 1. Navigates to the spelling/keyboard page (entryEffort)
112
+ * 2. Types first 1-3 letters to trigger predictions
113
+ * 3. Selects from 1-3 predictions (average selections)
114
+ *
115
+ * @param entryEffort - Effort to reach the spelling/keyboard page
116
+ * @param perLetterEffort - Average effort per letter on the keyboard
117
+ * @param avgSelections - Average number of predictions to check (default 1.5)
118
+ * @param lettersToType - Letters to type before prediction appears (default 2)
119
+ * @returns Prediction effort score
120
+ */
121
+ function predictionEffort(entryEffort = 10, perLetterEffort = 2.5, avgSelections = 1.5, lettersToType = 2) {
122
+ // Cost to navigate to keyboard + type first few letters + select from predictions
123
+ const typingCost = lettersToType * perLetterEffort;
124
+ const selectionCost = avgSelections * exports.EFFORT_CONSTANTS.SCAN_SELECTION_COST;
125
+ return entryEffort + typingCost + selectionCost;
102
126
  }
103
127
  /**
104
128
  * Calculate base board effort
@@ -204,8 +228,10 @@ function localScanEffort(distance) {
204
228
  *
205
229
  * @param steps - Number of scan steps to reach target
206
230
  * @param selections - Number of switch selections required
231
+ * @param stepCost - Optional override for scan step cost
232
+ * @param selectionCost - Optional override for scan selection cost
207
233
  * @returns Scanning effort score
208
234
  */
209
- function scanningEffort(steps, selections) {
210
- return (steps * exports.EFFORT_CONSTANTS.SCAN_STEP_COST + selections * exports.EFFORT_CONSTANTS.SCAN_SELECTION_COST);
235
+ function scanningEffort(steps, selections, stepCost = exports.EFFORT_CONSTANTS.SCAN_STEP_COST, selectionCost = exports.EFFORT_CONSTANTS.SCAN_SELECTION_COST) {
236
+ return steps * stepCost + selections * selectionCost;
211
237
  }
@@ -40,10 +40,23 @@ class SentenceAnalyzer {
40
40
  totalEffort += found.effort;
41
41
  }
42
42
  else {
43
- // Word not found - use spelling effort
44
- const spellEffort = (0, effort_1.spellingEffort)(word);
45
- wordEfforts.push({ word, effort: spellEffort, typed: true });
46
- totalEffort += spellEffort;
43
+ // Word not found - check for dynamic prediction fallback
44
+ let wordEffort = 0;
45
+ const isTyped = true;
46
+ const baseSpell = (0, effort_1.spellingEffort)(word, metrics.spelling_effort_base || 10, metrics.spelling_effort_per_letter || 2.5);
47
+ if (metrics.has_dynamic_prediction) {
48
+ // Predictive fallback: Base + (limited letters) + selection
49
+ // We assume on average typing 40% of the word finds it in the dictionary
50
+ const predictiveEffort = (metrics.spelling_effort_base || 10) +
51
+ word.length * 0.4 * (metrics.spelling_effort_per_letter || 2.5) +
52
+ 6.0; // Fixed selection cost from prediction bar
53
+ wordEffort = Math.min(baseSpell, predictiveEffort);
54
+ }
55
+ else {
56
+ wordEffort = baseSpell;
57
+ }
58
+ wordEfforts.push({ word, effort: wordEffort, typed: isTyped });
59
+ totalEffort += wordEffort;
47
60
  typing = true;
48
61
  missingWords.push(word);
49
62
  }
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * Defines the data structures used for AAC metrics analysis
5
5
  */
6
+ import { ScanningConfig } from '../../../types/aac';
6
7
  /**
7
8
  * Button-level metrics result
8
9
  */
@@ -52,6 +53,11 @@ export interface MetricsResult {
52
53
  alternates?: {
53
54
  [boardId: string]: AlternateBoardMetrics;
54
55
  };
56
+ spelling_effort_base?: number;
57
+ spelling_effort_per_letter?: number;
58
+ spelling_page_id?: string;
59
+ has_dynamic_prediction?: boolean;
60
+ prediction_page_id?: string;
55
61
  obfset?: any;
56
62
  }
57
63
  /**
@@ -63,6 +69,52 @@ export interface AlternateBoardMetrics {
63
69
  [level: number]: ButtonMetrics[];
64
70
  };
65
71
  }
72
+ /**
73
+ * Options for metrics calculation
74
+ */
75
+ export interface MetricsOptions {
76
+ /**
77
+ * Override scanning configuration
78
+ */
79
+ scanningConfig?: ScanningConfig;
80
+ /**
81
+ * Path to core vocabulary lists to use for analysis
82
+ */
83
+ coreLists?: string[];
84
+ /**
85
+ * Test sentences for sentence-level effort analysis
86
+ */
87
+ testSentences?: string[];
88
+ /**
89
+ * Custom scanning costs
90
+ */
91
+ scanStepCost?: number;
92
+ scanSelectionCost?: number;
93
+ /**
94
+ * Optional explicit ID of the spelling/keyboard page
95
+ */
96
+ spellingPageId?: string;
97
+ /**
98
+ * Whether to use prediction for missing words
99
+ *
100
+ * When true (default): Words not in the board are assumed to be accessible
101
+ * via prediction at reduced effort (spelling_page_base + prediction_selection)
102
+ *
103
+ * When false: Words not in the board must be manually spelled at full effort
104
+ * (10 + word_length * 2.5 per letter)
105
+ *
106
+ * Only applies when the board has prediction capability (e.g., SwiftKey)
107
+ */
108
+ usePrediction?: boolean;
109
+ /**
110
+ * Average number of selections to find a word in prediction
111
+ *
112
+ * When prediction is enabled, this estimates how many prediction
113
+ * slots a user needs to check before finding their target word.
114
+ * Default is 1.5 (checking 1-2 predictions on average).
115
+ */
116
+ predictionSelections?: number;
117
+ }
66
118
  /**
67
119
  * Comparison result between two board sets
68
120
  */
@@ -76,6 +128,13 @@ export interface ComparisonResult extends MetricsResult {
76
128
  columns: number;
77
129
  };
78
130
  comp_effort_score: number;
131
+ comp_spelling_effort_base?: number;
132
+ comp_spelling_effort_per_letter?: number;
133
+ comp_spelling_page_id?: string;
134
+ has_dynamic_prediction?: boolean;
135
+ prediction_page_id?: string;
136
+ comp_has_dynamic_prediction?: boolean;
137
+ comp_prediction_page_id?: string;
79
138
  missing_words: string[];
80
139
  extra_words: string[];
81
140
  overlapping_words: string[];
@@ -104,6 +163,8 @@ export interface ComparisonResult extends MetricsResult {
104
163
  comp_fringe: number;
105
164
  common_fringe: number;
106
165
  comp_common_fringe: number;
166
+ care_score: number;
167
+ comp_care_score: number;
107
168
  };
108
169
  sentences: SentenceAnalysis[];
109
170
  fringe_words: FringeWord[];
@@ -130,7 +130,7 @@ class VocabularyAnalyzer {
130
130
  if (btn) {
131
131
  return btn.effort;
132
132
  }
133
- return (0, effort_1.spellingEffort)(word);
133
+ return (0, effort_1.spellingEffort)(word, metrics.spelling_effort_base, metrics.spelling_effort_per_letter);
134
134
  }
135
135
  /**
136
136
  * Check if a word is in the board set
@@ -35,6 +35,12 @@ export declare class ReferenceLoader {
35
35
  loadBaseWords(): {
36
36
  [word: string]: boolean;
37
37
  };
38
+ /**
39
+ * Load common fringe vocabulary
40
+ * Common words that are NOT in core vocabulary lists
41
+ * (matching Ruby loader.rb:413-420)
42
+ */
43
+ loadCommonFringe(): string[];
38
44
  /**
39
45
  * Get all reference data at once
40
46
  */
@@ -75,7 +75,18 @@ class ReferenceLoader {
75
75
  loadFringe() {
76
76
  const filePath = path.join(this.dataDir, `fringe.${this.locale}.json`);
77
77
  const content = fs.readFileSync(filePath, 'utf-8');
78
- return JSON.parse(content);
78
+ const data = JSON.parse(content);
79
+ // Flatten nested category words if needed
80
+ if (Array.isArray(data) && data.length > 0 && data[0].categories) {
81
+ const flattened = [];
82
+ data.forEach((list) => {
83
+ list.categories.forEach((cat) => {
84
+ flattened.push(...cat.words);
85
+ });
86
+ });
87
+ return flattened;
88
+ }
89
+ return data;
79
90
  }
80
91
  /**
81
92
  * Load base words hash map
@@ -85,6 +96,23 @@ class ReferenceLoader {
85
96
  const content = fs.readFileSync(filePath, 'utf-8');
86
97
  return JSON.parse(content);
87
98
  }
99
+ /**
100
+ * Load common fringe vocabulary
101
+ * Common words that are NOT in core vocabulary lists
102
+ * (matching Ruby loader.rb:413-420)
103
+ */
104
+ loadCommonFringe() {
105
+ const commonWordsData = this.loadCommonWords();
106
+ const commonWords = new Set(commonWordsData.words.map((w) => w.toLowerCase()));
107
+ const coreLists = this.loadCoreLists();
108
+ const coreWords = new Set();
109
+ coreLists.forEach((list) => {
110
+ list.words.forEach((word) => coreWords.add(word.toLowerCase()));
111
+ });
112
+ // Common fringe = common words - core words
113
+ const commonFringe = Array.from(commonWords).filter((word) => !coreWords.has(word));
114
+ return commonFringe;
115
+ }
88
116
  /**
89
117
  * Get all reference data at once
90
118
  */
@@ -39,6 +39,7 @@ export interface ButtonForTranslation {
39
39
  message: string;
40
40
  textToTranslate: string;
41
41
  symbols: SymbolInfo[];
42
+ grammar?: any;
42
43
  }
43
44
  /**
44
45
  * LLM translation result with symbol mappings
@@ -68,7 +69,7 @@ export interface LLMLTranslationResult {
68
69
  export declare function normalizeButtonForTranslation(buttonId: string, label: string, message: string, symbols: SymbolInfo[], context?: {
69
70
  pageId?: string;
70
71
  pageName?: string;
71
- }): ButtonForTranslation;
72
+ }, grammar?: any): ButtonForTranslation;
72
73
  /**
73
74
  * Extract symbols from various button formats.
74
75
  *
@@ -39,13 +39,14 @@ exports.validateTranslationResults = validateTranslationResults;
39
39
  * @param context - Optional page context
40
40
  * @returns Normalized button data for translation
41
41
  */
42
- function normalizeButtonForTranslation(buttonId, label, message, symbols, context) {
42
+ function normalizeButtonForTranslation(buttonId, label, message, symbols, context, grammar) {
43
43
  return {
44
44
  buttonId,
45
45
  label,
46
46
  message,
47
47
  textToTranslate: message || label, // Translate message if present, otherwise label
48
48
  symbols,
49
+ grammar,
49
50
  ...context,
50
51
  };
51
52
  }
@@ -119,7 +120,8 @@ function extractAllButtonsForTranslation(buttons, contextFn) {
119
120
  if (!label && !message)
120
121
  continue;
121
122
  const context = contextFn ? contextFn(button) : undefined;
122
- results.push(normalizeButtonForTranslation(buttonId, label, message, symbols || [], context));
123
+ const grammar = button.parameters?.grammar || undefined;
124
+ results.push(normalizeButtonForTranslation(buttonId, label, message, symbols || [], context, grammar));
123
125
  }
124
126
  return results;
125
127
  }
@@ -144,6 +146,7 @@ Each button has:
144
146
  - message: The text spoken when the button is activated
145
147
  - textToTranslate: The actual text to translate (usually the message)
146
148
  - symbols: Visual symbols attached to specific words
149
+ - grammar: Grammatical context (e.g., pos: Part of Speech, person, number)
147
150
 
148
151
  IMPORTANT: After translation, you MUST reattach symbols to the correct translated words based on MEANING, not position.
149
152
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@willwade/aac-processors",
3
- "version": "0.0.15",
3
+ "version": "0.0.17",
4
4
  "description": "A comprehensive TypeScript library for processing AAC (Augmentative and Alternative Communication) file formats with translation support",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",