@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.
- package/LICENSE +674 -0
- package/README.md +787 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +189 -0
- package/dist/cli/prettyPrint.d.ts +2 -0
- package/dist/cli/prettyPrint.js +28 -0
- package/dist/core/analyze.d.ts +6 -0
- package/dist/core/analyze.js +49 -0
- package/dist/core/baseProcessor.d.ts +94 -0
- package/dist/core/baseProcessor.js +208 -0
- package/dist/core/fileProcessor.d.ts +7 -0
- package/dist/core/fileProcessor.js +51 -0
- package/dist/core/stringCasing.d.ts +37 -0
- package/dist/core/stringCasing.js +174 -0
- package/dist/core/treeStructure.d.ts +190 -0
- package/dist/core/treeStructure.js +223 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +96 -0
- package/dist/optional/symbolTools.d.ts +28 -0
- package/dist/optional/symbolTools.js +126 -0
- package/dist/processors/applePanelsProcessor.d.ts +23 -0
- package/dist/processors/applePanelsProcessor.js +521 -0
- package/dist/processors/astericsGridProcessor.d.ts +49 -0
- package/dist/processors/astericsGridProcessor.js +1427 -0
- package/dist/processors/dotProcessor.d.ts +21 -0
- package/dist/processors/dotProcessor.js +191 -0
- package/dist/processors/excelProcessor.d.ts +145 -0
- package/dist/processors/excelProcessor.js +556 -0
- package/dist/processors/gridset/helpers.d.ts +4 -0
- package/dist/processors/gridset/helpers.js +48 -0
- package/dist/processors/gridset/resolver.d.ts +8 -0
- package/dist/processors/gridset/resolver.js +100 -0
- package/dist/processors/gridsetProcessor.d.ts +28 -0
- package/dist/processors/gridsetProcessor.js +1339 -0
- package/dist/processors/index.d.ts +14 -0
- package/dist/processors/index.js +42 -0
- package/dist/processors/obfProcessor.d.ts +21 -0
- package/dist/processors/obfProcessor.js +278 -0
- package/dist/processors/opmlProcessor.d.ts +21 -0
- package/dist/processors/opmlProcessor.js +235 -0
- package/dist/processors/snap/helpers.d.ts +4 -0
- package/dist/processors/snap/helpers.js +27 -0
- package/dist/processors/snapProcessor.d.ts +44 -0
- package/dist/processors/snapProcessor.js +586 -0
- package/dist/processors/touchchat/helpers.d.ts +4 -0
- package/dist/processors/touchchat/helpers.js +27 -0
- package/dist/processors/touchchatProcessor.d.ts +27 -0
- package/dist/processors/touchchatProcessor.js +768 -0
- package/dist/types/aac.d.ts +47 -0
- package/dist/types/aac.js +2 -0
- package/docs/.keep +1 -0
- package/docs/ApplePanels.md +309 -0
- package/docs/Grid3-XML-Format.md +1788 -0
- package/docs/TobiiDynavox-Snap-Details.md +394 -0
- package/docs/asterics-Grid-fileformat-details.md +443 -0
- package/docs/obf_.obz Open Board File Formats.md +432 -0
- package/docs/touchchat.md +520 -0
- package/examples/.coverage +0 -0
- package/examples/.keep +1 -0
- package/examples/README.md +31 -0
- package/examples/communikate.dot +2637 -0
- package/examples/demo.js +143 -0
- package/examples/example-images.gridset +0 -0
- package/examples/example.ce +0 -0
- package/examples/example.dot +14 -0
- package/examples/example.grd +1 -0
- package/examples/example.gridset +0 -0
- package/examples/example.obf +27 -0
- package/examples/example.obz +0 -0
- package/examples/example.opml +18 -0
- package/examples/example.spb +0 -0
- package/examples/example.sps +0 -0
- package/examples/example2.grd +1 -0
- package/examples/gemini_response.txt +845 -0
- package/examples/image-map.js +45 -0
- package/examples/package-lock.json +1326 -0
- package/examples/package.json +10 -0
- package/examples/styled-output/converted-snap-to-touchchat.ce +0 -0
- package/examples/styled-output/styled-example.ce +0 -0
- package/examples/styled-output/styled-example.gridset +0 -0
- package/examples/styled-output/styled-example.obf +37 -0
- package/examples/styled-output/styled-example.spb +0 -0
- package/examples/styling-example.ts +316 -0
- package/examples/translate.js +39 -0
- package/examples/translate_demo.js +254 -0
- package/examples/translation_cache.json +44894 -0
- package/examples/typescript-demo.ts +251 -0
- package/examples/unified-interface-demo.ts +183 -0
- 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
|
+
}
|