101 lines
3.9 KiB
JavaScript
101 lines
3.9 KiB
JavaScript
#!/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); });
|