@willwade/aac-processors 0.3.0 → 0.3.1
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/processors/gotalkNow/nsKeyed.js +13 -6
- package/dist/browser/processors/gotalkNowProcessor.js +90 -15
- package/dist/browser/processors/gridsetProcessor.js +2 -2
- package/dist/browser/processors/obfProcessor.js +11 -4
- package/dist/core/treeStructure.d.ts +7 -0
- package/dist/processors/gotalkNow/nsKeyed.js +13 -6
- package/dist/processors/gotalkNowProcessor.js +90 -15
- package/dist/processors/gridsetProcessor.js +2 -2
- package/dist/processors/obfProcessor.js +11 -4
- package/package.json +1 -1
|
@@ -44,16 +44,23 @@ export function decodeNsColor(buffer) {
|
|
|
44
44
|
if (!buffer)
|
|
45
45
|
return undefined;
|
|
46
46
|
const s = bufferToLatinString(buffer);
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
47
|
+
// Match RGB[A] component strings like "1 0.949 0.561" or "0.5 0.5 0.5 1".
|
|
48
|
+
// Allow integer components (e.g. "1") but require at least one decimal to
|
|
49
|
+
// avoid matching bplist header bytes (e.g. "00 00 00").
|
|
50
|
+
const candidates = s.match(/\d+(?:\.\d+)?\s+\d+(?:\.\d+)?\s+\d+(?:\.\d+)?(?:\s+\d+(?:\.\d+)?)?/g);
|
|
51
|
+
if (candidates) {
|
|
52
|
+
// Find the first candidate that looks like RGB (has at least one decimal point).
|
|
53
|
+
const rgbStr = candidates.find((c) => c.includes('.'));
|
|
54
|
+
if (rgbStr) {
|
|
55
|
+
const parts = rgbStr.split(/\s+/).map(Number);
|
|
56
|
+
return rgbToHex(parts[0], parts[1], parts[2]);
|
|
57
|
+
}
|
|
53
58
|
}
|
|
54
59
|
return undefined;
|
|
55
60
|
}
|
|
56
61
|
const FONT_KEYWORDS = new Set([
|
|
62
|
+
'bplist',
|
|
63
|
+
'bplist00',
|
|
57
64
|
'UIFont',
|
|
58
65
|
'UIFontName',
|
|
59
66
|
'UIFontPointSize',
|
|
@@ -35,6 +35,7 @@ function buildSemanticAction(rawButton, pageId, buttonIndex) {
|
|
|
35
35
|
const actionData = rawButton.ActionData || {};
|
|
36
36
|
const label = rawButton.ButtonText ?? '';
|
|
37
37
|
const ttsText = actionData.TTSText;
|
|
38
|
+
const spoken = ttsText && ttsText.length > 0 ? ttsText : label;
|
|
38
39
|
switch (buttonType) {
|
|
39
40
|
case 'Jump': {
|
|
40
41
|
const jumpTo = actionData.JumpTo;
|
|
@@ -45,7 +46,7 @@ function buildSemanticAction(rawButton, pageId, buttonIndex) {
|
|
|
45
46
|
intent: AACSemanticIntent.NAVIGATE_TO,
|
|
46
47
|
targetId: target,
|
|
47
48
|
platformData: {
|
|
48
|
-
gotalkNow: { buttonType: 'Jump', jumpTo },
|
|
49
|
+
gotalkNow: { buttonType: 'Jump', jumpTo, jumpBook: actionData.JumpBook },
|
|
49
50
|
},
|
|
50
51
|
fallback: { type: 'NAVIGATE', targetPageId: target },
|
|
51
52
|
},
|
|
@@ -53,25 +54,78 @@ function buildSemanticAction(rawButton, pageId, buttonIndex) {
|
|
|
53
54
|
targetPageId: target,
|
|
54
55
|
};
|
|
55
56
|
}
|
|
56
|
-
case 'Audio':
|
|
57
|
-
|
|
57
|
+
case 'Audio':
|
|
58
|
+
case 'Recorded': {
|
|
59
|
+
// Recorded audio: plays a .caf file named <pageId>-<buttonIndex>.caf.
|
|
60
|
+
// Recorded buttons may also have TrimStart/TrimEnd for audio trimming.
|
|
58
61
|
const audioLocation = `${pageId}-${buttonIndex}.caf`;
|
|
59
62
|
return {
|
|
60
63
|
semanticAction: {
|
|
61
64
|
category: AACSemanticCategory.MEDIA,
|
|
62
65
|
intent: AACSemanticIntent.PLAY_SOUND,
|
|
63
66
|
platformData: {
|
|
64
|
-
gotalkNow: {
|
|
67
|
+
gotalkNow: {
|
|
68
|
+
buttonType,
|
|
69
|
+
audioLocation,
|
|
70
|
+
trimStart: actionData.TrimStart,
|
|
71
|
+
trimEnd: actionData.TrimEnd,
|
|
72
|
+
},
|
|
65
73
|
},
|
|
66
74
|
fallback: { type: 'ACTION' },
|
|
67
75
|
},
|
|
68
|
-
message:
|
|
76
|
+
message: spoken,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
case 'URL': {
|
|
80
|
+
return {
|
|
81
|
+
semanticAction: {
|
|
82
|
+
category: AACSemanticCategory.SYSTEM_CONTROL,
|
|
83
|
+
intent: AACSemanticIntent.WEB_NAVIGATE,
|
|
84
|
+
text: actionData.URL,
|
|
85
|
+
platformData: {
|
|
86
|
+
gotalkNow: {
|
|
87
|
+
buttonType: 'URL',
|
|
88
|
+
url: actionData.URL,
|
|
89
|
+
openInternally: actionData.OpenInternally,
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
fallback: { type: 'ACTION' },
|
|
93
|
+
},
|
|
94
|
+
message: spoken,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
case 'GoVisual': {
|
|
98
|
+
return {
|
|
99
|
+
semanticAction: {
|
|
100
|
+
category: AACSemanticCategory.SYSTEM_CONTROL,
|
|
101
|
+
intent: AACSemanticIntent.PLATFORM_SPECIFIC,
|
|
102
|
+
platformData: {
|
|
103
|
+
gotalkNow: {
|
|
104
|
+
buttonType: 'GoVisual',
|
|
105
|
+
goVisual: actionData.GoVisual,
|
|
106
|
+
goVisualLink: actionData.GoVisualLink,
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
fallback: { type: 'ACTION' },
|
|
110
|
+
},
|
|
111
|
+
message: spoken,
|
|
69
112
|
};
|
|
70
113
|
}
|
|
71
|
-
case 'Express':
|
|
72
|
-
case 'Bookmark':
|
|
73
|
-
case 'URL':
|
|
74
114
|
case 'Video': {
|
|
115
|
+
return {
|
|
116
|
+
semanticAction: {
|
|
117
|
+
category: AACSemanticCategory.MEDIA,
|
|
118
|
+
intent: AACSemanticIntent.PLAY_VIDEO,
|
|
119
|
+
platformData: {
|
|
120
|
+
gotalkNow: { buttonType: 'Video' },
|
|
121
|
+
},
|
|
122
|
+
fallback: { type: 'ACTION' },
|
|
123
|
+
},
|
|
124
|
+
message: spoken,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
case 'Express':
|
|
128
|
+
case 'Bookmark': {
|
|
75
129
|
return {
|
|
76
130
|
semanticAction: {
|
|
77
131
|
category: AACSemanticCategory.SYSTEM_CONTROL,
|
|
@@ -81,20 +135,19 @@ function buildSemanticAction(rawButton, pageId, buttonIndex) {
|
|
|
81
135
|
},
|
|
82
136
|
fallback: { type: 'ACTION' },
|
|
83
137
|
},
|
|
84
|
-
message:
|
|
138
|
+
message: spoken,
|
|
85
139
|
};
|
|
86
140
|
}
|
|
87
141
|
case 'TTS':
|
|
88
142
|
default: {
|
|
89
|
-
//
|
|
90
|
-
const spoken = ttsText && ttsText.length > 0 ? ttsText : label;
|
|
143
|
+
// Empty/placeholder cells (no ButtonType or no label) get a SPEAK with empty text.
|
|
91
144
|
return {
|
|
92
145
|
semanticAction: {
|
|
93
146
|
category: AACSemanticCategory.COMMUNICATION,
|
|
94
147
|
intent: AACSemanticIntent.SPEAK_TEXT,
|
|
95
148
|
text: spoken,
|
|
96
149
|
platformData: {
|
|
97
|
-
gotalkNow: { buttonType: 'TTS' },
|
|
150
|
+
gotalkNow: { buttonType: buttonType === 'TTS' ? 'TTS' : 'Empty' },
|
|
98
151
|
},
|
|
99
152
|
fallback: { type: 'SPEAK', message: spoken },
|
|
100
153
|
},
|
|
@@ -302,17 +355,39 @@ class GotalkNowProcessor extends BaseProcessor {
|
|
|
302
355
|
if (first?.SourceImageName)
|
|
303
356
|
button.symbolPath = first.SourceImageName;
|
|
304
357
|
}
|
|
305
|
-
// Visibility: GoTalk buttons can be disabled.
|
|
306
|
-
if (rawButton.Enabled === false) {
|
|
358
|
+
// Visibility: GoTalk buttons can be disabled via either field.
|
|
359
|
+
if (rawButton.Enabled === false || rawButton.Disabled === true) {
|
|
307
360
|
button.visibility = 'Disabled';
|
|
308
361
|
}
|
|
309
|
-
//
|
|
362
|
+
// Auditory cue (played when scanning/highlighting).
|
|
363
|
+
if (rawButton.CueType && rawButton.CueType !== 'None') {
|
|
364
|
+
const cueText = rawButton.CueData?.TTSText;
|
|
365
|
+
if (cueText && cueText.length > 0) {
|
|
366
|
+
button.audioDescription = cueText;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
// Recorded audio reference.
|
|
370
|
+
const btnType = rawButton.ButtonType;
|
|
371
|
+
if (btnType === 'Audio' || btnType === 'Recorded') {
|
|
372
|
+
button.audioRecording = {
|
|
373
|
+
identifier: `${pageId}-${btnIndex}.caf`,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
// AfterAction: motor-planning "speak and return" behaviour.
|
|
377
|
+
// AfterAction is an enum (0=stay, 2=..., 50=jump-back) and
|
|
378
|
+
// AfterActionData has the same shape as ActionData (e.g. {JumpTo: 0}).
|
|
379
|
+
const afterActionJumpTo = rawButton.AfterActionData?.JumpTo;
|
|
380
|
+
// Preserve raw style + extended data opaquely for round-trip styling.
|
|
310
381
|
button.parameters = {
|
|
311
382
|
...(button.parameters || {}),
|
|
312
383
|
__gotalkNow: {
|
|
313
384
|
buttonType: rawButton.ButtonType,
|
|
314
385
|
textShadow: rawButton.TextShadow,
|
|
315
386
|
textCenter: rawButton.TextCenter,
|
|
387
|
+
cueType: rawButton.CueType,
|
|
388
|
+
afterAction: rawButton.AfterAction,
|
|
389
|
+
afterActionJumpTo: afterActionJumpTo !== undefined ? String(afterActionJumpTo) : undefined,
|
|
390
|
+
jumpBook: rawButton.ActionData?.JumpBook,
|
|
316
391
|
},
|
|
317
392
|
};
|
|
318
393
|
page._loadButton(button);
|
|
@@ -2960,8 +2960,8 @@ class GridsetProcessor extends BaseProcessor {
|
|
|
2960
2960
|
cellContent.CaptionAndImage = captionAndImage;
|
|
2961
2961
|
}
|
|
2962
2962
|
const cellData = {
|
|
2963
|
-
'@_X': position.x
|
|
2964
|
-
'@_Y': position.y + yOffset
|
|
2963
|
+
'@_X': position.x, // Grid 3 coordinates are 0-based (matching loader)
|
|
2964
|
+
'@_Y': position.y + yOffset,
|
|
2965
2965
|
'@_ColumnSpan': position.columnSpan,
|
|
2966
2966
|
'@_RowSpan': position.rowSpan,
|
|
2967
2967
|
Content: cellContent,
|
|
@@ -144,14 +144,21 @@ class ObfProcessor extends BaseProcessor {
|
|
|
144
144
|
const pageId = boardData?.id ? String(boardData.id) : _boardPath?.split(/[/\\]/).pop() || '';
|
|
145
145
|
const images = boardData.images;
|
|
146
146
|
const buttons = await Promise.all(sourceButtons.map(async (btn) => {
|
|
147
|
+
// Resolve load_board.path to a board ID (strip .obf suffix if present).
|
|
148
|
+
// The OBF spec says load_board.path is a file path, but internally we
|
|
149
|
+
// store the board ID as targetPageId.
|
|
150
|
+
const rawPath = btn.load_board?.path;
|
|
151
|
+
const boardRef = rawPath
|
|
152
|
+
? rawPath.replace(/\.obf$/i, '').replace(/\.obz$/i, '')
|
|
153
|
+
: undefined;
|
|
147
154
|
const semanticAction = btn.load_board
|
|
148
155
|
? {
|
|
149
156
|
category: AACSemanticCategory.NAVIGATION,
|
|
150
157
|
intent: AACSemanticIntent.NAVIGATE_TO,
|
|
151
|
-
targetId:
|
|
158
|
+
targetId: boardRef,
|
|
152
159
|
fallback: {
|
|
153
160
|
type: 'NAVIGATE',
|
|
154
|
-
targetPageId:
|
|
161
|
+
targetPageId: boardRef,
|
|
155
162
|
},
|
|
156
163
|
}
|
|
157
164
|
: {
|
|
@@ -199,7 +206,7 @@ class ObfProcessor extends BaseProcessor {
|
|
|
199
206
|
resolvedImageEntry: resolvedImage,
|
|
200
207
|
parameters: Object.keys(buttonParameters).length > 0 ? buttonParameters : undefined,
|
|
201
208
|
semanticAction,
|
|
202
|
-
targetPageId:
|
|
209
|
+
targetPageId: boardRef,
|
|
203
210
|
semantic_id: btn.semantic_id, // Extract semantic_id if present
|
|
204
211
|
});
|
|
205
212
|
}));
|
|
@@ -618,7 +625,7 @@ class ObfProcessor extends BaseProcessor {
|
|
|
618
625
|
vocalization: button.message || button.label,
|
|
619
626
|
load_board: button.semanticAction?.intent === AACSemanticIntent.NAVIGATE_TO && button.targetPageId
|
|
620
627
|
? {
|
|
621
|
-
path: button.targetPageId,
|
|
628
|
+
path: this.getPageFilename(button.targetPageId, metadata),
|
|
622
629
|
}
|
|
623
630
|
: undefined,
|
|
624
631
|
background_color: button.style?.backgroundColor,
|
|
@@ -117,7 +117,14 @@ export interface AACSemanticAction {
|
|
|
117
117
|
gotalkNow?: {
|
|
118
118
|
buttonType: string;
|
|
119
119
|
jumpTo?: number;
|
|
120
|
+
jumpBook?: string;
|
|
120
121
|
audioLocation?: string;
|
|
122
|
+
trimStart?: number;
|
|
123
|
+
trimEnd?: number;
|
|
124
|
+
url?: string;
|
|
125
|
+
openInternally?: boolean;
|
|
126
|
+
goVisual?: string;
|
|
127
|
+
goVisualLink?: unknown;
|
|
121
128
|
};
|
|
122
129
|
};
|
|
123
130
|
fallback?: {
|
|
@@ -48,16 +48,23 @@ function decodeNsColor(buffer) {
|
|
|
48
48
|
if (!buffer)
|
|
49
49
|
return undefined;
|
|
50
50
|
const s = bufferToLatinString(buffer);
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
51
|
+
// Match RGB[A] component strings like "1 0.949 0.561" or "0.5 0.5 0.5 1".
|
|
52
|
+
// Allow integer components (e.g. "1") but require at least one decimal to
|
|
53
|
+
// avoid matching bplist header bytes (e.g. "00 00 00").
|
|
54
|
+
const candidates = s.match(/\d+(?:\.\d+)?\s+\d+(?:\.\d+)?\s+\d+(?:\.\d+)?(?:\s+\d+(?:\.\d+)?)?/g);
|
|
55
|
+
if (candidates) {
|
|
56
|
+
// Find the first candidate that looks like RGB (has at least one decimal point).
|
|
57
|
+
const rgbStr = candidates.find((c) => c.includes('.'));
|
|
58
|
+
if (rgbStr) {
|
|
59
|
+
const parts = rgbStr.split(/\s+/).map(Number);
|
|
60
|
+
return rgbToHex(parts[0], parts[1], parts[2]);
|
|
61
|
+
}
|
|
57
62
|
}
|
|
58
63
|
return undefined;
|
|
59
64
|
}
|
|
60
65
|
const FONT_KEYWORDS = new Set([
|
|
66
|
+
'bplist',
|
|
67
|
+
'bplist00',
|
|
61
68
|
'UIFont',
|
|
62
69
|
'UIFontName',
|
|
63
70
|
'UIFontPointSize',
|
|
@@ -41,6 +41,7 @@ function buildSemanticAction(rawButton, pageId, buttonIndex) {
|
|
|
41
41
|
const actionData = rawButton.ActionData || {};
|
|
42
42
|
const label = rawButton.ButtonText ?? '';
|
|
43
43
|
const ttsText = actionData.TTSText;
|
|
44
|
+
const spoken = ttsText && ttsText.length > 0 ? ttsText : label;
|
|
44
45
|
switch (buttonType) {
|
|
45
46
|
case 'Jump': {
|
|
46
47
|
const jumpTo = actionData.JumpTo;
|
|
@@ -51,7 +52,7 @@ function buildSemanticAction(rawButton, pageId, buttonIndex) {
|
|
|
51
52
|
intent: treeStructure_1.AACSemanticIntent.NAVIGATE_TO,
|
|
52
53
|
targetId: target,
|
|
53
54
|
platformData: {
|
|
54
|
-
gotalkNow: { buttonType: 'Jump', jumpTo },
|
|
55
|
+
gotalkNow: { buttonType: 'Jump', jumpTo, jumpBook: actionData.JumpBook },
|
|
55
56
|
},
|
|
56
57
|
fallback: { type: 'NAVIGATE', targetPageId: target },
|
|
57
58
|
},
|
|
@@ -59,25 +60,78 @@ function buildSemanticAction(rawButton, pageId, buttonIndex) {
|
|
|
59
60
|
targetPageId: target,
|
|
60
61
|
};
|
|
61
62
|
}
|
|
62
|
-
case 'Audio':
|
|
63
|
-
|
|
63
|
+
case 'Audio':
|
|
64
|
+
case 'Recorded': {
|
|
65
|
+
// Recorded audio: plays a .caf file named <pageId>-<buttonIndex>.caf.
|
|
66
|
+
// Recorded buttons may also have TrimStart/TrimEnd for audio trimming.
|
|
64
67
|
const audioLocation = `${pageId}-${buttonIndex}.caf`;
|
|
65
68
|
return {
|
|
66
69
|
semanticAction: {
|
|
67
70
|
category: treeStructure_1.AACSemanticCategory.MEDIA,
|
|
68
71
|
intent: treeStructure_1.AACSemanticIntent.PLAY_SOUND,
|
|
69
72
|
platformData: {
|
|
70
|
-
gotalkNow: {
|
|
73
|
+
gotalkNow: {
|
|
74
|
+
buttonType,
|
|
75
|
+
audioLocation,
|
|
76
|
+
trimStart: actionData.TrimStart,
|
|
77
|
+
trimEnd: actionData.TrimEnd,
|
|
78
|
+
},
|
|
71
79
|
},
|
|
72
80
|
fallback: { type: 'ACTION' },
|
|
73
81
|
},
|
|
74
|
-
message:
|
|
82
|
+
message: spoken,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
case 'URL': {
|
|
86
|
+
return {
|
|
87
|
+
semanticAction: {
|
|
88
|
+
category: treeStructure_1.AACSemanticCategory.SYSTEM_CONTROL,
|
|
89
|
+
intent: treeStructure_1.AACSemanticIntent.WEB_NAVIGATE,
|
|
90
|
+
text: actionData.URL,
|
|
91
|
+
platformData: {
|
|
92
|
+
gotalkNow: {
|
|
93
|
+
buttonType: 'URL',
|
|
94
|
+
url: actionData.URL,
|
|
95
|
+
openInternally: actionData.OpenInternally,
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
fallback: { type: 'ACTION' },
|
|
99
|
+
},
|
|
100
|
+
message: spoken,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
case 'GoVisual': {
|
|
104
|
+
return {
|
|
105
|
+
semanticAction: {
|
|
106
|
+
category: treeStructure_1.AACSemanticCategory.SYSTEM_CONTROL,
|
|
107
|
+
intent: treeStructure_1.AACSemanticIntent.PLATFORM_SPECIFIC,
|
|
108
|
+
platformData: {
|
|
109
|
+
gotalkNow: {
|
|
110
|
+
buttonType: 'GoVisual',
|
|
111
|
+
goVisual: actionData.GoVisual,
|
|
112
|
+
goVisualLink: actionData.GoVisualLink,
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
fallback: { type: 'ACTION' },
|
|
116
|
+
},
|
|
117
|
+
message: spoken,
|
|
75
118
|
};
|
|
76
119
|
}
|
|
77
|
-
case 'Express':
|
|
78
|
-
case 'Bookmark':
|
|
79
|
-
case 'URL':
|
|
80
120
|
case 'Video': {
|
|
121
|
+
return {
|
|
122
|
+
semanticAction: {
|
|
123
|
+
category: treeStructure_1.AACSemanticCategory.MEDIA,
|
|
124
|
+
intent: treeStructure_1.AACSemanticIntent.PLAY_VIDEO,
|
|
125
|
+
platformData: {
|
|
126
|
+
gotalkNow: { buttonType: 'Video' },
|
|
127
|
+
},
|
|
128
|
+
fallback: { type: 'ACTION' },
|
|
129
|
+
},
|
|
130
|
+
message: spoken,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
case 'Express':
|
|
134
|
+
case 'Bookmark': {
|
|
81
135
|
return {
|
|
82
136
|
semanticAction: {
|
|
83
137
|
category: treeStructure_1.AACSemanticCategory.SYSTEM_CONTROL,
|
|
@@ -87,20 +141,19 @@ function buildSemanticAction(rawButton, pageId, buttonIndex) {
|
|
|
87
141
|
},
|
|
88
142
|
fallback: { type: 'ACTION' },
|
|
89
143
|
},
|
|
90
|
-
message:
|
|
144
|
+
message: spoken,
|
|
91
145
|
};
|
|
92
146
|
}
|
|
93
147
|
case 'TTS':
|
|
94
148
|
default: {
|
|
95
|
-
//
|
|
96
|
-
const spoken = ttsText && ttsText.length > 0 ? ttsText : label;
|
|
149
|
+
// Empty/placeholder cells (no ButtonType or no label) get a SPEAK with empty text.
|
|
97
150
|
return {
|
|
98
151
|
semanticAction: {
|
|
99
152
|
category: treeStructure_1.AACSemanticCategory.COMMUNICATION,
|
|
100
153
|
intent: treeStructure_1.AACSemanticIntent.SPEAK_TEXT,
|
|
101
154
|
text: spoken,
|
|
102
155
|
platformData: {
|
|
103
|
-
gotalkNow: { buttonType: 'TTS' },
|
|
156
|
+
gotalkNow: { buttonType: buttonType === 'TTS' ? 'TTS' : 'Empty' },
|
|
104
157
|
},
|
|
105
158
|
fallback: { type: 'SPEAK', message: spoken },
|
|
106
159
|
},
|
|
@@ -308,17 +361,39 @@ class GotalkNowProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
308
361
|
if (first?.SourceImageName)
|
|
309
362
|
button.symbolPath = first.SourceImageName;
|
|
310
363
|
}
|
|
311
|
-
// Visibility: GoTalk buttons can be disabled.
|
|
312
|
-
if (rawButton.Enabled === false) {
|
|
364
|
+
// Visibility: GoTalk buttons can be disabled via either field.
|
|
365
|
+
if (rawButton.Enabled === false || rawButton.Disabled === true) {
|
|
313
366
|
button.visibility = 'Disabled';
|
|
314
367
|
}
|
|
315
|
-
//
|
|
368
|
+
// Auditory cue (played when scanning/highlighting).
|
|
369
|
+
if (rawButton.CueType && rawButton.CueType !== 'None') {
|
|
370
|
+
const cueText = rawButton.CueData?.TTSText;
|
|
371
|
+
if (cueText && cueText.length > 0) {
|
|
372
|
+
button.audioDescription = cueText;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
// Recorded audio reference.
|
|
376
|
+
const btnType = rawButton.ButtonType;
|
|
377
|
+
if (btnType === 'Audio' || btnType === 'Recorded') {
|
|
378
|
+
button.audioRecording = {
|
|
379
|
+
identifier: `${pageId}-${btnIndex}.caf`,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
// AfterAction: motor-planning "speak and return" behaviour.
|
|
383
|
+
// AfterAction is an enum (0=stay, 2=..., 50=jump-back) and
|
|
384
|
+
// AfterActionData has the same shape as ActionData (e.g. {JumpTo: 0}).
|
|
385
|
+
const afterActionJumpTo = rawButton.AfterActionData?.JumpTo;
|
|
386
|
+
// Preserve raw style + extended data opaquely for round-trip styling.
|
|
316
387
|
button.parameters = {
|
|
317
388
|
...(button.parameters || {}),
|
|
318
389
|
__gotalkNow: {
|
|
319
390
|
buttonType: rawButton.ButtonType,
|
|
320
391
|
textShadow: rawButton.TextShadow,
|
|
321
392
|
textCenter: rawButton.TextCenter,
|
|
393
|
+
cueType: rawButton.CueType,
|
|
394
|
+
afterAction: rawButton.AfterAction,
|
|
395
|
+
afterActionJumpTo: afterActionJumpTo !== undefined ? String(afterActionJumpTo) : undefined,
|
|
396
|
+
jumpBook: rawButton.ActionData?.JumpBook,
|
|
322
397
|
},
|
|
323
398
|
};
|
|
324
399
|
page._loadButton(button);
|
|
@@ -2963,8 +2963,8 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
2963
2963
|
cellContent.CaptionAndImage = captionAndImage;
|
|
2964
2964
|
}
|
|
2965
2965
|
const cellData = {
|
|
2966
|
-
'@_X': position.x
|
|
2967
|
-
'@_Y': position.y + yOffset
|
|
2966
|
+
'@_X': position.x, // Grid 3 coordinates are 0-based (matching loader)
|
|
2967
|
+
'@_Y': position.y + yOffset,
|
|
2968
2968
|
'@_ColumnSpan': position.columnSpan,
|
|
2969
2969
|
'@_RowSpan': position.rowSpan,
|
|
2970
2970
|
Content: cellContent,
|
|
@@ -147,14 +147,21 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
147
147
|
const pageId = boardData?.id ? String(boardData.id) : _boardPath?.split(/[/\\]/).pop() || '';
|
|
148
148
|
const images = boardData.images;
|
|
149
149
|
const buttons = await Promise.all(sourceButtons.map(async (btn) => {
|
|
150
|
+
// Resolve load_board.path to a board ID (strip .obf suffix if present).
|
|
151
|
+
// The OBF spec says load_board.path is a file path, but internally we
|
|
152
|
+
// store the board ID as targetPageId.
|
|
153
|
+
const rawPath = btn.load_board?.path;
|
|
154
|
+
const boardRef = rawPath
|
|
155
|
+
? rawPath.replace(/\.obf$/i, '').replace(/\.obz$/i, '')
|
|
156
|
+
: undefined;
|
|
150
157
|
const semanticAction = btn.load_board
|
|
151
158
|
? {
|
|
152
159
|
category: treeStructure_1.AACSemanticCategory.NAVIGATION,
|
|
153
160
|
intent: treeStructure_1.AACSemanticIntent.NAVIGATE_TO,
|
|
154
|
-
targetId:
|
|
161
|
+
targetId: boardRef,
|
|
155
162
|
fallback: {
|
|
156
163
|
type: 'NAVIGATE',
|
|
157
|
-
targetPageId:
|
|
164
|
+
targetPageId: boardRef,
|
|
158
165
|
},
|
|
159
166
|
}
|
|
160
167
|
: {
|
|
@@ -202,7 +209,7 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
202
209
|
resolvedImageEntry: resolvedImage,
|
|
203
210
|
parameters: Object.keys(buttonParameters).length > 0 ? buttonParameters : undefined,
|
|
204
211
|
semanticAction,
|
|
205
|
-
targetPageId:
|
|
212
|
+
targetPageId: boardRef,
|
|
206
213
|
semantic_id: btn.semantic_id, // Extract semantic_id if present
|
|
207
214
|
});
|
|
208
215
|
}));
|
|
@@ -621,7 +628,7 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
621
628
|
vocalization: button.message || button.label,
|
|
622
629
|
load_board: button.semanticAction?.intent === treeStructure_1.AACSemanticIntent.NAVIGATE_TO && button.targetPageId
|
|
623
630
|
? {
|
|
624
|
-
path: button.targetPageId,
|
|
631
|
+
path: this.getPageFilename(button.targetPageId, metadata),
|
|
625
632
|
}
|
|
626
633
|
: undefined,
|
|
627
634
|
background_color: button.style?.backgroundColor,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@willwade/aac-processors",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "A comprehensive TypeScript library for processing AAC (Augmentative and Alternative Communication) file formats with translation support",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"browser": "dist/browser/index.browser.js",
|