@camstack/addon-vision 0.1.0 → 0.1.2

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 (131) hide show
  1. package/dist/addons/animal-classifier/index.d.mts +6 -1
  2. package/dist/addons/animal-classifier/index.d.ts +6 -1
  3. package/dist/addons/animal-classifier/index.js +513 -49
  4. package/dist/addons/animal-classifier/index.js.map +1 -1
  5. package/dist/addons/animal-classifier/index.mjs +6 -4
  6. package/dist/addons/audio-classification/index.d.mts +6 -1
  7. package/dist/addons/audio-classification/index.d.ts +6 -1
  8. package/dist/addons/audio-classification/index.js +86 -26
  9. package/dist/addons/audio-classification/index.js.map +1 -1
  10. package/dist/addons/audio-classification/index.mjs +3 -2
  11. package/dist/addons/bird-global-classifier/index.d.mts +6 -1
  12. package/dist/addons/bird-global-classifier/index.d.ts +6 -1
  13. package/dist/addons/bird-global-classifier/index.js +514 -50
  14. package/dist/addons/bird-global-classifier/index.js.map +1 -1
  15. package/dist/addons/bird-global-classifier/index.mjs +6 -4
  16. package/dist/addons/bird-nabirds-classifier/index.d.mts +6 -1
  17. package/dist/addons/bird-nabirds-classifier/index.d.ts +6 -1
  18. package/dist/addons/bird-nabirds-classifier/index.js +523 -60
  19. package/dist/addons/bird-nabirds-classifier/index.js.map +1 -1
  20. package/dist/addons/bird-nabirds-classifier/index.mjs +6 -4
  21. package/dist/addons/face-detection/index.d.mts +6 -1
  22. package/dist/addons/face-detection/index.d.ts +6 -1
  23. package/dist/addons/face-detection/index.js +538 -39
  24. package/dist/addons/face-detection/index.js.map +1 -1
  25. package/dist/addons/face-detection/index.mjs +5 -3
  26. package/dist/addons/face-recognition/index.d.mts +6 -1
  27. package/dist/addons/face-recognition/index.d.ts +6 -1
  28. package/dist/addons/face-recognition/index.js +487 -33
  29. package/dist/addons/face-recognition/index.js.map +1 -1
  30. package/dist/addons/face-recognition/index.mjs +5 -3
  31. package/dist/addons/motion-detection/index.d.mts +3 -1
  32. package/dist/addons/motion-detection/index.d.ts +3 -1
  33. package/dist/addons/motion-detection/index.js +11 -3
  34. package/dist/addons/motion-detection/index.js.map +1 -1
  35. package/dist/addons/motion-detection/index.mjs +140 -3
  36. package/dist/addons/motion-detection/index.mjs.map +1 -1
  37. package/dist/addons/object-detection/index.d.mts +6 -1
  38. package/dist/addons/object-detection/index.d.ts +6 -1
  39. package/dist/addons/object-detection/index.js +369 -72
  40. package/dist/addons/object-detection/index.js.map +1 -1
  41. package/dist/addons/object-detection/index.mjs +5 -3
  42. package/dist/addons/plate-detection/index.d.mts +6 -1
  43. package/dist/addons/plate-detection/index.d.ts +6 -1
  44. package/dist/addons/plate-detection/index.js +531 -31
  45. package/dist/addons/plate-detection/index.js.map +1 -1
  46. package/dist/addons/plate-detection/index.mjs +5 -3
  47. package/dist/addons/plate-recognition/index.d.mts +7 -1
  48. package/dist/addons/plate-recognition/index.d.ts +7 -1
  49. package/dist/addons/plate-recognition/index.js +176 -44
  50. package/dist/addons/plate-recognition/index.js.map +1 -1
  51. package/dist/addons/plate-recognition/index.mjs +4 -3
  52. package/dist/addons/segmentation-refiner/index.d.mts +30 -0
  53. package/dist/addons/segmentation-refiner/index.d.ts +30 -0
  54. package/dist/addons/segmentation-refiner/index.js +1048 -0
  55. package/dist/addons/segmentation-refiner/index.js.map +1 -0
  56. package/dist/addons/segmentation-refiner/index.mjs +209 -0
  57. package/dist/addons/segmentation-refiner/index.mjs.map +1 -0
  58. package/dist/addons/vehicle-classifier/index.d.mts +31 -0
  59. package/dist/addons/vehicle-classifier/index.d.ts +31 -0
  60. package/dist/addons/vehicle-classifier/index.js +688 -0
  61. package/dist/addons/vehicle-classifier/index.js.map +1 -0
  62. package/dist/addons/vehicle-classifier/index.mjs +250 -0
  63. package/dist/addons/vehicle-classifier/index.mjs.map +1 -0
  64. package/dist/{chunk-6OR5TE7A.mjs → chunk-22BHCDT5.mjs} +2 -2
  65. package/dist/chunk-22BHCDT5.mjs.map +1 -0
  66. package/dist/{chunk-LPI42WL6.mjs → chunk-2IOKI4ES.mjs} +23 -12
  67. package/dist/chunk-2IOKI4ES.mjs.map +1 -0
  68. package/dist/chunk-7DYHXUPZ.mjs +36 -0
  69. package/dist/chunk-7DYHXUPZ.mjs.map +1 -0
  70. package/dist/chunk-BJTO5JO5.mjs +11 -0
  71. package/dist/chunk-BP7H4NFS.mjs +412 -0
  72. package/dist/chunk-BP7H4NFS.mjs.map +1 -0
  73. package/dist/chunk-BR2FPGOX.mjs +98 -0
  74. package/dist/chunk-BR2FPGOX.mjs.map +1 -0
  75. package/dist/{chunk-5AIQSN32.mjs → chunk-D6WEHN33.mjs} +66 -17
  76. package/dist/chunk-D6WEHN33.mjs.map +1 -0
  77. package/dist/{chunk-3MQFUDRU.mjs → chunk-DRYFGARD.mjs} +76 -47
  78. package/dist/chunk-DRYFGARD.mjs.map +1 -0
  79. package/dist/{chunk-ISOIDU4U.mjs → chunk-DUN6XU3N.mjs} +23 -5
  80. package/dist/chunk-DUN6XU3N.mjs.map +1 -0
  81. package/dist/{chunk-MEVASN3P.mjs → chunk-ESLHNWWE.mjs} +104 -22
  82. package/dist/chunk-ESLHNWWE.mjs.map +1 -0
  83. package/dist/{chunk-B3R66MPF.mjs → chunk-JUQEW6ON.mjs} +58 -21
  84. package/dist/chunk-JUQEW6ON.mjs.map +1 -0
  85. package/dist/{chunk-AYBFB7ID.mjs → chunk-R5J3WAUI.mjs} +200 -318
  86. package/dist/chunk-R5J3WAUI.mjs.map +1 -0
  87. package/dist/chunk-XZ6ZMXXU.mjs +39 -0
  88. package/dist/chunk-XZ6ZMXXU.mjs.map +1 -0
  89. package/dist/{chunk-5JJZGKL7.mjs → chunk-YPU4WTXZ.mjs} +102 -19
  90. package/dist/chunk-YPU4WTXZ.mjs.map +1 -0
  91. package/dist/{chunk-J4WRYHHY.mjs → chunk-YUCD2TFH.mjs} +66 -36
  92. package/dist/chunk-YUCD2TFH.mjs.map +1 -0
  93. package/dist/{chunk-PDSHDDPV.mjs → chunk-ZTJENCFC.mjs} +159 -35
  94. package/dist/chunk-ZTJENCFC.mjs.map +1 -0
  95. package/dist/{chunk-Q3SQOYG6.mjs → chunk-ZWYXXCXP.mjs} +67 -37
  96. package/dist/chunk-ZWYXXCXP.mjs.map +1 -0
  97. package/dist/index.d.mts +17 -5
  98. package/dist/index.d.ts +17 -5
  99. package/dist/index.js +1343 -550
  100. package/dist/index.js.map +1 -1
  101. package/dist/index.mjs +191 -20
  102. package/dist/index.mjs.map +1 -1
  103. package/package.json +94 -18
  104. package/python/coreml_inference.py +61 -18
  105. package/python/openvino_inference.py +12 -4
  106. package/python/pytorch_inference.py +12 -4
  107. package/dist/addons/camera-native-detection/index.d.mts +0 -32
  108. package/dist/addons/camera-native-detection/index.d.ts +0 -32
  109. package/dist/addons/camera-native-detection/index.js +0 -99
  110. package/dist/addons/camera-native-detection/index.js.map +0 -1
  111. package/dist/addons/camera-native-detection/index.mjs +0 -7
  112. package/dist/chunk-3MQFUDRU.mjs.map +0 -1
  113. package/dist/chunk-5AIQSN32.mjs.map +0 -1
  114. package/dist/chunk-5JJZGKL7.mjs.map +0 -1
  115. package/dist/chunk-6OR5TE7A.mjs.map +0 -1
  116. package/dist/chunk-AYBFB7ID.mjs.map +0 -1
  117. package/dist/chunk-B3R66MPF.mjs.map +0 -1
  118. package/dist/chunk-DTOAB2CE.mjs +0 -79
  119. package/dist/chunk-DTOAB2CE.mjs.map +0 -1
  120. package/dist/chunk-ISOIDU4U.mjs.map +0 -1
  121. package/dist/chunk-J4WRYHHY.mjs.map +0 -1
  122. package/dist/chunk-LPI42WL6.mjs.map +0 -1
  123. package/dist/chunk-MEVASN3P.mjs.map +0 -1
  124. package/dist/chunk-PDSHDDPV.mjs.map +0 -1
  125. package/dist/chunk-Q3SQOYG6.mjs.map +0 -1
  126. package/dist/chunk-QIMDG34B.mjs +0 -229
  127. package/dist/chunk-QIMDG34B.mjs.map +0 -1
  128. package/python/__pycache__/coreml_inference.cpython-313.pyc +0 -0
  129. package/python/__pycache__/openvino_inference.cpython-313.pyc +0 -0
  130. package/python/__pycache__/pytorch_inference.cpython-313.pyc +0 -0
  131. /package/dist/{addons/camera-native-detection/index.mjs.map → chunk-BJTO5JO5.mjs.map} +0 -0
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/shared/node-engine.ts","../src/shared/python-engine.ts","../src/shared/engine-resolver.ts"],"sourcesContent":["import type { IInferenceEngine, DetectionRuntime, DetectionDevice } from '@camstack/types'\nimport * as path from 'node:path'\n\nconst BACKEND_TO_PROVIDER: Readonly<Record<string, string>> = {\n cpu: 'cpu',\n coreml: 'coreml',\n cuda: 'cuda',\n tensorrt: 'tensorrt',\n dml: 'dml',\n} as const\n\nconst BACKEND_TO_DEVICE: Readonly<Record<string, DetectionDevice>> = {\n cpu: 'cpu',\n coreml: 'gpu-mps',\n cuda: 'gpu-cuda',\n tensorrt: 'tensorrt',\n} as const\n\nexport class NodeInferenceEngine implements IInferenceEngine {\n readonly runtime: DetectionRuntime = 'onnx'\n readonly device: DetectionDevice\n private session: unknown = null\n\n constructor(\n private readonly modelPath: string,\n private readonly backend: string,\n ) {\n this.device = (BACKEND_TO_DEVICE[backend] ?? 'cpu') as DetectionDevice\n }\n\n async initialize(): Promise<void> {\n const ort = await import('onnxruntime-node')\n const provider = BACKEND_TO_PROVIDER[this.backend] ?? 'cpu'\n\n // Resolve absolute path\n const absModelPath = path.isAbsolute(this.modelPath)\n ? this.modelPath\n : path.resolve(process.cwd(), this.modelPath)\n\n const sessionOptions: Record<string, unknown> = {\n executionProviders: [provider],\n }\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n this.session = await (ort as any).InferenceSession.create(absModelPath, sessionOptions)\n }\n\n async run(input: Float32Array, inputShape: readonly number[]): Promise<Float32Array> {\n if (!this.session) {\n throw new Error('NodeInferenceEngine: not initialized — call initialize() first')\n }\n\n const ort = await import('onnxruntime-node')\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const sess = this.session as any\n\n // Get the first input name\n const inputName: string = sess.inputNames[0]\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const tensor = new (ort as any).Tensor('float32', input, [...inputShape])\n const feeds: Record<string, unknown> = { [inputName]: tensor }\n\n const results = await sess.run(feeds)\n const outputName: string = sess.outputNames[0]\n const outputTensor = results[outputName]\n\n return outputTensor.data as Float32Array\n }\n\n async runMultiOutput(\n input: Float32Array,\n inputShape: readonly number[],\n ): Promise<Record<string, Float32Array>> {\n if (!this.session) {\n throw new Error('NodeInferenceEngine: not initialized — call initialize() first')\n }\n\n const ort = await import('onnxruntime-node')\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const sess = this.session as any\n\n const inputName: string = sess.inputNames[0]\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const tensor = new (ort as any).Tensor('float32', input, [...inputShape])\n const feeds: Record<string, unknown> = { [inputName]: tensor }\n\n const results = await sess.run(feeds)\n\n const out: Record<string, Float32Array> = {}\n for (const name of sess.outputNames as string[]) {\n out[name] = results[name].data as Float32Array\n }\n return out\n }\n\n async dispose(): Promise<void> {\n // onnxruntime-node sessions don't have explicit close in all versions\n // but we clear the reference\n this.session = null\n }\n}\n","import type { IInferenceEngine, DetectionRuntime, DetectionDevice } from '@camstack/types'\nimport { spawn, type ChildProcess } from 'node:child_process'\n\nexport class PythonInferenceEngine implements IInferenceEngine {\n readonly runtime: DetectionRuntime\n readonly device: DetectionDevice\n private process: ChildProcess | null = null\n private receiveBuffer: Buffer = Buffer.alloc(0)\n private pendingResolve: ((value: Record<string, unknown>) => void) | null = null\n private pendingReject: ((reason: Error) => void) | null = null\n\n constructor(\n private readonly pythonPath: string,\n private readonly scriptPath: string,\n runtime: DetectionRuntime,\n private readonly modelPath: string,\n private readonly extraArgs: readonly string[] = [],\n ) {\n this.runtime = runtime\n // Determine device from runtime\n const runtimeDeviceMap: Readonly<Record<DetectionRuntime, DetectionDevice>> = {\n onnx: 'cpu',\n coreml: 'gpu-mps',\n pytorch: 'cpu',\n openvino: 'cpu',\n tflite: 'cpu',\n }\n this.device = runtimeDeviceMap[runtime]\n }\n\n async initialize(): Promise<void> {\n const args = [this.scriptPath, this.modelPath, ...this.extraArgs]\n this.process = spawn(this.pythonPath, args, {\n stdio: ['pipe', 'pipe', 'pipe'],\n })\n\n if (!this.process.stdout || !this.process.stdin) {\n throw new Error('PythonInferenceEngine: failed to create process pipes')\n }\n\n this.process.stderr?.on('data', (chunk: Buffer) => {\n // Log stderr from python process for debugging\n process.stderr.write(`[python-engine] ${chunk.toString()}`)\n })\n\n this.process.on('error', (err) => {\n this.pendingReject?.(err)\n this.pendingReject = null\n this.pendingResolve = null\n })\n\n this.process.on('exit', (code) => {\n if (code !== 0) {\n const err = new Error(`PythonInferenceEngine: process exited with code ${code}`)\n this.pendingReject?.(err)\n this.pendingReject = null\n this.pendingResolve = null\n }\n })\n\n this.process.stdout.on('data', (chunk: Buffer) => {\n this.receiveBuffer = Buffer.concat([this.receiveBuffer, chunk])\n this._tryReceive()\n })\n\n // Give the process a moment to start up and load the model\n await new Promise<void>((resolve, reject) => {\n const timeout = setTimeout(() => resolve(), 2000)\n this.process?.on('error', (err) => {\n clearTimeout(timeout)\n reject(err)\n })\n this.process?.on('exit', (code) => {\n clearTimeout(timeout)\n if (code !== 0) {\n reject(new Error(`PythonInferenceEngine: process exited early with code ${code}`))\n }\n })\n })\n }\n\n private _tryReceive(): void {\n // Binary IPC: [4 bytes LE uint32 length][JSON bytes]\n if (this.receiveBuffer.length < 4) return\n\n const length = this.receiveBuffer.readUInt32LE(0)\n if (this.receiveBuffer.length < 4 + length) return\n\n const jsonBytes = this.receiveBuffer.subarray(4, 4 + length)\n this.receiveBuffer = this.receiveBuffer.subarray(4 + length)\n\n const resolve = this.pendingResolve\n const reject = this.pendingReject\n this.pendingResolve = null\n this.pendingReject = null\n\n if (!resolve) return\n\n try {\n const parsed = JSON.parse(jsonBytes.toString('utf8')) as Record<string, unknown>\n resolve(parsed)\n } catch (err) {\n reject?.(err instanceof Error ? err : new Error(String(err)))\n }\n }\n\n /** Send JPEG buffer, receive JSON detection results */\n async runJpeg(jpeg: Buffer): Promise<Record<string, unknown>> {\n if (!this.process?.stdin) {\n throw new Error('PythonInferenceEngine: process not initialized')\n }\n\n return new Promise<Record<string, unknown>>((resolve, reject) => {\n this.pendingResolve = resolve\n this.pendingReject = reject\n\n // Binary IPC: [4 bytes LE uint32 length][JPEG bytes]\n const lengthBuf = Buffer.allocUnsafe(4)\n lengthBuf.writeUInt32LE(jpeg.length, 0)\n this.process!.stdin!.write(Buffer.concat([lengthBuf, jpeg]))\n })\n }\n\n /** IInferenceEngine.run — wraps runJpeg for compatibility */\n async run(_input: Float32Array, _inputShape: readonly number[]): Promise<Float32Array> {\n throw new Error(\n 'PythonInferenceEngine: use runJpeg() directly — this engine operates on JPEG input',\n )\n }\n\n /** IInferenceEngine.runMultiOutput — not supported by Python engine (operates on JPEG input) */\n async runMultiOutput(\n _input: Float32Array,\n _inputShape: readonly number[],\n ): Promise<Record<string, Float32Array>> {\n throw new Error(\n 'PythonInferenceEngine: runMultiOutput() is not supported — this engine operates on JPEG input',\n )\n }\n\n async dispose(): Promise<void> {\n if (this.process) {\n this.process.stdin?.end()\n this.process.kill('SIGTERM')\n this.process = null\n }\n }\n}\n","// TODO: Wire PythonInferenceEngine for PyTorch/OpenVINO/TFLite runtimes\n// Currently falls back to ONNX CPU when non-ONNX runtime is requested.\n// See: packages/addon-vision/python/ for stub implementations.\n//\n// WHY THIS FILE USES RAW FILESYSTEM PATHS (modelsDir: string):\n//\n// Model files must be loaded via absolute filesystem paths because inference\n// engines (ONNX Runtime, CoreML, etc.) require direct file access — they do\n// not accept Buffer or stream inputs. This is fundamentally different from\n// user data (recordings, media) where IAddonFileStorage abstraction makes\n// sense. Models are closer to binaries or compiled artifacts that must live\n// on the local filesystem at runtime.\n//\n// IAddonFileStorage.readFile() returns a Buffer, but onnxruntime-node's\n// InferenceSession.create() only accepts a file path string or a Uint8Array\n// loaded into memory. For large models (hundreds of MB), loading into a\n// Uint8Array is impractical. Therefore, modelsDir stays as a raw string path\n// and is intentionally NOT replaced with IAddonFileStorage here.\n\nimport type {\n IInferenceEngine,\n DetectionRuntime,\n ModelCatalogEntry,\n ModelFormat,\n} from '@camstack/types'\nimport * as fs from 'node:fs'\nimport * as path from 'node:path'\nimport { NodeInferenceEngine } from './node-engine.js'\nimport { PythonInferenceEngine } from './python-engine.js'\n\nexport interface EngineResolverOptions {\n readonly runtime: DetectionRuntime | 'auto'\n readonly backend: string\n readonly modelEntry: ModelCatalogEntry\n readonly modelsDir: string\n readonly pythonPath?: string\n readonly downloadModel?: (url: string, destDir: string) => Promise<string>\n}\n\nexport interface ResolvedEngine {\n readonly engine: IInferenceEngine\n readonly format: ModelFormat\n readonly modelPath: string\n}\n\n/** Priority order for auto-selection of ONNX backends */\nconst AUTO_BACKEND_PRIORITY = ['coreml', 'cuda', 'tensorrt', 'cpu'] as const\n\n/** Map backend names to the model format they require */\nconst BACKEND_TO_FORMAT: Readonly<Record<string, ModelFormat>> = {\n cpu: 'onnx',\n coreml: 'coreml',\n cuda: 'onnx',\n tensorrt: 'onnx',\n} as const\n\n/** Map DetectionRuntime to ModelFormat */\nconst RUNTIME_TO_FORMAT: Readonly<Partial<Record<DetectionRuntime, ModelFormat>>> = {\n onnx: 'onnx',\n coreml: 'coreml',\n openvino: 'openvino',\n tflite: 'tflite',\n pytorch: 'pt',\n} as const\n\nfunction modelFilePath(modelsDir: string, modelEntry: ModelCatalogEntry, format: ModelFormat): string {\n const formatEntry = modelEntry.formats[format]\n if (!formatEntry) {\n throw new Error(`Model ${modelEntry.id} has no ${format} format`)\n }\n // Derive filename from URL\n const urlParts = formatEntry.url.split('/')\n const filename = urlParts[urlParts.length - 1] ?? `${modelEntry.id}.${format}`\n return path.join(modelsDir, filename)\n}\n\nfunction modelExists(filePath: string): boolean {\n try {\n return fs.existsSync(filePath)\n } catch {\n return false\n }\n}\n\nexport async function resolveEngine(options: EngineResolverOptions): Promise<ResolvedEngine> {\n const { runtime, backend, modelEntry, modelsDir, downloadModel } = options\n\n let selectedFormat: ModelFormat\n let selectedBackend: string\n\n if (runtime === 'auto') {\n // Probe available ONNX backends and pick best\n const available = await probeOnnxBackends()\n\n // Pick first priority backend that has a corresponding model format available\n let chosen: { backend: string; format: ModelFormat } | null = null\n\n for (const b of AUTO_BACKEND_PRIORITY) {\n if (!available.includes(b)) continue\n const fmt = BACKEND_TO_FORMAT[b]\n if (!fmt) continue\n if (!modelEntry.formats[fmt]) continue\n chosen = { backend: b, format: fmt }\n break\n }\n\n if (!chosen) {\n throw new Error(\n `resolveEngine: no compatible backend found for model ${modelEntry.id}. Available backends: ${available.join(', ')}`,\n )\n }\n\n selectedFormat = chosen.format\n selectedBackend = chosen.backend\n } else {\n // Explicit runtime requested\n const fmt = RUNTIME_TO_FORMAT[runtime]\n if (!fmt) {\n throw new Error(`resolveEngine: unsupported runtime \"${runtime}\"`)\n }\n if (!modelEntry.formats[fmt]) {\n throw new Error(\n `resolveEngine: model ${modelEntry.id} has no ${fmt} format for runtime ${runtime}`,\n )\n }\n selectedFormat = fmt\n // For onnx runtime, use the provided backend; otherwise use the runtime name\n selectedBackend = runtime === 'onnx' ? (backend || 'cpu') : runtime\n }\n\n // Resolve model path\n let modelPath = modelFilePath(modelsDir, modelEntry, selectedFormat)\n\n if (!modelExists(modelPath)) {\n if (downloadModel) {\n const formatEntry = modelEntry.formats[selectedFormat]!\n modelPath = await downloadModel(formatEntry.url, modelsDir)\n } else {\n throw new Error(\n `resolveEngine: model file not found at ${modelPath} and no downloadModel function provided`,\n )\n }\n }\n\n // Only ONNX runtime is handled by NodeInferenceEngine currently\n if (selectedFormat === 'onnx' || selectedFormat === 'coreml') {\n const engine = new NodeInferenceEngine(modelPath, selectedBackend)\n await engine.initialize()\n return { engine, format: selectedFormat, modelPath }\n }\n\n // For non-ONNX/CoreML formats, try PythonInferenceEngine when a python binary is available\n const { pythonPath } = options\n const PYTHON_SCRIPT_MAP: Readonly<Record<string, string>> = {\n coreml: 'coreml_inference.py',\n pytorch: 'pytorch_inference.py',\n openvino: 'openvino_inference.py',\n } as const\n\n const effectiveRuntime = runtime === 'auto' ? selectedBackend : runtime\n const scriptName = PYTHON_SCRIPT_MAP[effectiveRuntime]\n\n if (scriptName && pythonPath) {\n const scriptPath = path.join(__dirname, '../../python', scriptName)\n const inputSize = Math.max(modelEntry.inputSize.width, modelEntry.inputSize.height)\n const engine = new PythonInferenceEngine(pythonPath, scriptPath, effectiveRuntime as DetectionRuntime, modelPath, [\n `--input-size=${inputSize}`,\n `--confidence=0.25`,\n ])\n await engine.initialize()\n return { engine, format: selectedFormat, modelPath }\n }\n\n // Final fallback: use ONNX CPU if available\n const fallbackPath = modelFilePath(modelsDir, modelEntry, 'onnx')\n if (modelEntry.formats['onnx'] && modelExists(fallbackPath)) {\n const engine = new NodeInferenceEngine(fallbackPath, 'cpu')\n await engine.initialize()\n return { engine, format: 'onnx', modelPath: fallbackPath }\n }\n\n throw new Error(\n `resolveEngine: format ${selectedFormat} is not yet supported by NodeInferenceEngine, ` +\n `no Python runtime is available, and no ONNX fallback exists`,\n )\n}\n\n/** Probe which ONNX execution providers are available on this system */\nexport async function probeOnnxBackends(): Promise<string[]> {\n const available: string[] = ['cpu'] // CPU is always available\n\n try {\n const ort = await import('onnxruntime-node')\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const providers: string[] = (ort as any).env?.webgl?.disabled !== undefined\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n ? ((ort as any).InferenceSession?.getAvailableProviders?.() ?? [])\n : []\n\n for (const p of providers) {\n const normalized = p.toLowerCase().replace('executionprovider', '')\n if (normalized === 'coreml') available.push('coreml')\n else if (normalized === 'cuda') available.push('cuda')\n else if (normalized === 'tensorrt') available.push('tensorrt')\n }\n } catch {\n // onnxruntime-node may not be installed; CPU only\n }\n\n // Platform-specific hints when getAvailableProviders isn't exposed\n if (process.platform === 'darwin' && !available.includes('coreml')) {\n available.push('coreml')\n }\n\n return [...new Set(available)]\n}\n"],"mappings":";AACA,YAAY,UAAU;AAEtB,IAAM,sBAAwD;AAAA,EAC5D,KAAK;AAAA,EACL,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,UAAU;AAAA,EACV,KAAK;AACP;AAEA,IAAM,oBAA+D;AAAA,EACnE,KAAK;AAAA,EACL,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,UAAU;AACZ;AAEO,IAAM,sBAAN,MAAsD;AAAA,EAK3D,YACmB,WACA,SACjB;AAFiB;AACA;AAEjB,SAAK,SAAU,kBAAkB,OAAO,KAAK;AAAA,EAC/C;AAAA,EATS,UAA4B;AAAA,EAC5B;AAAA,EACD,UAAmB;AAAA,EAS3B,MAAM,aAA4B;AAChC,UAAM,MAAM,MAAM,OAAO,kBAAkB;AAC3C,UAAM,WAAW,oBAAoB,KAAK,OAAO,KAAK;AAGtD,UAAM,eAAoB,gBAAW,KAAK,SAAS,IAC/C,KAAK,YACA,aAAQ,QAAQ,IAAI,GAAG,KAAK,SAAS;AAE9C,UAAM,iBAA0C;AAAA,MAC9C,oBAAoB,CAAC,QAAQ;AAAA,IAC/B;AAGA,SAAK,UAAU,MAAO,IAAY,iBAAiB,OAAO,cAAc,cAAc;AAAA,EACxF;AAAA,EAEA,MAAM,IAAI,OAAqB,YAAsD;AACnF,QAAI,CAAC,KAAK,SAAS;AACjB,YAAM,IAAI,MAAM,qEAAgE;AAAA,IAClF;AAEA,UAAM,MAAM,MAAM,OAAO,kBAAkB;AAE3C,UAAM,OAAO,KAAK;AAGlB,UAAM,YAAoB,KAAK,WAAW,CAAC;AAE3C,UAAM,SAAS,IAAK,IAAY,OAAO,WAAW,OAAO,CAAC,GAAG,UAAU,CAAC;AACxE,UAAM,QAAiC,EAAE,CAAC,SAAS,GAAG,OAAO;AAE7D,UAAM,UAAU,MAAM,KAAK,IAAI,KAAK;AACpC,UAAM,aAAqB,KAAK,YAAY,CAAC;AAC7C,UAAM,eAAe,QAAQ,UAAU;AAEvC,WAAO,aAAa;AAAA,EACtB;AAAA,EAEA,MAAM,eACJ,OACA,YACuC;AACvC,QAAI,CAAC,KAAK,SAAS;AACjB,YAAM,IAAI,MAAM,qEAAgE;AAAA,IAClF;AAEA,UAAM,MAAM,MAAM,OAAO,kBAAkB;AAE3C,UAAM,OAAO,KAAK;AAElB,UAAM,YAAoB,KAAK,WAAW,CAAC;AAE3C,UAAM,SAAS,IAAK,IAAY,OAAO,WAAW,OAAO,CAAC,GAAG,UAAU,CAAC;AACxE,UAAM,QAAiC,EAAE,CAAC,SAAS,GAAG,OAAO;AAE7D,UAAM,UAAU,MAAM,KAAK,IAAI,KAAK;AAEpC,UAAM,MAAoC,CAAC;AAC3C,eAAW,QAAQ,KAAK,aAAyB;AAC/C,UAAI,IAAI,IAAI,QAAQ,IAAI,EAAE;AAAA,IAC5B;AACA,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,UAAyB;AAG7B,SAAK,UAAU;AAAA,EACjB;AACF;;;ACnGA,SAAS,aAAgC;AAElC,IAAM,wBAAN,MAAwD;AAAA,EAQ7D,YACmB,YACA,YACjB,SACiB,WACA,YAA+B,CAAC,GACjD;AALiB;AACA;AAEA;AACA;AAEjB,SAAK,UAAU;AAEf,UAAM,mBAAwE;AAAA,MAC5E,MAAM;AAAA,MACN,QAAQ;AAAA,MACR,SAAS;AAAA,MACT,UAAU;AAAA,MACV,QAAQ;AAAA,IACV;AACA,SAAK,SAAS,iBAAiB,OAAO;AAAA,EACxC;AAAA,EAxBS;AAAA,EACA;AAAA,EACD,UAA+B;AAAA,EAC/B,gBAAwB,OAAO,MAAM,CAAC;AAAA,EACtC,iBAAoE;AAAA,EACpE,gBAAkD;AAAA,EAqB1D,MAAM,aAA4B;AAChC,UAAM,OAAO,CAAC,KAAK,YAAY,KAAK,WAAW,GAAG,KAAK,SAAS;AAChE,SAAK,UAAU,MAAM,KAAK,YAAY,MAAM;AAAA,MAC1C,OAAO,CAAC,QAAQ,QAAQ,MAAM;AAAA,IAChC,CAAC;AAED,QAAI,CAAC,KAAK,QAAQ,UAAU,CAAC,KAAK,QAAQ,OAAO;AAC/C,YAAM,IAAI,MAAM,uDAAuD;AAAA,IACzE;AAEA,SAAK,QAAQ,QAAQ,GAAG,QAAQ,CAAC,UAAkB;AAEjD,cAAQ,OAAO,MAAM,mBAAmB,MAAM,SAAS,CAAC,EAAE;AAAA,IAC5D,CAAC;AAED,SAAK,QAAQ,GAAG,SAAS,CAAC,QAAQ;AAChC,WAAK,gBAAgB,GAAG;AACxB,WAAK,gBAAgB;AACrB,WAAK,iBAAiB;AAAA,IACxB,CAAC;AAED,SAAK,QAAQ,GAAG,QAAQ,CAAC,SAAS;AAChC,UAAI,SAAS,GAAG;AACd,cAAM,MAAM,IAAI,MAAM,mDAAmD,IAAI,EAAE;AAC/E,aAAK,gBAAgB,GAAG;AACxB,aAAK,gBAAgB;AACrB,aAAK,iBAAiB;AAAA,MACxB;AAAA,IACF,CAAC;AAED,SAAK,QAAQ,OAAO,GAAG,QAAQ,CAAC,UAAkB;AAChD,WAAK,gBAAgB,OAAO,OAAO,CAAC,KAAK,eAAe,KAAK,CAAC;AAC9D,WAAK,YAAY;AAAA,IACnB,CAAC;AAGD,UAAM,IAAI,QAAc,CAACA,UAAS,WAAW;AAC3C,YAAM,UAAU,WAAW,MAAMA,SAAQ,GAAG,GAAI;AAChD,WAAK,SAAS,GAAG,SAAS,CAAC,QAAQ;AACjC,qBAAa,OAAO;AACpB,eAAO,GAAG;AAAA,MACZ,CAAC;AACD,WAAK,SAAS,GAAG,QAAQ,CAAC,SAAS;AACjC,qBAAa,OAAO;AACpB,YAAI,SAAS,GAAG;AACd,iBAAO,IAAI,MAAM,yDAAyD,IAAI,EAAE,CAAC;AAAA,QACnF;AAAA,MACF,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AAAA,EAEQ,cAAoB;AAE1B,QAAI,KAAK,cAAc,SAAS,EAAG;AAEnC,UAAM,SAAS,KAAK,cAAc,aAAa,CAAC;AAChD,QAAI,KAAK,cAAc,SAAS,IAAI,OAAQ;AAE5C,UAAM,YAAY,KAAK,cAAc,SAAS,GAAG,IAAI,MAAM;AAC3D,SAAK,gBAAgB,KAAK,cAAc,SAAS,IAAI,MAAM;AAE3D,UAAMA,WAAU,KAAK;AACrB,UAAM,SAAS,KAAK;AACpB,SAAK,iBAAiB;AACtB,SAAK,gBAAgB;AAErB,QAAI,CAACA,SAAS;AAEd,QAAI;AACF,YAAM,SAAS,KAAK,MAAM,UAAU,SAAS,MAAM,CAAC;AACpD,MAAAA,SAAQ,MAAM;AAAA,IAChB,SAAS,KAAK;AACZ,eAAS,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC,CAAC;AAAA,IAC9D;AAAA,EACF;AAAA;AAAA,EAGA,MAAM,QAAQ,MAAgD;AAC5D,QAAI,CAAC,KAAK,SAAS,OAAO;AACxB,YAAM,IAAI,MAAM,gDAAgD;AAAA,IAClE;AAEA,WAAO,IAAI,QAAiC,CAACA,UAAS,WAAW;AAC/D,WAAK,iBAAiBA;AACtB,WAAK,gBAAgB;AAGrB,YAAM,YAAY,OAAO,YAAY,CAAC;AACtC,gBAAU,cAAc,KAAK,QAAQ,CAAC;AACtC,WAAK,QAAS,MAAO,MAAM,OAAO,OAAO,CAAC,WAAW,IAAI,CAAC,CAAC;AAAA,IAC7D,CAAC;AAAA,EACH;AAAA;AAAA,EAGA,MAAM,IAAI,QAAsB,aAAuD;AACrF,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAAA;AAAA,EAGA,MAAM,eACJ,QACA,aACuC;AACvC,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,UAAyB;AAC7B,QAAI,KAAK,SAAS;AAChB,WAAK,QAAQ,OAAO,IAAI;AACxB,WAAK,QAAQ,KAAK,SAAS;AAC3B,WAAK,UAAU;AAAA,IACjB;AAAA,EACF;AACF;;;AC1HA,YAAY,QAAQ;AACpB,YAAYC,WAAU;AAoBtB,IAAM,wBAAwB,CAAC,UAAU,QAAQ,YAAY,KAAK;AAGlE,IAAM,oBAA2D;AAAA,EAC/D,KAAK;AAAA,EACL,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,UAAU;AACZ;AAGA,IAAM,oBAA8E;AAAA,EAClF,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,SAAS;AACX;AAEA,SAAS,cAAc,WAAmB,YAA+B,QAA6B;AACpG,QAAM,cAAc,WAAW,QAAQ,MAAM;AAC7C,MAAI,CAAC,aAAa;AAChB,UAAM,IAAI,MAAM,SAAS,WAAW,EAAE,WAAW,MAAM,SAAS;AAAA,EAClE;AAEA,QAAM,WAAW,YAAY,IAAI,MAAM,GAAG;AAC1C,QAAM,WAAW,SAAS,SAAS,SAAS,CAAC,KAAK,GAAG,WAAW,EAAE,IAAI,MAAM;AAC5E,SAAY,WAAK,WAAW,QAAQ;AACtC;AAEA,SAAS,YAAY,UAA2B;AAC9C,MAAI;AACF,WAAU,cAAW,QAAQ;AAAA,EAC/B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,eAAsB,cAAc,SAAyD;AAC3F,QAAM,EAAE,SAAS,SAAS,YAAY,WAAW,cAAc,IAAI;AAEnE,MAAI;AACJ,MAAI;AAEJ,MAAI,YAAY,QAAQ;AAEtB,UAAM,YAAY,MAAM,kBAAkB;AAG1C,QAAI,SAA0D;AAE9D,eAAW,KAAK,uBAAuB;AACrC,UAAI,CAAC,UAAU,SAAS,CAAC,EAAG;AAC5B,YAAM,MAAM,kBAAkB,CAAC;AAC/B,UAAI,CAAC,IAAK;AACV,UAAI,CAAC,WAAW,QAAQ,GAAG,EAAG;AAC9B,eAAS,EAAE,SAAS,GAAG,QAAQ,IAAI;AACnC;AAAA,IACF;AAEA,QAAI,CAAC,QAAQ;AACX,YAAM,IAAI;AAAA,QACR,wDAAwD,WAAW,EAAE,yBAAyB,UAAU,KAAK,IAAI,CAAC;AAAA,MACpH;AAAA,IACF;AAEA,qBAAiB,OAAO;AACxB,sBAAkB,OAAO;AAAA,EAC3B,OAAO;AAEL,UAAM,MAAM,kBAAkB,OAAO;AACrC,QAAI,CAAC,KAAK;AACR,YAAM,IAAI,MAAM,uCAAuC,OAAO,GAAG;AAAA,IACnE;AACA,QAAI,CAAC,WAAW,QAAQ,GAAG,GAAG;AAC5B,YAAM,IAAI;AAAA,QACR,wBAAwB,WAAW,EAAE,WAAW,GAAG,uBAAuB,OAAO;AAAA,MACnF;AAAA,IACF;AACA,qBAAiB;AAEjB,sBAAkB,YAAY,SAAU,WAAW,QAAS;AAAA,EAC9D;AAGA,MAAI,YAAY,cAAc,WAAW,YAAY,cAAc;AAEnE,MAAI,CAAC,YAAY,SAAS,GAAG;AAC3B,QAAI,eAAe;AACjB,YAAM,cAAc,WAAW,QAAQ,cAAc;AACrD,kBAAY,MAAM,cAAc,YAAY,KAAK,SAAS;AAAA,IAC5D,OAAO;AACL,YAAM,IAAI;AAAA,QACR,0CAA0C,SAAS;AAAA,MACrD;AAAA,IACF;AAAA,EACF;AAGA,MAAI,mBAAmB,UAAU,mBAAmB,UAAU;AAC5D,UAAM,SAAS,IAAI,oBAAoB,WAAW,eAAe;AACjE,UAAM,OAAO,WAAW;AACxB,WAAO,EAAE,QAAQ,QAAQ,gBAAgB,UAAU;AAAA,EACrD;AAGA,QAAM,EAAE,WAAW,IAAI;AACvB,QAAM,oBAAsD;AAAA,IAC1D,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,UAAU;AAAA,EACZ;AAEA,QAAM,mBAAmB,YAAY,SAAS,kBAAkB;AAChE,QAAM,aAAa,kBAAkB,gBAAgB;AAErD,MAAI,cAAc,YAAY;AAC5B,UAAM,aAAkB,WAAK,WAAW,gBAAgB,UAAU;AAClE,UAAM,YAAY,KAAK,IAAI,WAAW,UAAU,OAAO,WAAW,UAAU,MAAM;AAClF,UAAM,SAAS,IAAI,sBAAsB,YAAY,YAAY,kBAAsC,WAAW;AAAA,MAChH,gBAAgB,SAAS;AAAA,MACzB;AAAA,IACF,CAAC;AACD,UAAM,OAAO,WAAW;AACxB,WAAO,EAAE,QAAQ,QAAQ,gBAAgB,UAAU;AAAA,EACrD;AAGA,QAAM,eAAe,cAAc,WAAW,YAAY,MAAM;AAChE,MAAI,WAAW,QAAQ,MAAM,KAAK,YAAY,YAAY,GAAG;AAC3D,UAAM,SAAS,IAAI,oBAAoB,cAAc,KAAK;AAC1D,UAAM,OAAO,WAAW;AACxB,WAAO,EAAE,QAAQ,QAAQ,QAAQ,WAAW,aAAa;AAAA,EAC3D;AAEA,QAAM,IAAI;AAAA,IACR,yBAAyB,cAAc;AAAA,EAEzC;AACF;AAGA,eAAsB,oBAAuC;AAC3D,QAAM,YAAsB,CAAC,KAAK;AAElC,MAAI;AACF,UAAM,MAAM,MAAM,OAAO,kBAAkB;AAE3C,UAAM,YAAuB,IAAY,KAAK,OAAO,aAAa,SAE5D,IAAY,kBAAkB,wBAAwB,KAAK,CAAC,IAC9D,CAAC;AAEL,eAAW,KAAK,WAAW;AACzB,YAAM,aAAa,EAAE,YAAY,EAAE,QAAQ,qBAAqB,EAAE;AAClE,UAAI,eAAe,SAAU,WAAU,KAAK,QAAQ;AAAA,eAC3C,eAAe,OAAQ,WAAU,KAAK,MAAM;AAAA,eAC5C,eAAe,WAAY,WAAU,KAAK,UAAU;AAAA,IAC/D;AAAA,EACF,QAAQ;AAAA,EAER;AAGA,MAAI,QAAQ,aAAa,YAAY,CAAC,UAAU,SAAS,QAAQ,GAAG;AAClE,cAAU,KAAK,QAAQ;AAAA,EACzB;AAEA,SAAO,CAAC,GAAG,IAAI,IAAI,SAAS,CAAC;AAC/B;","names":["resolve","path"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/catalogs/face-detection-models.ts","../src/shared/postprocess/scrfd.ts","../src/addons/face-detection/index.ts"],"sourcesContent":["import type { ModelCatalogEntry, LabelDefinition } from '@camstack/types'\nimport { hfModelUrl } from '@camstack/types'\n\nconst HF_REPO = 'camstack/camstack-models'\n\nconst FACE_LABELS: readonly LabelDefinition[] = [\n { id: 'face', name: 'Face' },\n] as const\n\nexport const FACE_DETECTION_MODELS: readonly ModelCatalogEntry[] = [\n {\n id: 'scrfd-500m',\n name: 'SCRFD 500M',\n description: 'SCRFD 500M — ultra-lightweight face detector',\n inputSize: { width: 640, height: 640 },\n labels: FACE_LABELS,\n formats: {\n onnx: {\n url: hfModelUrl(HF_REPO, 'faceDetection/scrfd/onnx/camstack-scrfd-500m.onnx'),\n sizeMB: 2.2,\n },\n coreml: {\n url: hfModelUrl(HF_REPO, 'faceDetection/scrfd/coreml/camstack-scrfd-500m.mlpackage'),\n sizeMB: 1.2,\n },\n openvino: {\n url: hfModelUrl(HF_REPO, 'faceDetection/scrfd/openvino/camstack-scrfd-500m.xml'),\n sizeMB: 1.3,\n },\n },\n },\n {\n id: 'scrfd-2.5g',\n name: 'SCRFD 2.5G',\n description: 'SCRFD 2.5G — balanced face detection model',\n inputSize: { width: 640, height: 640 },\n labels: FACE_LABELS,\n formats: {\n onnx: {\n url: hfModelUrl(HF_REPO, 'faceDetection/scrfd/onnx/camstack-scrfd-2.5g.onnx'),\n sizeMB: 3.1,\n },\n coreml: {\n url: hfModelUrl(HF_REPO, 'faceDetection/scrfd/coreml/camstack-scrfd-2.5g.mlpackage'),\n sizeMB: 1.7,\n },\n openvino: {\n url: hfModelUrl(HF_REPO, 'faceDetection/scrfd/openvino/camstack-scrfd-2.5g.xml'),\n sizeMB: 1.8,\n },\n },\n },\n {\n id: 'scrfd-10g',\n name: 'SCRFD 10G',\n description: 'SCRFD 10G — high-accuracy face detector',\n inputSize: { width: 640, height: 640 },\n labels: FACE_LABELS,\n formats: {\n onnx: {\n url: hfModelUrl(HF_REPO, 'faceDetection/scrfd/onnx/camstack-scrfd-10g.onnx'),\n sizeMB: 16,\n },\n coreml: {\n url: hfModelUrl(HF_REPO, 'faceDetection/scrfd/coreml/camstack-scrfd-10g.mlpackage'),\n sizeMB: 8.2,\n },\n openvino: {\n url: hfModelUrl(HF_REPO, 'faceDetection/scrfd/openvino/camstack-scrfd-10g.xml'),\n sizeMB: 8.3,\n },\n },\n },\n] as const\n","import type { SpatialDetection, BoundingBox, Landmark } from '@camstack/types'\nimport { iou, nms } from './yolo.js'\n\nexport interface ScrfdRawOutputs {\n readonly [key: string]: Float32Array\n}\n\nconst STRIDES = [8, 16, 32] as const\nconst NUM_ANCHORS_PER_STRIDE = 2\n\n/** Generate anchor points for a given stride and input size */\nfunction generateAnchors(stride: number, inputSize: number): Array<{ cx: number; cy: number }> {\n const featureSize = Math.ceil(inputSize / stride)\n const anchors: Array<{ cx: number; cy: number }> = []\n for (let y = 0; y < featureSize; y++) {\n for (let x = 0; x < featureSize; x++) {\n for (let k = 0; k < NUM_ANCHORS_PER_STRIDE; k++) {\n anchors.push({\n cx: (x + 0.5) * stride,\n cy: (y + 0.5) * stride,\n })\n }\n }\n }\n return anchors\n}\n\nexport function scrfdPostprocess(\n outputs: ScrfdRawOutputs,\n confidence: number,\n inputSize: number,\n originalWidth: number,\n originalHeight: number,\n): SpatialDetection[] {\n // Scale factor from letterbox (assume square crop, so same scale both axes)\n const scaleX = originalWidth / inputSize\n const scaleY = originalHeight / inputSize\n\n interface Candidate {\n readonly bbox: BoundingBox\n readonly score: number\n readonly landmarks?: readonly Landmark[]\n }\n\n const candidates: Candidate[] = []\n\n for (const stride of STRIDES) {\n const scoreKey = Object.keys(outputs).find((k) => k.includes(`score_${stride}`) || k.includes(`_${stride}_score`))\n const bboxKey = Object.keys(outputs).find((k) => k.includes(`bbox_${stride}`) || k.includes(`_${stride}_bbox`))\n const kpsKey = Object.keys(outputs).find((k) => k.includes(`kps_${stride}`) || k.includes(`_${stride}_kps`))\n\n if (!scoreKey || !bboxKey) continue\n\n const scores = outputs[scoreKey]!\n const bboxes = outputs[bboxKey]!\n const kps = kpsKey ? outputs[kpsKey] : undefined\n const anchors = generateAnchors(stride, inputSize)\n\n const n = anchors.length\n\n for (let i = 0; i < n; i++) {\n const score = scores[i]!\n if (score < confidence) continue\n\n const anchor = anchors[i]!\n\n // Bboxes are relative to the anchor center in stride units, scaled by stride\n const x1 = anchor.cx - bboxes[i * 4]! * stride\n const y1 = anchor.cy - bboxes[i * 4 + 1]! * stride\n const x2 = anchor.cx + bboxes[i * 4 + 2]! * stride\n const y2 = anchor.cy + bboxes[i * 4 + 3]! * stride\n\n const bbox: BoundingBox = {\n x: x1 * scaleX,\n y: y1 * scaleY,\n w: (x2 - x1) * scaleX,\n h: (y2 - y1) * scaleY,\n }\n\n let landmarks: readonly Landmark[] | undefined\n if (kps) {\n const pts: Landmark[] = []\n for (let p = 0; p < 5; p++) {\n pts.push({\n x: (anchor.cx + kps[i * 10 + p * 2]! * stride) * scaleX,\n y: (anchor.cy + kps[i * 10 + p * 2 + 1]! * stride) * scaleY,\n })\n }\n landmarks = pts\n }\n\n candidates.push({ bbox, score, landmarks })\n }\n }\n\n if (candidates.length === 0) return []\n\n const keptIndices = nms(candidates, 0.45)\n\n return keptIndices.map((idx) => {\n const { bbox, score, landmarks } = candidates[idx]!\n return {\n class: 'face',\n originalClass: 'face',\n score,\n bbox,\n ...(landmarks ? { landmarks } : {}),\n } satisfies SpatialDetection\n })\n}\n","import type {\n ICropperProvider,\n IDetectionAddon,\n AddonManifest,\n AddonContext,\n CropInput,\n CropperOutput,\n ConfigUISchema,\n ClassMapDefinition,\n ProbeResult,\n ModelCatalogEntry,\n DetectionModel,\n LabelDefinition,\n IInferenceEngine,\n} from '@camstack/types'\nimport { FACE_DETECTION_MODELS } from '../../catalogs/face-detection-models.js'\nimport { cropRegion, letterbox } from '../../shared/image-utils.js'\nimport { scrfdPostprocess } from '../../shared/postprocess/scrfd.js'\nimport { resolveEngine } from '../../shared/engine-resolver.js'\n\nconst FACE_LABEL: LabelDefinition = { id: 'face', name: 'Face' }\nconst FACE_LABELS: readonly LabelDefinition[] = [FACE_LABEL]\nconst FACE_CLASS_MAP: ClassMapDefinition = { mapping: {}, preserveOriginal: true }\n\nexport default class FaceDetectionAddon implements ICropperProvider, IDetectionAddon {\n readonly id = 'face-detection'\n readonly slot = 'cropper' as const\n readonly inputClasses = ['person'] as const\n readonly outputClasses = ['face'] as const\n readonly slotPriority = 0\n readonly manifest: AddonManifest = {\n id: 'face-detection',\n name: 'Face Detection',\n version: '0.1.0',\n description: 'SCRFD-based face detector — crops face regions from person detections',\n packageName: '@camstack/addon-vision',\n slot: 'cropper',\n inputClasses: ['person'],\n outputClasses: ['face'],\n supportsCustomModels: false,\n mayRequirePython: false,\n defaultConfig: {\n modelId: 'scrfd-500m',\n runtime: 'auto',\n backend: 'cpu',\n confidence: 0.5,\n },\n }\n\n private engine!: IInferenceEngine\n private modelEntry!: ModelCatalogEntry\n private confidence = 0.5\n\n async initialize(ctx: AddonContext): Promise<void> {\n const cfg = ctx.addonConfig\n const modelId = (cfg['modelId'] as string | undefined) ?? 'scrfd-500m'\n const runtime = (cfg['runtime'] as string | undefined) ?? 'auto'\n const backend = (cfg['backend'] as string | undefined) ?? 'cpu'\n this.confidence = (cfg['confidence'] as number | undefined) ?? 0.5\n\n const entry = FACE_DETECTION_MODELS.find((m) => m.id === modelId)\n if (!entry) {\n throw new Error(`FaceDetectionAddon: unknown modelId \"${modelId}\"`)\n }\n this.modelEntry = entry\n\n const resolved = await resolveEngine({\n runtime: runtime as 'auto',\n backend,\n modelEntry: entry,\n modelsDir: ctx.locationPaths.models,\n })\n this.engine = resolved.engine\n }\n\n async crop(input: CropInput): Promise<CropperOutput> {\n const start = Date.now()\n const { width: inputW, height: inputH } = this.modelEntry.inputSize\n const targetSize = Math.max(inputW, inputH)\n\n // Crop the person region from the full frame\n const personCrop = await cropRegion(input.frame.data, input.roi)\n\n // Letterbox resize to model input size\n const lb = await letterbox(personCrop, targetSize)\n\n // SCRFD has multiple output tensors\n const engineWithMulti = this.engine as IInferenceEngine & {\n runMultiOutput?: (input: Float32Array, shape: readonly number[]) => Promise<Record<string, Float32Array>>\n }\n\n let outputs: Record<string, Float32Array>\n if (typeof engineWithMulti.runMultiOutput === 'function') {\n outputs = await engineWithMulti.runMultiOutput(lb.data, [1, 3, targetSize, targetSize])\n } else {\n // Fallback: wrap single output\n const single = await this.engine.run(lb.data, [1, 3, targetSize, targetSize])\n outputs = { output0: single }\n }\n\n const crops = scrfdPostprocess(\n outputs,\n this.confidence,\n targetSize,\n lb.originalWidth,\n lb.originalHeight,\n )\n\n return {\n crops,\n inferenceMs: Date.now() - start,\n modelId: this.modelEntry.id,\n }\n }\n\n async shutdown(): Promise<void> {\n await this.engine?.dispose()\n }\n\n getConfigSchema(): ConfigUISchema {\n return {\n sections: [\n {\n id: 'model',\n title: 'Model',\n columns: 1,\n fields: [\n {\n key: 'modelId',\n label: 'Model',\n type: 'model-selector',\n catalog: [...FACE_DETECTION_MODELS],\n allowCustom: false,\n allowConversion: false,\n acceptFormats: ['onnx', 'coreml', 'openvino'],\n requiredMetadata: ['inputSize', 'labels', 'outputFormat'],\n outputFormatHint: 'ssd',\n },\n ],\n },\n {\n id: 'thresholds',\n title: 'Detection Thresholds',\n columns: 1,\n fields: [\n {\n key: 'confidence',\n label: 'Confidence Threshold',\n type: 'slider',\n min: 0.1,\n max: 1.0,\n step: 0.05,\n default: 0.5,\n },\n ],\n },\n ],\n }\n }\n\n getClassMap(): ClassMapDefinition {\n return FACE_CLASS_MAP\n }\n\n getModelCatalog(): ModelCatalogEntry[] {\n return [...FACE_DETECTION_MODELS]\n }\n\n getAvailableModels(): DetectionModel[] {\n return []\n }\n\n getActiveLabels(): readonly LabelDefinition[] {\n return FACE_LABELS\n }\n\n async probe(): Promise<ProbeResult> {\n return {\n available: true,\n runtime: this.engine?.runtime ?? 'onnx',\n device: this.engine?.device ?? 'cpu',\n capabilities: ['fp32'],\n }\n }\n}\n"],"mappings":"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","names":["FACE_LABELS"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/catalogs/plate-recognition-models.ts","../src/shared/postprocess/paddleocr.ts","../src/addons/plate-recognition/index.ts"],"sourcesContent":["import type { ModelCatalogEntry, LabelDefinition } from '@camstack/types'\nimport { hfModelUrl } from '@camstack/types'\n\nconst HF_REPO = 'camstack/camstack-models'\n\nconst PLATE_TEXT_LABELS: readonly LabelDefinition[] = [\n { id: 'text', name: 'Plate Text' },\n] as const\n\nexport const PLATE_RECOGNITION_MODELS: readonly ModelCatalogEntry[] = [\n {\n id: 'paddleocr-latin',\n name: 'PaddleOCR Latin',\n description: 'PaddleOCR recognition model for Latin-script license plates',\n inputSize: { width: 320, height: 48 },\n labels: PLATE_TEXT_LABELS,\n formats: {\n onnx: {\n url: hfModelUrl(HF_REPO, 'plateRecognition/paddleocr/onnx/camstack-paddleocr-latin-rec.onnx'),\n sizeMB: 7.5,\n },\n openvino: {\n url: hfModelUrl(HF_REPO, 'plateRecognition/paddleocr/openvino/camstack-paddleocr-latin.xml'),\n sizeMB: 4,\n },\n },\n },\n {\n id: 'paddleocr-en',\n name: 'PaddleOCR English',\n description: 'PaddleOCR recognition model optimized for English license plates',\n inputSize: { width: 320, height: 48 },\n labels: PLATE_TEXT_LABELS,\n formats: {\n onnx: {\n url: hfModelUrl(HF_REPO, 'plateRecognition/paddleocr/onnx/camstack-paddleocr-en-rec.onnx'),\n sizeMB: 7.5,\n },\n openvino: {\n url: hfModelUrl(HF_REPO, 'plateRecognition/paddleocr/openvino/camstack-paddleocr-en.xml'),\n sizeMB: 4,\n },\n },\n },\n] as const\n","/** Decode CTC output to text.\n *\n * Output shape: [1, seqLen, numChars]\n * Algorithm: argmax per timestep → collapse consecutive duplicates → remove blank (index 0) → join\n */\nexport function ctcDecode(\n output: Float32Array,\n seqLen: number,\n numChars: number,\n charset: readonly string[], // index 0 = blank token\n): { text: string; confidence: number } {\n // Step 1: argmax per timestep + track confidence as mean of selected scores\n let totalLogScore = 0\n const rawIndices: number[] = []\n\n for (let t = 0; t < seqLen; t++) {\n const offset = t * numChars\n let bestIdx = 0\n let bestVal = output[offset]!\n\n for (let c = 1; c < numChars; c++) {\n const val = output[offset + c]!\n if (val > bestVal) {\n bestVal = val\n bestIdx = c\n }\n }\n\n rawIndices.push(bestIdx)\n totalLogScore += bestVal\n }\n\n // Step 2: collapse consecutive duplicates\n const collapsed: number[] = []\n for (let i = 0; i < rawIndices.length; i++) {\n const cur = rawIndices[i]!\n if (i === 0 || cur !== rawIndices[i - 1]) {\n collapsed.push(cur)\n }\n }\n\n // Step 3: remove blank (index 0)\n const filtered = collapsed.filter((idx) => idx !== 0)\n\n // Step 4: join characters\n const text = filtered.map((idx) => charset[idx] ?? '').join('')\n\n const confidence = seqLen > 0 ? totalLogScore / seqLen : 0\n\n return { text, confidence }\n}\n","import type {\n IClassifierProvider,\n IDetectionAddon,\n AddonManifest,\n AddonContext,\n CropInput,\n ClassifierOutput,\n ConfigUISchema,\n ClassMapDefinition,\n ProbeResult,\n ModelCatalogEntry,\n DetectionModel,\n LabelDefinition,\n IInferenceEngine,\n RequiredStep,\n} from '@camstack/types'\nimport { PLATE_RECOGNITION_MODELS } from '../../catalogs/plate-recognition-models.js'\nimport { cropRegion, resizeAndNormalize } from '../../shared/image-utils.js'\nimport { ctcDecode } from '../../shared/postprocess/paddleocr.js'\nimport { resolveEngine } from '../../shared/engine-resolver.js'\n\nconst PLATE_TEXT_LABEL: LabelDefinition = { id: 'plate-text', name: 'Plate Text' }\nconst PLATE_TEXT_LABELS: readonly LabelDefinition[] = [PLATE_TEXT_LABEL]\nconst PLATE_REC_CLASS_MAP: ClassMapDefinition = { mapping: {}, preserveOriginal: true }\n\nimport * as fs from 'node:fs'\nimport * as path from 'node:path'\n\n/** Load charset from dict.txt file — index 0 is the CTC blank token */\nfunction loadCharset(modelsDir: string, modelId: string): readonly string[] {\n // Try to find the dict file next to the model\n const dictNames = [\n `camstack-${modelId}-dict.txt`,\n `camstack-paddleocr-latin-dict.txt`,\n `camstack-paddleocr-en-dict.txt`,\n ]\n for (const name of dictNames) {\n const dictPath = path.join(modelsDir, name)\n if (fs.existsSync(dictPath)) {\n const lines = fs.readFileSync(dictPath, 'utf-8').split('\\n').filter((l) => l.length > 0)\n // PaddleOCR convention: blank token at index 0, then dict chars, then a space token at end\n return ['', ...lines, ' ']\n }\n }\n throw new Error(`PlateRecognitionAddon: dict.txt not found in ${modelsDir}`)\n}\n\n// Will be loaded during initialize()\nlet CHARSET: readonly string[] = []\n\nconst REQUIRED_STEPS: readonly RequiredStep[] = [\n { slot: 'cropper', outputClasses: ['plate'], description: 'Requires a plate detector' },\n]\n\nexport default class PlateRecognitionAddon implements IClassifierProvider, IDetectionAddon {\n readonly id = 'plate-recognition'\n readonly slot = 'classifier' as const\n readonly inputClasses = ['plate'] as const\n readonly outputClasses = ['plate-text:*'] as const\n readonly slotPriority = 0\n readonly requiredSteps = REQUIRED_STEPS\n readonly manifest: AddonManifest = {\n id: 'plate-recognition',\n name: 'License Plate Recognition (OCR)',\n version: '0.1.0',\n description: 'PaddleOCR-based license plate text recognition',\n packageName: '@camstack/addon-vision',\n slot: 'classifier',\n inputClasses: ['plate'],\n outputClasses: ['plate-text:*'],\n requiredSteps: REQUIRED_STEPS,\n supportsCustomModels: false,\n mayRequirePython: false,\n defaultConfig: {\n modelId: 'paddleocr-latin',\n runtime: 'auto',\n backend: 'cpu',\n minConfidence: 0.5,\n },\n }\n\n private engine!: IInferenceEngine\n private modelEntry!: ModelCatalogEntry\n private minConfidence = 0.5\n\n async initialize(ctx: AddonContext): Promise<void> {\n const cfg = ctx.addonConfig\n const modelId = (cfg['modelId'] as string | undefined) ?? 'paddleocr-latin'\n const runtime = (cfg['runtime'] as string | undefined) ?? 'auto'\n const backend = (cfg['backend'] as string | undefined) ?? 'cpu'\n this.minConfidence = (cfg['minConfidence'] as number | undefined) ?? 0.5\n\n const entry = PLATE_RECOGNITION_MODELS.find((m) => m.id === modelId)\n if (!entry) {\n throw new Error(`PlateRecognitionAddon: unknown modelId \"${modelId}\"`)\n }\n this.modelEntry = entry\n\n // Load charset from dict.txt\n CHARSET = loadCharset(ctx.locationPaths.models, modelId)\n\n const resolved = await resolveEngine({\n runtime: runtime as 'auto',\n backend,\n modelEntry: entry,\n modelsDir: ctx.locationPaths.models,\n })\n this.engine = resolved.engine\n }\n\n async classify(input: CropInput): Promise<ClassifierOutput> {\n const start = Date.now()\n const { width: inputW, height: inputH } = this.modelEntry.inputSize\n\n // Crop the plate region\n const plateCrop = await cropRegion(input.frame.data, input.roi)\n\n // Resize to 320x48, normalize to [0,1], NCHW\n const normalized = await resizeAndNormalize(plateCrop, inputW, inputH, 'zero-one', 'nchw')\n\n const output = await this.engine.run(normalized, [1, 3, inputH, inputW])\n\n // PaddleOCR CTC output shape: [1, seqLen, numChars]\n const numChars = CHARSET.length\n const seqLen = output.length / numChars\n const { text, confidence } = ctcDecode(output, seqLen, numChars, CHARSET)\n\n if (confidence < this.minConfidence || text.trim().length === 0) {\n return {\n classifications: [],\n inferenceMs: Date.now() - start,\n modelId: this.modelEntry.id,\n }\n }\n\n return {\n classifications: [\n {\n class: 'plate-text',\n score: confidence,\n text,\n },\n ],\n inferenceMs: Date.now() - start,\n modelId: this.modelEntry.id,\n }\n }\n\n async shutdown(): Promise<void> {\n await this.engine?.dispose()\n }\n\n getConfigSchema(): ConfigUISchema {\n return {\n sections: [\n {\n id: 'model',\n title: 'Model',\n columns: 1,\n fields: [\n {\n key: 'modelId',\n label: 'Model',\n type: 'model-selector',\n catalog: [...PLATE_RECOGNITION_MODELS],\n allowCustom: false,\n allowConversion: false,\n acceptFormats: ['onnx', 'openvino'],\n requiredMetadata: ['inputSize', 'labels', 'outputFormat'],\n outputFormatHint: 'ocr',\n },\n ],\n },\n {\n id: 'thresholds',\n title: 'Recognition Settings',\n columns: 1,\n fields: [\n {\n key: 'minConfidence',\n label: 'Minimum Confidence',\n type: 'slider',\n min: 0.1,\n max: 1.0,\n step: 0.05,\n default: 0.5,\n },\n ],\n },\n ],\n }\n }\n\n getClassMap(): ClassMapDefinition {\n return PLATE_REC_CLASS_MAP\n }\n\n getModelCatalog(): ModelCatalogEntry[] {\n return [...PLATE_RECOGNITION_MODELS]\n }\n\n getAvailableModels(): DetectionModel[] {\n return []\n }\n\n getActiveLabels(): readonly LabelDefinition[] {\n return PLATE_TEXT_LABELS\n }\n\n async probe(): Promise<ProbeResult> {\n return {\n available: true,\n runtime: this.engine?.runtime ?? 'onnx',\n device: this.engine?.device ?? 'cpu',\n capabilities: ['fp32'],\n }\n }\n}\n"],"mappings":"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","names":["PLATE_TEXT_LABELS"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/addons/bird-global-classifier/index.ts"],"sourcesContent":["import type {\n IClassifierProvider,\n IDetectionAddon,\n AddonManifest,\n AddonContext,\n CropInput,\n ClassifierOutput,\n ConfigUISchema,\n ClassMapDefinition,\n ProbeResult,\n ModelCatalogEntry,\n DetectionModel,\n LabelDefinition,\n IInferenceEngine,\n} from '@camstack/types'\nimport { BIRD_SPECIES_MODELS } from '../../catalogs/animal-classification-models.js'\nimport { cropRegion, resizeAndNormalize } from '../../shared/image-utils.js'\nimport { resolveEngine } from '../../shared/engine-resolver.js'\n\nimport * as fs from 'node:fs'\nimport * as path from 'node:path'\n\nconst SPECIES_LABEL: LabelDefinition = { id: 'species', name: 'Bird Species' }\nconst SPECIES_LABELS: readonly LabelDefinition[] = [SPECIES_LABEL]\nconst BIRD_CLASS_MAP: ClassMapDefinition = { mapping: {}, preserveOriginal: true }\n\n/** Load bird species labels from JSON file in modelsDir */\nfunction loadLabels(modelsDir: string, modelId: string): readonly string[] {\n const labelNames = [\n `camstack-${modelId}-labels.json`,\n `camstack-bird-species-525-labels.json`,\n ]\n for (const name of labelNames) {\n const labelPath = path.join(modelsDir, name)\n if (fs.existsSync(labelPath)) {\n const raw = fs.readFileSync(labelPath, 'utf-8')\n return JSON.parse(raw) as string[]\n }\n }\n throw new Error(`BirdGlobalClassifierAddon: labels JSON not found in ${modelsDir}`)\n}\n\nfunction softmax(logits: Float32Array): Float32Array {\n const max = logits.reduce((a, b) => Math.max(a, b), -Infinity)\n const exps = logits.map((v) => Math.exp(v - max))\n const sum = exps.reduce((a, b) => a + b, 0)\n return exps.map((v) => v / sum) as unknown as Float32Array\n}\n\nexport default class BirdGlobalClassifierAddon implements IClassifierProvider, IDetectionAddon {\n readonly id = 'bird-global-classifier'\n readonly slot = 'classifier' as const\n readonly inputClasses = ['animal'] as const\n readonly outputClasses = ['species:*'] as const\n readonly slotPriority = 0\n readonly requiredSteps = [] as const\n readonly manifest: AddonManifest = {\n id: 'bird-global-classifier',\n name: 'Bird Classifier (Global, 525 species)',\n version: '0.1.0',\n description: 'EfficientNet — 525 worldwide bird species (MIT license, ONNX only)',\n packageName: '@camstack/addon-vision',\n slot: 'classifier',\n inputClasses: ['animal'],\n outputClasses: ['species:*'],\n supportsCustomModels: false,\n mayRequirePython: false,\n defaultConfig: {\n modelId: 'bird-species-525',\n runtime: 'auto',\n backend: 'cpu',\n minConfidence: 0.3,\n },\n }\n\n private engine!: IInferenceEngine\n private modelEntry!: ModelCatalogEntry\n private labels: readonly string[] = []\n private minConfidence = 0.3\n\n async initialize(ctx: AddonContext): Promise<void> {\n const cfg = ctx.addonConfig\n const modelId = (cfg['modelId'] as string | undefined) ?? 'bird-species-525'\n const runtime = (cfg['runtime'] as string | undefined) ?? 'auto'\n const backend = (cfg['backend'] as string | undefined) ?? 'cpu'\n this.minConfidence = (cfg['minConfidence'] as number | undefined) ?? 0.3\n\n const entry = BIRD_SPECIES_MODELS.find((m) => m.id === modelId)\n if (!entry) {\n throw new Error(`BirdGlobalClassifierAddon: unknown modelId \"${modelId}\"`)\n }\n this.modelEntry = entry\n\n // Load labels from JSON file\n this.labels = loadLabels(ctx.locationPaths.models, modelId)\n\n const resolved = await resolveEngine({\n runtime: runtime as 'auto',\n backend,\n modelEntry: entry,\n modelsDir: ctx.locationPaths.models,\n })\n this.engine = resolved.engine\n }\n\n async classify(input: CropInput): Promise<ClassifierOutput> {\n const start = Date.now()\n const { width: inputW, height: inputH } = this.modelEntry.inputSize\n\n // Crop the animal region\n const animalCrop = await cropRegion(input.frame.data, input.roi)\n\n // Resize to 224x224, ImageNet normalization, NCHW\n const normalized = await resizeAndNormalize(animalCrop, inputW, inputH, 'imagenet', 'nchw')\n\n // Run inference — output shape: [1, 525]\n const rawOutput = await this.engine.run(normalized, [1, 3, inputH, inputW])\n\n // Softmax to get probabilities\n const probs = softmax(rawOutput)\n\n // Find argmax\n let maxIdx = 0\n let maxScore = probs[0] ?? 0\n for (let i = 1; i < probs.length; i++) {\n const score = probs[i] ?? 0\n if (score > maxScore) {\n maxScore = score\n maxIdx = i\n }\n }\n\n if (maxScore < this.minConfidence) {\n return {\n classifications: [],\n inferenceMs: Date.now() - start,\n modelId: this.modelEntry.id,\n }\n }\n\n const label = this.labels[maxIdx] ?? `species_${maxIdx}`\n\n return {\n classifications: [\n {\n class: label,\n score: maxScore,\n },\n ],\n inferenceMs: Date.now() - start,\n modelId: this.modelEntry.id,\n }\n }\n\n async shutdown(): Promise<void> {\n await this.engine?.dispose()\n }\n\n getConfigSchema(): ConfigUISchema {\n return {\n sections: [\n {\n id: 'model',\n title: 'Model',\n columns: 1,\n fields: [\n {\n key: 'modelId',\n label: 'Model',\n type: 'model-selector',\n catalog: [...BIRD_SPECIES_MODELS],\n allowCustom: false,\n allowConversion: false,\n acceptFormats: ['onnx', 'coreml', 'openvino'],\n requiredMetadata: ['inputSize', 'labels'],\n outputFormatHint: 'classification',\n },\n ],\n },\n {\n id: 'thresholds',\n title: 'Classification Settings',\n columns: 1,\n fields: [\n {\n key: 'minConfidence',\n label: 'Minimum Confidence',\n type: 'slider',\n min: 0.05,\n max: 1.0,\n step: 0.05,\n default: 0.3,\n },\n ],\n },\n {\n id: 'runtime',\n title: 'Runtime',\n columns: 2,\n fields: [\n {\n key: 'runtime',\n label: 'Runtime',\n type: 'select',\n options: [\n { value: 'auto', label: 'Auto (recommended)' },\n { value: 'onnx', label: 'ONNX Runtime' },\n { value: 'coreml', label: 'CoreML (Apple)' },\n ],\n },\n {\n key: 'backend',\n label: 'Backend',\n type: 'select',\n dependsOn: { runtime: 'onnx' },\n options: [\n { value: 'cpu', label: 'CPU' },\n { value: 'coreml', label: 'CoreML' },\n { value: 'cuda', label: 'CUDA (NVIDIA)' },\n ],\n },\n ],\n },\n ],\n }\n }\n\n getClassMap(): ClassMapDefinition {\n return BIRD_CLASS_MAP\n }\n\n getModelCatalog(): ModelCatalogEntry[] {\n return [...BIRD_SPECIES_MODELS]\n }\n\n getAvailableModels(): DetectionModel[] {\n return []\n }\n\n getActiveLabels(): readonly LabelDefinition[] {\n return SPECIES_LABELS\n }\n\n async probe(): Promise<ProbeResult> {\n return {\n available: true,\n runtime: this.engine?.runtime ?? 'onnx',\n device: this.engine?.device ?? 'cpu',\n capabilities: ['fp32'],\n }\n }\n}\n"],"mappings":"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","names":[]}
@@ -1,229 +0,0 @@
1
- import {
2
- jpegToRgb,
3
- rgbToGrayscale
4
- } from "./chunk-6OR5TE7A.mjs";
5
-
6
- // src/addons/motion-detection/frame-diff.ts
7
- function detectMotion(current, previous, width, height, threshold, minArea) {
8
- const numPixels = width * height;
9
- const mask = new Uint8Array(numPixels);
10
- for (let i = 0; i < numPixels; i++) {
11
- mask[i] = Math.abs((current[i] ?? 0) - (previous[i] ?? 0)) > threshold ? 1 : 0;
12
- }
13
- const labels = new Int32Array(numPixels).fill(0);
14
- const parent = new Int32Array(numPixels + 1).fill(0);
15
- let nextLabel = 1;
16
- function findRoot(x) {
17
- while (parent[x] !== x) {
18
- parent[x] = parent[parent[x]];
19
- x = parent[x];
20
- }
21
- return x;
22
- }
23
- function union(a, b) {
24
- const ra = findRoot(a);
25
- const rb = findRoot(b);
26
- if (ra !== rb) parent[rb] = ra;
27
- return ra;
28
- }
29
- for (let i = 0; i <= numPixels; i++) {
30
- parent[i] = i;
31
- }
32
- for (let y = 0; y < height; y++) {
33
- for (let x = 0; x < width; x++) {
34
- const idx = y * width + x;
35
- if (!mask[idx]) continue;
36
- const above = y > 0 ? labels[(y - 1) * width + x] ?? 0 : 0;
37
- const left = x > 0 ? labels[y * width + (x - 1)] ?? 0 : 0;
38
- if (above === 0 && left === 0) {
39
- labels[idx] = nextLabel;
40
- parent[nextLabel] = nextLabel;
41
- nextLabel++;
42
- } else if (above !== 0 && left === 0) {
43
- labels[idx] = above;
44
- } else if (above === 0 && left !== 0) {
45
- labels[idx] = left;
46
- } else {
47
- labels[idx] = union(above, left);
48
- }
49
- }
50
- }
51
- for (let i = 0; i < numPixels; i++) {
52
- if (labels[i]) {
53
- labels[i] = findRoot(labels[i]);
54
- }
55
- }
56
- const bboxMap = /* @__PURE__ */ new Map();
57
- for (let y = 0; y < height; y++) {
58
- for (let x = 0; x < width; x++) {
59
- const idx = y * width + x;
60
- const label = labels[idx];
61
- if (!label) continue;
62
- const diff = Math.abs((current[idx] ?? 0) - (previous[idx] ?? 0));
63
- const existing = bboxMap.get(label);
64
- if (existing) {
65
- existing.minX = Math.min(existing.minX, x);
66
- existing.minY = Math.min(existing.minY, y);
67
- existing.maxX = Math.max(existing.maxX, x);
68
- existing.maxY = Math.max(existing.maxY, y);
69
- existing.count++;
70
- existing.intensitySum += diff;
71
- } else {
72
- bboxMap.set(label, {
73
- minX: x,
74
- minY: y,
75
- maxX: x,
76
- maxY: y,
77
- count: 1,
78
- intensitySum: diff
79
- });
80
- }
81
- }
82
- }
83
- const regions = [];
84
- for (const [, info] of bboxMap) {
85
- if (info.count < minArea) continue;
86
- regions.push({
87
- bbox: {
88
- x: info.minX,
89
- y: info.minY,
90
- w: info.maxX - info.minX + 1,
91
- h: info.maxY - info.minY + 1
92
- },
93
- pixelCount: info.count,
94
- intensity: info.intensitySum / info.count
95
- });
96
- }
97
- return regions;
98
- }
99
-
100
- // src/addons/motion-detection/index.ts
101
- var MOTION_LABEL = { id: "motion", name: "Motion" };
102
- var MOTION_LABELS = [MOTION_LABEL];
103
- var EMPTY_CLASS_MAP = { mapping: {}, preserveOriginal: true };
104
- var MotionDetectionAddon = class {
105
- id = "motion-detection";
106
- slot = "detector";
107
- inputClasses = null;
108
- outputClasses = ["motion"];
109
- slotPriority = 10;
110
- // runs first — feeds other detectors
111
- manifest = {
112
- id: "motion-detection",
113
- name: "Motion Detection",
114
- version: "0.1.0",
115
- description: "Frame-differencing motion detector \u2014 no inference engine required",
116
- packageName: "@camstack/addon-vision",
117
- slot: "detector",
118
- inputClasses: void 0,
119
- outputClasses: ["motion"],
120
- supportsCustomModels: false,
121
- mayRequirePython: false,
122
- defaultConfig: {
123
- threshold: 25,
124
- minArea: 500
125
- }
126
- };
127
- previousGray = null;
128
- previousWidth = 0;
129
- previousHeight = 0;
130
- threshold = 25;
131
- minArea = 500;
132
- async initialize(ctx) {
133
- const cfg = ctx.addonConfig;
134
- this.threshold = cfg["threshold"] ?? 25;
135
- this.minArea = cfg["minArea"] ?? 500;
136
- }
137
- async detect(frame) {
138
- const start = Date.now();
139
- const { data, width, height } = await jpegToRgb(frame.data);
140
- const currentGray = rgbToGrayscale(data, width, height);
141
- if (!this.previousGray || this.previousWidth !== width || this.previousHeight !== height) {
142
- this.previousGray = currentGray;
143
- this.previousWidth = width;
144
- this.previousHeight = height;
145
- return { detections: [], inferenceMs: Date.now() - start, modelId: "frame-diff" };
146
- }
147
- const regions = detectMotion(
148
- currentGray,
149
- this.previousGray,
150
- width,
151
- height,
152
- this.threshold,
153
- this.minArea
154
- );
155
- this.previousGray = currentGray;
156
- const detections = regions.map((r) => ({
157
- class: "motion",
158
- originalClass: "motion",
159
- score: Math.min(1, r.intensity / 255),
160
- bbox: r.bbox
161
- }));
162
- return {
163
- detections,
164
- inferenceMs: Date.now() - start,
165
- modelId: "frame-diff"
166
- };
167
- }
168
- async shutdown() {
169
- this.previousGray = null;
170
- }
171
- getConfigSchema() {
172
- return {
173
- sections: [
174
- {
175
- id: "motion",
176
- title: "Motion Detection",
177
- columns: 2,
178
- fields: [
179
- {
180
- key: "threshold",
181
- label: "Pixel Difference Threshold",
182
- description: "Minimum per-pixel intensity change to count as motion (0-255)",
183
- type: "slider",
184
- min: 5,
185
- max: 100,
186
- step: 5,
187
- default: 25
188
- },
189
- {
190
- key: "minArea",
191
- label: "Minimum Region Area (px)",
192
- description: "Minimum number of changed pixels to report a motion region",
193
- type: "number",
194
- min: 50,
195
- max: 1e4
196
- }
197
- ]
198
- }
199
- ]
200
- };
201
- }
202
- getClassMap() {
203
- return EMPTY_CLASS_MAP;
204
- }
205
- getModelCatalog() {
206
- return [];
207
- }
208
- getAvailableModels() {
209
- return [];
210
- }
211
- getActiveLabels() {
212
- return MOTION_LABELS;
213
- }
214
- async probe() {
215
- return {
216
- available: true,
217
- runtime: "onnx",
218
- // no inference; satisfies the type (any runtime works)
219
- device: "cpu",
220
- capabilities: ["fp32"]
221
- };
222
- }
223
- };
224
-
225
- export {
226
- detectMotion,
227
- MotionDetectionAddon
228
- };
229
- //# sourceMappingURL=chunk-QIMDG34B.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/addons/motion-detection/frame-diff.ts","../src/addons/motion-detection/index.ts"],"sourcesContent":["import type { BoundingBox } from '@camstack/types'\n\nexport interface MotionRegion {\n readonly bbox: BoundingBox\n readonly pixelCount: number\n readonly intensity: number\n}\n\n/**\n * Detect motion by frame differencing.\n *\n * @param current - Grayscale pixel array for the current frame (Uint8Array, length = width * height)\n * @param previous - Grayscale pixel array for the previous frame\n * @param width - Frame width in pixels\n * @param height - Frame height in pixels\n * @param threshold - Pixel diff threshold 0-255; differences below this are ignored\n * @param minArea - Minimum number of changed pixels for a region to be reported\n */\nexport function detectMotion(\n current: Uint8Array,\n previous: Uint8Array,\n width: number,\n height: number,\n threshold: number,\n minArea: number,\n): MotionRegion[] {\n const numPixels = width * height\n\n // Step 1: Compute binary mask — 1 where abs(current - previous) > threshold\n const mask = new Uint8Array(numPixels)\n for (let i = 0; i < numPixels; i++) {\n mask[i] = Math.abs((current[i] ?? 0) - (previous[i] ?? 0)) > threshold ? 1 : 0\n }\n\n // Step 2: Two-pass connected component labeling\n const labels = new Int32Array(numPixels).fill(0)\n const parent = new Int32Array(numPixels + 1).fill(0)\n let nextLabel = 1\n\n // Union-Find helpers\n function findRoot(x: number): number {\n while (parent[x] !== x) {\n parent[x] = parent[parent[x]!]! // path compression\n x = parent[x]!\n }\n return x\n }\n\n function union(a: number, b: number): number {\n const ra = findRoot(a)\n const rb = findRoot(b)\n if (ra !== rb) parent[rb] = ra\n return ra\n }\n\n // Initialize parent array as identity\n for (let i = 0; i <= numPixels; i++) {\n parent[i] = i\n }\n\n // First pass: assign provisional labels\n for (let y = 0; y < height; y++) {\n for (let x = 0; x < width; x++) {\n const idx = y * width + x\n if (!mask[idx]) continue\n\n const above = y > 0 ? labels[(y - 1) * width + x] ?? 0 : 0\n const left = x > 0 ? labels[y * width + (x - 1)] ?? 0 : 0\n\n if (above === 0 && left === 0) {\n labels[idx] = nextLabel\n parent[nextLabel] = nextLabel\n nextLabel++\n } else if (above !== 0 && left === 0) {\n labels[idx] = above\n } else if (above === 0 && left !== 0) {\n labels[idx] = left\n } else {\n // Both neighbors — merge\n labels[idx] = union(above, left)\n }\n }\n }\n\n // Second pass: resolve all labels to roots\n for (let i = 0; i < numPixels; i++) {\n if (labels[i]) {\n labels[i] = findRoot(labels[i]!)\n }\n }\n\n // Step 3: Collect bounding boxes and pixel counts per root label\n const bboxMap = new Map<\n number,\n { minX: number; minY: number; maxX: number; maxY: number; count: number; intensitySum: number }\n >()\n\n for (let y = 0; y < height; y++) {\n for (let x = 0; x < width; x++) {\n const idx = y * width + x\n const label = labels[idx]\n if (!label) continue\n\n const diff = Math.abs((current[idx] ?? 0) - (previous[idx] ?? 0))\n const existing = bboxMap.get(label)\n if (existing) {\n existing.minX = Math.min(existing.minX, x)\n existing.minY = Math.min(existing.minY, y)\n existing.maxX = Math.max(existing.maxX, x)\n existing.maxY = Math.max(existing.maxY, y)\n existing.count++\n existing.intensitySum += diff\n } else {\n bboxMap.set(label, {\n minX: x,\n minY: y,\n maxX: x,\n maxY: y,\n count: 1,\n intensitySum: diff,\n })\n }\n }\n }\n\n // Step 4: Filter by minimum area and build result\n const regions: MotionRegion[] = []\n for (const [, info] of bboxMap) {\n if (info.count < minArea) continue\n regions.push({\n bbox: {\n x: info.minX,\n y: info.minY,\n w: info.maxX - info.minX + 1,\n h: info.maxY - info.minY + 1,\n },\n pixelCount: info.count,\n intensity: info.intensitySum / info.count,\n })\n }\n\n return regions\n}\n","import type {\n IDetectorProvider,\n IDetectionAddon,\n AddonManifest,\n AddonContext,\n FrameInput,\n DetectorOutput,\n ConfigUISchema,\n ClassMapDefinition,\n ProbeResult,\n ModelCatalogEntry,\n DetectionModel,\n LabelDefinition,\n SpatialDetection,\n} from '@camstack/types'\nimport { jpegToRgb, rgbToGrayscale } from '../../shared/image-utils.js'\nimport { detectMotion } from './frame-diff.js'\n\nconst MOTION_LABEL: LabelDefinition = { id: 'motion', name: 'Motion' }\nconst MOTION_LABELS: readonly LabelDefinition[] = [MOTION_LABEL]\nconst EMPTY_CLASS_MAP: ClassMapDefinition = { mapping: {}, preserveOriginal: true }\n\nexport default class MotionDetectionAddon implements IDetectorProvider, IDetectionAddon {\n readonly id = 'motion-detection'\n readonly slot = 'detector' as const\n readonly inputClasses: readonly string[] | null = null\n readonly outputClasses = ['motion'] as const\n readonly slotPriority = 10 // runs first — feeds other detectors\n readonly manifest: AddonManifest = {\n id: 'motion-detection',\n name: 'Motion Detection',\n version: '0.1.0',\n description: 'Frame-differencing motion detector — no inference engine required',\n packageName: '@camstack/addon-vision',\n slot: 'detector',\n inputClasses: undefined,\n outputClasses: ['motion'],\n supportsCustomModels: false,\n mayRequirePython: false,\n defaultConfig: {\n threshold: 25,\n minArea: 500,\n },\n }\n\n private previousGray: Uint8Array | null = null\n private previousWidth = 0\n private previousHeight = 0\n private threshold = 25\n private minArea = 500\n\n async initialize(ctx: AddonContext): Promise<void> {\n const cfg = ctx.addonConfig\n this.threshold = (cfg['threshold'] as number | undefined) ?? 25\n this.minArea = (cfg['minArea'] as number | undefined) ?? 500\n }\n\n async detect(frame: FrameInput): Promise<DetectorOutput> {\n const start = Date.now()\n\n const { data, width, height } = await jpegToRgb(frame.data)\n const currentGray = rgbToGrayscale(data, width, height)\n\n if (!this.previousGray || this.previousWidth !== width || this.previousHeight !== height) {\n // Store first frame and return empty — no previous to diff against\n this.previousGray = currentGray\n this.previousWidth = width\n this.previousHeight = height\n return { detections: [], inferenceMs: Date.now() - start, modelId: 'frame-diff' }\n }\n\n const regions = detectMotion(\n currentGray,\n this.previousGray,\n width,\n height,\n this.threshold,\n this.minArea,\n )\n\n this.previousGray = currentGray\n\n const detections: SpatialDetection[] = regions.map((r) => ({\n class: 'motion',\n originalClass: 'motion',\n score: Math.min(1, r.intensity / 255),\n bbox: r.bbox,\n }))\n\n return {\n detections,\n inferenceMs: Date.now() - start,\n modelId: 'frame-diff',\n }\n }\n\n async shutdown(): Promise<void> {\n this.previousGray = null\n }\n\n getConfigSchema(): ConfigUISchema {\n return {\n sections: [\n {\n id: 'motion',\n title: 'Motion Detection',\n columns: 2,\n fields: [\n {\n key: 'threshold',\n label: 'Pixel Difference Threshold',\n description: 'Minimum per-pixel intensity change to count as motion (0-255)',\n type: 'slider',\n min: 5,\n max: 100,\n step: 5,\n default: 25,\n },\n {\n key: 'minArea',\n label: 'Minimum Region Area (px)',\n description: 'Minimum number of changed pixels to report a motion region',\n type: 'number',\n min: 50,\n max: 10000,\n },\n ],\n },\n ],\n }\n }\n\n getClassMap(): ClassMapDefinition {\n return EMPTY_CLASS_MAP\n }\n\n getModelCatalog(): ModelCatalogEntry[] {\n return []\n }\n\n getAvailableModels(): DetectionModel[] {\n return []\n }\n\n getActiveLabels(): readonly LabelDefinition[] {\n return MOTION_LABELS\n }\n\n async probe(): Promise<ProbeResult> {\n return {\n available: true,\n runtime: 'onnx', // no inference; satisfies the type (any runtime works)\n device: 'cpu',\n capabilities: ['fp32'],\n }\n }\n}\n"],"mappings":"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","names":[]}