diff --git a/packages/moonboard-ocr/src/browser.ts b/packages/moonboard-ocr/src/browser.ts index 9c1efbfb..9f46c013 100644 --- a/packages/moonboard-ocr/src/browser.ts +++ b/packages/moonboard-ocr/src/browser.ts @@ -21,7 +21,8 @@ */ import { CanvasImageProcessor } from './image-processor/canvas-processor'; -import { parseWithProcessor, deduplicateClimbs } from './parser'; +// Import from parser-core to avoid pulling in sharp via parser.ts +import { parseWithProcessor, deduplicateClimbs } from './parser-core'; import type { ParseResult, MoonBoardClimb, DetectedHold, GridCoordinate, HoldType } from './types'; // Re-export types for consumers @@ -79,5 +80,5 @@ export { deduplicateClimbs }; // Re-export the CanvasImageProcessor for advanced use cases export { CanvasImageProcessor } from './image-processor/canvas-processor'; -export { parseWithProcessor } from './parser'; +export { parseWithProcessor } from './parser-core'; export type { ImageProcessor, RawPixelData, ImageMetadata, ImageRegion } from './image-processor/types'; diff --git a/packages/moonboard-ocr/src/parser-core.ts b/packages/moonboard-ocr/src/parser-core.ts new file mode 100644 index 00000000..d8444a0c --- /dev/null +++ b/packages/moonboard-ocr/src/parser-core.ts @@ -0,0 +1,114 @@ +/** + * Core parsing functions that work in both Node.js and browser environments. + * This module does NOT import sharp or any Node.js-specific modules. + * + * For Node.js-specific functions (that use file paths), see parser.ts + */ + +import { ImageProcessor } from './image-processor/types'; +import { runOCR } from './core/ocr'; +import { detectHoldsFromPixelData } from './core/holds'; +import { calculateRegions } from './core/regions'; +import { MoonBoardClimb, ParseResult, GridCoordinate } from './types'; + +/** + * Parse a MoonBoard screenshot using the provided ImageProcessor. + * This is the core parsing function used by both Node and browser implementations. + */ +export async function parseWithProcessor( + processor: ImageProcessor +): Promise { + const warnings: string[] = []; + + try { + const metadata = processor.getMetadata(); + const regions = calculateRegions(metadata.width, metadata.height); + + // Extract header for OCR + const ocrImageData = await processor.extractForOCR(regions.header); + const ocrResult = await runOCR(ocrImageData); + warnings.push(...ocrResult.warnings); + + // Extract board region for hold detection + const boardPixels = await processor.extractRegion(regions.board); + const detectedHolds = detectHoldsFromPixelData(boardPixels, regions.board); + + // Group holds by type + const startHolds: GridCoordinate[] = []; + const handHolds: GridCoordinate[] = []; + const finishHolds: GridCoordinate[] = []; + + for (const hold of detectedHolds) { + switch (hold.type) { + case 'start': + startHolds.push(hold.coordinate); + break; + case 'hand': + handHolds.push(hold.coordinate); + break; + case 'finish': + finishHolds.push(hold.coordinate); + break; + } + } + + // Validate we found some holds + if (startHolds.length === 0) { + warnings.push('No start holds detected'); + } + if (finishHolds.length === 0) { + warnings.push('No finish holds detected'); + } + if (startHolds.length + handHolds.length + finishHolds.length === 0) { + return { success: false, error: 'No holds detected in image', warnings }; + } + + // Build the climb object + const climb: MoonBoardClimb = { + name: ocrResult.name, + setter: ocrResult.setter, + angle: ocrResult.angle, + userGrade: ocrResult.userGrade, + setterGrade: ocrResult.setterGrade, + isBenchmark: ocrResult.isBenchmark, + holds: { + start: [...new Set(startHolds)], // Dedupe + hand: [...new Set(handHolds)], + finish: [...new Set(finishHolds)], + }, + sourceFile: processor.getSourceName(), + parseWarnings: warnings.length > 0 ? warnings : undefined, + }; + + return { success: true, climb, warnings }; + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown error'; + return { success: false, error: `Failed to parse image: ${message}`, warnings }; + } +} + +/** + * Deduplicate climbs by their hold configuration + * Two climbs with the same holds are considered the same climb + */ +export function deduplicateClimbs(climbs: MoonBoardClimb[]): MoonBoardClimb[] { + const seen = new Map(); + + for (const climb of climbs) { + // Create a unique key from sorted hold positions + const key = [ + ...climb.holds.start.sort(), + '|', + ...climb.holds.hand.sort(), + '|', + ...climb.holds.finish.sort(), + ].join(','); + + // Keep the first occurrence (or could prefer one with better OCR results) + if (!seen.has(key)) { + seen.set(key, climb); + } + } + + return Array.from(seen.values()); +} diff --git a/packages/moonboard-ocr/src/parser.ts b/packages/moonboard-ocr/src/parser.ts index 55579816..3f4977b7 100644 --- a/packages/moonboard-ocr/src/parser.ts +++ b/packages/moonboard-ocr/src/parser.ts @@ -1,86 +1,17 @@ -import path from 'path'; -import { ImageProcessor } from './image-processor/types'; -import { SharpImageProcessor } from './image-processor/sharp-processor'; -import { runOCR } from './core/ocr'; -import { detectHoldsFromPixelData } from './core/holds'; -import { calculateRegions } from './core/regions'; -import { MoonBoardClimb, ParseResult, GridCoordinate } from './types'; - /** - * Parse a MoonBoard screenshot using the provided ImageProcessor. - * This is the core parsing function used by both Node and browser implementations. + * Node.js parsing functions for @boardsesh/moonboard-ocr + * + * This module provides file-path based APIs using Sharp for image processing. + * For browser usage, import from '@boardsesh/moonboard-ocr/browser' instead. */ -export async function parseWithProcessor( - processor: ImageProcessor -): Promise { - const warnings: string[] = []; - - try { - const metadata = processor.getMetadata(); - const regions = calculateRegions(metadata.width, metadata.height); - - // Extract header for OCR - const ocrImageData = await processor.extractForOCR(regions.header); - const ocrResult = await runOCR(ocrImageData); - warnings.push(...ocrResult.warnings); - - // Extract board region for hold detection - const boardPixels = await processor.extractRegion(regions.board); - const detectedHolds = detectHoldsFromPixelData(boardPixels, regions.board); - - // Group holds by type - const startHolds: GridCoordinate[] = []; - const handHolds: GridCoordinate[] = []; - const finishHolds: GridCoordinate[] = []; - - for (const hold of detectedHolds) { - switch (hold.type) { - case 'start': - startHolds.push(hold.coordinate); - break; - case 'hand': - handHolds.push(hold.coordinate); - break; - case 'finish': - finishHolds.push(hold.coordinate); - break; - } - } - - // Validate we found some holds - if (startHolds.length === 0) { - warnings.push('No start holds detected'); - } - if (finishHolds.length === 0) { - warnings.push('No finish holds detected'); - } - if (startHolds.length + handHolds.length + finishHolds.length === 0) { - return { success: false, error: 'No holds detected in image', warnings }; - } - // Build the climb object - const climb: MoonBoardClimb = { - name: ocrResult.name, - setter: ocrResult.setter, - angle: ocrResult.angle, - userGrade: ocrResult.userGrade, - setterGrade: ocrResult.setterGrade, - isBenchmark: ocrResult.isBenchmark, - holds: { - start: [...new Set(startHolds)], // Dedupe - hand: [...new Set(handHolds)], - finish: [...new Set(finishHolds)], - }, - sourceFile: processor.getSourceName(), - parseWarnings: warnings.length > 0 ? warnings : undefined, - }; +import path from 'path'; +import { SharpImageProcessor } from './image-processor/sharp-processor'; +import { MoonBoardClimb, ParseResult } from './types'; +import { parseWithProcessor, deduplicateClimbs } from './parser-core'; - return { success: true, climb, warnings }; - } catch (error) { - const message = error instanceof Error ? error.message : 'Unknown error'; - return { success: false, error: `Failed to parse image: ${message}`, warnings }; - } -} +// Re-export browser-safe core functions for backward compatibility +export { parseWithProcessor, deduplicateClimbs }; /** * Parse a single MoonBoard screenshot from file path (Node.js API). @@ -116,29 +47,3 @@ export async function parseMultipleScreenshots( return { climbs, errors }; } - -/** - * Deduplicate climbs by their hold configuration - * Two climbs with the same holds are considered the same climb - */ -export function deduplicateClimbs(climbs: MoonBoardClimb[]): MoonBoardClimb[] { - const seen = new Map(); - - for (const climb of climbs) { - // Create a unique key from sorted hold positions - const key = [ - ...climb.holds.start.sort(), - '|', - ...climb.holds.hand.sort(), - '|', - ...climb.holds.finish.sort(), - ].join(','); - - // Keep the first occurrence (or could prefer one with better OCR results) - if (!seen.has(key)) { - seen.set(key, climb); - } - } - - return Array.from(seen.values()); -}