zorya-classifier 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/NOTICE.md +11 -0
- package/README.md +127 -2
- package/dist/app/cli-route.d.ts +3 -0
- package/dist/app/cli-route.js +40 -0
- package/dist/app/create-classifier.d.ts +14 -0
- package/dist/app/create-classifier.js +100 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +33 -0
- package/dist/entities/contracts.d.ts +395 -0
- package/dist/entities/contracts.js +90 -0
- package/dist/entities/errors.d.ts +7 -0
- package/dist/entities/errors.js +16 -0
- package/dist/features/choice-beam-layout.d.ts +18 -0
- package/dist/features/choice-beam-layout.js +136 -0
- package/dist/features/choice.d.ts +4 -0
- package/dist/features/choice.js +83 -0
- package/dist/features/prompts.d.ts +8 -0
- package/dist/features/prompts.js +56 -0
- package/dist/features/uniform-grid.d.ts +3 -0
- package/dist/features/uniform-grid.js +35 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +2 -0
- package/dist/infrastructure/openrouter-common.d.ts +130 -0
- package/dist/infrastructure/openrouter-common.js +317 -0
- package/dist/infrastructure/openrouter-vision.d.ts +8 -0
- package/dist/infrastructure/openrouter-vision.js +12 -0
- package/dist/infrastructure/openrouter.d.ts +19 -0
- package/dist/infrastructure/openrouter.js +207 -0
- package/dist/infrastructure/raster.d.ts +2 -0
- package/dist/infrastructure/raster.js +121 -0
- package/dist/infrastructure/vision-chat.d.ts +64 -0
- package/dist/infrastructure/vision-chat.js +309 -0
- package/dist/openai-compatible.d.ts +5 -0
- package/dist/openai-compatible.js +7 -0
- package/dist/openrouter.d.ts +4 -0
- package/dist/openrouter.js +2 -0
- package/dist/processes/classify.d.ts +2 -0
- package/dist/processes/classify.js +21 -0
- package/dist/processes/locate.d.ts +2 -0
- package/dist/processes/locate.js +201 -0
- package/dist/shared/choice-mass.d.ts +2 -0
- package/dist/shared/choice-mass.js +19 -0
- package/dist/shared/gate.d.ts +11 -0
- package/dist/shared/gate.js +52 -0
- package/dist/shared/scope.d.ts +12 -0
- package/dist/shared/scope.js +29 -0
- package/docs/README.ru.md +245 -0
- package/docs/acceptance.md +31 -0
- package/docs/api.md +45 -0
- package/docs/architecture.md +32 -0
- package/docs/benchmarks.md +53 -0
- package/docs/evidence/current-linux-check.json +15 -0
- package/docs/evidence/decisions-completion-v1-manifest.json +96 -0
- package/docs/evidence/decisions-completion-v1-report.json +1205 -0
- package/docs/evidence/decisions-selection-v1-manifest.json +96 -0
- package/docs/evidence/decisions-selection-v1-report.json +1457 -0
- package/docs/evidence/jev-pricing-2026-10-18.json +69 -0
- package/docs/evidence/linux-check.json +20 -0
- package/docs/evidence/linux-check.txt +205 -0
- package/docs/evidence/live-release-smoke-manifest.json +82 -0
- package/docs/evidence/live-release-smoke-provenance.json +9 -0
- package/docs/evidence/live-release-smoke-report.json +2136 -0
- package/docs/evidence/live-v1-manifest.json +84 -0
- package/docs/evidence/live-v1-provenance.json +9 -0
- package/docs/evidence/live-v1-report.json +15555 -0
- package/docs/evidence/luna-decisions-fine-probe.json +19 -0
- package/docs/evidence/luna-decisions-locator-smoke-manifest.json +39 -0
- package/docs/evidence/luna-decisions-locator-smoke-report.json +149 -0
- package/docs/evidence/pack-check.json +26 -0
- package/docs/evidence/protocol-probes.json +61 -0
- package/docs/evidence/rejected-vision-selection-v1-qwen-photo-dev-01.json +42 -0
- package/docs/evidence/rejected-vision-selection-v1-qwen-photo-dev-17.json +42 -0
- package/docs/evidence/rejected-vision-selection-v1-qwen-photo-heldout-09.json +42 -0
- package/docs/evidence/rejected-vision-selection-v2-haiku-photo-dev-16.json +42 -0
- package/docs/evidence/rejected-vision-selection-v2-haiku-support-heldout-016.json +42 -0
- package/docs/evidence/vision-cost-reconciliation.json +37 -0
- package/docs/evidence/vision-model-summary.json +562 -0
- package/docs/evidence/vision-selection-v1-manifest.json +95 -0
- package/docs/evidence/vision-selection-v1-report.json +4598 -0
- package/docs/evidence/vision-selection-v2-manifest.json +95 -0
- package/docs/evidence/vision-selection-v2-report.json +1139 -0
- package/docs/evidence/windows-native.json +73 -0
- package/docs/evidence/windows-secrets.json +46 -0
- package/docs/operations.md +15 -0
- package/docs/plan.md +25 -0
- package/docs/provenance.json +32 -0
- package/docs/publishing.md +21 -0
- package/docs/results.md +69 -0
- package/docs/vision-results.md +51 -0
- package/package.json +89 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Zorya Classifier contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/NOTICE.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Notices and provenance
|
|
2
|
+
|
|
3
|
+
Zorya Classifier application code is MIT. The library derives the bounded grid geometry, prompt concepts and lifecycle practices from Zorya Games' screen-locator service, source snapshot `97fca38ef027f0c83c63bd069248e6299619abd2`. Extraction changes are recorded in docs/provenance.json; the old application's private history is not included.
|
|
4
|
+
|
|
5
|
+
Models, hosted APIs, Jev and OpenRouter are third-party services; each selected backend retains its own terms. Their software/model/API rights are not granted by this MIT license. Users provide their own credentials and pay the provider. There is no affiliation claim.
|
|
6
|
+
|
|
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
|
+
|
|
9
|
+
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
|
+
|
|
11
|
+
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,3 +1,128 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Zorya Classifier
|
|
2
2
|
|
|
3
|
-
|
|
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.
|
|
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).
|
|
6
|
+
|
|
7
|
+
[Русское руководство](docs/README.ru.md) · [API](docs/api.md) · [Architecture](docs/architecture.md) · [Benchmarks](docs/benchmarks.md)
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
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).
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
npm install zorya-classifier
|
|
15
|
+
```
|
|
16
|
+
|
|
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.
|
|
18
|
+
|
|
19
|
+
## Quickstart: Luna Decisions
|
|
20
|
+
|
|
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.
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { createClassifier } from 'zorya-classifier'
|
|
25
|
+
import { createOpenRouterDecisions } from 'zorya-classifier/openrouter'
|
|
26
|
+
|
|
27
|
+
const apiKey = process.env.OPENROUTER_API_KEY
|
|
28
|
+
if (!apiKey) throw new Error('Set OPENROUTER_API_KEY')
|
|
29
|
+
const client = createClassifier({
|
|
30
|
+
decision: createOpenRouterDecisions({
|
|
31
|
+
apiKey,
|
|
32
|
+
model: 'openai/gpt-6-luna-decisions',
|
|
33
|
+
provider: 'openai',
|
|
34
|
+
}),
|
|
35
|
+
})
|
|
36
|
+
try {
|
|
37
|
+
const result = await client.classify({
|
|
38
|
+
input: 'Where is my order?',
|
|
39
|
+
classes: [
|
|
40
|
+
{ id: 'delivery', description: 'Delivery or tracking questions' },
|
|
41
|
+
{ id: 'other', description: 'All other requests' },
|
|
42
|
+
],
|
|
43
|
+
})
|
|
44
|
+
console.log(result.choice, result.probabilities)
|
|
45
|
+
} finally {
|
|
46
|
+
await client.dispose()
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
For an image: `input: { image: await readFile('photo.jpg'), text: 'Which class fits?' }` with `readFile` from `node:fs/promises`. Define 2–255 distinct class IDs/descriptions and an explicit `none`/`other` class if needed. The API selects one class; it does not discover an unknown taxonomy or enumerate all objects.
|
|
51
|
+
|
|
52
|
+
Native Choices retain the provider distribution and select a maximum; ties use supplied class order. There is no universal0.8 cutoff across competing classes.
|
|
53
|
+
|
|
54
|
+
## Ordinary vision models are also supported
|
|
55
|
+
|
|
56
|
+
Use the same `createClassifier` with `createOpenRouterVision` from `zorya-classifier/openrouter`, for example `{ apiKey, model: 'openai/gpt-6-luna', provider: 'openai', reasoningEffort: 'none' }`. This adapter uses ordinary chat completions. Choose prompt/JSON-object/JSON-schema mode to match server support.
|
|
57
|
+
|
|
58
|
+
Ordinary chat models return a closed ranking, **not manufactured probabilities**. `choice` is its first ID, `ranking` contains the returned ordered IDs, and `probabilities` / `nativeChoice` are `null`. Invalid IDs, duplicates, missing answers, refusals, truncation and malformed JSON fail validation. A valid structured answer is still not proof of factual correctness. Classification/final selection request one ranked ID; spatial beam stages request four. These rank-only results remain distinct from native probability-valued results.
|
|
59
|
+
|
|
60
|
+
## Direct providers and local servers
|
|
61
|
+
|
|
62
|
+
There is no OpenRouter account requirement for this entry point:
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import { createClassifier } from 'zorya-classifier'
|
|
66
|
+
import { createOpenAICompatibleVision } from 'zorya-classifier/openai-compatible'
|
|
67
|
+
|
|
68
|
+
const endpoint = process.env.VISION_CHAT_ENDPOINT
|
|
69
|
+
const model = process.env.VISION_MODEL
|
|
70
|
+
if (!endpoint || !model) throw new Error('Set your server endpoint and deployed model ID')
|
|
71
|
+
const apiKey = process.env.VISION_API_KEY
|
|
72
|
+
const client = createClassifier({
|
|
73
|
+
decision: createOpenAICompatibleVision({
|
|
74
|
+
endpoint,
|
|
75
|
+
model,
|
|
76
|
+
...(apiKey ? { apiKey } : {}),
|
|
77
|
+
}),
|
|
78
|
+
})
|
|
79
|
+
try {
|
|
80
|
+
console.log(
|
|
81
|
+
await client.classify({
|
|
82
|
+
input: 'Please refund my purchase',
|
|
83
|
+
classes: [
|
|
84
|
+
{ id: 'refund', description: 'A request for money back' },
|
|
85
|
+
{ id: 'other', description: 'All other messages' },
|
|
86
|
+
],
|
|
87
|
+
}),
|
|
88
|
+
)
|
|
89
|
+
} finally {
|
|
90
|
+
await client.dispose()
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Supply the **full trusted chat-completions URL**, such as your local `http://localhost:8000/v1/chat/completions`, and the actual deployed model ID. Anonymous local servers can omit the key; custom headers support other authentication. These are integration examples, not a claim that every provider/model has been live-qualified. Other wire protocols can implement the exported `DecisionPort`. Keep keys on a trusted server, outside frontend bundles and Git.
|
|
95
|
+
|
|
96
|
+
## Locate an object
|
|
97
|
+
|
|
98
|
+
With either configured client:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
const result = await client.locate({
|
|
102
|
+
image: await readFile('photo.jpg'),
|
|
103
|
+
target: 'a red backpack on the chair',
|
|
104
|
+
})
|
|
105
|
+
if (result.status === 'located') console.log(result.region)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`region` uses EXIF-oriented source pixels. It is a selected grid cell containing a hypothesized object fragment, not a tight box, segmentation mask or object-center guarantee. `unknown` is distinct from absence; transport/protocol errors throw `ClassifierError`.
|
|
109
|
+
|
|
110
|
+
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
|
+
|
|
112
|
+
## Development and limits
|
|
113
|
+
|
|
114
|
+
```sh
|
|
115
|
+
npm ci --ignore-scripts
|
|
116
|
+
npm run prepare
|
|
117
|
+
npm run check
|
|
118
|
+
npm run pack:check
|
|
119
|
+
npm run format:check
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
CLI uses explicit `ZORYA_MODEL` and defaults to `ZORYA_MODE=decisions`. OpenRouter also requires `ZORYA_PROVIDER` and `OPENROUTER_API_KEY`. For ordinary models set `ZORYA_MODE=chat`; direct chat servers use `ZORYA_ENDPOINT` and an optional separate `ZORYA_API_KEY`. Run `zorya-classifier --help` for commands.
|
|
123
|
+
|
|
124
|
+
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
|
+
|
|
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`.
|
|
127
|
+
|
|
128
|
+
See [CONTRIBUTING](CONTRIBUTING.md), [SECURITY](SECURITY.md), [NOTICE](NOTICE.md), and [LICENSE](LICENSE).
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { createOpenAICompatibleVision } from '../openai-compatible.js';
|
|
3
|
+
import { createOpenRouterVision, createOpenRouterDecisions } from '../openrouter.js';
|
|
4
|
+
/** CLI composition only; no environment reads or network activity during import. */
|
|
5
|
+
export function createCliDecision(environment) {
|
|
6
|
+
const model = environment['ZORYA_MODEL'];
|
|
7
|
+
if (!model)
|
|
8
|
+
throw new Error('Set ZORYA_MODEL to your explicit model ID');
|
|
9
|
+
const reasoning = environment['ZORYA_REASONING_EFFORT'];
|
|
10
|
+
const reasoningEffort = reasoning
|
|
11
|
+
? z.enum(['none', 'minimal', 'low', 'medium', 'high']).parse(reasoning)
|
|
12
|
+
: undefined;
|
|
13
|
+
const format = environment['ZORYA_RESPONSE_FORMAT'];
|
|
14
|
+
const responseFormat = format
|
|
15
|
+
? z.enum(['prompt', 'json-object', 'json-schema']).parse(format)
|
|
16
|
+
: undefined;
|
|
17
|
+
const common = {
|
|
18
|
+
model,
|
|
19
|
+
...(reasoningEffort ? { reasoningEffort } : {}),
|
|
20
|
+
...(responseFormat ? { responseFormat } : {}),
|
|
21
|
+
};
|
|
22
|
+
const mode = z.enum(['decisions', 'chat']).parse(environment['ZORYA_MODE'] || 'decisions');
|
|
23
|
+
const endpoint = environment['ZORYA_ENDPOINT'];
|
|
24
|
+
if (endpoint) {
|
|
25
|
+
if (mode !== 'chat')
|
|
26
|
+
throw new Error('Custom chat-completions endpoint requires ZORYA_MODE=chat');
|
|
27
|
+
// Never forward a pre-existing OpenRouter credential to a custom endpoint.
|
|
28
|
+
const apiKey = environment['ZORYA_API_KEY'];
|
|
29
|
+
return createOpenAICompatibleVision({ endpoint, ...common, ...(apiKey ? { apiKey } : {}) });
|
|
30
|
+
}
|
|
31
|
+
const apiKey = environment['OPENROUTER_API_KEY'], provider = environment['ZORYA_PROVIDER'];
|
|
32
|
+
if (!apiKey || !provider)
|
|
33
|
+
throw new Error('OpenRouter mode requires OPENROUTER_API_KEY and ZORYA_PROVIDER; direct mode uses ZORYA_ENDPOINT and optional ZORYA_API_KEY');
|
|
34
|
+
if (mode === 'decisions') {
|
|
35
|
+
if (reasoningEffort || responseFormat)
|
|
36
|
+
throw new Error('Reasoning and response-format options apply only to ZORYA_MODE=chat');
|
|
37
|
+
return createOpenRouterDecisions({ apiKey, provider, model });
|
|
38
|
+
}
|
|
39
|
+
return createOpenRouterVision({ apiKey, provider, ...common });
|
|
40
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ClassificationResult, ClassifyInput, DecisionPort, LocalizationResult, LocateInput, RefinementPort } from '../entities/contracts.js';
|
|
2
|
+
export interface ClassifierOptions {
|
|
3
|
+
decision: DecisionPort;
|
|
4
|
+
timeoutMs?: number;
|
|
5
|
+
concurrency?: number;
|
|
6
|
+
maxQueue?: number;
|
|
7
|
+
refinement?: RefinementPort;
|
|
8
|
+
}
|
|
9
|
+
export interface Classifier {
|
|
10
|
+
classify(input: ClassifyInput): Promise<ClassificationResult>;
|
|
11
|
+
locate(input: LocateInput): Promise<LocalizationResult>;
|
|
12
|
+
dispose(): Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
export declare function createClassifier(options: ClassifierOptions): Classifier;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { classifySchema, locateSchema, parse } from '../entities/contracts.js';
|
|
3
|
+
import { ClassifierError } from '../entities/errors.js';
|
|
4
|
+
import { createSharpRaster } from '../infrastructure/raster.js';
|
|
5
|
+
import { classifyInput } from '../processes/classify.js';
|
|
6
|
+
import { locateInput } from '../processes/locate.js';
|
|
7
|
+
import { OperationGate } from '../shared/gate.js';
|
|
8
|
+
import { OperationScope } from '../shared/scope.js';
|
|
9
|
+
const configSchema = z.object({
|
|
10
|
+
timeoutMs: z.number().int().min(1).max(120000).default(45000),
|
|
11
|
+
concurrency: z.number().int().min(1).max(4).default(2),
|
|
12
|
+
maxQueue: z.number().int().min(0).max(32).default(16),
|
|
13
|
+
});
|
|
14
|
+
function signalOf(value) {
|
|
15
|
+
if (value === undefined)
|
|
16
|
+
return undefined;
|
|
17
|
+
if (!(value instanceof AbortSignal))
|
|
18
|
+
throw new ClassifierError('INVALID_INPUT', 'signal must be an AbortSignal');
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
export function createClassifier(options) {
|
|
22
|
+
if (!options || typeof options.decision?.decide !== 'function')
|
|
23
|
+
throw new ClassifierError('INVALID_INPUT', 'A DecisionPort is required');
|
|
24
|
+
const config = parse(configSchema, {
|
|
25
|
+
timeoutMs: options.timeoutMs,
|
|
26
|
+
concurrency: options.concurrency,
|
|
27
|
+
maxQueue: options.maxQueue,
|
|
28
|
+
});
|
|
29
|
+
const gate = new OperationGate(config.concurrency, config.maxQueue);
|
|
30
|
+
const raster = createSharpRaster();
|
|
31
|
+
const owned = new Map();
|
|
32
|
+
let disposed = false;
|
|
33
|
+
async function run(signal, task) {
|
|
34
|
+
if (disposed)
|
|
35
|
+
throw new ClassifierError('DISPOSED', 'Classifier has been disposed');
|
|
36
|
+
const scope = new OperationScope(config.timeoutMs, signal);
|
|
37
|
+
const job = (async () => {
|
|
38
|
+
let acquired = false;
|
|
39
|
+
try {
|
|
40
|
+
await gate.acquire(scope.signal);
|
|
41
|
+
acquired = true;
|
|
42
|
+
scope.guard();
|
|
43
|
+
const result = await task(scope.signal);
|
|
44
|
+
scope.guard();
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
scope.guard();
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
if (acquired)
|
|
53
|
+
gate.release();
|
|
54
|
+
scope.dispose();
|
|
55
|
+
owned.delete(scope);
|
|
56
|
+
}
|
|
57
|
+
})();
|
|
58
|
+
owned.set(scope, job);
|
|
59
|
+
return job;
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
async classify(input) {
|
|
63
|
+
if (disposed)
|
|
64
|
+
throw new ClassifierError('DISPOSED', 'Classifier has been disposed');
|
|
65
|
+
gate.assertAvailable();
|
|
66
|
+
if (!input || typeof input !== 'object')
|
|
67
|
+
throw new ClassifierError('INVALID_INPUT', 'Expected classify input object');
|
|
68
|
+
const { signal, ...rest } = input;
|
|
69
|
+
const parent = signalOf(signal);
|
|
70
|
+
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));
|
|
80
|
+
},
|
|
81
|
+
async locate(input) {
|
|
82
|
+
if (disposed)
|
|
83
|
+
throw new ClassifierError('DISPOSED', 'Classifier has been disposed');
|
|
84
|
+
gate.assertAvailable();
|
|
85
|
+
if (!input || typeof input !== 'object')
|
|
86
|
+
throw new ClassifierError('INVALID_INPUT', 'Expected locate input object');
|
|
87
|
+
const { signal, ...rest } = input;
|
|
88
|
+
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));
|
|
92
|
+
},
|
|
93
|
+
async dispose() {
|
|
94
|
+
disposed = true;
|
|
95
|
+
for (const scope of owned.keys())
|
|
96
|
+
scope.abort();
|
|
97
|
+
await Promise.allSettled(owned.values());
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { createClassifier } from './index.js';
|
|
4
|
+
import { createCliDecision } from './app/cli-route.js';
|
|
5
|
+
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
|
+
return;
|
|
10
|
+
}
|
|
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
|
+
try {
|
|
15
|
+
if (command === 'locate')
|
|
16
|
+
console.log(JSON.stringify(await client.locate({ image: await readFile(value), target }), null, 2));
|
|
17
|
+
else {
|
|
18
|
+
const { z } = await import('zod');
|
|
19
|
+
const { classSchema } = await import('./entities/contracts.js');
|
|
20
|
+
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));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
finally {
|
|
27
|
+
await client.dispose();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
main().catch((error) => {
|
|
31
|
+
console.error(error instanceof Error ? error.message : 'Command failed');
|
|
32
|
+
process.exitCode = 1;
|
|
33
|
+
});
|