@willwade/aac-processors 0.0.3

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.
Files changed (89) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +787 -0
  3. package/dist/cli/index.d.ts +2 -0
  4. package/dist/cli/index.js +189 -0
  5. package/dist/cli/prettyPrint.d.ts +2 -0
  6. package/dist/cli/prettyPrint.js +28 -0
  7. package/dist/core/analyze.d.ts +6 -0
  8. package/dist/core/analyze.js +49 -0
  9. package/dist/core/baseProcessor.d.ts +94 -0
  10. package/dist/core/baseProcessor.js +208 -0
  11. package/dist/core/fileProcessor.d.ts +7 -0
  12. package/dist/core/fileProcessor.js +51 -0
  13. package/dist/core/stringCasing.d.ts +37 -0
  14. package/dist/core/stringCasing.js +174 -0
  15. package/dist/core/treeStructure.d.ts +190 -0
  16. package/dist/core/treeStructure.js +223 -0
  17. package/dist/index.d.ts +23 -0
  18. package/dist/index.js +96 -0
  19. package/dist/optional/symbolTools.d.ts +28 -0
  20. package/dist/optional/symbolTools.js +126 -0
  21. package/dist/processors/applePanelsProcessor.d.ts +23 -0
  22. package/dist/processors/applePanelsProcessor.js +521 -0
  23. package/dist/processors/astericsGridProcessor.d.ts +49 -0
  24. package/dist/processors/astericsGridProcessor.js +1427 -0
  25. package/dist/processors/dotProcessor.d.ts +21 -0
  26. package/dist/processors/dotProcessor.js +191 -0
  27. package/dist/processors/excelProcessor.d.ts +145 -0
  28. package/dist/processors/excelProcessor.js +556 -0
  29. package/dist/processors/gridset/helpers.d.ts +4 -0
  30. package/dist/processors/gridset/helpers.js +48 -0
  31. package/dist/processors/gridset/resolver.d.ts +8 -0
  32. package/dist/processors/gridset/resolver.js +100 -0
  33. package/dist/processors/gridsetProcessor.d.ts +28 -0
  34. package/dist/processors/gridsetProcessor.js +1339 -0
  35. package/dist/processors/index.d.ts +14 -0
  36. package/dist/processors/index.js +42 -0
  37. package/dist/processors/obfProcessor.d.ts +21 -0
  38. package/dist/processors/obfProcessor.js +278 -0
  39. package/dist/processors/opmlProcessor.d.ts +21 -0
  40. package/dist/processors/opmlProcessor.js +235 -0
  41. package/dist/processors/snap/helpers.d.ts +4 -0
  42. package/dist/processors/snap/helpers.js +27 -0
  43. package/dist/processors/snapProcessor.d.ts +44 -0
  44. package/dist/processors/snapProcessor.js +586 -0
  45. package/dist/processors/touchchat/helpers.d.ts +4 -0
  46. package/dist/processors/touchchat/helpers.js +27 -0
  47. package/dist/processors/touchchatProcessor.d.ts +27 -0
  48. package/dist/processors/touchchatProcessor.js +768 -0
  49. package/dist/types/aac.d.ts +47 -0
  50. package/dist/types/aac.js +2 -0
  51. package/docs/.keep +1 -0
  52. package/docs/ApplePanels.md +309 -0
  53. package/docs/Grid3-XML-Format.md +1788 -0
  54. package/docs/TobiiDynavox-Snap-Details.md +394 -0
  55. package/docs/asterics-Grid-fileformat-details.md +443 -0
  56. package/docs/obf_.obz Open Board File Formats.md +432 -0
  57. package/docs/touchchat.md +520 -0
  58. package/examples/.coverage +0 -0
  59. package/examples/.keep +1 -0
  60. package/examples/README.md +31 -0
  61. package/examples/communikate.dot +2637 -0
  62. package/examples/demo.js +143 -0
  63. package/examples/example-images.gridset +0 -0
  64. package/examples/example.ce +0 -0
  65. package/examples/example.dot +14 -0
  66. package/examples/example.grd +1 -0
  67. package/examples/example.gridset +0 -0
  68. package/examples/example.obf +27 -0
  69. package/examples/example.obz +0 -0
  70. package/examples/example.opml +18 -0
  71. package/examples/example.spb +0 -0
  72. package/examples/example.sps +0 -0
  73. package/examples/example2.grd +1 -0
  74. package/examples/gemini_response.txt +845 -0
  75. package/examples/image-map.js +45 -0
  76. package/examples/package-lock.json +1326 -0
  77. package/examples/package.json +10 -0
  78. package/examples/styled-output/converted-snap-to-touchchat.ce +0 -0
  79. package/examples/styled-output/styled-example.ce +0 -0
  80. package/examples/styled-output/styled-example.gridset +0 -0
  81. package/examples/styled-output/styled-example.obf +37 -0
  82. package/examples/styled-output/styled-example.spb +0 -0
  83. package/examples/styling-example.ts +316 -0
  84. package/examples/translate.js +39 -0
  85. package/examples/translate_demo.js +254 -0
  86. package/examples/translation_cache.json +44894 -0
  87. package/examples/typescript-demo.ts +251 -0
  88. package/examples/unified-interface-demo.ts +183 -0
  89. package/package.json +106 -0
@@ -0,0 +1,44 @@
1
+ import { BaseProcessor, ProcessorOptions, ExtractStringsResult, TranslatedString, SourceString } from '../core/baseProcessor';
2
+ import { AACTree } from '../core/treeStructure';
3
+ declare class SnapProcessor extends BaseProcessor {
4
+ private symbolResolver;
5
+ private loadAudio;
6
+ constructor(symbolResolver?: unknown | null, options?: ProcessorOptions & {
7
+ loadAudio?: boolean;
8
+ });
9
+ extractTexts(filePathOrBuffer: string | Buffer): string[];
10
+ loadIntoTree(filePathOrBuffer: string | Buffer): AACTree;
11
+ processTexts(filePathOrBuffer: string | Buffer, translations: Map<string, string>, outputPath: string): Buffer;
12
+ saveFromTree(tree: AACTree, outputPath: string): void;
13
+ /**
14
+ * Add audio recording to a button in the database
15
+ */
16
+ addAudioToButton(dbPath: string, buttonId: number, audioData: Buffer, metadata?: string): number;
17
+ /**
18
+ * Create a copy of the pageset with audio recordings added
19
+ */
20
+ createAudioEnhancedPageset(sourceDbPath: string, targetDbPath: string, audioMappings: Map<number, {
21
+ audioData: Buffer;
22
+ metadata?: string;
23
+ }>): void;
24
+ /**
25
+ * Extract buttons from a specific page that need audio recordings
26
+ */
27
+ extractButtonsForAudio(dbPath: string, pageUniqueId: string): Array<{
28
+ id: number;
29
+ label: string;
30
+ message: string;
31
+ hasAudio: boolean;
32
+ }>;
33
+ /**
34
+ * Extract strings with metadata for aac-tools-platform compatibility
35
+ * Uses the generic implementation from BaseProcessor
36
+ */
37
+ extractStringsWithMetadata(filePath: string): Promise<ExtractStringsResult>;
38
+ /**
39
+ * Generate translated download for aac-tools-platform compatibility
40
+ * Uses the generic implementation from BaseProcessor
41
+ */
42
+ generateTranslatedDownload(filePath: string, translatedStrings: TranslatedString[], sourceStrings: SourceString[]): Promise<string>;
43
+ }
44
+ export { SnapProcessor };
@@ -0,0 +1,586 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.SnapProcessor = void 0;
7
+ const baseProcessor_1 = require("../core/baseProcessor");
8
+ const treeStructure_1 = require("../core/treeStructure");
9
+ // Removed unused import: FileProcessor
10
+ const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
11
+ const path_1 = __importDefault(require("path"));
12
+ const fs_1 = __importDefault(require("fs"));
13
+ const crypto_1 = __importDefault(require("crypto"));
14
+ class SnapProcessor extends baseProcessor_1.BaseProcessor {
15
+ constructor(symbolResolver = null, options = {}) {
16
+ super(options);
17
+ this.symbolResolver = null;
18
+ this.loadAudio = false;
19
+ this.symbolResolver = symbolResolver;
20
+ this.loadAudio = options.loadAudio !== undefined ? options.loadAudio : true;
21
+ }
22
+ extractTexts(filePathOrBuffer) {
23
+ const tree = this.loadIntoTree(filePathOrBuffer);
24
+ const texts = [];
25
+ for (const pageId in tree.pages) {
26
+ const page = tree.pages[pageId];
27
+ // Include page names
28
+ if (page.name)
29
+ texts.push(page.name);
30
+ // Include button texts
31
+ page.buttons.forEach((btn) => {
32
+ if (btn.label)
33
+ texts.push(btn.label);
34
+ if (btn.message && btn.message !== btn.label)
35
+ texts.push(btn.message);
36
+ });
37
+ }
38
+ return texts;
39
+ }
40
+ loadIntoTree(filePathOrBuffer) {
41
+ const tree = new treeStructure_1.AACTree();
42
+ const filePath = typeof filePathOrBuffer === 'string'
43
+ ? filePathOrBuffer
44
+ : path_1.default.join(process.cwd(), 'temp.spb');
45
+ if (Buffer.isBuffer(filePathOrBuffer)) {
46
+ fs_1.default.writeFileSync(filePath, filePathOrBuffer);
47
+ }
48
+ let db = null;
49
+ try {
50
+ db = new better_sqlite3_1.default(filePath, { readonly: true });
51
+ const getTableColumns = (tableName) => {
52
+ try {
53
+ const rows = db.prepare(`PRAGMA table_info(${tableName})`).all();
54
+ return new Set(rows.map((row) => row.name));
55
+ }
56
+ catch {
57
+ return new Set();
58
+ }
59
+ };
60
+ // Load pages first, using UniqueId as canonical id
61
+ const pages = db.prepare('SELECT * FROM Page').all();
62
+ // Map from numeric Id -> UniqueId for later lookup
63
+ const idToUniqueId = {};
64
+ pages.forEach((pageRow) => {
65
+ const uniqueId = String(pageRow.UniqueId || pageRow.Id);
66
+ idToUniqueId[String(pageRow.Id)] = uniqueId;
67
+ const page = new treeStructure_1.AACPage({
68
+ id: uniqueId,
69
+ name: pageRow.Title || pageRow.Name,
70
+ grid: [],
71
+ buttons: [],
72
+ parentId: null, // ParentId will be set via navigation buttons below
73
+ style: {
74
+ backgroundColor: pageRow.BackgroundColor
75
+ ? `#${pageRow.BackgroundColor.toString(16)}`
76
+ : undefined,
77
+ },
78
+ });
79
+ tree.addPage(page);
80
+ });
81
+ // Load buttons per page, using UniqueId for page id
82
+ for (const pageRow of pages) {
83
+ let buttons = [];
84
+ // Create a map to track page grid layouts
85
+ const pageGrids = new Map();
86
+ try {
87
+ const buttonColumns = getTableColumns('Button');
88
+ const selectFields = [
89
+ 'b.Id',
90
+ 'b.Label',
91
+ 'b.Message',
92
+ buttonColumns.has('LibrarySymbolId') ? 'b.LibrarySymbolId' : 'NULL AS LibrarySymbolId',
93
+ buttonColumns.has('PageSetImageId') ? 'b.PageSetImageId' : 'NULL AS PageSetImageId',
94
+ buttonColumns.has('BorderColor') ? 'b.BorderColor' : 'NULL AS BorderColor',
95
+ buttonColumns.has('BorderThickness') ? 'b.BorderThickness' : 'NULL AS BorderThickness',
96
+ buttonColumns.has('FontSize') ? 'b.FontSize' : 'NULL AS FontSize',
97
+ buttonColumns.has('FontFamily') ? 'b.FontFamily' : 'NULL AS FontFamily',
98
+ buttonColumns.has('FontStyle') ? 'b.FontStyle' : 'NULL AS FontStyle',
99
+ buttonColumns.has('LabelColor') ? 'b.LabelColor' : 'NULL AS LabelColor',
100
+ buttonColumns.has('BackgroundColor') ? 'b.BackgroundColor' : 'NULL AS BackgroundColor',
101
+ buttonColumns.has('NavigatePageId') ? 'b.NavigatePageId' : 'NULL AS NavigatePageId',
102
+ ];
103
+ if (this.loadAudio) {
104
+ selectFields.push(buttonColumns.has('MessageRecordingId')
105
+ ? 'b.MessageRecordingId'
106
+ : 'NULL AS MessageRecordingId');
107
+ selectFields.push(buttonColumns.has('UseMessageRecording')
108
+ ? 'b.UseMessageRecording'
109
+ : 'NULL AS UseMessageRecording');
110
+ selectFields.push(buttonColumns.has('SerializedMessageSoundMetadata')
111
+ ? 'b.SerializedMessageSoundMetadata'
112
+ : 'NULL AS SerializedMessageSoundMetadata');
113
+ }
114
+ selectFields.push('ep.GridPosition', 'er.PageId as ButtonPageId');
115
+ const buttonQuery = `
116
+ SELECT ${selectFields.join(', ')}
117
+ FROM Button b
118
+ INNER JOIN ElementReference er ON b.ElementReferenceId = er.Id
119
+ LEFT JOIN ElementPlacement ep ON ep.ElementReferenceId = er.Id
120
+ WHERE er.PageId = ?
121
+ `;
122
+ buttons = db.prepare(buttonQuery).all(pageRow.Id);
123
+ }
124
+ catch (err) {
125
+ const errorMessage = err instanceof Error ? err.message : String(err);
126
+ const errorCode = err && typeof err === 'object' && 'code' in err ? err.code : undefined;
127
+ if (errorCode === 'SQLITE_CORRUPT' ||
128
+ errorCode === 'SQLITE_NOTADB' ||
129
+ /malformed/i.test(errorMessage)) {
130
+ throw new Error(`Snap database is corrupted or incomplete: ${errorMessage}`);
131
+ }
132
+ console.warn(`Failed to load buttons for page ${pageRow.Id}: ${errorMessage}`);
133
+ // Skip this page instead of loading all buttons
134
+ buttons = [];
135
+ }
136
+ const uniqueId = String(pageRow.UniqueId || pageRow.Id);
137
+ const page = tree.getPage(uniqueId);
138
+ if (!page) {
139
+ continue;
140
+ }
141
+ // Initialize page grid if not exists (assume max 10x10 grid)
142
+ if (!pageGrids.has(uniqueId)) {
143
+ const grid = [];
144
+ for (let r = 0; r < 10; r++) {
145
+ grid[r] = new Array(10).fill(null);
146
+ }
147
+ pageGrids.set(uniqueId, grid);
148
+ }
149
+ const pageGrid = pageGrids.get(uniqueId);
150
+ if (!pageGrid)
151
+ continue;
152
+ buttons.forEach((btnRow) => {
153
+ // Determine navigation target UniqueId, if possible
154
+ let targetPageUniqueId = undefined;
155
+ if (btnRow.NavigatePageId && idToUniqueId[String(btnRow.NavigatePageId)]) {
156
+ targetPageUniqueId = idToUniqueId[String(btnRow.NavigatePageId)];
157
+ }
158
+ else if (btnRow.PageUniqueId) {
159
+ targetPageUniqueId = String(btnRow.PageUniqueId);
160
+ }
161
+ // Determine parent page association for this button
162
+ const parentPageId = btnRow.ButtonPageId ? String(btnRow.ButtonPageId) : undefined;
163
+ const parentUniqueId = parentPageId && idToUniqueId[parentPageId] ? idToUniqueId[parentPageId] : uniqueId;
164
+ // Load audio recording if requested and available
165
+ let audioRecording;
166
+ if (this.loadAudio && btnRow.MessageRecordingId && btnRow.MessageRecordingId > 0) {
167
+ try {
168
+ const recordingData = db
169
+ .prepare(`
170
+ SELECT Id, Identifier, Data FROM PageSetData WHERE Id = ?
171
+ `)
172
+ .get(btnRow.MessageRecordingId);
173
+ if (recordingData) {
174
+ audioRecording = {
175
+ id: recordingData.Id,
176
+ data: recordingData.Data,
177
+ identifier: recordingData.Identifier,
178
+ metadata: btnRow.SerializedMessageSoundMetadata || undefined,
179
+ };
180
+ }
181
+ }
182
+ catch (e) {
183
+ console.warn(`[SnapProcessor] Failed to load audio for button ${btnRow.Id}:`, e);
184
+ }
185
+ }
186
+ // Create semantic action for Snap button
187
+ let semanticAction;
188
+ let legacyAction = null;
189
+ if (targetPageUniqueId) {
190
+ semanticAction = {
191
+ category: treeStructure_1.AACSemanticCategory.NAVIGATION,
192
+ intent: treeStructure_1.AACSemanticIntent.NAVIGATE_TO,
193
+ targetId: targetPageUniqueId,
194
+ platformData: {
195
+ snap: {
196
+ navigatePageId: btnRow.NavigatePageId,
197
+ elementReferenceId: btnRow.Id,
198
+ },
199
+ },
200
+ fallback: {
201
+ type: 'NAVIGATE',
202
+ targetPageId: targetPageUniqueId,
203
+ },
204
+ };
205
+ legacyAction = {
206
+ type: 'NAVIGATE',
207
+ targetPageId: targetPageUniqueId,
208
+ };
209
+ }
210
+ else {
211
+ semanticAction = {
212
+ category: treeStructure_1.AACSemanticCategory.COMMUNICATION,
213
+ intent: treeStructure_1.AACSemanticIntent.SPEAK_TEXT,
214
+ text: btnRow.Message || btnRow.Label || '',
215
+ platformData: {
216
+ snap: {
217
+ elementReferenceId: btnRow.Id,
218
+ },
219
+ },
220
+ fallback: {
221
+ type: 'SPEAK',
222
+ message: btnRow.Message || btnRow.Label || '',
223
+ },
224
+ };
225
+ }
226
+ const button = new treeStructure_1.AACButton({
227
+ id: String(btnRow.Id),
228
+ label: btnRow.Label || '',
229
+ message: btnRow.Message || btnRow.Label || '',
230
+ targetPageId: targetPageUniqueId,
231
+ semanticAction: semanticAction,
232
+ audioRecording: audioRecording,
233
+ style: {
234
+ backgroundColor: btnRow.BackgroundColor
235
+ ? `#${btnRow.BackgroundColor.toString(16)}`
236
+ : undefined,
237
+ borderColor: btnRow.BorderColor ? `#${btnRow.BorderColor.toString(16)}` : undefined,
238
+ borderWidth: btnRow.BorderThickness,
239
+ fontColor: btnRow.LabelColor ? `#${btnRow.LabelColor.toString(16)}` : undefined,
240
+ fontSize: btnRow.FontSize,
241
+ fontFamily: btnRow.FontFamily,
242
+ fontStyle: btnRow.FontStyle?.toString(),
243
+ },
244
+ });
245
+ // Add to the intended parent page
246
+ const parentPage = tree.getPage(parentUniqueId);
247
+ if (parentPage) {
248
+ parentPage.addButton(button);
249
+ // Add button to grid layout if position data is available
250
+ const gridPositionStr = String(btnRow.GridPosition || '');
251
+ if (gridPositionStr && gridPositionStr.includes(',')) {
252
+ // Parse comma-separated coordinates "x,y"
253
+ const [xStr, yStr] = gridPositionStr.split(',');
254
+ const gridX = parseInt(xStr, 10);
255
+ const gridY = parseInt(yStr, 10);
256
+ // Place button in grid if within bounds and coordinates are valid
257
+ if (!isNaN(gridX) &&
258
+ !isNaN(gridY) &&
259
+ gridX >= 0 &&
260
+ gridY >= 0 &&
261
+ gridY < 10 &&
262
+ gridX < 10 &&
263
+ pageGrid[gridY] &&
264
+ pageGrid[gridY][gridX] === null) {
265
+ pageGrid[gridY][gridX] = button;
266
+ }
267
+ }
268
+ }
269
+ // If this is a navigation button, update the target page's parentId
270
+ if (targetPageUniqueId) {
271
+ const targetPage = tree.getPage(targetPageUniqueId);
272
+ if (targetPage) {
273
+ targetPage.parentId = parentUniqueId;
274
+ }
275
+ }
276
+ });
277
+ // Set grid layout for the current page
278
+ const currentPage = tree.getPage(uniqueId);
279
+ if (currentPage && pageGrid) {
280
+ currentPage.grid = pageGrid;
281
+ }
282
+ }
283
+ return tree;
284
+ }
285
+ catch (error) {
286
+ // Provide more specific error messages
287
+ if (error.code === 'SQLITE_NOTADB') {
288
+ throw new Error(`Invalid SQLite database file: ${typeof filePathOrBuffer === 'string' ? filePathOrBuffer : 'buffer'}`);
289
+ }
290
+ else if (error.code === 'ENOENT') {
291
+ throw new Error(`File not found: ${filePathOrBuffer}`);
292
+ }
293
+ else if (error.code === 'EACCES') {
294
+ throw new Error(`Permission denied accessing file: ${filePathOrBuffer}`);
295
+ }
296
+ else {
297
+ throw new Error(`Failed to load Snap file: ${error.message}`);
298
+ }
299
+ }
300
+ finally {
301
+ // Ensure database is closed
302
+ if (db) {
303
+ db.close();
304
+ }
305
+ // Clean up temporary file if created from buffer
306
+ if (Buffer.isBuffer(filePathOrBuffer) && fs_1.default.existsSync(filePath)) {
307
+ try {
308
+ fs_1.default.unlinkSync(filePath);
309
+ }
310
+ catch (e) {
311
+ console.warn('Failed to clean up temporary file:', e);
312
+ }
313
+ }
314
+ }
315
+ }
316
+ processTexts(filePathOrBuffer, translations, outputPath) {
317
+ // Load the tree, apply translations, and save to new file
318
+ const tree = this.loadIntoTree(filePathOrBuffer);
319
+ // Apply translations to all text content
320
+ Object.values(tree.pages).forEach((page) => {
321
+ // Translate page names
322
+ if (page.name && translations.has(page.name)) {
323
+ page.name = translations.get(page.name);
324
+ }
325
+ // Translate button labels and messages
326
+ page.buttons.forEach((button) => {
327
+ if (button.label && translations.has(button.label)) {
328
+ button.label = translations.get(button.label);
329
+ }
330
+ if (button.message && translations.has(button.message)) {
331
+ button.message = translations.get(button.message);
332
+ }
333
+ });
334
+ });
335
+ // Save the translated tree and return its content
336
+ this.saveFromTree(tree, outputPath);
337
+ return fs_1.default.readFileSync(outputPath);
338
+ }
339
+ saveFromTree(tree, outputPath) {
340
+ const outputDir = path_1.default.dirname(outputPath);
341
+ if (!fs_1.default.existsSync(outputDir)) {
342
+ fs_1.default.mkdirSync(outputDir, { recursive: true });
343
+ }
344
+ if (fs_1.default.existsSync(outputPath)) {
345
+ fs_1.default.unlinkSync(outputPath);
346
+ }
347
+ // Create a new SQLite database for Snap format
348
+ const db = new better_sqlite3_1.default(outputPath, { readonly: false });
349
+ try {
350
+ // Create basic Snap database schema (simplified)
351
+ db.exec(`
352
+ CREATE TABLE IF NOT EXISTS Page (
353
+ Id INTEGER PRIMARY KEY,
354
+ UniqueId TEXT UNIQUE,
355
+ Title TEXT,
356
+ Name TEXT,
357
+ BackgroundColor INTEGER
358
+ );
359
+
360
+ CREATE TABLE IF NOT EXISTS Button (
361
+ Id INTEGER PRIMARY KEY,
362
+ Label TEXT,
363
+ Message TEXT,
364
+ NavigatePageId INTEGER,
365
+ ElementReferenceId INTEGER,
366
+ LibrarySymbolId INTEGER,
367
+ PageSetImageId INTEGER,
368
+ MessageRecordingId INTEGER,
369
+ SerializedMessageSoundMetadata TEXT,
370
+ UseMessageRecording INTEGER,
371
+ LabelColor INTEGER,
372
+ BackgroundColor INTEGER,
373
+ BorderColor INTEGER,
374
+ BorderThickness REAL,
375
+ FontSize REAL,
376
+ FontFamily TEXT,
377
+ FontStyle INTEGER
378
+ );
379
+
380
+ CREATE TABLE IF NOT EXISTS ElementReference (
381
+ Id INTEGER PRIMARY KEY,
382
+ PageId INTEGER,
383
+ FOREIGN KEY (PageId) REFERENCES Page (Id)
384
+ );
385
+
386
+ CREATE TABLE IF NOT EXISTS ElementPlacement (
387
+ Id INTEGER PRIMARY KEY,
388
+ ElementReferenceId INTEGER,
389
+ GridPosition TEXT,
390
+ FOREIGN KEY (ElementReferenceId) REFERENCES ElementReference (Id)
391
+ );
392
+
393
+ CREATE TABLE IF NOT EXISTS PageSetData (
394
+ Id INTEGER PRIMARY KEY,
395
+ Identifier TEXT UNIQUE,
396
+ Data BLOB,
397
+ RefCount INTEGER DEFAULT 1
398
+ );
399
+ `);
400
+ // Insert pages
401
+ let pageIdCounter = 1;
402
+ let buttonIdCounter = 1;
403
+ let elementRefIdCounter = 1;
404
+ let placementIdCounter = 1;
405
+ let pageSetDataIdCounter = 1;
406
+ const pageIdMap = new Map();
407
+ const pageSetDataIdentifierMap = new Map();
408
+ const insertPageSetData = db.prepare('INSERT INTO PageSetData (Id, Identifier, Data, RefCount) VALUES (?, ?, ?, ?)');
409
+ const incrementRefCount = db.prepare('UPDATE PageSetData SET RefCount = RefCount + 1 WHERE Id = ?');
410
+ // First pass: create all pages
411
+ Object.values(tree.pages).forEach((page) => {
412
+ const numericPageId = pageIdCounter++;
413
+ pageIdMap.set(page.id, numericPageId);
414
+ const insertPage = db.prepare('INSERT INTO Page (Id, UniqueId, Title, Name, BackgroundColor) VALUES (?, ?, ?, ?, ?)');
415
+ insertPage.run(numericPageId, page.id, page.name || '', page.name || '', page.style?.backgroundColor
416
+ ? parseInt(page.style.backgroundColor.replace('#', ''), 16)
417
+ : null);
418
+ });
419
+ // Second pass: create buttons with proper page references
420
+ Object.values(tree.pages).forEach((page) => {
421
+ const numericPageId = pageIdMap.get(page.id);
422
+ page.buttons.forEach((button, index) => {
423
+ // Find button position in grid layout
424
+ let gridPosition = `${index % 4},${Math.floor(index / 4)}`; // Default fallback
425
+ if (page.grid && page.grid.length > 0) {
426
+ // Search for button in grid layout
427
+ for (let y = 0; y < page.grid.length; y++) {
428
+ for (let x = 0; x < page.grid[y].length; x++) {
429
+ const gridButton = page.grid[y][x];
430
+ if (gridButton && gridButton.id === button.id) {
431
+ // Convert grid coordinates to comma-separated format
432
+ gridPosition = `${x},${y}`;
433
+ break;
434
+ }
435
+ }
436
+ }
437
+ }
438
+ const elementRefId = elementRefIdCounter++;
439
+ // Insert ElementReference
440
+ const insertElementRef = db.prepare('INSERT INTO ElementReference (Id, PageId) VALUES (?, ?)');
441
+ insertElementRef.run(elementRefId, numericPageId);
442
+ // Insert Button - handle semantic actions
443
+ let navigatePageId = null;
444
+ // Use semantic action if available
445
+ if (button.semanticAction?.intent === treeStructure_1.AACSemanticIntent.NAVIGATE_TO) {
446
+ const targetId = button.semanticAction.targetId || button.targetPageId;
447
+ navigatePageId = targetId ? pageIdMap.get(targetId) || null : null;
448
+ }
449
+ const insertButton = db.prepare('INSERT INTO Button (Id, Label, Message, NavigatePageId, ElementReferenceId, LibrarySymbolId, PageSetImageId, MessageRecordingId, SerializedMessageSoundMetadata, UseMessageRecording, LabelColor, BackgroundColor, BorderColor, BorderThickness, FontSize, FontFamily, FontStyle) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)');
450
+ const audio = button.audioRecording;
451
+ let messageRecordingId = null;
452
+ let serializedMetadata = null;
453
+ let useMessageRecording = 0;
454
+ if (audio && Buffer.isBuffer(audio.data) && audio.data.length > 0) {
455
+ const identifier = audio.identifier && audio.identifier.trim().length > 0
456
+ ? audio.identifier.trim()
457
+ : `audio_${buttonIdCounter}`;
458
+ let audioId = pageSetDataIdentifierMap.get(identifier);
459
+ if (!audioId) {
460
+ audioId = pageSetDataIdCounter++;
461
+ insertPageSetData.run(audioId, identifier, audio.data, 1);
462
+ pageSetDataIdentifierMap.set(identifier, audioId);
463
+ }
464
+ else {
465
+ incrementRefCount.run(audioId);
466
+ }
467
+ messageRecordingId = audioId;
468
+ serializedMetadata = audio.metadata || null;
469
+ useMessageRecording = 1;
470
+ }
471
+ insertButton.run(buttonIdCounter++, button.label || '', button.message || button.label || '', navigatePageId, elementRefId, null, null, messageRecordingId, serializedMetadata, useMessageRecording, button.style?.fontColor ? parseInt(button.style.fontColor.replace('#', ''), 16) : null, button.style?.backgroundColor
472
+ ? parseInt(button.style.backgroundColor.replace('#', ''), 16)
473
+ : null, button.style?.borderColor
474
+ ? parseInt(button.style.borderColor.replace('#', ''), 16)
475
+ : null, button.style?.borderWidth, button.style?.fontSize, button.style?.fontFamily, button.style?.fontStyle ? parseInt(button.style.fontStyle) : null);
476
+ // Insert ElementPlacement
477
+ const insertPlacement = db.prepare('INSERT INTO ElementPlacement (Id, ElementReferenceId, GridPosition) VALUES (?, ?, ?)');
478
+ insertPlacement.run(placementIdCounter++, elementRefId, gridPosition);
479
+ });
480
+ });
481
+ }
482
+ finally {
483
+ db.close();
484
+ }
485
+ }
486
+ /**
487
+ * Add audio recording to a button in the database
488
+ */
489
+ addAudioToButton(dbPath, buttonId, audioData, metadata) {
490
+ const db = new better_sqlite3_1.default(dbPath, { fileMustExist: true });
491
+ try {
492
+ // Ensure PageSetData table exists
493
+ db.exec(`
494
+ CREATE TABLE IF NOT EXISTS PageSetData (
495
+ Id INTEGER PRIMARY KEY,
496
+ Identifier TEXT UNIQUE,
497
+ Data BLOB
498
+ );
499
+ `);
500
+ // Generate SHA1 hash for the identifier
501
+ const sha1Hash = crypto_1.default.createHash('sha1').update(audioData).digest('hex');
502
+ const identifier = `SND:${sha1Hash}`;
503
+ // Check if audio with this identifier already exists
504
+ let audioId;
505
+ const existingAudio = db
506
+ .prepare('SELECT Id FROM PageSetData WHERE Identifier = ?')
507
+ .get(identifier);
508
+ if (existingAudio) {
509
+ audioId = existingAudio.Id;
510
+ }
511
+ else {
512
+ // Insert new audio data
513
+ const result = db
514
+ .prepare('INSERT INTO PageSetData (Identifier, Data) VALUES (?, ?)')
515
+ .run(identifier, audioData);
516
+ audioId = Number(result.lastInsertRowid);
517
+ }
518
+ // Update button to reference the audio
519
+ const updateButton = db.prepare('UPDATE Button SET MessageRecordingId = ?, UseMessageRecording = 1, SerializedMessageSoundMetadata = ? WHERE Id = ?');
520
+ const metadataJson = metadata ? JSON.stringify({ FileName: metadata }) : null;
521
+ updateButton.run(audioId, metadataJson, buttonId);
522
+ return audioId;
523
+ }
524
+ finally {
525
+ db.close();
526
+ }
527
+ }
528
+ /**
529
+ * Create a copy of the pageset with audio recordings added
530
+ */
531
+ createAudioEnhancedPageset(sourceDbPath, targetDbPath, audioMappings) {
532
+ // Copy the source database to target
533
+ fs_1.default.copyFileSync(sourceDbPath, targetDbPath);
534
+ // Add audio recordings to the copy
535
+ audioMappings.forEach((audioInfo, buttonId) => {
536
+ this.addAudioToButton(targetDbPath, buttonId, audioInfo.audioData, audioInfo.metadata);
537
+ });
538
+ }
539
+ /**
540
+ * Extract buttons from a specific page that need audio recordings
541
+ */
542
+ extractButtonsForAudio(dbPath, pageUniqueId) {
543
+ const db = new better_sqlite3_1.default(dbPath, { readonly: true });
544
+ try {
545
+ // Find the page by UniqueId
546
+ const page = db.prepare('SELECT * FROM Page WHERE UniqueId = ?').get(pageUniqueId);
547
+ if (!page) {
548
+ throw new Error(`Page with UniqueId ${pageUniqueId} not found`);
549
+ }
550
+ // Get buttons for this page
551
+ const buttons = db
552
+ .prepare(`
553
+ SELECT
554
+ b.Id, b.Label, b.Message, b.MessageRecordingId, b.UseMessageRecording
555
+ FROM Button b
556
+ JOIN ElementReference er ON b.ElementReferenceId = er.Id
557
+ WHERE er.PageId = ?
558
+ `)
559
+ .all(page.Id);
560
+ return buttons.map((btn) => ({
561
+ id: btn.Id,
562
+ label: btn.Label || '',
563
+ message: btn.Message || btn.Label || '',
564
+ hasAudio: !!(btn.MessageRecordingId && btn.MessageRecordingId > 0),
565
+ }));
566
+ }
567
+ finally {
568
+ db.close();
569
+ }
570
+ }
571
+ /**
572
+ * Extract strings with metadata for aac-tools-platform compatibility
573
+ * Uses the generic implementation from BaseProcessor
574
+ */
575
+ async extractStringsWithMetadata(filePath) {
576
+ return this.extractStringsWithMetadataGeneric(filePath);
577
+ }
578
+ /**
579
+ * Generate translated download for aac-tools-platform compatibility
580
+ * Uses the generic implementation from BaseProcessor
581
+ */
582
+ async generateTranslatedDownload(filePath, translatedStrings, sourceStrings) {
583
+ return this.generateTranslatedDownloadGeneric(filePath, translatedStrings, sourceStrings);
584
+ }
585
+ }
586
+ exports.SnapProcessor = SnapProcessor;
@@ -0,0 +1,4 @@
1
+ import { AACTree } from '../../core/treeStructure';
2
+ export declare function getPageTokenImageMap(tree: AACTree, pageId: string): Map<string, string>;
3
+ export declare function getAllowedImageEntries(_tree: AACTree): Set<string>;
4
+ export declare function openImage(_ceFile: string | Buffer, _entryPath: string): Buffer | null;
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getPageTokenImageMap = getPageTokenImageMap;
4
+ exports.getAllowedImageEntries = getAllowedImageEntries;
5
+ exports.openImage = openImage;
6
+ // Minimal TouchChat helpers (stubs) to align with processors/<engine>/helpers pattern
7
+ // NOTE: TouchChat buttons currently do not populate resolvedImageEntry; these helpers
8
+ // therefore return empty collections until image resolution is implemented.
9
+ function getPageTokenImageMap(tree, pageId) {
10
+ const map = new Map();
11
+ const page = tree.getPage(pageId);
12
+ if (!page)
13
+ return map;
14
+ for (const btn of page.buttons) {
15
+ if (btn.resolvedImageEntry)
16
+ map.set(btn.id, String(btn.resolvedImageEntry));
17
+ }
18
+ return map;
19
+ }
20
+ function getAllowedImageEntries(_tree) {
21
+ // No known image entry paths for TouchChat yet
22
+ return new Set();
23
+ }
24
+ function openImage(_ceFile, _entryPath) {
25
+ // Not implemented for TouchChat yet
26
+ return null;
27
+ }