Skip to content

Commit 11f8839

Browse files
committed
add aacmetrics - replicating openaac/aacmetrics repo
1 parent f6a957b commit 11f8839

37 files changed

Lines changed: 22092 additions & 11 deletions

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
.DS_Store
2+
tmp/
23
# Logs
34
logs
45
*.log
@@ -177,3 +178,7 @@ scripts/*.output.*
177178
**/gemini_response.txt
178179
**/*_cache.json
179180
examples/gemini_response.txt
181+
182+
# AAC Metrics benchmarking data
183+
aac-metrics/
184+
compare-effort-scores.ts

src/cli/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@ function detectFormat(filePath: string): string {
1717
return 'ascconfig';
1818
}
1919

20+
// Map multi-file formats to their base processor
21+
if (filePath.endsWith('.obfset')) {
22+
return 'obf'; // Use ObfProcessor for .obfset files
23+
}
24+
if (filePath.endsWith('.gridset')) {
25+
return 'gridset';
26+
}
27+
2028
// Otherwise use file extension
2129
return path.extname(filePath).slice(1);
2230
}

src/core/analyze.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export function getProcessor(format: string, options?: ProcessorOptions): BasePr
2222
case 'opml':
2323
return new OpmlProcessor(options);
2424
case 'obf':
25+
case 'obfset': // Obfset files use ObfProcessor
2526
return new ObfProcessor(options);
2627
case 'touchchat':
2728
case 'ce': // TouchChat file extension

src/core/treeStructure.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ export interface AACSemanticAction {
107107
type: 'SPEAK' | 'NAVIGATE' | 'ACTION';
108108
message?: string;
109109
targetPageId?: string;
110+
temporary_home?: boolean | string | null;
111+
add_to_sentence?: boolean;
110112
};
111113
}
112114

@@ -143,6 +145,9 @@ export class AACButton implements IAACButton {
143145
directActivate?: boolean;
144146
audioDescription?: string;
145147
parameters?: { [key: string]: any };
148+
// Metrics support: Motor planning identifiers
149+
semantic_id?: string; // Unique ID for buttons with same semantic meaning across boards
150+
clone_id?: string; // Unique ID for buttons with same label+location across boards
146151

147152
constructor({
148153
id,
@@ -166,6 +171,8 @@ export class AACButton implements IAACButton {
166171
visibility,
167172
directActivate,
168173
parameters,
174+
semantic_id,
175+
clone_id,
169176
// Legacy input support
170177
type,
171178
action,
@@ -196,6 +203,8 @@ export class AACButton implements IAACButton {
196203
visibility?: 'Visible' | 'Hidden' | 'Disabled' | 'PointerAndTouchOnly' | 'Empty';
197204
directActivate?: boolean;
198205
parameters?: { [key: string]: any };
206+
semantic_id?: string;
207+
clone_id?: string;
199208
// Legacy constructor properties for backward compatibility
200209
type?: 'SPEAK' | 'NAVIGATE' | 'ACTION';
201210
action?: {
@@ -225,6 +234,8 @@ export class AACButton implements IAACButton {
225234
this.visibility = visibility;
226235
this.directActivate = directActivate;
227236
this.parameters = parameters;
237+
this.semantic_id = semantic_id;
238+
this.clone_id = clone_id;
228239

229240
// Legacy mapping: if no semanticAction provided, derive from legacy `action` first
230241
if (!this.semanticAction && action) {
@@ -319,6 +330,9 @@ export class AACPage implements IAACPage {
319330
descriptionHtml?: string;
320331
images?: any[];
321332
sounds?: any[];
333+
// Metrics support: Track semantic/clone IDs used on this page
334+
semantic_ids?: string[];
335+
clone_ids?: string[];
322336

323337
constructor({
324338
id,
@@ -331,6 +345,8 @@ export class AACPage implements IAACPage {
331345
descriptionHtml,
332346
images,
333347
sounds,
348+
semantic_ids,
349+
clone_ids,
334350
}: {
335351
id: string;
336352
name?: string;
@@ -342,6 +358,8 @@ export class AACPage implements IAACPage {
342358
descriptionHtml?: string;
343359
images?: any[];
344360
sounds?: any[];
361+
semantic_ids?: string[];
362+
clone_ids?: string[];
345363
}) {
346364
this.id = id;
347365
this.name = name;
@@ -361,6 +379,8 @@ export class AACPage implements IAACPage {
361379
this.descriptionHtml = descriptionHtml;
362380
this.images = images;
363381
this.sounds = sounds;
382+
this.semantic_ids = semantic_ids;
383+
this.clone_ids = clone_ids;
364384
}
365385

366386
addButton(button: AACButton): void {

src/index.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@ export * from './core/treeStructure';
33
export * from './core/baseProcessor';
44
export * from './core/stringCasing';
55
export * from './processors';
6+
export * from './validation';
7+
export * as Analytics from './optional/analytics';
68
export {
79
collectUnifiedHistory,
810
listGrid3Users as listHistoryGrid3Users,
911
listSnapUsers as listHistorySnapUsers,
10-
} from './analytics/history';
11-
export * from './validation';
12+
} from './optional/analytics/history';
1213

1314
import { BaseProcessor } from './core/baseProcessor';
1415
import { DotProcessor } from './processors/dotProcessor';
@@ -20,6 +21,7 @@ import { SnapProcessor } from './processors/snapProcessor';
2021
import { TouchChatProcessor } from './processors/touchchatProcessor';
2122
import { ApplePanelsProcessor } from './processors/applePanelsProcessor';
2223
import { AstericsGridProcessor } from './processors/astericsGridProcessor';
24+
import { ObfsetProcessor } from './processors/obfsetProcessor';
2325

2426
/**
2527
* Factory function to get the appropriate processor for a file extension
@@ -43,6 +45,8 @@ export function getProcessor(filePathOrExtension: string): BaseProcessor {
4345
case '.obf':
4446
case '.obz':
4547
return new ObfProcessor();
48+
case '.obfset':
49+
return new ObfsetProcessor();
4650
case '.gridset':
4751
case '.gridsetx':
4852
return new GridsetProcessor();
@@ -71,6 +75,7 @@ export function getSupportedExtensions(): string[] {
7175
'.opml',
7276
'.obf',
7377
'.obz',
78+
'.obfset',
7479
'.gridset',
7580
'.gridsetx',
7681
'.spb',
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# AAC Effort Metrics - User Guide
2+
3+
This guide explains the basics of the AAC Effort Algorithm and how to use the TypeScript implementation provided in this repository.
4+
5+
## 🧠 The AAC Effort Algorithm
6+
7+
The AAC Effort Algorithm measures the physical and cognitive "cost" of activating buttons in an AAC (Augmentative and Alternative Communication) system. It combines multiple factors into a single "effort score" for each word or phrase.
8+
9+
### Core Metrics
10+
11+
1. **Button Size Effort**: Calculated from grid dimensions. Larger grids (smaller buttons) require more effort to discriminate and target.
12+
2. **Field Size Effort**: Based on the number of visible buttons. More choices increase visual clutter and cognitive load.
13+
3. **Visual Scan Effort**: The time/effort taken to scan through buttons in a grid (usually left-to-right, top-to-bottom) before reaching the target.
14+
4. **Distance Effort**: The physical distance between the previous button (or the "home" position) and the current target.
15+
5. **Prior Effort (Navigation Cost)**: Every time a board changes (folder selection), a "Processing Effort" penalty (default: 1.0) is added, representing the cognitive cost of orienting to a new screen.
16+
17+
### Motor Planning Discounts
18+
19+
The algorithm rewards systems that support motor planning. If a button appears in a predictable location or follows a recognizable pattern, its effort is discounted:
20+
21+
- **Clones**: Exact copies of buttons in the same location across boards.
22+
- **Semantic Locations**: Patterns where types of words (e.g., verbs, colors) always appear in the same grid area.
23+
- **Upstream Matching**: If the button used to _enter_ a board matches the ID of a button _on_ that board, the effort is reduced.
24+
25+
---
26+
27+
## 💻 How to Use the Code
28+
29+
### 1. Basic Usage (Multi-Format Support)
30+
31+
The `MetricsCalculator` is format-agnostic. You can use any processor to load a pageset into an `AACTree`, then pass that tree to the calculator.
32+
33+
#### Loading an OBFSET
34+
```typescript
35+
import { ObfsetProcessor, Analytics } from '@willwade/aac-processors';
36+
37+
const processor = new ObfsetProcessor();
38+
const tree = processor.loadIntoTree('set.obfset');
39+
const result = new Analytics.MetricsCalculator().analyze(tree);
40+
```
41+
42+
#### Loading Grid 3 (.gridset)
43+
```typescript
44+
import { GridsetProcessor, Analytics } from '@willwade/aac-processors';
45+
46+
const processor = new GridsetProcessor();
47+
const tree = processor.loadIntoTree('my_file.gridset');
48+
const result = new Analytics.MetricsCalculator().analyze(tree);
49+
```
50+
51+
#### Loading Snap (.pageSet / .spb)
52+
```typescript
53+
import { SnapProcessor, Analytics } from '@willwade/aac-processors';
54+
55+
const processor = new SnapProcessor();
56+
const tree = processor.loadIntoTree('SnapBackup.pageSet');
57+
const result = new Analytics.MetricsCalculator().analyze(tree);
58+
```
59+
60+
#### Loading TouchChat (.zip)
61+
```typescript
62+
import { TouchChatProcessor, Analytics } from '@willwade/aac-processors';
63+
64+
const processor = new TouchChatProcessor();
65+
const tree = processor.loadIntoTree('TouchChatBackup.zip');
66+
const result = new Analytics.MetricsCalculator().analyze(tree);
67+
```
68+
69+
### 2. Result Structure
70+
71+
The `analyze` function returns:
72+
73+
- `total_words`: Number of unique vocalizations found.
74+
- `total_buttons`: Total number of button activations analyzed.
75+
- `buttons`: An array of `ButtonMetrics` for each word (taking the minimum effort path found).
76+
- `levels`: An object grouping buttons by their depth (e.g., Level 0 = home screen).
77+
78+
### 3. Requirements
79+
80+
- **Supported Formats**: Any format with a corresponding processor (`.obf`, `.obz`, `.obfset`, `.gridset`, `.pageSet`, `.spb`, `.zip` for TouchChat, etc.).
81+
- **Metadata**: For best accuracy, buttons should include `clone_id` or `semantic_id` where applicable.
82+
83+
---
84+
85+
## ⚖️ Implementation vs. Original Algorithm
86+
87+
This version is optimized for Node.js and adheres strictly to the **v0.2 Algorithm Specification**.
88+
89+
- **Accuracy**: Achieving ~95%+ parity with original benchmarking tools.
90+
- **Efficiency**: Uses a modified Breadth-First Search (BFS) to find the most efficient user path to any word.
91+
- **Improved Logic**: Resolves "double-discounting" issues present in legacy implementations, favoring a more realistic motor-planning model.
92+
93+
For detailed technical notes on implementation decisions, see [ALGORITHM_IMPLEMENTATION_NOTES.md](./ALGORITHM_IMPLEMENTATION_NOTES.md).
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# AAC Effort Algorithm Implementation Notes
2+
3+
This document details the discrepancies between the original Ruby `aac-metrics` implementation and this TypeScript implementation, specifically regarding how they align with the [v0.2 algorithm specification](./AAC%20Effort%20Algorithms.md).
4+
5+
## 🚀 Overview
6+
7+
The TypeScript implementation (this project) achieves high parity with the Ruby gem (~95% accuracy) while resolving several logical inconsistencies found in the Ruby code. Where the two implementations differ, this project favors the logic described in `AAC Effort Algorithms.md`.
8+
9+
## 🔍 Identified Discrepancies & "Ruby Bugs"
10+
11+
### 1. Sequential vs. Exclusive Discounting (Serial Discounting)
12+
13+
**The Ruby Issue:**
14+
In the Ruby gem (`metrics.rb` lines 250-264 and 278-291), discounts for `semantic_id` and `clone_id` are applied sequentially.
15+
16+
- If a button has both a `semantic_id` and a `clone_id`, Ruby applies a discount for the first, then applies the second discount to the _already discounted_ value.
17+
- **Example:** `effort = (effort * 0.5) * 0.33` results in an ~84% reduction.
18+
19+
**Our Implementation:**
20+
Following the principle of "best match," our TypeScript code uses `Math.min` or `else if` logic. We apply the single most favorable discount available, preventing "bottomless" effort scores that occur when multiple identifiers happen to overlap on the same button.
21+
22+
### 2. The "Sub-board Effort" Gap
23+
24+
**The Ruby Issue:**
25+
For buttons on sub-boards (Level 1+), Ruby occasionally records an "on-board" effort (the effort added _by_ the current board, excluding prior effort) as low as **0.02**.
26+
27+
- The specification states that every new board should start with a `board_effort` base (calculated from grid size). For a 6x10 grid, this base is **1.21**.
28+
- Even with a 90% discount (the maximum suggested), the effort should be ~0.12 + distance.
29+
- Ruby's 0.02 score suggests it is either failing to add the `board_effort` base for certain paths or allows discounts to stack far beyond the 10% floor.
30+
31+
**Our Implementation:**
32+
We strictly maintain the `board_effort` base for all boards, ensuring that the cognitive cost of processing a new screen (as defined in the "General Factors" section of the spec) is always accounted for.
33+
34+
### 3. Upstream ID Weighting
35+
36+
**The Ruby Issue:**
37+
Ruby calculates `board_pcts` (the percentage of links to a board that match an ID) by counting every individual link.
38+
39+
- In keyboard-heavy sets like WordPower, where 30+ buttons might link to the same "Keyboard" board, those 30 links dominate the percentage calculation.
40+
- This creates "Super-Discounts" for buttons on shared boards, even if the user only realistically takes one path to get there.
41+
42+
**Our Implementation:**
43+
While we currently replicate this link-weighted approach for parity, we have noted it as an area where the algorithm's intent (measuring predictability across boards) may be skewed by the technical structure of the OBF file.
44+
45+
### 4. Processing Effort Discount Placement
46+
47+
**The Ruby Issue:**
48+
Ruby subtracts a portion of the `BOARD_CHANGE_PROCESSING_EFFORT` (1.0) from the _final_ button effort if that button (a "Speak" button) matches the ID used to reach the board.
49+
50+
**Our Implementation:**
51+
We have implemented this to match Ruby, but noted that it acts as a "post-hoc" reward for navigation consistency rather than a forward-propagating motor planning discount.
52+
53+
## 📈 Parity Status
54+
55+
- **Level 0 (Root):** 100% Parity.
56+
- **Level 1+:** ~92-95% Parity.
57+
- **Overall:** ~95% Parity across most common AAC sets (WordPower 80, etc.).
58+
59+
By choosing to be "more faithful" to the algorithm document, our effort scores may be slightly higher than Ruby's in complex sub-boards, but they more accurately represent the cognitive and motor costs defined in the v0.2 specification.

0 commit comments

Comments
 (0)