@willwade/aac-processors 0.1.7 → 0.1.9
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/analytics.d.ts +7 -0
- package/dist/analytics.js +23 -0
- package/dist/browser/index.browser.js +5 -0
- package/dist/browser/metrics.js +17 -0
- package/dist/browser/processors/gridset/helpers.js +390 -0
- package/dist/browser/processors/gridset/pluginTypes.js +1 -0
- package/dist/browser/processors/gridsetProcessor.js +68 -1
- package/dist/browser/processors/obfProcessor.js +21 -13
- package/dist/browser/processors/snap/helpers.js +252 -0
- package/dist/browser/utilities/analytics/history.js +116 -0
- package/dist/browser/utilities/analytics/metrics/comparison.js +477 -0
- package/dist/browser/utilities/analytics/metrics/core.js +775 -0
- package/dist/browser/utilities/analytics/metrics/effort.js +221 -0
- package/dist/browser/utilities/analytics/metrics/obl-types.js +6 -0
- package/dist/browser/utilities/analytics/metrics/obl.js +282 -0
- package/dist/browser/utilities/analytics/metrics/sentence.js +121 -0
- package/dist/browser/utilities/analytics/metrics/types.js +6 -0
- package/dist/browser/utilities/analytics/metrics/vocabulary.js +138 -0
- package/dist/browser/utilities/analytics/reference/browser.js +67 -0
- package/dist/browser/utilities/analytics/reference/index.js +129 -0
- package/dist/browser/utils/dotnetTicks.js +17 -0
- package/dist/index.browser.d.ts +1 -0
- package/dist/index.browser.js +18 -1
- package/dist/index.node.d.ts +2 -2
- package/dist/index.node.js +5 -5
- package/dist/metrics.d.ts +17 -0
- package/dist/metrics.js +44 -0
- package/dist/processors/gridset/pluginTypes.d.ts +1 -0
- package/dist/processors/gridset/pluginTypes.js +1 -0
- package/dist/processors/gridsetProcessor.js +68 -1
- package/dist/processors/obfProcessor.js +21 -13
- package/dist/utilities/analytics/metrics/comparison.d.ts +2 -1
- package/dist/utilities/analytics/metrics/comparison.js +3 -3
- package/dist/utilities/analytics/metrics/vocabulary.d.ts +2 -2
- package/dist/utilities/analytics/reference/browser.d.ts +31 -0
- package/dist/utilities/analytics/reference/browser.js +73 -0
- package/dist/utilities/analytics/reference/index.d.ts +21 -0
- package/dist/utilities/analytics/reference/index.js +22 -46
- package/package.json +9 -1
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { AACSemanticCategory, AACSemanticIntent } from '../../core/treeStructure';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import Database from 'better-sqlite3';
|
|
5
|
+
import { dotNetTicksToDate } from '../../utils/dotnetTicks';
|
|
6
|
+
// Minimal Snap helpers (stubs) to align with processors/<engine>/helpers pattern
|
|
7
|
+
// NOTE: Snap buttons currently do not populate resolvedImageEntry; these helpers
|
|
8
|
+
// therefore return empty collections until image resolution is implemented.
|
|
9
|
+
function collectFiles(root, matcher, maxDepth = 3) {
|
|
10
|
+
const results = new Set();
|
|
11
|
+
const stack = [{ dir: root, depth: 0 }];
|
|
12
|
+
while (stack.length > 0) {
|
|
13
|
+
const current = stack.pop();
|
|
14
|
+
if (!current)
|
|
15
|
+
continue;
|
|
16
|
+
if (current.depth > maxDepth)
|
|
17
|
+
continue;
|
|
18
|
+
let entries;
|
|
19
|
+
try {
|
|
20
|
+
entries = fs.readdirSync(current.dir, { withFileTypes: true });
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
for (const entry of entries) {
|
|
26
|
+
const fullPath = path.join(current.dir, entry.name);
|
|
27
|
+
if (entry.isDirectory()) {
|
|
28
|
+
stack.push({ dir: fullPath, depth: current.depth + 1 });
|
|
29
|
+
}
|
|
30
|
+
else if (matcher(fullPath)) {
|
|
31
|
+
results.add(fullPath);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return Array.from(results);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Build a map of button IDs to resolved image entries for a specific page.
|
|
39
|
+
* Mirrors the Grid helper for consumers that expect image reference data.
|
|
40
|
+
*/
|
|
41
|
+
export function getPageTokenImageMap(tree, pageId) {
|
|
42
|
+
const map = new Map();
|
|
43
|
+
const page = tree.getPage(pageId);
|
|
44
|
+
if (!page)
|
|
45
|
+
return map;
|
|
46
|
+
for (const btn of page.buttons) {
|
|
47
|
+
if (btn.resolvedImageEntry)
|
|
48
|
+
map.set(btn.id, String(btn.resolvedImageEntry));
|
|
49
|
+
}
|
|
50
|
+
return map;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Collect all image entry paths referenced in a Snap tree.
|
|
54
|
+
* Currently empty until resolvedImageEntry is populated by the processor.
|
|
55
|
+
*/
|
|
56
|
+
export function getAllowedImageEntries(_tree) {
|
|
57
|
+
return new Set();
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Read a binary asset from a Snap pageset.
|
|
61
|
+
* Not implemented yet; provided for API symmetry with other processors.
|
|
62
|
+
*/
|
|
63
|
+
export function openImage(_dbOrFile, _entryPath) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Find Tobii Communicator Snap package paths
|
|
68
|
+
* Searches in %LOCALAPPDATA%\Packages for Snap-related packages
|
|
69
|
+
* @param packageNamePattern Optional pattern to filter package names (default: 'TobiiDynavox')
|
|
70
|
+
* @returns Array of Snap package path information
|
|
71
|
+
*/
|
|
72
|
+
export function findSnapPackages(packageNamePattern = 'TobiiDynavox') {
|
|
73
|
+
const results = [];
|
|
74
|
+
// Only works on Windows
|
|
75
|
+
if (process.platform !== 'win32') {
|
|
76
|
+
return results;
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
const localAppData = process.env.LOCALAPPDATA;
|
|
80
|
+
if (!localAppData) {
|
|
81
|
+
return results;
|
|
82
|
+
}
|
|
83
|
+
const packagesPath = path.join(localAppData, 'Packages');
|
|
84
|
+
// Check if Packages directory exists
|
|
85
|
+
if (!fs.existsSync(packagesPath)) {
|
|
86
|
+
return results;
|
|
87
|
+
}
|
|
88
|
+
// Enumerate packages
|
|
89
|
+
const packages = fs.readdirSync(packagesPath, { withFileTypes: true });
|
|
90
|
+
for (const packageDir of packages) {
|
|
91
|
+
if (!packageDir.isDirectory())
|
|
92
|
+
continue;
|
|
93
|
+
const packageName = packageDir.name;
|
|
94
|
+
// Filter by pattern
|
|
95
|
+
if (packageName.includes(packageNamePattern)) {
|
|
96
|
+
results.push({
|
|
97
|
+
packageName,
|
|
98
|
+
packagePath: path.join(packagesPath, packageName),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
// Silently fail if directory access fails
|
|
105
|
+
}
|
|
106
|
+
return results;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Find the first Snap package path matching the pattern
|
|
110
|
+
* Convenience method for when you expect only one Snap installation
|
|
111
|
+
* @param packageNamePattern Optional pattern to filter package names (default: 'TobiiDynavox')
|
|
112
|
+
* @returns Path to the first matching Snap package, or null if not found
|
|
113
|
+
*/
|
|
114
|
+
export function findSnapPackagePath(packageNamePattern = 'TobiiDynavox') {
|
|
115
|
+
const packages = findSnapPackages(packageNamePattern);
|
|
116
|
+
return packages.length > 0 ? packages[0].packagePath : null;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Find Snap user directories and their vocab files (.sps/.spb)
|
|
120
|
+
* Typical path:
|
|
121
|
+
* C:\Users\{username}\AppData\Roaming\Tobii Dynavox\Snap Scene\Users\{userId}\
|
|
122
|
+
* @param packageNamePattern Optional package filter (default TobiiDynavox)
|
|
123
|
+
* @returns Array of user info with vocab paths
|
|
124
|
+
*/
|
|
125
|
+
export function findSnapUsers(packageNamePattern = 'TobiiDynavox') {
|
|
126
|
+
const results = [];
|
|
127
|
+
if (process.platform !== 'win32') {
|
|
128
|
+
return results;
|
|
129
|
+
}
|
|
130
|
+
const packagePath = findSnapPackagePath(packageNamePattern);
|
|
131
|
+
if (!packagePath) {
|
|
132
|
+
return results;
|
|
133
|
+
}
|
|
134
|
+
const usersRoot = path.join(packagePath, 'LocalState', 'Users');
|
|
135
|
+
if (!fs.existsSync(usersRoot)) {
|
|
136
|
+
return results;
|
|
137
|
+
}
|
|
138
|
+
const entries = fs.readdirSync(usersRoot, { withFileTypes: true });
|
|
139
|
+
for (const entry of entries) {
|
|
140
|
+
if (!entry.isDirectory())
|
|
141
|
+
continue;
|
|
142
|
+
if (entry.name.toLowerCase().startsWith('swiftkey'))
|
|
143
|
+
continue;
|
|
144
|
+
const userPath = path.join(usersRoot, entry.name);
|
|
145
|
+
const vocabPaths = collectFiles(userPath, (full) => {
|
|
146
|
+
const ext = path.extname(full).toLowerCase();
|
|
147
|
+
return ext === '.sps' || ext === '.spb';
|
|
148
|
+
}, 2);
|
|
149
|
+
results.push({
|
|
150
|
+
userId: entry.name,
|
|
151
|
+
userPath,
|
|
152
|
+
vocabPaths,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
return results;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Find vocab files for a specific Snap user (or all users)
|
|
159
|
+
* @param userId Optional user identifier filter (case-sensitive directory name)
|
|
160
|
+
* @param packageNamePattern Optional package filter
|
|
161
|
+
* @returns Array of vocab file paths
|
|
162
|
+
*/
|
|
163
|
+
export function findSnapUserVocabularies(userId, packageNamePattern = 'TobiiDynavox') {
|
|
164
|
+
const users = findSnapUsers(packageNamePattern).filter((u) => !userId || u.userId === userId);
|
|
165
|
+
return users.flatMap((u) => u.vocabPaths);
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Attempt to find history/analytics files for a Snap user by name
|
|
169
|
+
* Currently searches for files containing "history" under the user directory
|
|
170
|
+
* @param userId User identifier (directory name)
|
|
171
|
+
* @param packageNamePattern Optional package filter
|
|
172
|
+
* @returns Array of history file paths (may be empty if not found)
|
|
173
|
+
*/
|
|
174
|
+
export function findSnapUserHistory(userId, packageNamePattern = 'TobiiDynavox') {
|
|
175
|
+
const user = findSnapUsers(packageNamePattern).find((u) => u.userId === userId);
|
|
176
|
+
if (!user)
|
|
177
|
+
return [];
|
|
178
|
+
return collectFiles(user.userPath, (full) => path.basename(full).toLowerCase().includes('history'), 2);
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Check whether TD Snap appears to be installed (Windows only)
|
|
182
|
+
*/
|
|
183
|
+
export function isSnapInstalled(packageNamePattern = 'TobiiDynavox') {
|
|
184
|
+
if (process.platform !== 'win32')
|
|
185
|
+
return false;
|
|
186
|
+
return Boolean(findSnapPackagePath(packageNamePattern));
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Read Snap usage history from a pageset file (.sps/.spb)
|
|
190
|
+
*/
|
|
191
|
+
export function readSnapUsage(pagesetPath) {
|
|
192
|
+
if (!fs.existsSync(pagesetPath))
|
|
193
|
+
return [];
|
|
194
|
+
const db = new Database(pagesetPath, { readonly: true });
|
|
195
|
+
const tableCheck = db
|
|
196
|
+
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name IN ('ButtonUsage','Button')")
|
|
197
|
+
.all();
|
|
198
|
+
if (tableCheck.length < 2)
|
|
199
|
+
return [];
|
|
200
|
+
const rows = db
|
|
201
|
+
.prepare(`
|
|
202
|
+
SELECT
|
|
203
|
+
bu.ButtonUniqueId as ButtonId,
|
|
204
|
+
bu.Timestamp as TickValue,
|
|
205
|
+
bu.Modeling as Modeling,
|
|
206
|
+
bu.AccessMethod as AccessMethod,
|
|
207
|
+
b.Label as Label,
|
|
208
|
+
b.Message as Message
|
|
209
|
+
FROM ButtonUsage bu
|
|
210
|
+
LEFT JOIN Button b ON bu.ButtonUniqueId = b.UniqueId
|
|
211
|
+
WHERE bu.Timestamp IS NOT NULL
|
|
212
|
+
ORDER BY bu.Timestamp ASC
|
|
213
|
+
`)
|
|
214
|
+
.all();
|
|
215
|
+
const events = new Map();
|
|
216
|
+
for (const row of rows) {
|
|
217
|
+
const buttonId = row.ButtonId ?? 'unknown';
|
|
218
|
+
const label = row.Label ?? undefined;
|
|
219
|
+
const message = row.Message ?? undefined;
|
|
220
|
+
const content = message || label || '';
|
|
221
|
+
const entry = events.get(buttonId) ??
|
|
222
|
+
{
|
|
223
|
+
id: `snap:${buttonId}`,
|
|
224
|
+
content,
|
|
225
|
+
occurrences: [],
|
|
226
|
+
platform: {
|
|
227
|
+
label,
|
|
228
|
+
message,
|
|
229
|
+
buttonId,
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
entry.occurrences.push({
|
|
233
|
+
timestamp: dotNetTicksToDate(BigInt(row.TickValue ?? 0)),
|
|
234
|
+
modeling: row.Modeling === 1,
|
|
235
|
+
accessMethod: row.AccessMethod ?? null,
|
|
236
|
+
type: 'button',
|
|
237
|
+
buttonId: row.ButtonId,
|
|
238
|
+
intent: AACSemanticIntent.SPEAK_TEXT,
|
|
239
|
+
category: AACSemanticCategory.COMMUNICATION,
|
|
240
|
+
});
|
|
241
|
+
events.set(buttonId, entry);
|
|
242
|
+
}
|
|
243
|
+
return Array.from(events.values());
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Read Snap usage history for a user (all pagesets)
|
|
247
|
+
*/
|
|
248
|
+
export function readSnapUsageForUser(userId, packageNamePattern = 'TobiiDynavox') {
|
|
249
|
+
const users = findSnapUsers(packageNamePattern).filter((u) => !userId || u.userId === userId);
|
|
250
|
+
const pagesets = users.flatMap((u) => u.vocabPaths);
|
|
251
|
+
return pagesets.flatMap((p) => readSnapUsage(p));
|
|
252
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { dotNetTicksToDate } from '../../utils/dotnetTicks';
|
|
2
|
+
import { findGrid3Users, readAllGrid3History as readAllGrid3HistoryImpl, readGrid3History as readGrid3HistoryImpl, readGrid3HistoryForUser as readGrid3HistoryForUserImpl, } from '../../processors/gridset/helpers';
|
|
3
|
+
import { findSnapUsers, readSnapUsage as readSnapUsageImpl, readSnapUsageForUser as readSnapUsageForUserImpl, } from '../../processors/snap/helpers';
|
|
4
|
+
export { dotNetTicksToDate };
|
|
5
|
+
const generateUuid = () => {
|
|
6
|
+
if (typeof globalThis.crypto?.randomUUID === 'function') {
|
|
7
|
+
return globalThis.crypto.randomUUID();
|
|
8
|
+
}
|
|
9
|
+
// RFC4122-ish fallback for Node without crypto.randomUUID
|
|
10
|
+
const hex = '0123456789abcdef';
|
|
11
|
+
const bytes = Array.from({ length: 16 }, () => Math.floor(Math.random() * 256));
|
|
12
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
|
13
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
14
|
+
const toHex = (b) => hex[(b >> 4) & 0x0f] + hex[b & 0x0f];
|
|
15
|
+
return (toHex(bytes[0]) +
|
|
16
|
+
toHex(bytes[1]) +
|
|
17
|
+
toHex(bytes[2]) +
|
|
18
|
+
toHex(bytes[3]) +
|
|
19
|
+
'-' +
|
|
20
|
+
toHex(bytes[4]) +
|
|
21
|
+
toHex(bytes[5]) +
|
|
22
|
+
'-' +
|
|
23
|
+
toHex(bytes[6]) +
|
|
24
|
+
toHex(bytes[7]) +
|
|
25
|
+
'-' +
|
|
26
|
+
toHex(bytes[8]) +
|
|
27
|
+
toHex(bytes[9]) +
|
|
28
|
+
'-' +
|
|
29
|
+
toHex(bytes[10]) +
|
|
30
|
+
toHex(bytes[11]) +
|
|
31
|
+
toHex(bytes[12]) +
|
|
32
|
+
toHex(bytes[13]) +
|
|
33
|
+
toHex(bytes[14]) +
|
|
34
|
+
toHex(bytes[15]));
|
|
35
|
+
};
|
|
36
|
+
export function exportHistoryToBaton(entries, options) {
|
|
37
|
+
const exportDate = options?.exportDate instanceof Date
|
|
38
|
+
? options.exportDate.toISOString()
|
|
39
|
+
: options?.exportDate || new Date().toISOString();
|
|
40
|
+
const anonymousUUID = options?.anonymousUUID || generateUuid();
|
|
41
|
+
const sentences = entries.map((entry) => ({
|
|
42
|
+
uuid: generateUuid(),
|
|
43
|
+
anonymousUUID,
|
|
44
|
+
content: entry.content,
|
|
45
|
+
metadata: entry.occurrences.map((occ) => ({
|
|
46
|
+
timestamp: occ.timestamp.toISOString(),
|
|
47
|
+
latitude: occ.latitude ?? null,
|
|
48
|
+
longitude: occ.longitude ?? null,
|
|
49
|
+
})),
|
|
50
|
+
source: entry.source,
|
|
51
|
+
}));
|
|
52
|
+
return {
|
|
53
|
+
version: options?.version || '1.0',
|
|
54
|
+
exportDate,
|
|
55
|
+
encryption: options?.encryption || 'none',
|
|
56
|
+
sentenceCount: sentences.length,
|
|
57
|
+
sentences,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Read Grid 3 phrase history from a history.sqlite database and tag entries with their source.
|
|
62
|
+
*/
|
|
63
|
+
export function readGrid3History(historyDbPath) {
|
|
64
|
+
return readGrid3HistoryImpl(historyDbPath).map((e) => ({
|
|
65
|
+
...e,
|
|
66
|
+
source: 'Grid',
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Read Grid 3 history for a specific user/language combination.
|
|
71
|
+
*/
|
|
72
|
+
export function readGrid3HistoryForUser(userName, langCode) {
|
|
73
|
+
return readGrid3HistoryForUserImpl(userName, langCode).map((e) => ({
|
|
74
|
+
...e,
|
|
75
|
+
source: 'Grid',
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Read every available Grid 3 history database on the machine.
|
|
80
|
+
*/
|
|
81
|
+
export function readAllGrid3History() {
|
|
82
|
+
return readAllGrid3HistoryImpl().map((e) => ({ ...e, source: 'Grid' }));
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Read Snap button usage from a pageset database and tag entries with source.
|
|
86
|
+
*/
|
|
87
|
+
export function readSnapUsage(pagesetPath) {
|
|
88
|
+
return readSnapUsageImpl(pagesetPath).map((e) => ({ ...e, source: 'Snap' }));
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Read Snap usage for a specific user across all discovered pagesets.
|
|
92
|
+
*/
|
|
93
|
+
export function readSnapUsageForUser(userId, packageNamePattern = 'TobiiDynavox') {
|
|
94
|
+
return readSnapUsageForUserImpl(userId, packageNamePattern).map((e) => ({
|
|
95
|
+
...e,
|
|
96
|
+
source: 'Snap',
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
99
|
+
export function listSnapUsers() {
|
|
100
|
+
return findSnapUsers();
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* List Grid 3 users on the current machine.
|
|
104
|
+
*/
|
|
105
|
+
export function listGrid3Users() {
|
|
106
|
+
return findGrid3Users();
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Convenience helper to gather all available history across Grid 3 and Snap.
|
|
110
|
+
* Returns an empty array if no history files are present.
|
|
111
|
+
*/
|
|
112
|
+
export function collectUnifiedHistory() {
|
|
113
|
+
const gridHistory = readAllGrid3History();
|
|
114
|
+
const snapHistory = findSnapUsers().flatMap((u) => readSnapUsageForUser(u.userId));
|
|
115
|
+
return [...gridHistory, ...snapHistory];
|
|
116
|
+
}
|