Skip to content

Commit cda5497

Browse files
authored
Merge pull request #10 from willwade/scanning-effort-calcs
Scanning effort calcs
2 parents 159feb3 + f3dc797 commit cda5497

15 files changed

Lines changed: 1271 additions & 57 deletions

File tree

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -833,6 +833,7 @@ Inspired by the Python AACProcessors project
833833

834834
- [ ] **Road Testing** - Perform comprehensive layout and formatting validation across diverse pagesets to verify conversion fidelity.
835835
- [ ] **Fix audio persistence issues** - Resolve functional audio recording persistence in `SnapProcessor` save/load cycle (5 failing tests remaining).
836+
- [x] **Access Method Modeling** - Support for switch scanning (linear, row-column, block) integrated into AAC metrics.
836837

837838
### 🚨 High Priority (Next Sprint)
838839

@@ -842,7 +843,7 @@ Inspired by the Python AACProcessors project
842843

843844
### ⚠️ Medium Priority
844845

845-
- [ ] **Access Method Modeling** - Define core rules for AAC systems (Switch scanning, blocks, dwell times) to support diverse access methods. We need to do this for metrics
846+
- [ ] **Adaptive Metrics** - Expand scanning analysis to include dwell times and more complex switch logic configurations.
846847

847848

848849
### Low Priority

scripts/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ Analysis and reporting tools for AAC pagesets.
2626
- **extract_vocabulary.js** - Extract vocabulary from pagesets
2727
- **generate_csv.js** - Generate CSV reports from vocabulary
2828
- **validate_complete_workflow.js** - Validate end-to-end workflows
29+
- **scanning_benchmark.ts** - Benchmark scanning efficiency and vocabulary coverage across multiple files
2930

3031
Example:
3132
```bash
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
#!/usr/bin/env npx ts-node
2+
/**
3+
* AAC Scanning & Vocabulary Benchmark Script
4+
*
5+
* This script analyzes AAC pagesets (Snap, Grid 3, TouchChat) for:
6+
* 1. Scanning efficiency (steps and selections)
7+
* 2. Vocabulary coverage (core word lists)
8+
* 3. Effort scores (how hard it is to reach words)
9+
*
10+
* Usage:
11+
* npx ts-node scripts/analysis/scanning_benchmark.ts [directory_path]
12+
*
13+
* If no directory is specified, it will look in '/tmp' as an example.
14+
*/
15+
16+
import * as fs from 'fs';
17+
import * as path from 'path';
18+
import { getProcessor, Analytics, AACTree, isExtensionSupported } from '../../src/index';
19+
20+
async function runBenchmark() {
21+
const targetDir = process.argv[2] || './tmp';
22+
23+
console.log(`\n🚀 AAC Scanning Benchmark`);
24+
console.log(`=========================`);
25+
console.log(`Target Directory: ${targetDir}`);
26+
console.log(`Date: ${new Date().toISOString()}\n`);
27+
28+
if (!fs.existsSync(targetDir)) {
29+
console.error(`❌ Error: Directory '${targetDir}' does not exist.`);
30+
console.log(`\nUsage: npx ts-node scripts/analysis/scanning_benchmark.ts [directory_path]`);
31+
console.log(`Hint: Provide a folder containing .gridset, .sps (Snap), or .zip (TouchChat) files.\n`);
32+
process.exit(1);
33+
}
34+
35+
const files = fs.readdirSync(targetDir).filter(file => {
36+
const ext = path.extname(file).toLowerCase();
37+
// Special case for TouchChat which can be .zip sometimes, or .ce
38+
return isExtensionSupported(ext) || ext === '.zip';
39+
});
40+
41+
if (files.length === 0) {
42+
console.warn(`⚠️ No supported AAC files found in ${targetDir}.`);
43+
console.log(`Supported extensions: .gridset, .sps, .spb, .ce, .obfset, etc.`);
44+
return;
45+
}
46+
47+
console.log(`Found ${files.length} pageset(s) to analyze.\n`);
48+
49+
const calculator = new Analytics.MetricsCalculator();
50+
const vocabAnalyzer = new Analytics.VocabularyAnalyzer();
51+
52+
for (const file of files) {
53+
const filePath = path.join(targetDir, file);
54+
const ext = path.extname(file).toLowerCase();
55+
56+
console.log(`Processing: ${file}...`);
57+
58+
try {
59+
// Use standard processor factory
60+
// TouchChat specifically often comes in .zip, so we map it if needed
61+
const processorExt = ext === '.zip' ? '.ce' : ext;
62+
const processor = getProcessor(processorExt);
63+
64+
const tree = processor.loadIntoTree(filePath);
65+
66+
// Analyze with default settings (auto-detects scanning if configured in properties)
67+
// or we can force scanning for the benchmark
68+
const metrics = calculator.analyze(tree);
69+
70+
// Vocabulary analysis
71+
const vocabAnalysis = vocabAnalyzer.analyze(metrics);
72+
73+
printSummary(file, metrics, vocabAnalysis);
74+
} catch (err) {
75+
console.error(` ❌ Failed to process ${file}: ${(err as Error).message}`);
76+
}
77+
console.log(`--------------------------------------------------`);
78+
}
79+
}
80+
81+
function printSummary(filename: string, metrics: any, vocab: any) {
82+
console.log(` ✅ Analysis Complete`);
83+
console.log(` Format-Specific Metric: ${metrics.grid.columns}x${metrics.grid.rows} Average Grid`);
84+
console.log(` Total Unique Words: ${vocab.total_unique_words}`);
85+
console.log(` Total Buttons Analyzed: ${metrics.total_buttons}`);
86+
87+
console.log(`\n 🌟 Effort Scores (Scanning Context):`);
88+
const avgEffort = metrics.buttons.reduce((sum: number, b: any) => sum + b.effort, 0) / metrics.buttons.length;
89+
console.log(` Average Effort: ${avgEffort.toFixed(3)}`);
90+
91+
// Highlighting Core List Coverage
92+
console.log(`\n 📊 Core Vocabulary Coverage:`);
93+
Object.entries(vocab.core_coverage).forEach(([listId, data]: [string, any]) => {
94+
console.log(` - ${data.name}: ${data.coverage_percent.toFixed(1)}% (${data.covered}/${data.total_words})`);
95+
console.log(` Avg Effort to core: ${data.average_effort.toFixed(3)}`);
96+
});
97+
98+
// Highlight specific low/high effort items
99+
if (vocab.low_effort_words.length > 0) {
100+
const top3 = vocab.low_effort_words.slice(0, 3).map((w: any) => `${w.word} (${w.effort.toFixed(2)})`).join(', ');
101+
console.log(`\n ✨ Most Accessible Words: ${top3}`);
102+
}
103+
}
104+
105+
runBenchmark().catch(err => {
106+
console.error(`FATAL ERROR: ${err.message}`);
107+
process.exit(1);
108+
});

src/core/treeStructure.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,27 @@ export enum AACSemanticIntent {
5252
PLATFORM_SPECIFIC = 'PLATFORM_SPECIFIC',
5353
}
5454

55+
/**
56+
* Scanning types for accessibility
57+
*/
58+
export enum AACScanType {
59+
LINEAR = 'linear', // Left-to-right, top-to-bottom
60+
ROW_COLUMN = 'row-column', // Scan rows, then columns
61+
COLUMN_ROW = 'column-row', // Scan columns, then rows
62+
BLOCK_ROW_COLUMN = 'block-row-column', // Scan blocks, then rows, then columns
63+
BLOCK_COLUMN_ROW = 'block-column-row', // Scan blocks, then columns, then rows
64+
}
65+
66+
/**
67+
* Configuration for a scan block
68+
*/
69+
export interface AACScanBlock {
70+
id: number;
71+
name?: string;
72+
order?: number; // Sequence in scanning
73+
scanType?: AACScanType; // Override scanning within this block
74+
}
75+
5576
// New semantic action interface for cross-platform compatibility
5677
export interface AACSemanticAction {
5778
// Make category optional for backward-compat with older tests constructing minimal actions
@@ -140,7 +161,14 @@ export class AACButton implements IAACButton {
140161
y?: number;
141162
columnSpan?: number;
142163
rowSpan?: number;
164+
/**
165+
* @deprecated Use scanBlock instead (singular, not array)
166+
*/
143167
scanBlocks?: number[];
168+
/**
169+
* Scan block number (1-8) for block scanning
170+
*/
171+
scanBlock?: number;
144172
visibility?: 'Visible' | 'Hidden' | 'Disabled' | 'PointerAndTouchOnly' | 'Empty';
145173
directActivate?: boolean;
146174
audioDescription?: string;
@@ -168,6 +196,7 @@ export class AACButton implements IAACButton {
168196
columnSpan,
169197
rowSpan,
170198
scanBlocks,
199+
scanBlock,
171200
visibility,
172201
directActivate,
173202
parameters,
@@ -200,6 +229,7 @@ export class AACButton implements IAACButton {
200229
columnSpan?: number;
201230
rowSpan?: number;
202231
scanBlocks?: number[];
232+
scanBlock?: number;
203233
visibility?: 'Visible' | 'Hidden' | 'Disabled' | 'PointerAndTouchOnly' | 'Empty';
204234
directActivate?: boolean;
205235
parameters?: { [key: string]: any };
@@ -231,6 +261,7 @@ export class AACButton implements IAACButton {
231261
this.columnSpan = columnSpan;
232262
this.rowSpan = rowSpan;
233263
this.scanBlocks = scanBlocks;
264+
this.scanBlock = scanBlock;
234265
this.visibility = visibility;
235266
this.directActivate = directActivate;
236267
this.parameters = parameters;
@@ -333,6 +364,12 @@ export class AACPage implements IAACPage {
333364
// Metrics support: Track semantic/clone IDs used on this page
334365
semantic_ids?: string[];
335366
clone_ids?: string[];
367+
// Scanning configuration for this page
368+
scanningConfig?: import('../types/aac').ScanningConfig;
369+
370+
// Scanning support
371+
scanType?: AACScanType;
372+
scanBlocksConfig?: AACScanBlock[];
336373

337374
constructor({
338375
id,
@@ -347,6 +384,9 @@ export class AACPage implements IAACPage {
347384
sounds,
348385
semantic_ids,
349386
clone_ids,
387+
scanningConfig,
388+
scanBlocksConfig,
389+
scanType,
350390
}: {
351391
id: string;
352392
name?: string;
@@ -360,6 +400,9 @@ export class AACPage implements IAACPage {
360400
sounds?: any[];
361401
semantic_ids?: string[];
362402
clone_ids?: string[];
403+
scanningConfig?: import('../types/aac').ScanningConfig;
404+
scanBlocksConfig?: AACScanBlock[];
405+
scanType?: AACScanType;
363406
}) {
364407
this.id = id;
365408
this.name = name;
@@ -381,6 +424,9 @@ export class AACPage implements IAACPage {
381424
this.sounds = sounds;
382425
this.semantic_ids = semantic_ids;
383426
this.clone_ids = clone_ids;
427+
this.scanningConfig = scanningConfig;
428+
this.scanBlocksConfig = scanBlocksConfig;
429+
this.scanType = scanType;
384430
}
385431

386432
addButton(button: AACButton): void {

src/optional/analytics/docs/AAC_METRICS_GUIDE.md

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,16 @@ The algorithm rewards systems that support motor planning. If a button appears i
2222
- **Semantic Locations**: Patterns where types of words (e.g., verbs, colors) always appear in the same grid area.
2323
- **Upstream Matching**: If the button used to _enter_ a board matches the ID of a button _on_ that board, the effort is reduced.
2424

25+
### Switch Scanning Support
26+
27+
For users who use scanning access methods, the implementation supports evaluating scanning effort. Instead of movement distance, the algorithm calculates:
28+
1. **Scan Steps**: The number of highlight movements (items or groups) to reach the target.
29+
2. **Selections**: The number of switch activations required.
30+
31+
Supported scanning types include `linear`, `row-column`, `column-row`, and `block-based` scanning across Grid 3, TD Snap, and TouchChat.
32+
33+
> 📖 **Detailed Guide**: For comprehensive information on scanning metrics across different platforms, see [SCANNING_METRICS_GUIDE.md](./SCANNING_METRICS_GUIDE.md).
34+
2535
---
2636

2737
## 💻 How to Use the Code
@@ -78,7 +88,28 @@ The `analyze` function returns:
7888
### 3. Requirements
7989

8090
- **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.
91+
- **Metadata**: For best accuracy, buttons should include `clone_id` or `semantic_id`. For scanning analysis, pages should have a `scanType` and buttons should have `scanBlocks` assigned.
92+
93+
### 4. Configuring Scanning
94+
95+
You can evaluate scanning efficiency by setting the `scanType` on `AACPage` objects:
96+
97+
```typescript
98+
import { AACScanType } from '@willwade/aac-processors';
99+
100+
// Set scanning behavior for a page
101+
page.scanType = AACScanType.ROW_COLUMN;
102+
103+
// Or using blocks
104+
page.scanType = AACScanType.BLOCK_ROW_COLUMN;
105+
page.scanBlocksConfig = [
106+
{ id: 1, name: 'Main', order: 1 },
107+
{ id: 2, name: 'Numbers', order: 2 }
108+
];
109+
110+
// Assign buttons to blocks
111+
button.scanBlocks = [2];
112+
```
82113

83114
---
84115

0 commit comments

Comments
 (0)