/** * Node.js TypeScript wrapper for Soroban Flashcard Generator * Calls Python functions via child_process */ import { spawn, execSync } from 'child_process'; import * as path from 'path'; import * as fs from 'fs/promises'; import * as os from 'os'; export interface FlashcardConfig { range: string; step?: number; cardsPerPage?: number; paperSize?: 'us-letter' | 'a4' | 'a3' | 'a5'; orientation?: 'portrait' | 'landscape'; margins?: { top?: string; bottom?: string; left?: string; right?: string; }; gutter?: string; shuffle?: boolean; seed?: number; showCutMarks?: boolean; showRegistration?: boolean; fontFamily?: string; fontSize?: string; columns?: string | number; showEmptyColumns?: boolean; hideInactiveBeads?: boolean; beadShape?: 'diamond' | 'circle' | 'square'; colorScheme?: 'monochrome' | 'place-value' | 'heaven-earth' | 'alternating'; coloredNumerals?: boolean; scaleFactor?: number; } export class SorobanGenerator { private pythonPath: string; private generatorPath: string; private projectRoot: string; constructor(projectRoot?: string) { // Find Python executable this.pythonPath = this.findPython(); // Set project root (where the Python scripts are) this.projectRoot = projectRoot || path.join(__dirname, '../../'); this.generatorPath = path.join(this.projectRoot, 'src', 'generate.py'); } private findPython(): string { // Try common Python commands const pythonCommands = ['python3', 'python']; for (const cmd of pythonCommands) { try { const version = execSync(`${cmd} --version`, { encoding: 'utf8' }); if (version.includes('Python 3')) { return cmd; } } catch { // Continue to next command } } throw new Error('Python 3 not found. Please install Python 3.'); } /** * Generate flashcards and return PDF as Buffer */ async generate(config: FlashcardConfig): Promise { // Create temp output file const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'soroban-')); const outputPath = path.join(tempDir, 'flashcards.pdf'); try { // Build command line arguments const args = [ this.generatorPath, '--output', outputPath, '--range', config.range, ]; if (config.step) args.push('--step', config.step.toString()); if (config.cardsPerPage) args.push('--cards-per-page', config.cardsPerPage.toString()); if (config.paperSize) args.push('--paper-size', config.paperSize); if (config.orientation) args.push('--orientation', config.orientation); if (config.gutter) args.push('--gutter', config.gutter); if (config.shuffle) args.push('--shuffle'); if (config.seed !== undefined) args.push('--seed', config.seed.toString()); if (config.showCutMarks) args.push('--cut-marks'); if (config.showRegistration) args.push('--registration'); if (config.fontFamily) args.push('--font-family', config.fontFamily); if (config.fontSize) args.push('--font-size', config.fontSize); if (config.columns !== undefined) args.push('--columns', config.columns.toString()); if (config.showEmptyColumns) args.push('--show-empty-columns'); if (config.hideInactiveBeads) args.push('--hide-inactive-beads'); if (config.beadShape) args.push('--bead-shape', config.beadShape); if (config.colorScheme) args.push('--color-scheme', config.colorScheme); if (config.coloredNumerals) args.push('--colored-numerals'); if (config.scaleFactor !== undefined) args.push('--scale-factor', config.scaleFactor.toString()); if (config.margins) { const m = config.margins; const marginStr = `${m.top || '0.5in'},${m.right || '0.5in'},${m.bottom || '0.5in'},${m.left || '0.5in'}`; args.push('--margins', marginStr); } // Execute Python script await this.executePython(args); // Read generated PDF const pdfBuffer = await fs.readFile(outputPath); return pdfBuffer; } finally { // Clean up temp directory await fs.rm(tempDir, { recursive: true, force: true }); } } /** * Generate flashcards and save to file */ async generateToFile(config: FlashcardConfig, outputPath: string): Promise { const pdfBuffer = await this.generate(config); await fs.writeFile(outputPath, pdfBuffer); } /** * Generate flashcards and return as base64 string */ async generateBase64(config: FlashcardConfig): Promise { const pdfBuffer = await this.generate(config); return pdfBuffer.toString('base64'); } private executePython(args: string[]): Promise { return new Promise((resolve, reject) => { const process = spawn(this.pythonPath, args, { cwd: this.projectRoot, env: { ...process.env, PYTHONPATH: this.projectRoot } }); let stderr = ''; process.stderr.on('data', (data) => { stderr += data.toString(); }); process.on('close', (code) => { if (code !== 0) { reject(new Error(`Python process failed with code ${code}: ${stderr}`)); } else { resolve(); } }); process.on('error', (err) => { reject(new Error(`Failed to start Python process: ${err.message}`)); }); }); } /** * Check if all dependencies are installed */ async checkDependencies(): Promise<{ python: boolean; typst: boolean; qpdf: boolean }> { const checks = { python: false, typst: false, qpdf: false }; try { execSync(`${this.pythonPath} --version`); checks.python = true; } catch {} try { execSync('typst --version'); checks.typst = true; } catch {} try { execSync('qpdf --version'); checks.qpdf = true; } catch {} return checks; } } // Example usage for Express/Next.js/etc export async function expressExample() { const generator = new SorobanGenerator(); // In your Express route handler: // app.post('/api/generate', async (req, res) => { // try { // const pdfBuffer = await generator.generate(req.body); // res.contentType('application/pdf'); // res.send(pdfBuffer); // } catch (error) { // res.status(500).json({ error: error.message }); // } // }); }