Refactor code structure for improved readability and maintainability

This commit is contained in:
2025-08-20 15:29:22 +02:00
parent 9d53410aab
commit b6135640e1
81 changed files with 112506 additions and 341 deletions

View File

@@ -0,0 +1,100 @@
#!/usr/bin/env node
// OCR + rebuild rules from an image using tesseract.js
// Usage: node scripts/ocr-and-rebuild-rules.js /path/to/image.png
const fs = require('fs');
const path = require('path');
const { createWorker } = require('tesseract.js');
const Database = require('better-sqlite3');
async function main(){
const img = process.argv[2];
if(!img){
console.error('Usage: node scripts/ocr-and-rebuild-rules.js /path/to/image.png');
process.exit(2);
}
if(!fs.existsSync(img)){
console.error('Image not found:', img);
process.exit(2);
}
const repoRoot = path.resolve(__dirname, '..');
const dbPath = path.join(repoRoot, 'database', 'sqlite', 'deathwatch.db');
const backupsDir = path.join(repoRoot, 'database', 'backups');
if(!fs.existsSync(dbPath)){
console.error('DB not found at', dbPath); process.exit(1);
}
if(!fs.existsSync(backupsDir)) fs.mkdirSync(backupsDir,{recursive:true});
const nowTs = ()=> new Date().toISOString().replace(/[:.]/g,'-');
console.log('Running OCR on', img);
const worker = createWorker({
logger: m => { if(m && m.status) process.stdout.write(`\rOCR: ${m.status} ${(m.progress||0).toFixed(2)}`); }
});
await worker.load();
await worker.loadLanguage('eng');
await worker.initialize('eng');
const { data: { text } } = await worker.recognize(img);
await worker.terminate();
console.log('\nOCR complete,', text.length, 'chars');
// Split text into lines and use heading heuristics: a heading is a short line (<=60 chars) that is mostly letters, possibly contains parentheses or ALL CAPS.
const lines = text.split(/\r?\n/).map(l=>l.replace(/\u00A0/g,' ').trim());
const isHeading = (l)=>{
if(!l) return false;
if(l.length>60) return false;
if(/\d{1,3}\s?[-–—]/.test(l)) return false; // avoid numbered table rows
if(/Table\s+\d+/i.test(l)) return false;
if(/^[A-Z0-9][A-Z0-9 \-'()]+$/.test(l) && l.split(' ').length<=6) return true; // ALL CAPS
if(/\w+\s*\(.+\)/.test(l)) return true; // has parentheses like (Basic)
if(/^([A-Za-z]{3,}\s?){1,3}$/.test(l)) return true; // 1-3 word simple title
return false;
};
const blocks = [];
let curr = null;
for(const line of lines){
if(isHeading(line)){
if(curr) blocks.push(curr);
curr = {title: line, content: ''};
}else{
if(!curr) continue; // skip until first heading
if(curr.content.length) curr.content += '\n';
curr.content += line;
}
}
if(curr) blocks.push(curr);
console.log('Detected', blocks.length, 'blocks from OCR');
// Simple filter: content length > 30
const skills = blocks.filter(b=> (b.content||'').replace(/\s+/g,'').length>30);
console.log('Filtered to', skills.length, 'skill-like blocks');
const db = new Database(dbPath);
const backupFile = path.join(backupsDir, `rules-backup-before-ocr-rebuild-${nowTs()}.json`);
const rows = db.prepare('SELECT * FROM rules').all();
fs.writeFileSync(backupFile, JSON.stringify(rows,null,2));
console.log('Wrote DB backup to', backupFile);
const deleteInfo = db.prepare('DELETE FROM rules').run();
console.log('Purged rules table, deleted rows:', deleteInfo.changes);
const insert = db.prepare(`INSERT INTO rules (rule_id,title,content,page,source,source_abbr,category,created_at) VALUES (?,?,?,?,?,?,?,datetime('now'))`);
let i=1;
const txn = db.transaction((rows)=>{
for(const r of rows){
const ruleId = `ocr_skill_${i}_${r.title.replace(/[^a-z0-9]+/ig,'_').toLowerCase()}`;
insert.run(ruleId, r.title, r.content, null, 'Core Rulebook', 'CR', 'skills');
i++;
}
});
txn(skills);
console.log('Inserted', skills.length, 'skill rows');
const report = path.join(backupsDir, `rules-ocr-rebuild-report-${nowTs()}.json`);
fs.writeFileSync(report, JSON.stringify({inserted: skills.length, sample: skills.slice(0,50)}, null, 2));
console.log('Wrote report to', report);
db.close();
console.log('Done');
}
main().catch(e=>{ console.error(e); process.exit(1); });