@stabrise/scaledp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +218 -0
  3. package/dist/box-DAfzwfhA.d.ts +119 -0
  4. package/dist/config-g6IrKlDC.d.ts +80 -0
  5. package/dist/data-to-image-DoZ4jQ3R.js +54 -0
  6. package/dist/data-to-image-DoZ4jQ3R.js.map +1 -0
  7. package/dist/detect/index.d.ts +71 -0
  8. package/dist/detect/index.js +2 -0
  9. package/dist/detect-q8AI_Jdj.js +274 -0
  10. package/dist/detect-q8AI_Jdj.js.map +1 -0
  11. package/dist/detector-output-C0Qt-jEq.d.ts +13 -0
  12. package/dist/detector-output-lyF1Mqb8.js +13 -0
  13. package/dist/detector-output-lyF1Mqb8.js.map +1 -0
  14. package/dist/display/index.d.ts +66 -0
  15. package/dist/display/index.js +237 -0
  16. package/dist/display/index.js.map +1 -0
  17. package/dist/document-B8I61TiY.d.ts +16 -0
  18. package/dist/entity-CedtRhU1.d.ts +22 -0
  19. package/dist/entity-D6Hxaugj.js +13 -0
  20. package/dist/entity-D6Hxaugj.js.map +1 -0
  21. package/dist/image-CAH2rLv9.js +511 -0
  22. package/dist/image-CAH2rLv9.js.map +1 -0
  23. package/dist/image-Dc5TSg46.d.ts +18 -0
  24. package/dist/image-DoZDJkcR.js +37 -0
  25. package/dist/image-DoZDJkcR.js.map +1 -0
  26. package/dist/image-draw-boxes-De0QbFv9.js +285 -0
  27. package/dist/image-draw-boxes-De0QbFv9.js.map +1 -0
  28. package/dist/index.d.ts +269 -0
  29. package/dist/index.js +11 -0
  30. package/dist/model-cache-BEaqqRZ9.js +182 -0
  31. package/dist/model-cache-BEaqqRZ9.js.map +1 -0
  32. package/dist/model-cache-BhFYpfZz.d.ts +36 -0
  33. package/dist/ner/index.d.ts +293 -0
  34. package/dist/ner/index.js +2 -0
  35. package/dist/ner-SsZLZ6ed.js +1028 -0
  36. package/dist/ner-SsZLZ6ed.js.map +1 -0
  37. package/dist/ocr/index.d.ts +440 -0
  38. package/dist/ocr/index.js +3 -0
  39. package/dist/ocr-OHX2WM3e.js +1294 -0
  40. package/dist/ocr-OHX2WM3e.js.map +1 -0
  41. package/dist/ort-CXDoPrtw.js +73 -0
  42. package/dist/ort-CXDoPrtw.js.map +1 -0
  43. package/dist/params-DapwK9Ns.js +37 -0
  44. package/dist/params-DapwK9Ns.js.map +1 -0
  45. package/dist/pdf/index.d.ts +123 -0
  46. package/dist/pdf/index.js +2 -0
  47. package/dist/pdf-BQl0dneD.js +417 -0
  48. package/dist/pdf-BQl0dneD.js.map +1 -0
  49. package/dist/pipeline-DACqGkpN.js +240 -0
  50. package/dist/pipeline-DACqGkpN.js.map +1 -0
  51. package/dist/pipeline-DeLO-OCE.d.ts +139 -0
  52. package/dist/registry/index.d.ts +169 -0
  53. package/dist/registry/index.js +1061 -0
  54. package/dist/registry/index.js.map +1 -0
  55. package/dist/text-ahMLpxN9.js +109 -0
  56. package/dist/text-ahMLpxN9.js.map +1 -0
  57. package/dist/worker/index.d.ts +105 -0
  58. package/dist/worker/index.js +180 -0
  59. package/dist/worker/index.js.map +1 -0
  60. package/package.json +135 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/registry/catalog.ts","../../src/registry/codegen.ts","../../src/registry/index.ts"],"sourcesContent":["/**\n * The stage catalogue: one `StageSpec` per concrete stage.\n *\n * Transcribed from `docs/stages.md`, which is the same table in prose. Every\n * `defaults` entry is the stage's own frozen constant rather than a copy, and\n * every enum's options come from the model registries that already exist\n * (`PADDLE_OCR_PRESETS`, `NER_MODELS`, `DETECTOR_MODELS`) rather than a second\n * hand-maintained list.\n *\n * Importing this module costs no ML runtime: every engine in the library is\n * reached through a dynamic `import()` inside `ocr/ort.ts`, `pdf/pdfjs.ts`,\n * `ocr/paddle-service.ts` and `ocr/tesseract.ts`, so pulling in the twelve stage\n * classes pulls in no `onnxruntime-web`, `pdfjs-dist`, `ppu-paddle-ocr` or\n * `tesseract-wasm`. It does pull in every stage's code, which is why this lives\n * behind its own subpath instead of the root barrel.\n */\n\nimport { FaceDetector, SignatureDetector, YOLO_DETECTOR_DEFAULTS, YoloOnnxDetector } from '../detect/index.js'\nimport { GLINER_NER_DEFAULTS, GlinerNer, modelSizeBytes, NER_MODELS } from '../ner/index.js'\nimport {\n DBNET_DETECTOR_DEFAULTS,\n DbnetOnnxDetector,\n DEFAULT_ORIENTATION_MODEL,\n DETECTOR_MODELS,\n LINE_ORIENTATION_DEFAULTS,\n LineOrientationDetector,\n PADDLE_DETECTOR_DEFAULTS,\n PADDLE_OCR_PRESETS,\n PADDLE_RECOGNIZER_DEFAULTS,\n PaddleTextDetector,\n PaddleTextRecognizer,\n TESSERACT_OCR_DEFAULTS,\n TESSERACT_RECOGNIZER_DEFAULTS,\n TesseractOcr,\n TesseractRecognizer,\n} from '../ocr/index.js'\nimport { PDF_TO_DOCUMENT_DEFAULTS, PDF_TO_IMAGE_DEFAULTS, PdfToDocument, PdfToImage } from '../pdf/index.js'\nimport { DATA_TO_IMAGE_DEFAULTS, DataToImage } from '../stages/data-to-image.js'\nimport { IMAGE_CROP_BOXES_DEFAULTS, ImageCropBoxes } from '../stages/image-crop-boxes.js'\nimport { IMAGE_DRAW_BOXES_DEFAULTS, ImageDrawBoxes } from '../stages/image-draw-boxes.js'\nimport type { ColumnKind, StageParamOption, StageParamSpec, StageSpec } from './types.js'\n\n/**\n * Widen a stage's typed defaults into an indexable record.\n *\n * `*_DEFAULTS` are typed as their stage's params interface, and an interface has\n * no index signature -- so it is not assignable to `Record<string, unknown>`\n * even though every key is a string. The values are frozen and read-only either\n * way; only the type changes.\n */\nconst asRecord = <T extends object>(defaults: Readonly<T>): Readonly<Record<string, unknown>> =>\n defaults as Readonly<Record<string, unknown>>\n\n/* ── Enum option lists, derived from the existing registries ─────────────── */\n\nconst IMAGE_TYPES: readonly StageParamOption[] = Object.freeze([\n { value: 'png', label: 'PNG', title: 'Lossless; the safe default for OCR' },\n { value: 'webp', label: 'WebP', title: 'Smaller, lossy' },\n { value: 'jpeg', label: 'JPEG', title: 'Smallest, lossy; artefacts can cost accuracy' },\n])\n\nconst STRATEGIES: readonly StageParamOption[] = Object.freeze([\n { value: 'per-box', label: 'Per box', title: 'Word-level boxes, as detected' },\n { value: 'per-line', label: 'Per line', title: 'Merge boxes that share a line' },\n { value: 'cross-line', label: 'Cross line', title: 'Merge across line breaks too' },\n])\n\n/**\n * Fields `ImageDrawBoxes` can render above a box.\n *\n * The label is built by reading these off the box or entity, so the valid names\n * are the schema's own field names -- `Box` for a detector or OCR result,\n * `Entity` for NER output. Numbers are formatted to two decimals.\n */\nconst LABEL_FIELDS: readonly StageParamOption[] = Object.freeze([\n { value: 'text', label: 'text', title: 'Box: the recognised text' },\n { value: 'score', label: 'score', title: 'Box or entity: confidence, 0-1' },\n { value: 'angle', label: 'angle', title: 'Box: degrees about its centre' },\n { value: 'entity_group', label: 'entity_group', title: 'Entity: the label it matched' },\n { value: 'word', label: 'word', title: 'Entity: the matched text' },\n { value: 'x', label: 'x', title: 'Box: top-left x' },\n { value: 'y', label: 'y', title: 'Box: top-left y' },\n { value: 'width', label: 'width', title: 'Box: the longer side' },\n { value: 'height', label: 'height', title: 'Box: the shorter side' },\n])\n\nconst OCR_PRESETS: readonly StageParamOption[] = Object.freeze(\n PADDLE_OCR_PRESETS.map((preset) => ({\n value: preset.value,\n label: preset.label,\n title: preset.scripts.join(', '),\n }))\n)\n\nconst NER_MODEL_OPTIONS: readonly StageParamOption[] = Object.freeze(\n NER_MODELS.map((model) => ({\n value: model.id,\n label: `${model.name} · ${model.languages.join('/')}${model.private ? ' · private' : ''}`,\n title: `${model.arch}, ${Math.round(modelSizeBytes(model) / 1e6)} MB, ${model.repo}`,\n // Private repos need configure({ auth }); offering one without it only\n // produces a 401 mid-pipeline.\n disabled: model.private === true,\n }))\n)\n\nconst DBNET_MODEL_OPTIONS: readonly StageParamOption[] = Object.freeze(\n DETECTOR_MODELS.filter((model) => model.kind === 'dbnet-onnx' && model.repo).map((model) => ({\n value: model.repo as string,\n label: model.name,\n title: model.notes,\n }))\n)\n\n/* ── Param builders ──────────────────────────────────────────────────────── */\n\nconst column = (key: string, label: string, extra: Partial<StageParamSpec> = {}): StageParamSpec => ({\n key,\n kind: 'column',\n label,\n ...extra,\n})\n\n/**\n * The six params every stage inherits from `BaseStageParams`.\n *\n * `inputCol` and `outputCol` are the wiring a builder actually cares about;\n * the rest is plumbing, so it is marked advanced. Stages that read several\n * columns pass `multiInput` -- there `inputCol` is inherited but unused, and\n * showing it as the input would be a lie.\n */\nfunction baseParams(\n options: { input?: { help?: string; accepts?: ColumnKind[] } | 'unused'; output?: { help?: string } } = {}\n): StageParamSpec[] {\n const input =\n options.input === 'unused'\n ? column('inputCol', 'Input column', {\n advanced: true,\n help: 'Inherited but unused: this stage reads the columns listed in inputCols.',\n })\n : column('inputCol', 'Input column', {\n help: options.input?.help,\n accepts: options.input?.accepts,\n })\n\n return [\n input,\n column('outputCol', 'Output column', { help: options.output?.help }),\n column('pathCol', 'Path column', {\n advanced: true,\n help: 'Row field holding the source path, copied onto the output.',\n }),\n column('pageCol', 'Page column', {\n advanced: true,\n help: 'Row field holding the page index for multi-page inputs.',\n }),\n {\n key: 'keepInputData',\n kind: 'boolean',\n label: 'Keep input column',\n advanced: true,\n help: 'Keep the input column in the output rows instead of dropping it.',\n },\n {\n key: 'propagateError',\n kind: 'boolean',\n label: 'Throw on error',\n advanced: true,\n help: 'Throw instead of recording the failure in the output’s exception field.',\n },\n ]\n}\n\nconst imageType: StageParamSpec = {\n key: 'imageType',\n kind: 'enum',\n label: 'Image format',\n options: IMAGE_TYPES,\n}\n\nconst scoreThreshold = (help: string): StageParamSpec => ({\n key: 'scoreThreshold',\n kind: 'number',\n label: 'Score threshold',\n min: 0,\n max: 1,\n step: 0.05,\n help,\n})\n\nconst lang: StageParamSpec = {\n key: 'lang',\n kind: 'stringList',\n label: 'Languages',\n required: true,\n help: 'Tesseract language codes, e.g. eng or eng, deu. Each needs its traineddata file.',\n}\n\nconst keepFormatting: StageParamSpec = {\n key: 'keepFormatting',\n kind: 'boolean',\n label: 'Keep formatting',\n help: 'Rebuild the original layout with spaces and blank lines.',\n}\n\nconst lineTolerance: StageParamSpec = {\n key: 'lineTolerance',\n kind: 'number',\n label: 'Line tolerance',\n min: 0,\n step: 1,\n help: 'Line-grouping tolerance in pixels; 0 derives it from character height.',\n}\n\n/**\n * `SignatureDetector` and `FaceDetector` are `YoloOnnxDetector` with different\n * pre-set params (`src/detect/index.ts`), so they take the same parameters.\n *\n * One caveat those two carry: they spread their options straight into `super`\n * rather than going through `resolveParams`, so their validators never run --\n * an empty `model` is accepted there and fails later, at init.\n */\nconst yoloParams: readonly StageParamSpec[] = Object.freeze([\n ...baseParams({ input: { accepts: ['image'] } }),\n {\n key: 'model',\n kind: 'string',\n label: 'Model',\n required: true,\n help: 'Hugging Face repo id, or a URL when self-hosting. No default.',\n },\n {\n key: 'labels',\n kind: 'stringList',\n label: 'Labels',\n help: 'Class index → label. Empty falls back to class_<n>.',\n },\n scoreThreshold('Drop detections below this confidence.'),\n {\n key: 'iouThreshold',\n kind: 'number',\n label: 'IoU threshold',\n min: 0,\n max: 1,\n step: 0.05,\n help: 'Overlap above which two same-class boxes count as duplicates.',\n },\n {\n key: 'padding',\n kind: 'number',\n label: 'Padding',\n min: 0,\n max: 1,\n step: 0.01,\n help: 'Grow each box by this fraction of its size, to avoid clipping edges.',\n },\n {\n key: 'outputType',\n kind: 'string',\n label: 'Output type',\n advanced: true,\n help: 'Recorded as DetectorOutput.type.',\n },\n])\n\n/* ── The catalogue ───────────────────────────────────────────────────────── */\n\nexport const STAGE_SPECS: readonly StageSpec[] = Object.freeze([\n {\n type: 'DataToImage',\n label: 'Image bytes',\n group: 'Read',\n subpath: '@stabrise/scaledp',\n summary: 'Wrap raw image bytes as an Image without re-encoding them.',\n consumes: ['bytes'],\n produces: 'image',\n defaults: asRecord(DATA_TO_IMAGE_DEFAULTS),\n params: [\n ...baseParams({ input: { help: 'Raw file bytes, normally the content column.' } }),\n { ...imageType, help: 'Encoding recorded on the output; the bytes pass through unchanged.' },\n {\n key: 'resolution',\n kind: 'number',\n label: 'Resolution',\n min: 0,\n step: 1,\n help: 'DPI to record when the row carries no resolution field. 0 leaves it unknown.',\n },\n ],\n },\n {\n type: 'PdfToImage',\n label: 'PDF pages',\n group: 'Read',\n subpath: '@stabrise/scaledp/pdf',\n summary: 'Rasterise every page of a PDF, one output row per page.',\n consumes: ['bytes'],\n produces: 'image',\n peer: 'pdfjs-dist',\n expands: true,\n defaults: asRecord(PDF_TO_IMAGE_DEFAULTS),\n params: [\n ...baseParams({ input: { help: 'Raw PDF bytes, normally the content column.' } }),\n {\n key: 'resolution',\n kind: 'number',\n label: 'Resolution (DPI)',\n min: 36,\n max: 600,\n step: 1,\n help: 'Render DPI. 300 matches ScaleDP; 200 is a good speed/accuracy trade.',\n },\n {\n key: 'pageLimit',\n kind: 'number',\n label: 'Page limit',\n min: 0,\n step: 1,\n help: '0 renders every page.',\n },\n imageType,\n ],\n },\n {\n type: 'PdfToDocument',\n label: 'PDF text layer',\n group: 'Read',\n subpath: '@stabrise/scaledp/pdf',\n summary: 'Read a PDF’s embedded text layer, skipping OCR entirely.',\n consumes: ['bytes'],\n produces: 'document',\n peer: 'pdfjs-dist',\n expands: true,\n defaults: asRecord(PDF_TO_DOCUMENT_DEFAULTS),\n params: [\n ...baseParams({ input: { help: 'Raw PDF bytes, normally the content column.' } }),\n {\n key: 'resolution',\n kind: 'number',\n label: 'Resolution (DPI)',\n min: 36,\n max: 600,\n step: 1,\n help: 'Pixel space the boxes are expressed in. Match PdfToImage to align them.',\n },\n {\n key: 'pageLimit',\n kind: 'number',\n label: 'Page limit',\n min: 0,\n step: 1,\n help: '0 reads every page.',\n },\n {\n key: 'splitWords',\n kind: 'boolean',\n label: 'Split words',\n help: 'Split pdf.js line runs into word boxes. Off yields run-level boxes.',\n },\n ],\n },\n {\n type: 'PaddleTextDetector',\n label: 'PaddleOCR detector',\n group: 'Detect',\n subpath: '@stabrise/scaledp/ocr',\n summary: 'Find text regions with the detection half of a PaddleOCR preset. Regions are line-level.',\n consumes: ['image'],\n produces: 'boxes',\n peer: 'ppu-paddle-ocr',\n cache: { kind: 'paddle-preset', param: 'preset' },\n defaults: asRecord(PADDLE_DETECTOR_DEFAULTS),\n params: [\n ...baseParams({ input: { accepts: ['image'] } }),\n {\n key: 'preset',\n kind: 'enum',\n label: 'Preset',\n options: OCR_PRESETS,\n help: 'Language/script pairing. Detection and recognition share the download.',\n },\n scoreThreshold('Drop regions below this confidence. 0 keeps everything.'),\n ],\n },\n {\n type: 'DbnetOnnxDetector',\n label: 'DBNet detector',\n group: 'Detect',\n subpath: '@stabrise/scaledp/ocr',\n summary:\n 'The DBNet ONNX detector ScaleDP uses server-side. Finds rotated regions, one box per text line.',\n consumes: ['image'],\n produces: 'boxes',\n peer: 'onnxruntime-web',\n cache: { kind: 'hf-repo', param: 'model', approxBytes: 4_800_000 },\n defaults: asRecord(DBNET_DETECTOR_DEFAULTS),\n params: [\n ...baseParams({ input: { accepts: ['image'] } }),\n {\n key: 'model',\n kind: 'enum',\n label: 'Model',\n options: DBNET_MODEL_OPTIONS,\n allowCustom: true,\n help: 'Hugging Face repo id, or a URL when self-hosting.',\n },\n scoreThreshold('Mean in-box probability a candidate must reach.'),\n {\n key: 'binaryThreshold',\n kind: 'number',\n label: 'Binary threshold',\n min: 0,\n max: 1,\n step: 0.05,\n help: 'Probability above which a pixel counts as text.',\n },\n {\n key: 'unclipRatio',\n kind: 'number',\n label: 'Unclip ratio',\n min: 0.5,\n max: 5,\n step: 0.1,\n help: 'How far to grow each box; DB shrinks text regions during training.',\n },\n {\n key: 'mergeBoxes',\n kind: 'boolean',\n label: 'Merge boxes',\n help: 'Merge overlapping boxes that share a line. Usually changes nothing here — this model already returns one region per line, so no two boxes overlap.',\n },\n ],\n },\n {\n type: 'YoloOnnxDetector',\n label: 'YOLO detector',\n group: 'Detect',\n subpath: '@stabrise/scaledp/detect',\n summary: 'Detect arbitrary objects with a YOLO ONNX model.',\n consumes: ['image'],\n produces: 'boxes',\n peer: 'onnxruntime-web',\n cache: { kind: 'hf-repo', param: 'model' },\n defaults: asRecord(YOLO_DETECTOR_DEFAULTS),\n params: yoloParams,\n },\n {\n type: 'SignatureDetector',\n label: 'Signature detector',\n group: 'Detect',\n subpath: '@stabrise/scaledp/detect',\n summary: 'YOLO pre-set for StabRise/signature_detection.',\n consumes: ['image'],\n produces: 'boxes',\n peer: 'onnxruntime-web',\n cache: { kind: 'hf-repo', param: 'model' },\n defaults: asRecord({\n ...YOLO_DETECTOR_DEFAULTS,\n model: 'StabRise/signature_detection',\n labels: ['signature'],\n outputCol: 'signatures',\n outputType: 'signature',\n scoreThreshold: 0.2,\n }),\n params: yoloParams,\n },\n {\n type: 'FaceDetector',\n label: 'Face detector',\n group: 'Detect',\n subpath: '@stabrise/scaledp/detect',\n summary: 'YOLO pre-set for StabRise/face_detection.',\n consumes: ['image'],\n produces: 'boxes',\n peer: 'onnxruntime-web',\n cache: { kind: 'hf-repo', param: 'model' },\n defaults: asRecord({\n ...YOLO_DETECTOR_DEFAULTS,\n model: 'StabRise/face_detection',\n labels: ['face'],\n outputCol: 'faces',\n outputType: 'face',\n scoreThreshold: 0.2,\n }),\n params: yoloParams,\n },\n {\n type: 'LineOrientationDetector',\n label: 'Line orientation',\n group: 'Transform',\n subpath: '@stabrise/scaledp/ocr',\n summary: 'Classify each detected region 0°/180° and turn the inverted ones.',\n consumes: ['image', 'boxes'],\n produces: 'image',\n alsoProduces: [{ param: 'orientationCol', kind: 'orientations' }],\n peer: 'onnxruntime-web',\n cache: { kind: 'hf-repo', param: 'model', approxBytes: 9_000_000 },\n expands: true,\n defaults: asRecord(LINE_ORIENTATION_DEFAULTS),\n params: [\n ...baseParams({\n input: 'unused',\n output: { help: 'The corrected page image.' },\n }),\n {\n key: 'inputCols',\n kind: 'columns',\n label: 'Input columns',\n arity: 2,\n accepts: ['image', 'boxes'],\n help: 'The page image, then the detector output whose regions to classify.',\n },\n column('orientationCol', 'Orientation column', {\n help: 'Per-region 0_degree / 180_degree labels.',\n }),\n {\n key: 'model',\n kind: 'string',\n label: 'Model',\n help: `Hugging Face repo id. Defaults to ${DEFAULT_ORIENTATION_MODEL}.`,\n },\n {\n key: 'correct',\n kind: 'boolean',\n label: 'Correct',\n help: 'Turn the inverted regions. Off classifies only, leaving the page as-is.',\n },\n {\n key: 'onlyRotated',\n kind: 'boolean',\n label: 'Only rotated',\n help: 'Classify only already-rotated boxes, where the signal is. Off also catches upside-down horizontal text, at a false-positive cost.',\n },\n {\n key: 'padding',\n kind: 'number',\n label: 'Padding',\n min: 0,\n step: 1,\n help: 'Grow each box before cropping, so glyph edges are not clipped.',\n },\n imageType,\n ],\n },\n {\n type: 'PaddleTextRecognizer',\n label: 'PaddleOCR',\n group: 'Recognise',\n subpath: '@stabrise/scaledp/ocr',\n summary: 'Detect and read a page in one pass. Ignores any separate detector’s boxes.',\n consumes: ['image'],\n produces: 'document',\n peer: 'ppu-paddle-ocr',\n cache: { kind: 'paddle-preset', param: 'preset' },\n defaults: asRecord(PADDLE_RECOGNIZER_DEFAULTS),\n params: [\n ...baseParams({ input: { accepts: ['image'] } }),\n {\n key: 'preset',\n kind: 'enum',\n label: 'Preset',\n options: OCR_PRESETS,\n help: 'Language/script pairing. Pick the one matching your documents.',\n },\n scoreThreshold('Drop words below this confidence.'),\n {\n key: 'strategy',\n kind: 'enum',\n label: 'Strategy',\n help: 'How the detected regions are grouped. It cannot subdivide them: the boxes are whatever the preset’s detector found, which is line-level.',\n options: STRATEGIES,\n },\n keepFormatting,\n lineTolerance,\n ],\n },\n {\n type: 'TesseractOcr',\n label: 'Tesseract',\n group: 'Recognise',\n subpath: '@stabrise/scaledp/ocr',\n summary:\n 'Read a whole page with Tesseract, detecting layout itself. The one stage here that returns word-level boxes.',\n consumes: ['image'],\n produces: 'document',\n peer: 'tesseract-wasm',\n defaults: asRecord(TESSERACT_OCR_DEFAULTS),\n params: [\n ...baseParams({ input: { accepts: ['image'] } }),\n lang,\n scoreThreshold('Drop words below this confidence.'),\n keepFormatting,\n lineTolerance,\n ],\n },\n {\n type: 'TesseractRecognizer',\n label: 'Tesseract on boxes',\n group: 'Recognise',\n subpath: '@stabrise/scaledp/ocr',\n summary:\n 'Read exactly the regions a detector found, cropping and turning each one. Returns a box per region, or per word with “Box level”.',\n consumes: ['image', 'boxes'],\n produces: 'document',\n peer: 'tesseract-wasm',\n cache: { kind: 'hf-repo', param: 'oriModel', approxBytes: 9_000_000 },\n defaults: asRecord(TESSERACT_RECOGNIZER_DEFAULTS),\n params: [\n ...baseParams({ input: 'unused' }),\n {\n key: 'inputCols',\n kind: 'columns',\n label: 'Input columns',\n arity: 2,\n accepts: ['image', 'boxes'],\n help: 'The page image, then the detector output whose regions to read.',\n },\n lang,\n {\n key: 'boxLevel',\n kind: 'enum',\n label: 'Box level',\n options: [\n {\n value: 'region',\n label: 'One box per region',\n title: 'ScaleDP’s behaviour: each detected region keeps its own box, carrying everything read inside it',\n },\n {\n value: 'word',\n label: 'One box per word',\n title: 'Tesseract’s own word boxes, mapped back into page coordinates',\n },\n ],\n help: 'The detectors here are line-level, so “region” gives line boxes. Pick “word” to get one box per word instead.',\n },\n {\n key: 'scaleFactor',\n kind: 'number',\n label: 'Scale factor',\n min: 0.1,\n max: 8,\n step: 0.1,\n help: 'Resize the page by this factor before cropping.',\n },\n {\n key: 'padding',\n kind: 'number',\n label: 'Padding',\n min: 0,\n step: 1,\n help: 'Grow each box before cropping. ScaleDP hardcodes 5.',\n },\n scoreThreshold('Drop words below this confidence.'),\n keepFormatting,\n lineTolerance,\n {\n key: 'detectLineOrientation',\n kind: 'boolean',\n label: 'Detect line orientation',\n help: 'Classify each crop 0°/180° and turn the inverted ones.',\n },\n {\n key: 'onlyRotated',\n kind: 'boolean',\n label: 'Only rotated',\n help: 'Read only rotated or inverted boxes. On, an ordinary page returns nothing.',\n },\n {\n key: 'oriModel',\n kind: 'string',\n label: 'Orientation model',\n advanced: true,\n help: `Hugging Face repo id. Defaults to ${DEFAULT_ORIENTATION_MODEL}.`,\n },\n ],\n },\n {\n type: 'GlinerNer',\n label: 'GLiNER entities',\n group: 'Understand',\n subpath: '@stabrise/scaledp/ner',\n summary: 'Find named entities in a document, scored against labels you write.',\n consumes: ['document'],\n produces: 'ner',\n peer: '@huggingface/transformers',\n cache: { kind: 'ner-id', param: 'model' },\n defaults: asRecord(GLINER_NER_DEFAULTS),\n params: [\n ...baseParams({ input: { accepts: ['document'] } }),\n {\n key: 'model',\n kind: 'enum',\n label: 'Model',\n options: NER_MODEL_OPTIONS,\n help: 'Registry id. Private repos need configure({ auth }).',\n },\n {\n key: 'labels',\n kind: 'stringList',\n label: 'Labels',\n required: true,\n help: 'GLiNER scores a label by its prompt text, so other wording asks a different question.',\n },\n {\n key: 'threshold',\n kind: 'number',\n label: 'Threshold',\n min: 0,\n max: 1,\n step: 0.05,\n help: 'Minimum score an entity must reach.',\n },\n {\n key: 'whiteList',\n kind: 'stringList',\n label: 'Only these groups',\n help: 'Keep only these entity groups; empty keeps everything.',\n },\n {\n key: 'chunkLength',\n kind: 'number',\n label: 'Chunk length',\n min: 1,\n step: 1,\n advanced: true,\n help: 'Tokens per inference window.',\n },\n {\n key: 'chunkStride',\n kind: 'number',\n label: 'Chunk stride',\n min: 1,\n step: 1,\n advanced: true,\n help: 'Window step. Below chunk length, windows overlap.',\n },\n {\n key: 'normaliseCasing',\n kind: 'boolean',\n label: 'Normalise casing',\n help: 'Title-case runs of capitals first. GLiNER1 models are cased; scans are often all caps.',\n },\n ],\n },\n {\n type: 'ImageDrawBoxes',\n label: 'Draw boxes',\n group: 'Transform',\n subpath: '@stabrise/scaledp',\n summary: 'Annotate a page with any boxes or entities found so far.',\n consumes: ['image', 'boxes'],\n produces: 'image',\n terminal: true,\n defaults: asRecord(IMAGE_DRAW_BOXES_DEFAULTS),\n params: [\n ...baseParams({ input: 'unused' }),\n {\n key: 'inputCols',\n kind: 'columns',\n label: 'Input columns',\n minArity: 2,\n accepts: ['image', 'boxes'],\n help: 'The image first, then one or more box, document or entity columns.',\n },\n {\n key: 'color',\n kind: 'color',\n label: 'Colour',\n help: 'One colour for every box. Unset colours by group instead.',\n },\n {\n key: 'filled',\n kind: 'boolean',\n label: 'Filled',\n help: 'Fill boxes as well as outlining them.',\n },\n { key: 'lineWidth', kind: 'number', label: 'Line width', min: 0, max: 20, step: 1 },\n { key: 'textSize', kind: 'number', label: 'Text size', min: 4, max: 72, step: 1 },\n {\n key: 'displayDataList',\n kind: 'stringList',\n label: 'Label fields',\n options: LABEL_FIELDS,\n help: 'Rendered above each box, joined by \":\". Pick \"text\" to read the OCR output back off the page.',\n },\n {\n key: 'padding',\n kind: 'number',\n label: 'Padding',\n min: 0,\n step: 1,\n help: 'Grow each box by this many pixels before drawing.',\n },\n {\n key: 'whiteList',\n kind: 'stringList',\n label: 'Only these groups',\n help: 'Draw only these entity groups; empty draws all.',\n },\n {\n key: 'blackList',\n kind: 'stringList',\n label: 'Never these groups',\n help: 'Never draw these entity groups.',\n },\n imageType,\n ],\n },\n {\n type: 'ImageCropBoxes',\n label: 'Crop boxes',\n group: 'Transform',\n subpath: '@stabrise/scaledp',\n summary: 'Cut each detected region out of the page, one output row per crop.',\n consumes: ['image', 'boxes'],\n produces: 'image',\n alsoProduces: [{ param: 'boxCol', kind: 'box' }],\n expands: true,\n defaults: asRecord(IMAGE_CROP_BOXES_DEFAULTS),\n params: [\n ...baseParams({ input: 'unused' }),\n {\n key: 'inputCols',\n kind: 'columns',\n label: 'Input columns',\n arity: 2,\n accepts: ['image', 'boxes'],\n help: 'The page image, then the box column to cut from it.',\n },\n column('boxCol', 'Box column', { help: 'Where each crop’s source box is written.' }),\n {\n key: 'padding',\n kind: 'number',\n label: 'Padding',\n min: 0,\n step: 1,\n help: 'Grow each box by this many pixels before cropping.',\n },\n {\n key: 'limit',\n kind: 'number',\n label: 'Limit',\n min: 0,\n step: 1,\n help: 'Maximum crops per page; 0 means all of them.',\n },\n {\n key: 'autoRotate',\n kind: 'boolean',\n label: 'Auto-rotate',\n help: 'Turn portrait crops a quarter turn, so text reads horizontally.',\n },\n {\n key: 'returnEmpty',\n kind: 'boolean',\n label: 'Return empty',\n help: 'Emit the whole page when nothing was detected, instead of failing.',\n },\n imageType,\n ],\n },\n])\n\n/** Name → constructor, for building a stage from a `StageDescriptor`. */\nexport const STAGE_CLASSES = Object.freeze({\n DataToImage,\n DbnetOnnxDetector,\n FaceDetector,\n GlinerNer,\n ImageCropBoxes,\n ImageDrawBoxes,\n LineOrientationDetector,\n PaddleTextDetector,\n PaddleTextRecognizer,\n PdfToDocument,\n PdfToImage,\n SignatureDetector,\n TesseractOcr,\n TesseractRecognizer,\n YoloOnnxDetector,\n}) as Readonly<Record<string, new (options?: never) => import('../core/pipeline.js').Stage>>\n","/**\n * A pipeline, written back out as the TypeScript that builds it.\n *\n * The registry already knows which subpath each stage is imported from and what\n * its defaults are, which is everything the source needs. Emitting only the\n * options that differ from the defaults keeps the result readable as the set of\n * decisions someone actually made -- the same reason the JSON form stores only\n * those.\n */\n\nimport type { StageDescriptor } from '../core/pipeline.js'\nimport { STAGE_SPECS } from './catalog.js'\n\nexport interface PipelineCodeOptions {\n /** Name bound to the pipeline. */\n variable?: string\n /** Emit the import block above it. */\n imports?: boolean\n /** Spaces per indent level. */\n indent?: number\n}\n\n/** Structural equality, deep enough for param values: scalars and string lists. */\nfunction same(a: unknown, b: unknown): boolean {\n if (Array.isArray(a) && Array.isArray(b)) {\n return a.length === b.length && a.every((value, index) => value === b[index])\n }\n return a === b\n}\n\n/** A value as TypeScript source, in the quote style the library is written in. */\nfunction literal(value: unknown): string {\n if (typeof value === 'string') return `'${value.replace(/\\\\/g, '\\\\\\\\').replace(/'/g, \"\\\\'\")}'`\n if (typeof value === 'number' || typeof value === 'boolean' || value === null) return String(value)\n if (Array.isArray(value)) return `[${value.map(literal).join(', ')}]`\n return JSON.stringify(value) ?? 'undefined'\n}\n\n/** The options that differ from the stage's defaults, as `key: value` source. */\nfunction changedOptions(descriptor: StageDescriptor): string[] {\n const spec = STAGE_SPECS.find((candidate) => candidate.type === descriptor.type)\n const options = descriptor.options ?? {}\n\n return Object.keys(options)\n .filter((key) => !spec || !same(spec.defaults[key], options[key]))\n .map((key) => `${key}: ${literal(options[key])}`)\n}\n\n/**\n * Import lines, grouped by subpath.\n *\n * The root comes first because `Pipeline` lives there and every pipeline needs\n * it; the engine subpaths follow in alphabetical order, which is what biome's\n * import sorting would produce anyway.\n */\nfunction importLines(descriptors: readonly StageDescriptor[], root: string): string[] {\n const bySubpath = new Map<string, Set<string>>()\n bySubpath.set(root, new Set(['Pipeline']))\n\n for (const descriptor of descriptors) {\n const spec = STAGE_SPECS.find((candidate) => candidate.type === descriptor.type)\n if (!spec) continue\n const names = bySubpath.get(spec.subpath) ?? new Set<string>()\n names.add(descriptor.type)\n bySubpath.set(spec.subpath, names)\n }\n\n return [...bySubpath.entries()]\n .sort(([a], [b]) => (a === root ? -1 : b === root ? 1 : a.localeCompare(b)))\n .map(([subpath, names]) => `import { ${[...names].sort().join(', ')} } from '${subpath}'`)\n}\n\nexport function pipelineCode(\n descriptors: readonly StageDescriptor[],\n options: PipelineCodeOptions = {}\n): string {\n const variable = options.variable ?? 'pipeline'\n const pad = ' '.repeat(options.indent ?? 4)\n const root = STAGE_SPECS.find((spec) => spec.subpath.endsWith('/scaledp'))?.subpath ?? '@stabrise/scaledp'\n\n const stages = descriptors.map((descriptor) => {\n const entries = changedOptions(descriptor)\n if (entries.length === 0) return `${pad}new ${descriptor.type}(),`\n\n const inline = `${pad}new ${descriptor.type}({ ${entries.join(', ')} }),`\n if (inline.length <= 100) return inline\n\n // One option per line once the call outgrows a sensible line length,\n // which is what a formatter would do to it anyway.\n return [\n `${pad}new ${descriptor.type}({`,\n ...entries.map((entry) => `${pad}${pad}${entry},`),\n `${pad}}),`,\n ].join('\\n')\n })\n\n const body = [`const ${variable} = new Pipeline([`, ...stages, '])']\n if (options.imports === false) return body.join('\\n')\n\n return [...importLines(descriptors, root), '', ...body].join('\\n')\n}\n","/**\n * @stabrise/scaledp/registry -- describe, build and serialise pipelines as data.\n *\n * A pipeline is an ordered array of stages, each a class with a params object.\n * That is fine to write by hand, but a UI that assembles one needs two things\n * the classes themselves do not carry at run time: a description of each\n * stage's parameters, and a way back from JSON to live objects.\n *\n * import { STAGE_SPECS, pipelineFromDescriptors } from '@stabrise/scaledp/registry'\n *\n * const stages = [\n * { type: 'PdfToImage', options: { resolution: 200 } },\n * { type: 'PaddleTextRecognizer', options: { keepFormatting: true } },\n * ]\n * const rows = await pipelineFromDescriptors(stages).transform(file)\n *\n * `StageDescriptor[]` is plain JSON, and the same shape the worker protocol\n * already sends across the boundary -- so a saved pipeline, a posted message and\n * a pipeline built here are all the same data.\n *\n * This subpath pulls in every stage class. It pulls in no ML runtime: engines\n * are reached through dynamic `import()` and stay lazy.\n */\n\nimport { Pipeline, type Stage, type StageDescriptor } from '../core/pipeline.js'\nimport { STAGE_CLASSES, STAGE_SPECS } from './catalog.js'\n\n// Re-exported so a consumer building pipelines as data needs only this subpath.\nexport type { StageDescriptor } from '../core/pipeline.js'\nexport type { PipelineCodeOptions } from './codegen.js'\nexport { pipelineCode } from './codegen.js'\nexport type {\n ColumnKind,\n StageCacheSpec,\n StageParamKind,\n StageParamOption,\n StageParamSpec,\n StageSpec,\n} from './types.js'\nexport { STAGE_CLASSES, STAGE_SPECS }\n\n/** Metadata for one stage, by exported class name. */\nexport function getStageSpec(type: string): (typeof STAGE_SPECS)[number] | undefined {\n return STAGE_SPECS.find((spec) => spec.type === type)\n}\n\n/**\n * Build one stage from its serialised form.\n *\n * Throws on an unknown type, and lets the stage's own constructor validators\n * throw on a bad param -- an unknown OCR preset should fail here, where the\n * message can name the offending field, rather than several stages later.\n */\nexport function createStage(descriptor: StageDescriptor): Stage {\n const Ctor = STAGE_CLASSES[descriptor.type]\n if (!Ctor) {\n throw new Error(\n `Unknown stage \"${descriptor.type}\". See STAGE_SPECS for the stages this build knows about.`\n )\n }\n return new Ctor(descriptor.options as never)\n}\n\n/** Build a whole pipeline from its serialised form. */\nexport function pipelineFromDescriptors(descriptors: readonly StageDescriptor[]): Pipeline {\n return new Pipeline(descriptors.map(createStage))\n}\n\n/**\n * The serialised form of a live stage.\n *\n * `stage.params` is fully resolved -- defaults merged in -- so this round-trips\n * exactly but is more verbose than the options originally passed.\n */\nexport function describeStage(stage: Stage): StageDescriptor {\n return { type: stage.name, options: { ...stage.params } }\n}\n\n/** The serialised form of a live pipeline. */\nexport function describePipeline(pipeline: Pipeline): StageDescriptor[] {\n return pipeline.stages.map(describeStage)\n}\n"],"mappings":"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"}
@@ -0,0 +1,109 @@
1
+ //#region src/core/text.ts
2
+ /**
3
+ * Arrange items into groups whose successive elements differ by no more than
4
+ * `maxGap`. Unlike Python's `cluster`, this does not sort the caller's array
5
+ * in place.
6
+ */
7
+ function cluster(items, maxGap, key = (x) => x) {
8
+ if (items.length === 0) return [];
9
+ const sorted = [...items].sort((a, b) => key(a) - key(b));
10
+ const groups = [[sorted[0]]];
11
+ for (const item of sorted.slice(1)) {
12
+ const group = groups[groups.length - 1];
13
+ const previous = group[group.length - 1];
14
+ if (Math.abs(key(item) - key(previous)) <= maxGap) group.push(item);
15
+ else groups.push([item]);
16
+ }
17
+ return groups;
18
+ }
19
+ /**
20
+ * A robust central value: drop the first and last quartile, then take the mode.
21
+ *
22
+ * Trimming the tails is what makes this survive OCR outliers -- a single giant
23
+ * heading would otherwise skew a plain mean. Always returns a truncated
24
+ * integer, matching Python's `int(...)` wrapper.
25
+ */
26
+ function getSize(items, key = (x) => x) {
27
+ if (items.length === 0) return 0;
28
+ const values = items.map(key);
29
+ if (values.length === 1) return values[0];
30
+ values.sort((a, b) => a - b);
31
+ if (values.length <= 4) return Math.trunc(values.reduce((sum, v) => sum + v, 0) / values.length);
32
+ const trim = Math.trunc(values.length / 4);
33
+ const middle = values.slice(trim, values.length - trim);
34
+ const counts = /* @__PURE__ */ new Map();
35
+ for (const value of middle) counts.set(value, (counts.get(value) ?? 0) + 1);
36
+ let best = middle[0];
37
+ let bestCount = 0;
38
+ for (const [value, count] of counts) if (count > bestCount) {
39
+ best = value;
40
+ bestCount = count;
41
+ }
42
+ return Math.trunc(best);
43
+ }
44
+ /**
45
+ * Median-ish width of a single character, used to size inter-word gaps.
46
+ * Takes grouped lines, matching Python's signature.
47
+ */
48
+ function getCharacterWidth(lines) {
49
+ const widths = [];
50
+ for (const line of lines) for (const box of line) if (box.text.length > 0) widths.push(Math.trunc(box.width / box.text.length));
51
+ return getSize(widths);
52
+ }
53
+ /**
54
+ * Group boxes into text lines, each sorted left to right.
55
+ *
56
+ * `lineTolerance` of 0 means "derive it": a third of the typical character
57
+ * height -- tight enough to keep adjacent lines apart, loose enough to tolerate
58
+ * baseline jitter.
59
+ */
60
+ function groupBoxesIntoLines(boxes, lineTolerance = 0) {
61
+ if (boxes.length === 0) return [];
62
+ const characterHeight = getSize(boxes, (b) => b.height);
63
+ return cluster(boxes, lineTolerance === 0 ? characterHeight / 3 : lineTolerance, (b) => Math.trunc(b.y)).map((line) => [...line].sort((a, b) => Math.trunc(a.x) - Math.trunc(b.x)));
64
+ }
65
+ /**
66
+ * Render grouped lines back to text, preserving horizontal spacing and blank
67
+ * lines.
68
+ *
69
+ * Each line is indented from x = 0, so the left margin is preserved rather than
70
+ * every line being flushed left. Vertical gaps wider than two character heights
71
+ * become blank lines. The running `y` starts at 0, which means the first line is
72
+ * measured against the top of the page -- that is Python's behaviour and it is
73
+ * what makes a top margin survive.
74
+ */
75
+ function linesToFormattedText(lines, characterHeight) {
76
+ const output = [];
77
+ const spaceWidth = getCharacterWidth(lines) || 1;
78
+ let y = 0;
79
+ for (const line of lines) {
80
+ const first = line[0];
81
+ if (!first) continue;
82
+ const lineDiffs = characterHeight > 0 ? Math.trunc((first.y - y) / (characterHeight * 2)) : 0;
83
+ y = first.y;
84
+ for (let i = 0; i < lineDiffs - 1; i++) output.push("");
85
+ let text = "";
86
+ let previousRight = 0;
87
+ for (const box of line) {
88
+ const spaces = Math.max(Math.trunc((box.x - previousRight) / spaceWidth), 1);
89
+ text += " ".repeat(spaces) + box.text;
90
+ previousRight = box.x + box.width;
91
+ }
92
+ output.push(text);
93
+ }
94
+ return output.join("\n");
95
+ }
96
+ /** Rebuild layout-preserving text straight from a flat box list. */
97
+ function boxesToFormattedText(boxes, lineTolerance = 0) {
98
+ if (boxes.length === 0) return "";
99
+ const characterHeight = getSize(boxes, (b) => b.height);
100
+ return linesToFormattedText(groupBoxesIntoLines(boxes, lineTolerance), characterHeight);
101
+ }
102
+ /** Plain reading-order join, the default when `keepFormatting` is off. */
103
+ function boxesToText(boxes, separator = " ") {
104
+ return boxes.map((b) => b.text).join(separator);
105
+ }
106
+ //#endregion
107
+ export { getSize as a, getCharacterWidth as i, boxesToText as n, groupBoxesIntoLines as o, cluster as r, linesToFormattedText as s, boxesToFormattedText as t };
108
+
109
+ //# sourceMappingURL=text-ahMLpxN9.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"text-ahMLpxN9.js","names":[],"sources":["../src/core/text.ts"],"sourcesContent":["/**\n * Layout-preserving text reconstruction from OCR boxes.\n *\n * Port of `BaseOcr.box_to_formatted_text` / `to_formatted_text` /\n * `get_character_width` plus `cluster` and `get_size` from\n * `scaledp/utils/__init__.py`. Pure geometry -- no engine involved.\n *\n * Python truncates with `int()` throughout rather than rounding, and the\n * results feed NER character offsets, so every `Math.trunc` below is\n * deliberate. Verified against the Python implementation in\n * test/unit/text-parity.test.ts.\n */\n\nimport type { Box } from '../schemas/box.js'\n\n/**\n * Arrange items into groups whose successive elements differ by no more than\n * `maxGap`. Unlike Python's `cluster`, this does not sort the caller's array\n * in place.\n */\nexport function cluster<T>(\n items: readonly T[],\n maxGap: number,\n key: (item: T) => number = (x) => x as unknown as number\n): T[][] {\n if (items.length === 0) return []\n const sorted = [...items].sort((a, b) => key(a) - key(b))\n\n const groups: T[][] = [[sorted[0] as T]]\n for (const item of sorted.slice(1)) {\n const group = groups[groups.length - 1] as T[]\n const previous = group[group.length - 1] as T\n if (Math.abs(key(item) - key(previous)) <= maxGap) group.push(item)\n else groups.push([item])\n }\n return groups\n}\n\n/**\n * A robust central value: drop the first and last quartile, then take the mode.\n *\n * Trimming the tails is what makes this survive OCR outliers -- a single giant\n * heading would otherwise skew a plain mean. Always returns a truncated\n * integer, matching Python's `int(...)` wrapper.\n */\nexport function getSize<T>(\n items: readonly T[],\n key: (item: T) => number = (x) => x as unknown as number\n): number {\n if (items.length === 0) return 0\n\n const values = items.map(key)\n if (values.length === 1) return values[0] as number\n\n values.sort((a, b) => a - b)\n if (values.length <= 4) {\n return Math.trunc(values.reduce((sum, v) => sum + v, 0) / values.length)\n }\n\n const trim = Math.trunc(values.length / 4)\n const middle = values.slice(trim, values.length - trim)\n\n // statistics.mode returns the first most-common value, so `>` (not `>=`)\n // keeps the earliest winner on ties.\n const counts = new Map<number, number>()\n for (const value of middle) counts.set(value, (counts.get(value) ?? 0) + 1)\n let best = middle[0] as number\n let bestCount = 0\n for (const [value, count] of counts) {\n if (count > bestCount) {\n best = value\n bestCount = count\n }\n }\n return Math.trunc(best)\n}\n\n/**\n * Median-ish width of a single character, used to size inter-word gaps.\n * Takes grouped lines, matching Python's signature.\n */\nexport function getCharacterWidth(lines: readonly (readonly Box[])[]): number {\n const widths: number[] = []\n for (const line of lines) {\n for (const box of line) {\n if (box.text.length > 0) widths.push(Math.trunc(box.width / box.text.length))\n }\n }\n return getSize(widths)\n}\n\n/**\n * Group boxes into text lines, each sorted left to right.\n *\n * `lineTolerance` of 0 means \"derive it\": a third of the typical character\n * height -- tight enough to keep adjacent lines apart, loose enough to tolerate\n * baseline jitter.\n */\nexport function groupBoxesIntoLines(boxes: readonly Box[], lineTolerance = 0): Box[][] {\n if (boxes.length === 0) return []\n const characterHeight = getSize(boxes, (b) => b.height)\n const tolerance = lineTolerance === 0 ? characterHeight / 3 : lineTolerance\n return cluster(boxes, tolerance, (b) => Math.trunc(b.y)).map((line) =>\n [...line].sort((a, b) => Math.trunc(a.x) - Math.trunc(b.x))\n )\n}\n\n/**\n * Render grouped lines back to text, preserving horizontal spacing and blank\n * lines.\n *\n * Each line is indented from x = 0, so the left margin is preserved rather than\n * every line being flushed left. Vertical gaps wider than two character heights\n * become blank lines. The running `y` starts at 0, which means the first line is\n * measured against the top of the page -- that is Python's behaviour and it is\n * what makes a top margin survive.\n */\nexport function linesToFormattedText(lines: readonly (readonly Box[])[], characterHeight: number): string {\n const output: string[] = []\n const spaceWidth = getCharacterWidth(lines) || 1\n let y = 0\n\n for (const line of lines) {\n const first = line[0]\n if (!first) continue\n\n const lineDiffs = characterHeight > 0 ? Math.trunc((first.y - y) / (characterHeight * 2)) : 0\n y = first.y\n for (let i = 0; i < lineDiffs - 1; i++) output.push('')\n\n let text = ''\n let previousRight = 0\n for (const box of line) {\n const spaces = Math.max(Math.trunc((box.x - previousRight) / spaceWidth), 1)\n text += ' '.repeat(spaces) + box.text\n previousRight = box.x + box.width\n }\n output.push(text)\n }\n return output.join('\\n')\n}\n\n/** Rebuild layout-preserving text straight from a flat box list. */\nexport function boxesToFormattedText(boxes: readonly Box[], lineTolerance = 0): string {\n if (boxes.length === 0) return ''\n const characterHeight = getSize(boxes, (b) => b.height)\n return linesToFormattedText(groupBoxesIntoLines(boxes, lineTolerance), characterHeight)\n}\n\n/** Plain reading-order join, the default when `keepFormatting` is off. */\nexport function boxesToText(boxes: readonly Box[], separator = ' '): string {\n return boxes.map((b) => b.text).join(separator)\n}\n"],"mappings":";;;;;;AAoBA,SAAgB,QACZ,OACA,QACA,OAA4B,MAAM,GAC7B;CACL,IAAI,MAAM,WAAW,GAAG,OAAO,CAAC;CAChC,MAAM,SAAS,CAAC,GAAG,KAAK,CAAC,CAAC,MAAM,GAAG,MAAM,IAAI,CAAC,IAAI,IAAI,CAAC,CAAC;CAExD,MAAM,SAAgB,CAAC,CAAC,OAAO,EAAO,CAAC;CACvC,KAAK,MAAM,QAAQ,OAAO,MAAM,CAAC,GAAG;EAChC,MAAM,QAAQ,OAAO,OAAO,SAAS;EACrC,MAAM,WAAW,MAAM,MAAM,SAAS;EACtC,IAAI,KAAK,IAAI,IAAI,IAAI,IAAI,IAAI,QAAQ,CAAC,KAAK,QAAQ,MAAM,KAAK,IAAI;OAC7D,OAAO,KAAK,CAAC,IAAI,CAAC;CAC3B;CACA,OAAO;AACX;;;;;;;;AASA,SAAgB,QACZ,OACA,OAA4B,MAAM,GAC5B;CACN,IAAI,MAAM,WAAW,GAAG,OAAO;CAE/B,MAAM,SAAS,MAAM,IAAI,GAAG;CAC5B,IAAI,OAAO,WAAW,GAAG,OAAO,OAAO;CAEvC,OAAO,MAAM,GAAG,MAAM,IAAI,CAAC;CAC3B,IAAI,OAAO,UAAU,GACjB,OAAO,KAAK,MAAM,OAAO,QAAQ,KAAK,MAAM,MAAM,GAAG,CAAC,IAAI,OAAO,MAAM;CAG3E,MAAM,OAAO,KAAK,MAAM,OAAO,SAAS,CAAC;CACzC,MAAM,SAAS,OAAO,MAAM,MAAM,OAAO,SAAS,IAAI;CAItD,MAAM,yBAAS,IAAI,IAAoB;CACvC,KAAK,MAAM,SAAS,QAAQ,OAAO,IAAI,QAAQ,OAAO,IAAI,KAAK,KAAK,KAAK,CAAC;CAC1E,IAAI,OAAO,OAAO;CAClB,IAAI,YAAY;CAChB,KAAK,MAAM,CAAC,OAAO,UAAU,QACzB,IAAI,QAAQ,WAAW;EACnB,OAAO;EACP,YAAY;CAChB;CAEJ,OAAO,KAAK,MAAM,IAAI;AAC1B;;;;;AAMA,SAAgB,kBAAkB,OAA4C;CAC1E,MAAM,SAAmB,CAAC;CAC1B,KAAK,MAAM,QAAQ,OACf,KAAK,MAAM,OAAO,MACd,IAAI,IAAI,KAAK,SAAS,GAAG,OAAO,KAAK,KAAK,MAAM,IAAI,QAAQ,IAAI,KAAK,MAAM,CAAC;CAGpF,OAAO,QAAQ,MAAM;AACzB;;;;;;;;AASA,SAAgB,oBAAoB,OAAuB,gBAAgB,GAAY;CACnF,IAAI,MAAM,WAAW,GAAG,OAAO,CAAC;CAChC,MAAM,kBAAkB,QAAQ,QAAQ,MAAM,EAAE,MAAM;CAEtD,OAAO,QAAQ,OADG,kBAAkB,IAAI,kBAAkB,IAAI,gBAC5B,MAAM,KAAK,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,SAC1D,CAAC,GAAG,IAAI,CAAC,CAAC,MAAM,GAAG,MAAM,KAAK,MAAM,EAAE,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC,CAAC,CAC9D;AACJ;;;;;;;;;;;AAYA,SAAgB,qBAAqB,OAAoC,iBAAiC;CACtG,MAAM,SAAmB,CAAC;CAC1B,MAAM,aAAa,kBAAkB,KAAK,KAAK;CAC/C,IAAI,IAAI;CAER,KAAK,MAAM,QAAQ,OAAO;EACtB,MAAM,QAAQ,KAAK;EACnB,IAAI,CAAC,OAAO;EAEZ,MAAM,YAAY,kBAAkB,IAAI,KAAK,OAAO,MAAM,IAAI,MAAM,kBAAkB,EAAE,IAAI;EAC5F,IAAI,MAAM;EACV,KAAK,IAAI,IAAI,GAAG,IAAI,YAAY,GAAG,KAAK,OAAO,KAAK,EAAE;EAEtD,IAAI,OAAO;EACX,IAAI,gBAAgB;EACpB,KAAK,MAAM,OAAO,MAAM;GACpB,MAAM,SAAS,KAAK,IAAI,KAAK,OAAO,IAAI,IAAI,iBAAiB,UAAU,GAAG,CAAC;GAC3E,QAAQ,IAAI,OAAO,MAAM,IAAI,IAAI;GACjC,gBAAgB,IAAI,IAAI,IAAI;EAChC;EACA,OAAO,KAAK,IAAI;CACpB;CACA,OAAO,OAAO,KAAK,IAAI;AAC3B;;AAGA,SAAgB,qBAAqB,OAAuB,gBAAgB,GAAW;CACnF,IAAI,MAAM,WAAW,GAAG,OAAO;CAC/B,MAAM,kBAAkB,QAAQ,QAAQ,MAAM,EAAE,MAAM;CACtD,OAAO,qBAAqB,oBAAoB,OAAO,aAAa,GAAG,eAAe;AAC1F;;AAGA,SAAgB,YAAY,OAAuB,YAAY,KAAa;CACxE,OAAO,MAAM,KAAK,MAAM,EAAE,IAAI,CAAC,CAAC,KAAK,SAAS;AAClD"}
@@ -0,0 +1,105 @@
1
+ import { d as StageDescriptor, l as Stage, s as Row } from "../pipeline-DeLO-OCE.js";
2
+ import { n as ScaleDpConfig, t as ModelProgress } from "../config-g6IrKlDC.js";
3
+ //#region src/worker/protocol.d.ts
4
+ type WorkerRequest = {
5
+ type: 'configure';
6
+ requestId: number;
7
+ config: Partial<ScaleDpConfig>;
8
+ } | {
9
+ type: 'transform';
10
+ requestId: number;
11
+ stages: StageDescriptor[];
12
+ rows: Row[];
13
+ } | {
14
+ type: 'dispose';
15
+ requestId: number;
16
+ };
17
+ type WorkerResponse = {
18
+ type: 'progress';
19
+ requestId: number;
20
+ progress: ModelProgress;
21
+ } | {
22
+ type: 'stage';
23
+ requestId: number;
24
+ name: string;
25
+ ms: number;
26
+ rows: number;
27
+ } | {
28
+ type: 'result';
29
+ requestId: number;
30
+ rows: Row[];
31
+ } | {
32
+ type: 'configured';
33
+ requestId: number;
34
+ } | {
35
+ type: 'disposed';
36
+ requestId: number;
37
+ } | {
38
+ type: 'error';
39
+ requestId: number;
40
+ message: string;
41
+ };
42
+ /**
43
+ * Config minus its function-valued fields.
44
+ *
45
+ * `auth` and `onProgress` are callbacks and cannot cross a postMessage
46
+ * boundary; the client re-implements both on the worker side.
47
+ */
48
+ type TransferableConfig = Omit<Partial<ScaleDpConfig>, 'auth' | 'onProgress'>;
49
+ //#endregion
50
+ //#region src/worker/client.d.ts
51
+ interface ScaleDpWorkerOptions {
52
+ /**
53
+ * The worker running `startScaleDpWorker()`. Constructed by the caller,
54
+ * because only the consuming app's bundler can resolve a worker entry URL:
55
+ *
56
+ * new Worker(new URL('./my-worker.ts', import.meta.url), { type: 'module' })
57
+ */
58
+ worker: Worker;
59
+ onProgress?: (progress: ModelProgress) => void;
60
+ onStage?: (name: string, ms: number, rows: number) => void;
61
+ }
62
+ declare class ScaleDpWorkerClient {
63
+ private readonly options;
64
+ private nextRequestId;
65
+ private readonly pending;
66
+ private queue;
67
+ /**
68
+ * Set once the worker itself fails. Requests are posted from inside the
69
+ * queue, so a request enqueued before the failure would otherwise sit
70
+ * unposted and never settle.
71
+ */
72
+ private failure;
73
+ constructor(options: ScaleDpWorkerOptions);
74
+ private send;
75
+ /**
76
+ * Push configuration into the worker.
77
+ *
78
+ * `auth` and `onProgress` are dropped: functions cannot be cloned across a
79
+ * postMessage boundary. Progress is forwarded back through `onProgress` on
80
+ * the client instead.
81
+ */
82
+ configure(config: TransferableConfig): Promise<void>;
83
+ /** Run a pipeline in the worker. Stages are named, not passed by reference. */
84
+ transform(stages: StageDescriptor[], rows: Row[]): Promise<Row[]>;
85
+ dispose(): Promise<void>;
86
+ }
87
+ /** Convenience wrapper around the client constructor. */
88
+ declare function createScaleDpWorker(options: ScaleDpWorkerOptions): ScaleDpWorkerClient;
89
+ //#endregion
90
+ //#region src/worker/host.d.ts
91
+ /** Builds a stage from its descriptor. Consumers register the stages they use. */
92
+ type StageFactory = (descriptor: StageDescriptor) => Stage | undefined;
93
+ /**
94
+ * Register stage constructors the worker may build.
95
+ *
96
+ * Explicit registration keeps the worker bundle to the engines actually used --
97
+ * importing every stage would pull pdf.js, ORT, PaddleOCR and Tesseract into
98
+ * every worker regardless.
99
+ */
100
+ declare function registerStages(stages: Record<string, new (options?: never) => Stage>): void;
101
+ /** Start listening for pipeline requests. Call once, from a worker entry. */
102
+ declare function startScaleDpWorker(scope?: DedicatedWorkerGlobalScope): void;
103
+ //#endregion
104
+ export { ScaleDpWorkerClient, type ScaleDpWorkerOptions, type StageDescriptor, type StageFactory, type TransferableConfig, type WorkerRequest, type WorkerResponse, createScaleDpWorker, registerStages, startScaleDpWorker };
105
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,180 @@
1
+ import { f as formatException, n as Pipeline, p as configure } from "../pipeline-DACqGkpN.js";
2
+ //#region src/worker/client.ts
3
+ var ScaleDpWorkerClient = class {
4
+ options;
5
+ nextRequestId = 1;
6
+ pending = /* @__PURE__ */ new Map();
7
+ queue = Promise.resolve();
8
+ /**
9
+ * Set once the worker itself fails. Requests are posted from inside the
10
+ * queue, so a request enqueued before the failure would otherwise sit
11
+ * unposted and never settle.
12
+ */
13
+ failure = null;
14
+ constructor(options) {
15
+ this.options = options;
16
+ options.worker.onmessage = (event) => {
17
+ const message = event.data;
18
+ if (message.type === "progress") {
19
+ options.onProgress?.(message.progress);
20
+ return;
21
+ }
22
+ if (message.type === "stage") {
23
+ options.onStage?.(message.name, message.ms, message.rows);
24
+ return;
25
+ }
26
+ const entry = this.pending.get(message.requestId);
27
+ if (!entry) return;
28
+ this.pending.delete(message.requestId);
29
+ if (message.type === "error") entry.reject(new Error(message.message));
30
+ else if (message.type === "result") entry.resolve(message.rows);
31
+ else entry.resolve(void 0);
32
+ };
33
+ options.worker.onerror = (event) => {
34
+ this.failure = /* @__PURE__ */ new Error(`Worker failed: ${event.message}`);
35
+ for (const [, entry] of this.pending) entry.reject(this.failure);
36
+ this.pending.clear();
37
+ };
38
+ }
39
+ send(request) {
40
+ const run = async () => {
41
+ if (this.failure) throw this.failure;
42
+ const requestId = this.nextRequestId++;
43
+ return new Promise((resolve, reject) => {
44
+ this.pending.set(requestId, {
45
+ resolve,
46
+ reject
47
+ });
48
+ this.options.worker.postMessage({
49
+ ...request,
50
+ requestId
51
+ });
52
+ });
53
+ };
54
+ const result = this.queue.then(run, run);
55
+ this.queue = result.catch(() => void 0);
56
+ return result;
57
+ }
58
+ /**
59
+ * Push configuration into the worker.
60
+ *
61
+ * `auth` and `onProgress` are dropped: functions cannot be cloned across a
62
+ * postMessage boundary. Progress is forwarded back through `onProgress` on
63
+ * the client instead.
64
+ */
65
+ configure(config) {
66
+ return this.send({
67
+ type: "configure",
68
+ config
69
+ });
70
+ }
71
+ /** Run a pipeline in the worker. Stages are named, not passed by reference. */
72
+ transform(stages, rows) {
73
+ return this.send({
74
+ type: "transform",
75
+ stages,
76
+ rows
77
+ });
78
+ }
79
+ async dispose() {
80
+ await this.send({ type: "dispose" }).catch(() => void 0);
81
+ this.options.worker.terminate();
82
+ }
83
+ };
84
+ /** Convenience wrapper around the client constructor. */
85
+ function createScaleDpWorker(options) {
86
+ return new ScaleDpWorkerClient(options);
87
+ }
88
+ //#endregion
89
+ //#region src/worker/host.ts
90
+ /**
91
+ * Worker-side pipeline host.
92
+ *
93
+ * Import this from your own worker entry point:
94
+ *
95
+ * import { startScaleDpWorker } from '@stabrise/scaledp/worker'
96
+ * startScaleDpWorker()
97
+ *
98
+ * A worker entry has to be a real module in the consuming app so the bundler
99
+ * can resolve it; a library cannot ship a URL that its consumer's bundler will
100
+ * honour.
101
+ */
102
+ const registry = /* @__PURE__ */ new Map();
103
+ /**
104
+ * Register stage constructors the worker may build.
105
+ *
106
+ * Explicit registration keeps the worker bundle to the engines actually used --
107
+ * importing every stage would pull pdf.js, ORT, PaddleOCR and Tesseract into
108
+ * every worker regardless.
109
+ */
110
+ function registerStages(stages) {
111
+ for (const [name, Ctor] of Object.entries(stages)) registry.set(name, (options) => new Ctor(options));
112
+ }
113
+ function buildStage(descriptor) {
114
+ const factory = registry.get(descriptor.type);
115
+ if (!factory) throw new Error(`Stage "${descriptor.type}" is not registered in this worker. Call registerStages({ ${descriptor.type} }) in the worker entry.`);
116
+ return factory(descriptor.options);
117
+ }
118
+ /** Start listening for pipeline requests. Call once, from a worker entry. */
119
+ function startScaleDpWorker(scope = self) {
120
+ const post = (message) => scope.postMessage(message);
121
+ let queue = Promise.resolve();
122
+ let active = null;
123
+ scope.onmessage = (event) => {
124
+ const message = event.data;
125
+ queue = queue.then(async () => {
126
+ try {
127
+ switch (message.type) {
128
+ case "configure":
129
+ configure({
130
+ ...message.config,
131
+ onProgress: (progress) => post({
132
+ type: "progress",
133
+ requestId: message.requestId,
134
+ progress
135
+ })
136
+ });
137
+ post({
138
+ type: "configured",
139
+ requestId: message.requestId
140
+ });
141
+ break;
142
+ case "transform": {
143
+ await active?.dispose();
144
+ active = new Pipeline(message.stages.map(buildStage));
145
+ const rows = await active.transform(message.rows, { onStage: (name, ms, count) => post({
146
+ type: "stage",
147
+ requestId: message.requestId,
148
+ name,
149
+ ms,
150
+ rows: count
151
+ }) });
152
+ post({
153
+ type: "result",
154
+ requestId: message.requestId,
155
+ rows
156
+ });
157
+ break;
158
+ }
159
+ case "dispose":
160
+ await active?.dispose();
161
+ active = null;
162
+ post({
163
+ type: "disposed",
164
+ requestId: message.requestId
165
+ });
166
+ }
167
+ } catch (error) {
168
+ post({
169
+ type: "error",
170
+ requestId: message.requestId,
171
+ message: formatException("worker", error)
172
+ });
173
+ }
174
+ });
175
+ };
176
+ }
177
+ //#endregion
178
+ export { ScaleDpWorkerClient, createScaleDpWorker, registerStages, startScaleDpWorker };
179
+
180
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/worker/client.ts","../../src/worker/host.ts"],"sourcesContent":["/**\n * Main-thread client for a pipeline worker.\n *\n * Presents the same `transform` shape as `Pipeline` while the work happens off\n * the main thread. Everything below the stages is already OffscreenCanvas-based\n * and DOM-free, so OCR, detection and NER all move across -- not just NER, as\n * in the pdftools prototype.\n */\n\nimport type { ModelProgress, ScaleDpConfig } from '../core/config.js'\nimport type { Row } from '../core/pipeline.js'\nimport type {\n StageDescriptor,\n TransferableConfig,\n WorkerRequest,\n WorkerRequestInit,\n WorkerResponse,\n} from './protocol.js'\n\nexport interface ScaleDpWorkerOptions {\n /**\n * The worker running `startScaleDpWorker()`. Constructed by the caller,\n * because only the consuming app's bundler can resolve a worker entry URL:\n *\n * new Worker(new URL('./my-worker.ts', import.meta.url), { type: 'module' })\n */\n worker: Worker\n onProgress?: (progress: ModelProgress) => void\n onStage?: (name: string, ms: number, rows: number) => void\n}\n\nexport class ScaleDpWorkerClient {\n private nextRequestId = 1\n private readonly pending = new Map<\n number,\n { resolve: (value: never) => void; reject: (reason: Error) => void }\n >()\n\n // ORT runs one inference at a time, so requests are serialised here as well\n // as in the host -- this keeps the *queueing* observable to callers.\n private queue: Promise<unknown> = Promise.resolve()\n\n /**\n * Set once the worker itself fails. Requests are posted from inside the\n * queue, so a request enqueued before the failure would otherwise sit\n * unposted and never settle.\n */\n private failure: Error | null = null\n\n constructor(private readonly options: ScaleDpWorkerOptions) {\n options.worker.onmessage = (event: MessageEvent<WorkerResponse>) => {\n const message = event.data\n if (message.type === 'progress') {\n options.onProgress?.(message.progress)\n return\n }\n if (message.type === 'stage') {\n options.onStage?.(message.name, message.ms, message.rows)\n return\n }\n\n const entry = this.pending.get(message.requestId)\n if (!entry) return\n this.pending.delete(message.requestId)\n\n if (message.type === 'error') entry.reject(new Error(message.message))\n else if (message.type === 'result') entry.resolve(message.rows as never)\n else entry.resolve(undefined as never)\n }\n\n options.worker.onerror = (event) => {\n this.failure = new Error(`Worker failed: ${event.message}`)\n for (const [, entry] of this.pending) entry.reject(this.failure)\n this.pending.clear()\n }\n }\n\n private send<T>(request: WorkerRequestInit): Promise<T> {\n const run = async (): Promise<T> => {\n if (this.failure) throw this.failure\n const requestId = this.nextRequestId++\n return new Promise<T>((resolve, reject) => {\n this.pending.set(requestId, {\n resolve: resolve as (value: never) => void,\n reject,\n })\n this.options.worker.postMessage({ ...request, requestId } as WorkerRequest)\n })\n }\n\n // Chain onto the queue but never let a rejection break it, otherwise one\n // failed request poisons every request that follows.\n const result = this.queue.then(run, run)\n this.queue = result.catch(() => undefined)\n return result\n }\n\n /**\n * Push configuration into the worker.\n *\n * `auth` and `onProgress` are dropped: functions cannot be cloned across a\n * postMessage boundary. Progress is forwarded back through `onProgress` on\n * the client instead.\n */\n configure(config: TransferableConfig): Promise<void> {\n return this.send({ type: 'configure', config: config as Partial<ScaleDpConfig> })\n }\n\n /** Run a pipeline in the worker. Stages are named, not passed by reference. */\n transform(stages: StageDescriptor[], rows: Row[]): Promise<Row[]> {\n return this.send<Row[]>({ type: 'transform', stages, rows })\n }\n\n async dispose(): Promise<void> {\n await this.send({ type: 'dispose' }).catch(() => undefined)\n this.options.worker.terminate()\n }\n}\n\n/** Convenience wrapper around the client constructor. */\nexport function createScaleDpWorker(options: ScaleDpWorkerOptions): ScaleDpWorkerClient {\n return new ScaleDpWorkerClient(options)\n}\n","/**\n * Worker-side pipeline host.\n *\n * Import this from your own worker entry point:\n *\n * import { startScaleDpWorker } from '@stabrise/scaledp/worker'\n * startScaleDpWorker()\n *\n * A worker entry has to be a real module in the consuming app so the bundler\n * can resolve it; a library cannot ship a URL that its consumer's bundler will\n * honour.\n */\n\nimport { configure } from '../core/config.js'\nimport { formatException } from '../core/errors.js'\nimport { Pipeline, type Row, type Stage } from '../core/pipeline.js'\nimport type { StageDescriptor, WorkerRequest, WorkerResponse } from './protocol.js'\n\n/** Builds a stage from its descriptor. Consumers register the stages they use. */\nexport type StageFactory = (descriptor: StageDescriptor) => Stage | undefined\n\nconst registry = new Map<string, (options?: Record<string, unknown>) => Stage>()\n\n/**\n * Register stage constructors the worker may build.\n *\n * Explicit registration keeps the worker bundle to the engines actually used --\n * importing every stage would pull pdf.js, ORT, PaddleOCR and Tesseract into\n * every worker regardless.\n */\nexport function registerStages(stages: Record<string, new (options?: never) => Stage>): void {\n for (const [name, Ctor] of Object.entries(stages)) {\n registry.set(name, (options) => new Ctor(options as never))\n }\n}\n\nfunction buildStage(descriptor: StageDescriptor): Stage {\n const factory = registry.get(descriptor.type)\n if (!factory) {\n throw new Error(\n `Stage \"${descriptor.type}\" is not registered in this worker. ` +\n `Call registerStages({ ${descriptor.type} }) in the worker entry.`\n )\n }\n return factory(descriptor.options)\n}\n\n/** Start listening for pipeline requests. Call once, from a worker entry. */\nexport function startScaleDpWorker(scope: DedicatedWorkerGlobalScope = self as never): void {\n const post = (message: WorkerResponse) => scope.postMessage(message)\n\n // ORT sessions run one inference at a time, so requests are serialised\n // rather than run concurrently. Without this, a second transform started\n // mid-flight fails with \"Session already started\".\n let queue: Promise<unknown> = Promise.resolve()\n let active: Pipeline | null = null\n\n scope.onmessage = (event: MessageEvent<WorkerRequest>) => {\n const message = event.data\n queue = queue.then(async () => {\n try {\n switch (message.type) {\n case 'configure': {\n configure({\n ...message.config,\n onProgress: (progress) =>\n post({ type: 'progress', requestId: message.requestId, progress }),\n })\n post({ type: 'configured', requestId: message.requestId })\n break\n }\n case 'transform': {\n await active?.dispose()\n active = new Pipeline(message.stages.map(buildStage))\n const rows: Row[] = await active.transform(message.rows, {\n onStage: (name, ms, count) =>\n post({\n type: 'stage',\n requestId: message.requestId,\n name,\n ms,\n rows: count,\n }),\n })\n post({ type: 'result', requestId: message.requestId, rows })\n break\n }\n case 'dispose': {\n await active?.dispose()\n active = null\n post({ type: 'disposed', requestId: message.requestId })\n break\n }\n }\n } catch (error) {\n post({\n type: 'error',\n requestId: message.requestId,\n message: formatException('worker', error),\n })\n }\n })\n }\n}\n"],"mappings":"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"}