Skip to content

Commit 232435a

Browse files
committed
convert to a gridset from a TSV set of files
1 parent 73f3682 commit 232435a

1 file changed

Lines changed: 232 additions & 0 deletions

File tree

scripts/txt-to-gridset.ts

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
#!/usr/bin/env tsx
2+
3+
import fs from 'fs';
4+
import path from 'path';
5+
import { GridsetProcessor } from '../src/processors/gridsetProcessor';
6+
import { AACTree, AACPage, AACButton, AACSemanticCategory, AACSemanticIntent } from '../src/core/treeStructure';
7+
8+
interface Cell {
9+
text: string;
10+
row: number;
11+
col: number;
12+
}
13+
14+
interface Grid {
15+
name: string;
16+
filename: string;
17+
rows: number;
18+
cols: number;
19+
cells: Cell[];
20+
categories: string[];
21+
}
22+
23+
function parseGridFile(filePath: string, filename: string): Grid {
24+
const content = fs.readFileSync(filePath, 'utf-8');
25+
const lines = content.split('\n');
26+
const cells: Cell[] = [];
27+
const categories: string[] = [];
28+
let maxRows = 0;
29+
let maxCols = 0;
30+
let currentRow = 0;
31+
32+
// Parse each line as a row of cells
33+
lines.forEach(line => {
34+
const trimmedLine = line.trim();
35+
36+
// Skip empty lines completely (OCR sometimes inserts blank lines)
37+
if (!trimmedLine) {
38+
return;
39+
}
40+
41+
// Split by tabs to get individual cells, preserve empty cells between tabs
42+
const cellTexts = line.split('\t');
43+
44+
cellTexts.forEach((text, colIndex) => {
45+
const trimmedText = text.trim();
46+
47+
// Add cell even if empty (to preserve position)
48+
if (trimmedText !== '...' && trimmedText !== '') {
49+
cells.push({
50+
text: trimmedText,
51+
row: currentRow,
52+
col: colIndex
53+
});
54+
}
55+
maxCols = Math.max(maxCols, colIndex + 1);
56+
});
57+
58+
currentRow += 1;
59+
maxRows = Math.max(maxRows, currentRow);
60+
});
61+
62+
// Identify categories (words that appear in the rightmost columns or are known categories)
63+
const knownCategories = [
64+
'Mitteilungen', 'Fragen', 'Leute', 'Verben', 'Eigenschaften', 'Gefühle',
65+
'Treffen', 'Lob', 'Beschwerde', 'Sprüche', 'Spielen', 'Multimedia',
66+
'Essen', 'Trinken', 'Farben/Formen', 'Sport', 'Musik', 'Draußen',
67+
'Fahrzeuge', 'Tiere', 'Pflanzen', 'Wetter', 'Buchstaben', 'Zahlen/Geld',
68+
'Haus', 'Kleidung', 'Körper', 'Arztbesuch', 'Therapie', 'Tagesplan',
69+
'Zeit', 'Schule', 'Buch', 'Basteln/Büro', 'Werken', 'Feste/Religion',
70+
'Freizeit', 'Biografie', 'Berufe', 'Geografie', 'Politik',
71+
'Dies und das', 'Wortbausteine'
72+
];
73+
74+
cells.forEach(cell => {
75+
if (knownCategories.includes(cell.text)) {
76+
categories.push(cell.text);
77+
}
78+
});
79+
80+
// Strip extensions and any parent path prefix (e.g. "Home->Fragen.PNG.txt" -> "Fragen")
81+
const baseName = filename
82+
.replace(/\.txt$/i, '')
83+
.replace(/\.png$/i, '');
84+
const parts = baseName.split('->');
85+
const pageName = parts[parts.length - 1] || baseName;
86+
87+
return {
88+
name: pageName,
89+
filename,
90+
rows: maxRows,
91+
cols: maxCols,
92+
cells,
93+
categories: [...new Set(categories)]
94+
};
95+
}
96+
97+
function createAACPage(grid: Grid): AACPage {
98+
const buttons: AACButton[] = [];
99+
// Build an explicit 2D grid so downstream exporters keep the shape (6×11 etc.)
100+
const gridLayout: (AACButton | null)[][] = Array.from({ length: grid.rows }, () =>
101+
Array.from({ length: grid.cols }, () => null)
102+
);
103+
104+
grid.cells.forEach(cell => {
105+
const text = cell.text;
106+
const isCategory = grid.categories.includes(text);
107+
const isNavigation = text === 'Home' || text === 'weiter' || text === 'zurück';
108+
109+
// Ensure position is within bounds
110+
if (cell.row < grid.rows && cell.col < grid.cols) {
111+
const button: AACButton = {
112+
id: `${grid.name.toLowerCase().replace(/[^a-z]/g, '_')}_${cell.row}_${cell.col}`,
113+
label: text,
114+
style: {
115+
backgroundColor: isCategory ? '#4CAF50' : isNavigation ? '#2196F3' : '#FFFFFF',
116+
fontColor: isCategory || isNavigation ? '#FFFFFF' : '#000000',
117+
borderColor: '#CCCCCC',
118+
borderWidth: 1,
119+
},
120+
position: {
121+
row: cell.row,
122+
col: cell.col,
123+
width: 1,
124+
height: 1,
125+
},
126+
semanticAction: isCategory ? {
127+
category: AACSemanticCategory.NAVIGATION,
128+
intent: AACSemanticIntent.NAVIGATE_TO,
129+
targetId: text.toLowerCase().replace(/[^a-z]/g, '_'),
130+
text: text
131+
} : isNavigation ? {
132+
category: AACSemanticCategory.NAVIGATION,
133+
intent: text === 'Home' ? AACSemanticIntent.GO_HOME : AACSemanticIntent.GO_BACK,
134+
} : {
135+
category: AACSemanticCategory.COMMUNICATION,
136+
intent: AACSemanticIntent.SPEAK_IMMEDIATE,
137+
text: text
138+
}
139+
};
140+
141+
buttons.push(button);
142+
gridLayout[cell.row][cell.col] = button;
143+
}
144+
});
145+
146+
return {
147+
id: grid.name.toLowerCase().replace(/[^a-z]/g, '_'),
148+
name: grid.name,
149+
grid: gridLayout,
150+
buttons,
151+
style: {
152+
rows: grid.rows,
153+
cols: grid.cols,
154+
backgroundColor: '#F5F5F5',
155+
gap: 2,
156+
}
157+
};
158+
}
159+
160+
async function convertTextFilesToGridset() {
161+
console.log('=== TXT to Gridset Converter ===\n');
162+
console.log('This script converts tab-separated text files to Grid3 format.\n');
163+
164+
// Default to the OCR outputs we generate from screenshots
165+
const baseDir = path.resolve(__dirname, '../examples/text-conversion');
166+
const txtDir = path.join(baseDir, 'ocr-results');
167+
if (!fs.existsSync(txtDir)) {
168+
console.log('Creating txt-files directory...');
169+
fs.mkdirSync(txtDir, { recursive: true });
170+
console.log('Please put your tab-separated text files in the txt-files directory.');
171+
console.log('File format: Home.txt, Home->Fragen.txt, etc.');
172+
return;
173+
}
174+
175+
const files = fs.readdirSync(txtDir).filter(f => f.endsWith('.txt'));
176+
177+
if (files.length === 0) {
178+
console.log('No .txt files found in txt-files directory.');
179+
return;
180+
}
181+
182+
console.log(`Found ${files.length} text files:\n`);
183+
184+
const tree = new AACTree();
185+
186+
// Process each text file
187+
for (const file of files) {
188+
const filePath = path.join(txtDir, file);
189+
const grid = parseGridFile(filePath, file);
190+
const page = createAACPage(grid);
191+
192+
tree.addPage(page);
193+
194+
console.log(`✓ ${file}`);
195+
console.log(` - Page: ${grid.name}`);
196+
console.log(` - Grid: ${grid.rows}×${grid.cols}`);
197+
console.log(` - Cells: ${grid.cells.length}`);
198+
console.log(` - Categories: ${grid.categories.join(', ') || 'none'}`);
199+
console.log();
200+
}
201+
202+
// Set Home as root page if it exists
203+
const homePage = Object.values(tree.pages).find(p => p.id === 'home');
204+
if (homePage) {
205+
tree.rootId = 'home';
206+
console.log('✓ Set Home as root page\n');
207+
}
208+
209+
// Export to Grid3
210+
const processor = new GridsetProcessor();
211+
const outputPath = path.join(baseDir, 'converted-from-txt.gridset');
212+
213+
processor.saveFromTree(tree, outputPath);
214+
215+
const stats = fs.statSync(outputPath);
216+
console.log(`🎉 Conversion complete!`);
217+
console.log(`📦 Output file: converted-from-txt.gridset`);
218+
console.log(`📊 File size: ${(stats.size / 1024).toFixed(2)} KB`);
219+
console.log(`📁 Full path: ${outputPath}\n`);
220+
221+
console.log('You can now import converted-from-txt.gridset into Grid3!');
222+
}
223+
224+
// Also create a function to parse one file for testing
225+
export function parseSingleTextFile(filePath: string): Grid {
226+
const filename = path.basename(filePath);
227+
return parseGridFile(filePath, filename);
228+
}
229+
230+
if (require.main === module) {
231+
convertTextFilesToGridset();
232+
}

0 commit comments

Comments
 (0)