@willwade/aac-processors 0.0.12 → 0.0.14
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/README.md +46 -44
- package/dist/core/baseProcessor.d.ts +41 -0
- package/dist/core/baseProcessor.js +41 -0
- package/dist/core/treeStructure.d.ts +35 -2
- package/dist/core/treeStructure.js +18 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/processors/astericsGridProcessor.d.ts +15 -0
- package/dist/processors/astericsGridProcessor.js +17 -0
- package/dist/processors/gridset/helpers.d.ts +4 -1
- package/dist/processors/gridset/helpers.js +4 -0
- package/dist/processors/gridset/pluginTypes.js +51 -50
- package/dist/processors/gridset/symbolAlignment.d.ts +125 -0
- package/dist/processors/gridset/symbolAlignment.js +283 -0
- package/dist/processors/gridset/symbolExtractor.js +3 -2
- package/dist/processors/gridset/symbolSearch.js +9 -7
- package/dist/processors/gridsetProcessor.d.ts +26 -0
- package/dist/processors/gridsetProcessor.js +178 -25
- package/dist/processors/obfProcessor.d.ts +26 -0
- package/dist/processors/obfProcessor.js +94 -1
- package/dist/processors/snap/helpers.d.ts +5 -1
- package/dist/processors/snap/helpers.js +5 -0
- package/dist/processors/snapProcessor.d.ts +2 -0
- package/dist/processors/snapProcessor.js +156 -5
- package/dist/processors/touchchatProcessor.d.ts +26 -0
- package/dist/processors/touchchatProcessor.js +106 -6
- package/dist/types/aac.d.ts +63 -0
- package/dist/types/aac.js +33 -0
- package/dist/{optional → utilities}/analytics/history.d.ts +12 -1
- package/dist/{optional → utilities}/analytics/index.d.ts +2 -0
- package/dist/{optional → utilities}/analytics/index.js +6 -1
- package/dist/{optional → utilities}/analytics/metrics/comparison.js +8 -4
- package/dist/{optional → utilities}/analytics/metrics/core.d.ts +9 -0
- package/dist/{optional → utilities}/analytics/metrics/core.js +190 -37
- package/dist/{optional → utilities}/analytics/metrics/effort.d.ts +10 -0
- package/dist/{optional → utilities}/analytics/metrics/effort.js +13 -0
- package/dist/utilities/analytics/metrics/obl-types.d.ts +93 -0
- package/dist/utilities/analytics/metrics/obl-types.js +7 -0
- package/dist/utilities/analytics/metrics/obl.d.ts +40 -0
- package/dist/utilities/analytics/metrics/obl.js +287 -0
- package/dist/{optional → utilities}/analytics/metrics/vocabulary.js +6 -4
- package/dist/{optional → utilities}/symbolTools.js +13 -16
- package/dist/utilities/translation/translationProcessor.d.ts +119 -0
- package/dist/utilities/translation/translationProcessor.js +204 -0
- package/dist/validation/gridsetValidator.js +10 -0
- package/package.json +1 -1
- /package/dist/{optional → utilities}/analytics/history.js +0 -0
- /package/dist/{optional → utilities}/analytics/metrics/comparison.d.ts +0 -0
- /package/dist/{optional → utilities}/analytics/metrics/index.d.ts +0 -0
- /package/dist/{optional → utilities}/analytics/metrics/index.js +0 -0
- /package/dist/{optional → utilities}/analytics/metrics/sentence.d.ts +0 -0
- /package/dist/{optional → utilities}/analytics/metrics/sentence.js +0 -0
- /package/dist/{optional → utilities}/analytics/metrics/types.d.ts +0 -0
- /package/dist/{optional → utilities}/analytics/metrics/types.js +0 -0
- /package/dist/{optional → utilities}/analytics/metrics/vocabulary.d.ts +0 -0
- /package/dist/{optional → utilities}/analytics/reference/index.d.ts +0 -0
- /package/dist/{optional → utilities}/analytics/reference/index.js +0 -0
- /package/dist/{optional → utilities}/analytics/utils/idGenerator.d.ts +0 -0
- /package/dist/{optional → utilities}/analytics/utils/idGenerator.js +0 -0
- /package/dist/{optional → utilities}/symbolTools.d.ts +0 -0
|
@@ -106,10 +106,10 @@ function detectPluginCellType(content) {
|
|
|
106
106
|
if (!content) {
|
|
107
107
|
return { cellType: Grid3CellType.Regular };
|
|
108
108
|
}
|
|
109
|
-
const contentType = content.ContentType || content.contenttype
|
|
110
|
-
const contentSubType = content.ContentSubType || content.contentsubtype
|
|
109
|
+
const contentType = content.ContentType || content.contenttype;
|
|
110
|
+
const contentSubType = content.ContentSubType || content.contentsubtype;
|
|
111
111
|
// Workspace cells - full editing workspaces
|
|
112
|
-
if (contentType === 'Workspace') {
|
|
112
|
+
if (contentType === 'Workspace' || content.Style?.BasedOnStyle === 'Workspace') {
|
|
113
113
|
return {
|
|
114
114
|
cellType: Grid3CellType.Workspace,
|
|
115
115
|
subType: contentSubType || undefined,
|
|
@@ -118,7 +118,7 @@ function detectPluginCellType(content) {
|
|
|
118
118
|
};
|
|
119
119
|
}
|
|
120
120
|
// LiveCell detection - dynamic content displays
|
|
121
|
-
if (contentType === 'LiveCell') {
|
|
121
|
+
if (contentType === 'LiveCell' || content.Style?.BasedOnStyle === 'LiveCell') {
|
|
122
122
|
return {
|
|
123
123
|
cellType: Grid3CellType.LiveCell,
|
|
124
124
|
liveCellType: contentSubType || undefined,
|
|
@@ -127,7 +127,7 @@ function detectPluginCellType(content) {
|
|
|
127
127
|
};
|
|
128
128
|
}
|
|
129
129
|
// AutoContent detection - dynamic word/content suggestions
|
|
130
|
-
if (contentType === 'AutoContent') {
|
|
130
|
+
if (contentType === 'AutoContent' || content.Style?.BasedOnStyle === 'AutoContent') {
|
|
131
131
|
const autoContentType = extractAutoContentType(content);
|
|
132
132
|
return {
|
|
133
133
|
cellType: Grid3CellType.AutoContent,
|
|
@@ -172,38 +172,38 @@ function inferWorkspacePlugin(subType) {
|
|
|
172
172
|
return undefined;
|
|
173
173
|
const normalized = subType.toLowerCase();
|
|
174
174
|
if (normalized.includes('chat'))
|
|
175
|
-
return '
|
|
175
|
+
return 'Grid3.Chat';
|
|
176
176
|
if (normalized.includes('email') || normalized.includes('mail'))
|
|
177
|
-
return '
|
|
178
|
-
if (normalized.includes('word') || normalized.includes('
|
|
179
|
-
return '
|
|
177
|
+
return 'Grid3.Email';
|
|
178
|
+
if (normalized.includes('word') || normalized.includes('doc'))
|
|
179
|
+
return 'Grid3.WordProcessor';
|
|
180
180
|
if (normalized.includes('phone'))
|
|
181
|
-
return '
|
|
181
|
+
return 'Grid3.Phone';
|
|
182
182
|
if (normalized.includes('sms') || normalized.includes('text'))
|
|
183
|
-
return '
|
|
184
|
-
if (normalized.includes('
|
|
185
|
-
return '
|
|
186
|
-
if (normalized.includes('computer')
|
|
187
|
-
return '
|
|
188
|
-
if (normalized.includes('
|
|
189
|
-
return '
|
|
190
|
-
if (normalized.includes('timer')
|
|
191
|
-
return '
|
|
183
|
+
return 'Grid3.Sms';
|
|
184
|
+
if (normalized.includes('browser') || normalized.includes('web'))
|
|
185
|
+
return 'Grid3.WebBrowser';
|
|
186
|
+
if (normalized.includes('computer'))
|
|
187
|
+
return 'Grid3.ComputerControl';
|
|
188
|
+
if (normalized.includes('calc'))
|
|
189
|
+
return 'Grid3.Calculator';
|
|
190
|
+
if (normalized.includes('timer'))
|
|
191
|
+
return 'Grid3.Timer';
|
|
192
192
|
if (normalized.includes('music') || normalized.includes('video'))
|
|
193
|
-
return '
|
|
193
|
+
return 'Grid3.MusicVideo';
|
|
194
194
|
if (normalized.includes('photo') || normalized.includes('image'))
|
|
195
|
-
return '
|
|
195
|
+
return 'Grid3.Photos';
|
|
196
196
|
if (normalized.includes('contact'))
|
|
197
|
-
return '
|
|
197
|
+
return 'Grid3.Contacts';
|
|
198
198
|
if (normalized.includes('learning'))
|
|
199
|
-
return '
|
|
200
|
-
if (normalized.includes('message') && normalized.includes('
|
|
201
|
-
return '
|
|
202
|
-
if (normalized.includes('
|
|
203
|
-
return '
|
|
204
|
-
if (normalized.includes('
|
|
205
|
-
return '
|
|
206
|
-
return
|
|
199
|
+
return 'Grid3.InteractiveLearning';
|
|
200
|
+
if (normalized.includes('message') && normalized.includes('banking'))
|
|
201
|
+
return 'Grid3.MessageBanking';
|
|
202
|
+
if (normalized.includes('control'))
|
|
203
|
+
return 'Grid3.EnvironmentControl';
|
|
204
|
+
if (normalized.includes('settings'))
|
|
205
|
+
return 'Grid3.Settings';
|
|
206
|
+
return `Grid3.${subType}`;
|
|
207
207
|
}
|
|
208
208
|
/**
|
|
209
209
|
* Infer plugin ID from live cell type
|
|
@@ -212,24 +212,25 @@ function inferLiveCellPlugin(liveCellType) {
|
|
|
212
212
|
if (!liveCellType)
|
|
213
213
|
return undefined;
|
|
214
214
|
const normalized = liveCellType.toLowerCase();
|
|
215
|
-
if (normalized.includes('clock')
|
|
216
|
-
return '
|
|
217
|
-
|
|
215
|
+
if (normalized.includes('clock'))
|
|
216
|
+
return 'Grid3.Clock';
|
|
217
|
+
if (normalized.includes('date'))
|
|
218
|
+
return 'Grid3.Clock';
|
|
218
219
|
if (normalized.includes('volume'))
|
|
219
|
-
return '
|
|
220
|
+
return 'Grid3.Volume';
|
|
220
221
|
if (normalized.includes('speed'))
|
|
221
|
-
return '
|
|
222
|
+
return 'Grid3.Speed';
|
|
222
223
|
if (normalized.includes('voice'))
|
|
223
|
-
return '
|
|
224
|
+
return 'Grid3.Speech';
|
|
224
225
|
if (normalized.includes('message'))
|
|
225
|
-
return '
|
|
226
|
+
return 'Grid3.Chat';
|
|
226
227
|
if (normalized.includes('battery'))
|
|
227
|
-
return '
|
|
228
|
-
if (normalized.includes('wifi')
|
|
229
|
-
return '
|
|
228
|
+
return 'Grid3.Battery';
|
|
229
|
+
if (normalized.includes('wifi'))
|
|
230
|
+
return 'Grid3.Wifi';
|
|
230
231
|
if (normalized.includes('bluetooth'))
|
|
231
|
-
return '
|
|
232
|
-
return
|
|
232
|
+
return 'Grid3.Bluetooth';
|
|
233
|
+
return `Grid3.${liveCellType}`;
|
|
233
234
|
}
|
|
234
235
|
/**
|
|
235
236
|
* Infer plugin ID from auto content type
|
|
@@ -239,24 +240,24 @@ function inferAutoContentPlugin(autoContentType) {
|
|
|
239
240
|
return undefined;
|
|
240
241
|
const normalized = autoContentType.toLowerCase();
|
|
241
242
|
if (normalized.includes('voice') || normalized.includes('speed'))
|
|
242
|
-
return '
|
|
243
|
+
return 'Grid3.Speech';
|
|
243
244
|
if (normalized.includes('email') || normalized.includes('mail'))
|
|
244
|
-
return '
|
|
245
|
+
return 'Grid3.Email';
|
|
245
246
|
if (normalized.includes('phone'))
|
|
246
|
-
return '
|
|
247
|
+
return 'Grid3.Phone';
|
|
247
248
|
if (normalized.includes('sms') || normalized.includes('text'))
|
|
248
|
-
return '
|
|
249
|
+
return 'Grid3.Sms';
|
|
249
250
|
if (normalized.includes('web') ||
|
|
250
251
|
normalized.includes('favorite') ||
|
|
251
252
|
normalized.includes('history')) {
|
|
252
|
-
return '
|
|
253
|
+
return 'Grid3.WebBrowser';
|
|
253
254
|
}
|
|
254
255
|
if (normalized.includes('prediction'))
|
|
255
|
-
return '
|
|
256
|
+
return 'Grid3.Prediction';
|
|
256
257
|
if (normalized.includes('grammar'))
|
|
257
|
-
return '
|
|
258
|
+
return 'Grid3.Grammar';
|
|
258
259
|
if (normalized.includes('context'))
|
|
259
|
-
return '
|
|
260
|
+
return 'Grid3.AutoContent';
|
|
260
261
|
return undefined;
|
|
261
262
|
}
|
|
262
263
|
/**
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Symbol Alignment for Translation
|
|
3
|
+
*
|
|
4
|
+
* Utilities to preserve symbol positions during text translation.
|
|
5
|
+
* When translating AAC gridset messages that contain symbols attached
|
|
6
|
+
* to specific words, we need to maintain the symbol-to-word associations
|
|
7
|
+
* across languages.
|
|
8
|
+
*
|
|
9
|
+
* Example:
|
|
10
|
+
* English: "I want apple juice" with apple symbol on "apple"
|
|
11
|
+
* Spanish: "Yo quiero jugo de manzana" with apple symbol on "manzana"
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Represents a symbol anchored to a specific word in the text
|
|
15
|
+
*/
|
|
16
|
+
export interface SymbolAnchor {
|
|
17
|
+
symbolRef: string;
|
|
18
|
+
wordIndex: number;
|
|
19
|
+
originalWord: string;
|
|
20
|
+
startPos: number;
|
|
21
|
+
endPos: number;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Parsed message with symbol anchors
|
|
25
|
+
*/
|
|
26
|
+
export interface ParsedMessage {
|
|
27
|
+
text: string;
|
|
28
|
+
words: string[];
|
|
29
|
+
symbols: SymbolAnchor[];
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Translation result with preserved symbols
|
|
33
|
+
*/
|
|
34
|
+
export interface TranslatedMessage {
|
|
35
|
+
text: string;
|
|
36
|
+
alignment: {
|
|
37
|
+
originalWord: string;
|
|
38
|
+
translatedWord: string;
|
|
39
|
+
originalIndex: number;
|
|
40
|
+
translatedIndex: number;
|
|
41
|
+
}[];
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Parse a message to extract text and symbol anchors.
|
|
45
|
+
*
|
|
46
|
+
* This handles various formats:
|
|
47
|
+
* 1. Plain text with no symbols
|
|
48
|
+
* 2. Rich text with embedded symbol markers (future enhancement)
|
|
49
|
+
* 3. Text where symbols are tracked separately (via richText.symbols)
|
|
50
|
+
*
|
|
51
|
+
* For now, this assumes symbols are tracked separately in the richText structure.
|
|
52
|
+
* The text itself is plain, and we need to tokenize it to find word positions.
|
|
53
|
+
*
|
|
54
|
+
* @param message - The message text (may contain words or be plain)
|
|
55
|
+
* @param richTextSymbols - Optional symbols from richText.symbols array
|
|
56
|
+
* @returns Parsed message with word positions and symbol anchors
|
|
57
|
+
*/
|
|
58
|
+
export declare function parseMessageWithSymbols(message: string, richTextSymbols?: Array<{
|
|
59
|
+
text: string;
|
|
60
|
+
image?: string;
|
|
61
|
+
}>): ParsedMessage;
|
|
62
|
+
/**
|
|
63
|
+
* Align words from original text to translated text.
|
|
64
|
+
*
|
|
65
|
+
* This is a simple alignment strategy that works for many cases:
|
|
66
|
+
* 1. Exact word matching (for cognates, names, numbers)
|
|
67
|
+
* 2. Position-based alignment (assumes similar word order)
|
|
68
|
+
*
|
|
69
|
+
* For more accurate alignment, you could integrate with:
|
|
70
|
+
* - Translation APIs that return alignment (e.g., Google Translate's word alignment)
|
|
71
|
+
* - Statistical machine translation alignment tools
|
|
72
|
+
* - Bilingual dictionaries
|
|
73
|
+
*
|
|
74
|
+
* @param originalWords - Words from the original text
|
|
75
|
+
* @param translatedWords - Words from the translated text
|
|
76
|
+
* @returns Alignment mapping between original and translated word indices
|
|
77
|
+
*/
|
|
78
|
+
export declare function alignWords(originalWords: string[], translatedWords: string[]): TranslatedMessage['alignment'];
|
|
79
|
+
/**
|
|
80
|
+
* Reattach symbols to translated text based on word alignment.
|
|
81
|
+
*
|
|
82
|
+
* @param translatedText - The translated plain text
|
|
83
|
+
* @param originalParsed - The original parsed message with symbols
|
|
84
|
+
* @param alignment - Word alignment between original and translation
|
|
85
|
+
* @returns Translated text with symbols embedded (as rich text structure)
|
|
86
|
+
*/
|
|
87
|
+
export declare function reattachSymbols(translatedText: string, originalParsed: ParsedMessage, alignment: TranslatedMessage['alignment']): {
|
|
88
|
+
text: string;
|
|
89
|
+
richTextSymbols: Array<{
|
|
90
|
+
text: string;
|
|
91
|
+
image?: string;
|
|
92
|
+
}>;
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* Complete pipeline: translate a message while preserving symbol positions.
|
|
96
|
+
*
|
|
97
|
+
* @param originalMessage - The original message text
|
|
98
|
+
* @param translatedText - The translated text (from translation API)
|
|
99
|
+
* @param richTextSymbols - Original symbols from richText.symbols
|
|
100
|
+
* @returns Object with translated text and aligned symbols
|
|
101
|
+
*/
|
|
102
|
+
export declare function translateWithSymbols(originalMessage: string, translatedText: string, richTextSymbols?: Array<{
|
|
103
|
+
text: string;
|
|
104
|
+
image?: string;
|
|
105
|
+
}>): {
|
|
106
|
+
text: string;
|
|
107
|
+
richTextSymbols: Array<{
|
|
108
|
+
text: string;
|
|
109
|
+
image?: string;
|
|
110
|
+
}>;
|
|
111
|
+
};
|
|
112
|
+
/**
|
|
113
|
+
* Extract symbols from a button for use during translation.
|
|
114
|
+
*
|
|
115
|
+
* This helper extracts symbols from either:
|
|
116
|
+
* - button.semanticAction.richText.symbols
|
|
117
|
+
* - button.image (if it's a symbol library reference)
|
|
118
|
+
*
|
|
119
|
+
* @param button - The AAC button
|
|
120
|
+
* @returns Array of symbol attachments
|
|
121
|
+
*/
|
|
122
|
+
export declare function extractSymbolsFromButton(button: any): Array<{
|
|
123
|
+
text: string;
|
|
124
|
+
image?: string;
|
|
125
|
+
}> | undefined;
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Symbol Alignment for Translation
|
|
4
|
+
*
|
|
5
|
+
* Utilities to preserve symbol positions during text translation.
|
|
6
|
+
* When translating AAC gridset messages that contain symbols attached
|
|
7
|
+
* to specific words, we need to maintain the symbol-to-word associations
|
|
8
|
+
* across languages.
|
|
9
|
+
*
|
|
10
|
+
* Example:
|
|
11
|
+
* English: "I want apple juice" with apple symbol on "apple"
|
|
12
|
+
* Spanish: "Yo quiero jugo de manzana" with apple symbol on "manzana"
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.parseMessageWithSymbols = parseMessageWithSymbols;
|
|
16
|
+
exports.alignWords = alignWords;
|
|
17
|
+
exports.reattachSymbols = reattachSymbols;
|
|
18
|
+
exports.translateWithSymbols = translateWithSymbols;
|
|
19
|
+
exports.extractSymbolsFromButton = extractSymbolsFromButton;
|
|
20
|
+
/**
|
|
21
|
+
* Parse a message to extract text and symbol anchors.
|
|
22
|
+
*
|
|
23
|
+
* This handles various formats:
|
|
24
|
+
* 1. Plain text with no symbols
|
|
25
|
+
* 2. Rich text with embedded symbol markers (future enhancement)
|
|
26
|
+
* 3. Text where symbols are tracked separately (via richText.symbols)
|
|
27
|
+
*
|
|
28
|
+
* For now, this assumes symbols are tracked separately in the richText structure.
|
|
29
|
+
* The text itself is plain, and we need to tokenize it to find word positions.
|
|
30
|
+
*
|
|
31
|
+
* @param message - The message text (may contain words or be plain)
|
|
32
|
+
* @param richTextSymbols - Optional symbols from richText.symbols array
|
|
33
|
+
* @returns Parsed message with word positions and symbol anchors
|
|
34
|
+
*/
|
|
35
|
+
function parseMessageWithSymbols(message, richTextSymbols) {
|
|
36
|
+
// Normalize whitespace for consistent tokenization
|
|
37
|
+
const normalizedMessage = message.trim().replace(/\s+/g, ' ');
|
|
38
|
+
// Tokenize into words, preserving punctuation
|
|
39
|
+
const words = [];
|
|
40
|
+
const wordPositions = [];
|
|
41
|
+
// Split by whitespace but track positions
|
|
42
|
+
let currentPos = 0;
|
|
43
|
+
const parts = normalizedMessage.split(/(\s+)/); // Keep delimiters
|
|
44
|
+
for (const part of parts) {
|
|
45
|
+
if (part.trim().length > 0) {
|
|
46
|
+
// This is a word
|
|
47
|
+
const startPos = currentPos;
|
|
48
|
+
const endPos = currentPos + part.length;
|
|
49
|
+
words.push(part);
|
|
50
|
+
wordPositions.push({ start: startPos, end: endPos, word: part });
|
|
51
|
+
currentPos = endPos;
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
// This is whitespace
|
|
55
|
+
currentPos += part.length;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// Extract symbol anchors from richText.symbols if provided
|
|
59
|
+
const symbols = [];
|
|
60
|
+
if (richTextSymbols && richTextSymbols.length > 0) {
|
|
61
|
+
for (const sym of richTextSymbols) {
|
|
62
|
+
// Find which word this symbol is attached to
|
|
63
|
+
const wordIndex = words.findIndex((w) => w === sym.text);
|
|
64
|
+
if (wordIndex !== -1) {
|
|
65
|
+
const pos = wordPositions[wordIndex];
|
|
66
|
+
symbols.push({
|
|
67
|
+
symbolRef: sym.image || '',
|
|
68
|
+
wordIndex,
|
|
69
|
+
originalWord: sym.text,
|
|
70
|
+
startPos: pos.start,
|
|
71
|
+
endPos: pos.end,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
// Fuzzy match - find closest word (handles case differences, punctuation)
|
|
76
|
+
const normalizedSymText = sym.text.toLowerCase().replace(/[^\w]/g, '');
|
|
77
|
+
const fuzzyIndex = words.findIndex((w) => w.toLowerCase().replace(/[^\w]/g, '') === normalizedSymText);
|
|
78
|
+
if (fuzzyIndex !== -1) {
|
|
79
|
+
const pos = wordPositions[fuzzyIndex];
|
|
80
|
+
symbols.push({
|
|
81
|
+
symbolRef: sym.image || '',
|
|
82
|
+
wordIndex: fuzzyIndex,
|
|
83
|
+
originalWord: words[fuzzyIndex],
|
|
84
|
+
startPos: pos.start,
|
|
85
|
+
endPos: pos.end,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
text: normalizedMessage,
|
|
93
|
+
words,
|
|
94
|
+
symbols,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Align words from original text to translated text.
|
|
99
|
+
*
|
|
100
|
+
* This is a simple alignment strategy that works for many cases:
|
|
101
|
+
* 1. Exact word matching (for cognates, names, numbers)
|
|
102
|
+
* 2. Position-based alignment (assumes similar word order)
|
|
103
|
+
*
|
|
104
|
+
* For more accurate alignment, you could integrate with:
|
|
105
|
+
* - Translation APIs that return alignment (e.g., Google Translate's word alignment)
|
|
106
|
+
* - Statistical machine translation alignment tools
|
|
107
|
+
* - Bilingual dictionaries
|
|
108
|
+
*
|
|
109
|
+
* @param originalWords - Words from the original text
|
|
110
|
+
* @param translatedWords - Words from the translated text
|
|
111
|
+
* @returns Alignment mapping between original and translated word indices
|
|
112
|
+
*/
|
|
113
|
+
function alignWords(originalWords, translatedWords) {
|
|
114
|
+
const alignment = [];
|
|
115
|
+
// Strategy 1: Try to match identical words (numbers, names, cognates)
|
|
116
|
+
const matchedTranslatedIndices = new Set();
|
|
117
|
+
for (let origIdx = 0; origIdx < originalWords.length; origIdx++) {
|
|
118
|
+
const origWord = originalWords[origIdx];
|
|
119
|
+
const normalizedOrig = origWord.toLowerCase().replace(/[^\w]/g, '');
|
|
120
|
+
// Try to find this word in the translation
|
|
121
|
+
for (let transIdx = 0; transIdx < translatedWords.length; transIdx++) {
|
|
122
|
+
if (matchedTranslatedIndices.has(transIdx))
|
|
123
|
+
continue;
|
|
124
|
+
const transWord = translatedWords[transIdx];
|
|
125
|
+
const normalizedTrans = transWord.toLowerCase().replace(/[^\w]/g, '');
|
|
126
|
+
// Exact match (case-insensitive, ignoring punctuation)
|
|
127
|
+
if (normalizedOrig === normalizedTrans && normalizedOrig.length > 0) {
|
|
128
|
+
alignment.push({
|
|
129
|
+
originalWord: origWord,
|
|
130
|
+
translatedWord: transWord,
|
|
131
|
+
originalIndex: origIdx,
|
|
132
|
+
translatedIndex: transIdx,
|
|
133
|
+
});
|
|
134
|
+
matchedTranslatedIndices.add(transIdx);
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// Strategy 2: For unmatched words, use positional alignment
|
|
140
|
+
// This is a simple fallback that assumes similar word order
|
|
141
|
+
for (let origIdx = 0; origIdx < originalWords.length; origIdx++) {
|
|
142
|
+
if (alignment.find((a) => a.originalIndex === origIdx))
|
|
143
|
+
continue; // Already matched
|
|
144
|
+
// Find the closest unmatched position in translation
|
|
145
|
+
let bestTransIdx = -1;
|
|
146
|
+
let minDistance = Infinity;
|
|
147
|
+
for (let transIdx = 0; transIdx < translatedWords.length; transIdx++) {
|
|
148
|
+
if (matchedTranslatedIndices.has(transIdx))
|
|
149
|
+
continue;
|
|
150
|
+
// Calculate relative position
|
|
151
|
+
const relativeOrigPos = origIdx / originalWords.length;
|
|
152
|
+
const relativeTransPos = transIdx / translatedWords.length;
|
|
153
|
+
const distance = Math.abs(relativeOrigPos - relativeTransPos);
|
|
154
|
+
if (distance < minDistance) {
|
|
155
|
+
minDistance = distance;
|
|
156
|
+
bestTransIdx = transIdx;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (bestTransIdx !== -1) {
|
|
160
|
+
alignment.push({
|
|
161
|
+
originalWord: originalWords[origIdx],
|
|
162
|
+
translatedWord: translatedWords[bestTransIdx],
|
|
163
|
+
originalIndex: origIdx,
|
|
164
|
+
translatedIndex: bestTransIdx,
|
|
165
|
+
});
|
|
166
|
+
matchedTranslatedIndices.add(bestTransIdx);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return alignment;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Reattach symbols to translated text based on word alignment.
|
|
173
|
+
*
|
|
174
|
+
* @param translatedText - The translated plain text
|
|
175
|
+
* @param originalParsed - The original parsed message with symbols
|
|
176
|
+
* @param alignment - Word alignment between original and translation
|
|
177
|
+
* @returns Translated text with symbols embedded (as rich text structure)
|
|
178
|
+
*/
|
|
179
|
+
function reattachSymbols(translatedText, originalParsed, alignment) {
|
|
180
|
+
// Tokenize the translated text
|
|
181
|
+
const translatedWords = translatedText
|
|
182
|
+
.trim()
|
|
183
|
+
.replace(/\s+/g, ' ')
|
|
184
|
+
.split(/\s+/)
|
|
185
|
+
.filter((w) => w.length > 0);
|
|
186
|
+
// Create the rich text symbols array
|
|
187
|
+
const richTextSymbols = [];
|
|
188
|
+
for (const symbol of originalParsed.symbols) {
|
|
189
|
+
// Find the alignment for this word
|
|
190
|
+
const wordAlignment = alignment.find((a) => a.originalIndex === symbol.wordIndex);
|
|
191
|
+
if (wordAlignment && wordAlignment.translatedIndex < translatedWords.length) {
|
|
192
|
+
const translatedWord = translatedWords[wordAlignment.translatedIndex];
|
|
193
|
+
// Attach the symbol to the translated word
|
|
194
|
+
richTextSymbols.push({
|
|
195
|
+
text: translatedWord,
|
|
196
|
+
image: symbol.symbolRef,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
// Fallback: keep symbol on original word if no alignment found
|
|
201
|
+
richTextSymbols.push({
|
|
202
|
+
text: symbol.originalWord,
|
|
203
|
+
image: symbol.symbolRef,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
text: translatedText,
|
|
209
|
+
richTextSymbols,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Complete pipeline: translate a message while preserving symbol positions.
|
|
214
|
+
*
|
|
215
|
+
* @param originalMessage - The original message text
|
|
216
|
+
* @param translatedText - The translated text (from translation API)
|
|
217
|
+
* @param richTextSymbols - Original symbols from richText.symbols
|
|
218
|
+
* @returns Object with translated text and aligned symbols
|
|
219
|
+
*/
|
|
220
|
+
function translateWithSymbols(originalMessage, translatedText, richTextSymbols) {
|
|
221
|
+
// Step 1: Parse original message
|
|
222
|
+
const parsedOriginal = parseMessageWithSymbols(originalMessage, richTextSymbols);
|
|
223
|
+
// If no symbols, return as-is
|
|
224
|
+
if (parsedOriginal.symbols.length === 0) {
|
|
225
|
+
return {
|
|
226
|
+
text: translatedText,
|
|
227
|
+
richTextSymbols: [],
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
// Step 2: Tokenize translated text
|
|
231
|
+
const translatedWords = translatedText
|
|
232
|
+
.trim()
|
|
233
|
+
.replace(/\s+/g, ' ')
|
|
234
|
+
.split(/\s+/)
|
|
235
|
+
.filter((w) => w.length > 0);
|
|
236
|
+
// Step 3: Align words
|
|
237
|
+
const alignment = alignWords(parsedOriginal.words, translatedWords);
|
|
238
|
+
// Step 4: Reattach symbols
|
|
239
|
+
const result = reattachSymbols(translatedText, parsedOriginal, alignment);
|
|
240
|
+
return result;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Extract symbols from a button for use during translation.
|
|
244
|
+
*
|
|
245
|
+
* This helper extracts symbols from either:
|
|
246
|
+
* - button.semanticAction.richText.symbols
|
|
247
|
+
* - button.image (if it's a symbol library reference)
|
|
248
|
+
*
|
|
249
|
+
* @param button - The AAC button
|
|
250
|
+
* @returns Array of symbol attachments
|
|
251
|
+
*/
|
|
252
|
+
function extractSymbolsFromButton(button) {
|
|
253
|
+
// First check richText structure
|
|
254
|
+
if (button.semanticAction?.richText?.symbols) {
|
|
255
|
+
return button.semanticAction.richText.symbols;
|
|
256
|
+
}
|
|
257
|
+
// Check if button has a symbol library reference as image
|
|
258
|
+
if (button.symbolLibrary && button.symbolPath) {
|
|
259
|
+
// Create a symbol attachment for the label/message
|
|
260
|
+
const text = button.label || button.message || '';
|
|
261
|
+
if (text) {
|
|
262
|
+
return [
|
|
263
|
+
{
|
|
264
|
+
text,
|
|
265
|
+
image: `[${button.symbolLibrary}]${button.symbolPath}`,
|
|
266
|
+
},
|
|
267
|
+
];
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
// Check if image field contains a symbol reference
|
|
271
|
+
if (button.image && button.image.startsWith('[')) {
|
|
272
|
+
const text = button.label || button.message || '';
|
|
273
|
+
if (text) {
|
|
274
|
+
return [
|
|
275
|
+
{
|
|
276
|
+
text,
|
|
277
|
+
image: button.image,
|
|
278
|
+
},
|
|
279
|
+
];
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return undefined;
|
|
283
|
+
}
|
|
@@ -154,10 +154,11 @@ function extractSymbolLibraryImage(reference, options = {}) {
|
|
|
154
154
|
};
|
|
155
155
|
}
|
|
156
156
|
// Successfully extracted!
|
|
157
|
-
const
|
|
157
|
+
const data = resolved.data;
|
|
158
|
+
const format = data ? detectImageFormat(data) : 'unknown';
|
|
158
159
|
return {
|
|
159
160
|
found: true,
|
|
160
|
-
data
|
|
161
|
+
data,
|
|
161
162
|
format,
|
|
162
163
|
source: 'symbol-library',
|
|
163
164
|
reference: reference,
|
|
@@ -126,13 +126,15 @@ function searchSymbols(searchTerm, options = {}) {
|
|
|
126
126
|
// Exact match first
|
|
127
127
|
if (index.searchTerms.has(lowerSearchTerm)) {
|
|
128
128
|
const symbolFilename = index.searchTerms.get(lowerSearchTerm);
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
129
|
+
if (symbolFilename) {
|
|
130
|
+
results.push({
|
|
131
|
+
searchTerm: lowerSearchTerm,
|
|
132
|
+
symbolFilename,
|
|
133
|
+
displayName: index.filenames.get(symbolFilename) || lowerSearchTerm,
|
|
134
|
+
library: libraryName,
|
|
135
|
+
exactMatch: true,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
136
138
|
}
|
|
137
139
|
// Fuzzy match if enabled
|
|
138
140
|
if (options.fuzzyMatch !== false) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { BaseProcessor, ProcessorOptions, ExtractStringsResult, TranslatedString, SourceString } from '../core/baseProcessor';
|
|
2
2
|
import { AACTree } from '../core/treeStructure';
|
|
3
|
+
import { type ButtonForTranslation, type LLMLTranslationResult } from '../utilities/translation/translationProcessor';
|
|
3
4
|
import { ValidationResult } from '../validation/validationTypes';
|
|
4
5
|
declare class GridsetProcessor extends BaseProcessor {
|
|
5
6
|
constructor(options?: ProcessorOptions);
|
|
@@ -18,6 +19,31 @@ declare class GridsetProcessor extends BaseProcessor {
|
|
|
18
19
|
extractTexts(filePathOrBuffer: string | Buffer): string[];
|
|
19
20
|
loadIntoTree(filePathOrBuffer: string | Buffer): AACTree;
|
|
20
21
|
processTexts(filePathOrBuffer: string | Buffer, translations: Map<string, string>, outputPath: string): Buffer;
|
|
22
|
+
/**
|
|
23
|
+
* Extract symbol information from a gridset for LLM-based translation.
|
|
24
|
+
* Returns a structured format showing which buttons have symbols and their context.
|
|
25
|
+
*
|
|
26
|
+
* This method uses shared translation utilities that work across all AAC formats.
|
|
27
|
+
*
|
|
28
|
+
* @param filePathOrBuffer - Path to gridset file or buffer
|
|
29
|
+
* @returns Array of symbol information for LLM processing
|
|
30
|
+
*/
|
|
31
|
+
extractSymbolsForLLM(filePathOrBuffer: string | Buffer): ButtonForTranslation[];
|
|
32
|
+
/**
|
|
33
|
+
* Apply LLM translations with symbol information.
|
|
34
|
+
* The LLM should provide translations with symbol attachments in the correct positions.
|
|
35
|
+
*
|
|
36
|
+
* This method uses shared translation utilities that work across all AAC formats.
|
|
37
|
+
*
|
|
38
|
+
* @param filePathOrBuffer - Path to gridset file or buffer
|
|
39
|
+
* @param llmTranslations - Array of LLM translations with symbol info
|
|
40
|
+
* @param outputPath - Where to save the translated gridset
|
|
41
|
+
* @param options - Translation options (e.g., allowPartial for testing)
|
|
42
|
+
* @returns Buffer of the translated gridset
|
|
43
|
+
*/
|
|
44
|
+
processLLMTranslations(filePathOrBuffer: string | Buffer, llmTranslations: LLMLTranslationResult[], outputPath: string, options?: {
|
|
45
|
+
allowPartial?: boolean;
|
|
46
|
+
}): Buffer;
|
|
21
47
|
saveFromTree(tree: AACTree, outputPath: string): void;
|
|
22
48
|
private calculateColumnDefinitions;
|
|
23
49
|
private calculateRowDefinitions;
|