@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.
@@ -12,6 +12,17 @@ const path_1 = __importDefault(require("path"));
12
12
  const fs_1 = __importDefault(require("fs"));
13
13
  const crypto_1 = __importDefault(require("crypto"));
14
14
  const snapValidator_1 = require("../validation/snapValidator");
15
+ /**
16
+ * Map Snap Visible value to AAC standard visibility
17
+ * Snap: 0 = hidden, 1 (or non-zero) = visible
18
+ * Maps to: 'Hidden' | 'Visible' | undefined
19
+ */
20
+ function mapSnapVisibility(visible) {
21
+ if (visible === null || visible === undefined) {
22
+ return undefined; // Default to visible
23
+ }
24
+ return visible === 0 ? 'Hidden' : 'Visible';
25
+ }
15
26
  class SnapProcessor extends baseProcessor_1.BaseProcessor {
16
27
  constructor(symbolResolver = null, options = {}) {
17
28
  super(options);
@@ -63,6 +74,40 @@ class SnapProcessor extends baseProcessor_1.BaseProcessor {
63
74
  };
64
75
  // Load pages first, using UniqueId as canonical id
65
76
  const pages = db.prepare('SELECT * FROM Page').all();
77
+ // Load PageSetProperties to find default Keyboard and Home pages
78
+ let defaultKeyboardPageId;
79
+ let defaultHomePageId;
80
+ let dashboardPageId;
81
+ try {
82
+ const properties = db.prepare('SELECT * FROM PageSetProperties').get();
83
+ if (properties) {
84
+ defaultKeyboardPageId = properties.DefaultKeyboardPageUniqueId;
85
+ defaultHomePageId = properties.DefaultHomePageUniqueId;
86
+ dashboardPageId = properties.DashboardUniqueId;
87
+ const toolbarId = properties.ToolBarUniqueId;
88
+ const hasGlobalToolbar = toolbarId && toolbarId !== '00000000-0000-0000-0000-000000000000';
89
+ if (hasGlobalToolbar) {
90
+ tree.rootId = toolbarId;
91
+ }
92
+ else if (defaultHomePageId) {
93
+ tree.rootId = defaultHomePageId;
94
+ }
95
+ }
96
+ }
97
+ catch (e) {
98
+ console.warn('[SnapProcessor] Failed to load PageSetProperties:', e);
99
+ }
100
+ // If still no root, look for a page titled "Tool Bar" or similar
101
+ if (!tree.rootId || tree.rootId === defaultHomePageId) {
102
+ const toolbarPage = pages.find((p) => p.Title === 'Tool Bar' || p.Name === 'Tool Bar');
103
+ if (toolbarPage) {
104
+ tree.rootId = String(toolbarPage.UniqueId || toolbarPage.Id);
105
+ }
106
+ }
107
+ // If still no root, fallback to first page
108
+ if (!tree.rootId && pages.length > 0) {
109
+ tree.rootId = String(pages[0].UniqueId || pages[0].Id);
110
+ }
66
111
  // Map from numeric Id -> UniqueId for later lookup
67
112
  const idToUniqueId = {};
68
113
  pages.forEach((pageRow) => {
@@ -219,6 +264,7 @@ class SnapProcessor extends baseProcessor_1.BaseProcessor {
219
264
  buttonColumns.has('LabelColor') ? 'b.LabelColor' : 'NULL AS LabelColor',
220
265
  buttonColumns.has('BackgroundColor') ? 'b.BackgroundColor' : 'NULL AS BackgroundColor',
221
266
  buttonColumns.has('NavigatePageId') ? 'b.NavigatePageId' : 'NULL AS NavigatePageId',
267
+ buttonColumns.has('ContentType') ? 'b.ContentType' : 'NULL AS ContentType',
222
268
  ];
223
269
  if (this.loadAudio) {
224
270
  selectFields.push(buttonColumns.has('MessageRecordingId')
@@ -232,14 +278,30 @@ class SnapProcessor extends baseProcessor_1.BaseProcessor {
232
278
  : 'NULL AS SerializedMessageSoundMetadata');
233
279
  }
234
280
  const placementColumns = getTableColumns('ElementPlacement');
235
- selectFields.push(placementColumns.has('GridPosition') ? 'ep.GridPosition' : 'NULL AS GridPosition', placementColumns.has('PageLayoutId') ? 'ep.PageLayoutId' : 'NULL AS PageLayoutId', 'er.PageId as ButtonPageId');
281
+ const hasButtonPageLink = getTableColumns('ButtonPageLink').size > 0;
282
+ selectFields.push(placementColumns.has('GridPosition') ? 'ep.GridPosition' : 'NULL AS GridPosition', placementColumns.has('PageLayoutId') ? 'ep.PageLayoutId' : 'NULL AS PageLayoutId', placementColumns.has('Visible') ? 'ep.Visible' : 'NULL AS Visible', 'er.PageId as ButtonPageId');
283
+ if (hasButtonPageLink) {
284
+ selectFields.push('bpl.PageUniqueId AS LinkedPageUniqueId');
285
+ }
286
+ else {
287
+ selectFields.push('NULL AS LinkedPageUniqueId');
288
+ }
289
+ const hasCommandSequence = getTableColumns('CommandSequence').size > 0;
290
+ if (hasCommandSequence) {
291
+ selectFields.push('cs.SerializedCommands');
292
+ }
293
+ else {
294
+ selectFields.push('NULL AS SerializedCommands');
295
+ }
236
296
  const buttonQuery = `
237
- SELECT ${selectFields.join(', ')}
238
- FROM Button b
239
- INNER JOIN ElementReference er ON b.ElementReferenceId = er.Id
240
- LEFT JOIN ElementPlacement ep ON ep.ElementReferenceId = er.Id
241
- WHERE er.PageId = ? ${selectedPageLayoutId ? 'AND ep.PageLayoutId = ?' : ''}
242
- `;
297
+ SELECT ${selectFields.join(', ')}
298
+ FROM Button b
299
+ INNER JOIN ElementReference er ON b.ElementReferenceId = er.Id
300
+ LEFT JOIN ElementPlacement ep ON ep.ElementReferenceId = er.Id
301
+ ${hasButtonPageLink ? 'LEFT JOIN ButtonPageLink bpl ON b.Id = bpl.ButtonId' : ''}
302
+ ${hasCommandSequence ? 'LEFT JOIN CommandSequence cs ON b.Id = cs.ButtonId' : ''}
303
+ WHERE er.PageId = ? ${selectedPageLayoutId ? 'AND ep.PageLayoutId = ?' : ''}
304
+ `;
243
305
  const queryParams = selectedPageLayoutId
244
306
  ? [pageRow.Id, selectedPageLayoutId]
245
307
  : [pageRow.Id];
@@ -279,9 +341,40 @@ class SnapProcessor extends baseProcessor_1.BaseProcessor {
279
341
  if (btnRow.NavigatePageId && idToUniqueId[String(btnRow.NavigatePageId)]) {
280
342
  targetPageUniqueId = idToUniqueId[String(btnRow.NavigatePageId)];
281
343
  }
344
+ else if (btnRow.LinkedPageUniqueId) {
345
+ targetPageUniqueId = String(btnRow.LinkedPageUniqueId);
346
+ }
282
347
  else if (btnRow.PageUniqueId) {
283
348
  targetPageUniqueId = String(btnRow.PageUniqueId);
284
349
  }
350
+ // Parse CommandSequence for navigation targets if not found yet
351
+ if (btnRow.SerializedCommands) {
352
+ try {
353
+ const commands = JSON.parse(btnRow.SerializedCommands);
354
+ const values = commands.$values || [];
355
+ for (const cmd of values) {
356
+ if (cmd.$type === '2' && cmd.LinkedPageId) {
357
+ // Normal Navigation
358
+ targetPageUniqueId = String(cmd.LinkedPageId);
359
+ }
360
+ else if (cmd.$type === '16') {
361
+ // Go to Home
362
+ targetPageUniqueId = defaultHomePageId;
363
+ }
364
+ else if (cmd.$type === '17') {
365
+ // Go to Keyboard
366
+ targetPageUniqueId = defaultKeyboardPageId;
367
+ }
368
+ else if (cmd.$type === '18') {
369
+ // Go to Dashboard
370
+ targetPageUniqueId = dashboardPageId;
371
+ }
372
+ }
373
+ }
374
+ catch (e) {
375
+ // Ignore JSON parse errors in commands
376
+ }
377
+ }
285
378
  // Determine parent page association for this button
286
379
  const parentPageId = btnRow.ButtonPageId ? String(btnRow.ButtonPageId) : undefined;
287
380
  const parentUniqueId = parentPageId && idToUniqueId[parentPageId] ? idToUniqueId[parentPageId] : uniqueId;
@@ -344,11 +437,14 @@ class SnapProcessor extends baseProcessor_1.BaseProcessor {
344
437
  }
345
438
  const button = new treeStructure_1.AACButton({
346
439
  id: String(btnRow.Id),
347
- label: btnRow.Label || '',
348
- message: btnRow.Message || btnRow.Label || '',
440
+ label: btnRow.Label || (btnRow.ContentType === 1 ? '[Prediction]' : ''),
441
+ message: btnRow.Message || (btnRow.ContentType === 1 ? '[Prediction]' : btnRow.Label || ''),
349
442
  targetPageId: targetPageUniqueId,
350
443
  semanticAction: semanticAction,
444
+ contentType: btnRow.ContentType === 1 ? 'AutoContent' : undefined,
445
+ contentSubType: btnRow.ContentType === 1 ? 'Prediction' : undefined,
351
446
  audioRecording: audioRecording,
447
+ visibility: mapSnapVisibility(btnRow.Visible),
352
448
  semantic_id: btnRow.LibrarySymbolId
353
449
  ? `snap_symbol_${btnRow.LibrarySymbolId}`
354
450
  : undefined, // Extract semantic_id from LibrarySymbolId
@@ -85,8 +85,20 @@ class TouchChatProcessor extends baseProcessor_1.BaseProcessor {
85
85
  const tree = new treeStructure_1.AACTree();
86
86
  // Set root ID to the first page ID (will be updated if we find a better root)
87
87
  let rootPageId = null;
88
+ const getTableColumns = (tableName) => {
89
+ if (!db)
90
+ return new Set();
91
+ try {
92
+ const rows = db.prepare(`PRAGMA table_info(${tableName})`).all();
93
+ return new Set(rows.map((row) => row.name));
94
+ }
95
+ catch {
96
+ return new Set();
97
+ }
98
+ };
88
99
  // Load ID mappings first
89
100
  const idMappings = new Map();
101
+ const numericToRid = new Map();
90
102
  try {
91
103
  const mappingQuery = 'SELECT numeric_id, string_id FROM page_id_mapping';
92
104
  const mappings = db.prepare(mappingQuery).all();
@@ -116,15 +128,18 @@ class TouchChatProcessor extends baseProcessor_1.BaseProcessor {
116
128
  // console.log('No styles found:', e);
117
129
  }
118
130
  // First, load all pages and get their names from resources
131
+ const resourceColumns = getTableColumns('resources');
132
+ const hasRid = resourceColumns.has('rid');
119
133
  const pageQuery = `
120
- SELECT p.*, r.name
134
+ SELECT p.*, r.name${hasRid ? ', r.rid' : ''}
121
135
  FROM pages p
122
136
  JOIN resources r ON r.id = p.resource_id
123
137
  `;
124
138
  const pages = db.prepare(pageQuery).all();
125
139
  pages.forEach((pageRow) => {
126
- // Use mapped string ID if available, otherwise use numeric ID as string
127
- const pageId = idMappings.get(pageRow.id) || String(pageRow.id);
140
+ // Use resource RID (UUID) if available, otherwise mapped string ID, then numeric ID
141
+ const pageId = (hasRid ? pageRow.rid : null) || idMappings.get(pageRow.id) || String(pageRow.id);
142
+ numericToRid.set(pageRow.id, pageId);
128
143
  const style = pageStyles.get(pageRow.page_style_id);
129
144
  const page = new treeStructure_1.AACPage({
130
145
  id: pageId,
@@ -166,6 +181,7 @@ class TouchChatProcessor extends baseProcessor_1.BaseProcessor {
166
181
  touchChat: {
167
182
  actionCode: 0, // Default speak action
168
183
  actionData: cell.message || cell.label || '',
184
+ resourceId: cell.resource_id,
169
185
  },
170
186
  },
171
187
  fallback: {
@@ -210,7 +226,7 @@ class TouchChatProcessor extends baseProcessor_1.BaseProcessor {
210
226
  const pageGrids = new Map();
211
227
  boxInstances.forEach((instance) => {
212
228
  // Use mapped string ID if available, otherwise use numeric ID as string
213
- const pageId = idMappings.get(instance.page_id) || String(instance.page_id);
229
+ const pageId = numericToRid.get(instance.page_id) || String(instance.page_id);
214
230
  const page = tree.getPage(pageId);
215
231
  const buttons = buttonBoxes.get(instance.button_box_id);
216
232
  if (page && buttons) {
@@ -341,7 +357,7 @@ class TouchChatProcessor extends baseProcessor_1.BaseProcessor {
341
357
  },
342
358
  });
343
359
  // Find the page that references this resource
344
- const page = Object.values(tree.pages).find((p) => p.id === String(btnRow.id));
360
+ const page = Object.values(tree.pages).find((p) => p.id === (numericToRid.get(btnRow.id) || String(btnRow.id)));
345
361
  if (page)
346
362
  page.addButton(button);
347
363
  });
@@ -351,23 +367,27 @@ class TouchChatProcessor extends baseProcessor_1.BaseProcessor {
351
367
  }
352
368
  // Load navigation actions
353
369
  const navActionsQuery = `
354
- SELECT b.id as button_id, ad.value as target_page_id
355
- FROM buttons b
356
- JOIN actions a ON a.resource_id = b.resource_id
370
+ SELECT a.resource_id, COALESCE(${hasRid ? 'r_rid.rid, r_id.rid, ' : ''}r_id.id, ad.value) as target_page_id
371
+ FROM actions a
357
372
  JOIN action_data ad ON ad.action_id = a.id
358
- WHERE a.code = 1
373
+ ${hasRid ? 'LEFT JOIN resources r_rid ON r_rid.rid = ad.value AND r_rid.type = 7' : ''}
374
+ LEFT JOIN resources r_id ON (CASE WHEN ad.value GLOB '[0-9]*' THEN CAST(ad.value AS INTEGER) ELSE -1 END) = r_id.id AND r_id.type = 7
375
+ WHERE a.code IN (1, 8, 9)
359
376
  `;
360
377
  try {
361
378
  const navActions = db.prepare(navActionsQuery).all();
362
379
  navActions.forEach((nav) => {
363
- // Find button in any page
380
+ // Find button in any page by its resourceId
364
381
  for (const pageId in tree.pages) {
365
382
  const page = tree.pages[pageId];
366
- const button = page.buttons.find((b) => b.id === String(nav.button_id));
383
+ const button = page.buttons.find((b) => b.semanticAction?.platformData?.touchChat?.resourceId === nav.resource_id);
367
384
  if (button) {
368
385
  // Use mapped string ID for target page if available
369
- const targetPageId = idMappings.get(parseInt(nav.target_page_id)) || nav.target_page_id;
370
- button.targetPageId = String(targetPageId);
386
+ const numericTargetId = parseInt(String(nav.target_page_id));
387
+ const targetPageId = !isNaN(numericTargetId)
388
+ ? numericToRid.get(numericTargetId) || String(numericTargetId)
389
+ : String(nav.target_page_id);
390
+ button.targetPageId = targetPageId;
371
391
  // Create semantic action for navigation
372
392
  button.semanticAction = {
373
393
  category: treeStructure_1.AACSemanticCategory.NAVIGATION,
@@ -4,7 +4,7 @@ import { AACSemanticAction } from '../core/treeStructure';
4
4
  * Determines how the scanning advances through items
5
5
  */
6
6
  export declare enum ScanningSelectionMethod {
7
- /** Automatically advance through items at timed intervals */
7
+ /** Automatically advance through items at timed intervals (1 Switch) */
8
8
  AutoScan = "AutoScan",
9
9
  /** Automatic scanning with overscan (two-stage scanning) */
10
10
  AutoScanWithOverscan = "AutoScanWithOverscan",
@@ -12,8 +12,12 @@ export declare enum ScanningSelectionMethod {
12
12
  HoldToAdvance = "HoldToAdvance",
13
13
  /** Hold to advance with overscan */
14
14
  HoldToAdvanceWithOverscan = "HoldToAdvanceWithOverscan",
15
- /** Tap switch to advance, tap again to select */
16
- TapToAdvance = "TapToAdvance"
15
+ /** Tap switch to advance, tap again to select (Automatic) */
16
+ TapToAdvance = "TapToAdvance",
17
+ /** Tap switch to advance, another switch to select (2 Switch Step Scan) */
18
+ StepScan2Switch = "StepScan2Switch",
19
+ /** Tap switch 1 to advance, tap switch 1 again to select (1 Switch Step Scan) */
20
+ StepScan1Switch = "StepScan1Switch"
17
21
  }
18
22
  /**
19
23
  * Cell scanning order patterns
@@ -48,6 +52,12 @@ export interface ScanningConfig {
48
52
  dwellTime?: number;
49
53
  /** How the selection is accepted */
50
54
  acceptScanMethod?: 'Switch' | 'Timeout' | 'Hold';
55
+ /** Whether to factor in error correction effort (e.g., missed hits) */
56
+ errorCorrectionEnabled?: boolean;
57
+ /** Maximum number of loops before the scan times out */
58
+ maxLoops?: number;
59
+ /** Estimated probability of missing a target (0.0 to 1.0) */
60
+ errorRate?: number;
51
61
  }
52
62
  export interface AACStyle {
53
63
  backgroundColor?: string;
package/dist/types/aac.js CHANGED
@@ -7,7 +7,7 @@ exports.CellScanningOrder = exports.ScanningSelectionMethod = void 0;
7
7
  */
8
8
  var ScanningSelectionMethod;
9
9
  (function (ScanningSelectionMethod) {
10
- /** Automatically advance through items at timed intervals */
10
+ /** Automatically advance through items at timed intervals (1 Switch) */
11
11
  ScanningSelectionMethod["AutoScan"] = "AutoScan";
12
12
  /** Automatic scanning with overscan (two-stage scanning) */
13
13
  ScanningSelectionMethod["AutoScanWithOverscan"] = "AutoScanWithOverscan";
@@ -15,8 +15,12 @@ var ScanningSelectionMethod;
15
15
  ScanningSelectionMethod["HoldToAdvance"] = "HoldToAdvance";
16
16
  /** Hold to advance with overscan */
17
17
  ScanningSelectionMethod["HoldToAdvanceWithOverscan"] = "HoldToAdvanceWithOverscan";
18
- /** Tap switch to advance, tap again to select */
18
+ /** Tap switch to advance, tap again to select (Automatic) */
19
19
  ScanningSelectionMethod["TapToAdvance"] = "TapToAdvance";
20
+ /** Tap switch to advance, another switch to select (2 Switch Step Scan) */
21
+ ScanningSelectionMethod["StepScan2Switch"] = "StepScan2Switch";
22
+ /** Tap switch 1 to advance, tap switch 1 again to select (1 Switch Step Scan) */
23
+ ScanningSelectionMethod["StepScan1Switch"] = "StepScan1Switch";
20
24
  })(ScanningSelectionMethod || (exports.ScanningSelectionMethod = ScanningSelectionMethod = {}));
21
25
  /**
22
26
  * Cell scanning order patterns
@@ -5,18 +5,20 @@
5
5
  * analyze vocabulary differences, and generate CARE component scores.
6
6
  */
7
7
  import { MetricsResult, ComparisonResult } from './types';
8
+ import { MetricsOptions } from './types';
8
9
  export declare class ComparisonAnalyzer {
9
10
  private vocabAnalyzer;
10
11
  private sentenceAnalyzer;
11
12
  private referenceLoader;
12
13
  constructor();
14
+ private normalize;
13
15
  /**
14
16
  * Compare two board sets
15
17
  */
16
18
  compare(targetResult: MetricsResult, compareResult: MetricsResult, options?: {
17
19
  includeSentences?: boolean;
18
20
  locale?: string;
19
- }): ComparisonResult;
21
+ } & Partial<MetricsOptions>): ComparisonResult;
20
22
  /**
21
23
  * Calculate CARE component scores
22
24
  */