Skip to content

Commit ce6a026

Browse files
committed
Add Open Board Logging (OBL) support and utilities
Introduces OBL file format types, parsing, generation, and anonymization utilities for AAC usage logs. Updates analytics and history modules to support OBL-aligned fields and semantic intent mapping. Adds documentation and analysis scripts for OBL, extends history reading for Grid 3 and TD Snap to include semantic fields, and provides comprehensive tests for OBL functionality.
1 parent 8681ca2 commit ce6a026

11 files changed

Lines changed: 837 additions & 3 deletions

File tree

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import * as fs from 'fs';
2+
import * as path from 'path';
3+
import { OblUtil } from '../../src/optional/analytics/index';
4+
5+
/**
6+
* Script to bulk-analyze OBLA clinical data and extract utterances to a CSV.
7+
*/
8+
9+
const OBLA_DIR = path.join(__dirname, '../../obla-improvements/small-obla/small');
10+
const OUTPUT_CSV = path.join(__dirname, 'obl_utterances.csv');
11+
12+
interface UtteranceRecord {
13+
file: string;
14+
userId: string;
15+
timestamp: string;
16+
type: string;
17+
content: string;
18+
boardId?: string;
19+
}
20+
21+
function run() {
22+
console.log(`Analyzing OBLA data in: ${OBLA_DIR}...`);
23+
24+
if (!fs.existsSync(OBLA_DIR)) {
25+
console.error(`Directory not found: ${OBLA_DIR}`);
26+
process.exit(1);
27+
}
28+
29+
const files = fs.readdirSync(OBLA_DIR).filter(f => f.endsWith('.obla'));
30+
console.log(`Found ${files.length} files.`);
31+
32+
const records: any[] = [];
33+
34+
for (const file of files) {
35+
try {
36+
const content = fs.readFileSync(path.join(OBLA_DIR, file), 'utf8');
37+
const obl = OblUtil.parse(content);
38+
39+
for (const session of obl.sessions) {
40+
let currentSentence: string[] = [];
41+
let sentenceStartTime: string | null = null;
42+
43+
// Sort events within session by timestamp to be sure
44+
const sortedEvents = [...session.events].sort((a, b) => a.timestamp.localeCompare(b.timestamp));
45+
46+
for (let i = 0; i < sortedEvents.length; i++) {
47+
const event = sortedEvents[i];
48+
const nextEvent = sortedEvents[i + 1];
49+
50+
if (!sentenceStartTime) sentenceStartTime = event.timestamp;
51+
52+
let text = '';
53+
let isBoundary = false;
54+
55+
if (event.type === 'button') {
56+
text = (event as any).label || (event as any).vocalization || '[?]';
57+
} else if (event.type === 'utterance') {
58+
text = (event as any).text;
59+
isBoundary = true; // Utterances are usually complete sentences
60+
} else if (event.type === 'action') {
61+
const action = (event as any).action;
62+
if (action === ':clear' || action === ':speak' || action === ':home') {
63+
isBoundary = true;
64+
}
65+
if (action === ':backspace') {
66+
currentSentence.pop();
67+
} else {
68+
text = `[${action}]`;
69+
}
70+
}
71+
72+
if (text) currentSentence.push(text);
73+
74+
// Check for time gap boundary (> 15 seconds)
75+
if (nextEvent) {
76+
const currentMs = new Date(event.timestamp).getTime();
77+
const nextMs = new Date(nextEvent.timestamp).getTime();
78+
if (nextMs - currentMs > 15000) isBoundary = true;
79+
} else {
80+
isBoundary = true; // End of session
81+
}
82+
83+
if (isBoundary && currentSentence.length > 0) {
84+
records.push({
85+
timestamp: sentenceStartTime,
86+
userId: obl.user_id,
87+
sentence: currentSentence.join(' '),
88+
file: file
89+
});
90+
currentSentence = [];
91+
sentenceStartTime = null;
92+
}
93+
}
94+
}
95+
} catch (err) {
96+
console.error(`Error processing ${file}:`, err);
97+
}
98+
}
99+
100+
// Sort by timestamp
101+
records.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
102+
103+
// Write CSV
104+
const header = 'Timestamp,User ID,Sentence,File\n';
105+
const csvLines = records.map(r => {
106+
const escapedSentence = `"${r.sentence.replace(/"/g, '""')}"`;
107+
return `${r.timestamp},${r.userId},${escapedSentence},${r.file}`;
108+
});
109+
110+
fs.writeFileSync(OUTPUT_CSV, header + csvLines.join('\n'));
111+
112+
console.log(`\nAnalysis complete!`);
113+
console.log(`Total sentences reconstructed: ${records.length}`);
114+
console.log(`Results saved to: ${OUTPUT_CSV}`);
115+
116+
// Show a preview
117+
console.log('\nPreview (10 Reconstructed Sentences):');
118+
const preview = records.filter(r => r.sentence.length > 5 && !r.sentence.includes('000')).slice(0, 20);
119+
preview.forEach(r => {
120+
console.log(`[${r.timestamp}] ${r.sentence}`);
121+
});
122+
}
123+
124+
run();

src/optional/analytics/docs/AAC_METRICS_GUIDE.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,14 @@ page.scanBlocksConfig = [
111111
button.scanBlocks = [2];
112112
```
113113

114+
### 5. Unified History & OBL Support
115+
116+
This implementation supports gathering and analyzing historical usage data from multiple sources:
117+
- **Local History**: Automatically discover and read logs from Grid 3 and TD Snap on the local machine.
118+
- **OBL Format**: Support for the [Open Board Logging (OBL)](./OBL_SUPPORT_GUIDE.md) standard for cross-platform log sharing.
119+
120+
See [OBL_SUPPORT_GUIDE.md](./OBL_SUPPORT_GUIDE.md) for details on parsing and anonymizing shared AAC logs.
121+
114122
---
115123

116124
## ⚖️ Implementation vs. Original Algorithm
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Open Board Logging (OBL) Support
2+
3+
This repository provides full support for the **.obl (Open Board Logging)** and **.obla (Anonymized OBL)** file formats. These formats are designed to standardize AAC usage logs across different platforms while protecting user privacy.
4+
5+
## 📁 Format Overview
6+
7+
- **.obl**: Standard JSON-based log containing sessions, events, and metadata (timestamps, names, geolocation).
8+
- **.obla**: Anonymized version where sensitive data is masked or transformed according to standardized protocols.
9+
10+
## 🚀 Key Features
11+
12+
1. **Bidirectional Conversion**: Seamlessly convert between OBL and the internal `HistoryEntry` format used by the analytics module.
13+
2. **Semantic Mapping**: Automatically maps unified `AACSemanticIntent` types to OBL-standard actions like `:home`, `:back`, and `:open_board`.
14+
3. **Privacy Suite**: Built-in support for OBL anonymization protocols.
15+
4. **Header Handling**: Correctly handles the recommended `/* NOTICE */` header in OBL files.
16+
17+
## 💻 How to Use
18+
19+
### Parsing and Generation
20+
21+
```typescript
22+
import { OblUtil } from '@willwade/aac-processors/optional/analytics';
23+
24+
// Parse an OBL file
25+
const content = fs.readFileSync('user_log.obl', 'utf8');
26+
const obl = OblUtil.parse(content);
27+
28+
// Convert to internal history format for analysis
29+
const history = OblUtil.toHistoryEntries(obl);
30+
31+
// Convert history back to OBL for sharing
32+
const newObl = OblUtil.fromHistoryEntries(history, 'patient_123');
33+
const json = OblUtil.stringify(newObl);
34+
```
35+
36+
### Anonymization
37+
38+
The `OblAnonymizer` allows you to selectively apply privacy protocols:
39+
40+
```typescript
41+
import { OblAnonymizer } from '@willwade/aac-processors/optional/analytics';
42+
43+
const anonymized = OblAnonymizer.anonymize(obl, [
44+
'timestamp_shift', // Shift logs to begin on 2000-01-01
45+
'geolocation_masking', // Remove GPS and location IDs
46+
'name_masking', // Redact user and author names
47+
'url_stripping' // Remove links to images or author profiles
48+
]);
49+
```
50+
51+
## 🧠 Semantic Intent Mapping
52+
53+
The OBL implementation leverages the `AACSemanticAction` system. When exporting data to OBL, the following intents are automatically mapped to standard OBL actions:
54+
55+
| Intent | OBL Action String |
56+
| :--- | :--- |
57+
| `NAVIGATE_TO` | `:open_board` |
58+
| `GO_HOME` | `:home` |
59+
| `GO_BACK` | `:back` |
60+
| `CLEAR_TEXT` | `:clear` |
61+
| `DELETE_CHARACTER` | `:backspace` |
62+
| `SPEAK_TEXT` | `:speak` (if not a standard utterance) |
63+
64+
## 📊 Analyzing OBL Data
65+
66+
You can use the OBL utilities to feed external data into the `MetricsCalculator` or other vocabulary analysis tools by converting it to `HistoryEntry` format first.
67+
68+
See `scripts/analysis/analyze_obl_data.ts` for an example of bulk-extracting utterances from a clinical dataset.

src/optional/analytics/history.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@ import {
1212
readSnapUsageForUser as readSnapUsageForUserImpl,
1313
SnapUserInfo,
1414
} from '../../processors/snap/helpers';
15+
import { AACSemanticCategory, AACSemanticIntent } from '../../core/treeStructure';
1516

16-
export type HistorySource = 'Grid' | 'Snap';
17+
export type HistorySource = 'Grid' | 'Snap' | 'OBL' | string;
1718

1819
export interface HistoryOccurrence {
1920
timestamp: Date;
@@ -22,13 +23,25 @@ export interface HistoryOccurrence {
2223
modeling?: boolean;
2324
accessMethod?: number | null;
2425
pageId?: string | null;
26+
// OBL-aligned fields
27+
buttonId?: string | null;
28+
boardId?: string | null;
29+
spoken?: boolean;
30+
vocalization?: string;
31+
imageUrl?: string;
32+
actions?: any[]; // For OBL actions
33+
type?: 'button' | 'action' | 'utterance' | 'note' | 'other';
34+
// Semantic semantic alignment
35+
intent?: AACSemanticIntent | string;
36+
category?: AACSemanticCategory;
2537
}
2638

2739
export interface HistoryPlatformExtras {
2840
label?: string;
2941
message?: string;
3042
buttonId?: string;
3143
contentXml?: string;
44+
[key: string]: any;
3245
}
3346

3447
export interface HistoryEntry {

src/optional/analytics/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ export * from './utils/idGenerator';
2121
// Export history functionality
2222
export * from './history';
2323

24+
// Export OBL logging support
25+
export * from './metrics/obl-types';
26+
export { OblUtil, OblAnonymizer } from './metrics/obl';
27+
2428
// Export core metrics calculator
2529
export { MetricsCalculator } from './metrics/core';
2630

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/**
2+
* .obl (Open Board Logging) File Format Types
3+
*
4+
* Based on the .obl specification for AAC logging.
5+
*/
6+
7+
export interface OblAction {
8+
action: string;
9+
destination_board_id?: string;
10+
text?: string;
11+
modification_type?: string;
12+
[key: string]: any; // Support for extensions
13+
}
14+
15+
export interface OblEventBase {
16+
id: string;
17+
timestamp: string; // ISO 8601
18+
type: 'button' | 'action' | 'utterance' | 'note' | 'other' | string;
19+
locale?: string;
20+
geo?: [number, number, number?]; // lat, long, alt
21+
location_id?: string;
22+
modeling?: boolean;
23+
system?: string;
24+
window_width?: number;
25+
window_height?: number;
26+
percent_x?: number;
27+
percent_y?: number;
28+
[key: string]: any; // Support for extensions
29+
}
30+
31+
export interface OblButtonEvent extends OblEventBase {
32+
type: 'button';
33+
label: string;
34+
spoken: boolean;
35+
button_id?: string;
36+
board_id?: string;
37+
vocalization?: string;
38+
image_url?: string;
39+
actions?: OblAction[];
40+
}
41+
42+
export interface OblActionEvent extends OblEventBase {
43+
type: 'action';
44+
action: string;
45+
destination_board_id?: string;
46+
text?: string;
47+
modification_type?: string;
48+
}
49+
50+
export interface OblUtteranceEvent extends OblEventBase {
51+
type: 'utterance';
52+
text: string;
53+
buttons?: Array<{
54+
id?: string;
55+
label?: string;
56+
board_id?: string;
57+
vocalization?: string;
58+
action?: string;
59+
text?: string;
60+
}>;
61+
}
62+
63+
export interface OblNoteEvent extends OblEventBase {
64+
type: 'note';
65+
text: string;
66+
author_name?: string;
67+
author_email?: string;
68+
author_url?: string;
69+
}
70+
71+
export type OblEvent =
72+
| OblButtonEvent
73+
| OblActionEvent
74+
| OblUtteranceEvent
75+
| OblNoteEvent
76+
| OblEventBase;
77+
78+
export interface OblSession {
79+
id: string;
80+
type: 'log' | string;
81+
started: string; // ISO 8601
82+
ended: string; // ISO 8601
83+
device_id?: string;
84+
locale?: string;
85+
anonymizations?: string[];
86+
events: OblEvent[];
87+
[key: string]: any; // Support for extensions
88+
}
89+
90+
export interface OblFile {
91+
format: 'open-board-log-0.1' | string;
92+
user_id: string;
93+
user_name?: string;
94+
source?: string;
95+
locale?: string;
96+
anonymized?: boolean;
97+
license?: {
98+
type: string;
99+
copyright_notice_url?: string;
100+
source_url?: string;
101+
author_name?: string;
102+
author_url?: string;
103+
author_email?: string;
104+
};
105+
sessions: OblSession[];
106+
[key: string]: any; // Support for extensions
107+
}

0 commit comments

Comments
 (0)