@willwade/aac-processors 0.0.14 → 0.0.15

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.
@@ -1,453 +0,0 @@
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.ScreenshotConverter = void 0;
7
- const treeStructure_1 = require("../core/treeStructure");
8
- const path_1 = __importDefault(require("path"));
9
- class ScreenshotConverter {
10
- /**
11
- * Parse filename to extract page hierarchy and names
12
- * Examples:
13
- * - "Home.png" → pageName: "Home", parentPath: ""
14
- * - "Home->Fragen.png" → pageName: "Fragen", parentPath: "Home"
15
- * - "Home->Settings->Profile.jpg" → pageName: "Profile", parentPath: "Home->Settings"
16
- */
17
- static parseFilename(filename, delimiter = '->') {
18
- const baseName = path_1.default.parse(filename).name;
19
- const parts = baseName.split(delimiter).map((part) => part.trim());
20
- return {
21
- pageName: parts[parts.length - 1] || baseName,
22
- parentPath: parts.slice(0, -1).join(delimiter),
23
- };
24
- }
25
- /**
26
- * Build page hierarchy from an array of screenshots
27
- */
28
- static buildPageHierarchy(screenshots) {
29
- const hierarchy = {};
30
- const delimiter = this.defaultOptions.filenameDelimiter || '->';
31
- // First pass: parse all filenames
32
- screenshots.forEach((screenshot, index) => {
33
- const { pageName, parentPath } = this.parseFilename(screenshot.filename, delimiter);
34
- screenshot.pageName = pageName;
35
- screenshot.parentPath = parentPath;
36
- const pageId = `page_${index}`;
37
- hierarchy[pageId] = {
38
- page: screenshot,
39
- children: [],
40
- parent: undefined,
41
- };
42
- });
43
- // Second pass: establish parent-child relationships
44
- Object.entries(hierarchy).forEach(([pageId, entry]) => {
45
- const parentPath = entry.page.parentPath;
46
- if (parentPath) {
47
- // Find parent by matching the full path
48
- const parent = Object.values(hierarchy).find((h) => h.page.pageName === parentPath.split(delimiter).pop());
49
- if (parent) {
50
- entry.parent = Object.keys(hierarchy).find((key) => hierarchy[key] === parent);
51
- parent.children.push(pageId);
52
- }
53
- }
54
- });
55
- return hierarchy;
56
- }
57
- static parseOCRText(ocrResult) {
58
- const lines = ocrResult.split('\n').filter((line) => line.trim());
59
- const cells = [];
60
- const categories = new Set();
61
- // Skip header metadata
62
- const contentStart = lines.findIndex((line) => line.includes('ich möchte') && !line.includes('ich möchte ich'));
63
- if (contentStart === -1) {
64
- // Try another approach if the first pattern doesn't match
65
- const gridStart = lines.findIndex((line) => line.includes('ich möchte') && line.split(/\s+/).length > 2);
66
- if (gridStart === -1)
67
- return { rows: 6, cols: 11, cells: [], categories: [] };
68
- }
69
- // Find the line with the grid content (usually has tab-separated values)
70
- const gridLineIndex = lines.findIndex((line) => line.includes('ich möchte') && line.includes('\t') && line.split(/\s+/).length > 5);
71
- let rows = 6;
72
- let cols = 11;
73
- // If we found a properly formatted grid line
74
- if (gridLineIndex !== -1) {
75
- const gridLine = lines[gridLineIndex];
76
- // Split by tabs to get individual cell values
77
- const tokens = gridLine
78
- .split('\t')
79
- .map((t) => t.trim())
80
- .filter((t) => t);
81
- cols = Math.max(tokens.length, cols);
82
- // Create first row from the main grid line
83
- tokens.forEach((token, col) => {
84
- const isCategory = this.isCategoryToken(token);
85
- const isNavigation = this.isNavigationToken(token);
86
- const isEmpty = !token || token === '...' || token === '';
87
- if (isCategory)
88
- categories.add(token);
89
- if (!isEmpty) {
90
- cells.push({
91
- text: token,
92
- row: 0,
93
- col,
94
- isCategory,
95
- isNavigation,
96
- isEmpty,
97
- });
98
- }
99
- });
100
- // Process subsequent lines
101
- let currentRow = 1;
102
- for (let i = gridLineIndex + 1; i < lines.length; i++) {
103
- const line = lines[i].trim();
104
- if (!line)
105
- continue;
106
- // Skip lines that look like headers or metadata
107
- if (line.match(/^\d+:\d+/) || line.match(/[A-Z][a-z]{2},\s+\d+/) || line.includes('%'))
108
- continue;
109
- // Skip duplicate "ich möchte" at start
110
- if (line === 'ich möchte' && currentRow === 1) {
111
- currentRow = 0;
112
- continue;
113
- }
114
- const tokens = line
115
- .split('\t')
116
- .map((t) => t.trim())
117
- .filter((t) => t);
118
- tokens.forEach((token, col) => {
119
- const isCategory = this.isCategoryToken(token);
120
- const isNavigation = this.isNavigationToken(token);
121
- const isEmpty = !token || token === '...' || token === '';
122
- if (isCategory)
123
- categories.add(token);
124
- if (!isEmpty) {
125
- cells.push({
126
- text: token,
127
- row: currentRow,
128
- col,
129
- isCategory,
130
- isNavigation,
131
- isEmpty,
132
- });
133
- }
134
- });
135
- currentRow++;
136
- if (currentRow >= rows)
137
- break;
138
- }
139
- }
140
- else {
141
- // Fallback: simple whitespace parsing for unstructured OCR
142
- let currentRow = 0;
143
- lines.forEach((line, _lineIndex) => {
144
- if (!line.trim())
145
- return;
146
- // Skip metadata
147
- if (line.includes('%') || line.match(/\d+:\d+/) || line.match(/[A-Z][a-z]{2},\s+\d+/))
148
- return;
149
- const tokens = line.trim().split(/\s+/);
150
- tokens.forEach((token, tokenIndex) => {
151
- if (tokenIndex >= cols)
152
- return; // Skip if beyond expected columns
153
- const isCategory = this.isCategoryToken(token);
154
- const isNavigation = this.isNavigationToken(token);
155
- const isEmpty = !token || token.trim() === '' || token === '...';
156
- if (isCategory)
157
- categories.add(token);
158
- if (!isEmpty) {
159
- cells.push({
160
- text: token,
161
- row: currentRow,
162
- col: tokenIndex,
163
- isCategory,
164
- isNavigation,
165
- isEmpty,
166
- });
167
- }
168
- });
169
- currentRow++;
170
- });
171
- }
172
- // Auto-detect actual grid dimensions
173
- if (cells.length > 0) {
174
- rows = Math.max(...cells.map((c) => c.row)) + 1;
175
- cols = Math.max(...cells.map((c) => c.col)) + 1;
176
- }
177
- return {
178
- rows,
179
- cols,
180
- cells,
181
- categories: Array.from(categories),
182
- };
183
- }
184
- static isCategoryToken(token) {
185
- const knownCategories = [
186
- // English categories
187
- 'Questions',
188
- 'Meetings',
189
- 'Praise',
190
- 'Complaints',
191
- 'Phrases',
192
- 'Conversations',
193
- 'Verbs',
194
- 'People',
195
- 'Messages',
196
- 'Properties',
197
- 'Feelings',
198
- 'Actions',
199
- 'Activities',
200
- 'Food',
201
- 'Drink',
202
- 'Colors',
203
- 'Shapes',
204
- 'Settings',
205
- 'Home',
206
- 'Back',
207
- 'Next',
208
- 'Menu',
209
- // German categories
210
- 'Fragen',
211
- 'Treffen',
212
- 'Lob',
213
- 'Beschwerde',
214
- 'Sprüche',
215
- 'Gespräche',
216
- 'Verben',
217
- 'Leute',
218
- 'Mitteilungen',
219
- 'Eigenschaften',
220
- 'Gefühle',
221
- 'Spielen',
222
- 'Multimedia',
223
- 'Essen',
224
- 'Trinken',
225
- 'Farben/Formen',
226
- ];
227
- // Check for known categories
228
- if (knownCategories.includes(token)) {
229
- return true;
230
- }
231
- // Check for common category patterns
232
- const categoryPatterns = [
233
- /.*Questions?$/,
234
- /.*Category.*/,
235
- /.*Menu.*/,
236
- /.*Settings?$/,
237
- /.*Options?$/,
238
- /谈话/i, // Chinese
239
- /質問/i, // Japanese
240
- /preguntas/i, // Spanish
241
- ];
242
- return categoryPatterns.some((pattern) => pattern.test(token));
243
- }
244
- static isNavigationToken(token) {
245
- const navTokens = [
246
- // English
247
- 'Home',
248
- 'Back',
249
- 'Next',
250
- 'Previous',
251
- 'Menu',
252
- 'Settings',
253
- 'Exit',
254
- 'Close',
255
- 'OK',
256
- 'Cancel',
257
- 'Yes',
258
- 'No',
259
- 'Help',
260
- 'Search',
261
- // German
262
- 'Home',
263
- 'Zurück',
264
- 'Weiter',
265
- 'Menü',
266
- 'Einstellungen',
267
- 'Beenden',
268
- 'Schließen',
269
- 'Hilfe',
270
- 'Suche',
271
- // Navigation indicators
272
- '←',
273
- '→',
274
- '↑',
275
- '↓',
276
- '◀',
277
- '▶',
278
- '▲',
279
- '▼',
280
- ];
281
- return (navTokens.includes(token) || token === '←' || token === '→' || token === '↑' || token === '↓');
282
- }
283
- static convertToAACPage(screenshotPage, pageHierarchy, options) {
284
- const opts = {
285
- ...this.defaultOptions,
286
- ...options,
287
- };
288
- const buttons = [];
289
- // Convert cells to AAC buttons
290
- screenshotPage.grid.cells.forEach((cell) => {
291
- if (cell.isEmpty && !opts.includeEmptyCells)
292
- return;
293
- const button = new treeStructure_1.AACButton({
294
- id: `cell_${cell.row}_${cell.col}`,
295
- label: cell.text,
296
- message: cell.text,
297
- style: {
298
- backgroundColor: cell.isCategory ? '#4CAF50' : cell.isNavigation ? '#2196F3' : '#FFFFFF',
299
- fontColor: cell.isCategory || cell.isNavigation ? '#FFFFFF' : '#000000',
300
- borderColor: '#CCCCCC',
301
- borderWidth: 1,
302
- },
303
- semanticAction: this.createSemanticAction(cell, screenshotPage, pageHierarchy, opts),
304
- x: cell.col,
305
- y: cell.row,
306
- });
307
- buttons.push(button);
308
- });
309
- return new treeStructure_1.AACPage({
310
- id: screenshotPage.pageName || 'screenshot_page',
311
- name: screenshotPage.pageTitle || screenshotPage.pageName || 'Screenshot Page',
312
- buttons,
313
- grid: {
314
- columns: screenshotPage.grid.cols,
315
- rows: screenshotPage.grid.rows,
316
- },
317
- style: {
318
- backgroundColor: '#F5F5F5',
319
- },
320
- parentId: null,
321
- });
322
- }
323
- static createSemanticAction(cell, screenshotPage, pageHierarchy, options) {
324
- if (cell.isEmpty)
325
- return undefined;
326
- const _opts = {
327
- ...this.defaultOptions,
328
- ...options,
329
- };
330
- if (cell.isCategory) {
331
- // Try to find target page in hierarchy based on category name
332
- let targetId = `category_${cell.text.toLowerCase().replace(/\s+/g, '_')}`;
333
- if (pageHierarchy) {
334
- // Look for a page that matches this category
335
- const matchingPage = Object.values(pageHierarchy).find((h) => h.page.pageName?.toLowerCase() === cell.text.toLowerCase());
336
- if (matchingPage) {
337
- targetId = matchingPage.page.pageName || targetId;
338
- }
339
- }
340
- return {
341
- category: treeStructure_1.AACSemanticCategory.NAVIGATION,
342
- intent: treeStructure_1.AACSemanticIntent.NAVIGATE_TO,
343
- targetId,
344
- parameters: { category: cell.text },
345
- };
346
- }
347
- if (cell.isNavigation) {
348
- const text = cell.text.toLowerCase();
349
- // Home navigation
350
- if (text === 'home' || text === '⌂') {
351
- return {
352
- category: treeStructure_1.AACSemanticCategory.NAVIGATION,
353
- intent: treeStructure_1.AACSemanticIntent.GO_HOME,
354
- };
355
- }
356
- // Back navigation
357
- if (text === 'back' || text === 'zurück' || text === '←' || text === '◀') {
358
- return {
359
- category: treeStructure_1.AACSemanticCategory.NAVIGATION,
360
- intent: treeStructure_1.AACSemanticIntent.GO_BACK,
361
- };
362
- }
363
- // Next/forward navigation
364
- if (text === 'next' || text === 'weiter' || text === '→' || text === '▶') {
365
- // If we have hierarchy, navigate to parent
366
- if (pageHierarchy && screenshotPage.parentPath) {
367
- const parentId = Object.keys(pageHierarchy).find((key) => pageHierarchy[key].page.pageName === screenshotPage.parentPath?.split('->').pop());
368
- if (parentId) {
369
- return {
370
- category: treeStructure_1.AACSemanticCategory.NAVIGATION,
371
- intent: treeStructure_1.AACSemanticIntent.NAVIGATE_TO,
372
- targetId: parentId,
373
- };
374
- }
375
- }
376
- return {
377
- category: treeStructure_1.AACSemanticCategory.NAVIGATION,
378
- intent: treeStructure_1.AACSemanticIntent.NAVIGATE_TO,
379
- targetId: 'next_page',
380
- parameters: { direction: 'next' },
381
- };
382
- }
383
- // Menu navigation
384
- if (text === 'menu' || text === 'menü') {
385
- return {
386
- category: treeStructure_1.AACSemanticCategory.NAVIGATION,
387
- intent: treeStructure_1.AACSemanticIntent.NAVIGATE_TO,
388
- targetId: 'main_menu',
389
- };
390
- }
391
- }
392
- // Default to speaking the text
393
- return {
394
- category: treeStructure_1.AACSemanticCategory.COMMUNICATION,
395
- intent: treeStructure_1.AACSemanticIntent.SPEAK_IMMEDIATE,
396
- text: cell.text,
397
- richText: {
398
- text: cell.text,
399
- },
400
- };
401
- }
402
- static convertToAACTree(screenshotPages, options) {
403
- const opts = { ...this.defaultOptions, ...options };
404
- const tree = new treeStructure_1.AACTree();
405
- // Build page hierarchy
406
- const pageHierarchy = this.buildPageHierarchy(screenshotPages);
407
- // Set metadata on tree
408
- tree.version = '1.0';
409
- tree.metadata = {
410
- name: 'Screenshot Conversion',
411
- author: 'AAC Processors',
412
- description: 'Converted from screenshot images',
413
- language: opts.language,
414
- };
415
- // Convert each screenshot page to AAC page
416
- screenshotPages.forEach((screenshotPage, index) => {
417
- const page = this.convertToAACPage(screenshotPage, pageHierarchy, opts);
418
- // Ensure unique ID by using page name if available
419
- if (screenshotPage.pageName) {
420
- page.id = this.sanitizePageId(screenshotPage.pageName);
421
- }
422
- else {
423
- page.id = `screenshot_page_${index}`;
424
- }
425
- tree.addPage(page);
426
- });
427
- // Set root page to the one with no parent
428
- const rootPage = Object.entries(pageHierarchy).find(([_, entry]) => !entry.parent);
429
- if (rootPage) {
430
- const rootPageId = this.sanitizePageId(rootPage[1].page.pageName || 'home');
431
- if (tree.pages[rootPageId]) {
432
- tree.rootId = rootPageId;
433
- }
434
- }
435
- return tree;
436
- }
437
- static sanitizePageId(pageName) {
438
- return pageName
439
- .toLowerCase()
440
- .replace(/[^a-z0-9]/g, '_')
441
- .replace(/_+/g, '_')
442
- .replace(/^_|_$/g, '');
443
- }
444
- }
445
- exports.ScreenshotConverter = ScreenshotConverter;
446
- ScreenshotConverter.defaultOptions = {
447
- includeEmptyCells: false,
448
- generateIds: true,
449
- targetPlatform: 'grid3',
450
- language: 'en',
451
- fallbackCategory: 'General',
452
- filenameDelimiter: '->',
453
- };