Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -833,6 +833,7 @@ Inspired by the Python AACProcessors project

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

### 🚨 High Priority (Next Sprint)

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

### ⚠️ Medium Priority

- [ ] **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
- [ ] **Adaptive Metrics** - Expand scanning analysis to include dwell times and more complex switch logic configurations.


### Low Priority
Expand Down
1 change: 1 addition & 0 deletions scripts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Analysis and reporting tools for AAC pagesets.
- **extract_vocabulary.js** - Extract vocabulary from pagesets
- **generate_csv.js** - Generate CSV reports from vocabulary
- **validate_complete_workflow.js** - Validate end-to-end workflows
- **scanning_benchmark.ts** - Benchmark scanning efficiency and vocabulary coverage across multiple files

Example:
```bash
Expand Down
108 changes: 108 additions & 0 deletions scripts/analysis/scanning_benchmark.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
#!/usr/bin/env npx ts-node
/**
* AAC Scanning & Vocabulary Benchmark Script
*
* This script analyzes AAC pagesets (Snap, Grid 3, TouchChat) for:
* 1. Scanning efficiency (steps and selections)
* 2. Vocabulary coverage (core word lists)
* 3. Effort scores (how hard it is to reach words)
*
* Usage:
* npx ts-node scripts/analysis/scanning_benchmark.ts [directory_path]
*
* If no directory is specified, it will look in '/tmp' as an example.
*/

import * as fs from 'fs';
import * as path from 'path';
import { getProcessor, Analytics, AACTree, isExtensionSupported } from '../../src/index';

async function runBenchmark() {
const targetDir = process.argv[2] || './tmp';

console.log(`\n🚀 AAC Scanning Benchmark`);
console.log(`=========================`);
console.log(`Target Directory: ${targetDir}`);
console.log(`Date: ${new Date().toISOString()}\n`);

if (!fs.existsSync(targetDir)) {
console.error(`❌ Error: Directory '${targetDir}' does not exist.`);
console.log(`\nUsage: npx ts-node scripts/analysis/scanning_benchmark.ts [directory_path]`);
console.log(`Hint: Provide a folder containing .gridset, .sps (Snap), or .zip (TouchChat) files.\n`);
process.exit(1);
}

const files = fs.readdirSync(targetDir).filter(file => {
const ext = path.extname(file).toLowerCase();
// Special case for TouchChat which can be .zip sometimes, or .ce
return isExtensionSupported(ext) || ext === '.zip';
});

if (files.length === 0) {
console.warn(`⚠️ No supported AAC files found in ${targetDir}.`);
console.log(`Supported extensions: .gridset, .sps, .spb, .ce, .obfset, etc.`);
return;
}

console.log(`Found ${files.length} pageset(s) to analyze.\n`);

const calculator = new Analytics.MetricsCalculator();
const vocabAnalyzer = new Analytics.VocabularyAnalyzer();

for (const file of files) {
const filePath = path.join(targetDir, file);
const ext = path.extname(file).toLowerCase();

console.log(`Processing: ${file}...`);

try {
// Use standard processor factory
// TouchChat specifically often comes in .zip, so we map it if needed
const processorExt = ext === '.zip' ? '.ce' : ext;
const processor = getProcessor(processorExt);

const tree = processor.loadIntoTree(filePath);

// Analyze with default settings (auto-detects scanning if configured in properties)
// or we can force scanning for the benchmark
const metrics = calculator.analyze(tree);

// Vocabulary analysis
const vocabAnalysis = vocabAnalyzer.analyze(metrics);

printSummary(file, metrics, vocabAnalysis);
} catch (err) {
console.error(` ❌ Failed to process ${file}: ${(err as Error).message}`);
}
console.log(`--------------------------------------------------`);
}
}

function printSummary(filename: string, metrics: any, vocab: any) {
console.log(` ✅ Analysis Complete`);
console.log(` Format-Specific Metric: ${metrics.grid.columns}x${metrics.grid.rows} Average Grid`);
console.log(` Total Unique Words: ${vocab.total_unique_words}`);
console.log(` Total Buttons Analyzed: ${metrics.total_buttons}`);

console.log(`\n 🌟 Effort Scores (Scanning Context):`);
const avgEffort = metrics.buttons.reduce((sum: number, b: any) => sum + b.effort, 0) / metrics.buttons.length;
console.log(` Average Effort: ${avgEffort.toFixed(3)}`);

// Highlighting Core List Coverage
console.log(`\n 📊 Core Vocabulary Coverage:`);
Object.entries(vocab.core_coverage).forEach(([listId, data]: [string, any]) => {
console.log(` - ${data.name}: ${data.coverage_percent.toFixed(1)}% (${data.covered}/${data.total_words})`);
console.log(` Avg Effort to core: ${data.average_effort.toFixed(3)}`);
});

// Highlight specific low/high effort items
if (vocab.low_effort_words.length > 0) {
const top3 = vocab.low_effort_words.slice(0, 3).map((w: any) => `${w.word} (${w.effort.toFixed(2)})`).join(', ');
console.log(`\n ✨ Most Accessible Words: ${top3}`);
}
}

runBenchmark().catch(err => {
console.error(`FATAL ERROR: ${err.message}`);
process.exit(1);
});
46 changes: 46 additions & 0 deletions src/core/treeStructure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,27 @@ export enum AACSemanticIntent {
PLATFORM_SPECIFIC = 'PLATFORM_SPECIFIC',
}

/**
* Scanning types for accessibility
*/
export enum AACScanType {
LINEAR = 'linear', // Left-to-right, top-to-bottom
ROW_COLUMN = 'row-column', // Scan rows, then columns
COLUMN_ROW = 'column-row', // Scan columns, then rows
BLOCK_ROW_COLUMN = 'block-row-column', // Scan blocks, then rows, then columns
BLOCK_COLUMN_ROW = 'block-column-row', // Scan blocks, then columns, then rows
}

/**
* Configuration for a scan block
*/
export interface AACScanBlock {
id: number;
name?: string;
order?: number; // Sequence in scanning
scanType?: AACScanType; // Override scanning within this block
}

// New semantic action interface for cross-platform compatibility
export interface AACSemanticAction {
// Make category optional for backward-compat with older tests constructing minimal actions
Expand Down Expand Up @@ -140,7 +161,14 @@ export class AACButton implements IAACButton {
y?: number;
columnSpan?: number;
rowSpan?: number;
/**
* @deprecated Use scanBlock instead (singular, not array)
*/
scanBlocks?: number[];
/**
* Scan block number (1-8) for block scanning
*/
scanBlock?: number;
visibility?: 'Visible' | 'Hidden' | 'Disabled' | 'PointerAndTouchOnly' | 'Empty';
directActivate?: boolean;
audioDescription?: string;
Expand Down Expand Up @@ -168,6 +196,7 @@ export class AACButton implements IAACButton {
columnSpan,
rowSpan,
scanBlocks,
scanBlock,
visibility,
directActivate,
parameters,
Expand Down Expand Up @@ -200,6 +229,7 @@ export class AACButton implements IAACButton {
columnSpan?: number;
rowSpan?: number;
scanBlocks?: number[];
scanBlock?: number;
visibility?: 'Visible' | 'Hidden' | 'Disabled' | 'PointerAndTouchOnly' | 'Empty';
directActivate?: boolean;
parameters?: { [key: string]: any };
Expand Down Expand Up @@ -231,6 +261,7 @@ export class AACButton implements IAACButton {
this.columnSpan = columnSpan;
this.rowSpan = rowSpan;
this.scanBlocks = scanBlocks;
this.scanBlock = scanBlock;
this.visibility = visibility;
this.directActivate = directActivate;
this.parameters = parameters;
Expand Down Expand Up @@ -333,6 +364,12 @@ export class AACPage implements IAACPage {
// Metrics support: Track semantic/clone IDs used on this page
semantic_ids?: string[];
clone_ids?: string[];
// Scanning configuration for this page
scanningConfig?: import('../types/aac').ScanningConfig;

// Scanning support
scanType?: AACScanType;
scanBlocksConfig?: AACScanBlock[];

constructor({
id,
Expand All @@ -347,6 +384,9 @@ export class AACPage implements IAACPage {
sounds,
semantic_ids,
clone_ids,
scanningConfig,
scanBlocksConfig,
scanType,
}: {
id: string;
name?: string;
Expand All @@ -360,6 +400,9 @@ export class AACPage implements IAACPage {
sounds?: any[];
semantic_ids?: string[];
clone_ids?: string[];
scanningConfig?: import('../types/aac').ScanningConfig;
scanBlocksConfig?: AACScanBlock[];
scanType?: AACScanType;
}) {
this.id = id;
this.name = name;
Expand All @@ -381,6 +424,9 @@ export class AACPage implements IAACPage {
this.sounds = sounds;
this.semantic_ids = semantic_ids;
this.clone_ids = clone_ids;
this.scanningConfig = scanningConfig;
this.scanBlocksConfig = scanBlocksConfig;
this.scanType = scanType;
}

addButton(button: AACButton): void {
Expand Down
33 changes: 32 additions & 1 deletion src/optional/analytics/docs/AAC_METRICS_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@ The algorithm rewards systems that support motor planning. If a button appears i
- **Semantic Locations**: Patterns where types of words (e.g., verbs, colors) always appear in the same grid area.
- **Upstream Matching**: If the button used to _enter_ a board matches the ID of a button _on_ that board, the effort is reduced.

### Switch Scanning Support

For users who use scanning access methods, the implementation supports evaluating scanning effort. Instead of movement distance, the algorithm calculates:
1. **Scan Steps**: The number of highlight movements (items or groups) to reach the target.
2. **Selections**: The number of switch activations required.

Supported scanning types include `linear`, `row-column`, `column-row`, and `block-based` scanning across Grid 3, TD Snap, and TouchChat.

> 📖 **Detailed Guide**: For comprehensive information on scanning metrics across different platforms, see [SCANNING_METRICS_GUIDE.md](./SCANNING_METRICS_GUIDE.md).

---

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

- **Supported Formats**: Any format with a corresponding processor (`.obf`, `.obz`, `.obfset`, `.gridset`, `.pageSet`, `.spb`, `.zip` for TouchChat, etc.).
- **Metadata**: For best accuracy, buttons should include `clone_id` or `semantic_id` where applicable.
- **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.

### 4. Configuring Scanning

You can evaluate scanning efficiency by setting the `scanType` on `AACPage` objects:

```typescript
import { AACScanType } from '@willwade/aac-processors';

// Set scanning behavior for a page
page.scanType = AACScanType.ROW_COLUMN;

// Or using blocks
page.scanType = AACScanType.BLOCK_ROW_COLUMN;
page.scanBlocksConfig = [
{ id: 1, name: 'Main', order: 1 },
{ id: 2, name: 'Numbers', order: 2 }
];

// Assign buttons to blocks
button.scanBlocks = [2];
```

---

Expand Down
Loading
Loading