/** * WebGPU inference engine for PaddleOCR PP-OCRv6 small recognition model. * * Architecture overview (small_rec): * LCNetV4 backbone (Conv + BN-folded + HardSigmoid/HSwish/MaxPool) * → GlobalAvgPool collapse to [N, 120, 1, W] * → permute + squeeze → sequence [N, T, 120] * → 2 × SVTR block: * LN → (Linear → reshape → permute → slice QKV → MHA → Linear), * LN → (Linear → GELU), Linear, Add residual * → LN → Linear(120, 18710) → [N, T, 18710] * → CPU: greedy CTC decode * * The engine loads a single ONNX file (inference.onnx) and a character * dictionary (inference.yml) at load time, then runs recognize() on * arbitrary image input. */ import { ModelType } from './types.js'; export interface PaddleOcrOptions { /** * Model size variant: 'tiny', 'small', or 'medium'. * Auto-derives hfRepo, modelUrl and vocabUrl from the appropriate * HuggingFace repo when not explicitly set. */ modelType?: ModelType; /** * HuggingFace repo ID (e.g. "PaddlePaddle/PP-OCRv6_small_rec_onnx"). * When set, modelUrl and vocabUrl are auto-derived from HF. */ hfRepo?: string; /** URL to the inference.onnx model file. Required unless hfRepo or modelType is set. */ modelUrl?: string; /** URL to the inference.yml vocab file. Required unless hfRepo or modelType is set. */ vocabUrl?: string; /** Optional override of max input image width after preprocessing. */ maxWidth?: number; /** Channel ordering for the input tensor. Default: 'rgb'. */ channelOrder?: 'rgb' | 'bgr'; /** Optional progress callback. `pct` is in [0,1] when known, may be omitted. */ onProgress?: (stage: string, pct?: number) => void; } export interface RecognizeResult { text: string; confidence: number; timeMs: number; shape: number[]; } export declare class PaddleOcrEngine { private device; private pipelines; private layouts; private batchedEncoder; private uniformBuffers; private dict; private initializers; private activations; private shadowShapeValues; /** Per-width input GPU buffers — kept permanently so cached bind groups * always reference the same buffer object for a given W. */ private inputBuffers; /** Reusable Float32Array for the NCHW padded batch — one per padded width W. * Eliminates per-line heap allocation (~184 KB × N lines) and the V8 * MinorGC pressure it causes between lines. */ private batchBuffers; /** Reusable Float32Array for the raw preprocessed image — one per resized * width. Safe to reuse because we only process one line at a time in * recognizePipelined, and writeBuffer copies the data before we reuse. */ private preprocessBuffers; /** Back-compat alias updated by recognize() before calling walkGraph(). */ private inputBuffer; /** Compiled graph cache keyed by batch-size × preprocessed input width W. */ private compiledRuns; /** When true, skip the compiled-run cache and rebuild GPU resources every * call. The old bug (cached bind groups referencing stale buffers) was * caused by intermediate submit() calls between nodes: first-run recorded * all dispatches but stripped submit boundaries, so replay saw a different * command-buffer structure. Now that the graph runs as a single command * buffer (no per-node submits), first-run and replay are structurally * identical and the cache is safe. */ disableCache: boolean; /** Step list populated during the first (compilation) walk; null otherwise. */ private currentSteps; /** Cached parsed ONNX graph — populated once in init(), reused across * recognize() calls so we don't re-fetch + re-parse the 21 MB model. */ private parsedGraph; /** * Conv-bias fusion map built from the ONNX graph at parse time. * Key : Conv node's output tensor name. * Value: { biasName, addOut } where addOut is the downstream Add node's * output name (which callers downstream expect to find in acts). * When a Conv output feeds a single Add whose other input is an initializer * of the right size, we absorb the Add into the Conv dispatch and skip the * Add node entirely — eliminating 54 separate GPU passes on PP-OCRv6. */ private convBiasFuse; /** Add node output names that have been absorbed into a fused Conv. */ private fusedAddOutputs; /** Resolve a tensor by name, falling back to the initializer map. */ private resolveTensor; private modelUrl; private vocabUrl; private onProgress?; private maxWidth; /** Channel layout for input tensor. Default 'rgb'; can flip to 'bgr' via init(). */ private preprocessChannelOrder; /** Load model + char dict. Throws on failure. */ init(options: PaddleOcrOptions): Promise; /** Compile all WGSL shaders used by the engine. */ private compileShaders; /** * Fetch a binary file, using the browser Cache API to persist it across page * loads. On cache hit the progress callback jumps straight to 90% (the file * loads from disk in milliseconds). On cache miss the file is streamed with * per-chunk progress and stored in the cache for next time. * Falls through to a plain fetch if the Cache API is unavailable (private * browsing, non-secure context, quota exceeded). */ private fetchCachedBuffer; private loadModel; private uploadAsF32; private loadVocab; /** * Recognise a single image. The image is preprocessed on CPU to * [1, 3, 48, W] (BGR, float32, normalized), then the model runs on GPU. */ /** Last preprocessed tensor — exposed so the demo can render a debug preview. */ lastPreprocessed: { shape: number[]; data: Float32Array; } | null; /** * Pipelined single-line recognition: submits all lines' GPU work first, * then collects readbacks. Uses N=1 per chunk (avoids the N>1 batch bug) * but queues all GPU dispatches before the first mapAsync, so the Chrome * IPC latency (~37ms) is paid once for the whole page instead of per line. */ recognizePipelined(canvases: HTMLCanvasElement[]): Promise; recognize(image: HTMLCanvasElement | HTMLImageElement | ImageData | Uint8ClampedArray | Float32Array): Promise; /** * Batch-recognize multiple text-line images in a single GPU forward pass. * Preprocesses each canvas, pads to a common width, dispatches the graph * once, and returns one result per input. */ recognizeMany(canvases: HTMLCanvasElement[]): Promise; /** * Resize to height 48 (preserving aspect ratio up to maxWidth), convert to * BGR, normalize to [0,1] float, return as Float32Array with shape [1,3,48,W]. */ private preprocessToTensor; /** * Re-parse ONNX at recognition time so we don't have to ship parsed graph * state. (The engine already has weights; we need node list + connections.) */ private parseGraph; /** * The ONNX model was exported with fixed batch=1. Reshape nodes that * restore the NCHW layout use hardcoded `[1, C, H, -1]` shape initializers. * Replace leading `1` with `-1` so the runtime -1 inference handles any N. */ private patchReshapeBatchDims; /** Debug: quick statistic about nodes by op_type for debugging graph parsing. */ private dumpNodeHistogram; /** * Pre-scan the ONNX graph once to find Conv → Add(bias) pairs that can be * fused into a single Conv dispatch. PP-OCRv6 has 54 such pairs (all Conv * nodes export without bias; the bias is a separate Add with an initializer). * Fusing eliminates 54 GPU passes and their intermediate buffer round-trips. */ private buildConvBiasFuseMap; private walkGraph; /** Allocate a fresh activation buffer (first-run only; subsequent runs * reuse cached buffers via the compiled-run activation map). */ private alloc; /** alloc + return as ActTensor for storing in `activations`. */ private allocTensor; /** * Create a uniform buffer + queue.writeBuffer. Caller is responsible for * retaining the buffer until the dispatch is submitted. */ /** Allocate a small 16-byte zero-buffer used as a dummy bias slot when * a kernel expects one but we don't need it. */ /** * Build a GPUCommandEncoder (or a duck-typed recording wrapper when * `this.currentSteps` is non-null) that intercepts every compute-pass * dispatch and buffer copy so they can be replayed on subsequent calls. */ private makeRecordingEncoder; private makeZeroUniform; private makeUniform; /** * Compute total elements from ONNX-style shape (negative-dim safe). */ private shapeTotal; private dispatchNode; private dispatchConv; private dispatchRelu; private dispatchHardSigmoid; private dispatchSigmoid; private dispatchTanh; private dispatchHardSwish; private dispatchLeakyRelu; private dispatchUnary; private dispatchErf; private dispatchMath; private dispatchBinaryBroadcast; private sameShape; private broadcastShape; private isLastDimBroadcast; private dispatchReduceMean; private dispatchReduceSum; private dispatchReduceMax; private dispatchSoftmax; private dispatchMatMul; private dispatchGemm; private dispatchMaxPool; private dispatchAveragePool; private dispatchGlobalAveragePool; private dispatchGlobalMaxPool; private dispatchReshape; private dispatchTranspose; private dispatchSqueeze; private dispatchUnsqueeze; private dispatchShape; private dispatchGather; private dispatchSlice; private computeStrides; /** For Concat axis=0 / dynamic shape sources, return the slice length * contributed by this input name (1 for a constant; full size for Slice; * 0 if missing). */ private inputLen; /** Synchronously read a small shape-tensor (rank <= 8) from a GPU buffer. * Submits the current batched encoder mid-flight, copies source → staging, * submits, then maps the staging buffer and reads. */ private readShapeFromBuffer; private dispatchConcat; private dispatchFlatten; private dispatchBatchNorm; private dispatchInstanceNorm; private dispatchLayerNormOp; private readOutputAndDecode; /** * Submits the argMax + staging-copy encoders for the current activations but * does NOT await mapAsync. Call collectArgMaxReadback() on the returned * handle after all lines have been submitted to batch the Chrome IPC wait. */ private submitArgMaxNonBlocking; private collectArgMaxReadback; private readBuffer; } //# sourceMappingURL=engine-ppocr.d.ts.map