@willwade/aac-processors 0.1.20 → 0.2.0
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/dist/browser/core/baseProcessor.js +4 -0
- package/dist/browser/processors/applePanelsProcessor.js +33 -40
- package/dist/browser/processors/astericsGridProcessor.js +31 -26
- package/dist/browser/processors/dotProcessor.js +11 -12
- package/dist/browser/processors/gridset/colorUtils.js +354 -0
- package/dist/browser/processors/gridset/helpers.js +60 -53
- package/dist/browser/processors/gridset/index.js +61 -0
- package/dist/browser/processors/gridset/styleHelpers.js +205 -0
- package/dist/browser/processors/gridset/symbolExtractor.js +331 -0
- package/dist/browser/processors/gridset/symbolSearch.js +248 -0
- package/dist/browser/processors/gridset/symbols.js +39 -72
- package/dist/browser/processors/gridsetProcessor.js +39 -48
- package/dist/browser/processors/obfProcessor.js +39 -53
- package/dist/browser/processors/opmlProcessor.js +11 -12
- package/dist/browser/processors/snap/helpers.js +57 -49
- package/dist/browser/processors/snapProcessor.js +48 -51
- package/dist/browser/processors/touchchatProcessor.js +60 -52
- package/dist/browser/utilities/analytics/history.js +24 -18
- package/dist/browser/utilities/analytics/metrics/comparison.js +16 -16
- package/dist/browser/utilities/analytics/metrics/vocabulary.js +2 -2
- package/dist/browser/utilities/analytics/reference/browser.js +16 -16
- package/dist/browser/utilities/analytics/reference/index.js +44 -35
- package/dist/browser/utils/io.js +78 -21
- package/dist/browser/utils/sqlite.js +8 -10
- package/dist/browser/utils/zip.js +43 -43
- package/dist/browser/validation/baseValidator.js +5 -0
- package/dist/browser/validation/gridsetValidator.js +12 -20
- package/dist/browser/validation/obfValidator.js +6 -5
- package/dist/browser/validation/snapValidator.js +11 -7
- package/dist/browser/validation/touchChatValidator.js +23 -13
- package/dist/cli/index.js +22 -24
- package/dist/core/baseProcessor.d.ts +7 -7
- package/dist/core/baseProcessor.js +4 -0
- package/dist/processors/applePanelsProcessor.js +32 -39
- package/dist/processors/astericsGridProcessor.d.ts +4 -4
- package/dist/processors/astericsGridProcessor.js +30 -25
- package/dist/processors/dotProcessor.js +10 -11
- package/dist/processors/excelProcessor.d.ts +3 -3
- package/dist/processors/excelProcessor.js +14 -20
- package/dist/processors/gridset/helpers.d.ts +12 -14
- package/dist/processors/gridset/helpers.js +60 -79
- package/dist/processors/gridset/imageDebug.d.ts +3 -5
- package/dist/processors/gridset/imageDebug.js +4 -4
- package/dist/processors/gridset/password.d.ts +1 -1
- package/dist/processors/gridset/symbolExtractor.d.ts +5 -3
- package/dist/processors/gridset/symbolExtractor.js +15 -38
- package/dist/processors/gridset/symbolSearch.d.ts +11 -10
- package/dist/processors/gridset/symbolSearch.js +29 -51
- package/dist/processors/gridset/symbols.d.ts +8 -6
- package/dist/processors/gridset/symbols.js +38 -71
- package/dist/processors/gridset/wordlistHelpers.d.ts +4 -6
- package/dist/processors/gridset/wordlistHelpers.js +15 -74
- package/dist/processors/gridsetProcessor.d.ts +2 -2
- package/dist/processors/gridsetProcessor.js +38 -70
- package/dist/processors/obfProcessor.d.ts +2 -2
- package/dist/processors/obfProcessor.js +38 -75
- package/dist/processors/obfsetProcessor.js +2 -3
- package/dist/processors/opmlProcessor.js +10 -11
- package/dist/processors/snap/helpers.d.ts +9 -9
- package/dist/processors/snap/helpers.js +58 -76
- package/dist/processors/snapProcessor.d.ts +2 -2
- package/dist/processors/snapProcessor.js +47 -50
- package/dist/processors/touchchatProcessor.d.ts +2 -2
- package/dist/processors/touchchatProcessor.js +59 -51
- package/dist/types/aac.d.ts +2 -2
- package/dist/utilities/analytics/history.d.ts +8 -8
- package/dist/utilities/analytics/history.js +24 -18
- package/dist/utilities/analytics/index.d.ts +3 -2
- package/dist/utilities/analytics/index.js +9 -10
- package/dist/utilities/analytics/metrics/comparison.d.ts +1 -1
- package/dist/utilities/analytics/metrics/comparison.js +16 -16
- package/dist/utilities/analytics/metrics/vocabulary.d.ts +1 -1
- package/dist/utilities/analytics/metrics/vocabulary.js +2 -2
- package/dist/utilities/analytics/reference/browser.d.ts +9 -9
- package/dist/utilities/analytics/reference/browser.js +16 -16
- package/dist/utilities/analytics/reference/index.d.ts +25 -23
- package/dist/utilities/analytics/reference/index.js +43 -34
- package/dist/utilities/symbolTools.d.ts +8 -6
- package/dist/utilities/symbolTools.js +21 -18
- package/dist/utils/io.d.ts +24 -6
- package/dist/utils/io.js +79 -25
- package/dist/utils/sqlite.d.ts +3 -1
- package/dist/utils/sqlite.js +7 -9
- package/dist/utils/zip.d.ts +7 -3
- package/dist/utils/zip.js +43 -43
- package/dist/validation/applePanelsValidator.d.ts +2 -1
- package/dist/validation/applePanelsValidator.js +10 -11
- package/dist/validation/astericsValidator.d.ts +2 -1
- package/dist/validation/astericsValidator.js +5 -4
- package/dist/validation/baseValidator.d.ts +2 -2
- package/dist/validation/baseValidator.js +5 -0
- package/dist/validation/dotValidator.d.ts +2 -1
- package/dist/validation/dotValidator.js +5 -4
- package/dist/validation/excelValidator.d.ts +2 -1
- package/dist/validation/excelValidator.js +5 -4
- package/dist/validation/gridsetValidator.d.ts +2 -1
- package/dist/validation/gridsetValidator.js +11 -22
- package/dist/validation/index.d.ts +2 -2
- package/dist/validation/index.js +5 -4
- package/dist/validation/obfValidator.d.ts +2 -1
- package/dist/validation/obfValidator.js +5 -4
- package/dist/validation/obfsetValidator.d.ts +2 -1
- package/dist/validation/obfsetValidator.js +5 -4
- package/dist/validation/opmlValidator.d.ts +2 -1
- package/dist/validation/opmlValidator.js +5 -4
- package/dist/validation/snapValidator.d.ts +2 -1
- package/dist/validation/snapValidator.js +10 -6
- package/dist/validation/touchChatValidator.d.ts +4 -6
- package/dist/validation/touchChatValidator.js +22 -12
- package/dist/validation/validationTypes.d.ts +8 -1
- package/package.json +1 -1
- package/dist/core/fileProcessor.d.ts +0 -7
- package/dist/core/fileProcessor.js +0 -57
|
@@ -2,9 +2,8 @@ import { BaseProcessor, } from '../core/baseProcessor';
|
|
|
2
2
|
import { AACTree, AACPage, AACButton, AACSemanticCategory, AACSemanticIntent, } from '../core/treeStructure';
|
|
3
3
|
import { generateCloneId } from '../utilities/analytics/utils/idGenerator';
|
|
4
4
|
import { SnapValidator } from '../validation/snapValidator';
|
|
5
|
-
import {
|
|
5
|
+
import { getNodeRequire, isNodeRuntime } from '../utils/io';
|
|
6
6
|
import { openSqliteDatabase, requireBetterSqlite3 } from '../utils/sqlite';
|
|
7
|
-
import { openZipFromInput } from '../utils/zip';
|
|
8
7
|
/**
|
|
9
8
|
* Convert a Buffer or Uint8Array to base64 string (browser and Node compatible)
|
|
10
9
|
* Node.js Buffers support toString('base64'), but Uint8Arrays in browser do not.
|
|
@@ -36,15 +35,15 @@ function mapSnapVisibility(visible) {
|
|
|
36
35
|
return visible === 0 ? 'Hidden' : 'Visible';
|
|
37
36
|
}
|
|
38
37
|
class SnapProcessor extends BaseProcessor {
|
|
39
|
-
constructor(symbolResolver = null, options
|
|
38
|
+
constructor(symbolResolver = null, options) {
|
|
40
39
|
super(options);
|
|
41
40
|
this.symbolResolver = null;
|
|
42
41
|
this.loadAudio = false;
|
|
43
42
|
this.pageLayoutPreference = 'scanning'; // Default to scanning for metrics
|
|
44
43
|
this.symbolResolver = symbolResolver;
|
|
45
|
-
this.loadAudio = options
|
|
44
|
+
this.loadAudio = options?.loadAudio !== undefined ? options.loadAudio : true;
|
|
46
45
|
this.pageLayoutPreference =
|
|
47
|
-
options
|
|
46
|
+
options?.pageLayoutPreference !== undefined ? options.pageLayoutPreference : 'scanning'; // Default to scanning
|
|
48
47
|
}
|
|
49
48
|
async extractTexts(filePathOrBuffer) {
|
|
50
49
|
const tree = await this.loadIntoTree(filePathOrBuffer);
|
|
@@ -65,7 +64,7 @@ class SnapProcessor extends BaseProcessor {
|
|
|
65
64
|
return texts;
|
|
66
65
|
}
|
|
67
66
|
async loadIntoTree(filePathOrBuffer) {
|
|
68
|
-
|
|
67
|
+
const { writeBinaryToPath, removePath, mkTempDir, basename, join } = this.options.fileAdapter;
|
|
69
68
|
const tree = new AACTree();
|
|
70
69
|
let dbResult = null;
|
|
71
70
|
let cleanupTempZip = null;
|
|
@@ -73,32 +72,32 @@ class SnapProcessor extends BaseProcessor {
|
|
|
73
72
|
// Handle .sub.zip files (Snap pageset backups containing .sps files)
|
|
74
73
|
let inputFile = filePathOrBuffer;
|
|
75
74
|
if (typeof filePathOrBuffer === 'string') {
|
|
76
|
-
const fileName =
|
|
75
|
+
const fileName = basename(filePathOrBuffer).toLowerCase();
|
|
77
76
|
if (fileName.endsWith('.sub.zip') || filePathOrBuffer.endsWith('.sub')) {
|
|
78
|
-
const fs = getFs();
|
|
79
|
-
const path = getPath();
|
|
80
|
-
const os = getOs();
|
|
81
77
|
// Extract .sub.zip to find the embedded .sps file
|
|
82
|
-
const tempDir =
|
|
83
|
-
const
|
|
78
|
+
const tempDir = await mkTempDir('snap-sub-');
|
|
79
|
+
const zip = await this.options.zipAdapter(filePathOrBuffer);
|
|
84
80
|
// Find the .sps file in the archive
|
|
85
81
|
const files = zip.listFiles();
|
|
86
82
|
const spsFile = files.find((f) => f.endsWith('.sps'));
|
|
87
83
|
if (!spsFile) {
|
|
88
|
-
|
|
84
|
+
await removePath(tempDir, { recursive: true, force: true });
|
|
89
85
|
throw new Error('No .sps file found in .sub.zip archive');
|
|
90
86
|
}
|
|
91
87
|
// Extract the .sps file
|
|
92
88
|
const spsData = await zip.readFile(spsFile);
|
|
93
|
-
const extractedSpsPath =
|
|
94
|
-
|
|
89
|
+
const extractedSpsPath = join(tempDir, basename(spsFile));
|
|
90
|
+
await writeBinaryToPath(extractedSpsPath, Buffer.from(spsData));
|
|
95
91
|
inputFile = extractedSpsPath;
|
|
96
|
-
cleanupTempZip = () => {
|
|
97
|
-
|
|
92
|
+
cleanupTempZip = async () => {
|
|
93
|
+
await removePath(tempDir, { recursive: true, force: true });
|
|
98
94
|
};
|
|
99
95
|
}
|
|
100
96
|
}
|
|
101
|
-
dbResult = await openSqliteDatabase(inputFile, {
|
|
97
|
+
dbResult = await openSqliteDatabase(inputFile, {
|
|
98
|
+
readonly: true,
|
|
99
|
+
fileAdapter: this.options.fileAdapter,
|
|
100
|
+
});
|
|
102
101
|
const db = dbResult.db;
|
|
103
102
|
const getTableColumns = (tableName) => {
|
|
104
103
|
try {
|
|
@@ -683,7 +682,7 @@ class SnapProcessor extends BaseProcessor {
|
|
|
683
682
|
}
|
|
684
683
|
finally {
|
|
685
684
|
if (dbResult?.cleanup) {
|
|
686
|
-
dbResult.cleanup();
|
|
685
|
+
await dbResult.cleanup();
|
|
687
686
|
}
|
|
688
687
|
else if (dbResult?.db) {
|
|
689
688
|
dbResult.db.close();
|
|
@@ -691,7 +690,7 @@ class SnapProcessor extends BaseProcessor {
|
|
|
691
690
|
// Clean up temporary extracted .sps file from .sub.zip
|
|
692
691
|
if (cleanupTempZip) {
|
|
693
692
|
try {
|
|
694
|
-
cleanupTempZip();
|
|
693
|
+
await cleanupTempZip();
|
|
695
694
|
}
|
|
696
695
|
catch (e) {
|
|
697
696
|
console.warn('[SnapProcessor] Failed to clean up temporary .sps file:', e);
|
|
@@ -700,21 +699,21 @@ class SnapProcessor extends BaseProcessor {
|
|
|
700
699
|
}
|
|
701
700
|
}
|
|
702
701
|
async processTexts(filePathOrBuffer, translations, outputPath) {
|
|
702
|
+
const { pathExists, mkDir, writeBinaryToPath, readBinaryFromInput, removePath, dirname } = this.options.fileAdapter;
|
|
703
703
|
if (!isNodeRuntime()) {
|
|
704
704
|
throw new Error('processTexts is only supported in Node.js environments for Snap files.');
|
|
705
705
|
}
|
|
706
|
-
const fs = getFs();
|
|
707
|
-
const path = getPath();
|
|
708
706
|
if (typeof filePathOrBuffer === 'string') {
|
|
709
707
|
const inputPath = filePathOrBuffer;
|
|
710
|
-
const outputDir =
|
|
711
|
-
|
|
712
|
-
|
|
708
|
+
const outputDir = dirname(outputPath);
|
|
709
|
+
const dirExists = await pathExists(outputDir);
|
|
710
|
+
if (!dirExists) {
|
|
711
|
+
await mkDir(outputDir, { recursive: true });
|
|
713
712
|
}
|
|
714
|
-
if (
|
|
715
|
-
|
|
713
|
+
if (await pathExists(outputPath)) {
|
|
714
|
+
await removePath(outputPath);
|
|
716
715
|
}
|
|
717
|
-
|
|
716
|
+
await writeBinaryToPath(outputPath, await readBinaryFromInput(inputPath));
|
|
718
717
|
const Database = requireBetterSqlite3();
|
|
719
718
|
const db = new Database(outputPath, { readonly: false });
|
|
720
719
|
try {
|
|
@@ -776,7 +775,7 @@ class SnapProcessor extends BaseProcessor {
|
|
|
776
775
|
finally {
|
|
777
776
|
db.close();
|
|
778
777
|
}
|
|
779
|
-
return
|
|
778
|
+
return await readBinaryFromInput(outputPath);
|
|
780
779
|
}
|
|
781
780
|
// Fallback for buffer inputs: rebuild from tree (may drop Snap assets)
|
|
782
781
|
const tree = await this.loadIntoTree(filePathOrBuffer);
|
|
@@ -803,21 +802,20 @@ class SnapProcessor extends BaseProcessor {
|
|
|
803
802
|
});
|
|
804
803
|
});
|
|
805
804
|
await this.saveFromTree(tree, outputPath);
|
|
806
|
-
return
|
|
805
|
+
return await readBinaryFromInput(outputPath);
|
|
807
806
|
}
|
|
808
807
|
async saveFromTree(tree, outputPath) {
|
|
808
|
+
const { pathExists, mkDir, removePath, dirname } = this.options.fileAdapter;
|
|
809
809
|
if (!isNodeRuntime()) {
|
|
810
810
|
throw new Error('saveFromTree is only supported in Node.js environments for Snap files.');
|
|
811
811
|
}
|
|
812
|
-
|
|
813
|
-
const
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
if (!fs.existsSync(outputDir)) {
|
|
817
|
-
fs.mkdirSync(outputDir, { recursive: true });
|
|
812
|
+
const outputDir = dirname(outputPath);
|
|
813
|
+
const dirExists = await pathExists(outputDir);
|
|
814
|
+
if (!dirExists) {
|
|
815
|
+
await mkDir(outputDir, { recursive: true });
|
|
818
816
|
}
|
|
819
|
-
if (
|
|
820
|
-
|
|
817
|
+
if (await pathExists(outputPath)) {
|
|
818
|
+
await removePath(outputPath);
|
|
821
819
|
}
|
|
822
820
|
// Create a new SQLite database for Snap format
|
|
823
821
|
const Database = requireBetterSqlite3();
|
|
@@ -1052,7 +1050,6 @@ class SnapProcessor extends BaseProcessor {
|
|
|
1052
1050
|
* Add audio recording to a button in the database
|
|
1053
1051
|
*/
|
|
1054
1052
|
async addAudioToButton(dbPath, buttonId, audioData, metadata) {
|
|
1055
|
-
await Promise.resolve();
|
|
1056
1053
|
if (!isNodeRuntime()) {
|
|
1057
1054
|
throw new Error('addAudioToButton is only supported in Node.js environments.');
|
|
1058
1055
|
}
|
|
@@ -1090,7 +1087,7 @@ class SnapProcessor extends BaseProcessor {
|
|
|
1090
1087
|
const updateButton = db.prepare('UPDATE Button SET MessageRecordingId = ?, UseMessageRecording = 1, SerializedMessageSoundMetadata = ? WHERE Id = ?');
|
|
1091
1088
|
const metadataJson = metadata ? JSON.stringify({ FileName: metadata }) : null;
|
|
1092
1089
|
updateButton.run(audioId, metadataJson, buttonId);
|
|
1093
|
-
return audioId;
|
|
1090
|
+
return Promise.resolve(audioId);
|
|
1094
1091
|
}
|
|
1095
1092
|
finally {
|
|
1096
1093
|
db.close();
|
|
@@ -1100,12 +1097,12 @@ class SnapProcessor extends BaseProcessor {
|
|
|
1100
1097
|
* Create a copy of the pageset with audio recordings added
|
|
1101
1098
|
*/
|
|
1102
1099
|
async createAudioEnhancedPageset(sourceDbPath, targetDbPath, audioMappings) {
|
|
1100
|
+
const { writeBinaryToPath, readBinaryFromInput } = this.options.fileAdapter;
|
|
1103
1101
|
if (!isNodeRuntime()) {
|
|
1104
1102
|
throw new Error('createAudioEnhancedPageset is only supported in Node.js environments.');
|
|
1105
1103
|
}
|
|
1106
|
-
const fs = getFs();
|
|
1107
1104
|
// Copy the source database to target
|
|
1108
|
-
|
|
1105
|
+
await writeBinaryToPath(targetDbPath, await readBinaryFromInput(sourceDbPath));
|
|
1109
1106
|
// Add audio recordings to the copy
|
|
1110
1107
|
for (const [buttonId, audioInfo] of audioMappings.entries()) {
|
|
1111
1108
|
await this.addAudioToButton(targetDbPath, buttonId, audioInfo.audioData, audioInfo.metadata);
|
|
@@ -1167,23 +1164,22 @@ class SnapProcessor extends BaseProcessor {
|
|
|
1167
1164
|
* @returns Promise with validation result
|
|
1168
1165
|
*/
|
|
1169
1166
|
async validate(filePath) {
|
|
1170
|
-
return SnapValidator.validateFile(filePath);
|
|
1167
|
+
return SnapValidator.validateFile(filePath, this.options.fileAdapter);
|
|
1171
1168
|
}
|
|
1172
1169
|
/**
|
|
1173
1170
|
* Get available PageLayouts for a Snap file
|
|
1174
1171
|
* Useful for UI components that want to let users select layout size
|
|
1175
1172
|
* @param filePath - Path to the Snap file
|
|
1176
|
-
* @returns
|
|
1173
|
+
* @returns Promise resolving to available PageLayouts with their dimensions
|
|
1177
1174
|
*/
|
|
1178
|
-
getAvailablePageLayouts(filePath) {
|
|
1175
|
+
async getAvailablePageLayouts(filePath) {
|
|
1176
|
+
const { writeBinaryToPath, removePath, pathExists, join } = this.options.fileAdapter;
|
|
1179
1177
|
if (!isNodeRuntime()) {
|
|
1180
1178
|
throw new Error('getAvailablePageLayouts is only supported in Node.js environments.');
|
|
1181
1179
|
}
|
|
1182
|
-
const
|
|
1183
|
-
const path = getPath();
|
|
1184
|
-
const dbPath = typeof filePath === 'string' ? filePath : path.join(process.cwd(), 'temp.spb');
|
|
1180
|
+
const dbPath = typeof filePath === 'string' ? filePath : join(process.cwd(), 'temp.spb');
|
|
1185
1181
|
if (Buffer.isBuffer(filePath)) {
|
|
1186
|
-
|
|
1182
|
+
await writeBinaryToPath(dbPath, filePath);
|
|
1187
1183
|
}
|
|
1188
1184
|
let db = null;
|
|
1189
1185
|
try {
|
|
@@ -1234,9 +1230,10 @@ class SnapProcessor extends BaseProcessor {
|
|
|
1234
1230
|
db.close();
|
|
1235
1231
|
}
|
|
1236
1232
|
// Clean up temporary file if created from buffer
|
|
1237
|
-
|
|
1233
|
+
const exists = await pathExists(dbPath);
|
|
1234
|
+
if (Buffer.isBuffer(filePath) && exists) {
|
|
1238
1235
|
try {
|
|
1239
|
-
|
|
1236
|
+
await removePath(dbPath);
|
|
1240
1237
|
}
|
|
1241
1238
|
catch (e) {
|
|
1242
1239
|
console.warn('Failed to clean up temporary file:', e);
|
|
@@ -3,10 +3,10 @@ import { AACTree, AACPage, AACButton, AACSemanticCategory, AACSemanticIntent, }
|
|
|
3
3
|
import { generateCloneId } from '../utilities/analytics/utils/idGenerator';
|
|
4
4
|
import { detectCasing, isNumericOrEmpty } from '../core/stringCasing';
|
|
5
5
|
import { TouchChatValidator } from '../validation/touchChatValidator';
|
|
6
|
-
import {
|
|
6
|
+
import { isNodeRuntime } from '../utils/io';
|
|
7
7
|
import { extractAllButtonsForTranslation, validateTranslationResults, } from '../utilities/translation/translationProcessor';
|
|
8
8
|
import { openSqliteDatabase, requireBetterSqlite3, } from '../utils/sqlite';
|
|
9
|
-
import {
|
|
9
|
+
import { getZipEntriesFromAdapter } from './gridset';
|
|
10
10
|
const toNumberOrUndefined = (value) => typeof value === 'number' ? value : undefined;
|
|
11
11
|
const toStringOrUndefined = (value) => typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
12
12
|
const toBooleanOrUndefined = (value) => typeof value === 'number' ? value !== 0 : undefined;
|
|
@@ -55,7 +55,7 @@ class TouchChatProcessor extends BaseProcessor {
|
|
|
55
55
|
return texts;
|
|
56
56
|
}
|
|
57
57
|
async loadIntoTree(filePathOrBuffer) {
|
|
58
|
-
|
|
58
|
+
const { readBinaryFromInput } = this.options.fileAdapter;
|
|
59
59
|
// Unzip .ce file, extract the .c4v SQLite DB, and parse pages/buttons
|
|
60
60
|
let db = null;
|
|
61
61
|
let cleanup;
|
|
@@ -63,16 +63,17 @@ class TouchChatProcessor extends BaseProcessor {
|
|
|
63
63
|
// Store source file path or buffer
|
|
64
64
|
this.sourceFile = filePathOrBuffer;
|
|
65
65
|
// Step 1: Unzip
|
|
66
|
-
const zipInput = readBinaryFromInput(filePathOrBuffer);
|
|
67
|
-
const
|
|
68
|
-
? await this.options.zipAdapter(zipInput)
|
|
69
|
-
: await openZipFromInput(zipInput);
|
|
66
|
+
const zipInput = await readBinaryFromInput(filePathOrBuffer);
|
|
67
|
+
const zip = await this.options.zipAdapter(zipInput);
|
|
70
68
|
const vocabEntry = zip.listFiles().find((name) => name.endsWith('.c4v'));
|
|
71
69
|
if (!vocabEntry) {
|
|
72
70
|
throw new Error('No .c4v vocab DB found in TouchChat export');
|
|
73
71
|
}
|
|
74
72
|
const dbBuffer = await zip.readFile(vocabEntry);
|
|
75
|
-
const dbResult = await openSqliteDatabase(dbBuffer, {
|
|
73
|
+
const dbResult = await openSqliteDatabase(dbBuffer, {
|
|
74
|
+
readonly: true,
|
|
75
|
+
fileAdapter: this.options.fileAdapter,
|
|
76
|
+
});
|
|
76
77
|
db = dbResult.db;
|
|
77
78
|
cleanup = dbResult.cleanup;
|
|
78
79
|
// Step 3: Create tree and load pages
|
|
@@ -467,7 +468,7 @@ class TouchChatProcessor extends BaseProcessor {
|
|
|
467
468
|
finally {
|
|
468
469
|
// Clean up
|
|
469
470
|
if (cleanup) {
|
|
470
|
-
cleanup();
|
|
471
|
+
await cleanup();
|
|
471
472
|
}
|
|
472
473
|
else if (db) {
|
|
473
474
|
db.close();
|
|
@@ -475,6 +476,7 @@ class TouchChatProcessor extends BaseProcessor {
|
|
|
475
476
|
}
|
|
476
477
|
}
|
|
477
478
|
async processTexts(filePathOrBuffer, translations, outputPath) {
|
|
479
|
+
const { pathExists, mkDir, removePath, mkTempDir, writeBinaryToPath, readBinaryFromInput, dirname, join, } = this.options.fileAdapter;
|
|
478
480
|
if (!isNodeRuntime()) {
|
|
479
481
|
throw new Error('processTexts is only supported in Node.js environments for TouchChat files.');
|
|
480
482
|
}
|
|
@@ -487,28 +489,25 @@ class TouchChatProcessor extends BaseProcessor {
|
|
|
487
489
|
* within the embedded SQLite database, ensuring assets and metadata remain intact.
|
|
488
490
|
*/
|
|
489
491
|
if (typeof filePathOrBuffer === 'string') {
|
|
490
|
-
const fs = getFs();
|
|
491
|
-
const path = getPath();
|
|
492
|
-
const os = getOs();
|
|
493
|
-
const AdmZip = getNodeRequire()('adm-zip');
|
|
494
492
|
const inputPath = filePathOrBuffer;
|
|
495
|
-
const outputDir =
|
|
496
|
-
|
|
497
|
-
|
|
493
|
+
const outputDir = dirname(outputPath);
|
|
494
|
+
const dirExists = await pathExists(outputDir);
|
|
495
|
+
if (!dirExists) {
|
|
496
|
+
await mkDir(outputDir, { recursive: true });
|
|
498
497
|
}
|
|
499
|
-
if (
|
|
500
|
-
|
|
498
|
+
if (await pathExists(outputPath)) {
|
|
499
|
+
await removePath(outputPath);
|
|
501
500
|
}
|
|
502
|
-
const zip =
|
|
503
|
-
const entries = zip
|
|
501
|
+
const zip = await this.options.zipAdapter(inputPath);
|
|
502
|
+
const entries = getZipEntriesFromAdapter(zip);
|
|
504
503
|
const vocabEntry = entries.find((entry) => entry.entryName.endsWith('.c4v'));
|
|
505
504
|
if (!vocabEntry) {
|
|
506
505
|
throw new Error('No .c4v vocab DB found in TouchChat export');
|
|
507
506
|
}
|
|
508
|
-
const tempDir =
|
|
509
|
-
const dbPath =
|
|
507
|
+
const tempDir = await mkTempDir('touchchat-translate-');
|
|
508
|
+
const dbPath = join(tempDir, 'vocab.c4v');
|
|
510
509
|
try {
|
|
511
|
-
|
|
510
|
+
await writeBinaryToPath(dbPath, await vocabEntry.getData());
|
|
512
511
|
const Database = requireBetterSqlite3();
|
|
513
512
|
const db = new Database(dbPath, { readonly: false });
|
|
514
513
|
try {
|
|
@@ -561,26 +560,34 @@ class TouchChatProcessor extends BaseProcessor {
|
|
|
561
560
|
finally {
|
|
562
561
|
db.close();
|
|
563
562
|
}
|
|
564
|
-
const outputZip =
|
|
565
|
-
|
|
563
|
+
const outputZip = await this.options.zipAdapter();
|
|
564
|
+
const files = [];
|
|
565
|
+
for (const entry of entries) {
|
|
566
566
|
if (entry.entryName === vocabEntry.entryName) {
|
|
567
|
-
|
|
567
|
+
continue;
|
|
568
568
|
}
|
|
569
|
-
const data =
|
|
570
|
-
|
|
569
|
+
const data = await entry.getData();
|
|
570
|
+
files.push({
|
|
571
|
+
name: entry.entryName,
|
|
572
|
+
data,
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
files.push({
|
|
576
|
+
name: vocabEntry.entryName,
|
|
577
|
+
data: await readBinaryFromInput(dbPath),
|
|
571
578
|
});
|
|
572
|
-
outputZip.
|
|
573
|
-
|
|
579
|
+
const zipData = await outputZip.writeFiles(files);
|
|
580
|
+
await writeBinaryToPath(outputPath, zipData);
|
|
574
581
|
}
|
|
575
582
|
finally {
|
|
576
583
|
try {
|
|
577
|
-
|
|
584
|
+
await removePath(tempDir, { recursive: true, force: true });
|
|
578
585
|
}
|
|
579
586
|
catch {
|
|
580
587
|
// Best-effort cleanup
|
|
581
588
|
}
|
|
582
589
|
}
|
|
583
|
-
return
|
|
590
|
+
return await readBinaryFromInput(outputPath);
|
|
584
591
|
}
|
|
585
592
|
// Fallback for buffer inputs: rebuild from tree (may drop TouchChat metadata)
|
|
586
593
|
const tree = await this.loadIntoTree(filePathOrBuffer);
|
|
@@ -607,20 +614,16 @@ class TouchChatProcessor extends BaseProcessor {
|
|
|
607
614
|
});
|
|
608
615
|
});
|
|
609
616
|
await this.saveFromTree(tree, outputPath);
|
|
610
|
-
|
|
611
|
-
return fs.readFileSync(outputPath);
|
|
617
|
+
return await readBinaryFromInput(outputPath);
|
|
612
618
|
}
|
|
613
619
|
async saveFromTree(tree, outputPath) {
|
|
614
|
-
|
|
620
|
+
const { writeBinaryToPath, mkTempDir, readBinaryFromInput, pathExists, removePath, join } = this.options.fileAdapter;
|
|
615
621
|
if (!isNodeRuntime()) {
|
|
616
622
|
throw new Error('saveFromTree is only supported in Node.js environments for TouchChat files.');
|
|
617
623
|
}
|
|
618
|
-
const fs = getFs();
|
|
619
|
-
const path = getPath();
|
|
620
|
-
const os = getOs();
|
|
621
624
|
// Create a TouchChat database that matches the expected schema for loading
|
|
622
|
-
const tmpDir =
|
|
623
|
-
const dbPath =
|
|
625
|
+
const tmpDir = await mkTempDir('touchchat-export-');
|
|
626
|
+
const dbPath = join(tmpDir, 'vocab.c4v');
|
|
624
627
|
try {
|
|
625
628
|
const Database = requireBetterSqlite3();
|
|
626
629
|
const db = new Database(dbPath);
|
|
@@ -913,15 +916,20 @@ class TouchChatProcessor extends BaseProcessor {
|
|
|
913
916
|
}
|
|
914
917
|
db.close();
|
|
915
918
|
// Create zip file with the database
|
|
916
|
-
const
|
|
917
|
-
const
|
|
918
|
-
zip.
|
|
919
|
-
|
|
919
|
+
const zip = await this.options.zipAdapter();
|
|
920
|
+
const data = await readBinaryFromInput(dbPath);
|
|
921
|
+
const zipData = await zip.writeFiles([
|
|
922
|
+
{
|
|
923
|
+
name: 'vocab.c4v',
|
|
924
|
+
data,
|
|
925
|
+
},
|
|
926
|
+
]);
|
|
927
|
+
await writeBinaryToPath(outputPath, zipData);
|
|
920
928
|
}
|
|
921
929
|
finally {
|
|
922
930
|
// Clean up
|
|
923
|
-
if (
|
|
924
|
-
|
|
931
|
+
if (await pathExists(tmpDir)) {
|
|
932
|
+
await removePath(tmpDir, { recursive: true, force: true });
|
|
925
933
|
}
|
|
926
934
|
}
|
|
927
935
|
}
|
|
@@ -1004,7 +1012,7 @@ class TouchChatProcessor extends BaseProcessor {
|
|
|
1004
1012
|
const outputPath = filePath.replace(/\.ce$/, '_translated.ce');
|
|
1005
1013
|
// Use existing processTexts method
|
|
1006
1014
|
await this.processTexts(filePath, translations, outputPath);
|
|
1007
|
-
return
|
|
1015
|
+
return outputPath;
|
|
1008
1016
|
}
|
|
1009
1017
|
catch (error) {
|
|
1010
1018
|
return Promise.reject(new Error(`Failed to generate translated download: ${error instanceof Error ? error.message : 'Unknown error'}`));
|
|
@@ -1016,7 +1024,7 @@ class TouchChatProcessor extends BaseProcessor {
|
|
|
1016
1024
|
* @returns Promise with validation result
|
|
1017
1025
|
*/
|
|
1018
1026
|
async validate(filePath) {
|
|
1019
|
-
return TouchChatValidator.validateFile(filePath);
|
|
1027
|
+
return await TouchChatValidator.validateFile(filePath, this.options.fileAdapter);
|
|
1020
1028
|
}
|
|
1021
1029
|
/**
|
|
1022
1030
|
* Extract symbol information from a TouchChat file for LLM-based translation.
|
|
@@ -1025,7 +1033,7 @@ class TouchChatProcessor extends BaseProcessor {
|
|
|
1025
1033
|
* This method uses shared translation utilities that work across all AAC formats.
|
|
1026
1034
|
*
|
|
1027
1035
|
* @param filePathOrBuffer - Path to TouchChat .ce file or buffer
|
|
1028
|
-
* @returns
|
|
1036
|
+
* @returns Promise resolving to symbol information for LLM processing
|
|
1029
1037
|
*/
|
|
1030
1038
|
async extractSymbolsForLLM(filePathOrBuffer) {
|
|
1031
1039
|
const tree = await this.loadIntoTree(filePathOrBuffer);
|
|
@@ -1055,9 +1063,10 @@ class TouchChatProcessor extends BaseProcessor {
|
|
|
1055
1063
|
* @param llmTranslations - Array of LLM translations with symbol info
|
|
1056
1064
|
* @param outputPath - Where to save the translated TouchChat file
|
|
1057
1065
|
* @param options - Translation options (e.g., allowPartial for testing)
|
|
1058
|
-
* @returns
|
|
1066
|
+
* @returns Promise resolving to a buffer of the translated TouchChat file
|
|
1059
1067
|
*/
|
|
1060
1068
|
async processLLMTranslations(filePathOrBuffer, llmTranslations, outputPath, options) {
|
|
1069
|
+
const { readBinaryFromInput } = this.options.fileAdapter;
|
|
1061
1070
|
if (!isNodeRuntime()) {
|
|
1062
1071
|
throw new Error('processLLMTranslations is only supported in Node.js environments for TouchChat files.');
|
|
1063
1072
|
}
|
|
@@ -1099,8 +1108,7 @@ class TouchChatProcessor extends BaseProcessor {
|
|
|
1099
1108
|
});
|
|
1100
1109
|
// Save and return
|
|
1101
1110
|
await this.saveFromTree(tree, outputPath);
|
|
1102
|
-
|
|
1103
|
-
return fs.readFileSync(outputPath);
|
|
1111
|
+
return await readBinaryFromInput(outputPath);
|
|
1104
1112
|
}
|
|
1105
1113
|
}
|
|
1106
1114
|
export { TouchChatProcessor };
|
|
@@ -60,8 +60,9 @@ export function exportHistoryToBaton(entries, options) {
|
|
|
60
60
|
/**
|
|
61
61
|
* Read Grid 3 phrase history from a history.sqlite database and tag entries with their source.
|
|
62
62
|
*/
|
|
63
|
-
export function readGrid3History(historyDbPath) {
|
|
64
|
-
|
|
63
|
+
export async function readGrid3History(historyDbPath) {
|
|
64
|
+
const history = await readGrid3HistoryImpl(historyDbPath);
|
|
65
|
+
return history.map((e) => ({
|
|
65
66
|
...e,
|
|
66
67
|
source: 'Grid',
|
|
67
68
|
}));
|
|
@@ -69,8 +70,9 @@ export function readGrid3History(historyDbPath) {
|
|
|
69
70
|
/**
|
|
70
71
|
* Read Grid 3 history for a specific user/language combination.
|
|
71
72
|
*/
|
|
72
|
-
export function readGrid3HistoryForUser(userName, langCode) {
|
|
73
|
-
|
|
73
|
+
export async function readGrid3HistoryForUser(userName, langCode) {
|
|
74
|
+
const history = await readGrid3HistoryForUserImpl(userName, langCode);
|
|
75
|
+
return history.map((e) => ({
|
|
74
76
|
...e,
|
|
75
77
|
source: 'Grid',
|
|
76
78
|
}));
|
|
@@ -78,39 +80,43 @@ export function readGrid3HistoryForUser(userName, langCode) {
|
|
|
78
80
|
/**
|
|
79
81
|
* Read every available Grid 3 history database on the machine.
|
|
80
82
|
*/
|
|
81
|
-
export function readAllGrid3History() {
|
|
82
|
-
|
|
83
|
+
export async function readAllGrid3History() {
|
|
84
|
+
const history = await readAllGrid3HistoryImpl();
|
|
85
|
+
return history.map((e) => ({ ...e, source: 'Grid' }));
|
|
83
86
|
}
|
|
84
87
|
/**
|
|
85
88
|
* Read Snap button usage from a pageset database and tag entries with source.
|
|
86
89
|
*/
|
|
87
|
-
export function readSnapUsage(pagesetPath) {
|
|
88
|
-
|
|
90
|
+
export async function readSnapUsage(pagesetPath) {
|
|
91
|
+
const usage = await readSnapUsageImpl(pagesetPath);
|
|
92
|
+
return usage.map((e) => ({ ...e, source: 'Snap' }));
|
|
89
93
|
}
|
|
90
94
|
/**
|
|
91
95
|
* Read Snap usage for a specific user across all discovered pagesets.
|
|
92
96
|
*/
|
|
93
|
-
export function readSnapUsageForUser(userId, packageNamePattern = 'TobiiDynavox') {
|
|
94
|
-
|
|
97
|
+
export async function readSnapUsageForUser(userId, packageNamePattern = 'TobiiDynavox') {
|
|
98
|
+
const usage = await readSnapUsageForUserImpl(userId, packageNamePattern);
|
|
99
|
+
return usage.map((e) => ({
|
|
95
100
|
...e,
|
|
96
101
|
source: 'Snap',
|
|
97
102
|
}));
|
|
98
103
|
}
|
|
99
|
-
export function listSnapUsers() {
|
|
100
|
-
return findSnapUsers();
|
|
104
|
+
export async function listSnapUsers() {
|
|
105
|
+
return await findSnapUsers();
|
|
101
106
|
}
|
|
102
107
|
/**
|
|
103
108
|
* List Grid 3 users on the current machine.
|
|
104
109
|
*/
|
|
105
|
-
export function listGrid3Users() {
|
|
106
|
-
return findGrid3Users();
|
|
110
|
+
export async function listGrid3Users() {
|
|
111
|
+
return await findGrid3Users();
|
|
107
112
|
}
|
|
108
113
|
/**
|
|
109
114
|
* Convenience helper to gather all available history across Grid 3 and Snap.
|
|
110
115
|
* Returns an empty array if no history files are present.
|
|
111
116
|
*/
|
|
112
|
-
export function collectUnifiedHistory() {
|
|
113
|
-
const gridHistory = readAllGrid3History();
|
|
114
|
-
const
|
|
115
|
-
|
|
117
|
+
export async function collectUnifiedHistory() {
|
|
118
|
+
const gridHistory = await readAllGrid3History();
|
|
119
|
+
const users = await findSnapUsers();
|
|
120
|
+
const snapHistory = await Promise.all(users.map(async (u) => await readSnapUsageForUser(u.userId)));
|
|
121
|
+
return [...gridHistory, ...snapHistory.flat()];
|
|
116
122
|
}
|
|
@@ -23,7 +23,7 @@ export class ComparisonAnalyzer {
|
|
|
23
23
|
/**
|
|
24
24
|
* Compare two board sets
|
|
25
25
|
*/
|
|
26
|
-
compare(targetResult, compareResult, options) {
|
|
26
|
+
async compare(targetResult, compareResult, options) {
|
|
27
27
|
// Create base result from target
|
|
28
28
|
const baseResult = { ...targetResult };
|
|
29
29
|
// Create word maps with normalized keys
|
|
@@ -77,7 +77,7 @@ export class ComparisonAnalyzer {
|
|
|
77
77
|
};
|
|
78
78
|
});
|
|
79
79
|
// Calculate CARE components
|
|
80
|
-
const careComponents = this.calculateCareComponents(targetResult, compareResult, overlappingWords, options);
|
|
80
|
+
const careComponents = await this.calculateCareComponents(targetResult, compareResult, overlappingWords, options);
|
|
81
81
|
// Analyze high/low effort words
|
|
82
82
|
const highEffortWords = [];
|
|
83
83
|
const lowEffortWords = [];
|
|
@@ -110,7 +110,7 @@ export class ComparisonAnalyzer {
|
|
|
110
110
|
// Sentence analysis
|
|
111
111
|
let sentences = [];
|
|
112
112
|
if (options?.includeSentences) {
|
|
113
|
-
const testSentences = this.referenceLoader.loadSentences();
|
|
113
|
+
const testSentences = await this.referenceLoader.loadSentences();
|
|
114
114
|
const targetSentences = this.sentenceAnalyzer.analyzeSentences(targetResult, testSentences);
|
|
115
115
|
const compareSentences = this.sentenceAnalyzer.analyzeSentences(compareResult, testSentences);
|
|
116
116
|
sentences = targetSentences.map((ts, idx) => ({
|
|
@@ -123,7 +123,7 @@ export class ComparisonAnalyzer {
|
|
|
123
123
|
}));
|
|
124
124
|
}
|
|
125
125
|
// Core vocabulary analysis
|
|
126
|
-
const coreLists = this.referenceLoader.loadCoreLists();
|
|
126
|
+
const coreLists = await this.referenceLoader.loadCoreLists();
|
|
127
127
|
const cores = {};
|
|
128
128
|
coreLists.forEach((list) => {
|
|
129
129
|
let targetTotal = 0;
|
|
@@ -171,8 +171,8 @@ export class ComparisonAnalyzer {
|
|
|
171
171
|
}
|
|
172
172
|
});
|
|
173
173
|
// Fringe vocabulary analysis
|
|
174
|
-
const fringeWords = this.analyzeFringe(targetWords, compareWords);
|
|
175
|
-
const commonFringeWords = this.analyzeCommonFringe(targetWords, compareWords);
|
|
174
|
+
const fringeWords = await this.analyzeFringe(targetWords, compareWords);
|
|
175
|
+
const commonFringeWords = await this.analyzeCommonFringe(targetWords, compareWords);
|
|
176
176
|
return {
|
|
177
177
|
...baseResult,
|
|
178
178
|
buttons: enrichedButtons,
|
|
@@ -214,9 +214,9 @@ export class ComparisonAnalyzer {
|
|
|
214
214
|
/**
|
|
215
215
|
* Calculate CARE component scores
|
|
216
216
|
*/
|
|
217
|
-
calculateCareComponents(targetResult, compareResult, _overlappingWords, options) {
|
|
217
|
+
async calculateCareComponents(targetResult, compareResult, _overlappingWords, options) {
|
|
218
218
|
// Load common words with baseline efforts (matching Ruby line 527-534)
|
|
219
|
-
const commonWordsData = this.referenceLoader.loadCommonWords();
|
|
219
|
+
const commonWordsData = await this.referenceLoader.loadCommonWords();
|
|
220
220
|
const commonWords = new Map();
|
|
221
221
|
commonWordsData.words.forEach((word) => {
|
|
222
222
|
commonWords.set(word.toLowerCase(), commonWordsData.efforts[word] || 0);
|
|
@@ -268,10 +268,10 @@ export class ComparisonAnalyzer {
|
|
|
268
268
|
}
|
|
269
269
|
});
|
|
270
270
|
// Load reference data
|
|
271
|
-
const coreLists = this.referenceLoader.loadCoreLists();
|
|
272
|
-
const fringe = this.referenceLoader.loadFringe();
|
|
273
|
-
const commonFringe = this.referenceLoader.loadCommonFringe();
|
|
274
|
-
const sentences = this.referenceLoader.loadSentences();
|
|
271
|
+
const coreLists = await this.referenceLoader.loadCoreLists();
|
|
272
|
+
const fringe = await this.referenceLoader.loadFringe();
|
|
273
|
+
const commonFringe = await this.referenceLoader.loadCommonFringe();
|
|
274
|
+
const sentences = await this.referenceLoader.loadSentences();
|
|
275
275
|
// Calculate core coverage and effort (matching Ruby lines 609-647)
|
|
276
276
|
let coreCount = 0;
|
|
277
277
|
let compCoreCount = 0;
|
|
@@ -423,8 +423,8 @@ export class ComparisonAnalyzer {
|
|
|
423
423
|
/**
|
|
424
424
|
* Analyze fringe vocabulary
|
|
425
425
|
*/
|
|
426
|
-
analyzeFringe(targetWords, compareWords) {
|
|
427
|
-
const fringe = this.referenceLoader.loadFringe();
|
|
426
|
+
async analyzeFringe(targetWords, compareWords) {
|
|
427
|
+
const fringe = await this.referenceLoader.loadFringe();
|
|
428
428
|
const result = [];
|
|
429
429
|
fringe.forEach((word) => {
|
|
430
430
|
const key = this.normalize(word);
|
|
@@ -444,8 +444,8 @@ export class ComparisonAnalyzer {
|
|
|
444
444
|
/**
|
|
445
445
|
* Analyze common fringe vocabulary
|
|
446
446
|
*/
|
|
447
|
-
analyzeCommonFringe(targetWords, compareWords) {
|
|
448
|
-
const fringe = this.referenceLoader.loadFringe();
|
|
447
|
+
async analyzeCommonFringe(targetWords, compareWords) {
|
|
448
|
+
const fringe = await this.referenceLoader.loadFringe();
|
|
449
449
|
const result = [];
|
|
450
450
|
fringe.forEach((word) => {
|
|
451
451
|
const key = this.normalize(word);
|