@willwade/aac-processors 0.0.27 → 0.0.29
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/processors/gridsetProcessor.d.ts +5 -0
- package/dist/processors/gridsetProcessor.js +86 -16
- package/dist/processors/obfProcessor.d.ts +11 -0
- package/dist/processors/obfProcessor.js +120 -0
- package/dist/utilities/analytics/metrics/core.d.ts +4 -1
- package/dist/utilities/analytics/metrics/core.js +36 -14
- package/dist/utilities/analytics/metrics/types.d.ts +14 -0
- package/package.json +1 -1
|
@@ -12,6 +12,11 @@ declare class GridsetProcessor extends BaseProcessor {
|
|
|
12
12
|
private decryptGridsetEntry;
|
|
13
13
|
private getGridsetPassword;
|
|
14
14
|
private ensureAlphaChannel;
|
|
15
|
+
/**
|
|
16
|
+
* Calculate appropriate font color (black or white) based on background brightness
|
|
17
|
+
* Uses WCAG relative luminance formula to determine contrast
|
|
18
|
+
*/
|
|
19
|
+
private getContrastFontColor;
|
|
15
20
|
/**
|
|
16
21
|
* Extract words from Grid3 WordList structure
|
|
17
22
|
*/
|
|
@@ -58,6 +58,19 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
58
58
|
ensureAlphaChannel(color) {
|
|
59
59
|
if (!color)
|
|
60
60
|
return '#FFFFFFFF';
|
|
61
|
+
// Handle rgb() and rgba() formats
|
|
62
|
+
const rgbMatch = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
|
|
63
|
+
if (rgbMatch) {
|
|
64
|
+
const r = parseInt(rgbMatch[1]);
|
|
65
|
+
const g = parseInt(rgbMatch[2]);
|
|
66
|
+
const b = parseInt(rgbMatch[3]);
|
|
67
|
+
const a = rgbMatch[4] !== undefined ? parseFloat(rgbMatch[4]) : 1.0;
|
|
68
|
+
const alphaHex = Math.round(a * 255)
|
|
69
|
+
.toString(16)
|
|
70
|
+
.toUpperCase()
|
|
71
|
+
.padStart(2, '0');
|
|
72
|
+
return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}${alphaHex}`;
|
|
73
|
+
}
|
|
61
74
|
// If already 8 digits (with alpha), return as is
|
|
62
75
|
if (color.match(/^#[0-9A-Fa-f]{8}$/))
|
|
63
76
|
return color;
|
|
@@ -74,6 +87,37 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
74
87
|
// Invalid or unknown format, return white
|
|
75
88
|
return '#FFFFFFFF';
|
|
76
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Calculate appropriate font color (black or white) based on background brightness
|
|
92
|
+
* Uses WCAG relative luminance formula to determine contrast
|
|
93
|
+
*/
|
|
94
|
+
getContrastFontColor(backgroundColor) {
|
|
95
|
+
if (!backgroundColor)
|
|
96
|
+
return '#FF000000FF'; // Default to black
|
|
97
|
+
// Parse color from various formats
|
|
98
|
+
let r = 255, g = 255, b = 255;
|
|
99
|
+
// Handle hex colors
|
|
100
|
+
const hexMatch = backgroundColor.match(/#?([0-9A-Fa-f]{2})([0-9A-Fa-f]{2})([0-9A-Fa-f]{2})/);
|
|
101
|
+
if (hexMatch) {
|
|
102
|
+
r = parseInt(hexMatch[1], 16);
|
|
103
|
+
g = parseInt(hexMatch[2], 16);
|
|
104
|
+
b = parseInt(hexMatch[3], 16);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
// Handle rgb() format
|
|
108
|
+
const rgbMatch = backgroundColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
|
|
109
|
+
if (rgbMatch) {
|
|
110
|
+
r = parseInt(rgbMatch[1]);
|
|
111
|
+
g = parseInt(rgbMatch[2]);
|
|
112
|
+
b = parseInt(rgbMatch[3]);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// Calculate relative luminance using WCAG formula
|
|
116
|
+
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
|
117
|
+
// Use white text for dark backgrounds (luminance < 0.5), black for light backgrounds
|
|
118
|
+
// Return 6-digit hex (ensureAlphaChannel will add FF for alpha)
|
|
119
|
+
return luminance < 0.5 ? '#FFFFFF' : '#000000';
|
|
120
|
+
}
|
|
77
121
|
/**
|
|
78
122
|
* Extract words from Grid3 WordList structure
|
|
79
123
|
*/
|
|
@@ -1628,7 +1672,8 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
1628
1672
|
// For "None" surround, just use BackColour for the fill (no TileColour)
|
|
1629
1673
|
BackColour: this.ensureAlphaChannel(style.backgroundColor),
|
|
1630
1674
|
BorderColour: this.ensureAlphaChannel(style.borderColor),
|
|
1631
|
-
|
|
1675
|
+
// Calculate font color based on background if not explicitly set
|
|
1676
|
+
FontColour: this.ensureAlphaChannel(style.fontColor || this.getContrastFontColor(style.backgroundColor)),
|
|
1632
1677
|
FontName: style.fontFamily || 'Arial',
|
|
1633
1678
|
FontSize: style.fontSize?.toString() || '16',
|
|
1634
1679
|
};
|
|
@@ -1687,26 +1732,50 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
1687
1732
|
if (imageMatch) {
|
|
1688
1733
|
imageExt = imageMatch[1].toLowerCase();
|
|
1689
1734
|
}
|
|
1690
|
-
// Grid3 dynamically constructs image filenames by prepending cell coordinates
|
|
1691
|
-
// The XML should only contain the suffix: -0-text-0.{ext}
|
|
1692
|
-
// Grid3 automatically adds the X-Y prefix based on the Cell's position
|
|
1693
|
-
captionAndImage.Image = `-0-text-0.${imageExt}`;
|
|
1694
1735
|
// Extract image data from button parameters if available
|
|
1695
1736
|
// (AstericsGridProcessor stores it there during loadIntoTree)
|
|
1737
|
+
// Also handle data URLs from OBZ conversion
|
|
1696
1738
|
let imageData = Buffer.alloc(0);
|
|
1739
|
+
let hasImageData = false;
|
|
1697
1740
|
if (button.parameters &&
|
|
1698
1741
|
button.parameters.imageData &&
|
|
1699
1742
|
Buffer.isBuffer(button.parameters.imageData)) {
|
|
1700
1743
|
imageData = button.parameters.imageData;
|
|
1744
|
+
hasImageData = imageData.length > 0;
|
|
1745
|
+
}
|
|
1746
|
+
else if (button.image &&
|
|
1747
|
+
typeof button.image === 'string' &&
|
|
1748
|
+
button.image.startsWith('data:image')) {
|
|
1749
|
+
// Convert data URL to Buffer (for OBZ → Grid3 conversion)
|
|
1750
|
+
try {
|
|
1751
|
+
const matches = button.image.match(/^data:image\/(\w+);base64,(.+)$/);
|
|
1752
|
+
if (matches) {
|
|
1753
|
+
const extension = matches[1]; // e.g., 'png', 'jpeg', 'gif'
|
|
1754
|
+
const base64Data = matches[2];
|
|
1755
|
+
imageData = Buffer.from(base64Data, 'base64');
|
|
1756
|
+
imageExt = extension; // Override the detected extension
|
|
1757
|
+
hasImageData = imageData.length > 0;
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
catch (err) {
|
|
1761
|
+
console.warn(`[Grid3] Failed to convert data URL to Buffer for button ${button.id}:`, err);
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
// Only add image reference if we have actual image data
|
|
1765
|
+
if (hasImageData) {
|
|
1766
|
+
// Grid3 dynamically constructs image filenames by prepending cell coordinates
|
|
1767
|
+
// The XML should only contain the suffix: -0-text-0.{ext}
|
|
1768
|
+
// Grid3 automatically adds the X-Y prefix based on the Cell's position
|
|
1769
|
+
captionAndImage.Image = `-0-text-0.${imageExt}`;
|
|
1770
|
+
// Store image data for later writing to ZIP
|
|
1771
|
+
buttonImages.set(button.id, {
|
|
1772
|
+
imageData: imageData,
|
|
1773
|
+
ext: imageExt,
|
|
1774
|
+
pageName: page.name || page.id,
|
|
1775
|
+
x: position.x,
|
|
1776
|
+
y: position.y + yOffset,
|
|
1777
|
+
});
|
|
1701
1778
|
}
|
|
1702
|
-
// Store image data for later writing to ZIP
|
|
1703
|
-
buttonImages.set(button.id, {
|
|
1704
|
-
imageData: imageData,
|
|
1705
|
-
ext: imageExt,
|
|
1706
|
-
pageName: page.name || page.id,
|
|
1707
|
-
x: position.x,
|
|
1708
|
-
y: position.y + yOffset,
|
|
1709
|
-
});
|
|
1710
1779
|
}
|
|
1711
1780
|
const cellData = {
|
|
1712
1781
|
'@_X': position.x, // Grid3 uses 0-based X coordinates (defaults to 0 when omitted)
|
|
@@ -1736,9 +1805,10 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
1736
1805
|
if (button.style?.borderColor) {
|
|
1737
1806
|
styleObj.BorderColour = this.ensureAlphaChannel(button.style.borderColor);
|
|
1738
1807
|
}
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1808
|
+
// Always add font color inline - either from button style or calculated from background
|
|
1809
|
+
const fontColor = button.style?.fontColor ||
|
|
1810
|
+
this.getContrastFontColor(button.style?.backgroundColor);
|
|
1811
|
+
styleObj.FontColour = this.ensureAlphaChannel(fontColor);
|
|
1742
1812
|
if (button.style?.fontFamily) {
|
|
1743
1813
|
styleObj.FontName = button.style.fontFamily;
|
|
1744
1814
|
}
|
|
@@ -3,7 +3,18 @@ import { AACTree } from '../core/treeStructure';
|
|
|
3
3
|
import { ValidationResult } from '../validation/validationTypes';
|
|
4
4
|
import { type ButtonForTranslation, type LLMLTranslationResult } from '../utilities/translation/translationProcessor';
|
|
5
5
|
declare class ObfProcessor extends BaseProcessor {
|
|
6
|
+
private zipFile?;
|
|
7
|
+
private imageCache;
|
|
6
8
|
constructor(options?: ProcessorOptions);
|
|
9
|
+
/**
|
|
10
|
+
* Extract an image from the ZIP file as a Buffer
|
|
11
|
+
*/
|
|
12
|
+
private extractImageAsBuffer;
|
|
13
|
+
/**
|
|
14
|
+
* Extract an image from the ZIP file and convert to data URL
|
|
15
|
+
*/
|
|
16
|
+
private extractImageAsDataUrl;
|
|
17
|
+
private getMimeTypeFromFilename;
|
|
7
18
|
private processBoard;
|
|
8
19
|
extractTexts(filePathOrBuffer: string | Buffer): string[];
|
|
9
20
|
loadIntoTree(filePathOrBuffer: string | Buffer): AACTree;
|
|
@@ -26,6 +26,104 @@ function mapObfVisibility(hidden) {
|
|
|
26
26
|
class ObfProcessor extends baseProcessor_1.BaseProcessor {
|
|
27
27
|
constructor(options) {
|
|
28
28
|
super(options);
|
|
29
|
+
this.imageCache = new Map(); // Cache for data URLs
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Extract an image from the ZIP file as a Buffer
|
|
33
|
+
*/
|
|
34
|
+
extractImageAsBuffer(imageId, images) {
|
|
35
|
+
if (!this.zipFile || !images) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
// Find the image metadata
|
|
39
|
+
const imageData = images.find((img) => img.id === imageId);
|
|
40
|
+
if (!imageData) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
// Try to get the image file from the ZIP
|
|
44
|
+
const possiblePaths = [
|
|
45
|
+
imageData.path,
|
|
46
|
+
`images/${imageData.filename || imageId}`,
|
|
47
|
+
imageData.id,
|
|
48
|
+
].filter(Boolean);
|
|
49
|
+
for (const imagePath of possiblePaths) {
|
|
50
|
+
try {
|
|
51
|
+
const entry = this.zipFile.getEntry(imagePath);
|
|
52
|
+
if (entry) {
|
|
53
|
+
return entry.getData(); // Return raw Buffer
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Extract an image from the ZIP file and convert to data URL
|
|
64
|
+
*/
|
|
65
|
+
extractImageAsDataUrl(imageId, images) {
|
|
66
|
+
// Check cache first
|
|
67
|
+
if (this.imageCache.has(imageId)) {
|
|
68
|
+
return this.imageCache.get(imageId) ?? null;
|
|
69
|
+
}
|
|
70
|
+
if (!this.zipFile || !images) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
// Find the image metadata
|
|
74
|
+
const imageData = images.find((img) => img.id === imageId);
|
|
75
|
+
if (!imageData) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
// Try to get the image file from the ZIP
|
|
79
|
+
// Images are typically stored in an 'images' folder or root
|
|
80
|
+
const possiblePaths = [
|
|
81
|
+
imageData.path, // Explicit path if provided
|
|
82
|
+
`images/${imageData.filename || imageId}`, // Standard images folder
|
|
83
|
+
imageData.id, // Just the ID
|
|
84
|
+
].filter(Boolean);
|
|
85
|
+
for (const imagePath of possiblePaths) {
|
|
86
|
+
try {
|
|
87
|
+
const entry = this.zipFile.getEntry(imagePath);
|
|
88
|
+
if (entry) {
|
|
89
|
+
const buffer = entry.getData();
|
|
90
|
+
const contentType = imageData.content_type ||
|
|
91
|
+
this.getMimeTypeFromFilename(imagePath);
|
|
92
|
+
const dataUrl = `data:${contentType};base64,${buffer.toString('base64')}`;
|
|
93
|
+
this.imageCache.set(imageId, dataUrl);
|
|
94
|
+
return dataUrl;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
// Continue to next path
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// If image has a URL, use that as fallback
|
|
103
|
+
if (imageData.url) {
|
|
104
|
+
const url = imageData.url;
|
|
105
|
+
this.imageCache.set(imageId, url);
|
|
106
|
+
return url;
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
getMimeTypeFromFilename(filename) {
|
|
111
|
+
const ext = filename.toLowerCase().split('.').pop();
|
|
112
|
+
switch (ext) {
|
|
113
|
+
case 'png':
|
|
114
|
+
return 'image/png';
|
|
115
|
+
case 'jpg':
|
|
116
|
+
case 'jpeg':
|
|
117
|
+
return 'image/jpeg';
|
|
118
|
+
case 'gif':
|
|
119
|
+
return 'image/gif';
|
|
120
|
+
case 'svg':
|
|
121
|
+
return 'image/svg+xml';
|
|
122
|
+
case 'webp':
|
|
123
|
+
return 'image/webp';
|
|
124
|
+
default:
|
|
125
|
+
return 'image/png';
|
|
126
|
+
}
|
|
29
127
|
}
|
|
30
128
|
processBoard(boardData, _boardPath) {
|
|
31
129
|
const sourceButtons = boardData.buttons || [];
|
|
@@ -55,6 +153,22 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
55
153
|
message: String(btn?.vocalization || btn?.label || ''),
|
|
56
154
|
},
|
|
57
155
|
};
|
|
156
|
+
// Resolve image if image_id is present
|
|
157
|
+
let resolvedImage;
|
|
158
|
+
let imageBuffer;
|
|
159
|
+
if (btn.image_id && boardData.images) {
|
|
160
|
+
resolvedImage = this.extractImageAsDataUrl(btn.image_id, boardData.images) || undefined;
|
|
161
|
+
imageBuffer = this.extractImageAsBuffer(btn.image_id, boardData.images) || undefined;
|
|
162
|
+
}
|
|
163
|
+
// Build parameters object for Grid3 export compatibility
|
|
164
|
+
const buttonParameters = {};
|
|
165
|
+
if (imageBuffer) {
|
|
166
|
+
buttonParameters.imageData = imageBuffer;
|
|
167
|
+
}
|
|
168
|
+
// Store image_id for web viewers to fetch images via API
|
|
169
|
+
if (btn.image_id) {
|
|
170
|
+
buttonParameters.image_id = btn.image_id;
|
|
171
|
+
}
|
|
58
172
|
return new treeStructure_1.AACButton({
|
|
59
173
|
// Make button ID unique by combining page ID and button ID
|
|
60
174
|
id: `${pageId}::${btn?.id || ''}`,
|
|
@@ -65,6 +179,9 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
65
179
|
backgroundColor: btn.background_color,
|
|
66
180
|
borderColor: btn.border_color,
|
|
67
181
|
},
|
|
182
|
+
image: resolvedImage, // Set the resolved image data URL
|
|
183
|
+
resolvedImageEntry: resolvedImage,
|
|
184
|
+
parameters: Object.keys(buttonParameters).length > 0 ? buttonParameters : undefined,
|
|
68
185
|
semanticAction,
|
|
69
186
|
targetPageId: btn.load_board?.path,
|
|
70
187
|
semantic_id: btn.semantic_id, // Extract semantic_id if present
|
|
@@ -271,6 +388,9 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
271
388
|
console.error('[OBF] Error instantiating AdmZip with input:', err);
|
|
272
389
|
throw err;
|
|
273
390
|
}
|
|
391
|
+
// Store the ZIP file reference for image extraction
|
|
392
|
+
this.zipFile = zip;
|
|
393
|
+
this.imageCache.clear(); // Clear cache for new file
|
|
274
394
|
console.log('[OBF] Detected zip archive, extracting .obf files');
|
|
275
395
|
zip.getEntries().forEach((entry) => {
|
|
276
396
|
if (entry.entryName.endsWith('.obf')) {
|
|
@@ -47,10 +47,13 @@ export declare class MetricsCalculator {
|
|
|
47
47
|
* - Parent button's cumulative effort (to reach the button)
|
|
48
48
|
* - + Effort to select the word form from its position in predictions grid
|
|
49
49
|
*
|
|
50
|
+
* If a word exists as both a regular button and a word form, the version
|
|
51
|
+
* with lower effort is kept.
|
|
52
|
+
*
|
|
50
53
|
* @param tree - The AAC tree
|
|
51
54
|
* @param buttons - Already calculated button metrics
|
|
52
55
|
* @param options - Metrics options
|
|
53
|
-
* @returns
|
|
56
|
+
* @returns Object containing word form metrics and labels that were replaced
|
|
54
57
|
*/
|
|
55
58
|
private calculateWordFormMetrics;
|
|
56
59
|
/**
|
|
@@ -83,11 +83,20 @@ class MetricsCalculator {
|
|
|
83
83
|
});
|
|
84
84
|
// Update buttons using dynamic spelling effort if applicable
|
|
85
85
|
const buttons = Array.from(knownButtons.values()).sort((a, b) => a.effort - b.effort);
|
|
86
|
-
// Calculate metrics for word forms (smart grammar predictions)
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
86
|
+
// Calculate metrics for word forms (smart grammar predictions) if enabled
|
|
87
|
+
// Default to true if not specified
|
|
88
|
+
const useSmartGrammar = options.useSmartGrammar !== false;
|
|
89
|
+
if (useSmartGrammar) {
|
|
90
|
+
const { wordFormMetrics, replacedLabels } = this.calculateWordFormMetrics(tree, buttons, options);
|
|
91
|
+
// Remove buttons that were replaced by lower-effort word forms
|
|
92
|
+
const filteredButtons = buttons.filter((btn) => !replacedLabels.has(btn.label.toLowerCase()));
|
|
93
|
+
// Add word forms and re-sort
|
|
94
|
+
filteredButtons.push(...wordFormMetrics);
|
|
95
|
+
filteredButtons.sort((a, b) => a.effort - b.effort);
|
|
96
|
+
// Replace the original buttons array
|
|
97
|
+
buttons.length = 0;
|
|
98
|
+
buttons.push(...filteredButtons);
|
|
99
|
+
}
|
|
91
100
|
// Calculate grid dimensions
|
|
92
101
|
const grid = this.calculateGridDimensions(tree);
|
|
93
102
|
// Identify prediction metrics
|
|
@@ -573,14 +582,18 @@ class MetricsCalculator {
|
|
|
573
582
|
* - Parent button's cumulative effort (to reach the button)
|
|
574
583
|
* - + Effort to select the word form from its position in predictions grid
|
|
575
584
|
*
|
|
585
|
+
* If a word exists as both a regular button and a word form, the version
|
|
586
|
+
* with lower effort is kept.
|
|
587
|
+
*
|
|
576
588
|
* @param tree - The AAC tree
|
|
577
589
|
* @param buttons - Already calculated button metrics
|
|
578
590
|
* @param options - Metrics options
|
|
579
|
-
* @returns
|
|
591
|
+
* @returns Object containing word form metrics and labels that were replaced
|
|
580
592
|
*/
|
|
581
593
|
calculateWordFormMetrics(tree, buttons, _options = {}) {
|
|
582
594
|
const wordFormMetrics = [];
|
|
583
|
-
|
|
595
|
+
const replacedLabels = new Set();
|
|
596
|
+
// Track buttons by label to compare efforts
|
|
584
597
|
const existingLabels = new Map();
|
|
585
598
|
buttons.forEach((btn) => existingLabels.set(btn.label.toLowerCase(), btn));
|
|
586
599
|
// Iterate through all pages to find buttons with predictions
|
|
@@ -596,10 +609,6 @@ class MetricsCalculator {
|
|
|
596
609
|
// Calculate effort for each word form
|
|
597
610
|
btn.predictions.forEach((wordForm, index) => {
|
|
598
611
|
const wordFormLower = wordForm.toLowerCase();
|
|
599
|
-
// Skip if this word form already exists as a regular button
|
|
600
|
-
if (existingLabels.has(wordFormLower)) {
|
|
601
|
-
return;
|
|
602
|
-
}
|
|
603
612
|
// Calculate effort based on position in predictions array
|
|
604
613
|
// Assume predictions are displayed in a grid layout (e.g., 2 columns)
|
|
605
614
|
const predictionsGridCols = 2; // Typical predictions layout
|
|
@@ -611,7 +620,17 @@ class MetricsCalculator {
|
|
|
611
620
|
const predictionSelectionEffort = (0, effort_1.visualScanEffort)(predictionPriorItems);
|
|
612
621
|
// Word form effort = parent button's cumulative effort + selection effort
|
|
613
622
|
const wordFormEffort = parentMetrics.effort + predictionSelectionEffort;
|
|
614
|
-
//
|
|
623
|
+
// Check if this word already exists as a regular button
|
|
624
|
+
const existingBtn = existingLabels.get(wordFormLower);
|
|
625
|
+
// If word exists and has lower or equal effort, skip the word form
|
|
626
|
+
if (existingBtn && existingBtn.effort <= wordFormEffort) {
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
// If word exists but word form has lower effort, mark for replacement
|
|
630
|
+
if (existingBtn && existingBtn.effort > wordFormEffort) {
|
|
631
|
+
replacedLabels.add(wordFormLower);
|
|
632
|
+
}
|
|
633
|
+
// Create word form metric
|
|
615
634
|
const wordFormBtn = {
|
|
616
635
|
id: `${btn.id}_wordform_${index}`,
|
|
617
636
|
label: wordForm,
|
|
@@ -631,8 +650,11 @@ class MetricsCalculator {
|
|
|
631
650
|
});
|
|
632
651
|
});
|
|
633
652
|
});
|
|
634
|
-
console.log(`📝 Calculated ${wordFormMetrics.length} word form metrics
|
|
635
|
-
|
|
653
|
+
console.log(`📝 Calculated ${wordFormMetrics.length} word form metrics` +
|
|
654
|
+
(replacedLabels.size > 0
|
|
655
|
+
? ` (${replacedLabels.size} replaced higher-effort buttons: ${Array.from(replacedLabels).join(', ')})`
|
|
656
|
+
: ''));
|
|
657
|
+
return { wordFormMetrics, replacedLabels };
|
|
636
658
|
}
|
|
637
659
|
/**
|
|
638
660
|
* Calculate grid dimensions from the tree
|
|
@@ -117,6 +117,20 @@ export interface MetricsOptions {
|
|
|
117
117
|
* Default is 1.5 (checking 1-2 predictions on average).
|
|
118
118
|
*/
|
|
119
119
|
predictionSelections?: number;
|
|
120
|
+
/**
|
|
121
|
+
* Whether to include smart grammar word forms in metrics
|
|
122
|
+
*
|
|
123
|
+
* When true (default): Word forms from smart grammar predictions are included
|
|
124
|
+
* in the metrics. If a word exists as both a regular button and a word form,
|
|
125
|
+
* the version with lower effort is used.
|
|
126
|
+
*
|
|
127
|
+
* When false: Smart grammar word forms are excluded from metrics. Only actual
|
|
128
|
+
* buttons in the tree are analyzed.
|
|
129
|
+
*
|
|
130
|
+
* Only applicable to systems that support smart grammar (e.g., Grid 3).
|
|
131
|
+
* Default is true.
|
|
132
|
+
*/
|
|
133
|
+
useSmartGrammar?: boolean;
|
|
120
134
|
}
|
|
121
135
|
/**
|
|
122
136
|
* Comparison result between two board sets
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@willwade/aac-processors",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.29",
|
|
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
|
"types": "dist/index.d.ts",
|