zorya-classifier 0.1.0 → 0.2.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 (313) hide show
  1. package/NOTICE.md +2 -0
  2. package/README.md +18 -6
  3. package/dist/app/cli-arguments.d.ts +40 -0
  4. package/dist/app/cli-arguments.js +67 -0
  5. package/dist/app/create-classifier.d.ts +19 -0
  6. package/dist/app/create-classifier.js +226 -14
  7. package/dist/app/extraction-files.d.ts +9 -0
  8. package/dist/app/extraction-files.js +28 -0
  9. package/dist/cli.js +58 -14
  10. package/dist/entities/boundary-view.d.ts +29 -0
  11. package/dist/entities/boundary-view.js +1 -0
  12. package/dist/entities/contracts.d.ts +77 -1
  13. package/dist/entities/contracts.js +7 -0
  14. package/dist/entities/cutout-raster.d.ts +7 -0
  15. package/dist/entities/cutout-raster.js +1 -0
  16. package/dist/entities/object-proposals.d.ts +53 -0
  17. package/dist/entities/object-proposals.js +8 -0
  18. package/dist/entities/representation.d.ts +225 -0
  19. package/dist/entities/representation.js +33 -0
  20. package/dist/entities/segmentation-metadata.d.ts +11 -0
  21. package/dist/entities/segmentation-metadata.js +1 -0
  22. package/dist/entities/segmentation-verification.d.ts +30 -0
  23. package/dist/entities/segmentation-verification.js +1 -0
  24. package/dist/entities/segmentation.d.ts +159 -0
  25. package/dist/entities/segmentation.js +5 -0
  26. package/dist/entities/whole-object.d.ts +128 -0
  27. package/dist/entities/whole-object.js +7 -0
  28. package/dist/features/choice.d.ts +1 -1
  29. package/dist/features/choice.js +2 -2
  30. package/dist/features/segmentation/box-check.d.ts +10 -0
  31. package/dist/features/segmentation/box-check.js +299 -0
  32. package/dist/features/segmentation/mask.d.ts +6 -0
  33. package/dist/features/segmentation/mask.js +89 -0
  34. package/dist/features/segmentation/packed-mask.d.ts +5 -0
  35. package/dist/features/segmentation/packed-mask.js +53 -0
  36. package/dist/index.d.ts +6 -1
  37. package/dist/infrastructure/boundary-view.d.ts +2 -0
  38. package/dist/infrastructure/boundary-view.js +263 -0
  39. package/dist/infrastructure/openrouter-common.d.ts +8 -8
  40. package/dist/infrastructure/openrouter-vision.d.ts +3 -0
  41. package/dist/infrastructure/openrouter-vision.js +5 -0
  42. package/dist/infrastructure/raster.d.ts +10 -1
  43. package/dist/infrastructure/raster.js +99 -4
  44. package/dist/infrastructure/segmentation/box-presentation.d.ts +2 -0
  45. package/dist/infrastructure/segmentation/box-presentation.js +136 -0
  46. package/dist/infrastructure/segmentation/cutout-raster.d.ts +3 -0
  47. package/dist/infrastructure/segmentation/cutout-raster.js +79 -0
  48. package/dist/infrastructure/segmentation/grabcut/index.d.ts +13 -0
  49. package/dist/infrastructure/segmentation/grabcut/index.js +118 -0
  50. package/dist/infrastructure/segmentation/grabcut/worker-inference.d.ts +47 -0
  51. package/dist/infrastructure/segmentation/grabcut/worker-inference.js +260 -0
  52. package/dist/infrastructure/segmentation/index.d.ts +2 -0
  53. package/dist/infrastructure/segmentation/index.js +2 -0
  54. package/dist/infrastructure/segmentation/mobile-sam/index.d.ts +58 -0
  55. package/dist/infrastructure/segmentation/mobile-sam/index.js +472 -0
  56. package/dist/infrastructure/segmentation/mobile-sam/worker-inference.d.ts +149 -0
  57. package/dist/infrastructure/segmentation/mobile-sam/worker-inference.js +845 -0
  58. package/dist/infrastructure/segmentation/native-child.d.ts +23 -0
  59. package/dist/infrastructure/segmentation/native-child.js +297 -0
  60. package/dist/infrastructure/segmentation/native-protocol.d.ts +923 -0
  61. package/dist/infrastructure/segmentation/native-protocol.js +242 -0
  62. package/dist/infrastructure/segmentation/native-worker.d.ts +1 -0
  63. package/dist/infrastructure/segmentation/native-worker.js +93 -0
  64. package/dist/infrastructure/segmentation/probe-backend.d.ts +1 -0
  65. package/dist/infrastructure/segmentation/probe-backend.js +31 -0
  66. package/dist/infrastructure/segmentation/proposal-presentation.d.ts +3 -0
  67. package/dist/infrastructure/segmentation/proposal-presentation.js +371 -0
  68. package/dist/infrastructure/segmentation/verify-asset.d.ts +4 -0
  69. package/dist/infrastructure/segmentation/verify-asset.js +32 -0
  70. package/dist/infrastructure/text-representation.d.ts +23 -0
  71. package/dist/infrastructure/text-representation.js +226 -0
  72. package/dist/infrastructure/vision-chat.d.ts +1 -1
  73. package/dist/infrastructure/vision-chat.js +13 -4
  74. package/dist/openai-compatible.d.ts +3 -0
  75. package/dist/openai-compatible.js +4 -0
  76. package/dist/openrouter.d.ts +2 -2
  77. package/dist/openrouter.js +1 -1
  78. package/dist/processes/boundary-state.d.ts +12 -0
  79. package/dist/processes/boundary-state.js +196 -0
  80. package/dist/processes/classify.d.ts +2 -1
  81. package/dist/processes/classify.js +21 -3
  82. package/dist/processes/extract-from-region.d.ts +19 -0
  83. package/dist/processes/extract-from-region.js +415 -0
  84. package/dist/processes/extract.d.ts +25 -0
  85. package/dist/processes/extract.js +223 -0
  86. package/dist/processes/locate.d.ts +2 -1
  87. package/dist/processes/locate.js +84 -11
  88. package/dist/processes/proposal-location.d.ts +19 -0
  89. package/dist/processes/proposal-location.js +396 -0
  90. package/dist/processes/whole-object.d.ts +7 -0
  91. package/dist/processes/whole-object.js +272 -0
  92. package/dist/segmentation.d.ts +10 -0
  93. package/dist/segmentation.js +4 -0
  94. package/dist/shared/input-budget.d.ts +7 -0
  95. package/dist/shared/input-budget.js +21 -0
  96. package/docs/0.2.0-plan.md +190 -0
  97. package/docs/README.ru.md +18 -5
  98. package/docs/acceptance.md +116 -3
  99. package/docs/api.md +8 -4
  100. package/docs/architecture.md +20 -7
  101. package/docs/benchmarks.md +10 -0
  102. package/docs/categorical-presence-port.md +26 -0
  103. package/docs/evidence/v02/baseline.json +1043 -0
  104. package/docs/evidence/v02/boundary-variants.md +17 -0
  105. package/docs/evidence/v02/box-confirmation-dev40/README.md +15 -0
  106. package/docs/evidence/v02/box-confirmation-dev40/component-refinement.json +246 -0
  107. package/docs/evidence/v02/box-confirmation-dev40/evaluation.json +1663 -0
  108. package/docs/evidence/v02/box-confirmation-dev40/provenance.json +130 -0
  109. package/docs/evidence/v02/box-confirmation-dev40/report.json +127 -0
  110. package/docs/evidence/v02/box-confirmation-dev40-freeze-20261020-v1.json +230 -0
  111. package/docs/evidence/v02/box-confirmation-method-selection-20261020-v1.json +85 -0
  112. package/docs/evidence/v02/box-heldout-cancelled-before-dispatch-20261021-v1.json +32 -0
  113. package/docs/evidence/v02/box-heldout-combined-seal-20261021-v1.json +89 -0
  114. package/docs/evidence/v02/box-heldout-evaluator-truth-seal-20261021-v1.json +61 -0
  115. package/docs/evidence/v02/box-heldout-exclusion-audit-20261020-v1.json +81 -0
  116. package/docs/evidence/v02/box-heldout-map-seal-20261021-v1.json +48 -0
  117. package/docs/evidence/v02/box-heldout-photo-seal-20261020-v1.json +62 -0
  118. package/docs/evidence/v02/box-heldout-run-freeze-20261021-v1.json +61 -0
  119. package/docs/evidence/v02/box-heldout-source-review-20261020-v1.json +134 -0
  120. package/docs/evidence/v02/box-heldout-ui-seal-20261020-v1.json +44 -0
  121. package/docs/evidence/v02/calculator-proof-root-review-20261020-v1.json +63 -0
  122. package/docs/evidence/v02/categorical-dev40-freeze.json +94 -0
  123. package/docs/evidence/v02/categorical-dev40-preserve.json +460 -0
  124. package/docs/evidence/v02/categorical-dev40-target-only.json +460 -0
  125. package/docs/evidence/v02/categorical-heldout-admission-20261020-v3.json +215 -0
  126. package/docs/evidence/v02/categorical-heldout-admission-20261020-v4.json +270 -0
  127. package/docs/evidence/v02/categorical-heldout-dispatch-authorization-20261020-v4.json +79 -0
  128. package/docs/evidence/v02/categorical-heldout-predispatch-20261020-v3.json +57 -0
  129. package/docs/evidence/v02/categorical-heldout-producer-source-map-v4.json +262 -0
  130. package/docs/evidence/v02/categorical-heldout-producer-source-map.json +263 -0
  131. package/docs/evidence/v02/categorical-heldout-run-freeze-20261020-v3.json +35 -0
  132. package/docs/evidence/v02/categorical-heldout-run-freeze-20261020-v4.json +35 -0
  133. package/docs/evidence/v02/categorical-heldout-v4/README.md +35 -0
  134. package/docs/evidence/v02/categorical-heldout-v4/candidate-evaluation.json +2812 -0
  135. package/docs/evidence/v02/categorical-heldout-v4/candidate-physical-receipts.json +15261 -0
  136. package/docs/evidence/v02/categorical-heldout-v4/candidate-provenance.json +131 -0
  137. package/docs/evidence/v02/categorical-heldout-v4/control-evaluation.json +855 -0
  138. package/docs/evidence/v02/categorical-heldout-v4/control-physical-receipts.json +4402 -0
  139. package/docs/evidence/v02/categorical-heldout-v4/control-provenance.json +31 -0
  140. package/docs/evidence/v02/categorical-heldout-v4/mask-normalized-exploratory.json +682 -0
  141. package/docs/evidence/v02/categorical-heldout-v4/paired-qualification.json +21 -0
  142. package/docs/evidence/v02/categorical-heldout-v4/report.json +258 -0
  143. package/docs/evidence/v02/categorical-presence-dev6.json +144 -0
  144. package/docs/evidence/v02/categorical-presence-dev6.md +18 -0
  145. package/docs/evidence/v02/categorical-vision-method-selection.json +90 -0
  146. package/docs/evidence/v02/checked-region-component-v1/README.md +7 -0
  147. package/docs/evidence/v02/checked-region-component-v1/evidence.json +311 -0
  148. package/docs/evidence/v02/checked-region-resource-interrupted-2b0d09f.json +102 -0
  149. package/docs/evidence/v02/checked-region-segmentation-resource-4df544c-receipt.json +167 -0
  150. package/docs/evidence/v02/checked-region-segmentation-resource-4df544c-root-review.json +58 -0
  151. package/docs/evidence/v02/checked-region-segmentation-resource-987ac32-receipt.json +158 -0
  152. package/docs/evidence/v02/ci-012b935.json +17 -0
  153. package/docs/evidence/v02/ci-0c365cb.json +372 -0
  154. package/docs/evidence/v02/ci-185bde8.json +32 -0
  155. package/docs/evidence/v02/ci-24f849f.json +95 -0
  156. package/docs/evidence/v02/ci-25c19cd.json +375 -0
  157. package/docs/evidence/v02/ci-2922caf.json +375 -0
  158. package/docs/evidence/v02/ci-29357d7.json +29 -0
  159. package/docs/evidence/v02/ci-32aa90d.json +24 -0
  160. package/docs/evidence/v02/ci-35e3064.json +376 -0
  161. package/docs/evidence/v02/ci-4331b1b.json +384 -0
  162. package/docs/evidence/v02/ci-43b7426.json +376 -0
  163. package/docs/evidence/v02/ci-450439c.json +373 -0
  164. package/docs/evidence/v02/ci-46283fe.json +29 -0
  165. package/docs/evidence/v02/ci-4c9c09f.json +34 -0
  166. package/docs/evidence/v02/ci-5579646.json +27 -0
  167. package/docs/evidence/v02/ci-6f9cc15.json +127 -0
  168. package/docs/evidence/v02/ci-7c1051f.json +27 -0
  169. package/docs/evidence/v02/ci-9837888.json +34 -0
  170. package/docs/evidence/v02/ci-987ac32.json +29 -0
  171. package/docs/evidence/v02/ci-9d00fe5.json +25 -0
  172. package/docs/evidence/v02/ci-9f85acc.json +127 -0
  173. package/docs/evidence/v02/ci-be275a6.json +201 -0
  174. package/docs/evidence/v02/ci-c107ddf.json +372 -0
  175. package/docs/evidence/v02/ci-c35f67d.json +127 -0
  176. package/docs/evidence/v02/ci-c5eddd8.json +32 -0
  177. package/docs/evidence/v02/ci-d2042e0.json +372 -0
  178. package/docs/evidence/v02/ci-d684259.json +375 -0
  179. package/docs/evidence/v02/ci-e0c190b.json +127 -0
  180. package/docs/evidence/v02/ci-e551253.json +372 -0
  181. package/docs/evidence/v02/ci-f9d4633.json +29 -0
  182. package/docs/evidence/v02/dev1-proposal-evaluation.md +13 -0
  183. package/docs/evidence/v02/dev2-mobile-sam-proposal-oracle.md +21 -0
  184. package/docs/evidence/v02/dev2-proposal-evaluation.md +21 -0
  185. package/docs/evidence/v02/fresh-development.md +16 -0
  186. package/docs/evidence/v02/fresh-native-numeric/artifact-hashes.json +9 -0
  187. package/docs/evidence/v02/fresh-native-numeric/evaluation-root-v1.json +568 -0
  188. package/docs/evidence/v02/fresh-native-numeric/run-manifest.json +342 -0
  189. package/docs/evidence/v02/fresh-native-numeric/summary.json +5654 -0
  190. package/docs/evidence/v02/fresh-native-outline-axis/artifact-hashes.json +9 -0
  191. package/docs/evidence/v02/fresh-native-outline-axis/evaluation-root-v1.json +568 -0
  192. package/docs/evidence/v02/fresh-native-outline-axis/run-manifest.json +378 -0
  193. package/docs/evidence/v02/fresh-native-outline-axis/summary.json +5605 -0
  194. package/docs/evidence/v02/fresh-text/README.md +23 -0
  195. package/docs/evidence/v02/fresh-text/ascii-contours-v1/classify-followup-run-manifest.json +405 -0
  196. package/docs/evidence/v02/fresh-text/ascii-contours-v1/classify-followup-summary.json +1646 -0
  197. package/docs/evidence/v02/fresh-text/ascii-contours-v1/evaluation.json +568 -0
  198. package/docs/evidence/v02/fresh-text/ascii-contours-v1/original-run-manifest.json +404 -0
  199. package/docs/evidence/v02/fresh-text/ascii-contours-v1/original-summary.json +4416 -0
  200. package/docs/evidence/v02/fresh-text/ascii-luma-v1/classify-followup-run-manifest.json +405 -0
  201. package/docs/evidence/v02/fresh-text/ascii-luma-v1/classify-followup-summary.json +1646 -0
  202. package/docs/evidence/v02/fresh-text/ascii-luma-v1/evaluation.json +568 -0
  203. package/docs/evidence/v02/fresh-text/ascii-luma-v1/original-run-manifest.json +404 -0
  204. package/docs/evidence/v02/fresh-text/ascii-luma-v1/original-summary.json +4647 -0
  205. package/docs/evidence/v02/fresh-text/ascii-svg-layered-v1/classify-followup-run-manifest.json +405 -0
  206. package/docs/evidence/v02/fresh-text/ascii-svg-layered-v1/classify-followup-summary.json +846 -0
  207. package/docs/evidence/v02/fresh-text/ascii-svg-layered-v1/evaluation.json +568 -0
  208. package/docs/evidence/v02/fresh-text/ascii-svg-layered-v1/original-run-manifest.json +404 -0
  209. package/docs/evidence/v02/fresh-text/ascii-svg-layered-v1/original-summary.json +1789 -0
  210. package/docs/evidence/v02/fresh-text/no-image-control/evaluation.json +568 -0
  211. package/docs/evidence/v02/fresh-text/no-image-control/original-run-manifest.json +404 -0
  212. package/docs/evidence/v02/fresh-text/no-image-control/original-summary.json +3953 -0
  213. package/docs/evidence/v02/fresh-text/palette-grid-v1/classify-followup-run-manifest.json +405 -0
  214. package/docs/evidence/v02/fresh-text/palette-grid-v1/classify-followup-summary.json +1646 -0
  215. package/docs/evidence/v02/fresh-text/palette-grid-v1/evaluation.json +568 -0
  216. package/docs/evidence/v02/fresh-text/palette-grid-v1/original-run-manifest.json +404 -0
  217. package/docs/evidence/v02/fresh-text/palette-grid-v1/original-summary.json +3974 -0
  218. package/docs/evidence/v02/fresh-text/pixel-rle-v1/classify-followup-run-manifest.json +405 -0
  219. package/docs/evidence/v02/fresh-text/pixel-rle-v1/classify-followup-summary.json +1646 -0
  220. package/docs/evidence/v02/fresh-text/pixel-rle-v1/evaluation.json +568 -0
  221. package/docs/evidence/v02/fresh-text/pixel-rle-v1/original-run-manifest.json +404 -0
  222. package/docs/evidence/v02/fresh-text/pixel-rle-v1/original-summary.json +4945 -0
  223. package/docs/evidence/v02/fresh-text/report.json +284 -0
  224. package/docs/evidence/v02/fresh-text/svg-contours-v1/classify-followup-run-manifest.json +405 -0
  225. package/docs/evidence/v02/fresh-text/svg-contours-v1/classify-followup-summary.json +846 -0
  226. package/docs/evidence/v02/fresh-text/svg-contours-v1/evaluation.json +568 -0
  227. package/docs/evidence/v02/fresh-text/svg-contours-v1/original-run-manifest.json +404 -0
  228. package/docs/evidence/v02/fresh-text/svg-contours-v1/original-summary.json +2189 -0
  229. package/docs/evidence/v02/heldout-evaluation-20261020.md +37 -0
  230. package/docs/evidence/v02/heldout-input-derivation.md +18 -0
  231. package/docs/evidence/v02/integration-0c365cb.json +90 -0
  232. package/docs/evidence/v02/integration-25c19cd.json +52 -0
  233. package/docs/evidence/v02/integration-35e3064.json +41 -0
  234. package/docs/evidence/v02/integration-4331b1b.json +42 -0
  235. package/docs/evidence/v02/integration-4c9c09f.json +46 -0
  236. package/docs/evidence/v02/integration-8010aad.json +48 -0
  237. package/docs/evidence/v02/integration-c1aa640.json +41 -0
  238. package/docs/evidence/v02/integration-eea1790.json +46 -0
  239. package/docs/evidence/v02/integration-fa82e65.json +42 -0
  240. package/docs/evidence/v02/jpeg-transport-offline.json +268 -0
  241. package/docs/evidence/v02/jpeg-transport-offline.md +7 -0
  242. package/docs/evidence/v02/local-archive-cleanup.json +59 -0
  243. package/docs/evidence/v02/native-base64-after.json +30 -0
  244. package/docs/evidence/v02/native-base64-before.json +32 -0
  245. package/docs/evidence/v02/native-checked-region-windows-46283fe.json +248 -0
  246. package/docs/evidence/v02/native-checked-region-windows-987ac32.json +141 -0
  247. package/docs/evidence/v02/native-grabcut-windows-4c494a3-run2.json +148 -0
  248. package/docs/evidence/v02/native-grabcut-windows-4c494a3-run3.json +148 -0
  249. package/docs/evidence/v02/native-grabcut-windows-4c494a3.json +56 -0
  250. package/docs/evidence/v02/native-proposal-windows-0c365cb.json +148 -0
  251. package/docs/evidence/v02/native-proposal-windows-450439c.json +156 -0
  252. package/docs/evidence/v02/native-proposal-windows-4c9c09f-retry1.json +147 -0
  253. package/docs/evidence/v02/native-proposal-windows-4c9c09f.json +52 -0
  254. package/docs/evidence/v02/native-region-windows-efc9318.json +163 -0
  255. package/docs/evidence/v02/native-windows-eea1790-baseline.json +198 -0
  256. package/docs/evidence/v02/native-windows-eea1790.json +228 -0
  257. package/docs/evidence/v02/ordinary-luna-axis/run-manifest.json +99 -0
  258. package/docs/evidence/v02/ordinary-luna-axis/summary.json +811 -0
  259. package/docs/evidence/v02/owned-temp-cleanup-20261020-v1.json +28 -0
  260. package/docs/evidence/v02/presentation-paired-20261020.json +1438 -0
  261. package/docs/evidence/v02/proposal-resource-soak-261dc23.json +201 -0
  262. package/docs/evidence/v02/proposal-resource-soak-261dc23.md +15 -0
  263. package/docs/evidence/v02/proposal-resource-soak-450439c-receipt.md +13 -0
  264. package/docs/evidence/v02/reconstructed-c03-box.json +41 -0
  265. package/docs/evidence/v02/region-seeded-dev-v1/README.md +17 -0
  266. package/docs/evidence/v02/region-seeded-dev-v1/evaluation.json +260 -0
  267. package/docs/evidence/v02/region-seeded-dev-v2/README.md +15 -0
  268. package/docs/evidence/v02/region-seeded-dev-v2/evaluation.json +232 -0
  269. package/docs/evidence/v02/region-seeded-dev-v3-candidate-only/README.md +9 -0
  270. package/docs/evidence/v02/region-seeded-dev-v3-candidate-only/control-identity-failure.json +22 -0
  271. package/docs/evidence/v02/region-seeded-dev-v3-candidate-only/evaluation.json +160 -0
  272. package/docs/evidence/v02/region-seeded-dev-v3-control-r2/README.md +9 -0
  273. package/docs/evidence/v02/region-seeded-dev-v3-control-r2/attempt-accounting.json +55 -0
  274. package/docs/evidence/v02/region-seeded-dev-v3-control-r2/evaluation.json +136 -0
  275. package/docs/evidence/v02/region-seeded-development-control-r2-freeze-20261021.json +132 -0
  276. package/docs/evidence/v02/region-seeded-development-freeze-20261021-v1.json +167 -0
  277. package/docs/evidence/v02/region-seeded-development-freeze-20261021-v2.json +186 -0
  278. package/docs/evidence/v02/region-seeded-development-freeze-20261021-v3.json +205 -0
  279. package/docs/evidence/v02/region-seeded-heldout-cancelled-before-dispatch-20261021-v2.json +20 -0
  280. package/docs/evidence/v02/region-seeded-heldout-dispatch-authorization-20261021-v3.json +47 -0
  281. package/docs/evidence/v02/region-seeded-heldout-readiness-audit-20261021-v3.json +125 -0
  282. package/docs/evidence/v02/region-seeded-heldout-run-freeze-20261021-v2.json +234 -0
  283. package/docs/evidence/v02/region-seeded-heldout-run-freeze-20261021-v3.json +357 -0
  284. package/docs/evidence/v02/region-seeded-heldout-v3/README.md +19 -0
  285. package/docs/evidence/v02/region-seeded-heldout-v3/evaluation-summary.json +445 -0
  286. package/docs/evidence/v02/region-seeded-method-selection-20261021-v2.json +66 -0
  287. package/docs/evidence/v02/region-seeded-method-selection-20261021-v3.json +120 -0
  288. package/docs/evidence/v02/region-segmentation-resource-0ba4c87-receipt.json +156 -0
  289. package/docs/evidence/v02/resource-soak-linux-8010aad.json +125 -0
  290. package/docs/evidence/v02/screen-v2-terminal-evaluation.md +22 -0
  291. package/docs/evidence/v02/seed-outline/run-manifest.json +59 -0
  292. package/docs/evidence/v02/seed-outline/summary.json +487 -0
  293. package/docs/evidence/v02/seed-unbounded/run-manifest.json +80 -0
  294. package/docs/evidence/v02/seed-unbounded/summary.json +730 -0
  295. package/docs/evidence/v02/segmentation/README.md +21 -0
  296. package/docs/evidence/v02/segmentation/component-comparison-summary.json +240 -0
  297. package/docs/evidence/v02/segmentation/provenance/MOBILE-SAM-LICENSE.txt +201 -0
  298. package/docs/evidence/v02/segmentation/provenance/OPENCV-LICENSE.txt +202 -0
  299. package/docs/evidence/v02/segmentation/provenance/mobile-sam-download-receipt.json +29 -0
  300. package/docs/evidence/v02/segmentation/provenance/mobile-sam-graph-audit.json +214 -0
  301. package/docs/evidence/v02/segmentation/provenance/mobile-sam-model-metadata.json +166 -0
  302. package/docs/evidence/v02/split-choice-noul-dev6.json +72 -0
  303. package/docs/evidence/v02/split-choice-noul-dev6.md +11 -0
  304. package/docs/evidence/v02/supported-vision-screen-20261020.md +65 -0
  305. package/docs/evidence/v02/wesnoth-1.16.11-minimap-projection-source-audit.md +56 -0
  306. package/docs/evidence/v02/wesnoth-proof-root-review-20261020-v1.json +76 -0
  307. package/docs/operations.md +6 -0
  308. package/docs/plan.md +2 -0
  309. package/docs/publishing.md +10 -1
  310. package/docs/research/text-transport-audit.md +66 -0
  311. package/docs/v02-usage.md +162 -0
  312. package/docs/vision-results.md +29 -2
  313. package/package.json +30 -5
package/NOTICE.md CHANGED
@@ -6,6 +6,8 @@ Models, hosted APIs, Jev and OpenRouter are third-party services; each selected
6
6
 
7
7
  Direct runtime dependencies: Zod 3.25.76 (MIT), Sharp 0.34.5 (Apache-2.0). Sharp's separately installed native packages contain libvips and other libraries with their own notices, including LGPL terms; preserve the notices shipped with those packages when redistributing them. npm resolves and installs them separately; this archive does not bundle their binaries.
8
8
 
9
+ The 0.2 optional MobileSAM adapter requires separately installed ONNX Runtime Node 1.23.2 (MIT) and separately obtained pinned Acly/MobileSAM ONNX assets. The archived MobileSAM model license is Apache-2.0. The explicit GrabCut adapter uses the separately obtained OpenCV 4.13.0 JS asset under its archived Apache-2.0 license. These assets and native binaries are not bundled or downloaded on import. See docs/evidence/v02/segmentation/provenance for license texts, source revision and artifact hashes. The library's MIT license does not replace these third-party licenses.
10
+
9
11
  Optional benchmark photographs are CC BY 2.0, Open Images annotations CC BY 4.0, and newly authored text fixtures MIT. Preserve benchmarks/corpus/ATTRIBUTION.md and each asset's attribution. Photographs and bulk upstream annotation CSVs are downloaded separately and excluded from the npm archive and source release archive.
10
12
 
11
13
  The public result JSON under docs/evidence also includes derived Open Images bounding-box annotations. These annotations remain CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/); source: Open Images V7, https://storage.googleapis.com/openimages/web/index.html. Reference coordinates were normalized/joined for evaluation. Photo pixels are not included there.
package/README.md CHANGED
@@ -1,11 +1,19 @@
1
1
  # Zorya Classifier
2
2
 
3
- Provider-independent text/image classification and coarse-to-fine object localization for Node.js and TypeScript. MIT application code. **Native decision models are the primary integration**, with Luna Decisions as the main example; ordinary vision language models are also supported without a native classification API.
3
+ [![npm version](https://img.shields.io/npm/v/zorya-classifier)](https://www.npmjs.com/package/zorya-classifier)
4
4
 
5
- Use any image-capable instruction-following model through an OpenAI-compatible API, or implement the small `DecisionPort` for another protocol. OpenRouter is an optional adapter, not a mandatory service. Model compatibility and accuracy vary; see the exact [tested models/results](docs/vision-results.md), [acceptance](docs/acceptance.md), and historical [Jev baseline](docs/results.md).
5
+ Provider-independent text/image classification and coarse-to-fine object localization for Node.js and TypeScript. MIT application code. The library never selects or connects a model for you: create a client with an explicit `DecisionPort`, either injected by your application or built with an adapter you configure. The model and provider shown in examples are examples, not defaults. Ordinary vision language models and native decision APIs are both supported.
6
+
7
+ Use an explicitly configured image-capable model through a supported adapter, or implement the small `DecisionPort` for another protocol. OpenRouter is an optional adapter, not a mandatory service. A more capable model may help on a task, but model strength does not guarantee a monotonic accuracy improvement. Compatibility and quality vary; see the exact [tested models/results](docs/vision-results.md), [acceptance](docs/acceptance.md), and historical [Jev baseline](docs/results.md).
6
8
 
7
9
  [Русское руководство](docs/README.ru.md) · [API](docs/api.md) · [Architecture](docs/architecture.md) · [Benchmarks](docs/benchmarks.md)
8
10
 
11
+ Version 0.2 adds region-seeded segmentation, mask-derived bounds and optional transparent cutouts, with a closed completeness check and one bounded mask expansion. See the [usage guide](docs/v02-usage.md) for contracts, backend ownership and CLI.
12
+
13
+ The [frozen 80-case evaluation](docs/evidence/v02/region-seeded-heldout-v3/README.md) passed its predeclared admission criteria, but localization remains limited: 26/60 positive seed hits and 8/60 complete object boxes; no false seeds or cutouts on 20 negatives. Eight of 30 reference masks reached IoU 0.8, three failed and 19 had no predicted mask. These results describe one explicit caller-configured Luna/OpenAI + MobileSAM recipe, not every model or game.
14
+
15
+ Six experimental encodings for text-only models were also tested on fresh photos and real UI captures. None qualified for image localization; see the [measured comparison](docs/evidence/v02/fresh-text/README.md). They remain research code rather than an advertised replacement for a vision model.
16
+
9
17
  ## Install
10
18
 
11
19
  Node.js 22.12+ is required. Source: [zoomplus/zorya-classifier](https://github.com/zoomplus/zorya-classifier). Package: [zorya-classifier](https://www.npmjs.com/package/zorya-classifier).
@@ -14,11 +22,11 @@ Node.js 22.12+ is required. Source: [zoomplus/zorya-classifier](https://github.c
14
22
  npm install zorya-classifier
15
23
  ```
16
24
 
17
- The release archive also installs with `npm install ./zorya-classifier-0.1.0.tgz`. Runtime dependencies are Zod and Sharp; model weights, Electron, a database and background services are not included.
25
+ The release archive also installs with `npm install ./zorya-classifier-0.1.0.tgz`. Runtime dependencies are Zod and Sharp; model weights, Electron, a database and background services are not included. Optional local segmentation backends and their model assets are a separate responsibility of the caller; the library does not download or choose them.
18
26
 
19
- ## Quickstart: Luna Decisions
27
+ ## Example: Luna Decisions
20
28
 
21
- This example uses the typed **Decisions API** with genuine Choice probabilities. Model and provider are explicit; there are no automatic retries or fallbacks. Jev is optional and is not used by this example.
29
+ This example uses the typed **Decisions API** with genuine Choice probabilities. Model and provider are explicit; there are no automatic retries or fallbacks. It illustrates one integration and is not a library default. Jev is optional and is not used by this example.
22
30
 
23
31
  ```ts
24
32
  import { createClassifier } from 'zorya-classifier'
@@ -109,6 +117,10 @@ if (result.status === 'located') console.log(result.region)
109
117
 
110
118
  The locator uses up to three sequential calls: coarse 4×4 regions, fine grids in four padded regions, then four enlarged candidates. Coordinates come from code, never free-form model coordinates. Ordinary-model rankings select candidates; independent present/absent/unknown answers govern abstention. Multiple matching physical instances are allowed; one candidate is returned. Localization requires the model/endpoint to accept multiple images per call (up to nine); single-image-only endpoints can still support `classify`.
111
119
 
120
+ ## Extract a mask-derived object
121
+
122
+ For a visible object box, center, or transparent cutout, use `extract` with an explicitly configured caller-owned segmenter. The existing region locator supplies a seed; the segmenter receives that seed while processing the full oriented source image. The library derives the returned box and center from actual mask pixels. After segmentation, the caller's decision port checks the original image with the mask-derived box and seed anchor. An incomplete box permits one enclosing-mask correction and one recheck; unresolved results remain unknown. This adds one or two closed checks to the normal region requests, without model-generated coordinates or an extent search. See the [0.2 usage guide](docs/v02-usage.md) and runnable [`extract-object.ts`](examples/extract-object.ts). The classifier and segmenter are separately caller-owned and both must be disposed.
123
+
112
124
  ## Development and limits
113
125
 
114
126
  ```sh
@@ -123,6 +135,6 @@ CLI uses explicit `ZORYA_MODEL` and defaults to `ZORYA_MODE=decisions`. OpenRout
123
135
 
124
136
  Tests are offline. Examples, CLI and explicitly authorized model benchmarks make paid calls. Default operation limits: 45-second absolute timeout, two active operations, sixteen waiters, bounded image/request/response sizes. Honor cancellation and call `dispose` on shutdown. The library has no import-time network request, hidden model switching or automatic retry.
125
137
 
126
- A model must understand the input, follow instructions and produce the checked answer shape. JSON Schema/JSON-object modes are opt-in when the chosen endpoint supports them; prompt-only mode does not require structured-output support. No universal accuracy, calibration or sub-second guarantee is asserted. Optional segmentation/refinement is a caller-owned `RefinementPort`.
138
+ A model must understand the input, follow instructions and produce the checked answer shape. JSON Schema/JSON-object modes are opt-in when the chosen endpoint supports them; prompt-only mode does not require structured-output support. No universal accuracy, calibration or sub-second guarantee is asserted. Legacy region refinement uses `RefinementPort`; the 0.2 explicit `extract` workflow uses a caller-owned `SegmentationPort`.
127
139
 
128
140
  See [CONTRIBUTING](CONTRIBUTING.md), [SECURITY](SECURITY.md), [NOTICE](NOTICE.md), and [LICENSE](LICENSE).
@@ -0,0 +1,40 @@
1
+ import { z } from 'zod';
2
+ declare const schema: z.ZodObject<{
3
+ command: z.ZodEnum<["classify-text", "classify-image", "locate", "extract"]>;
4
+ value: z.ZodString;
5
+ target: z.ZodString;
6
+ mode: z.ZodOptional<z.ZodEnum<["region", "whole-object"]>>;
7
+ output: z.ZodOptional<z.ZodString>;
8
+ backend: z.ZodOptional<z.ZodEnum<["mobile-sam", "grabcut"]>>;
9
+ encoder: z.ZodOptional<z.ZodString>;
10
+ decoder: z.ZodOptional<z.ZodString>;
11
+ opencv: z.ZodOptional<z.ZodString>;
12
+ provider: z.ZodOptional<z.ZodEnum<["cpu", "dml"]>>;
13
+ }, "strict", z.ZodTypeAny, {
14
+ value: string;
15
+ target: string;
16
+ command: "classify-text" | "classify-image" | "locate" | "extract";
17
+ mode?: "region" | "whole-object" | undefined;
18
+ provider?: "cpu" | "dml" | undefined;
19
+ output?: string | undefined;
20
+ backend?: "mobile-sam" | "grabcut" | undefined;
21
+ encoder?: string | undefined;
22
+ decoder?: string | undefined;
23
+ opencv?: string | undefined;
24
+ }, {
25
+ value: string;
26
+ target: string;
27
+ command: "classify-text" | "classify-image" | "locate" | "extract";
28
+ mode?: "region" | "whole-object" | undefined;
29
+ provider?: "cpu" | "dml" | undefined;
30
+ output?: string | undefined;
31
+ backend?: "mobile-sam" | "grabcut" | undefined;
32
+ encoder?: string | undefined;
33
+ decoder?: string | undefined;
34
+ opencv?: string | undefined;
35
+ }>;
36
+ type CliArguments = z.infer<typeof schema>;
37
+ export declare function parseCliArguments(args: string[]): CliArguments | {
38
+ command: 'help';
39
+ };
40
+ export {};
@@ -0,0 +1,67 @@
1
+ import { parseArgs } from 'node:util';
2
+ import { z } from 'zod';
3
+ import { parse } from '../entities/contracts.js';
4
+ import { ClassifierError } from '../entities/errors.js';
5
+ const schema = z
6
+ .object({
7
+ command: z.enum(['classify-text', 'classify-image', 'locate', 'extract']),
8
+ value: z.string().min(1),
9
+ target: z.string().min(1),
10
+ mode: z.enum(['region', 'whole-object']).optional(),
11
+ output: z.string().min(1).optional(),
12
+ backend: z.enum(['mobile-sam', 'grabcut']).optional(),
13
+ encoder: z.string().min(1).optional(),
14
+ decoder: z.string().min(1).optional(),
15
+ opencv: z.string().min(1).optional(),
16
+ provider: z.enum(['cpu', 'dml']).optional(),
17
+ })
18
+ .strict();
19
+ export function parseCliArguments(args) {
20
+ let raw;
21
+ try {
22
+ raw = parseArgs({
23
+ args,
24
+ allowPositionals: true,
25
+ strict: true,
26
+ options: {
27
+ help: { type: 'boolean', short: 'h' },
28
+ mode: { type: 'string' },
29
+ output: { type: 'string' },
30
+ backend: { type: 'string' },
31
+ encoder: { type: 'string' },
32
+ decoder: { type: 'string' },
33
+ opencv: { type: 'string' },
34
+ provider: { type: 'string' },
35
+ },
36
+ });
37
+ }
38
+ catch {
39
+ throw new ClassifierError('INVALID_INPUT', 'Invalid CLI arguments; use --help');
40
+ }
41
+ if (raw.values['help'] || raw.positionals.length === 0)
42
+ return { command: 'help' };
43
+ if (raw.positionals.length !== 3)
44
+ throw new ClassifierError('INVALID_INPUT', 'Expected command, input and target/classes; use --help');
45
+ const { help: _help, ...values } = raw.values;
46
+ const parsed = parse(schema, {
47
+ ...values,
48
+ command: raw.positionals[0],
49
+ value: raw.positionals[1],
50
+ target: raw.positionals[2],
51
+ });
52
+ const nativeFlags = ['backend', 'encoder', 'decoder', 'opencv', 'provider', 'output'];
53
+ if (parsed.command !== 'extract' && nativeFlags.some((key) => parsed[key] !== undefined))
54
+ throw new ClassifierError('INVALID_INPUT', 'Native/output flags apply only to extract');
55
+ if (parsed.mode !== undefined && !['locate', 'extract'].includes(parsed.command))
56
+ throw new ClassifierError('INVALID_INPUT', 'Mode applies only to image localization');
57
+ if (parsed.command === 'extract') {
58
+ if (!parsed.output || !parsed.backend || parsed.mode === 'region')
59
+ throw new ClassifierError('INVALID_INPUT', 'extract requires --output and explicit --backend, and uses whole-object mode');
60
+ if (parsed.backend === 'mobile-sam' && (!parsed.encoder || !parsed.decoder || parsed.opencv))
61
+ throw new ClassifierError('INVALID_INPUT', 'mobile-sam requires --encoder and --decoder');
62
+ if (parsed.backend === 'grabcut' &&
63
+ (!parsed.opencv || parsed.encoder || parsed.decoder || parsed.provider))
64
+ throw new ClassifierError('INVALID_INPUT', 'grabcut requires --opencv; model/provider flags do not apply');
65
+ }
66
+ return parsed;
67
+ }
@@ -1,14 +1,33 @@
1
1
  import type { ClassificationResult, ClassifyInput, DecisionPort, LocalizationResult, LocateInput, RefinementPort } from '../entities/contracts.js';
2
+ import type { AnyLocateInput, WholeObjectLocateInput, WholeObjectLocalizationResult } from '../entities/whole-object.js';
3
+ import type { WholeObjectIntervalPolicy } from '../processes/whole-object.js';
4
+ import type { ImageRepresentationPort } from '../entities/representation.js';
5
+ import type { ExtractInput, ExtractionResult, SegmentationPort } from '../entities/segmentation.js';
6
+ import type { BoundaryViewPort } from '../entities/boundary-view.js';
7
+ import type { ObjectProposalPort } from '../entities/object-proposals.js';
8
+ import type { SegmentationBoxCheckPort } from '../entities/segmentation-verification.js';
2
9
  export interface ClassifierOptions {
3
10
  decision: DecisionPort;
4
11
  timeoutMs?: number;
5
12
  concurrency?: number;
6
13
  maxQueue?: number;
14
+ maxInputBytes?: number;
7
15
  refinement?: RefinementPort;
16
+ /** Caller-owned; classifier disposal never disposes a shared segmenter. */
17
+ segmentation?: SegmentationPort;
18
+ /** Optional caller-owned containment checker; by default reuses the injected decision port. */
19
+ segmentationBoxCheck?: SegmentationBoxCheckPort;
20
+ /** Explicit, caller-owned pixel-mask proposal strategy for whole-object operations. */
21
+ objectProposals?: ObjectProposalPort;
8
22
  }
9
23
  export interface Classifier {
10
24
  classify(input: ClassifyInput): Promise<ClassificationResult>;
25
+ locate(input: WholeObjectLocateInput): Promise<WholeObjectLocalizationResult>;
11
26
  locate(input: LocateInput): Promise<LocalizationResult>;
27
+ locate(input: AnyLocateInput): Promise<LocalizationResult | WholeObjectLocalizationResult>;
28
+ extract(input: ExtractInput): Promise<ExtractionResult>;
12
29
  dispose(): Promise<void>;
13
30
  }
14
31
  export declare function createClassifier(options: ClassifierOptions): Classifier;
32
+ /** Internal benchmark composition. No research codec is exported by the npm entry points. */
33
+ export declare function createResearchClassifier(options: ClassifierOptions, experimental?: ImageRepresentationPort, boundaryView?: BoundaryViewPort, intervalPolicy?: WholeObjectIntervalPolicy): Classifier;
@@ -6,10 +6,28 @@ import { classifyInput } from '../processes/classify.js';
6
6
  import { locateInput } from '../processes/locate.js';
7
7
  import { OperationGate } from '../shared/gate.js';
8
8
  import { OperationScope } from '../shared/scope.js';
9
+ import { InputBudget } from '../shared/input-budget.js';
10
+ import { wholeObjectSchema } from '../entities/whole-object.js';
11
+ import { locateWholeObjectInput } from '../processes/whole-object.js';
12
+ import { preparedTextSchema, representationLimits } from '../entities/representation.js';
13
+ import { extractSchema } from '../entities/segmentation.js';
14
+ import { extractObject } from '../processes/extract.js';
15
+ import { extractFromRegion } from '../processes/extract-from-region.js';
16
+ import { createSharpCutoutRaster } from '../infrastructure/segmentation/cutout-raster.js';
17
+ import { locateFromProposals } from '../processes/proposal-location.js';
18
+ import { createSharpObjectProposalPresentation } from '../infrastructure/segmentation/proposal-presentation.js';
19
+ import { createSegmentationBoxCheck } from '../features/segmentation/box-check.js';
20
+ import { createSharpSegmentationBoxPresentation } from '../infrastructure/segmentation/box-presentation.js';
9
21
  const configSchema = z.object({
10
22
  timeoutMs: z.number().int().min(1).max(120000).default(45000),
11
23
  concurrency: z.number().int().min(1).max(4).default(2),
12
24
  maxQueue: z.number().int().min(0).max(32).default(16),
25
+ maxInputBytes: z
26
+ .number()
27
+ .int()
28
+ .min(1)
29
+ .max(256 * 1024 * 1024)
30
+ .default(64 * 1024 * 1024),
13
31
  });
14
32
  function signalOf(value) {
15
33
  if (value === undefined)
@@ -19,17 +37,123 @@ function signalOf(value) {
19
37
  return value;
20
38
  }
21
39
  export function createClassifier(options) {
40
+ return composeClassifier(options);
41
+ }
42
+ /** Internal benchmark composition. No research codec is exported by the npm entry points. */
43
+ export function createResearchClassifier(options, experimental, boundaryView, intervalPolicy) {
44
+ return composeClassifier(options, experimental, boundaryView, intervalPolicy);
45
+ }
46
+ function composeClassifier(options, experimental, boundaryView, intervalPolicy) {
22
47
  if (!options || typeof options.decision?.decide !== 'function')
23
48
  throw new ClassifierError('INVALID_INPUT', 'A DecisionPort is required');
49
+ const segmenter = options.segmentation;
50
+ const objectProposals = options.objectProposals;
51
+ if (segmenter !== undefined &&
52
+ (!segmenter ||
53
+ typeof segmenter.segment !== 'function' ||
54
+ (segmenter.expand !== undefined && typeof segmenter.expand !== 'function') ||
55
+ typeof segmenter.settle !== 'function' ||
56
+ typeof segmenter.dispose !== 'function'))
57
+ throw new ClassifierError('INVALID_INPUT', 'segmentation must implement SegmentationPort');
58
+ if (options.segmentationBoxCheck !== undefined &&
59
+ (!options.segmentationBoxCheck || typeof options.segmentationBoxCheck.check !== 'function'))
60
+ throw new ClassifierError('INVALID_INPUT', 'segmentationBoxCheck must implement its check port');
61
+ if (objectProposals !== undefined &&
62
+ (!objectProposals ||
63
+ typeof objectProposals.propose !== 'function' ||
64
+ typeof objectProposals.settle !== 'function' ||
65
+ typeof objectProposals.dispose !== 'function'))
66
+ throw new ClassifierError('INVALID_INPUT', 'objectProposals must implement ObjectProposalPort');
24
67
  const config = parse(configSchema, {
25
68
  timeoutMs: options.timeoutMs,
26
69
  concurrency: options.concurrency,
27
70
  maxQueue: options.maxQueue,
71
+ maxInputBytes: options.maxInputBytes,
28
72
  });
29
73
  const gate = new OperationGate(config.concurrency, config.maxQueue);
74
+ const inputBudget = new InputBudget(config.maxInputBytes);
30
75
  const raster = createSharpRaster();
76
+ const cutoutRaster = createSharpCutoutRaster();
77
+ const boxCheck = segmenter
78
+ ? (options.segmentationBoxCheck ??
79
+ createSegmentationBoxCheck({
80
+ decision: options.decision,
81
+ presentation: createSharpSegmentationBoxPresentation(),
82
+ imageState: raster.imageState,
83
+ }))
84
+ : undefined;
85
+ const proposalDependencies = objectProposals
86
+ ? {
87
+ decision: options.decision,
88
+ raster,
89
+ image: cutoutRaster,
90
+ proposals: objectProposals,
91
+ presentation: createSharpObjectProposalPresentation(),
92
+ }
93
+ : undefined;
31
94
  const owned = new Map();
32
95
  let disposed = false;
96
+ const textRepresentation = experimental
97
+ ? {
98
+ async prepare(input) {
99
+ const result = parse(preparedTextSchema, await experimental.prepare(input), 'INVALID_RESPONSE');
100
+ const bytes = result.state.reduce((sum, part) => sum + Buffer.byteLength(part, 'utf8'), 0);
101
+ if (bytes > representationLimits.totalBytes || bytes !== result.metadata.bytes)
102
+ throw new ClassifierError('INVALID_RESPONSE', 'Representation byte accounting is inconsistent');
103
+ return {
104
+ state: result.state,
105
+ metadata: {
106
+ ...result.metadata,
107
+ views: result.metadata.views.map(({ content, ...view }) => ({
108
+ ...view,
109
+ ...(content === undefined ? {} : { content }),
110
+ })),
111
+ },
112
+ };
113
+ },
114
+ }
115
+ : undefined;
116
+ function representationFor(value) {
117
+ if (value?.type !== 'experimental-text')
118
+ return undefined;
119
+ if (!textRepresentation)
120
+ throw new ClassifierError('UNSUPPORTED', 'No text-image profile has passed release qualification yet');
121
+ return textRepresentation;
122
+ }
123
+ async function locateWhole(input, signal, representation) {
124
+ if (segmenter) {
125
+ const result = await extractRegion(input, signal, representation);
126
+ if (result.status === 'extracted')
127
+ return result.location;
128
+ if (result.status === 'unknown' && result.reason === 'segmentation-uncertain')
129
+ return {
130
+ status: 'unknown',
131
+ reason: 'segmentation-uncertain',
132
+ metadata: 'metadata' in result ? result.metadata : result.location.metadata,
133
+ ...(result.verification ? { verification: result.verification } : {}),
134
+ };
135
+ return result;
136
+ }
137
+ if (proposalDependencies) {
138
+ if (representation)
139
+ throw new ClassifierError('UNSUPPORTED', 'Mask proposal selection requires vision input');
140
+ return (await locateFromProposals(input, proposalDependencies, signal)).location;
141
+ }
142
+ return locateWholeObjectInput(input, options.decision, raster, signal, representation, boundaryView, intervalPolicy);
143
+ }
144
+ function extractRegion(input, signal, representation) {
145
+ if (!segmenter || !boxCheck)
146
+ throw new ClassifierError('UNSUPPORTED', 'Region extraction requires a SegmentationPort');
147
+ if (representation)
148
+ throw new ClassifierError('UNSUPPORTED', 'Checked segmentation requires vision input');
149
+ return extractFromRegion({ ...input, signal }, {
150
+ raster,
151
+ image: cutoutRaster,
152
+ segmenter,
153
+ boxCheck,
154
+ locateRegion: (source, { signal: operationSignal, ...request }) => locateInput({ ...request, image: source.bytes }, options.decision, raster, operationSignal, undefined, representation),
155
+ });
156
+ }
33
157
  async function run(signal, task) {
34
158
  if (disposed)
35
159
  throw new ClassifierError('DISPOSED', 'Classifier has been disposed');
@@ -58,6 +182,37 @@ export function createClassifier(options) {
58
182
  owned.set(scope, job);
59
183
  return job;
60
184
  }
185
+ async function locate(input) {
186
+ if (disposed)
187
+ throw new ClassifierError('DISPOSED', 'Classifier has been disposed');
188
+ gate.assertAvailable();
189
+ if (!input || typeof input !== 'object')
190
+ throw new ClassifierError('INVALID_INPUT', 'Expected locate input object');
191
+ const { signal, ...rest } = input;
192
+ const parent = signalOf(signal);
193
+ if (rest.mode === 'whole-object') {
194
+ const parsed = parse(wholeObjectSchema, rest);
195
+ const representation = representationFor(parsed.representation);
196
+ const release = inputBudget.reserve(parsed.image.byteLength);
197
+ try {
198
+ const snapshot = { ...parsed, image: new Uint8Array(parsed.image) };
199
+ return await run(parent, (s) => locateWhole(snapshot, s, representation));
200
+ }
201
+ finally {
202
+ release();
203
+ }
204
+ }
205
+ const parsed = parse(locateSchema, rest);
206
+ const representation = representationFor(parsed.representation);
207
+ const release = inputBudget.reserve(parsed.image.byteLength);
208
+ try {
209
+ const snapshot = { ...parsed, image: new Uint8Array(parsed.image) };
210
+ return await run(parent, (s) => locateInput(snapshot, options.decision, raster, s, options.refinement, representation));
211
+ }
212
+ finally {
213
+ release();
214
+ }
215
+ }
61
216
  return {
62
217
  async classify(input) {
63
218
  if (disposed)
@@ -68,27 +223,84 @@ export function createClassifier(options) {
68
223
  const { signal, ...rest } = input;
69
224
  const parent = signalOf(signal);
70
225
  const parsed = parse(classifySchema, rest);
71
- const media = typeof parsed.input === 'string'
72
- ? parsed.input
73
- : {
74
- ...parsed.input,
75
- ...(parsed.input.image === undefined
76
- ? {}
77
- : { image: new Uint8Array(parsed.input.image) }),
78
- };
79
- return run(parent, (s) => classifyInput({ ...parsed, input: media }, options.decision, raster, s));
226
+ const hasImage = typeof parsed.input !== 'string' && parsed.input.image !== undefined;
227
+ const representation = hasImage ? representationFor(parsed.representation) : undefined;
228
+ const byteCount = typeof parsed.input === 'string'
229
+ ? Buffer.byteLength(parsed.input)
230
+ : (parsed.input.image?.byteLength ?? 0) + Buffer.byteLength(parsed.input.text ?? '');
231
+ const release = inputBudget.reserve(byteCount);
232
+ try {
233
+ const media = typeof parsed.input === 'string'
234
+ ? parsed.input
235
+ : {
236
+ ...parsed.input,
237
+ ...(parsed.input.image === undefined
238
+ ? {}
239
+ : { image: new Uint8Array(parsed.input.image) }),
240
+ };
241
+ return await run(parent, (s) => classifyInput({ ...parsed, input: media }, options.decision, raster, s, representation));
242
+ }
243
+ finally {
244
+ release();
245
+ }
80
246
  },
81
- async locate(input) {
247
+ locate,
248
+ async extract(input) {
82
249
  if (disposed)
83
250
  throw new ClassifierError('DISPOSED', 'Classifier has been disposed');
84
251
  gate.assertAvailable();
85
252
  if (!input || typeof input !== 'object')
86
- throw new ClassifierError('INVALID_INPUT', 'Expected locate input object');
253
+ throw new ClassifierError('INVALID_INPUT', 'Expected extract input object');
87
254
  const { signal, ...rest } = input;
88
255
  const parent = signalOf(signal);
89
- const parsed = parse(locateSchema, rest);
90
- const snapshot = { ...parsed, image: new Uint8Array(parsed.image) };
91
- return run(parent, (s) => locateInput(snapshot, options.decision, raster, s, options.refinement));
256
+ const parsed = parse(extractSchema, rest);
257
+ if (!segmenter && !proposalDependencies)
258
+ throw new ClassifierError('UNSUPPORTED', 'extract requires an explicit SegmentationPort or ObjectProposalPort');
259
+ const representation = representationFor(parsed.representation);
260
+ const release = inputBudget.reserve(parsed.image.byteLength);
261
+ try {
262
+ const snapshot = { ...parsed, image: new Uint8Array(parsed.image) };
263
+ return await run(parent, (s) => {
264
+ if (segmenter)
265
+ return extractRegion(snapshot, s, representation);
266
+ let prepared;
267
+ // Per-operation ownership: no global selected-mask cache or second native inference.
268
+ const selectedMask = {
269
+ async segment() {
270
+ if (!prepared?.mask || !prepared.segmentation)
271
+ throw new ClassifierError('INVALID_RESPONSE', 'Proposal selection omitted its mask');
272
+ return {
273
+ status: 'segmented',
274
+ mask: prepared.mask,
275
+ metadata: prepared.segmentation,
276
+ };
277
+ },
278
+ async settle() { },
279
+ async dispose() { },
280
+ };
281
+ return extractObject({ ...snapshot, mode: 'whole-object', signal: s }, {
282
+ raster,
283
+ image: cutoutRaster,
284
+ segmenter: selectedMask,
285
+ locateWhole: async (source, { signal: requestSignal, ...request }) => {
286
+ const wholeInput = {
287
+ ...request,
288
+ mode: 'whole-object',
289
+ image: source.bytes,
290
+ };
291
+ if (!proposalDependencies)
292
+ throw new ClassifierError('UNSUPPORTED', 'ObjectProposalPort is required');
293
+ if (representation)
294
+ throw new ClassifierError('UNSUPPORTED', 'Mask proposal selection requires vision input');
295
+ prepared = await locateFromProposals(wholeInput, proposalDependencies, requestSignal);
296
+ return prepared.location;
297
+ },
298
+ });
299
+ });
300
+ }
301
+ finally {
302
+ release();
303
+ }
92
304
  },
93
305
  async dispose() {
94
306
  disposed = true;
@@ -0,0 +1,9 @@
1
+ import type { ExtractionResult } from '../entities/segmentation.js';
2
+ /** Reserve every destination exclusively before writing; never overwrite an existing artifact. */
3
+ export declare function writeExtractionFiles(prefix: string, result: Extract<ExtractionResult, {
4
+ status: 'extracted';
5
+ }>): Promise<{
6
+ png: string;
7
+ mask: string;
8
+ descriptor: string;
9
+ }>;
@@ -0,0 +1,28 @@
1
+ import { open, unlink } from 'node:fs/promises';
2
+ /** Reserve every destination exclusively before writing; never overwrite an existing artifact. */
3
+ export async function writeExtractionFiles(prefix, result) {
4
+ const files = { png: `${prefix}.png`, mask: `${prefix}.mask.u8`, descriptor: `${prefix}.json` };
5
+ const { png, mask, ...description } = result;
6
+ const descriptor = JSON.stringify({
7
+ ...description,
8
+ mask: { width: mask.width, height: mask.height, format: mask.format },
9
+ files,
10
+ }, null, 2);
11
+ const owned = [];
12
+ try {
13
+ for (const path of Object.values(files))
14
+ owned.push({ path, handle: await open(path, 'wx') });
15
+ for (const [index, bytes] of [png, mask.data, descriptor].entries())
16
+ await owned[index].handle.writeFile(bytes);
17
+ await Promise.all(owned.map(({ handle }) => handle.close()));
18
+ return files;
19
+ }
20
+ catch (error) {
21
+ const closed = await Promise.allSettled(owned.map(({ handle }) => handle.close()));
22
+ const removed = await Promise.allSettled(owned.map(({ path }) => unlink(path)));
23
+ const cleanupErrors = [...closed, ...removed].flatMap((outcome) => outcome.status === 'rejected' ? [outcome.reason] : []);
24
+ if (cleanupErrors.length)
25
+ throw new AggregateError([error, ...cleanupErrors], 'Extraction output and cleanup failed');
26
+ throw error;
27
+ }
28
+ }
package/dist/cli.js CHANGED
@@ -2,29 +2,73 @@
2
2
  import { readFile } from 'node:fs/promises';
3
3
  import { createClassifier } from './index.js';
4
4
  import { createCliDecision } from './app/cli-route.js';
5
+ import { parseCliArguments } from './app/cli-arguments.js';
6
+ import { writeExtractionFiles } from './app/extraction-files.js';
7
+ import { createMobileSamSegmenter, createGrabCutSegmenter } from './segmentation.js';
5
8
  async function main() {
6
- const [command, value, target] = process.argv.slice(2);
7
- if (command === '--help' || !command) {
8
- console.log('zorya-classifier classify-text <text> <classes.json>\nzorya-classifier classify-image <image> <classes.json>\nzorya-classifier locate <image> <target>\nSet ZORYA_MODEL. Default ZORYA_MODE=decisions; use chat for ordinary vision models/custom chat endpoints. OpenRouter: OPENROUTER_API_KEY + ZORYA_PROVIDER. Direct: ZORYA_ENDPOINT + optional ZORYA_API_KEY. Optional ZORYA_REASONING_EFFORT and ZORYA_RESPONSE_FORMAT. One classify request; up to three locate requests.');
9
+ const args = parseCliArguments(process.argv.slice(2));
10
+ if (args.command === 'help') {
11
+ console.log('zorya-classifier classify-text <text> <classes.json>\nzorya-classifier classify-image <image> <classes.json>\nzorya-classifier locate <image> <target> [--mode region|whole-object]\nzorya-classifier extract <image> <target> --output <prefix> --backend mobile-sam --encoder <path> --decoder <path> [--provider cpu|dml]\nzorya-classifier extract <image> <target> --output <prefix> --backend grabcut --opencv <path>\nSet ZORYA_MODEL. Default ZORYA_MODE=decisions; use chat for ordinary vision models/custom chat endpoints. OpenRouter: OPENROUTER_API_KEY + ZORYA_PROVIDER. Direct: ZORYA_ENDPOINT + optional ZORYA_API_KEY. Optional ZORYA_REASONING_EFFORT and ZORYA_RESPONSE_FORMAT. One classify request; up to three region or six whole-object requests. Extraction additionally runs the explicit local backend. Output files are never overwritten; native assets are never downloaded automatically.');
9
12
  return;
10
13
  }
11
- if (!['classify-text', 'classify-image', 'locate'].includes(command) || !value || !target)
12
- throw new Error('Invalid arguments; use --help');
13
- const client = createClassifier({ decision: createCliDecision(process.env) });
14
+ const { command, value, target } = args;
15
+ const decision = createCliDecision(process.env);
16
+ const controller = new AbortController();
17
+ const abort = () => controller.abort();
18
+ process.once('SIGINT', abort);
19
+ process.once('SIGTERM', abort);
20
+ let segmenter;
21
+ let client;
14
22
  try {
15
- if (command === 'locate')
16
- console.log(JSON.stringify(await client.locate({ image: await readFile(value), target }), null, 2));
17
- else {
23
+ // Validate local data before creating a native worker or dispatching any model request.
24
+ const image = command === 'classify-text' ? undefined : await readFile(value);
25
+ let classes;
26
+ if (command === 'classify-text' || command === 'classify-image') {
18
27
  const { z } = await import('zod');
19
- const { classSchema } = await import('./entities/contracts.js');
28
+ const { classSchema, limits } = await import('./entities/contracts.js');
20
29
  const unknownClasses = JSON.parse(await readFile(target, 'utf8'));
21
- const classes = z.array(classSchema).parse(unknownClasses);
22
- const input = command === 'classify-text' ? value : { image: await readFile(value) };
23
- console.log(JSON.stringify(await client.classify({ input, classes }), null, 2));
30
+ classes = z.array(classSchema).min(2).max(limits.maxClasses).parse(unknownClasses);
31
+ }
32
+ controller.signal.throwIfAborted();
33
+ if (command === 'extract')
34
+ segmenter =
35
+ args.backend === 'mobile-sam'
36
+ ? await createMobileSamSegmenter({
37
+ encoderPath: args.encoder,
38
+ decoderPath: args.decoder,
39
+ executionProvider: args.provider ?? 'cpu',
40
+ })
41
+ : await createGrabCutSegmenter({ assetPath: args.opencv });
42
+ client = createClassifier({ decision, ...(segmenter ? { segmentation: segmenter } : {}) });
43
+ controller.signal.throwIfAborted();
44
+ if (command === 'locate') {
45
+ console.log(JSON.stringify(await client.locate({
46
+ image: image,
47
+ target,
48
+ ...(args.mode ? { mode: args.mode } : {}),
49
+ signal: controller.signal,
50
+ }), null, 2));
51
+ }
52
+ else if (command === 'extract') {
53
+ const result = await client.extract({ image: image, target, signal: controller.signal });
54
+ console.log(JSON.stringify(result.status === 'extracted'
55
+ ? { status: result.status, files: await writeExtractionFiles(args.output, result) }
56
+ : result, null, 2));
57
+ }
58
+ else {
59
+ const input = command === 'classify-text' ? value : { image: image };
60
+ console.log(JSON.stringify(await client.classify({ input, classes: classes, signal: controller.signal }), null, 2));
24
61
  }
25
62
  }
26
63
  finally {
27
- await client.dispose();
64
+ process.removeListener('SIGINT', abort);
65
+ process.removeListener('SIGTERM', abort);
66
+ try {
67
+ await client?.dispose();
68
+ }
69
+ finally {
70
+ await segmenter?.dispose();
71
+ }
28
72
  }
29
73
  }
30
74
  main().catch((error) => {