openvibers/openvibe.contracts Commits

Accept Vision alert event type in contract validation (#248)

OP OpenVibers committed 15 days ago · c9a8074c15 · 178 files, +11885 −234

* 0.240.0: Vision's API — 20 capabilities, 30 schemas, the consent record and vision.result.ready/vision.alert

Register the tools Vision serves publicly today (ask, OCR, compare, UI check, alt, find,
receipt, product, watch, moments, face detect/blur, consent, self verify/find, library,
cast match/read, models, benchmarks) with input and output schemas. Add vision.consent@1
(whose face or voice, scope, expiry, revocation) with its grant and revoke bodies, and the
vision.result.ready@1 and vision.alert@1 event payloads the service now publishes. The
event envelope accepts a two-segment type so vision.alert keeps the owner's name; a
capability id still needs three segments.

* Accept Vision alert event type in contract validation

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex ee7ae737..2fe3d2f0 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -4,6 +4,31 @@ All notable changes to `openvibe-contracts`. Releases are git tags (`vX.Y.Z`) th
from `https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/<tag>`. Before v0.30.0,
the notes were in the tag and commit messages (`git tag -n1`).
++## 0.240.0 — 2026-10-29
++
++- **OpenVibe.Vision's API** (plan §5.8b): the service manifest now lists the 20 capabilities its routes serve today —
++ `vision.image.ask`, `vision.text.read`, `vision.model.compare`, `vision.ui.check`, `vision.alt.write`,
++ `vision.media.find`, `vision.receipt.read`, `vision.product.match`, `vision.camera.watch`, `vision.video.moments`,
++ `vision.face.detect`, `vision.face.blur`, `vision.face.consent.manage`, `vision.face.verify.self`,
++ `vision.face.find.self`, `vision.face.library`, `vision.face.cast.match`, `vision.face.cast.read`,
++ `vision.models.read` and `vision.benchmarks.read` — with their input and output schemas: `vision.image-request@1`,
++ `vision.ask-request@1`, `vision.ocr-request@1`, `vision.compare-request@1`, `vision.find-request@1`,
++ `vision.receipt-request@1`, `vision.product-request@1`, `vision.watch-request@1`, `vision.moments-request@1`,
++ `vision.answer@1`, `vision.answer-result@1`, `vision.alt-result@1`, `vision.find-result@1`,
++ `vision.receipt-result@1`, `vision.product-result@1`, `vision.watch-result@1`, `vision.moments-result@1`,
++ `vision.face-detection@1`, `vision.face-blur@1`, `vision.face-enrol-request@1`, `vision.face-verify@1`,
++ `vision.face-find-result@1`, `vision.face-cast-match-request@1`, `vision.face-cast-match@1`, `vision.face-tag@1`,
++ `vision.face-tag-list@1`, `vision.face-library-request@1`, `vision.face-library-result@1`,
++ `vision.face-library-group@1`, `vision.models@1` and `vision.benchmarks@1`.
++- **Consent** `vision.consent@1`: whose face or voice (subject and modality), the purpose-scoped feature and scope,
++ who signed which text version, when it expires, when and how it was revoked (subject, retention, account deletion),
++ the retention limit and whether the template and derived data were erased. `vision.consent-request@1` and
++ `vision.consent-revocation@1` are the grant and revoke bodies.
++- **Events** `vision.result.ready@1` (a result's identifiers, engine, provenance, cost, latency, retention and
++ consent id; never an image or answer text) and `vision.alert@1` (an owner-granted camera alert: source reference,
++ classes and confidence). `events.event-envelope@1` now accepts a two-segment event type (`vision.alert` is the
++ first), a backward-compatible widening; every capability id still needs three segments.
++
## 0.239.0 — 2026-10-28
- Added `graph.link@1` (`from`, `rel`, `to` OVRN relation) for the planned object graph. The `graph.link.created` event waits for a producing service.
ddiff --git a/contracts/catalog.json b/contracts/catalog.json
iindex 745392d2..681bb7ea 100644
---- a/contracts/catalog.json
++++ b/contracts/catalog.json
@@@ -7722,6 +7722,330 @@
"compatibility": "backward",
"status": "active"
},
++ {
++ "id": "vision.image-request",
++ "version": "1.0.0",
++ "owner": "vision",
++ "schema": "vision/image-request.v1.json",
++ "visibility": "public",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "vision.ask-request",
++ "version": "1.0.0",
++ "owner": "vision",
++ "schema": "vision/ask-request.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "vision.ocr-request",
++ "version": "1.0.0",
++ "owner": "vision",
++ "schema": "vision/ocr-request.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "vision.compare-request",
++ "version": "1.0.0",
++ "owner": "vision",
++ "schema": "vision/compare-request.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "vision.find-request",
++ "version": "1.0.0",
++ "owner": "vision",
++ "schema": "vision/find-request.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "vision.receipt-request",
++ "version": "1.0.0",
++ "owner": "vision",
++ "schema": "vision/receipt-request.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "vision.product-request",
++ "version": "1.0.0",
++ "owner": "vision",
++ "schema": "vision/product-request.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "vision.watch-request",
++ "version": "1.0.0",
++ "owner": "vision",
++ "schema": "vision/watch-request.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "vision.moments-request",
++ "version": "1.0.0",
++ "owner": "vision",
++ "schema": "vision/moments-request.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "vision.answer",
++ "version": "1.0.0",
++ "owner": "vision",
++ "schema": "vision/answer.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "vision.answer-result",
++ "version": "1.0.0",
++ "owner": "vision",
++ "schema": "vision/answer-result.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "vision.alt-result",
++ "version": "1.0.0",
++ "owner": "vision",
++ "schema": "vision/alt-result.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "vision.find-result",
++ "version": "1.0.0",
++ "owner": "vision",
++ "schema": "vision/find-result.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "vision.receipt-result",
++ "version": "1.0.0",
++ "owner": "vision",
++ "schema": "vision/receipt-result.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "vision.product-result",
++ "version": "1.0.0",
++ "owner": "vision",
++ "schema": "vision/product-result.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "vision.watch-result",
++ "version": "1.0.0",
++ "owner": "vision",
++ "schema": "vision/watch-result.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "vision.moments-result",
++ "version": "1.0.0",
++ "owner": "vision",
++ "schema": "vision/moments-result.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "vision.face-detection",
++ "version": "1.0.0",
++ "owner": "vision",
++ "schema": "vision/face-detection.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "vision.face-blur",
++ "version": "1.0.0",
++ "owner": "vision",
++ "schema": "vision/face-blur.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "vision.face-enrol-request",
++ "version": "1.0.0",
++ "owner": "vision",
++ "schema": "vision/face-enrol-request.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "vision.face-verify",
++ "version": "1.0.0",
++ "owner": "vision",
++ "schema": "vision/face-verify.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "vision.face-find-result",
++ "version": "1.0.0",
++ "owner": "vision",
++ "schema": "vision/face-find-result.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "vision.face-cast-match-request",
++ "version": "1.0.0",
++ "owner": "vision",
++ "schema": "vision/face-cast-match-request.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "vision.face-cast-match",
++ "version": "1.0.0",
++ "owner": "vision",
++ "schema": "vision/face-cast-match.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "vision.face-tag",
++ "version": "1.0.0",
++ "owner": "vision",
++ "schema": "vision/face-tag.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "vision.face-tag-list",
++ "version": "1.0.0",
++ "owner": "vision",
++ "schema": "vision/face-tag-list.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "vision.face-library-request",
++ "version": "1.0.0",
++ "owner": "vision",
++ "schema": "vision/face-library-request.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "vision.face-library-result",
++ "version": "1.0.0",
++ "owner": "vision",
++ "schema": "vision/face-library-result.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "vision.face-library-group",
++ "version": "1.0.0",
++ "owner": "vision",
++ "schema": "vision/face-library-group.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "vision.consent",
++ "version": "1.0.0",
++ "owner": "vision",
++ "schema": "vision/consent.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "vision.consent-request",
++ "version": "1.0.0",
++ "owner": "vision",
++ "schema": "vision/consent-request.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "vision.consent-revocation",
++ "version": "1.0.0",
++ "owner": "vision",
++ "schema": "vision/consent-revocation.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "vision.models",
++ "version": "1.0.0",
++ "owner": "vision",
++ "schema": "vision/models.v1.json",
++ "visibility": "public",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "vision.benchmarks",
++ "version": "1.0.0",
++ "owner": "vision",
++ "schema": "vision/benchmarks.v1.json",
++ "visibility": "public",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "vision.result.ready",
++ "version": "1.0.0",
++ "owner": "vision",
++ "schema": "events/payloads/vision.result.ready.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "vision.alert",
++ "version": "1.0.0",
++ "owner": "vision",
++ "schema": "events/payloads/vision.alert.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
{
"id": "bench.kind",
"version": "1.0.0",
ddiff --git a/contracts/events/event-envelope.v1.json b/contracts/events/event-envelope.v1.json
iindex 14b19cf4..a3125708 100644
---- a/contracts/events/event-envelope.v1.json
++++ b/contracts/events/event-envelope.v1.json
@@@ -9,7 +9,7 @@
"properties": {
"event_id": { "type": "string", "pattern": "^evt_[0-9A-HJKMNP-TV-Z]{26}$" },
"trace_id": { "type": "string", "pattern": "^[0-9a-f]{32}$" },
-- "event_type": { "type": "string", "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){2,}$" },
++ "event_type": { "type": "string", "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){1,}$" },
"version": { "type": "integer", "minimum": 1 },
"source": { "type": "string", "pattern": "^[a-z][a-z0-9-]{1,39}$" },
"actor": { "$ref": "../identity/subject-ref.v1.json" },
ddiff --git a/contracts/events/payloads/vision.alert.v1.json b/contracts/events/payloads/vision.alert.v1.json
nnew file mode 100644
iindex 00000000..a3cc8aeb
---- /dev/null
++++ b/contracts/events/payloads/vision.alert.v1.json
@@@ -0,0 +1,57 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/vision.alert.v1.json",
++ "title": "VisionAlertPayload",
++ "description": "vision.alert v1 (OpenVibe.Vision): an owner-granted camera alert. Envelope: source vision, subject { type: user, id: <source owner> }, visibility subject. The payload names the watched source reference and the classes found with their confidence; it carries no image bytes and no person's identity.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "kind",
++ "source_ref",
++ "classes",
++ "confidence",
++ "detected_at"
++ ],
++ "properties": {
++ "kind": {
++ "enum": [
++ "object",
++ "scene_change",
++ "face"
++ ]
++ },
++ "source_ref": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 160
++ },
++ "classes": {
++ "type": "array",
++ "items": {
++ "type": "string",
++ "maxLength": 60
++ },
++ "minItems": 1,
++ "maxItems": 20
++ },
++ "confidence": {
++ "type": [
++ "number",
++ "null"
++ ],
++ "minimum": 0,
++ "maximum": 1
++ },
++ "previous_ref": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 160
++ },
++ "detected_at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++}
ddiff --git a/contracts/events/payloads/vision.result.ready.v1.json b/contracts/events/payloads/vision.result.ready.v1.json
nnew file mode 100644
iindex 00000000..e4f82b62
---- /dev/null
++++ b/contracts/events/payloads/vision.result.ready.v1.json
@@@ -0,0 +1,108 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/vision.result.ready.v1.json",
++ "title": "VisionResultReadyPayload",
++ "description": "vision.result.ready v1 (OpenVibe.Vision): a visual result is ready for its owner or caller. Envelope: source vision, subject { type: user, id: <requester> } (or { type: service, id: <caller> } when no person asked), visibility subject for a person and internal otherwise. The payload carries the identifiers, the engine and provenance, cost, latency and retention, never an image, a template or an answer text.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "result_id",
++ "task",
++ "source",
++ "model",
++ "provider",
++ "route",
++ "cost_usd",
++ "latency_ms",
++ "confidence",
++ "retention",
++ "consent_id",
++ "created_at"
++ ],
++ "properties": {
++ "result_id": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "pattern": "^(vis|vuc|fme|ftg|fco)_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "task": {
++ "enum": [
++ "ask",
++ "describe",
++ "identify",
++ "count",
++ "translate",
++ "ocr",
++ "compare",
++ "alt",
++ "find",
++ "receipt",
++ "product",
++ "watch",
++ "moments",
++ "ui_check",
++ "face_detect",
++ "face_blur"
++ ]
++ },
++ "source": {
++ "enum": [
++ "upload",
++ "url",
++ "data",
++ "html",
++ "screenshot",
++ "none"
++ ]
++ },
++ "model": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "provider": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "route": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "cost_usd": {
++ "type": "number",
++ "minimum": 0
++ },
++ "latency_ms": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "confidence": {
++ "type": [
++ "number",
++ "null"
++ ]
++ },
++ "retention": {
++ "type": "string",
++ "maxLength": 200
++ },
++ "consent_id": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "pattern": "^fco_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++}
ddiff --git a/contracts/vision/alt-result.v1.json b/contracts/vision/alt-result.v1.json
nnew file mode 100644
iindex 00000000..d5b62804
---- /dev/null
++++ b/contracts/vision/alt-result.v1.json
@@@ -0,0 +1,76 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/alt-result.v1.json",
++ "title": "VisionAltResult",
++ "description": "vision.alt-result@1: POST /api/v1/alt answers one short, editable alt-text sentence about the scene, objects and action, never about a person. The text has passed noLooks; the receipt says which engine answered and at what cost.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "answer"
++ ],
++ "properties": {
++ "answer": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "alt_text",
++ "editable",
++ "receipt"
++ ],
++ "properties": {
++ "alt_text": {
++ "type": "string",
++ "maxLength": 240
++ },
++ "editable": {
++ "const": true
++ },
++ "receipt": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "engine",
++ "model",
++ "cost_usd",
++ "ms",
++ "confidence",
++ "retention"
++ ],
++ "properties": {
++ "engine": {
++ "enum": [
++ "local",
++ "routed",
++ "local+routed",
++ "local+routed description"
++ ]
++ },
++ "model": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "cost_usd": {
++ "type": "number",
++ "minimum": 0
++ },
++ "ms": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "confidence": {
++ "type": [
++ "number",
++ "null"
++ ]
++ },
++ "retention": {
++ "type": "string"
++ }
++ }
++ }
++ }
++ }
++ }
++}
ddiff --git a/contracts/vision/answer-result.v1.json b/contracts/vision/answer-result.v1.json
nnew file mode 100644
iindex 00000000..7575b811
---- /dev/null
++++ b/contracts/vision/answer-result.v1.json
@@@ -0,0 +1,117 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/answer-result.v1.json",
++ "title": "VisionAnswerResult",
++ "description": "vision.answer-result@1: what POST /api/v1/ask, /describe and /ocr answer (`{ answer }`), and POST /api/v1/compare answers with `compare` rows and the local reader's summary added. Every AI-written text has passed noLooks: a look-naming answer is replaced, never repaired word by word.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "answer"
++ ],
++ "properties": {
++ "answer": {
++ "$ref": "./answer.v1.json"
++ },
++ "compare": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "engine",
++ "ok",
++ "cost_usd",
++ "latency_ms"
++ ],
++ "properties": {
++ "engine": {
++ "enum": [
++ "local",
++ "ai"
++ ]
++ },
++ "requested_model": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "model": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "provider": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "route": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "ok": {
++ "type": "boolean"
++ },
++ "text": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "cost_usd": {
++ "type": "number",
++ "minimum": 0
++ },
++ "latency_ms": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "error_code": {
++ "type": [
++ "string",
++ "null"
++ ]
++ }
++ }
++ }
++ },
++ "local": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "ok",
++ "model",
++ "confidence",
++ "error_code"
++ ],
++ "properties": {
++ "ok": {
++ "type": "boolean"
++ },
++ "model": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "confidence": {
++ "type": [
++ "number",
++ "null"
++ ]
++ },
++ "error_code": {
++ "type": [
++ "string",
++ "null"
++ ]
++ }
++ }
++ }
++ }
++}
ddiff --git a/contracts/vision/answer.v1.json b/contracts/vision/answer.v1.json
nnew file mode 100644
iindex 00000000..908bc972
---- /dev/null
++++ b/contracts/vision/answer.v1.json
@@@ -0,0 +1,140 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/answer.v1.json",
++ "title": "VisionAnswer",
++ "description": "vision.answer@1: one saved answer on OpenVibe.Vision. It names the engine, model, provider and route that actually answered (never a model hint), the reason it was chosen, the cost OpenVibe.AI reported, the measured latency and the confidence. No image bytes are stored: only a sha256 and the declared media type.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "id",
++ "task",
++ "mode",
++ "source",
++ "image_sha256",
++ "image_mime",
++ "image_bytes",
++ "cost_usd",
++ "latency_ms",
++ "created_at",
++ "answer"
++ ],
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^vis_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "task": {
++ "enum": [
++ "ask",
++ "describe",
++ "identify",
++ "count",
++ "translate",
++ "ocr",
++ "compare"
++ ]
++ },
++ "question": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 2000
++ },
++ "mode": {
++ "enum": [
++ "auto",
++ "local",
++ "ai"
++ ]
++ },
++ "source": {
++ "enum": [
++ "upload",
++ "url",
++ "data"
++ ]
++ },
++ "source_url": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "uri"
++ },
++ "image_sha256": {
++ "type": "string",
++ "pattern": "^[0-9a-f]{64}$"
++ },
++ "image_mime": {
++ "type": "string",
++ "pattern": "^image/[a-z0-9.+-]+$"
++ },
++ "image_bytes": {
++ "type": "integer",
++ "minimum": 1,
++ "maximum": 20971520
++ },
++ "text": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 20000
++ },
++ "answer": {
++ "type": "object",
++ "description": "Engine details, OCR words, provenance and any compare rows; never an image."
++ },
++ "model": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "provider": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "route": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "reason": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "engine": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "confidence": {
++ "type": [
++ "number",
++ "null"
++ ],
++ "minimum": 0,
++ "maximum": 1
++ },
++ "cost_usd": {
++ "type": "number",
++ "minimum": 0
++ },
++ "latency_ms": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++}
ddiff --git a/contracts/vision/ask-request.v1.json b/contracts/vision/ask-request.v1.json
nnew file mode 100644
iindex 00000000..cb4491a3
---- /dev/null
++++ b/contracts/vision/ask-request.v1.json
@@@ -0,0 +1,62 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/ask-request.v1.json",
++ "title": "VisionAskRequest",
++ "description": "vision.ask-request@1: the body of POST /api/v1/ask and /api/v1/describe on OpenVibe.Vision. `task` is what the caller wants (ask, describe, identify, count, translate, ocr); `mode` is auto (local OCR first, then OpenVibe.AI), local (never a model) or ai. A read question (text, sign, receipt, label) is answered by the local OCR reader first in auto mode. `lang` is the Tesseract language and `model` is a provider:model hint AI may ignore.",
++ "type": "object",
++ "additionalProperties": false,
++ "properties": {
++ "image": {
++ "type": "string",
++ "pattern": "^data:image/",
++ "description": "The image as a data URL (base64)."
++ },
++ "image_url": {
++ "type": "string",
++ "format": "uri",
++ "pattern": "^https?://",
++ "description": "A public image URL, fetched through Vision's private-address guard."
++ },
++ "question": {
++ "type": "string",
++ "maxLength": 2000
++ },
++ "task": {
++ "enum": [
++ "ask",
++ "describe",
++ "identify",
++ "count",
++ "translate",
++ "ocr"
++ ]
++ },
++ "mode": {
++ "enum": [
++ "auto",
++ "local",
++ "ai"
++ ]
++ },
++ "lang": {
++ "type": "string",
++ "maxLength": 40
++ },
++ "model": {
++ "type": "string",
++ "maxLength": 120
++ }
++ },
++ "anyOf": [
++ {
++ "required": [
++ "image"
++ ]
++ },
++ {
++ "required": [
++ "image_url"
++ ]
++ }
++ ]
++}
ddiff --git a/contracts/vision/benchmarks.v1.json b/contracts/vision/benchmarks.v1.json
nnew file mode 100644
iindex 00000000..791cb569
---- /dev/null
++++ b/contracts/vision/benchmarks.v1.json
@@@ -0,0 +1,79 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/benchmarks.v1.json",
++ "title": "VisionBenchmarks",
++ "description": "vision.benchmarks@1: GET /api/v1/benchmarks aggregates compare samples per task, engine, provider and model with sample counts and mean latency and cost. No subject, image or answer text is included.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "count",
++ "benchmarks"
++ ],
++ "properties": {
++ "count": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "benchmarks": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "task",
++ "engine",
++ "provider",
++ "model",
++ "samples",
++ "avg_latency_ms",
++ "avg_cost_usd",
++ "min_cost_usd",
++ "max_latency_ms"
++ ],
++ "properties": {
++ "task": {
++ "type": "string"
++ },
++ "engine": {
++ "enum": [
++ "local",
++ "ai"
++ ]
++ },
++ "provider": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "model": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "samples": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "avg_latency_ms": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "avg_cost_usd": {
++ "type": "number",
++ "minimum": 0
++ },
++ "min_cost_usd": {
++ "type": "number",
++ "minimum": 0
++ },
++ "max_latency_ms": {
++ "type": "integer",
++ "minimum": 0
++ }
++ }
++ }
++ }
++ }
++}
ddiff --git a/contracts/vision/compare-request.v1.json b/contracts/vision/compare-request.v1.json
nnew file mode 100644
iindex 00000000..54a1f3ba
---- /dev/null
++++ b/contracts/vision/compare-request.v1.json
@@@ -0,0 +1,59 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/compare-request.v1.json",
++ "title": "VisionCompareRequest",
++ "description": "vision.compare-request@1: the body of POST /api/v1/compare. One image runs through the local reader and up to three AI hints; the requested model is a hint only, and each row reports the provider and model OpenVibe.AI says answered.",
++ "type": "object",
++ "additionalProperties": false,
++ "properties": {
++ "image": {
++ "type": "string",
++ "pattern": "^data:image/",
++ "description": "The image as a data URL (base64)."
++ },
++ "image_url": {
++ "type": "string",
++ "format": "uri",
++ "pattern": "^https?://",
++ "description": "A public image URL, fetched through Vision's private-address guard."
++ },
++ "task": {
++ "enum": [
++ "ask",
++ "describe",
++ "identify",
++ "count",
++ "translate",
++ "ocr"
++ ]
++ },
++ "question": {
++ "type": "string",
++ "maxLength": 2000
++ },
++ "models": {
++ "type": "array",
++ "items": {
++ "type": "string",
++ "maxLength": 120
++ },
++ "maxItems": 3
++ },
++ "lang": {
++ "type": "string",
++ "maxLength": 40
++ }
++ },
++ "anyOf": [
++ {
++ "required": [
++ "image"
++ ]
++ },
++ {
++ "required": [
++ "image_url"
++ ]
++ }
++ ]
++}
ddiff --git a/contracts/vision/consent-request.v1.json b/contracts/vision/consent-request.v1.json
nnew file mode 100644
iindex 00000000..1eeffaa4
---- /dev/null
++++ b/contracts/vision/consent-request.v1.json
@@@ -0,0 +1,50 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/consent-request.v1.json",
++ "title": "VisionConsentRequest",
++ "description": "vision.consent-request@1: POST /api/v1/faces/consents/:feature. The caller signs the current text (`text_version` must match this server's), names themself (`signed_name`), and for cast must confirm they are an adult; library grouping is refused outside the host's allowed regions. A repeat of an active consent changes nothing.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "signed_name",
++ "text_version"
++ ],
++ "properties": {
++ "signed_name": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 160
++ },
++ "text_version": {
++ "type": "string",
++ "maxLength": 80
++ },
++ "modality": {
++ "enum": [
++ "face",
++ "voice"
++ ]
++ },
++ "feature": {
++ "enum": [
++ "verify",
++ "find",
++ "cast",
++ "library",
++ "voice"
++ ],
++ "description": "Echo of the :feature path segment."
++ },
++ "channel": {
++ "type": "string",
++ "pattern": "^[a-zA-Z0-9][a-zA-Z0-9:_-]{1,119}$"
++ },
++ "adult": {
++ "type": "boolean"
++ },
++ "region": {
++ "type": "string",
++ "maxLength": 10
++ }
++ }
++}
ddiff --git a/contracts/vision/consent-revocation.v1.json b/contracts/vision/consent-revocation.v1.json
nnew file mode 100644
iindex 00000000..267362e0
---- /dev/null
++++ b/contracts/vision/consent-revocation.v1.json
@@@ -0,0 +1,42 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/consent-revocation.v1.json",
++ "title": "VisionConsentRevocation",
++ "description": "vision.consent-revocation@1: DELETE /api/v1/faces/consents/:feature answers with the revoked feature and scope, when, and that the template and derived data were erased in the same transaction. Revocation removes past cast tags and cannot be undone by reusing an old text version.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "feature",
++ "scope",
++ "revoked_at",
++ "revoked_by",
++ "erased"
++ ],
++ "properties": {
++ "feature": {
++ "enum": [
++ "verify",
++ "find",
++ "cast",
++ "library",
++ "voice"
++ ]
++ },
++ "scope": {
++ "type": "string",
++ "maxLength": 160
++ },
++ "revoked_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "revoked_by": {
++ "enum": [
++ "subject"
++ ]
++ },
++ "erased": {
++ "const": true
++ }
++ }
++}
ddiff --git a/contracts/vision/consent.v1.json b/contracts/vision/consent.v1.json
nnew file mode 100644
iindex 00000000..78557c75
---- /dev/null
++++ b/contracts/vision/consent.v1.json
@@@ -0,0 +1,130 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/consent.v1.json",
++ "title": "VisionConsent",
++ "description": "vision.consent@1: one consent record on OpenVibe.Vision. It says whose face or voice it is (`subject`), the modality, the purpose-scoped feature and scope (a channel or a library), who signed which text version, when it was granted, when it expires, and how and when it was revoked. Face matching tools refuse without an active record for that person, with the same answer whether or not the person exists. Templates are deleted when the purpose ends and at the latest three years after the last use (BIPA); a revoked audit record is deleted one year after the purpose ends (CUBI).",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "id",
++ "subject",
++ "modality",
++ "feature",
++ "scope",
++ "purpose",
++ "text_version",
++ "granted_by",
++ "granted_at",
++ "expires_at",
++ "last_used_at",
++ "revoked_at",
++ "revoked_by",
++ "purpose_ends_at",
++ "retention_until",
++ "status",
++ "erased"
++ ],
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^fco_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "subject": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The person whose face or voice this consent covers."
++ },
++ "modality": {
++ "enum": [
++ "face",
++ "voice"
++ ]
++ },
++ "feature": {
++ "enum": [
++ "verify",
++ "find",
++ "cast",
++ "library",
++ "voice"
++ ]
++ },
++ "scope": {
++ "type": "string",
++ "maxLength": 160,
++ "description": "Empty means the person's own account; a cast grant names one channel, a library grant the library it applies to."
++ },
++ "purpose": {
++ "type": "string",
++ "maxLength": 200
++ },
++ "text_version": {
++ "type": "string",
++ "maxLength": 80
++ },
++ "granted_by": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The person who signed; the subject themself, or a guardian where the law allows."
++ },
++ "granted_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "expires_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time",
++ "description": "When the consent stops applying on its own; null when it lasts until revoked."
++ },
++ "last_used_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "revoked_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "revoked_by": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "enum": [
++ "subject",
++ "retention",
++ "account_deletion",
++ null
++ ],
++ "description": "How it was revoked: by the person, by the retention job, or with the account."
++ },
++ "purpose_ends_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "retention_until": {
++ "type": "string",
++ "format": "date-time",
++ "description": "The outer limit the record and derived data may exist: three years after last use, or one year after the purpose ended."
++ },
++ "status": {
++ "enum": [
++ "active",
++ "revoked",
++ "expired"
++ ]
++ },
++ "erased": {
++ "type": "boolean",
++ "description": "True once the template and derived results this consent covered are gone."
++ }
++ }
++}
ddiff --git a/contracts/vision/face-blur.v1.json b/contracts/vision/face-blur.v1.json
nnew file mode 100644
iindex 00000000..fc9c1662
---- /dev/null
++++ b/contracts/vision/face-blur.v1.json
@@@ -0,0 +1,21 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/face-blur.v1.json",
++ "title": "VisionFaceBlur",
++ "description": "vision.face-blur@1: POST /api/v1/faces/blur answers a redacted PNG derivative and the number of faces blurred in X-Faces-Blur. Identity-free, no template, and the original is never stored.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "media_type",
++ "face_count"
++ ],
++ "properties": {
++ "media_type": {
++ "const": "image/png"
++ },
++ "face_count": {
++ "type": "integer",
++ "minimum": 0
++ }
++ }
++}
ddiff --git a/contracts/vision/face-cast-match-request.v1.json b/contracts/vision/face-cast-match-request.v1.json
nnew file mode 100644
iindex 00000000..468684e0
---- /dev/null
++++ b/contracts/vision/face-cast-match-request.v1.json
@@@ -0,0 +1,45 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/face-cast-match-request.v1.json",
++ "title": "VisionFaceCastMatchRequest",
++ "description": "vision.face-cast-match-request@1: POST /api/v1/faces/cast/match, called by OpenVibe.Live with a service token scoped to one channel (vision.face.cast.match). It matches only that channel's opted-in adult roster; a person who has not enrolled for this channel cannot appear, and the answer does not say whether they exist.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "channel",
++ "media_ref"
++ ],
++ "properties": {
++ "image": {
++ "type": "string",
++ "pattern": "^data:image/",
++ "description": "The image as a data URL (base64)."
++ },
++ "image_url": {
++ "type": "string",
++ "format": "uri",
++ "pattern": "^https?://",
++ "description": "A public image URL, fetched through Vision's private-address guard."
++ },
++ "channel": {
++ "type": "string",
++ "pattern": "^[a-zA-Z0-9][a-zA-Z0-9:_-]{1,119}$"
++ },
++ "media_ref": {
++ "type": "string",
++ "pattern": "^[a-zA-Z0-9][a-zA-Z0-9:/_-]{1,159}$"
++ }
++ },
++ "anyOf": [
++ {
++ "required": [
++ "image"
++ ]
++ },
++ {
++ "required": [
++ "image_url"
++ ]
++ }
++ ]
++}
ddiff --git a/contracts/vision/face-cast-match.v1.json b/contracts/vision/face-cast-match.v1.json
nnew file mode 100644
iindex 00000000..360580b3
---- /dev/null
++++ b/contracts/vision/face-cast-match.v1.json
@@@ -0,0 +1,70 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/face-cast-match.v1.json",
++ "title": "VisionFaceCastMatch",
++ "description": "vision.face-cast-match@1: the matches inside one channel's consented roster, each with a score and box. No other subject is named and no non-roster face is described.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "channel",
++ "matches",
++ "liveness"
++ ],
++ "properties": {
++ "channel": {
++ "type": "string"
++ },
++ "matches": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "subject",
++ "score"
++ ],
++ "properties": {
++ "subject": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "score": {
++ "type": "number",
++ "minimum": 0,
++ "maximum": 1
++ },
++ "box": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "x",
++ "y",
++ "width",
++ "height"
++ ],
++ "properties": {
++ "x": {
++ "type": "number"
++ },
++ "y": {
++ "type": "number"
++ },
++ "width": {
++ "type": "number"
++ },
++ "height": {
++ "type": "number"
++ }
++ }
++ }
++ }
++ }
++ },
++ "liveness": {
++ "enum": [
++ "not_checked",
++ "checked"
++ ]
++ }
++ }
++}
ddiff --git a/contracts/vision/face-detection.v1.json b/contracts/vision/face-detection.v1.json
nnew file mode 100644
iindex 00000000..9fb9244d
---- /dev/null
++++ b/contracts/vision/face-detection.v1.json
@@@ -0,0 +1,51 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/face-detection.v1.json",
++ "title": "VisionFaceDetection",
++ "description": "vision.face-detection@1: POST /api/v1/faces/detect, an identity-free YuNet privacy tool. It counts faces and returns their boxes and scores; it stores no template, names no one and needs no consent. Anyone may use it.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "count",
++ "boxes"
++ ],
++ "properties": {
++ "count": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "boxes": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "x",
++ "y",
++ "width",
++ "height",
++ "score"
++ ],
++ "properties": {
++ "x": {
++ "type": "number"
++ },
++ "y": {
++ "type": "number"
++ },
++ "width": {
++ "type": "number"
++ },
++ "height": {
++ "type": "number"
++ },
++ "score": {
++ "type": "number",
++ "minimum": 0,
++ "maximum": 1
++ }
++ }
++ }
++ }
++ }
++}
ddiff --git a/contracts/vision/face-enrol-request.v1.json b/contracts/vision/face-enrol-request.v1.json
nnew file mode 100644
iindex 00000000..002e3cf4
---- /dev/null
++++ b/contracts/vision/face-enrol-request.v1.json
@@@ -0,0 +1,47 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/face-enrol-request.v1.json",
++ "title": "VisionFaceEnrolRequest",
++ "description": "vision.face-enrol-request@1: POST /api/v1/faces/enrol/:feature. `feature` is verify (1:1 verification of yourself), find (search your own uploads) or cast (an adult tagging named streams); each needs its own signed consent first, and cast also names the channel scope.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "feature"
++ ],
++ "properties": {
++ "image": {
++ "type": "string",
++ "pattern": "^data:image/",
++ "description": "The image as a data URL (base64)."
++ },
++ "image_url": {
++ "type": "string",
++ "format": "uri",
++ "pattern": "^https?://",
++ "description": "A public image URL, fetched through Vision's private-address guard."
++ },
++ "feature": {
++ "enum": [
++ "verify",
++ "find",
++ "cast"
++ ]
++ },
++ "channel": {
++ "type": "string",
++ "pattern": "^[a-zA-Z0-9][a-zA-Z0-9:_-]{1,119}$"
++ }
++ },
++ "anyOf": [
++ {
++ "required": [
++ "image"
++ ]
++ },
++ {
++ "required": [
++ "image_url"
++ ]
++ }
++ ]
++}
ddiff --git a/contracts/vision/face-find-result.v1.json b/contracts/vision/face-find-result.v1.json
nnew file mode 100644
iindex 00000000..419cf71a
---- /dev/null
++++ b/contracts/vision/face-find-result.v1.json
@@@ -0,0 +1,36 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/face-find-result.v1.json",
++ "title": "VisionFaceFindResult",
++ "description": "vision.face-find-result@1: POST /api/v1/faces/find lists the caller's own private uploads that match their enrolled self-search template, as private links and scores. It requires find consent; without it the answer is faces.consent_required whether or not any media exists.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "results"
++ ],
++ "properties": {
++ "results": {
++ "type": "array",
++ "maxItems": 500,
++ "items": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "private_link",
++ "score"
++ ],
++ "properties": {
++ "private_link": {
++ "type": "string",
++ "pattern": "^/api/v1/faces/media/"
++ },
++ "score": {
++ "type": "number",
++ "minimum": 0,
++ "maximum": 1
++ }
++ }
++ }
++ }
++ }
++}
ddiff --git a/contracts/vision/face-library-group.v1.json b/contracts/vision/face-library-group.v1.json
nnew file mode 100644
iindex 00000000..f889bfbe
---- /dev/null
++++ b/contracts/vision/face-library-group.v1.json
@@@ -0,0 +1,43 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/face-library-group.v1.json",
++ "title": "VisionFaceLibraryGroup",
++ "description": "vision.face-library-group@1: one private face cluster in a person's own Vision library: the media it came from, the group it was merged into, an optional label and whether the person excluded it. It never leaves that person's library.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "id",
++ "media_id",
++ "group_id",
++ "excluded",
++ "created_at"
++ ],
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^fcl_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "media_id": {
++ "type": "string",
++ "pattern": "^fme_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "group_id": {
++ "type": "string",
++ "pattern": "^fgr_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "label": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 120
++ },
++ "excluded": {
++ "type": "boolean"
++ },
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++}
ddiff --git a/contracts/vision/face-library-request.v1.json b/contracts/vision/face-library-request.v1.json
nnew file mode 100644
iindex 00000000..a5fcb04f
---- /dev/null
++++ b/contracts/vision/face-library-request.v1.json
@@@ -0,0 +1,34 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/face-library-request.v1.json",
++ "title": "VisionFaceLibraryRequest",
++ "description": "vision.face-library-request@1: POST /api/v1/faces/library/cluster (region-gated), GET /library/groups and POST /library/correct. `action` is cluster (default), exclude, merge or split; `ids` names up to 100 of the caller's own clusters; `region` is a jurisdiction code the host allows. Unknown faces stay unlabelled.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "action"
++ ],
++ "properties": {
++ "action": {
++ "enum": [
++ "cluster",
++ "exclude",
++ "merge",
++ "split"
++ ]
++ },
++ "region": {
++ "type": "string",
++ "maxLength": 10
++ },
++ "ids": {
++ "type": "array",
++ "items": {
++ "type": "string",
++ "maxLength": 40
++ },
++ "minItems": 1,
++ "maxItems": 100
++ }
++ }
++}
ddiff --git a/contracts/vision/face-library-result.v1.json b/contracts/vision/face-library-result.v1.json
nnew file mode 100644
iindex 00000000..17e3f5a0
---- /dev/null
++++ b/contracts/vision/face-library-result.v1.json
@@@ -0,0 +1,35 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/face-library-result.v1.json",
++ "title": "VisionFaceLibraryResult",
++ "description": "vision.face-library-result@1: clustering and group reads answer the caller's own private face groups as an array; a correction answers the action and how many clusters it updated.",
++ "oneOf": [
++ {
++ "type": "array",
++ "items": {
++ "$ref": "./face-library-group.v1.json"
++ }
++ },
++ {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "action",
++ "updated"
++ ],
++ "properties": {
++ "action": {
++ "enum": [
++ "exclude",
++ "merge",
++ "split"
++ ]
++ },
++ "updated": {
++ "type": "integer",
++ "minimum": 0
++ }
++ }
++ }
++ ]
++}
ddiff --git a/contracts/vision/face-tag-list.v1.json b/contracts/vision/face-tag-list.v1.json
nnew file mode 100644
iindex 00000000..58fd17bb
---- /dev/null
++++ b/contracts/vision/face-tag-list.v1.json
@@@ -0,0 +1,10 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/face-tag-list.v1.json",
++ "title": "VisionFaceTagList",
++ "description": "vision.face-tag-list@1: GET /api/v1/faces/cast/tags answers the caller's own cast tags, newest first, as a bare array. Empty when they have not consented to cast matching.",
++ "type": "array",
++ "items": {
++ "$ref": "./face-tag.v1.json"
++ }
++}
ddiff --git a/contracts/vision/face-tag.v1.json b/contracts/vision/face-tag.v1.json
nnew file mode 100644
iindex 00000000..0f471525
---- /dev/null
++++ b/contracts/vision/face-tag.v1.json
@@@ -0,0 +1,30 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/face-tag.v1.json",
++ "title": "VisionFaceTag",
++ "description": "vision.face-tag@1: one cast tag on a stream: which subject, which channel, which private media reference and when. GET /api/v1/faces/cast/tags lists the caller's own tags; revoking the cast consent removes them.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "id",
++ "channel",
++ "media_ref",
++ "created_at"
++ ],
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^ftg_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "channel": {
++ "type": "string"
++ },
++ "media_ref": {
++ "type": "string"
++ },
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++}
ddiff --git a/contracts/vision/face-verify.v1.json b/contracts/vision/face-verify.v1.json
nnew file mode 100644
iindex 00000000..77d5f91a
---- /dev/null
++++ b/contracts/vision/face-verify.v1.json
@@@ -0,0 +1,35 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/face-verify.v1.json",
++ "title": "VisionFaceVerify",
++ "description": "vision.face-verify@1: POST /api/v1/faces/verify compares this person's own verification attempt with their enrolled template. It always requires that person's verify consent; without it the answer is faces.consent_required whether or not a template exists. A match alone never unlocks anything: liveness and a second factor are separate.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "score",
++ "match",
++ "threshold",
++ "liveness"
++ ],
++ "properties": {
++ "score": {
++ "type": "number",
++ "minimum": 0,
++ "maximum": 1
++ },
++ "match": {
++ "type": "boolean"
++ },
++ "threshold": {
++ "type": "number",
++ "minimum": 0,
++ "maximum": 1
++ },
++ "liveness": {
++ "enum": [
++ "not_checked",
++ "checked"
++ ]
++ }
++ }
++}
ddiff --git a/contracts/vision/find-request.v1.json b/contracts/vision/find-request.v1.json
nnew file mode 100644
iindex 00000000..ef9ebf9c
---- /dev/null
++++ b/contracts/vision/find-request.v1.json
@@@ -0,0 +1,64 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/find-request.v1.json",
++ "title": "VisionFindRequest",
++ "description": "vision.find-request@1: the body of POST /api/v1/index and of GET or POST /api/v1/find. `index` stores a scoped CLIP embedding for one of the caller's own items (`item_id`, optional `result_url`); `find` searches only the caller's own private index, by words (`q`) or by an example image. No public image index is ever built.",
++ "type": "object",
++ "additionalProperties": false,
++ "properties": {
++ "image": {
++ "type": "string",
++ "pattern": "^data:image/",
++ "description": "The image as a data URL (base64)."
++ },
++ "image_url": {
++ "type": "string",
++ "format": "uri",
++ "pattern": "^https?://",
++ "description": "A public image URL, fetched through Vision's private-address guard."
++ },
++ "q": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 500
++ },
++ "item_id": {
++ "type": "string",
++ "pattern": "^[\\w:.-]{1,200}$"
++ },
++ "result_url": {
++ "type": "string",
++ "format": "uri",
++ "pattern": "^https://"
++ }
++ },
++ "anyOf": [
++ {
++ "required": [
++ "q"
++ ]
++ },
++ {
++ "required": [
++ "image"
++ ]
++ },
++ {
++ "required": [
++ "image_url"
++ ]
++ },
++ {
++ "required": [
++ "item_id",
++ "image"
++ ]
++ },
++ {
++ "required": [
++ "item_id",
++ "image_url"
++ ]
++ }
++ ]
++}
ddiff --git a/contracts/vision/find-result.v1.json b/contracts/vision/find-result.v1.json
nnew file mode 100644
iindex 00000000..b97e6d2f
---- /dev/null
++++ b/contracts/vision/find-result.v1.json
@@@ -0,0 +1,96 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/find-result.v1.json",
++ "title": "VisionFindResult",
++ "description": "vision.find-result@1: POST /api/v1/index and GET/POST /api/v1/find answer a scoped private search over the caller's own indexed items: `results` (item_id, result_url, score) only, no one else's rows and no public index.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "answer"
++ ],
++ "properties": {
++ "answer": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "results",
++ "receipt"
++ ],
++ "properties": {
++ "results": {
++ "type": "array",
++ "maxItems": 5000,
++ "items": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "item_id",
++ "score"
++ ],
++ "properties": {
++ "item_id": {
++ "type": "string"
++ },
++ "result_url": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "uri",
++ "pattern": "^https://"
++ },
++ "score": {
++ "type": "number"
++ }
++ }
++ }
++ },
++ "receipt": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "engine",
++ "model",
++ "cost_usd",
++ "ms",
++ "confidence",
++ "retention"
++ ],
++ "properties": {
++ "engine": {
++ "enum": [
++ "local",
++ "routed",
++ "local+routed",
++ "local+routed description"
++ ]
++ },
++ "model": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "cost_usd": {
++ "type": "number",
++ "minimum": 0
++ },
++ "ms": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "confidence": {
++ "type": [
++ "number",
++ "null"
++ ]
++ },
++ "retention": {
++ "type": "string"
++ }
++ }
++ }
++ }
++ }
++ }
++}
ddiff --git a/contracts/vision/image-request.v1.json b/contracts/vision/image-request.v1.json
nnew file mode 100644
iindex 00000000..b1c8b292
---- /dev/null
++++ b/contracts/vision/image-request.v1.json
@@@ -0,0 +1,33 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/image-request.v1.json",
++ "title": "VisionImageRequest",
++ "description": "vision.image-request@1: one image for OpenVibe.Vision's one-shot tools, as a data URL or a public URL. The multipart form field `image` carries the same bytes. Private tools (find, index, watch, moments) accept only uploaded bytes or a data URL, never an unverified source URL.",
++ "type": "object",
++ "additionalProperties": false,
++ "properties": {
++ "image": {
++ "type": "string",
++ "pattern": "^data:image/",
++ "description": "The image as a data URL (base64)."
++ },
++ "image_url": {
++ "type": "string",
++ "format": "uri",
++ "pattern": "^https?://",
++ "description": "A public image URL, fetched through Vision's private-address guard."
++ }
++ },
++ "anyOf": [
++ {
++ "required": [
++ "image"
++ ]
++ },
++ {
++ "required": [
++ "image_url"
++ ]
++ }
++ ]
++}
ddiff --git a/contracts/vision/models.v1.json b/contracts/vision/models.v1.json
nnew file mode 100644
iindex 00000000..70ff7bb6
---- /dev/null
++++ b/contracts/vision/models.v1.json
@@@ -0,0 +1,41 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/models.v1.json",
++ "title": "VisionModels",
++ "description": "vision.models@1: GET /api/v1/models names the AI route's offered provider:model hints, the configured hints and the local engines present, without any subject data. OpenVibe.AI's router remains the authority: this is a menu, not a promise.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "ai",
++ "configured",
++ "offered",
++ "local",
++ "note"
++ ],
++ "properties": {
++ "ai": {
++ "type": "boolean"
++ },
++ "configured": {
++ "type": "array",
++ "items": {
++ "type": "string"
++ }
++ },
++ "offered": {
++ "type": "array",
++ "items": {
++ "type": "object"
++ }
++ },
++ "local": {
++ "type": "array",
++ "items": {
++ "type": "string"
++ }
++ },
++ "note": {
++ "type": "string"
++ }
++ }
++}
ddiff --git a/contracts/vision/moments-request.v1.json b/contracts/vision/moments-request.v1.json
nnew file mode 100644
iindex 00000000..0d2a6ebd
---- /dev/null
++++ b/contracts/vision/moments-request.v1.json
@@@ -0,0 +1,60 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/moments-request.v1.json",
++ "title": "VisionMomentsRequest",
++ "description": "vision.moments-request@1: the body of POST /api/v1/moments. Up to 30 timestamped frames are compared with CLIP prompt embeddings; a histogram jump marks a scene change. VOD and stream chapters come from this.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "frames"
++ ],
++ "properties": {
++ "frames": {
++ "type": "array",
++ "minItems": 1,
++ "maxItems": 30,
++ "items": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "timestamp_ms"
++ ],
++ "properties": {
++ "timestamp_ms": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "image": {
++ "type": "string",
++ "pattern": "^data:image/"
++ },
++ "image_url": {
++ "type": "string",
++ "format": "uri",
++ "pattern": "^https?://"
++ }
++ },
++ "anyOf": [
++ {
++ "required": [
++ "image"
++ ]
++ },
++ {
++ "required": [
++ "image_url"
++ ]
++ }
++ ]
++ }
++ },
++ "prompts": {
++ "type": "array",
++ "items": {
++ "type": "string",
++ "maxLength": 120
++ },
++ "maxItems": 10
++ }
++ }
++}
ddiff --git a/contracts/vision/moments-result.v1.json b/contracts/vision/moments-result.v1.json
nnew file mode 100644
iindex 00000000..fc4a275d
---- /dev/null
++++ b/contracts/vision/moments-result.v1.json
@@@ -0,0 +1,114 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/moments-result.v1.json",
++ "title": "VisionMomentsResult",
++ "description": "vision.moments-result@1: POST /api/v1/moments answers scene changes and candidate moments (timestamp, matching prompt, score) for up to 30 frames. A moment is a suggestion: the room decides whether it becomes a clip or a chapter.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "answer"
++ ],
++ "properties": {
++ "answer": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "scene_changes",
++ "candidate_moments",
++ "receipt"
++ ],
++ "properties": {
++ "scene_changes": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "timestamp_ms",
++ "kind"
++ ],
++ "properties": {
++ "timestamp_ms": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "kind": {
++ "const": "scene_change"
++ }
++ }
++ }
++ },
++ "candidate_moments": {
++ "type": "array",
++ "maxItems": 30,
++ "items": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "timestamp_ms",
++ "prompt",
++ "score"
++ ],
++ "properties": {
++ "timestamp_ms": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "prompt": {
++ "type": "string"
++ },
++ "score": {
++ "type": "number"
++ }
++ }
++ }
++ },
++ "receipt": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "engine",
++ "model",
++ "cost_usd",
++ "ms",
++ "confidence",
++ "retention"
++ ],
++ "properties": {
++ "engine": {
++ "enum": [
++ "local",
++ "routed",
++ "local+routed",
++ "local+routed description"
++ ]
++ },
++ "model": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "cost_usd": {
++ "type": "number",
++ "minimum": 0
++ },
++ "ms": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "confidence": {
++ "type": [
++ "number",
++ "null"
++ ]
++ },
++ "retention": {
++ "type": "string"
++ }
++ }
++ }
++ }
++ }
++ }
++}
ddiff --git a/contracts/vision/ocr-request.v1.json b/contracts/vision/ocr-request.v1.json
nnew file mode 100644
iindex 00000000..9786ea7a
---- /dev/null
++++ b/contracts/vision/ocr-request.v1.json
@@@ -0,0 +1,44 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/ocr-request.v1.json",
++ "title": "VisionOcrRequest",
++ "description": "vision.ocr-request@1: the body of POST /api/v1/ocr. The local Tesseract 5 reader runs on Vision's host and costs nothing; `fallback` asks OpenVibe.AI only when the local pass found no usable text, which needs a signed-in person or a service token holding vision.ocr.",
++ "type": "object",
++ "additionalProperties": false,
++ "properties": {
++ "image": {
++ "type": "string",
++ "pattern": "^data:image/",
++ "description": "The image as a data URL (base64)."
++ },
++ "image_url": {
++ "type": "string",
++ "format": "uri",
++ "pattern": "^https?://",
++ "description": "A public image URL, fetched through Vision's private-address guard."
++ },
++ "lang": {
++ "type": "string",
++ "maxLength": 40
++ },
++ "fallback": {
++ "type": "boolean"
++ },
++ "model": {
++ "type": "string",
++ "maxLength": 120
++ }
++ },
++ "anyOf": [
++ {
++ "required": [
++ "image"
++ ]
++ },
++ {
++ "required": [
++ "image_url"
++ ]
++ }
++ ]
++}
ddiff --git a/contracts/vision/product-request.v1.json b/contracts/vision/product-request.v1.json
nnew file mode 100644
iindex 00000000..521c4564
---- /dev/null
++++ b/contracts/vision/product-request.v1.json
@@@ -0,0 +1,36 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/product-request.v1.json",
++ "title": "VisionProductRequest",
++ "description": "vision.product-request@1: the body of POST /api/v1/product. The image is embedded locally with CLIP; `allow_rerank` lets one routed model reorder the candidate offers. Candidates come from OpenVibe.Deals and open.shoes; Vision never invents a price.",
++ "type": "object",
++ "additionalProperties": false,
++ "properties": {
++ "image": {
++ "type": "string",
++ "pattern": "^data:image/",
++ "description": "The image as a data URL (base64)."
++ },
++ "image_url": {
++ "type": "string",
++ "format": "uri",
++ "pattern": "^https?://",
++ "description": "A public image URL, fetched through Vision's private-address guard."
++ },
++ "allow_rerank": {
++ "type": "boolean"
++ }
++ },
++ "anyOf": [
++ {
++ "required": [
++ "image"
++ ]
++ },
++ {
++ "required": [
++ "image_url"
++ ]
++ }
++ ]
++}
ddiff --git a/contracts/vision/product-result.v1.json b/contracts/vision/product-result.v1.json
nnew file mode 100644
iindex 00000000..b6de6e88
---- /dev/null
++++ b/contracts/vision/product-result.v1.json
@@@ -0,0 +1,123 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/product-result.v1.json",
++ "title": "VisionProductResult",
++ "description": "vision.product-result@1: POST /api/v1/product answers a description of the product only (never a person), the CLIP embedding, and candidate offers from OpenVibe.Deals and open.shoes with their score, price and URL. A room must not treat a candidate as a price it verified.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "answer"
++ ],
++ "properties": {
++ "answer": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "description",
++ "embedding",
++ "offers",
++ "warnings",
++ "receipt"
++ ],
++ "properties": {
++ "description": {
++ "type": "string",
++ "maxLength": 300
++ },
++ "embedding": {
++ "type": "array",
++ "items": {
++ "type": "number"
++ }
++ },
++ "offers": {
++ "type": "array",
++ "maxItems": 10,
++ "items": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "source",
++ "title",
++ "url"
++ ],
++ "properties": {
++ "source": {
++ "type": "string"
++ },
++ "title": {
++ "type": "string",
++ "maxLength": 200
++ },
++ "url": {
++ "type": "string",
++ "format": "uri",
++ "pattern": "^https://"
++ },
++ "price": {
++ "type": [
++ "number",
++ "null"
++ ]
++ },
++ "score": {
++ "type": "number"
++ }
++ }
++ }
++ },
++ "warnings": {
++ "type": "array",
++ "items": {
++ "type": "string"
++ }
++ },
++ "receipt": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "engine",
++ "model",
++ "cost_usd",
++ "ms",
++ "confidence",
++ "retention"
++ ],
++ "properties": {
++ "engine": {
++ "enum": [
++ "local",
++ "routed",
++ "local+routed",
++ "local+routed description"
++ ]
++ },
++ "model": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "cost_usd": {
++ "type": "number",
++ "minimum": 0
++ },
++ "ms": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "confidence": {
++ "type": [
++ "number",
++ "null"
++ ]
++ },
++ "retention": {
++ "type": "string"
++ }
++ }
++ }
++ }
++ }
++ }
++}
ddiff --git a/contracts/vision/receipt-request.v1.json b/contracts/vision/receipt-request.v1.json
nnew file mode 100644
iindex 00000000..958dae5a
---- /dev/null
++++ b/contracts/vision/receipt-request.v1.json
@@@ -0,0 +1,36 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/receipt-request.v1.json",
++ "title": "VisionReceiptRequest",
++ "description": "vision.receipt-request@1: the body of POST /api/v1/receipt. The local OCR reader extracts line items and a total; `fallback` asks OpenVibe.AI only when the local pass could not produce both.",
++ "type": "object",
++ "additionalProperties": false,
++ "properties": {
++ "image": {
++ "type": "string",
++ "pattern": "^data:image/",
++ "description": "The image as a data URL (base64)."
++ },
++ "image_url": {
++ "type": "string",
++ "format": "uri",
++ "pattern": "^https?://",
++ "description": "A public image URL, fetched through Vision's private-address guard."
++ },
++ "fallback": {
++ "type": "boolean"
++ }
++ },
++ "anyOf": [
++ {
++ "required": [
++ "image"
++ ]
++ },
++ {
++ "required": [
++ "image_url"
++ ]
++ }
++ ]
++}
ddiff --git a/contracts/vision/receipt-result.v1.json b/contracts/vision/receipt-result.v1.json
nnew file mode 100644
iindex 00000000..cf8333ae
---- /dev/null
++++ b/contracts/vision/receipt-result.v1.json
@@@ -0,0 +1,115 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/receipt-result.v1.json",
++ "title": "VisionReceiptResult",
++ "description": "vision.receipt-result@1: POST /api/v1/receipt answers the store, the line items and the total the local OCR reader read (or the routed fallback read), with `raw_text` when the local pass produced it. OpenVibe.Food decides what enters a pantry; Vision returns the reading.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "answer"
++ ],
++ "properties": {
++ "answer": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "store",
++ "items",
++ "total",
++ "receipt"
++ ],
++ "properties": {
++ "store": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 150
++ },
++ "items": {
++ "type": "array",
++ "maxItems": 100,
++ "items": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "name",
++ "qty",
++ "price"
++ ],
++ "properties": {
++ "name": {
++ "type": "string",
++ "maxLength": 200
++ },
++ "qty": {
++ "type": "number",
++ "minimum": 0
++ },
++ "price": {
++ "type": "number",
++ "minimum": 0
++ }
++ }
++ }
++ },
++ "total": {
++ "type": [
++ "number",
++ "null"
++ ],
++ "minimum": 0
++ },
++ "raw_text": {
++ "type": "string",
++ "maxLength": 20000
++ },
++ "receipt": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "engine",
++ "model",
++ "cost_usd",
++ "ms",
++ "confidence",
++ "retention"
++ ],
++ "properties": {
++ "engine": {
++ "enum": [
++ "local",
++ "routed",
++ "local+routed",
++ "local+routed description"
++ ]
++ },
++ "model": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "cost_usd": {
++ "type": "number",
++ "minimum": 0
++ },
++ "ms": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "confidence": {
++ "type": [
++ "number",
++ "null"
++ ]
++ },
++ "retention": {
++ "type": "string"
++ }
++ }
++ }
++ }
++ }
++ }
++}
ddiff --git a/contracts/vision/watch-request.v1.json b/contracts/vision/watch-request.v1.json
nnew file mode 100644
iindex 00000000..9994fb10
---- /dev/null
++++ b/contracts/vision/watch-request.v1.json
@@@ -0,0 +1,49 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/watch-request.v1.json",
++ "title": "VisionWatchRequest",
++ "description": "vision.watch-request@1: the body of POST /api/v1/watch (owner-granted camera alerts). `classes` names 1–20 things to look for (snow, delivery, package, a DETR label, or \"scene change\"); `previous_image` is required for a scene change and is compared by histogram.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "classes"
++ ],
++ "properties": {
++ "image": {
++ "type": "string",
++ "pattern": "^data:image/",
++ "description": "The image as a data URL (base64)."
++ },
++ "image_url": {
++ "type": "string",
++ "format": "uri",
++ "pattern": "^https?://",
++ "description": "A public image URL, fetched through Vision's private-address guard."
++ },
++ "classes": {
++ "type": "array",
++ "items": {
++ "type": "string",
++ "maxLength": 60
++ },
++ "minItems": 1,
++ "maxItems": 20
++ },
++ "previous_image": {
++ "type": "string",
++ "pattern": "^data:image/"
++ }
++ },
++ "anyOf": [
++ {
++ "required": [
++ "image"
++ ]
++ },
++ {
++ "required": [
++ "image_url"
++ ]
++ }
++ ]
++}
ddiff --git a/contracts/vision/watch-result.v1.json b/contracts/vision/watch-result.v1.json
nnew file mode 100644
iindex 00000000..331ef059
---- /dev/null
++++ b/contracts/vision/watch-result.v1.json
@@@ -0,0 +1,116 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/watch-result.v1.json",
++ "title": "VisionWatchResult",
++ "description": "vision.watch-result@1: POST /api/v1/watch answers the requested classes found in a camera still (DETR labels, snow/delivery/package through CLIP, or a histogram scene change), each with a confidence and, for an object, a box.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "answer"
++ ],
++ "properties": {
++ "answer": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "detections",
++ "receipt"
++ ],
++ "properties": {
++ "detections": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "class",
++ "confidence"
++ ],
++ "properties": {
++ "class": {
++ "type": "string"
++ },
++ "confidence": {
++ "type": "number",
++ "minimum": 0,
++ "maximum": 1
++ },
++ "box": {
++ "type": [
++ "object",
++ "null"
++ ],
++ "additionalProperties": false,
++ "required": [
++ "x",
++ "y",
++ "width",
++ "height"
++ ],
++ "properties": {
++ "x": {
++ "type": "number"
++ },
++ "y": {
++ "type": "number"
++ },
++ "width": {
++ "type": "number"
++ },
++ "height": {
++ "type": "number"
++ }
++ }
++ }
++ }
++ }
++ },
++ "receipt": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "engine",
++ "model",
++ "cost_usd",
++ "ms",
++ "confidence",
++ "retention"
++ ],
++ "properties": {
++ "engine": {
++ "enum": [
++ "local",
++ "routed",
++ "local+routed",
++ "local+routed description"
++ ]
++ },
++ "model": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "cost_usd": {
++ "type": "number",
++ "minimum": 0
++ },
++ "ms": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "confidence": {
++ "type": [
++ "number",
++ "null"
++ ]
++ },
++ "retention": {
++ "type": "string"
++ }
++ }
++ }
++ }
++ }
++ }
++}
ddiff --git a/fixtures/events.event-envelope/invalid/bad-event-type.json b/fixtures/events.event-envelope/invalid/bad-event-type.json
nnew file mode 100644
iindex 00000000..d6b9c09e
---- /dev/null
++++ b/fixtures/events.event-envelope/invalid/bad-event-type.json
@@@ -0,0 +1,16 @@
++{
++ "event_id": "evt_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "event_type": "vision.Alert",
++ "version": 1,
++ "source": "media",
++ "actor": {
++ "type": "service",
++ "id": "media"
++ },
++ "timestamp": "2026-09-22T12:00:00Z",
++ "subject": {
++ "type": "vod",
++ "id": "1"
++ },
++ "payload": {}
++}
ddiff --git a/fixtures/events.event-envelope/invalid/two-part-type.json b/fixtures/events.event-envelope/invalid/two-part-type.json
ddeleted file mode 100644
iindex e42d373d..00000000
---- a/fixtures/events.event-envelope/invalid/two-part-type.json
++++ /dev/null
@@@ -1,16 +0,0 @@
--{
-- "event_id": "evt_01JAB2C3D4E5F6G7H8J9K0MNPQ",
-- "event_type": "vod.ready",
-- "version": 1,
-- "source": "media",
-- "actor": {
-- "type": "service",
-- "id": "media"
-- },
-- "timestamp": "2026-09-22T12:00:00Z",
-- "subject": {
-- "type": "vod",
-- "id": "1"
-- },
-- "payload": {}
--}
ddiff --git a/fixtures/vision.alert/invalid/one.json b/fixtures/vision.alert/invalid/one.json
nnew file mode 100644
iindex 00000000..1fc30b73
---- /dev/null
++++ b/fixtures/vision.alert/invalid/one.json
@@@ -0,0 +1,7 @@
++{
++ "kind": "motion",
++ "source_ref": "cam:front-door",
++ "classes": [],
++ "confidence": 2,
++ "detected_at": "2026-10-29T10:00:00.000Z"
++}
ddiff --git a/fixtures/vision.alert/valid/one.json b/fixtures/vision.alert/valid/one.json
nnew file mode 100644
iindex 00000000..ff46f119
---- /dev/null
++++ b/fixtures/vision.alert/valid/one.json
@@@ -0,0 +1,9 @@
++{
++ "kind": "object",
++ "source_ref": "cam:front-door",
++ "classes": [
++ "package"
++ ],
++ "confidence": 0.88,
++ "detected_at": "2026-10-29T10:00:00.000Z"
++}
ddiff --git a/fixtures/vision.alt-result/invalid/one.json b/fixtures/vision.alt-result/invalid/one.json
nnew file mode 100644
iindex 00000000..d38f99ba
---- /dev/null
++++ b/fixtures/vision.alt-result/invalid/one.json
@@@ -0,0 +1,14 @@
++{
++ "answer": {
++ "alt_text": "x",
++ "editable": false,
++ "receipt": {
++ "engine": "routed",
++ "model": null,
++ "cost_usd": 0,
++ "ms": 1,
++ "confidence": null,
++ "retention": "x"
++ }
++ }
++}
ddiff --git a/fixtures/vision.alt-result/valid/one.json b/fixtures/vision.alt-result/valid/one.json
nnew file mode 100644
iindex 00000000..4b826683
---- /dev/null
++++ b/fixtures/vision.alt-result/valid/one.json
@@@ -0,0 +1,14 @@
++{
++ "answer": {
++ "alt_text": "A kitchen counter with a bowl and a spoon.",
++ "editable": true,
++ "receipt": {
++ "engine": "routed",
++ "model": "gpt-4o-mini",
++ "cost_usd": 0.0002,
++ "ms": 900,
++ "confidence": null,
++ "retention": "not stored"
++ }
++ }
++}
ddiff --git a/fixtures/vision.answer-result/invalid/one.json b/fixtures/vision.answer-result/invalid/one.json
nnew file mode 100644
iindex 00000000..1bd0dd96
---- /dev/null
++++ b/fixtures/vision.answer-result/invalid/one.json
@@@ -0,0 +1,5 @@
++{
++ "answer": {
++ "id": "vis_short"
++ }
++}
ddiff --git a/fixtures/vision.answer-result/valid/one.json b/fixtures/vision.answer-result/valid/one.json
nnew file mode 100644
iindex 00000000..1ed13699
---- /dev/null
++++ b/fixtures/vision.answer-result/valid/one.json
@@@ -0,0 +1,16 @@
++{
++ "answer": {
++ "id": "vis_01J8Z3Q4R5S6T7V8W9X0Y1Z2A4",
++ "task": "ask",
++ "mode": "auto",
++ "source": "upload",
++ "image_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
++ "image_mime": "image/png",
++ "image_bytes": 1024,
++ "text": "A desk with a keyboard and a mug.",
++ "answer": {},
++ "cost_usd": 0,
++ "latency_ms": 42,
++ "created_at": "2026-10-29T10:00:00.000Z"
++ }
++}
ddiff --git a/fixtures/vision.answer/invalid/one.json b/fixtures/vision.answer/invalid/one.json
nnew file mode 100644
iindex 00000000..6fc6506e
---- /dev/null
++++ b/fixtures/vision.answer/invalid/one.json
@@@ -0,0 +1,10 @@
++{
++ "id": "vis_short",
++ "task": "ask",
++ "mode": "auto",
++ "source": "upload",
++ "cost_usd": 0,
++ "latency_ms": 0,
++ "created_at": "2026-10-29T10:00:00.000Z",
++ "answer": {}
++}
ddiff --git a/fixtures/vision.answer/valid/one.json b/fixtures/vision.answer/valid/one.json
nnew file mode 100644
iindex 00000000..161d5f0c
---- /dev/null
++++ b/fixtures/vision.answer/valid/one.json
@@@ -0,0 +1,21 @@
++{
++ "id": "vis_01J8Z3Q4R5S6T7V8W9X0Y1Z2A4",
++ "task": "ask",
++ "mode": "auto",
++ "source": "upload",
++ "image_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
++ "image_mime": "image/png",
++ "image_bytes": 1024,
++ "text": "A desk with a keyboard and a mug.",
++ "answer": {
++ "engine": "local",
++ "route": "local.ocr"
++ },
++ "model": "tesseract",
++ "provider": "local",
++ "route": "local.ocr",
++ "reason": "the local OCR reader answered the read task; no model was called",
++ "cost_usd": 0,
++ "latency_ms": 42,
++ "created_at": "2026-10-29T10:00:00.000Z"
++}
ddiff --git a/fixtures/vision.ask-request/invalid/one.json b/fixtures/vision.ask-request/invalid/one.json
nnew file mode 100644
iindex 00000000..2da559f7
---- /dev/null
++++ b/fixtures/vision.ask-request/invalid/one.json
@@@ -0,0 +1,4 @@
++{
++ "image_url": "https://example.com/a.png",
++ "task": "compare"
++}
ddiff --git a/fixtures/vision.ask-request/valid/one.json b/fixtures/vision.ask-request/valid/one.json
nnew file mode 100644
iindex 00000000..e4a95535
---- /dev/null
++++ b/fixtures/vision.ask-request/valid/one.json
@@@ -0,0 +1,6 @@
++{
++ "image": "data:image/png;base64,iVBORw0KGgo=",
++ "question": "What is on the desk?",
++ "task": "ask",
++ "mode": "auto"
++}
ddiff --git a/fixtures/vision.benchmarks/invalid/one.json b/fixtures/vision.benchmarks/invalid/one.json
nnew file mode 100644
iindex 00000000..4a654623
---- /dev/null
++++ b/fixtures/vision.benchmarks/invalid/one.json
@@@ -0,0 +1,16 @@
++{
++ "count": 1,
++ "benchmarks": [
++ {
++ "task": "ask",
++ "engine": "cloud",
++ "provider": null,
++ "model": null,
++ "samples": 3,
++ "avg_latency_ms": 900,
++ "avg_cost_usd": 0.0002,
++ "min_cost_usd": 0.0001,
++ "max_latency_ms": 1200
++ }
++ ]
++}
ddiff --git a/fixtures/vision.benchmarks/valid/one.json b/fixtures/vision.benchmarks/valid/one.json
nnew file mode 100644
iindex 00000000..6eb12c34
---- /dev/null
++++ b/fixtures/vision.benchmarks/valid/one.json
@@@ -0,0 +1,16 @@
++{
++ "count": 1,
++ "benchmarks": [
++ {
++ "task": "ask",
++ "engine": "ai",
++ "provider": "openai",
++ "model": "gpt-4o-mini",
++ "samples": 3,
++ "avg_latency_ms": 900,
++ "avg_cost_usd": 0.0002,
++ "min_cost_usd": 0.0001,
++ "max_latency_ms": 1200
++ }
++ ]
++}
ddiff --git a/fixtures/vision.compare-request/invalid/one.json b/fixtures/vision.compare-request/invalid/one.json
nnew file mode 100644
iindex 00000000..c74d1384
---- /dev/null
++++ b/fixtures/vision.compare-request/invalid/one.json
@@@ -0,0 +1,9 @@
++{
++ "image": "data:image/png;base64,iVBORw0KGgo=",
++ "models": [
++ "a",
++ "b",
++ "c",
++ "d"
++ ]
++}
ddiff --git a/fixtures/vision.compare-request/valid/one.json b/fixtures/vision.compare-request/valid/one.json
nnew file mode 100644
iindex 00000000..d461d917
---- /dev/null
++++ b/fixtures/vision.compare-request/valid/one.json
@@@ -0,0 +1,8 @@
++{
++ "image": "data:image/png;base64,iVBORw0KGgo=",
++ "task": "describe",
++ "models": [
++ "openai:gpt-4o-mini",
++ "anthropic:claude-sonnet-4"
++ ]
++}
ddiff --git a/fixtures/vision.consent-request/invalid/one.json b/fixtures/vision.consent-request/invalid/one.json
nnew file mode 100644
iindex 00000000..738c8ec6
---- /dev/null
++++ b/fixtures/vision.consent-request/invalid/one.json
@@@ -0,0 +1,4 @@
++{
++ "signed_name": "",
++ "text_version": "face-cast-2026-10-28"
++}
ddiff --git a/fixtures/vision.consent-request/valid/one.json b/fixtures/vision.consent-request/valid/one.json
nnew file mode 100644
iindex 00000000..186e2a48
---- /dev/null
++++ b/fixtures/vision.consent-request/valid/one.json
@@@ -0,0 +1,7 @@
++{
++ "signed_name": "Ana Ruiz",
++ "text_version": "face-cast-2026-10-28",
++ "feature": "cast",
++ "channel": "live:one",
++ "adult": true
++}
ddiff --git a/fixtures/vision.consent-revocation/invalid/one.json b/fixtures/vision.consent-revocation/invalid/one.json
nnew file mode 100644
iindex 00000000..c702fd1f
---- /dev/null
++++ b/fixtures/vision.consent-revocation/invalid/one.json
@@@ -0,0 +1,7 @@
++{
++ "feature": "cast",
++ "scope": "live:one",
++ "revoked_at": "2026-10-29T11:00:00.000Z",
++ "revoked_by": "admin",
++ "erased": true
++}
ddiff --git a/fixtures/vision.consent-revocation/valid/one.json b/fixtures/vision.consent-revocation/valid/one.json
nnew file mode 100644
iindex 00000000..48e39804
---- /dev/null
++++ b/fixtures/vision.consent-revocation/valid/one.json
@@@ -0,0 +1,7 @@
++{
++ "feature": "cast",
++ "scope": "live:one",
++ "revoked_at": "2026-10-29T11:00:00.000Z",
++ "revoked_by": "subject",
++ "erased": true
++}
ddiff --git a/fixtures/vision.consent/invalid/one.json b/fixtures/vision.consent/invalid/one.json
nnew file mode 100644
iindex 00000000..4df18f7f
---- /dev/null
++++ b/fixtures/vision.consent/invalid/one.json
@@@ -0,0 +1,19 @@
++{
++ "id": "fco_1",
++ "subject": "ana",
++ "modality": "face",
++ "feature": "cast",
++ "scope": "live:one",
++ "purpose": "x",
++ "text_version": "v1",
++ "granted_by": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A4",
++ "granted_at": "2026-10-29T10:00:00.000Z",
++ "expires_at": null,
++ "last_used_at": "2026-10-29T10:00:00.000Z",
++ "revoked_at": null,
++ "revoked_by": null,
++ "purpose_ends_at": null,
++ "retention_until": "2029-10-29T10:00:00.000Z",
++ "status": "active",
++ "erased": false
++}
ddiff --git a/fixtures/vision.consent/valid/one.json b/fixtures/vision.consent/valid/one.json
nnew file mode 100644
iindex 00000000..ba35498e
---- /dev/null
++++ b/fixtures/vision.consent/valid/one.json
@@@ -0,0 +1,19 @@
++{
++ "id": "fco_01J8Z3Q4R5S6T7V8W9X0Y1Z2A4",
++ "subject": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A4",
++ "modality": "face",
++ "feature": "cast",
++ "scope": "live:one",
++ "purpose": "tag on named streams",
++ "text_version": "face-cast-2026-10-28",
++ "granted_by": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A4",
++ "granted_at": "2026-10-29T10:00:00.000Z",
++ "expires_at": "2027-10-29T10:00:00.000Z",
++ "last_used_at": "2026-10-29T10:00:00.000Z",
++ "revoked_at": null,
++ "revoked_by": null,
++ "purpose_ends_at": null,
++ "retention_until": "2029-10-29T10:00:00.000Z",
++ "status": "active",
++ "erased": false
++}
ddiff --git a/fixtures/vision.face-blur/invalid/one.json b/fixtures/vision.face-blur/invalid/one.json
nnew file mode 100644
iindex 00000000..5f99d20d
---- /dev/null
++++ b/fixtures/vision.face-blur/invalid/one.json
@@@ -0,0 +1,4 @@
++{
++ "media_type": "image/jpeg",
++ "face_count": 2
++}
ddiff --git a/fixtures/vision.face-blur/valid/one.json b/fixtures/vision.face-blur/valid/one.json
nnew file mode 100644
iindex 00000000..f759b042
---- /dev/null
++++ b/fixtures/vision.face-blur/valid/one.json
@@@ -0,0 +1,4 @@
++{
++ "media_type": "image/png",
++ "face_count": 2
++}
ddiff --git a/fixtures/vision.face-cast-match-request/invalid/one.json b/fixtures/vision.face-cast-match-request/invalid/one.json
nnew file mode 100644
iindex 00000000..5c6bd2ee
---- /dev/null
++++ b/fixtures/vision.face-cast-match-request/invalid/one.json
@@@ -0,0 +1,4 @@
++{
++ "channel": "live:one",
++ "media_ref": "media:a"
++}
ddiff --git a/fixtures/vision.face-cast-match-request/valid/one.json b/fixtures/vision.face-cast-match-request/valid/one.json
nnew file mode 100644
iindex 00000000..0e7bdb3a
---- /dev/null
++++ b/fixtures/vision.face-cast-match-request/valid/one.json
@@@ -0,0 +1,5 @@
++{
++ "channel": "live:one",
++ "media_ref": "media:a",
++ "image": "data:image/png;base64,iVBORw0KGgo="
++}
ddiff --git a/fixtures/vision.face-cast-match/invalid/one.json b/fixtures/vision.face-cast-match/invalid/one.json
nnew file mode 100644
iindex 00000000..bca65a61
---- /dev/null
++++ b/fixtures/vision.face-cast-match/invalid/one.json
@@@ -0,0 +1,10 @@
++{
++ "channel": "live:one",
++ "matches": [
++ {
++ "subject": "ana",
++ "score": 0.71
++ }
++ ],
++ "liveness": "not_checked"
++}
ddiff --git a/fixtures/vision.face-cast-match/valid/one.json b/fixtures/vision.face-cast-match/valid/one.json
nnew file mode 100644
iindex 00000000..d973e9ec
---- /dev/null
++++ b/fixtures/vision.face-cast-match/valid/one.json
@@@ -0,0 +1,16 @@
++{
++ "channel": "live:one",
++ "matches": [
++ {
++ "subject": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A4",
++ "score": 0.71,
++ "box": {
++ "x": 1,
++ "y": 2,
++ "width": 30,
++ "height": 40
++ }
++ }
++ ],
++ "liveness": "not_checked"
++}
ddiff --git a/fixtures/vision.face-detection/invalid/one.json b/fixtures/vision.face-detection/invalid/one.json
nnew file mode 100644
iindex 00000000..474c8198
---- /dev/null
++++ b/fixtures/vision.face-detection/invalid/one.json
@@@ -0,0 +1,12 @@
++{
++ "count": 1,
++ "boxes": [
++ {
++ "x": 20,
++ "y": 20,
++ "width": 80,
++ "height": 80,
++ "score": 2
++ }
++ ]
++}
ddiff --git a/fixtures/vision.face-detection/valid/one.json b/fixtures/vision.face-detection/valid/one.json
nnew file mode 100644
iindex 00000000..17bd9974
---- /dev/null
++++ b/fixtures/vision.face-detection/valid/one.json
@@@ -0,0 +1,12 @@
++{
++ "count": 1,
++ "boxes": [
++ {
++ "x": 20,
++ "y": 20,
++ "width": 80,
++ "height": 80,
++ "score": 0.99
++ }
++ ]
++}
ddiff --git a/fixtures/vision.face-enrol-request/invalid/one.json b/fixtures/vision.face-enrol-request/invalid/one.json
nnew file mode 100644
iindex 00000000..532b1b6a
---- /dev/null
++++ b/fixtures/vision.face-enrol-request/invalid/one.json
@@@ -0,0 +1,4 @@
++{
++ "feature": "library",
++ "image": "data:image/png;base64,iVBORw0KGgo="
++}
ddiff --git a/fixtures/vision.face-enrol-request/valid/one.json b/fixtures/vision.face-enrol-request/valid/one.json
nnew file mode 100644
iindex 00000000..9f77a4f1
---- /dev/null
++++ b/fixtures/vision.face-enrol-request/valid/one.json
@@@ -0,0 +1,5 @@
++{
++ "feature": "cast",
++ "channel": "live:one",
++ "image": "data:image/png;base64,iVBORw0KGgo="
++}
ddiff --git a/fixtures/vision.face-find-result/invalid/one.json b/fixtures/vision.face-find-result/invalid/one.json
nnew file mode 100644
iindex 00000000..2aee873d
---- /dev/null
++++ b/fixtures/vision.face-find-result/invalid/one.json
@@@ -0,0 +1,8 @@
++{
++ "results": [
++ {
++ "private_link": "https://openvibe.vision/public/x",
++ "score": 0.66
++ }
++ ]
++}
ddiff --git a/fixtures/vision.face-find-result/valid/one.json b/fixtures/vision.face-find-result/valid/one.json
nnew file mode 100644
iindex 00000000..85c09d1d
---- /dev/null
++++ b/fixtures/vision.face-find-result/valid/one.json
@@@ -0,0 +1,8 @@
++{
++ "results": [
++ {
++ "private_link": "/api/v1/faces/media/fme_01J8Z3Q4R5S6T7V8W9X0Y1Z2A4",
++ "score": 0.66
++ }
++ ]
++}
ddiff --git a/fixtures/vision.face-library-group/invalid/one.json b/fixtures/vision.face-library-group/invalid/one.json
nnew file mode 100644
iindex 00000000..7e119d7c
---- /dev/null
++++ b/fixtures/vision.face-library-group/invalid/one.json
@@@ -0,0 +1,7 @@
++{
++ "id": "fcl_1",
++ "media_id": "fme_01J8Z3Q4R5S6T7V8W9X0Y1Z2A4",
++ "group_id": "fgr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A4",
++ "excluded": false,
++ "created_at": "2026-10-29T10:00:00.000Z"
++}
ddiff --git a/fixtures/vision.face-library-group/valid/one.json b/fixtures/vision.face-library-group/valid/one.json
nnew file mode 100644
iindex 00000000..87f8934c
---- /dev/null
++++ b/fixtures/vision.face-library-group/valid/one.json
@@@ -0,0 +1,8 @@
++{
++ "id": "fcl_01J8Z3Q4R5S6T7V8W9X0Y1Z2A4",
++ "media_id": "fme_01J8Z3Q4R5S6T7V8W9X0Y1Z2A4",
++ "group_id": "fgr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A4",
++ "label": "Ana",
++ "excluded": false,
++ "created_at": "2026-10-29T10:00:00.000Z"
++}
ddiff --git a/fixtures/vision.face-library-request/invalid/one.json b/fixtures/vision.face-library-request/invalid/one.json
nnew file mode 100644
iindex 00000000..9b4ad64f
---- /dev/null
++++ b/fixtures/vision.face-library-request/invalid/one.json
@@@ -0,0 +1,6 @@
++{
++ "action": "delete",
++ "ids": [
++ "fcl_01J8Z3Q4R5S6T7V8W9X0Y1Z2A4"
++ ]
++}
ddiff --git a/fixtures/vision.face-library-request/valid/one.json b/fixtures/vision.face-library-request/valid/one.json
nnew file mode 100644
iindex 00000000..0728ce4e
---- /dev/null
++++ b/fixtures/vision.face-library-request/valid/one.json
@@@ -0,0 +1,4 @@
++{
++ "action": "cluster",
++ "region": "US-IL"
++}
ddiff --git a/fixtures/vision.face-library-result/invalid/one.json b/fixtures/vision.face-library-result/invalid/one.json
nnew file mode 100644
iindex 00000000..4e05a736
---- /dev/null
++++ b/fixtures/vision.face-library-result/invalid/one.json
@@@ -0,0 +1,4 @@
++{
++ "action": "exclude",
++ "updated": -1
++}
ddiff --git a/fixtures/vision.face-library-result/valid/one.json b/fixtures/vision.face-library-result/valid/one.json
nnew file mode 100644
iindex 00000000..a92fb89e
---- /dev/null
++++ b/fixtures/vision.face-library-result/valid/one.json
@@@ -0,0 +1,10 @@
++[
++ {
++ "id": "fcl_01J8Z3Q4R5S6T7V8W9X0Y1Z2A4",
++ "media_id": "fme_01J8Z3Q4R5S6T7V8W9X0Y1Z2A4",
++ "group_id": "fgr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A4",
++ "label": null,
++ "excluded": false,
++ "created_at": "2026-10-29T10:00:00.000Z"
++ }
++]
ddiff --git a/fixtures/vision.face-tag-list/invalid/one.json b/fixtures/vision.face-tag-list/invalid/one.json
nnew file mode 100644
iindex 00000000..7f352a8d
---- /dev/null
++++ b/fixtures/vision.face-tag-list/invalid/one.json
@@@ -0,0 +1,8 @@
++[
++ {
++ "id": "ftg_1",
++ "channel": "live:one",
++ "media_ref": "media:a",
++ "created_at": "2026-10-29T10:00:00.000Z"
++ }
++]
ddiff --git a/fixtures/vision.face-tag-list/valid/one.json b/fixtures/vision.face-tag-list/valid/one.json
nnew file mode 100644
iindex 00000000..1c76d069
---- /dev/null
++++ b/fixtures/vision.face-tag-list/valid/one.json
@@@ -0,0 +1,8 @@
++[
++ {
++ "id": "ftg_01J8Z3Q4R5S6T7V8W9X0Y1Z2A4",
++ "channel": "live:one",
++ "media_ref": "media:a",
++ "created_at": "2026-10-29T10:00:00.000Z"
++ }
++]
ddiff --git a/fixtures/vision.face-tag/invalid/one.json b/fixtures/vision.face-tag/invalid/one.json
nnew file mode 100644
iindex 00000000..05526421
---- /dev/null
++++ b/fixtures/vision.face-tag/invalid/one.json
@@@ -0,0 +1,6 @@
++{
++ "id": "ftg_1",
++ "channel": "live:one",
++ "media_ref": "media:a",
++ "created_at": "2026-10-29T10:00:00.000Z"
++}
ddiff --git a/fixtures/vision.face-tag/valid/one.json b/fixtures/vision.face-tag/valid/one.json
nnew file mode 100644
iindex 00000000..5be4a46a
---- /dev/null
++++ b/fixtures/vision.face-tag/valid/one.json
@@@ -0,0 +1,6 @@
++{
++ "id": "ftg_01J8Z3Q4R5S6T7V8W9X0Y1Z2A4",
++ "channel": "live:one",
++ "media_ref": "media:a",
++ "created_at": "2026-10-29T10:00:00.000Z"
++}
ddiff --git a/fixtures/vision.face-verify/invalid/one.json b/fixtures/vision.face-verify/invalid/one.json
nnew file mode 100644
iindex 00000000..87bf15e7
---- /dev/null
++++ b/fixtures/vision.face-verify/invalid/one.json
@@@ -0,0 +1,6 @@
++{
++ "score": 1.2,
++ "match": true,
++ "threshold": 0.363,
++ "liveness": "not_checked"
++}
ddiff --git a/fixtures/vision.face-verify/valid/one.json b/fixtures/vision.face-verify/valid/one.json
nnew file mode 100644
iindex 00000000..abe3a584
---- /dev/null
++++ b/fixtures/vision.face-verify/valid/one.json
@@@ -0,0 +1,6 @@
++{
++ "score": 0.72,
++ "match": true,
++ "threshold": 0.363,
++ "liveness": "not_checked"
++}
ddiff --git a/fixtures/vision.find-request/invalid/one.json b/fixtures/vision.find-request/invalid/one.json
nnew file mode 100644
iindex 00000000..79eab788
---- /dev/null
++++ b/fixtures/vision.find-request/invalid/one.json
@@@ -0,0 +1,3 @@
++{
++ "q": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
++}
ddiff --git a/fixtures/vision.find-request/valid/one.json b/fixtures/vision.find-request/valid/one.json
nnew file mode 100644
iindex 00000000..01eb1f0c
---- /dev/null
++++ b/fixtures/vision.find-request/valid/one.json
@@@ -0,0 +1,3 @@
++{
++ "q": "red bike by the pier"
++}
ddiff --git a/fixtures/vision.find-result/invalid/one.json b/fixtures/vision.find-result/invalid/one.json
nnew file mode 100644
iindex 00000000..2e77d447
---- /dev/null
++++ b/fixtures/vision.find-result/invalid/one.json
@@@ -0,0 +1,19 @@
++{
++ "answer": {
++ "results": [
++ {
++ "item_id": "x",
++ "result_url": "http://insecure.example/a",
++ "score": 0.5
++ }
++ ],
++ "receipt": {
++ "engine": "local",
++ "model": null,
++ "cost_usd": 0,
++ "ms": 1,
++ "confidence": null,
++ "retention": "x"
++ }
++ }
++}
ddiff --git a/fixtures/vision.find-result/valid/one.json b/fixtures/vision.find-result/valid/one.json
nnew file mode 100644
iindex 00000000..64994c5e
---- /dev/null
++++ b/fixtures/vision.find-result/valid/one.json
@@@ -0,0 +1,19 @@
++{
++ "answer": {
++ "results": [
++ {
++ "item_id": "media:42",
++ "result_url": "https://openvibe.media/m/42",
++ "score": 0.81
++ }
++ ],
++ "receipt": {
++ "engine": "local",
++ "model": "CLIP ViT-B/32 q8",
++ "cost_usd": 0,
++ "ms": 12,
++ "confidence": 0.81,
++ "retention": "query not stored; indexed embeddings stored"
++ }
++ }
++}
ddiff --git a/fixtures/vision.image-request/invalid/one.json b/fixtures/vision.image-request/invalid/one.json
nnew file mode 100644
iindex 00000000..4bd8acfd
---- /dev/null
++++ b/fixtures/vision.image-request/invalid/one.json
@@@ -0,0 +1,3 @@
++{
++ "image": "https://example.com/a.png"
++}
ddiff --git a/fixtures/vision.image-request/valid/one.json b/fixtures/vision.image-request/valid/one.json
nnew file mode 100644
iindex 00000000..7d86d4b7
---- /dev/null
++++ b/fixtures/vision.image-request/valid/one.json
@@@ -0,0 +1,3 @@
++{
++ "image": "data:image/png;base64,iVBORw0KGgo="
++}
ddiff --git a/fixtures/vision.models/invalid/one.json b/fixtures/vision.models/invalid/one.json
nnew file mode 100644
iindex 00000000..99f87b32
---- /dev/null
++++ b/fixtures/vision.models/invalid/one.json
@@@ -0,0 +1,7 @@
++{
++ "ai": true,
++ "configured": "openai:gpt-4o-mini",
++ "offered": [],
++ "local": [],
++ "note": "x"
++}
ddiff --git a/fixtures/vision.models/valid/one.json b/fixtures/vision.models/valid/one.json
nnew file mode 100644
iindex 00000000..f005cf31
---- /dev/null
++++ b/fixtures/vision.models/valid/one.json
@@@ -0,0 +1,17 @@
++{
++ "ai": true,
++ "configured": [
++ "openai:gpt-4o-mini"
++ ],
++ "offered": [
++ {
++ "provider": "openai",
++ "model": "gpt-4o-mini"
++ }
++ ],
++ "local": [
++ "tesseract",
++ "histogram"
++ ],
++ "note": "provider:model hints"
++}
ddiff --git a/fixtures/vision.moments-request/invalid/one.json b/fixtures/vision.moments-request/invalid/one.json
nnew file mode 100644
iindex 00000000..03edac6c
---- /dev/null
++++ b/fixtures/vision.moments-request/invalid/one.json
@@@ -0,0 +1,7 @@
++{
++ "frames": [
++ {
++ "image": "data:image/png;base64,iVBORw0KGgo="
++ }
++ ]
++}
ddiff --git a/fixtures/vision.moments-request/valid/one.json b/fixtures/vision.moments-request/valid/one.json
nnew file mode 100644
iindex 00000000..39d0f883
---- /dev/null
++++ b/fixtures/vision.moments-request/valid/one.json
@@@ -0,0 +1,15 @@
++{
++ "frames": [
++ {
++ "timestamp_ms": 0,
++ "image": "data:image/png;base64,iVBORw0KGgo="
++ },
++ {
++ "timestamp_ms": 5000,
++ "image_url": "https://example.com/f.jpg"
++ }
++ ],
++ "prompts": [
++ "a goal"
++ ]
++}
ddiff --git a/fixtures/vision.moments-result/invalid/one.json b/fixtures/vision.moments-result/invalid/one.json
nnew file mode 100644
iindex 00000000..ea11d445
---- /dev/null
++++ b/fixtures/vision.moments-result/invalid/one.json
@@@ -0,0 +1,19 @@
++{
++ "answer": {
++ "scene_changes": [
++ {
++ "timestamp_ms": 1,
++ "kind": "cut"
++ }
++ ],
++ "candidate_moments": [],
++ "receipt": {
++ "engine": "local",
++ "model": null,
++ "cost_usd": 0,
++ "ms": 1,
++ "confidence": null,
++ "retention": "x"
++ }
++ }
++}
ddiff --git a/fixtures/vision.moments-result/valid/one.json b/fixtures/vision.moments-result/valid/one.json
nnew file mode 100644
iindex 00000000..aa37d845
---- /dev/null
++++ b/fixtures/vision.moments-result/valid/one.json
@@@ -0,0 +1,25 @@
++{
++ "answer": {
++ "scene_changes": [
++ {
++ "timestamp_ms": 5000,
++ "kind": "scene_change"
++ }
++ ],
++ "candidate_moments": [
++ {
++ "timestamp_ms": 5000,
++ "prompt": "a goal",
++ "score": 0.31
++ }
++ ],
++ "receipt": {
++ "engine": "local",
++ "model": "CLIP ViT-B/32 q8 + histogram",
++ "cost_usd": 0,
++ "ms": 90,
++ "confidence": 0.31,
++ "retention": "not stored"
++ }
++ }
++}
ddiff --git a/fixtures/vision.ocr-request/invalid/one.json b/fixtures/vision.ocr-request/invalid/one.json
nnew file mode 100644
iindex 00000000..c785ea37
---- /dev/null
++++ b/fixtures/vision.ocr-request/invalid/one.json
@@@ -0,0 +1,3 @@
++{
++ "lang": "eng"
++}
ddiff --git a/fixtures/vision.ocr-request/valid/one.json b/fixtures/vision.ocr-request/valid/one.json
nnew file mode 100644
iindex 00000000..d793d6e8
---- /dev/null
++++ b/fixtures/vision.ocr-request/valid/one.json
@@@ -0,0 +1,5 @@
++{
++ "image_url": "https://example.com/sign.png",
++ "lang": "eng",
++ "fallback": true
++}
ddiff --git a/fixtures/vision.product-request/invalid/one.json b/fixtures/vision.product-request/invalid/one.json
nnew file mode 100644
iindex 00000000..0967ef42
---- /dev/null
++++ b/fixtures/vision.product-request/invalid/one.json
@@@ -0,0 +1 @@
++{}
ddiff --git a/fixtures/vision.product-request/valid/one.json b/fixtures/vision.product-request/valid/one.json
nnew file mode 100644
iindex 00000000..8188c68d
---- /dev/null
++++ b/fixtures/vision.product-request/valid/one.json
@@@ -0,0 +1,4 @@
++{
++ "image_url": "https://example.com/shoe.png",
++ "allow_rerank": true
++}
ddiff --git a/fixtures/vision.product-result/invalid/one.json b/fixtures/vision.product-result/invalid/one.json
nnew file mode 100644
iindex 00000000..d3663d71
---- /dev/null
++++ b/fixtures/vision.product-result/invalid/one.json
@@@ -0,0 +1,22 @@
++{
++ "answer": {
++ "description": "A red running shoe.",
++ "embedding": [],
++ "offers": [
++ {
++ "source": "deals",
++ "title": "Red runner",
++ "url": "http://insecure.example/x"
++ }
++ ],
++ "warnings": [],
++ "receipt": {
++ "engine": "local",
++ "model": null,
++ "cost_usd": 0,
++ "ms": 1,
++ "confidence": null,
++ "retention": "x"
++ }
++ }
++}
ddiff --git a/fixtures/vision.product-result/valid/one.json b/fixtures/vision.product-result/valid/one.json
nnew file mode 100644
iindex 00000000..4fc83a7a
---- /dev/null
++++ b/fixtures/vision.product-result/valid/one.json
@@@ -0,0 +1,27 @@
++{
++ "answer": {
++ "description": "A red running shoe.",
++ "embedding": [
++ 0.1,
++ 0.2
++ ],
++ "offers": [
++ {
++ "source": "deals",
++ "title": "Red runner",
++ "url": "https://openvibe.deals/x",
++ "price": 79.99,
++ "score": 0.9
++ }
++ ],
++ "warnings": [],
++ "receipt": {
++ "engine": "local+routed description",
++ "model": "gpt-4o-mini",
++ "cost_usd": 0.0001,
++ "ms": 800,
++ "confidence": 0.9,
++ "retention": "not stored"
++ }
++ }
++}
ddiff --git a/fixtures/vision.receipt-request/invalid/one.json b/fixtures/vision.receipt-request/invalid/one.json
nnew file mode 100644
iindex 00000000..d24bd095
---- /dev/null
++++ b/fixtures/vision.receipt-request/invalid/one.json
@@@ -0,0 +1,4 @@
++{
++ "image": "data:image/png;base64,iVBORw0KGgo=",
++ "fallback": "yes"
++}
ddiff --git a/fixtures/vision.receipt-request/valid/one.json b/fixtures/vision.receipt-request/valid/one.json
nnew file mode 100644
iindex 00000000..2378cd54
---- /dev/null
++++ b/fixtures/vision.receipt-request/valid/one.json
@@@ -0,0 +1,4 @@
++{
++ "image": "data:image/png;base64,iVBORw0KGgo=",
++ "fallback": false
++}
ddiff --git a/fixtures/vision.receipt-result/invalid/one.json b/fixtures/vision.receipt-result/invalid/one.json
nnew file mode 100644
iindex 00000000..46eadf19
---- /dev/null
++++ b/fixtures/vision.receipt-result/invalid/one.json
@@@ -0,0 +1,21 @@
++{
++ "answer": {
++ "store": "Corner Market",
++ "items": [
++ {
++ "name": "Milk",
++ "qty": "two",
++ "price": 3.5
++ }
++ ],
++ "total": 7,
++ "receipt": {
++ "engine": "local",
++ "model": "Tesseract 5",
++ "cost_usd": 0,
++ "ms": 120,
++ "confidence": 0.9,
++ "retention": "not stored"
++ }
++ }
++}
ddiff --git a/fixtures/vision.receipt-result/valid/one.json b/fixtures/vision.receipt-result/valid/one.json
nnew file mode 100644
iindex 00000000..8bf518e1
---- /dev/null
++++ b/fixtures/vision.receipt-result/valid/one.json
@@@ -0,0 +1,21 @@
++{
++ "answer": {
++ "store": "Corner Market",
++ "items": [
++ {
++ "name": "Milk",
++ "qty": 2,
++ "price": 3.5
++ }
++ ],
++ "total": 7,
++ "receipt": {
++ "engine": "local",
++ "model": "Tesseract 5",
++ "cost_usd": 0,
++ "ms": 120,
++ "confidence": 0.9,
++ "retention": "not stored"
++ }
++ }
++}
ddiff --git a/fixtures/vision.result.ready/invalid/one.json b/fixtures/vision.result.ready/invalid/one.json
nnew file mode 100644
iindex 00000000..d6b65109
---- /dev/null
++++ b/fixtures/vision.result.ready/invalid/one.json
@@@ -0,0 +1,14 @@
++{
++ "result_id": "vis_01J8Z3Q4R5S6T7V8W9X0Y1Z2A4",
++ "task": "look",
++ "source": "upload",
++ "model": null,
++ "provider": null,
++ "route": null,
++ "cost_usd": 0,
++ "latency_ms": 0,
++ "confidence": null,
++ "retention": "x",
++ "consent_id": null,
++ "created_at": "2026-10-29T10:00:00.000Z"
++}
ddiff --git a/fixtures/vision.result.ready/valid/one.json b/fixtures/vision.result.ready/valid/one.json
nnew file mode 100644
iindex 00000000..1eb7aa59
---- /dev/null
++++ b/fixtures/vision.result.ready/valid/one.json
@@@ -0,0 +1,14 @@
++{
++ "result_id": "vis_01J8Z3Q4R5S6T7V8W9X0Y1Z2A4",
++ "task": "ask",
++ "source": "upload",
++ "model": "gpt-4o-mini",
++ "provider": "openai",
++ "route": "ai.fast",
++ "cost_usd": 0.0002,
++ "latency_ms": 900,
++ "confidence": null,
++ "retention": "not stored",
++ "consent_id": null,
++ "created_at": "2026-10-29T10:00:00.000Z"
++}
ddiff --git a/fixtures/vision.watch-request/invalid/one.json b/fixtures/vision.watch-request/invalid/one.json
nnew file mode 100644
iindex 00000000..ea87a99d
---- /dev/null
++++ b/fixtures/vision.watch-request/invalid/one.json
@@@ -0,0 +1,4 @@
++{
++ "image": "data:image/png;base64,iVBORw0KGgo=",
++ "classes": []
++}
ddiff --git a/fixtures/vision.watch-request/valid/one.json b/fixtures/vision.watch-request/valid/one.json
nnew file mode 100644
iindex 00000000..417a5ff4
---- /dev/null
++++ b/fixtures/vision.watch-request/valid/one.json
@@@ -0,0 +1,8 @@
++{
++ "image": "data:image/png;base64,iVBORw0KGgo=",
++ "classes": [
++ "package",
++ "scene change"
++ ],
++ "previous_image": "data:image/png;base64,iVBORw0KGgo="
++}
ddiff --git a/fixtures/vision.watch-result/invalid/one.json b/fixtures/vision.watch-result/invalid/one.json
nnew file mode 100644
iindex 00000000..fa66d923
---- /dev/null
++++ b/fixtures/vision.watch-result/invalid/one.json
@@@ -0,0 +1,18 @@
++{
++ "answer": {
++ "detections": [
++ {
++ "class": "package",
++ "confidence": 1.5
++ }
++ ],
++ "receipt": {
++ "engine": "local",
++ "model": null,
++ "cost_usd": 0,
++ "ms": 1,
++ "confidence": null,
++ "retention": "x"
++ }
++ }
++}
ddiff --git a/fixtures/vision.watch-result/valid/one.json b/fixtures/vision.watch-result/valid/one.json
nnew file mode 100644
iindex 00000000..6fc83340
---- /dev/null
++++ b/fixtures/vision.watch-result/valid/one.json
@@@ -0,0 +1,29 @@
++{
++ "answer": {
++ "detections": [
++ {
++ "class": "package",
++ "confidence": 0.88,
++ "box": {
++ "x": 1,
++ "y": 2,
++ "width": 30,
++ "height": 40
++ }
++ },
++ {
++ "class": "scene change",
++ "confidence": 0.4,
++ "box": null
++ }
++ ],
++ "receipt": {
++ "engine": "local",
++ "model": "DETR ResNet-50 q8 + histogram",
++ "cost_usd": 0,
++ "ms": 80,
++ "confidence": 0.88,
++ "retention": "not stored"
++ }
++ }
++}
ddiff --git a/generated/json-schema/bundle.json b/generated/json-schema/bundle.json
iindex fbf4c05e..1d2e0716 100644
---- a/generated/json-schema/bundle.json
++++ b/generated/json-schema/bundle.json
@@@ -1772,7 +1772,7 @@
},
"event_type": {
"type": "string",
-- "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){2,}$"
++ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){1,}$"
},
"version": {
"type": "integer",
@@@ -87007,6 +87007,2490 @@
}
}
},
++ "vision.image-request": {
++ "version": "1.0.0",
++ "owner": "vision",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/image-request.v1.json",
++ "title": "VisionImageRequest",
++ "description": "vision.image-request@1: one image for OpenVibe.Vision's one-shot tools, as a data URL or a public URL. The multipart form field `image` carries the same bytes. Private tools (find, index, watch, moments) accept only uploaded bytes or a data URL, never an unverified source URL.",
++ "type": "object",
++ "additionalProperties": false,
++ "properties": {
++ "image": {
++ "type": "string",
++ "pattern": "^data:image/",
++ "description": "The image as a data URL (base64)."
++ },
++ "image_url": {
++ "type": "string",
++ "format": "uri",
++ "pattern": "^https?://",
++ "description": "A public image URL, fetched through Vision's private-address guard."
++ }
++ },
++ "anyOf": [
++ {
++ "required": [
++ "image"
++ ]
++ },
++ {
++ "required": [
++ "image_url"
++ ]
++ }
++ ]
++ }
++ },
++ "vision.ask-request": {
++ "version": "1.0.0",
++ "owner": "vision",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/ask-request.v1.json",
++ "title": "VisionAskRequest",
++ "description": "vision.ask-request@1: the body of POST /api/v1/ask and /api/v1/describe on OpenVibe.Vision. `task` is what the caller wants (ask, describe, identify, count, translate, ocr); `mode` is auto (local OCR first, then OpenVibe.AI), local (never a model) or ai. A read question (text, sign, receipt, label) is answered by the local OCR reader first in auto mode. `lang` is the Tesseract language and `model` is a provider:model hint AI may ignore.",
++ "type": "object",
++ "additionalProperties": false,
++ "properties": {
++ "image": {
++ "type": "string",
++ "pattern": "^data:image/",
++ "description": "The image as a data URL (base64)."
++ },
++ "image_url": {
++ "type": "string",
++ "format": "uri",
++ "pattern": "^https?://",
++ "description": "A public image URL, fetched through Vision's private-address guard."
++ },
++ "question": {
++ "type": "string",
++ "maxLength": 2000
++ },
++ "task": {
++ "enum": [
++ "ask",
++ "describe",
++ "identify",
++ "count",
++ "translate",
++ "ocr"
++ ]
++ },
++ "mode": {
++ "enum": [
++ "auto",
++ "local",
++ "ai"
++ ]
++ },
++ "lang": {
++ "type": "string",
++ "maxLength": 40
++ },
++ "model": {
++ "type": "string",
++ "maxLength": 120
++ }
++ },
++ "anyOf": [
++ {
++ "required": [
++ "image"
++ ]
++ },
++ {
++ "required": [
++ "image_url"
++ ]
++ }
++ ]
++ }
++ },
++ "vision.ocr-request": {
++ "version": "1.0.0",
++ "owner": "vision",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/ocr-request.v1.json",
++ "title": "VisionOcrRequest",
++ "description": "vision.ocr-request@1: the body of POST /api/v1/ocr. The local Tesseract 5 reader runs on Vision's host and costs nothing; `fallback` asks OpenVibe.AI only when the local pass found no usable text, which needs a signed-in person or a service token holding vision.ocr.",
++ "type": "object",
++ "additionalProperties": false,
++ "properties": {
++ "image": {
++ "type": "string",
++ "pattern": "^data:image/",
++ "description": "The image as a data URL (base64)."
++ },
++ "image_url": {
++ "type": "string",
++ "format": "uri",
++ "pattern": "^https?://",
++ "description": "A public image URL, fetched through Vision's private-address guard."
++ },
++ "lang": {
++ "type": "string",
++ "maxLength": 40
++ },
++ "fallback": {
++ "type": "boolean"
++ },
++ "model": {
++ "type": "string",
++ "maxLength": 120
++ }
++ },
++ "anyOf": [
++ {
++ "required": [
++ "image"
++ ]
++ },
++ {
++ "required": [
++ "image_url"
++ ]
++ }
++ ]
++ }
++ },
++ "vision.compare-request": {
++ "version": "1.0.0",
++ "owner": "vision",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/compare-request.v1.json",
++ "title": "VisionCompareRequest",
++ "description": "vision.compare-request@1: the body of POST /api/v1/compare. One image runs through the local reader and up to three AI hints; the requested model is a hint only, and each row reports the provider and model OpenVibe.AI says answered.",
++ "type": "object",
++ "additionalProperties": false,
++ "properties": {
++ "image": {
++ "type": "string",
++ "pattern": "^data:image/",
++ "description": "The image as a data URL (base64)."
++ },
++ "image_url": {
++ "type": "string",
++ "format": "uri",
++ "pattern": "^https?://",
++ "description": "A public image URL, fetched through Vision's private-address guard."
++ },
++ "task": {
++ "enum": [
++ "ask",
++ "describe",
++ "identify",
++ "count",
++ "translate",
++ "ocr"
++ ]
++ },
++ "question": {
++ "type": "string",
++ "maxLength": 2000
++ },
++ "models": {
++ "type": "array",
++ "items": {
++ "type": "string",
++ "maxLength": 120
++ },
++ "maxItems": 3
++ },
++ "lang": {
++ "type": "string",
++ "maxLength": 40
++ }
++ },
++ "anyOf": [
++ {
++ "required": [
++ "image"
++ ]
++ },
++ {
++ "required": [
++ "image_url"
++ ]
++ }
++ ]
++ }
++ },
++ "vision.find-request": {
++ "version": "1.0.0",
++ "owner": "vision",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/find-request.v1.json",
++ "title": "VisionFindRequest",
++ "description": "vision.find-request@1: the body of POST /api/v1/index and of GET or POST /api/v1/find. `index` stores a scoped CLIP embedding for one of the caller's own items (`item_id`, optional `result_url`); `find` searches only the caller's own private index, by words (`q`) or by an example image. No public image index is ever built.",
++ "type": "object",
++ "additionalProperties": false,
++ "properties": {
++ "image": {
++ "type": "string",
++ "pattern": "^data:image/",
++ "description": "The image as a data URL (base64)."
++ },
++ "image_url": {
++ "type": "string",
++ "format": "uri",
++ "pattern": "^https?://",
++ "description": "A public image URL, fetched through Vision's private-address guard."
++ },
++ "q": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 500
++ },
++ "item_id": {
++ "type": "string",
++ "pattern": "^[\\w:.-]{1,200}$"
++ },
++ "result_url": {
++ "type": "string",
++ "format": "uri",
++ "pattern": "^https://"
++ }
++ },
++ "anyOf": [
++ {
++ "required": [
++ "q"
++ ]
++ },
++ {
++ "required": [
++ "image"
++ ]
++ },
++ {
++ "required": [
++ "image_url"
++ ]
++ },
++ {
++ "required": [
++ "item_id",
++ "image"
++ ]
++ },
++ {
++ "required": [
++ "item_id",
++ "image_url"
++ ]
++ }
++ ]
++ }
++ },
++ "vision.receipt-request": {
++ "version": "1.0.0",
++ "owner": "vision",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/receipt-request.v1.json",
++ "title": "VisionReceiptRequest",
++ "description": "vision.receipt-request@1: the body of POST /api/v1/receipt. The local OCR reader extracts line items and a total; `fallback` asks OpenVibe.AI only when the local pass could not produce both.",
++ "type": "object",
++ "additionalProperties": false,
++ "properties": {
++ "image": {
++ "type": "string",
++ "pattern": "^data:image/",
++ "description": "The image as a data URL (base64)."
++ },
++ "image_url": {
++ "type": "string",
++ "format": "uri",
++ "pattern": "^https?://",
++ "description": "A public image URL, fetched through Vision's private-address guard."
++ },
++ "fallback": {
++ "type": "boolean"
++ }
++ },
++ "anyOf": [
++ {
++ "required": [
++ "image"
++ ]
++ },
++ {
++ "required": [
++ "image_url"
++ ]
++ }
++ ]
++ }
++ },
++ "vision.product-request": {
++ "version": "1.0.0",
++ "owner": "vision",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/product-request.v1.json",
++ "title": "VisionProductRequest",
++ "description": "vision.product-request@1: the body of POST /api/v1/product. The image is embedded locally with CLIP; `allow_rerank` lets one routed model reorder the candidate offers. Candidates come from OpenVibe.Deals and open.shoes; Vision never invents a price.",
++ "type": "object",
++ "additionalProperties": false,
++ "properties": {
++ "image": {
++ "type": "string",
++ "pattern": "^data:image/",
++ "description": "The image as a data URL (base64)."
++ },
++ "image_url": {
++ "type": "string",
++ "format": "uri",
++ "pattern": "^https?://",
++ "description": "A public image URL, fetched through Vision's private-address guard."
++ },
++ "allow_rerank": {
++ "type": "boolean"
++ }
++ },
++ "anyOf": [
++ {
++ "required": [
++ "image"
++ ]
++ },
++ {
++ "required": [
++ "image_url"
++ ]
++ }
++ ]
++ }
++ },
++ "vision.watch-request": {
++ "version": "1.0.0",
++ "owner": "vision",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/watch-request.v1.json",
++ "title": "VisionWatchRequest",
++ "description": "vision.watch-request@1: the body of POST /api/v1/watch (owner-granted camera alerts). `classes` names 1–20 things to look for (snow, delivery, package, a DETR label, or \"scene change\"); `previous_image` is required for a scene change and is compared by histogram.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "classes"
++ ],
++ "properties": {
++ "image": {
++ "type": "string",
++ "pattern": "^data:image/",
++ "description": "The image as a data URL (base64)."
++ },
++ "image_url": {
++ "type": "string",
++ "format": "uri",
++ "pattern": "^https?://",
++ "description": "A public image URL, fetched through Vision's private-address guard."
++ },
++ "classes": {
++ "type": "array",
++ "items": {
++ "type": "string",
++ "maxLength": 60
++ },
++ "minItems": 1,
++ "maxItems": 20
++ },
++ "previous_image": {
++ "type": "string",
++ "pattern": "^data:image/"
++ }
++ },
++ "anyOf": [
++ {
++ "required": [
++ "image"
++ ]
++ },
++ {
++ "required": [
++ "image_url"
++ ]
++ }
++ ]
++ }
++ },
++ "vision.moments-request": {
++ "version": "1.0.0",
++ "owner": "vision",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/moments-request.v1.json",
++ "title": "VisionMomentsRequest",
++ "description": "vision.moments-request@1: the body of POST /api/v1/moments. Up to 30 timestamped frames are compared with CLIP prompt embeddings; a histogram jump marks a scene change. VOD and stream chapters come from this.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "frames"
++ ],
++ "properties": {
++ "frames": {
++ "type": "array",
++ "minItems": 1,
++ "maxItems": 30,
++ "items": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "timestamp_ms"
++ ],
++ "properties": {
++ "timestamp_ms": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "image": {
++ "type": "string",
++ "pattern": "^data:image/"
++ },
++ "image_url": {
++ "type": "string",
++ "format": "uri",
++ "pattern": "^https?://"
++ }
++ },
++ "anyOf": [
++ {
++ "required": [
++ "image"
++ ]
++ },
++ {
++ "required": [
++ "image_url"
++ ]
++ }
++ ]
++ }
++ },
++ "prompts": {
++ "type": "array",
++ "items": {
++ "type": "string",
++ "maxLength": 120
++ },
++ "maxItems": 10
++ }
++ }
++ }
++ },
++ "vision.answer": {
++ "version": "1.0.0",
++ "owner": "vision",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/answer.v1.json",
++ "title": "VisionAnswer",
++ "description": "vision.answer@1: one saved answer on OpenVibe.Vision. It names the engine, model, provider and route that actually answered (never a model hint), the reason it was chosen, the cost OpenVibe.AI reported, the measured latency and the confidence. No image bytes are stored: only a sha256 and the declared media type.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "id",
++ "task",
++ "mode",
++ "source",
++ "image_sha256",
++ "image_mime",
++ "image_bytes",
++ "cost_usd",
++ "latency_ms",
++ "created_at",
++ "answer"
++ ],
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^vis_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "task": {
++ "enum": [
++ "ask",
++ "describe",
++ "identify",
++ "count",
++ "translate",
++ "ocr",
++ "compare"
++ ]
++ },
++ "question": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 2000
++ },
++ "mode": {
++ "enum": [
++ "auto",
++ "local",
++ "ai"
++ ]
++ },
++ "source": {
++ "enum": [
++ "upload",
++ "url",
++ "data"
++ ]
++ },
++ "source_url": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "uri"
++ },
++ "image_sha256": {
++ "type": "string",
++ "pattern": "^[0-9a-f]{64}$"
++ },
++ "image_mime": {
++ "type": "string",
++ "pattern": "^image/[a-z0-9.+-]+$"
++ },
++ "image_bytes": {
++ "type": "integer",
++ "minimum": 1,
++ "maximum": 20971520
++ },
++ "text": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 20000
++ },
++ "answer": {
++ "type": "object",
++ "description": "Engine details, OCR words, provenance and any compare rows; never an image."
++ },
++ "model": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "provider": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "route": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "reason": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "engine": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "confidence": {
++ "type": [
++ "number",
++ "null"
++ ],
++ "minimum": 0,
++ "maximum": 1
++ },
++ "cost_usd": {
++ "type": "number",
++ "minimum": 0
++ },
++ "latency_ms": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ }
++ },
++ "vision.answer-result": {
++ "version": "1.0.0",
++ "owner": "vision",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/answer-result.v1.json",
++ "title": "VisionAnswerResult",
++ "description": "vision.answer-result@1: what POST /api/v1/ask, /describe and /ocr answer (`{ answer }`), and POST /api/v1/compare answers with `compare` rows and the local reader's summary added. Every AI-written text has passed noLooks: a look-naming answer is replaced, never repaired word by word.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "answer"
++ ],
++ "properties": {
++ "answer": {
++ "$ref": "./answer.v1.json"
++ },
++ "compare": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "engine",
++ "ok",
++ "cost_usd",
++ "latency_ms"
++ ],
++ "properties": {
++ "engine": {
++ "enum": [
++ "local",
++ "ai"
++ ]
++ },
++ "requested_model": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "model": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "provider": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "route": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "ok": {
++ "type": "boolean"
++ },
++ "text": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "cost_usd": {
++ "type": "number",
++ "minimum": 0
++ },
++ "latency_ms": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "error_code": {
++ "type": [
++ "string",
++ "null"
++ ]
++ }
++ }
++ }
++ },
++ "local": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "ok",
++ "model",
++ "confidence",
++ "error_code"
++ ],
++ "properties": {
++ "ok": {
++ "type": "boolean"
++ },
++ "model": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "confidence": {
++ "type": [
++ "number",
++ "null"
++ ]
++ },
++ "error_code": {
++ "type": [
++ "string",
++ "null"
++ ]
++ }
++ }
++ }
++ }
++ }
++ },
++ "vision.alt-result": {
++ "version": "1.0.0",
++ "owner": "vision",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/alt-result.v1.json",
++ "title": "VisionAltResult",
++ "description": "vision.alt-result@1: POST /api/v1/alt answers one short, editable alt-text sentence about the scene, objects and action, never about a person. The text has passed noLooks; the receipt says which engine answered and at what cost.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "answer"
++ ],
++ "properties": {
++ "answer": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "alt_text",
++ "editable",
++ "receipt"
++ ],
++ "properties": {
++ "alt_text": {
++ "type": "string",
++ "maxLength": 240
++ },
++ "editable": {
++ "const": true
++ },
++ "receipt": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "engine",
++ "model",
++ "cost_usd",
++ "ms",
++ "confidence",
++ "retention"
++ ],
++ "properties": {
++ "engine": {
++ "enum": [
++ "local",
++ "routed",
++ "local+routed",
++ "local+routed description"
++ ]
++ },
++ "model": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "cost_usd": {
++ "type": "number",
++ "minimum": 0
++ },
++ "ms": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "confidence": {
++ "type": [
++ "number",
++ "null"
++ ]
++ },
++ "retention": {
++ "type": "string"
++ }
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "vision.find-result": {
++ "version": "1.0.0",
++ "owner": "vision",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/find-result.v1.json",
++ "title": "VisionFindResult",
++ "description": "vision.find-result@1: POST /api/v1/index and GET/POST /api/v1/find answer a scoped private search over the caller's own indexed items: `results` (item_id, result_url, score) only, no one else's rows and no public index.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "answer"
++ ],
++ "properties": {
++ "answer": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "results",
++ "receipt"
++ ],
++ "properties": {
++ "results": {
++ "type": "array",
++ "maxItems": 5000,
++ "items": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "item_id",
++ "score"
++ ],
++ "properties": {
++ "item_id": {
++ "type": "string"
++ },
++ "result_url": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "uri",
++ "pattern": "^https://"
++ },
++ "score": {
++ "type": "number"
++ }
++ }
++ }
++ },
++ "receipt": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "engine",
++ "model",
++ "cost_usd",
++ "ms",
++ "confidence",
++ "retention"
++ ],
++ "properties": {
++ "engine": {
++ "enum": [
++ "local",
++ "routed",
++ "local+routed",
++ "local+routed description"
++ ]
++ },
++ "model": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "cost_usd": {
++ "type": "number",
++ "minimum": 0
++ },
++ "ms": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "confidence": {
++ "type": [
++ "number",
++ "null"
++ ]
++ },
++ "retention": {
++ "type": "string"
++ }
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "vision.receipt-result": {
++ "version": "1.0.0",
++ "owner": "vision",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/receipt-result.v1.json",
++ "title": "VisionReceiptResult",
++ "description": "vision.receipt-result@1: POST /api/v1/receipt answers the store, the line items and the total the local OCR reader read (or the routed fallback read), with `raw_text` when the local pass produced it. OpenVibe.Food decides what enters a pantry; Vision returns the reading.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "answer"
++ ],
++ "properties": {
++ "answer": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "store",
++ "items",
++ "total",
++ "receipt"
++ ],
++ "properties": {
++ "store": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 150
++ },
++ "items": {
++ "type": "array",
++ "maxItems": 100,
++ "items": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "name",
++ "qty",
++ "price"
++ ],
++ "properties": {
++ "name": {
++ "type": "string",
++ "maxLength": 200
++ },
++ "qty": {
++ "type": "number",
++ "minimum": 0
++ },
++ "price": {
++ "type": "number",
++ "minimum": 0
++ }
++ }
++ }
++ },
++ "total": {
++ "type": [
++ "number",
++ "null"
++ ],
++ "minimum": 0
++ },
++ "raw_text": {
++ "type": "string",
++ "maxLength": 20000
++ },
++ "receipt": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "engine",
++ "model",
++ "cost_usd",
++ "ms",
++ "confidence",
++ "retention"
++ ],
++ "properties": {
++ "engine": {
++ "enum": [
++ "local",
++ "routed",
++ "local+routed",
++ "local+routed description"
++ ]
++ },
++ "model": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "cost_usd": {
++ "type": "number",
++ "minimum": 0
++ },
++ "ms": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "confidence": {
++ "type": [
++ "number",
++ "null"
++ ]
++ },
++ "retention": {
++ "type": "string"
++ }
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "vision.product-result": {
++ "version": "1.0.0",
++ "owner": "vision",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/product-result.v1.json",
++ "title": "VisionProductResult",
++ "description": "vision.product-result@1: POST /api/v1/product answers a description of the product only (never a person), the CLIP embedding, and candidate offers from OpenVibe.Deals and open.shoes with their score, price and URL. A room must not treat a candidate as a price it verified.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "answer"
++ ],
++ "properties": {
++ "answer": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "description",
++ "embedding",
++ "offers",
++ "warnings",
++ "receipt"
++ ],
++ "properties": {
++ "description": {
++ "type": "string",
++ "maxLength": 300
++ },
++ "embedding": {
++ "type": "array",
++ "items": {
++ "type": "number"
++ }
++ },
++ "offers": {
++ "type": "array",
++ "maxItems": 10,
++ "items": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "source",
++ "title",
++ "url"
++ ],
++ "properties": {
++ "source": {
++ "type": "string"
++ },
++ "title": {
++ "type": "string",
++ "maxLength": 200
++ },
++ "url": {
++ "type": "string",
++ "format": "uri",
++ "pattern": "^https://"
++ },
++ "price": {
++ "type": [
++ "number",
++ "null"
++ ]
++ },
++ "score": {
++ "type": "number"
++ }
++ }
++ }
++ },
++ "warnings": {
++ "type": "array",
++ "items": {
++ "type": "string"
++ }
++ },
++ "receipt": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "engine",
++ "model",
++ "cost_usd",
++ "ms",
++ "confidence",
++ "retention"
++ ],
++ "properties": {
++ "engine": {
++ "enum": [
++ "local",
++ "routed",
++ "local+routed",
++ "local+routed description"
++ ]
++ },
++ "model": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "cost_usd": {
++ "type": "number",
++ "minimum": 0
++ },
++ "ms": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "confidence": {
++ "type": [
++ "number",
++ "null"
++ ]
++ },
++ "retention": {
++ "type": "string"
++ }
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "vision.watch-result": {
++ "version": "1.0.0",
++ "owner": "vision",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/watch-result.v1.json",
++ "title": "VisionWatchResult",
++ "description": "vision.watch-result@1: POST /api/v1/watch answers the requested classes found in a camera still (DETR labels, snow/delivery/package through CLIP, or a histogram scene change), each with a confidence and, for an object, a box.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "answer"
++ ],
++ "properties": {
++ "answer": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "detections",
++ "receipt"
++ ],
++ "properties": {
++ "detections": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "class",
++ "confidence"
++ ],
++ "properties": {
++ "class": {
++ "type": "string"
++ },
++ "confidence": {
++ "type": "number",
++ "minimum": 0,
++ "maximum": 1
++ },
++ "box": {
++ "type": [
++ "object",
++ "null"
++ ],
++ "additionalProperties": false,
++ "required": [
++ "x",
++ "y",
++ "width",
++ "height"
++ ],
++ "properties": {
++ "x": {
++ "type": "number"
++ },
++ "y": {
++ "type": "number"
++ },
++ "width": {
++ "type": "number"
++ },
++ "height": {
++ "type": "number"
++ }
++ }
++ }
++ }
++ }
++ },
++ "receipt": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "engine",
++ "model",
++ "cost_usd",
++ "ms",
++ "confidence",
++ "retention"
++ ],
++ "properties": {
++ "engine": {
++ "enum": [
++ "local",
++ "routed",
++ "local+routed",
++ "local+routed description"
++ ]
++ },
++ "model": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "cost_usd": {
++ "type": "number",
++ "minimum": 0
++ },
++ "ms": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "confidence": {
++ "type": [
++ "number",
++ "null"
++ ]
++ },
++ "retention": {
++ "type": "string"
++ }
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "vision.moments-result": {
++ "version": "1.0.0",
++ "owner": "vision",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/moments-result.v1.json",
++ "title": "VisionMomentsResult",
++ "description": "vision.moments-result@1: POST /api/v1/moments answers scene changes and candidate moments (timestamp, matching prompt, score) for up to 30 frames. A moment is a suggestion: the room decides whether it becomes a clip or a chapter.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "answer"
++ ],
++ "properties": {
++ "answer": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "scene_changes",
++ "candidate_moments",
++ "receipt"
++ ],
++ "properties": {
++ "scene_changes": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "timestamp_ms",
++ "kind"
++ ],
++ "properties": {
++ "timestamp_ms": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "kind": {
++ "const": "scene_change"
++ }
++ }
++ }
++ },
++ "candidate_moments": {
++ "type": "array",
++ "maxItems": 30,
++ "items": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "timestamp_ms",
++ "prompt",
++ "score"
++ ],
++ "properties": {
++ "timestamp_ms": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "prompt": {
++ "type": "string"
++ },
++ "score": {
++ "type": "number"
++ }
++ }
++ }
++ },
++ "receipt": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "engine",
++ "model",
++ "cost_usd",
++ "ms",
++ "confidence",
++ "retention"
++ ],
++ "properties": {
++ "engine": {
++ "enum": [
++ "local",
++ "routed",
++ "local+routed",
++ "local+routed description"
++ ]
++ },
++ "model": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "cost_usd": {
++ "type": "number",
++ "minimum": 0
++ },
++ "ms": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "confidence": {
++ "type": [
++ "number",
++ "null"
++ ]
++ },
++ "retention": {
++ "type": "string"
++ }
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "vision.face-detection": {
++ "version": "1.0.0",
++ "owner": "vision",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/face-detection.v1.json",
++ "title": "VisionFaceDetection",
++ "description": "vision.face-detection@1: POST /api/v1/faces/detect, an identity-free YuNet privacy tool. It counts faces and returns their boxes and scores; it stores no template, names no one and needs no consent. Anyone may use it.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "count",
++ "boxes"
++ ],
++ "properties": {
++ "count": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "boxes": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "x",
++ "y",
++ "width",
++ "height",
++ "score"
++ ],
++ "properties": {
++ "x": {
++ "type": "number"
++ },
++ "y": {
++ "type": "number"
++ },
++ "width": {
++ "type": "number"
++ },
++ "height": {
++ "type": "number"
++ },
++ "score": {
++ "type": "number",
++ "minimum": 0,
++ "maximum": 1
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "vision.face-blur": {
++ "version": "1.0.0",
++ "owner": "vision",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/face-blur.v1.json",
++ "title": "VisionFaceBlur",
++ "description": "vision.face-blur@1: POST /api/v1/faces/blur answers a redacted PNG derivative and the number of faces blurred in X-Faces-Blur. Identity-free, no template, and the original is never stored.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "media_type",
++ "face_count"
++ ],
++ "properties": {
++ "media_type": {
++ "const": "image/png"
++ },
++ "face_count": {
++ "type": "integer",
++ "minimum": 0
++ }
++ }
++ }
++ },
++ "vision.face-enrol-request": {
++ "version": "1.0.0",
++ "owner": "vision",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/face-enrol-request.v1.json",
++ "title": "VisionFaceEnrolRequest",
++ "description": "vision.face-enrol-request@1: POST /api/v1/faces/enrol/:feature. `feature` is verify (1:1 verification of yourself), find (search your own uploads) or cast (an adult tagging named streams); each needs its own signed consent first, and cast also names the channel scope.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "feature"
++ ],
++ "properties": {
++ "image": {
++ "type": "string",
++ "pattern": "^data:image/",
++ "description": "The image as a data URL (base64)."
++ },
++ "image_url": {
++ "type": "string",
++ "format": "uri",
++ "pattern": "^https?://",
++ "description": "A public image URL, fetched through Vision's private-address guard."
++ },
++ "feature": {
++ "enum": [
++ "verify",
++ "find",
++ "cast"
++ ]
++ },
++ "channel": {
++ "type": "string",
++ "pattern": "^[a-zA-Z0-9][a-zA-Z0-9:_-]{1,119}$"
++ }
++ },
++ "anyOf": [
++ {
++ "required": [
++ "image"
++ ]
++ },
++ {
++ "required": [
++ "image_url"
++ ]
++ }
++ ]
++ }
++ },
++ "vision.face-verify": {
++ "version": "1.0.0",
++ "owner": "vision",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/face-verify.v1.json",
++ "title": "VisionFaceVerify",
++ "description": "vision.face-verify@1: POST /api/v1/faces/verify compares this person's own verification attempt with their enrolled template. It always requires that person's verify consent; without it the answer is faces.consent_required whether or not a template exists. A match alone never unlocks anything: liveness and a second factor are separate.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "score",
++ "match",
++ "threshold",
++ "liveness"
++ ],
++ "properties": {
++ "score": {
++ "type": "number",
++ "minimum": 0,
++ "maximum": 1
++ },
++ "match": {
++ "type": "boolean"
++ },
++ "threshold": {
++ "type": "number",
++ "minimum": 0,
++ "maximum": 1
++ },
++ "liveness": {
++ "enum": [
++ "not_checked",
++ "checked"
++ ]
++ }
++ }
++ }
++ },
++ "vision.face-find-result": {
++ "version": "1.0.0",
++ "owner": "vision",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/face-find-result.v1.json",
++ "title": "VisionFaceFindResult",
++ "description": "vision.face-find-result@1: POST /api/v1/faces/find lists the caller's own private uploads that match their enrolled self-search template, as private links and scores. It requires find consent; without it the answer is faces.consent_required whether or not any media exists.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "results"
++ ],
++ "properties": {
++ "results": {
++ "type": "array",
++ "maxItems": 500,
++ "items": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "private_link",
++ "score"
++ ],
++ "properties": {
++ "private_link": {
++ "type": "string",
++ "pattern": "^/api/v1/faces/media/"
++ },
++ "score": {
++ "type": "number",
++ "minimum": 0,
++ "maximum": 1
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "vision.face-cast-match-request": {
++ "version": "1.0.0",
++ "owner": "vision",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/face-cast-match-request.v1.json",
++ "title": "VisionFaceCastMatchRequest",
++ "description": "vision.face-cast-match-request@1: POST /api/v1/faces/cast/match, called by OpenVibe.Live with a service token scoped to one channel (vision.face.cast.match). It matches only that channel's opted-in adult roster; a person who has not enrolled for this channel cannot appear, and the answer does not say whether they exist.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "channel",
++ "media_ref"
++ ],
++ "properties": {
++ "image": {
++ "type": "string",
++ "pattern": "^data:image/",
++ "description": "The image as a data URL (base64)."
++ },
++ "image_url": {
++ "type": "string",
++ "format": "uri",
++ "pattern": "^https?://",
++ "description": "A public image URL, fetched through Vision's private-address guard."
++ },
++ "channel": {
++ "type": "string",
++ "pattern": "^[a-zA-Z0-9][a-zA-Z0-9:_-]{1,119}$"
++ },
++ "media_ref": {
++ "type": "string",
++ "pattern": "^[a-zA-Z0-9][a-zA-Z0-9:/_-]{1,159}$"
++ }
++ },
++ "anyOf": [
++ {
++ "required": [
++ "image"
++ ]
++ },
++ {
++ "required": [
++ "image_url"
++ ]
++ }
++ ]
++ }
++ },
++ "vision.face-cast-match": {
++ "version": "1.0.0",
++ "owner": "vision",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/face-cast-match.v1.json",
++ "title": "VisionFaceCastMatch",
++ "description": "vision.face-cast-match@1: the matches inside one channel's consented roster, each with a score and box. No other subject is named and no non-roster face is described.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "channel",
++ "matches",
++ "liveness"
++ ],
++ "properties": {
++ "channel": {
++ "type": "string"
++ },
++ "matches": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "subject",
++ "score"
++ ],
++ "properties": {
++ "subject": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "score": {
++ "type": "number",
++ "minimum": 0,
++ "maximum": 1
++ },
++ "box": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "x",
++ "y",
++ "width",
++ "height"
++ ],
++ "properties": {
++ "x": {
++ "type": "number"
++ },
++ "y": {
++ "type": "number"
++ },
++ "width": {
++ "type": "number"
++ },
++ "height": {
++ "type": "number"
++ }
++ }
++ }
++ }
++ }
++ },
++ "liveness": {
++ "enum": [
++ "not_checked",
++ "checked"
++ ]
++ }
++ }
++ }
++ },
++ "vision.face-tag": {
++ "version": "1.0.0",
++ "owner": "vision",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/face-tag.v1.json",
++ "title": "VisionFaceTag",
++ "description": "vision.face-tag@1: one cast tag on a stream: which subject, which channel, which private media reference and when. GET /api/v1/faces/cast/tags lists the caller's own tags; revoking the cast consent removes them.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "id",
++ "channel",
++ "media_ref",
++ "created_at"
++ ],
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^ftg_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "channel": {
++ "type": "string"
++ },
++ "media_ref": {
++ "type": "string"
++ },
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ }
++ },
++ "vision.face-tag-list": {
++ "version": "1.0.0",
++ "owner": "vision",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/face-tag-list.v1.json",
++ "title": "VisionFaceTagList",
++ "description": "vision.face-tag-list@1: GET /api/v1/faces/cast/tags answers the caller's own cast tags, newest first, as a bare array. Empty when they have not consented to cast matching.",
++ "type": "array",
++ "items": {
++ "$ref": "./face-tag.v1.json"
++ }
++ }
++ },
++ "vision.face-library-request": {
++ "version": "1.0.0",
++ "owner": "vision",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/face-library-request.v1.json",
++ "title": "VisionFaceLibraryRequest",
++ "description": "vision.face-library-request@1: POST /api/v1/faces/library/cluster (region-gated), GET /library/groups and POST /library/correct. `action` is cluster (default), exclude, merge or split; `ids` names up to 100 of the caller's own clusters; `region` is a jurisdiction code the host allows. Unknown faces stay unlabelled.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "action"
++ ],
++ "properties": {
++ "action": {
++ "enum": [
++ "cluster",
++ "exclude",
++ "merge",
++ "split"
++ ]
++ },
++ "region": {
++ "type": "string",
++ "maxLength": 10
++ },
++ "ids": {
++ "type": "array",
++ "items": {
++ "type": "string",
++ "maxLength": 40
++ },
++ "minItems": 1,
++ "maxItems": 100
++ }
++ }
++ }
++ },
++ "vision.face-library-result": {
++ "version": "1.0.0",
++ "owner": "vision",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/face-library-result.v1.json",
++ "title": "VisionFaceLibraryResult",
++ "description": "vision.face-library-result@1: clustering and group reads answer the caller's own private face groups as an array; a correction answers the action and how many clusters it updated.",
++ "oneOf": [
++ {
++ "type": "array",
++ "items": {
++ "$ref": "./face-library-group.v1.json"
++ }
++ },
++ {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "action",
++ "updated"
++ ],
++ "properties": {
++ "action": {
++ "enum": [
++ "exclude",
++ "merge",
++ "split"
++ ]
++ },
++ "updated": {
++ "type": "integer",
++ "minimum": 0
++ }
++ }
++ }
++ ]
++ }
++ },
++ "vision.face-library-group": {
++ "version": "1.0.0",
++ "owner": "vision",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/face-library-group.v1.json",
++ "title": "VisionFaceLibraryGroup",
++ "description": "vision.face-library-group@1: one private face cluster in a person's own Vision library: the media it came from, the group it was merged into, an optional label and whether the person excluded it. It never leaves that person's library.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "id",
++ "media_id",
++ "group_id",
++ "excluded",
++ "created_at"
++ ],
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^fcl_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "media_id": {
++ "type": "string",
++ "pattern": "^fme_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "group_id": {
++ "type": "string",
++ "pattern": "^fgr_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "label": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 120
++ },
++ "excluded": {
++ "type": "boolean"
++ },
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ }
++ },
++ "vision.consent": {
++ "version": "1.0.0",
++ "owner": "vision",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/consent.v1.json",
++ "title": "VisionConsent",
++ "description": "vision.consent@1: one consent record on OpenVibe.Vision. It says whose face or voice it is (`subject`), the modality, the purpose-scoped feature and scope (a channel or a library), who signed which text version, when it was granted, when it expires, and how and when it was revoked. Face matching tools refuse without an active record for that person, with the same answer whether or not the person exists. Templates are deleted when the purpose ends and at the latest three years after the last use (BIPA); a revoked audit record is deleted one year after the purpose ends (CUBI).",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "id",
++ "subject",
++ "modality",
++ "feature",
++ "scope",
++ "purpose",
++ "text_version",
++ "granted_by",
++ "granted_at",
++ "expires_at",
++ "last_used_at",
++ "revoked_at",
++ "revoked_by",
++ "purpose_ends_at",
++ "retention_until",
++ "status",
++ "erased"
++ ],
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^fco_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "subject": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The person whose face or voice this consent covers."
++ },
++ "modality": {
++ "enum": [
++ "face",
++ "voice"
++ ]
++ },
++ "feature": {
++ "enum": [
++ "verify",
++ "find",
++ "cast",
++ "library",
++ "voice"
++ ]
++ },
++ "scope": {
++ "type": "string",
++ "maxLength": 160,
++ "description": "Empty means the person's own account; a cast grant names one channel, a library grant the library it applies to."
++ },
++ "purpose": {
++ "type": "string",
++ "maxLength": 200
++ },
++ "text_version": {
++ "type": "string",
++ "maxLength": 80
++ },
++ "granted_by": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The person who signed; the subject themself, or a guardian where the law allows."
++ },
++ "granted_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "expires_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time",
++ "description": "When the consent stops applying on its own; null when it lasts until revoked."
++ },
++ "last_used_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "revoked_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "revoked_by": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "enum": [
++ "subject",
++ "retention",
++ "account_deletion",
++ null
++ ],
++ "description": "How it was revoked: by the person, by the retention job, or with the account."
++ },
++ "purpose_ends_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "retention_until": {
++ "type": "string",
++ "format": "date-time",
++ "description": "The outer limit the record and derived data may exist: three years after last use, or one year after the purpose ended."
++ },
++ "status": {
++ "enum": [
++ "active",
++ "revoked",
++ "expired"
++ ]
++ },
++ "erased": {
++ "type": "boolean",
++ "description": "True once the template and derived results this consent covered are gone."
++ }
++ }
++ }
++ },
++ "vision.consent-request": {
++ "version": "1.0.0",
++ "owner": "vision",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/consent-request.v1.json",
++ "title": "VisionConsentRequest",
++ "description": "vision.consent-request@1: POST /api/v1/faces/consents/:feature. The caller signs the current text (`text_version` must match this server's), names themself (`signed_name`), and for cast must confirm they are an adult; library grouping is refused outside the host's allowed regions. A repeat of an active consent changes nothing.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "signed_name",
++ "text_version"
++ ],
++ "properties": {
++ "signed_name": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 160
++ },
++ "text_version": {
++ "type": "string",
++ "maxLength": 80
++ },
++ "modality": {
++ "enum": [
++ "face",
++ "voice"
++ ]
++ },
++ "feature": {
++ "enum": [
++ "verify",
++ "find",
++ "cast",
++ "library",
++ "voice"
++ ],
++ "description": "Echo of the :feature path segment."
++ },
++ "channel": {
++ "type": "string",
++ "pattern": "^[a-zA-Z0-9][a-zA-Z0-9:_-]{1,119}$"
++ },
++ "adult": {
++ "type": "boolean"
++ },
++ "region": {
++ "type": "string",
++ "maxLength": 10
++ }
++ }
++ }
++ },
++ "vision.consent-revocation": {
++ "version": "1.0.0",
++ "owner": "vision",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/consent-revocation.v1.json",
++ "title": "VisionConsentRevocation",
++ "description": "vision.consent-revocation@1: DELETE /api/v1/faces/consents/:feature answers with the revoked feature and scope, when, and that the template and derived data were erased in the same transaction. Revocation removes past cast tags and cannot be undone by reusing an old text version.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "feature",
++ "scope",
++ "revoked_at",
++ "revoked_by",
++ "erased"
++ ],
++ "properties": {
++ "feature": {
++ "enum": [
++ "verify",
++ "find",
++ "cast",
++ "library",
++ "voice"
++ ]
++ },
++ "scope": {
++ "type": "string",
++ "maxLength": 160
++ },
++ "revoked_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "revoked_by": {
++ "enum": [
++ "subject"
++ ]
++ },
++ "erased": {
++ "const": true
++ }
++ }
++ }
++ },
++ "vision.models": {
++ "version": "1.0.0",
++ "owner": "vision",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/models.v1.json",
++ "title": "VisionModels",
++ "description": "vision.models@1: GET /api/v1/models names the AI route's offered provider:model hints, the configured hints and the local engines present, without any subject data. OpenVibe.AI's router remains the authority: this is a menu, not a promise.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "ai",
++ "configured",
++ "offered",
++ "local",
++ "note"
++ ],
++ "properties": {
++ "ai": {
++ "type": "boolean"
++ },
++ "configured": {
++ "type": "array",
++ "items": {
++ "type": "string"
++ }
++ },
++ "offered": {
++ "type": "array",
++ "items": {
++ "type": "object"
++ }
++ },
++ "local": {
++ "type": "array",
++ "items": {
++ "type": "string"
++ }
++ },
++ "note": {
++ "type": "string"
++ }
++ }
++ }
++ },
++ "vision.benchmarks": {
++ "version": "1.0.0",
++ "owner": "vision",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/vision/benchmarks.v1.json",
++ "title": "VisionBenchmarks",
++ "description": "vision.benchmarks@1: GET /api/v1/benchmarks aggregates compare samples per task, engine, provider and model with sample counts and mean latency and cost. No subject, image or answer text is included.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "count",
++ "benchmarks"
++ ],
++ "properties": {
++ "count": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "benchmarks": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "task",
++ "engine",
++ "provider",
++ "model",
++ "samples",
++ "avg_latency_ms",
++ "avg_cost_usd",
++ "min_cost_usd",
++ "max_latency_ms"
++ ],
++ "properties": {
++ "task": {
++ "type": "string"
++ },
++ "engine": {
++ "enum": [
++ "local",
++ "ai"
++ ]
++ },
++ "provider": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "model": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "samples": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "avg_latency_ms": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "avg_cost_usd": {
++ "type": "number",
++ "minimum": 0
++ },
++ "min_cost_usd": {
++ "type": "number",
++ "minimum": 0
++ },
++ "max_latency_ms": {
++ "type": "integer",
++ "minimum": 0
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "vision.result.ready": {
++ "version": "1.0.0",
++ "owner": "vision",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/vision.result.ready.v1.json",
++ "title": "VisionResultReadyPayload",
++ "description": "vision.result.ready v1 (OpenVibe.Vision): a visual result is ready for its owner or caller. Envelope: source vision, subject { type: user, id: <requester> } (or { type: service, id: <caller> } when no person asked), visibility subject for a person and internal otherwise. The payload carries the identifiers, the engine and provenance, cost, latency and retention, never an image, a template or an answer text.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "result_id",
++ "task",
++ "source",
++ "model",
++ "provider",
++ "route",
++ "cost_usd",
++ "latency_ms",
++ "confidence",
++ "retention",
++ "consent_id",
++ "created_at"
++ ],
++ "properties": {
++ "result_id": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "pattern": "^(vis|vuc|fme|ftg|fco)_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "task": {
++ "enum": [
++ "ask",
++ "describe",
++ "identify",
++ "count",
++ "translate",
++ "ocr",
++ "compare",
++ "alt",
++ "find",
++ "receipt",
++ "product",
++ "watch",
++ "moments",
++ "ui_check",
++ "face_detect",
++ "face_blur"
++ ]
++ },
++ "source": {
++ "enum": [
++ "upload",
++ "url",
++ "data",
++ "html",
++ "screenshot",
++ "none"
++ ]
++ },
++ "model": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "provider": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "route": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "cost_usd": {
++ "type": "number",
++ "minimum": 0
++ },
++ "latency_ms": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "confidence": {
++ "type": [
++ "number",
++ "null"
++ ]
++ },
++ "retention": {
++ "type": "string",
++ "maxLength": 200
++ },
++ "consent_id": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "pattern": "^fco_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ }
++ },
++ "vision.alert": {
++ "version": "1.0.0",
++ "owner": "vision",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/vision.alert.v1.json",
++ "title": "VisionAlertPayload",
++ "description": "vision.alert v1 (OpenVibe.Vision): an owner-granted camera alert. Envelope: source vision, subject { type: user, id: <source owner> }, visibility subject. The payload names the watched source reference and the classes found with their confidence; it carries no image bytes and no person's identity.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "kind",
++ "source_ref",
++ "classes",
++ "confidence",
++ "detected_at"
++ ],
++ "properties": {
++ "kind": {
++ "enum": [
++ "object",
++ "scene_change",
++ "face"
++ ]
++ },
++ "source_ref": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 160
++ },
++ "classes": {
++ "type": "array",
++ "items": {
++ "type": "string",
++ "maxLength": 60
++ },
++ "minItems": 1,
++ "maxItems": 20
++ },
++ "confidence": {
++ "type": [
++ "number",
++ "null"
++ ],
++ "minimum": 0,
++ "maximum": 1
++ },
++ "previous_ref": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 160
++ },
++ "detected_at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ }
++ },
"bench.kind": {
"version": "1.0.0",
"owner": "bench",
ddiff --git a/generated/openapi/actor.json b/generated/openapi/actor.json
iindex 91da1b78..92f62bf9 100644
---- a/generated/openapi/actor.json
++++ b/generated/openapi/actor.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Actor API",
-- "version": "0.239.0",
-- "description": "Routes of OpenVibe.Actor as the OpenVibe contracts describe them (openvibe-contracts 0.239.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.240.0",
++ "description": "Routes of OpenVibe.Actor as the OpenVibe contracts describe them (openvibe-contracts 0.240.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/ai.json b/generated/openapi/ai.json
iindex 8822e4a7..8e3d4b85 100644
---- a/generated/openapi/ai.json
++++ b/generated/openapi/ai.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.AI API",
-- "version": "0.239.0",
-- "description": "Routes of OpenVibe.AI as the OpenVibe contracts describe them (openvibe-contracts 0.239.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.240.0",
++ "description": "Routes of OpenVibe.AI as the OpenVibe contracts describe them (openvibe-contracts 0.240.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/bench.json b/generated/openapi/bench.json
iindex cfba3245..42ee335b 100644
---- a/generated/openapi/bench.json
++++ b/generated/openapi/bench.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Bench API",
-- "version": "0.239.0",
-- "description": "Routes of OpenVibe.Bench as the OpenVibe contracts describe them (openvibe-contracts 0.239.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.240.0",
++ "description": "Routes of OpenVibe.Bench as the OpenVibe contracts describe them (openvibe-contracts 0.240.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/billing.json b/generated/openapi/billing.json
iindex 1d4d9d71..2d99f19a 100644
---- a/generated/openapi/billing.json
++++ b/generated/openapi/billing.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Billing API",
-- "version": "0.239.0",
-- "description": "Routes of OpenVibe.Billing as the OpenVibe contracts describe them (openvibe-contracts 0.239.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.240.0",
++ "description": "Routes of OpenVibe.Billing as the OpenVibe contracts describe them (openvibe-contracts 0.240.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/blog.json b/generated/openapi/blog.json
iindex ecfa1b9e..69445204 100644
---- a/generated/openapi/blog.json
++++ b/generated/openapi/blog.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Blog API",
-- "version": "0.239.0",
-- "description": "Routes of OpenVibe.Blog as the OpenVibe contracts describe them (openvibe-contracts 0.239.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.240.0",
++ "description": "Routes of OpenVibe.Blog as the OpenVibe contracts describe them (openvibe-contracts 0.240.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/bot.json b/generated/openapi/bot.json
iindex f318cef2..e5cfd995 100644
---- a/generated/openapi/bot.json
++++ b/generated/openapi/bot.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Bot API",
-- "version": "0.239.0",
-- "description": "Routes of OpenVibe.Bot as the OpenVibe contracts describe them (openvibe-contracts 0.239.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.240.0",
++ "description": "Routes of OpenVibe.Bot as the OpenVibe contracts describe them (openvibe-contracts 0.240.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/chat.json b/generated/openapi/chat.json
iindex 6a9fc1d8..4f21aabe 100644
---- a/generated/openapi/chat.json
++++ b/generated/openapi/chat.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Chat API",
-- "version": "0.239.0",
-- "description": "Routes of OpenVibe.Chat as the OpenVibe contracts describe them (openvibe-contracts 0.239.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.240.0",
++ "description": "Routes of OpenVibe.Chat as the OpenVibe contracts describe them (openvibe-contracts 0.240.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/codes.json b/generated/openapi/codes.json
iindex d1644200..e1aaf729 100644
---- a/generated/openapi/codes.json
++++ b/generated/openapi/codes.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Codes API",
-- "version": "0.239.0",
-- "description": "Routes of OpenVibe.Codes as the OpenVibe contracts describe them (openvibe-contracts 0.239.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.240.0",
++ "description": "Routes of OpenVibe.Codes as the OpenVibe contracts describe them (openvibe-contracts 0.240.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/community.json b/generated/openapi/community.json
iindex c8fab8c6..02e0798b 100644
---- a/generated/openapi/community.json
++++ b/generated/openapi/community.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Community API",
-- "version": "0.239.0",
-- "description": "Routes of OpenVibe.Community as the OpenVibe contracts describe them (openvibe-contracts 0.239.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.240.0",
++ "description": "Routes of OpenVibe.Community as the OpenVibe contracts describe them (openvibe-contracts 0.240.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/coupons.json b/generated/openapi/coupons.json
iindex 896b7d61..10923bdc 100644
---- a/generated/openapi/coupons.json
++++ b/generated/openapi/coupons.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Coupons API",
-- "version": "0.239.0",
-- "description": "Routes of OpenVibe.Coupons as the OpenVibe contracts describe them (openvibe-contracts 0.239.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.240.0",
++ "description": "Routes of OpenVibe.Coupons as the OpenVibe contracts describe them (openvibe-contracts 0.240.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/deals.json b/generated/openapi/deals.json
iindex a1a2e055..74cac7d5 100644
---- a/generated/openapi/deals.json
++++ b/generated/openapi/deals.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Deals API",
-- "version": "0.239.0",
-- "description": "Routes of OpenVibe.Deals as the OpenVibe contracts describe them (openvibe-contracts 0.239.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.240.0",
++ "description": "Routes of OpenVibe.Deals as the OpenVibe contracts describe them (openvibe-contracts 0.240.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/domains.json b/generated/openapi/domains.json
iindex 193f31a0..e0ecc604 100644
---- a/generated/openapi/domains.json
++++ b/generated/openapi/domains.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Domains API",
-- "version": "0.239.0",
-- "description": "Routes of OpenVibe.Domains as the OpenVibe contracts describe them (openvibe-contracts 0.239.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.240.0",
++ "description": "Routes of OpenVibe.Domains as the OpenVibe contracts describe them (openvibe-contracts 0.240.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/events.json b/generated/openapi/events.json
iindex 7a6133fe..b101a83c 100644
---- a/generated/openapi/events.json
++++ b/generated/openapi/events.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Events API",
-- "version": "0.239.0",
-- "description": "Routes of OpenVibe.Events as the OpenVibe contracts describe them (openvibe-contracts 0.239.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.240.0",
++ "description": "Routes of OpenVibe.Events as the OpenVibe contracts describe them (openvibe-contracts 0.240.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
@@@ -1503,7 +1503,7 @@
},
"event_type": {
"type": "string",
-- "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){2,}$"
++ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){1,}$"
},
"version": {
"type": "integer",
ddiff --git a/generated/openapi/food.json b/generated/openapi/food.json
iindex d9af5a8d..8724757d 100644
---- a/generated/openapi/food.json
++++ b/generated/openapi/food.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Food API",
-- "version": "0.239.0",
-- "description": "Routes of OpenVibe.Food as the OpenVibe contracts describe them (openvibe-contracts 0.239.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.240.0",
++ "description": "Routes of OpenVibe.Food as the OpenVibe contracts describe them (openvibe-contracts 0.240.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/games.json b/generated/openapi/games.json
iindex 08834382..ea76ff07 100644
---- a/generated/openapi/games.json
++++ b/generated/openapi/games.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Games API",
-- "version": "0.239.0",
-- "description": "Routes of OpenVibe.Games as the OpenVibe contracts describe them (openvibe-contracts 0.239.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.240.0",
++ "description": "Routes of OpenVibe.Games as the OpenVibe contracts describe them (openvibe-contracts 0.240.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/host.json b/generated/openapi/host.json
iindex 0374e8df..224de392 100644
---- a/generated/openapi/host.json
++++ b/generated/openapi/host.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Host API",
-- "version": "0.239.0",
-- "description": "Routes of OpenVibe.Host as the OpenVibe contracts describe them (openvibe-contracts 0.239.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.240.0",
++ "description": "Routes of OpenVibe.Host as the OpenVibe contracts describe them (openvibe-contracts 0.240.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/index.json b/generated/openapi/index.json
iindex 79f6ec30..7d8e3a1a 100644
---- a/generated/openapi/index.json
++++ b/generated/openapi/index.json
@@@ -250,8 +250,8 @@
"service": "vision",
"name": "OpenVibe.Vision",
"origin": "https://openvibe.vision",
-- "operations": 2,
-- "capabilities": 1
++ "operations": 32,
++ "capabilities": 20
},
{
"service": "watch",
ddiff --git a/generated/openapi/inventory.json b/generated/openapi/inventory.json
iindex 36de6cd2..d3932757 100644
---- a/generated/openapi/inventory.json
++++ b/generated/openapi/inventory.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Inventory API",
-- "version": "0.239.0",
-- "description": "Routes of OpenVibe.Inventory as the OpenVibe contracts describe them (openvibe-contracts 0.239.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.240.0",
++ "description": "Routes of OpenVibe.Inventory as the OpenVibe contracts describe them (openvibe-contracts 0.240.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/live.json b/generated/openapi/live.json
iindex 4178507b..25e12ffc 100644
---- a/generated/openapi/live.json
++++ b/generated/openapi/live.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Live API",
-- "version": "0.239.0",
-- "description": "Routes of OpenVibe.Live as the OpenVibe contracts describe them (openvibe-contracts 0.239.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.240.0",
++ "description": "Routes of OpenVibe.Live as the OpenVibe contracts describe them (openvibe-contracts 0.240.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/media-hub.json b/generated/openapi/media-hub.json
iindex b2eb0cb3..c107b3db 100644
---- a/generated/openapi/media-hub.json
++++ b/generated/openapi/media-hub.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.MediaHub API",
-- "version": "0.239.0",
-- "description": "Routes of OpenVibe.MediaHub as the OpenVibe contracts describe them (openvibe-contracts 0.239.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.240.0",
++ "description": "Routes of OpenVibe.MediaHub as the OpenVibe contracts describe them (openvibe-contracts 0.240.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/media.json b/generated/openapi/media.json
iindex d165d6ea..ec291d2a 100644
---- a/generated/openapi/media.json
++++ b/generated/openapi/media.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Media API",
-- "version": "0.239.0",
-- "description": "Routes of OpenVibe.Media as the OpenVibe contracts describe them (openvibe-contracts 0.239.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.240.0",
++ "description": "Routes of OpenVibe.Media as the OpenVibe contracts describe them (openvibe-contracts 0.240.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/network.json b/generated/openapi/network.json
iindex f4bdc609..a9d31f70 100644
---- a/generated/openapi/network.json
++++ b/generated/openapi/network.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Network API",
-- "version": "0.239.0",
-- "description": "Routes of OpenVibe.Network as the OpenVibe contracts describe them (openvibe-contracts 0.239.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.240.0",
++ "description": "Routes of OpenVibe.Network as the OpenVibe contracts describe them (openvibe-contracts 0.240.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/news.json b/generated/openapi/news.json
iindex 8a9fe20f..34ab8c91 100644
---- a/generated/openapi/news.json
++++ b/generated/openapi/news.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.News API",
-- "version": "0.239.0",
-- "description": "Routes of OpenVibe.News as the OpenVibe contracts describe them (openvibe-contracts 0.239.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.240.0",
++ "description": "Routes of OpenVibe.News as the OpenVibe contracts describe them (openvibe-contracts 0.240.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/openre.json b/generated/openapi/openre.json
iindex 40be05e4..8b1a8d17 100644
---- a/generated/openapi/openre.json
++++ b/generated/openapi/openre.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenRestream API",
-- "version": "0.239.0",
-- "description": "Routes of OpenRestream as the OpenVibe contracts describe them (openvibe-contracts 0.239.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.240.0",
++ "description": "Routes of OpenRestream as the OpenVibe contracts describe them (openvibe-contracts 0.240.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/play.json b/generated/openapi/play.json
iindex 47c4d104..22df3194 100644
---- a/generated/openapi/play.json
++++ b/generated/openapi/play.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Play API",
-- "version": "0.239.0",
-- "description": "Routes of OpenVibe.Play as the OpenVibe contracts describe them (openvibe-contracts 0.239.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.240.0",
++ "description": "Routes of OpenVibe.Play as the OpenVibe contracts describe them (openvibe-contracts 0.240.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/quest.json b/generated/openapi/quest.json
iindex f1205ffb..49603bcc 100644
---- a/generated/openapi/quest.json
++++ b/generated/openapi/quest.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Quest API",
-- "version": "0.239.0",
-- "description": "Routes of OpenVibe.Quest as the OpenVibe contracts describe them (openvibe-contracts 0.239.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.240.0",
++ "description": "Routes of OpenVibe.Quest as the OpenVibe contracts describe them (openvibe-contracts 0.240.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/reviews.json b/generated/openapi/reviews.json
iindex 6d28be3e..3a00c747 100644
---- a/generated/openapi/reviews.json
++++ b/generated/openapi/reviews.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Reviews API",
-- "version": "0.239.0",
-- "description": "Routes of OpenVibe.Reviews as the OpenVibe contracts describe them (openvibe-contracts 0.239.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.240.0",
++ "description": "Routes of OpenVibe.Reviews as the OpenVibe contracts describe them (openvibe-contracts 0.240.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/run.json b/generated/openapi/run.json
iindex 13fdbb8a..ddd4d1c3 100644
---- a/generated/openapi/run.json
++++ b/generated/openapi/run.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Run API",
-- "version": "0.239.0",
-- "description": "Routes of OpenVibe.Run as the OpenVibe contracts describe them (openvibe-contracts 0.239.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.240.0",
++ "description": "Routes of OpenVibe.Run as the OpenVibe contracts describe them (openvibe-contracts 0.240.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/search.json b/generated/openapi/search.json
iindex 834181db..7121488a 100644
---- a/generated/openapi/search.json
++++ b/generated/openapi/search.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Search API",
-- "version": "0.239.0",
-- "description": "Routes of OpenVibe.Search as the OpenVibe contracts describe them (openvibe-contracts 0.239.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.240.0",
++ "description": "Routes of OpenVibe.Search as the OpenVibe contracts describe them (openvibe-contracts 0.240.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/services.json b/generated/openapi/services.json
iindex 683b5e21..0e86740f 100644
---- a/generated/openapi/services.json
++++ b/generated/openapi/services.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Services API",
-- "version": "0.239.0",
-- "description": "Routes of OpenVibe.Services as the OpenVibe contracts describe them (openvibe-contracts 0.239.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.240.0",
++ "description": "Routes of OpenVibe.Services as the OpenVibe contracts describe them (openvibe-contracts 0.240.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/sources.json b/generated/openapi/sources.json
iindex eab27bc3..1c0996f6 100644
---- a/generated/openapi/sources.json
++++ b/generated/openapi/sources.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Sources API",
-- "version": "0.239.0",
-- "description": "Routes of OpenVibe.Sources as the OpenVibe contracts describe them (openvibe-contracts 0.239.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.240.0",
++ "description": "Routes of OpenVibe.Sources as the OpenVibe contracts describe them (openvibe-contracts 0.240.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/space.json b/generated/openapi/space.json
iindex ad0984ee..45169ae4 100644
---- a/generated/openapi/space.json
++++ b/generated/openapi/space.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Space API",
-- "version": "0.239.0",
-- "description": "Routes of OpenVibe.Space as the OpenVibe contracts describe them (openvibe-contracts 0.239.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.240.0",
++ "description": "Routes of OpenVibe.Space as the OpenVibe contracts describe them (openvibe-contracts 0.240.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/tips.json b/generated/openapi/tips.json
iindex 428beb6c..23e2bf25 100644
---- a/generated/openapi/tips.json
++++ b/generated/openapi/tips.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Tips API",
-- "version": "0.239.0",
-- "description": "Routes of OpenVibe.Tips as the OpenVibe contracts describe them (openvibe-contracts 0.239.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.240.0",
++ "description": "Routes of OpenVibe.Tips as the OpenVibe contracts describe them (openvibe-contracts 0.240.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/tools.json b/generated/openapi/tools.json
iindex 5c5a8af5..7572905c 100644
---- a/generated/openapi/tools.json
++++ b/generated/openapi/tools.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Tools API",
-- "version": "0.239.0",
-- "description": "Routes of OpenVibe.Tools as the OpenVibe contracts describe them (openvibe-contracts 0.239.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.240.0",
++ "description": "Routes of OpenVibe.Tools as the OpenVibe contracts describe them (openvibe-contracts 0.240.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/trade.json b/generated/openapi/trade.json
iindex c8c35b04..0cb27171 100644
---- a/generated/openapi/trade.json
++++ b/generated/openapi/trade.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Trade API",
-- "version": "0.239.0",
-- "description": "Routes of OpenVibe.Trade as the OpenVibe contracts describe them (openvibe-contracts 0.239.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.240.0",
++ "description": "Routes of OpenVibe.Trade as the OpenVibe contracts describe them (openvibe-contracts 0.240.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/vip.json b/generated/openapi/vip.json
iindex 47226444..89de768d 100644
---- a/generated/openapi/vip.json
++++ b/generated/openapi/vip.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.VIP API",
-- "version": "0.239.0",
-- "description": "Routes of OpenVibe.VIP as the OpenVibe contracts describe them (openvibe-contracts 0.239.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.240.0",
++ "description": "Routes of OpenVibe.VIP as the OpenVibe contracts describe them (openvibe-contracts 0.240.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/vision.json b/generated/openapi/vision.json
iindex 3dcfba2b..28d219ed 100644
---- a/generated/openapi/vision.json
++++ b/generated/openapi/vision.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Vision API",
-- "version": "0.239.0",
-- "description": "Routes of OpenVibe.Vision as the OpenVibe contracts describe them (openvibe-contracts 0.239.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.240.0",
++ "description": "Routes of OpenVibe.Vision as the OpenVibe contracts describe them (openvibe-contracts 0.240.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
@@@ -16,25 +16,25 @@
}
],
"paths": {
-- "/api/v1/ui-check": {
++ "/api/v1/alt": {
"post": {
-- "operationId": "post_api_v1_ui_check",
-- "summary": "Run OpenVibe.Vision's UI check: POST /api/v1/ui-check takes HTML, a public URL or a screenshot plus an optional viewport and returns overflow, cut-off text, low contrast, overlap and layering findings",
-- "description": "**vision.ui.check** (first-party): Run OpenVibe.Vision's UI check: POST /api/v1/ui-check takes HTML, a public URL or a screenshot plus an optional viewport and returns overflow, cut-off text, low contrast, overlap and layering findings with counts and severity; GET /api/v1/ui-checks/:id reads a saved check. OpenVibe.Design's Arena is the caller today; a signed-in person may also use it. The check runs entirely on Vision's host (static HTML/CSS parsing, or OCR boxes and pixel sampling for a screenshot): it executes no JavaScript and fetches only a public URL through the SSRF guard.",
++ "operationId": "post_api_v1_alt",
++ "summary": "Write editable alt text for an image on OpenVibe.Vision: POST /api/v1/alt returns one short sentence about the scene, objects and action, never about a person.",
++ "description": "**vision.alt.write** (first-party): Write editable alt text for an image on OpenVibe.Vision: POST /api/v1/alt returns one short sentence about the scene, objects and action, never about a person. The answer has passed noLooks; the receipt names the engine, cost and latency. Rooms use it for Pics, Media and Chat uploads.",
"tags": [
-- "vision.ui"
++ "vision.alt"
],
"x-openvibe-capabilities": [
-- "vision.ui.check"
++ "vision.alt.write"
],
"x-openvibe-visibility": [
"first-party"
],
-- "x-openvibe-quota-class": "vision-ui-check",
++ "x-openvibe-quota-class": "vision-alt",
"security": [
{
"openvibeToken": [
-- "vision.ui.check"
++ "vision.alt.write"
]
}
],
@@@ -43,7 +43,7 @@
"content": {
"application/json": {
"schema": {
-- "$ref": "#/components/schemas/vision.ui-check-request.v1"
++ "$ref": "#/components/schemas/vision.image-request.v1"
}
}
}
@@@ -54,7 +54,277 @@
"content": {
"application/json": {
"schema": {
-- "$ref": "#/components/schemas/vision.ui-check-result.v1"
++ "$ref": "#/components/schemas/vision.alt-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/ask": {
++ "post": {
++ "operationId": "post_api_v1_ask",
++ "summary": "Ask or describe an image on OpenVibe.Vision: POST /api/v1/ask and /api/v1/describe take an image (multipart, data URL or public URL), a question, a task (ask, describe, identify, count, translate, ocr",
++ "description": "**vision.image.ask** (first-party): Ask or describe an image on OpenVibe.Vision: POST /api/v1/ask and /api/v1/describe take an image (multipart, data URL or public URL), a question, a task (ask, describe, identify, count, translate, ocr) and a mode (auto, local, ai). A read question is answered by the local OCR reader first; everything else goes through OpenVibe.AI, whose provenance the answer reports. In auto mode a read question also works anonymously; any other task needs a signed-in person or this capability.",
++ "tags": [
++ "vision.image"
++ ],
++ "x-openvibe-capabilities": [
++ "vision.image.ask"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "vision-ask",
++ "security": [
++ {
++ "openvibeToken": [
++ "vision.image.ask"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/vision.ask-request.v1"
++ }
++ }
++ }
++ },
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/vision.answer-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/benchmarks": {
++ "get": {
++ "operationId": "get_api_v1_benchmarks",
++ "summary": "Read OpenVibe.Vision's aggregate comparison results: GET /api/v1/benchmarks groups compare samples by task, engine, provider and model with sample counts, mean latency and mean cost, with no subject, ",
++ "description": "**vision.benchmarks.read** (first-party): Read OpenVibe.Vision's aggregate comparison results: GET /api/v1/benchmarks groups compare samples by task, engine, provider and model with sample counts, mean latency and mean cost, with no subject, image or answer text. Public.",
++ "tags": [
++ "vision.benchmarks"
++ ],
++ "x-openvibe-capabilities": [
++ "vision.benchmarks.read"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "vision-read",
++ "security": [
++ {
++ "openvibeToken": [
++ "vision.benchmarks.read"
++ ]
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/vision.benchmarks.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/compare": {
++ "post": {
++ "operationId": "post_api_v1_compare",
++ "summary": "Run OpenVibe.Vision's side-by-side comparison: POST /api/v1/compare runs the local reader and up to three AI model hints on the same image and returns each row with its success, text, cost and latency.",
++ "description": "**vision.model.compare** (first-party): Run OpenVibe.Vision's side-by-side comparison: POST /api/v1/compare runs the local reader and up to three AI model hints on the same image and returns each row with its success, text, cost and latency. The requested model is a hint; OpenVibe.AI's own provenance is what each row reports.",
++ "tags": [
++ "vision.model"
++ ],
++ "x-openvibe-capabilities": [
++ "vision.model.compare"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "vision-compare",
++ "security": [
++ {
++ "openvibeToken": [
++ "vision.model.compare"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/vision.compare-request.v1"
++ }
++ }
++ }
++ },
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/vision.answer-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/describe": {
++ "post": {
++ "operationId": "post_api_v1_describe",
++ "summary": "Ask or describe an image on OpenVibe.Vision: POST /api/v1/ask and /api/v1/describe take an image (multipart, data URL or public URL), a question, a task (ask, describe, identify, count, translate, ocr",
++ "description": "**vision.image.ask** (first-party): Ask or describe an image on OpenVibe.Vision: POST /api/v1/ask and /api/v1/describe take an image (multipart, data URL or public URL), a question, a task (ask, describe, identify, count, translate, ocr) and a mode (auto, local, ai). A read question is answered by the local OCR reader first; everything else goes through OpenVibe.AI, whose provenance the answer reports. In auto mode a read question also works anonymously; any other task needs a signed-in person or this capability.",
++ "tags": [
++ "vision.image"
++ ],
++ "x-openvibe-capabilities": [
++ "vision.image.ask"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "vision-ask",
++ "security": [
++ {
++ "openvibeToken": [
++ "vision.image.ask"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/vision.ask-request.v1"
++ }
++ }
++ }
++ },
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/vision.answer-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/faces/blur": {
++ "post": {
++ "operationId": "post_api_v1_faces_blur",
++ "summary": "Identity-free face blurring on OpenVibe.Vision: POST /api/v1/faces/blur returns a redacted PNG derivative and the blurred count in X-Faces-Blur.",
++ "description": "**vision.face.blur** (first-party): Identity-free face blurring on OpenVibe.Vision: POST /api/v1/faces/blur returns a redacted PNG derivative and the blurred count in X-Faces-Blur. No template is stored and the original is discarded; anyone may use it.",
++ "tags": [
++ "vision.face"
++ ],
++ "x-openvibe-capabilities": [
++ "vision.face.blur"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "vision-face",
++ "security": [
++ {
++ "openvibeToken": [
++ "vision.face.blur"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/vision.image-request.v1"
++ }
++ }
++ }
++ },
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/vision.face-blur.v1"
}
}
}
@@@ -70,133 +340,3619 @@
}
}
}
-- }
-- },
-- "/api/v1/ui-checks/{id}": {
-- "get": {
-- "operationId": "get_api_v1_ui_checks_id",
-- "summary": "Run OpenVibe.Vision's UI check: POST /api/v1/ui-check takes HTML, a public URL or a screenshot plus an optional viewport and returns overflow, cut-off text, low contrast, overlap and layering findings",
-- "description": "**vision.ui.check** (first-party): Run OpenVibe.Vision's UI check: POST /api/v1/ui-check takes HTML, a public URL or a screenshot plus an optional viewport and returns overflow, cut-off text, low contrast, overlap and layering findings with counts and severity; GET /api/v1/ui-checks/:id reads a saved check. OpenVibe.Design's Arena is the caller today; a signed-in person may also use it. The check runs entirely on Vision's host (static HTML/CSS parsing, or OCR boxes and pixel sampling for a screenshot): it executes no JavaScript and fetches only a public URL through the SSRF guard.",
-- "tags": [
-- "vision.ui"
-- ],
-- "x-openvibe-capabilities": [
-- "vision.ui.check"
-- ],
-- "x-openvibe-visibility": [
-- "first-party"
-- ],
-- "x-openvibe-quota-class": "vision-ui-check",
-- "security": [
-- {
-- "openvibeToken": [
-- "vision.ui.check"
-- ]
++ }
++ },
++ "/api/v1/faces/cast/match": {
++ "post": {
++ "operationId": "post_api_v1_faces_cast_match",
++ "summary": "Match faces against one channel's opted-in adult cast roster for OpenVibe.Live: POST /api/v1/faces/cast/match.",
++ "description": "**vision.face.cast.match** (first-party): Match faces against one channel's opted-in adult cast roster for OpenVibe.Live: POST /api/v1/faces/cast/match. The caller is the Live service token scoped to that channel (its channel_id claim must equal the body's channel) and must hold this capability. A non-consented person is never matched and the answer does not say whether they exist.",
++ "tags": [
++ "vision.face"
++ ],
++ "x-openvibe-capabilities": [
++ "vision.face.cast.match"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "vision-face",
++ "security": [
++ {
++ "openvibeToken": [
++ "vision.face.cast.match"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/vision.face-cast-match-request.v1"
++ }
++ }
++ }
++ },
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/vision.face-cast-match.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/faces/cast/tags": {
++ "get": {
++ "operationId": "get_api_v1_faces_cast_tags",
++ "summary": "Read your own cast tags on OpenVibe.Vision: GET /api/v1/faces/cast/tags lists the named channel and media reference for each tag the person's own cast consent produced.",
++ "description": "**vision.face.cast.read** (first-party): Read your own cast tags on OpenVibe.Vision: GET /api/v1/faces/cast/tags lists the named channel and media reference for each tag the person's own cast consent produced. Revoking the consent removes them.",
++ "tags": [
++ "vision.face"
++ ],
++ "x-openvibe-capabilities": [
++ "vision.face.cast.read"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "vision-face",
++ "security": [
++ {
++ "openvibeToken": [
++ "vision.face.cast.read"
++ ]
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/vision.face-tag-list.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/faces/consents": {
++ "get": {
++ "operationId": "get_api_v1_faces_consents",
++ "summary": "Read, grant and revoke purpose-scoped face or voice consent on OpenVibe.Vision: GET /api/v1/faces/consents and /consents/text, POST /consents/:feature with the signed current text, and DELETE /consent",
++ "description": "**vision.face.consent.manage** (first-party): Read, grant and revoke purpose-scoped face or voice consent on OpenVibe.Vision: GET /api/v1/faces/consents and /consents/text, POST /consents/:feature with the signed current text, and DELETE /consents/:feature, which revokes and erases the template and derived data in one transaction. A person manages only their own record; the record says whose face or voice, the scope, expiry and how it was revoked (vision.consent@1).",
++ "tags": [
++ "vision.face"
++ ],
++ "x-openvibe-capabilities": [
++ "vision.face.consent.manage"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "vision-face",
++ "security": [
++ {
++ "openvibeToken": [
++ "vision.face.consent.manage"
++ ]
++ }
++ ],
++ "x-openvibe-input": {
++ "$ref": "#/components/schemas/vision.consent-request.v1"
++ },
++ "parameters": [
++ {
++ "name": "signed_name",
++ "in": "query",
++ "required": true,
++ "schema": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 160
++ }
++ },
++ {
++ "name": "text_version",
++ "in": "query",
++ "required": true,
++ "schema": {
++ "type": "string",
++ "maxLength": 80
++ }
++ },
++ {
++ "name": "modality",
++ "in": "query",
++ "required": false,
++ "schema": {
++ "enum": [
++ "face",
++ "voice"
++ ]
++ }
++ },
++ {
++ "name": "feature",
++ "in": "query",
++ "required": false,
++ "schema": {
++ "enum": [
++ "verify",
++ "find",
++ "cast",
++ "library",
++ "voice"
++ ],
++ "description": "Echo of the :feature path segment."
++ },
++ "description": "Echo of the :feature path segment."
++ },
++ {
++ "name": "channel",
++ "in": "query",
++ "required": false,
++ "schema": {
++ "type": "string",
++ "pattern": "^[a-zA-Z0-9][a-zA-Z0-9:_-]{1,119}$"
++ }
++ },
++ {
++ "name": "adult",
++ "in": "query",
++ "required": false,
++ "schema": {
++ "type": "boolean"
++ }
++ },
++ {
++ "name": "region",
++ "in": "query",
++ "required": false,
++ "schema": {
++ "type": "string",
++ "maxLength": 10
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/vision.consent.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/faces/consents/{feature}": {
++ "delete": {
++ "operationId": "delete_api_v1_faces_consents_feature",
++ "summary": "Read, grant and revoke purpose-scoped face or voice consent on OpenVibe.Vision: GET /api/v1/faces/consents and /consents/text, POST /consents/:feature with the signed current text, and DELETE /consent",
++ "description": "**vision.face.consent.manage** (first-party): Read, grant and revoke purpose-scoped face or voice consent on OpenVibe.Vision: GET /api/v1/faces/consents and /consents/text, POST /consents/:feature with the signed current text, and DELETE /consents/:feature, which revokes and erases the template and derived data in one transaction. A person manages only their own record; the record says whose face or voice, the scope, expiry and how it was revoked (vision.consent@1).",
++ "tags": [
++ "vision.face"
++ ],
++ "x-openvibe-capabilities": [
++ "vision.face.consent.manage"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "vision-face",
++ "security": [
++ {
++ "openvibeToken": [
++ "vision.face.consent.manage"
++ ]
++ }
++ ],
++ "x-openvibe-input": {
++ "$ref": "#/components/schemas/vision.consent-request.v1"
++ },
++ "parameters": [
++ {
++ "name": "feature",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ },
++ {
++ "name": "signed_name",
++ "in": "query",
++ "required": true,
++ "schema": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 160
++ }
++ },
++ {
++ "name": "text_version",
++ "in": "query",
++ "required": true,
++ "schema": {
++ "type": "string",
++ "maxLength": 80
++ }
++ },
++ {
++ "name": "modality",
++ "in": "query",
++ "required": false,
++ "schema": {
++ "enum": [
++ "face",
++ "voice"
++ ]
++ }
++ },
++ {
++ "name": "channel",
++ "in": "query",
++ "required": false,
++ "schema": {
++ "type": "string",
++ "pattern": "^[a-zA-Z0-9][a-zA-Z0-9:_-]{1,119}$"
++ }
++ },
++ {
++ "name": "adult",
++ "in": "query",
++ "required": false,
++ "schema": {
++ "type": "boolean"
++ }
++ },
++ {
++ "name": "region",
++ "in": "query",
++ "required": false,
++ "schema": {
++ "type": "string",
++ "maxLength": 10
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/vision.consent.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ },
++ "post": {
++ "operationId": "post_api_v1_faces_consents_feature",
++ "summary": "Read, grant and revoke purpose-scoped face or voice consent on OpenVibe.Vision: GET /api/v1/faces/consents and /consents/text, POST /consents/:feature with the signed current text, and DELETE /consent",
++ "description": "**vision.face.consent.manage** (first-party): Read, grant and revoke purpose-scoped face or voice consent on OpenVibe.Vision: GET /api/v1/faces/consents and /consents/text, POST /consents/:feature with the signed current text, and DELETE /consents/:feature, which revokes and erases the template and derived data in one transaction. A person manages only their own record; the record says whose face or voice, the scope, expiry and how it was revoked (vision.consent@1).",
++ "tags": [
++ "vision.face"
++ ],
++ "x-openvibe-capabilities": [
++ "vision.face.consent.manage"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "vision-face",
++ "security": [
++ {
++ "openvibeToken": [
++ "vision.face.consent.manage"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/vision.consent-request.v1"
++ }
++ }
++ }
++ },
++ "parameters": [
++ {
++ "name": "feature",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/vision.consent.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/faces/consents/text": {
++ "get": {
++ "operationId": "get_api_v1_faces_consents_text",
++ "summary": "Read, grant and revoke purpose-scoped face or voice consent on OpenVibe.Vision: GET /api/v1/faces/consents and /consents/text, POST /consents/:feature with the signed current text, and DELETE /consent",
++ "description": "**vision.face.consent.manage** (first-party): Read, grant and revoke purpose-scoped face or voice consent on OpenVibe.Vision: GET /api/v1/faces/consents and /consents/text, POST /consents/:feature with the signed current text, and DELETE /consents/:feature, which revokes and erases the template and derived data in one transaction. A person manages only their own record; the record says whose face or voice, the scope, expiry and how it was revoked (vision.consent@1).",
++ "tags": [
++ "vision.face"
++ ],
++ "x-openvibe-capabilities": [
++ "vision.face.consent.manage"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "vision-face",
++ "security": [
++ {
++ "openvibeToken": [
++ "vision.face.consent.manage"
++ ]
++ }
++ ],
++ "x-openvibe-input": {
++ "$ref": "#/components/schemas/vision.consent-request.v1"
++ },
++ "parameters": [
++ {
++ "name": "signed_name",
++ "in": "query",
++ "required": true,
++ "schema": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 160
++ }
++ },
++ {
++ "name": "text_version",
++ "in": "query",
++ "required": true,
++ "schema": {
++ "type": "string",
++ "maxLength": 80
++ }
++ },
++ {
++ "name": "modality",
++ "in": "query",
++ "required": false,
++ "schema": {
++ "enum": [
++ "face",
++ "voice"
++ ]
++ }
++ },
++ {
++ "name": "feature",
++ "in": "query",
++ "required": false,
++ "schema": {
++ "enum": [
++ "verify",
++ "find",
++ "cast",
++ "library",
++ "voice"
++ ],
++ "description": "Echo of the :feature path segment."
++ },
++ "description": "Echo of the :feature path segment."
++ },
++ {
++ "name": "channel",
++ "in": "query",
++ "required": false,
++ "schema": {
++ "type": "string",
++ "pattern": "^[a-zA-Z0-9][a-zA-Z0-9:_-]{1,119}$"
++ }
++ },
++ {
++ "name": "adult",
++ "in": "query",
++ "required": false,
++ "schema": {
++ "type": "boolean"
++ }
++ },
++ {
++ "name": "region",
++ "in": "query",
++ "required": false,
++ "schema": {
++ "type": "string",
++ "maxLength": 10
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/vision.consent.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/faces/detect": {
++ "post": {
++ "operationId": "post_api_v1_faces_detect",
++ "summary": "Identity-free face detection on OpenVibe.Vision: POST /api/v1/faces/detect counts faces and returns their boxes and scores with local YuNet.",
++ "description": "**vision.face.detect** (first-party): Identity-free face detection on OpenVibe.Vision: POST /api/v1/faces/detect counts faces and returns their boxes and scores with local YuNet. It stores no template, names no one and needs no consent; anyone may use it.",
++ "tags": [
++ "vision.face"
++ ],
++ "x-openvibe-capabilities": [
++ "vision.face.detect"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "vision-face",
++ "security": [
++ {
++ "openvibeToken": [
++ "vision.face.detect"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/vision.image-request.v1"
++ }
++ }
++ }
++ },
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/vision.face-detection.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/faces/enrol/{feature}": {
++ "post": {
++ "operationId": "post_api_v1_faces_enrol_feature",
++ "summary": "Verify yourself against your own enrolled template on OpenVibe.Vision: POST /api/v1/faces/enrol/verify and /api/v1/faces/verify.",
++ "description": "**vision.face.verify.self** (first-party): Verify yourself against your own enrolled template on OpenVibe.Vision: POST /api/v1/faces/enrol/verify and /api/v1/faces/verify. Refused with faces.consent_required without that person's verify consent, the same answer whether or not a template exists; a match alone unlocks nothing.",
++ "tags": [
++ "vision.face"
++ ],
++ "x-openvibe-capabilities": [
++ "vision.face.verify.self"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "vision-face",
++ "security": [
++ {
++ "openvibeToken": [
++ "vision.face.verify.self"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/vision.face-enrol-request.v1"
++ }
++ }
++ }
++ },
++ "parameters": [
++ {
++ "name": "feature",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/vision.face-verify.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/faces/find": {
++ "post": {
++ "operationId": "post_api_v1_faces_find",
++ "summary": "Find yourself in your own private uploads on OpenVibe.Vision: POST /api/v1/faces/media, GET /api/v1/faces/media/:id and POST /api/v1/faces/find.",
++ "description": "**vision.face.find.self** (first-party): Find yourself in your own private uploads on OpenVibe.Vision: POST /api/v1/faces/media, GET /api/v1/faces/media/:id and POST /api/v1/faces/find. Refused with faces.consent_required without find consent, the same answer whether or not any media exists; results are private links to the caller's own rows.",
++ "tags": [
++ "vision.face"
++ ],
++ "x-openvibe-capabilities": [
++ "vision.face.find.self"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "vision-face",
++ "security": [
++ {
++ "openvibeToken": [
++ "vision.face.find.self"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/vision.find-request.v1"
++ }
++ }
++ }
++ },
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/vision.face-find-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/faces/library/cluster": {
++ "post": {
++ "operationId": "post_api_v1_faces_library_cluster",
++ "summary": "Group faces in your own Vision library: POST /api/v1/faces/library/cluster (region-gated), GET /api/v1/faces/library/groups and POST /api/v1/faces/library/correct (exclude, merge, split).",
++ "description": "**vision.face.library** (first-party): Group faces in your own Vision library: POST /api/v1/faces/library/cluster (region-gated), GET /api/v1/faces/library/groups and POST /api/v1/faces/library/correct (exclude, merge, split). Requires library consent; unknown faces stay unlabelled and nothing leaves that person's library.",
++ "tags": [
++ "vision.face"
++ ],
++ "x-openvibe-capabilities": [
++ "vision.face.library"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "vision-face",
++ "security": [
++ {
++ "openvibeToken": [
++ "vision.face.library"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/vision.face-library-request.v1"
++ }
++ }
++ }
++ },
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/vision.face-library-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/faces/library/correct": {
++ "post": {
++ "operationId": "post_api_v1_faces_library_correct",
++ "summary": "Group faces in your own Vision library: POST /api/v1/faces/library/cluster (region-gated), GET /api/v1/faces/library/groups and POST /api/v1/faces/library/correct (exclude, merge, split).",
++ "description": "**vision.face.library** (first-party): Group faces in your own Vision library: POST /api/v1/faces/library/cluster (region-gated), GET /api/v1/faces/library/groups and POST /api/v1/faces/library/correct (exclude, merge, split). Requires library consent; unknown faces stay unlabelled and nothing leaves that person's library.",
++ "tags": [
++ "vision.face"
++ ],
++ "x-openvibe-capabilities": [
++ "vision.face.library"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "vision-face",
++ "security": [
++ {
++ "openvibeToken": [
++ "vision.face.library"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/vision.face-library-request.v1"
++ }
++ }
++ }
++ },
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/vision.face-library-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/faces/library/groups": {
++ "get": {
++ "operationId": "get_api_v1_faces_library_groups",
++ "summary": "Group faces in your own Vision library: POST /api/v1/faces/library/cluster (region-gated), GET /api/v1/faces/library/groups and POST /api/v1/faces/library/correct (exclude, merge, split).",
++ "description": "**vision.face.library** (first-party): Group faces in your own Vision library: POST /api/v1/faces/library/cluster (region-gated), GET /api/v1/faces/library/groups and POST /api/v1/faces/library/correct (exclude, merge, split). Requires library consent; unknown faces stay unlabelled and nothing leaves that person's library.",
++ "tags": [
++ "vision.face"
++ ],
++ "x-openvibe-capabilities": [
++ "vision.face.library"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "vision-face",
++ "security": [
++ {
++ "openvibeToken": [
++ "vision.face.library"
++ ]
++ }
++ ],
++ "x-openvibe-input": {
++ "$ref": "#/components/schemas/vision.face-library-request.v1"
++ },
++ "parameters": [
++ {
++ "name": "action",
++ "in": "query",
++ "required": true,
++ "schema": {
++ "enum": [
++ "cluster",
++ "exclude",
++ "merge",
++ "split"
++ ]
++ }
++ },
++ {
++ "name": "region",
++ "in": "query",
++ "required": false,
++ "schema": {
++ "type": "string",
++ "maxLength": 10
++ }
++ },
++ {
++ "name": "ids",
++ "in": "query",
++ "required": false,
++ "schema": {
++ "type": "array",
++ "items": {
++ "type": "string",
++ "maxLength": 40
++ },
++ "minItems": 1,
++ "maxItems": 100
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/vision.face-library-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/faces/media": {
++ "post": {
++ "operationId": "post_api_v1_faces_media",
++ "summary": "Find yourself in your own private uploads on OpenVibe.Vision: POST /api/v1/faces/media, GET /api/v1/faces/media/:id and POST /api/v1/faces/find.",
++ "description": "**vision.face.find.self** (first-party): Find yourself in your own private uploads on OpenVibe.Vision: POST /api/v1/faces/media, GET /api/v1/faces/media/:id and POST /api/v1/faces/find. Refused with faces.consent_required without find consent, the same answer whether or not any media exists; results are private links to the caller's own rows.",
++ "tags": [
++ "vision.face"
++ ],
++ "x-openvibe-capabilities": [
++ "vision.face.find.self"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "vision-face",
++ "security": [
++ {
++ "openvibeToken": [
++ "vision.face.find.self"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/vision.find-request.v1"
++ }
++ }
++ }
++ },
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/vision.face-find-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/faces/media/{id}": {
++ "get": {
++ "operationId": "get_api_v1_faces_media_id",
++ "summary": "Find yourself in your own private uploads on OpenVibe.Vision: POST /api/v1/faces/media, GET /api/v1/faces/media/:id and POST /api/v1/faces/find.",
++ "description": "**vision.face.find.self** (first-party): Find yourself in your own private uploads on OpenVibe.Vision: POST /api/v1/faces/media, GET /api/v1/faces/media/:id and POST /api/v1/faces/find. Refused with faces.consent_required without find consent, the same answer whether or not any media exists; results are private links to the caller's own rows.",
++ "tags": [
++ "vision.face"
++ ],
++ "x-openvibe-capabilities": [
++ "vision.face.find.self"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "vision-face",
++ "security": [
++ {
++ "openvibeToken": [
++ "vision.face.find.self"
++ ]
++ }
++ ],
++ "x-openvibe-input": {
++ "$ref": "#/components/schemas/vision.find-request.v1"
++ },
++ "parameters": [
++ {
++ "name": "id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/vision.face-find-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/faces/verify": {
++ "post": {
++ "operationId": "post_api_v1_faces_verify",
++ "summary": "Verify yourself against your own enrolled template on OpenVibe.Vision: POST /api/v1/faces/enrol/verify and /api/v1/faces/verify.",
++ "description": "**vision.face.verify.self** (first-party): Verify yourself against your own enrolled template on OpenVibe.Vision: POST /api/v1/faces/enrol/verify and /api/v1/faces/verify. Refused with faces.consent_required without that person's verify consent, the same answer whether or not a template exists; a match alone unlocks nothing.",
++ "tags": [
++ "vision.face"
++ ],
++ "x-openvibe-capabilities": [
++ "vision.face.verify.self"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "vision-face",
++ "security": [
++ {
++ "openvibeToken": [
++ "vision.face.verify.self"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/vision.face-enrol-request.v1"
++ }
++ }
++ }
++ },
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/vision.face-verify.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/find": {
++ "get": {
++ "operationId": "get_api_v1_find",
++ "summary": "Index and search a person's own images by CLIP embedding on OpenVibe.Vision: POST /api/v1/index stores one scoped embedding per item and GET/POST /api/v1/find searches only that person's own private i",
++ "description": "**vision.media.find** (first-party): Index and search a person's own images by CLIP embedding on OpenVibe.Vision: POST /api/v1/index stores one scoped embedding per item and GET/POST /api/v1/find searches only that person's own private index, by words or by an example image. No public image index is made; every read is owner-checked.",
++ "tags": [
++ "vision.media"
++ ],
++ "x-openvibe-capabilities": [
++ "vision.media.find"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "vision-find",
++ "security": [
++ {
++ "openvibeToken": [
++ "vision.media.find"
++ ]
++ }
++ ],
++ "x-openvibe-input": {
++ "$ref": "#/components/schemas/vision.find-request.v1"
++ },
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/vision.find-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ },
++ "post": {
++ "operationId": "post_api_v1_find",
++ "summary": "Index and search a person's own images by CLIP embedding on OpenVibe.Vision: POST /api/v1/index stores one scoped embedding per item and GET/POST /api/v1/find searches only that person's own private i",
++ "description": "**vision.media.find** (first-party): Index and search a person's own images by CLIP embedding on OpenVibe.Vision: POST /api/v1/index stores one scoped embedding per item and GET/POST /api/v1/find searches only that person's own private index, by words or by an example image. No public image index is made; every read is owner-checked.",
++ "tags": [
++ "vision.media"
++ ],
++ "x-openvibe-capabilities": [
++ "vision.media.find"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "vision-find",
++ "security": [
++ {
++ "openvibeToken": [
++ "vision.media.find"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/vision.find-request.v1"
++ }
++ }
++ }
++ },
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/vision.find-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/index": {
++ "post": {
++ "operationId": "post_api_v1_index",
++ "summary": "Index and search a person's own images by CLIP embedding on OpenVibe.Vision: POST /api/v1/index stores one scoped embedding per item and GET/POST /api/v1/find searches only that person's own private i",
++ "description": "**vision.media.find** (first-party): Index and search a person's own images by CLIP embedding on OpenVibe.Vision: POST /api/v1/index stores one scoped embedding per item and GET/POST /api/v1/find searches only that person's own private index, by words or by an example image. No public image index is made; every read is owner-checked.",
++ "tags": [
++ "vision.media"
++ ],
++ "x-openvibe-capabilities": [
++ "vision.media.find"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "vision-find",
++ "security": [
++ {
++ "openvibeToken": [
++ "vision.media.find"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/vision.find-request.v1"
++ }
++ }
++ }
++ },
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/vision.find-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/models": {
++ "get": {
++ "operationId": "get_api_v1_models",
++ "summary": "Read OpenVibe.Vision's model menu: GET /api/v1/models returns the AI route's offered provider:model hints, the configured hints and the local engines present, with no subject data.",
++ "description": "**vision.models.read** (first-party): Read OpenVibe.Vision's model menu: GET /api/v1/models returns the AI route's offered provider:model hints, the configured hints and the local engines present, with no subject data. Public; OpenVibe.AI's router is still the authority.",
++ "tags": [
++ "vision.models"
++ ],
++ "x-openvibe-capabilities": [
++ "vision.models.read"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "vision-read",
++ "security": [
++ {
++ "openvibeToken": [
++ "vision.models.read"
++ ]
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/vision.models.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/moments": {
++ "post": {
++ "operationId": "post_api_v1_moments",
++ "summary": "Find moments in a stream or VOD on OpenVibe.Vision: POST /api/v1/moments compares up to 30 timestamped frames with CLIP prompt embeddings and a histogram scene-change pass, returning scene changes and",
++ "description": "**vision.video.moments** (first-party): Find moments in a stream or VOD on OpenVibe.Vision: POST /api/v1/moments compares up to 30 timestamped frames with CLIP prompt embeddings and a histogram scene-change pass, returning scene changes and candidate moments. A candidate is a suggestion for Live or Media to accept.",
++ "tags": [
++ "vision.video"
++ ],
++ "x-openvibe-capabilities": [
++ "vision.video.moments"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "vision-moments",
++ "security": [
++ {
++ "openvibeToken": [
++ "vision.video.moments"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/vision.moments-request.v1"
++ }
++ }
++ }
++ },
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/vision.moments-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/ocr": {
++ "post": {
++ "operationId": "post_api_v1_ocr",
++ "summary": "Read text from an image with OpenVibe.Vision's local Tesseract 5 reader: POST /api/v1/ocr returns the text, mean confidence and word boxes at zero cost, anonymously.",
++ "description": "**vision.text.read** (first-party): Read text from an image with OpenVibe.Vision's local Tesseract 5 reader: POST /api/v1/ocr returns the text, mean confidence and word boxes at zero cost, anonymously. `fallback` asks OpenVibe.AI only when the local pass found no usable text, and that fallback needs a signed-in person or this capability.",
++ "tags": [
++ "vision.text"
++ ],
++ "x-openvibe-capabilities": [
++ "vision.text.read"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "vision-ocr",
++ "security": [
++ {
++ "openvibeToken": [
++ "vision.text.read"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/vision.ocr-request.v1"
++ }
++ }
++ }
++ },
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/vision.answer-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/product": {
++ "post": {
++ "operationId": "post_api_v1_product",
++ "summary": "Match a product photo to offers on OpenVibe.Vision: POST /api/v1/product describes the product only (never a person), embeds it with CLIP and returns candidate offers from OpenVibe.Deals and open.shoe",
++ "description": "**vision.product.match** (first-party): Match a product photo to offers on OpenVibe.Vision: POST /api/v1/product describes the product only (never a person), embeds it with CLIP and returns candidate offers from OpenVibe.Deals and open.shoes with their score and URL. A candidate is not a verified price.",
++ "tags": [
++ "vision.product"
++ ],
++ "x-openvibe-capabilities": [
++ "vision.product.match"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "vision-product",
++ "security": [
++ {
++ "openvibeToken": [
++ "vision.product.match"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/vision.product-request.v1"
++ }
++ }
++ }
++ },
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/vision.product-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/receipt": {
++ "post": {
++ "operationId": "post_api_v1_receipt",
++ "summary": "Read a receipt on OpenVibe.Vision: POST /api/v1/receipt returns the store, line items and total the local OCR reader extracted; `fallback` asks OpenVibe.AI only when the local pass could not produce b",
++ "description": "**vision.receipt.read** (first-party): Read a receipt on OpenVibe.Vision: POST /api/v1/receipt returns the store, line items and total the local OCR reader extracted; `fallback` asks OpenVibe.AI only when the local pass could not produce both. OpenVibe.Food decides what enters a pantry.",
++ "tags": [
++ "vision.receipt"
++ ],
++ "x-openvibe-capabilities": [
++ "vision.receipt.read"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "vision-receipt",
++ "security": [
++ {
++ "openvibeToken": [
++ "vision.receipt.read"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/vision.receipt-request.v1"
++ }
++ }
++ }
++ },
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/vision.receipt-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/ui-check": {
++ "post": {
++ "operationId": "post_api_v1_ui_check",
++ "summary": "Run OpenVibe.Vision's UI check: POST /api/v1/ui-check takes HTML, a public URL or a screenshot plus an optional viewport and returns overflow, cut-off text, low contrast, overlap and layering findings",
++ "description": "**vision.ui.check** (first-party): Run OpenVibe.Vision's UI check: POST /api/v1/ui-check takes HTML, a public URL or a screenshot plus an optional viewport and returns overflow, cut-off text, low contrast, overlap and layering findings with counts and severity; GET /api/v1/ui-checks/:id reads a saved check. OpenVibe.Design's Arena is the caller today; a signed-in person may also use it. The check runs entirely on Vision's host (static HTML/CSS parsing, or OCR boxes and pixel sampling for a screenshot): it executes no JavaScript and fetches only a public URL through the SSRF guard.",
++ "tags": [
++ "vision.ui"
++ ],
++ "x-openvibe-capabilities": [
++ "vision.ui.check"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "vision-ui-check",
++ "security": [
++ {
++ "openvibeToken": [
++ "vision.ui.check"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/vision.ui-check-request.v1"
++ }
++ }
++ }
++ },
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/vision.ui-check-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/ui-checks/{id}": {
++ "get": {
++ "operationId": "get_api_v1_ui_checks_id",
++ "summary": "Run OpenVibe.Vision's UI check: POST /api/v1/ui-check takes HTML, a public URL or a screenshot plus an optional viewport and returns overflow, cut-off text, low contrast, overlap and layering findings",
++ "description": "**vision.ui.check** (first-party): Run OpenVibe.Vision's UI check: POST /api/v1/ui-check takes HTML, a public URL or a screenshot plus an optional viewport and returns overflow, cut-off text, low contrast, overlap and layering findings with counts and severity; GET /api/v1/ui-checks/:id reads a saved check. OpenVibe.Design's Arena is the caller today; a signed-in person may also use it. The check runs entirely on Vision's host (static HTML/CSS parsing, or OCR boxes and pixel sampling for a screenshot): it executes no JavaScript and fetches only a public URL through the SSRF guard.",
++ "tags": [
++ "vision.ui"
++ ],
++ "x-openvibe-capabilities": [
++ "vision.ui.check"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "vision-ui-check",
++ "security": [
++ {
++ "openvibeToken": [
++ "vision.ui.check"
++ ]
++ }
++ ],
++ "x-openvibe-input": {
++ "$ref": "#/components/schemas/vision.ui-check-request.v1"
++ },
++ "parameters": [
++ {
++ "name": "id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/vision.ui-check-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/watch": {
++ "post": {
++ "operationId": "post_api_v1_watch",
++ "summary": "Watch a camera still on OpenVibe.Vision: POST /api/v1/watch finds the requested classes (DETR labels, snow, delivery, package through CLIP, or a histogram scene change against previous_image) and retu",
++ "description": "**vision.camera.watch** (first-party): Watch a camera still on OpenVibe.Vision: POST /api/v1/watch finds the requested classes (DETR labels, snow, delivery, package through CLIP, or a histogram scene change against previous_image) and returns detections with confidence and boxes. Owner-granted camera alerting only; a watched class can raise vision.alert.",
++ "tags": [
++ "vision.camera"
++ ],
++ "x-openvibe-capabilities": [
++ "vision.camera.watch"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "vision-watch",
++ "security": [
++ {
++ "openvibeToken": [
++ "vision.camera.watch"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/vision.watch-request.v1"
++ }
++ }
++ }
++ },
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/vision.watch-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "components": {
++ "schemas": {
++ "errors.problem.v1": {
++ "title": "Problem",
++ "description": "RFC 9457 problem details (application/problem+json) with a stable OpenVibe error code and trace identifiers. The legacy 'error' string is allowed during migration so existing clients keep working.",
++ "type": "object",
++ "required": [
++ "type",
++ "title",
++ "status",
++ "code"
++ ],
++ "additionalProperties": true,
++ "properties": {
++ "type": {
++ "type": "string",
++ "format": "uri-reference"
++ },
++ "title": {
++ "type": "string",
++ "minLength": 1
++ },
++ "status": {
++ "type": "integer",
++ "minimum": 400,
++ "maximum": 599
++ },
++ "detail": {
++ "type": "string"
++ },
++ "instance": {
++ "type": "string",
++ "format": "uri-reference"
++ },
++ "code": {
++ "type": "string",
++ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+)*$"
++ },
++ "request_id": {
++ "type": "string"
++ },
++ "trace_id": {
++ "type": "string",
++ "pattern": "^[0-9a-f]{32}$"
++ },
++ "errors": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "message"
++ ],
++ "properties": {
++ "path": {
++ "type": "string"
++ },
++ "message": {
++ "type": "string"
++ }
++ }
++ }
++ },
++ "error": {
++ "type": "string",
++ "description": "Deprecated compatibility field: same text as title/detail for clients that read {error}."
++ }
++ }
++ },
++ "vision.alt-result.v1": {
++ "title": "VisionAltResult",
++ "description": "vision.alt-result@1: POST /api/v1/alt answers one short, editable alt-text sentence about the scene, objects and action, never about a person. The text has passed noLooks; the receipt says which engine answered and at what cost.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "answer"
++ ],
++ "properties": {
++ "answer": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "alt_text",
++ "editable",
++ "receipt"
++ ],
++ "properties": {
++ "alt_text": {
++ "type": "string",
++ "maxLength": 240
++ },
++ "editable": {
++ "const": true
++ },
++ "receipt": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "engine",
++ "model",
++ "cost_usd",
++ "ms",
++ "confidence",
++ "retention"
++ ],
++ "properties": {
++ "engine": {
++ "enum": [
++ "local",
++ "routed",
++ "local+routed",
++ "local+routed description"
++ ]
++ },
++ "model": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "cost_usd": {
++ "type": "number",
++ "minimum": 0
++ },
++ "ms": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "confidence": {
++ "type": [
++ "number",
++ "null"
++ ]
++ },
++ "retention": {
++ "type": "string"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "vision.answer-result.v1": {
++ "title": "VisionAnswerResult",
++ "description": "vision.answer-result@1: what POST /api/v1/ask, /describe and /ocr answer (`{ answer }`), and POST /api/v1/compare answers with `compare` rows and the local reader's summary added. Every AI-written text has passed noLooks: a look-naming answer is replaced, never repaired word by word.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "answer"
++ ],
++ "properties": {
++ "answer": {
++ "$ref": "#/components/schemas/vision.answer.v1"
++ },
++ "compare": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "engine",
++ "ok",
++ "cost_usd",
++ "latency_ms"
++ ],
++ "properties": {
++ "engine": {
++ "enum": [
++ "local",
++ "ai"
++ ]
++ },
++ "requested_model": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "model": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "provider": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "route": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "ok": {
++ "type": "boolean"
++ },
++ "text": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "cost_usd": {
++ "type": "number",
++ "minimum": 0
++ },
++ "latency_ms": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "error_code": {
++ "type": [
++ "string",
++ "null"
++ ]
++ }
++ }
++ }
++ },
++ "local": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "ok",
++ "model",
++ "confidence",
++ "error_code"
++ ],
++ "properties": {
++ "ok": {
++ "type": "boolean"
++ },
++ "model": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "confidence": {
++ "type": [
++ "number",
++ "null"
++ ]
++ },
++ "error_code": {
++ "type": [
++ "string",
++ "null"
++ ]
++ }
++ }
++ }
++ }
++ },
++ "vision.answer.v1": {
++ "title": "VisionAnswer",
++ "description": "vision.answer@1: one saved answer on OpenVibe.Vision. It names the engine, model, provider and route that actually answered (never a model hint), the reason it was chosen, the cost OpenVibe.AI reported, the measured latency and the confidence. No image bytes are stored: only a sha256 and the declared media type.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "id",
++ "task",
++ "mode",
++ "source",
++ "image_sha256",
++ "image_mime",
++ "image_bytes",
++ "cost_usd",
++ "latency_ms",
++ "created_at",
++ "answer"
++ ],
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^vis_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "task": {
++ "enum": [
++ "ask",
++ "describe",
++ "identify",
++ "count",
++ "translate",
++ "ocr",
++ "compare"
++ ]
++ },
++ "question": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 2000
++ },
++ "mode": {
++ "enum": [
++ "auto",
++ "local",
++ "ai"
++ ]
++ },
++ "source": {
++ "enum": [
++ "upload",
++ "url",
++ "data"
++ ]
++ },
++ "source_url": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "uri"
++ },
++ "image_sha256": {
++ "type": "string",
++ "pattern": "^[0-9a-f]{64}$"
++ },
++ "image_mime": {
++ "type": "string",
++ "pattern": "^image/[a-z0-9.+-]+$"
++ },
++ "image_bytes": {
++ "type": "integer",
++ "minimum": 1,
++ "maximum": 20971520
++ },
++ "text": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 20000
++ },
++ "answer": {
++ "type": "object",
++ "description": "Engine details, OCR words, provenance and any compare rows; never an image."
++ },
++ "model": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "provider": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "route": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "reason": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "engine": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "confidence": {
++ "type": [
++ "number",
++ "null"
++ ],
++ "minimum": 0,
++ "maximum": 1
++ },
++ "cost_usd": {
++ "type": "number",
++ "minimum": 0
++ },
++ "latency_ms": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ },
++ "vision.ask-request.v1": {
++ "title": "VisionAskRequest",
++ "description": "vision.ask-request@1: the body of POST /api/v1/ask and /api/v1/describe on OpenVibe.Vision. `task` is what the caller wants (ask, describe, identify, count, translate, ocr); `mode` is auto (local OCR first, then OpenVibe.AI), local (never a model) or ai. A read question (text, sign, receipt, label) is answered by the local OCR reader first in auto mode. `lang` is the Tesseract language and `model` is a provider:model hint AI may ignore.",
++ "type": "object",
++ "additionalProperties": false,
++ "properties": {
++ "image": {
++ "type": "string",
++ "pattern": "^data:image/",
++ "description": "The image as a data URL (base64)."
++ },
++ "image_url": {
++ "type": "string",
++ "format": "uri",
++ "pattern": "^https?://",
++ "description": "A public image URL, fetched through Vision's private-address guard."
++ },
++ "question": {
++ "type": "string",
++ "maxLength": 2000
++ },
++ "task": {
++ "enum": [
++ "ask",
++ "describe",
++ "identify",
++ "count",
++ "translate",
++ "ocr"
++ ]
++ },
++ "mode": {
++ "enum": [
++ "auto",
++ "local",
++ "ai"
++ ]
++ },
++ "lang": {
++ "type": "string",
++ "maxLength": 40
++ },
++ "model": {
++ "type": "string",
++ "maxLength": 120
++ }
++ },
++ "anyOf": [
++ {
++ "required": [
++ "image"
++ ]
++ },
++ {
++ "required": [
++ "image_url"
++ ]
++ }
++ ]
++ },
++ "vision.benchmarks.v1": {
++ "title": "VisionBenchmarks",
++ "description": "vision.benchmarks@1: GET /api/v1/benchmarks aggregates compare samples per task, engine, provider and model with sample counts and mean latency and cost. No subject, image or answer text is included.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "count",
++ "benchmarks"
++ ],
++ "properties": {
++ "count": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "benchmarks": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "task",
++ "engine",
++ "provider",
++ "model",
++ "samples",
++ "avg_latency_ms",
++ "avg_cost_usd",
++ "min_cost_usd",
++ "max_latency_ms"
++ ],
++ "properties": {
++ "task": {
++ "type": "string"
++ },
++ "engine": {
++ "enum": [
++ "local",
++ "ai"
++ ]
++ },
++ "provider": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "model": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "samples": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "avg_latency_ms": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "avg_cost_usd": {
++ "type": "number",
++ "minimum": 0
++ },
++ "min_cost_usd": {
++ "type": "number",
++ "minimum": 0
++ },
++ "max_latency_ms": {
++ "type": "integer",
++ "minimum": 0
++ }
++ }
++ }
++ }
++ }
++ },
++ "vision.compare-request.v1": {
++ "title": "VisionCompareRequest",
++ "description": "vision.compare-request@1: the body of POST /api/v1/compare. One image runs through the local reader and up to three AI hints; the requested model is a hint only, and each row reports the provider and model OpenVibe.AI says answered.",
++ "type": "object",
++ "additionalProperties": false,
++ "properties": {
++ "image": {
++ "type": "string",
++ "pattern": "^data:image/",
++ "description": "The image as a data URL (base64)."
++ },
++ "image_url": {
++ "type": "string",
++ "format": "uri",
++ "pattern": "^https?://",
++ "description": "A public image URL, fetched through Vision's private-address guard."
++ },
++ "task": {
++ "enum": [
++ "ask",
++ "describe",
++ "identify",
++ "count",
++ "translate",
++ "ocr"
++ ]
++ },
++ "question": {
++ "type": "string",
++ "maxLength": 2000
++ },
++ "models": {
++ "type": "array",
++ "items": {
++ "type": "string",
++ "maxLength": 120
++ },
++ "maxItems": 3
++ },
++ "lang": {
++ "type": "string",
++ "maxLength": 40
++ }
++ },
++ "anyOf": [
++ {
++ "required": [
++ "image"
++ ]
++ },
++ {
++ "required": [
++ "image_url"
++ ]
++ }
++ ]
++ },
++ "vision.consent-request.v1": {
++ "title": "VisionConsentRequest",
++ "description": "vision.consent-request@1: POST /api/v1/faces/consents/:feature. The caller signs the current text (`text_version` must match this server's), names themself (`signed_name`), and for cast must confirm they are an adult; library grouping is refused outside the host's allowed regions. A repeat of an active consent changes nothing.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "signed_name",
++ "text_version"
++ ],
++ "properties": {
++ "signed_name": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 160
++ },
++ "text_version": {
++ "type": "string",
++ "maxLength": 80
++ },
++ "modality": {
++ "enum": [
++ "face",
++ "voice"
++ ]
++ },
++ "feature": {
++ "enum": [
++ "verify",
++ "find",
++ "cast",
++ "library",
++ "voice"
++ ],
++ "description": "Echo of the :feature path segment."
++ },
++ "channel": {
++ "type": "string",
++ "pattern": "^[a-zA-Z0-9][a-zA-Z0-9:_-]{1,119}$"
++ },
++ "adult": {
++ "type": "boolean"
++ },
++ "region": {
++ "type": "string",
++ "maxLength": 10
++ }
++ }
++ },
++ "vision.consent.v1": {
++ "title": "VisionConsent",
++ "description": "vision.consent@1: one consent record on OpenVibe.Vision. It says whose face or voice it is (`subject`), the modality, the purpose-scoped feature and scope (a channel or a library), who signed which text version, when it was granted, when it expires, and how and when it was revoked. Face matching tools refuse without an active record for that person, with the same answer whether or not the person exists. Templates are deleted when the purpose ends and at the latest three years after the last use (BIPA); a revoked audit record is deleted one year after the purpose ends (CUBI).",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "id",
++ "subject",
++ "modality",
++ "feature",
++ "scope",
++ "purpose",
++ "text_version",
++ "granted_by",
++ "granted_at",
++ "expires_at",
++ "last_used_at",
++ "revoked_at",
++ "revoked_by",
++ "purpose_ends_at",
++ "retention_until",
++ "status",
++ "erased"
++ ],
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^fco_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "subject": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The person whose face or voice this consent covers."
++ },
++ "modality": {
++ "enum": [
++ "face",
++ "voice"
++ ]
++ },
++ "feature": {
++ "enum": [
++ "verify",
++ "find",
++ "cast",
++ "library",
++ "voice"
++ ]
++ },
++ "scope": {
++ "type": "string",
++ "maxLength": 160,
++ "description": "Empty means the person's own account; a cast grant names one channel, a library grant the library it applies to."
++ },
++ "purpose": {
++ "type": "string",
++ "maxLength": 200
++ },
++ "text_version": {
++ "type": "string",
++ "maxLength": 80
++ },
++ "granted_by": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The person who signed; the subject themself, or a guardian where the law allows."
++ },
++ "granted_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "expires_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time",
++ "description": "When the consent stops applying on its own; null when it lasts until revoked."
++ },
++ "last_used_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "revoked_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "revoked_by": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "enum": [
++ "subject",
++ "retention",
++ "account_deletion",
++ null
++ ],
++ "description": "How it was revoked: by the person, by the retention job, or with the account."
++ },
++ "purpose_ends_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "retention_until": {
++ "type": "string",
++ "format": "date-time",
++ "description": "The outer limit the record and derived data may exist: three years after last use, or one year after the purpose ended."
++ },
++ "status": {
++ "enum": [
++ "active",
++ "revoked",
++ "expired"
++ ]
++ },
++ "erased": {
++ "type": "boolean",
++ "description": "True once the template and derived results this consent covered are gone."
++ }
++ }
++ },
++ "vision.face-blur.v1": {
++ "title": "VisionFaceBlur",
++ "description": "vision.face-blur@1: POST /api/v1/faces/blur answers a redacted PNG derivative and the number of faces blurred in X-Faces-Blur. Identity-free, no template, and the original is never stored.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "media_type",
++ "face_count"
++ ],
++ "properties": {
++ "media_type": {
++ "const": "image/png"
++ },
++ "face_count": {
++ "type": "integer",
++ "minimum": 0
++ }
++ }
++ },
++ "vision.face-cast-match-request.v1": {
++ "title": "VisionFaceCastMatchRequest",
++ "description": "vision.face-cast-match-request@1: POST /api/v1/faces/cast/match, called by OpenVibe.Live with a service token scoped to one channel (vision.face.cast.match). It matches only that channel's opted-in adult roster; a person who has not enrolled for this channel cannot appear, and the answer does not say whether they exist.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "channel",
++ "media_ref"
++ ],
++ "properties": {
++ "image": {
++ "type": "string",
++ "pattern": "^data:image/",
++ "description": "The image as a data URL (base64)."
++ },
++ "image_url": {
++ "type": "string",
++ "format": "uri",
++ "pattern": "^https?://",
++ "description": "A public image URL, fetched through Vision's private-address guard."
++ },
++ "channel": {
++ "type": "string",
++ "pattern": "^[a-zA-Z0-9][a-zA-Z0-9:_-]{1,119}$"
++ },
++ "media_ref": {
++ "type": "string",
++ "pattern": "^[a-zA-Z0-9][a-zA-Z0-9:/_-]{1,159}$"
++ }
++ },
++ "anyOf": [
++ {
++ "required": [
++ "image"
++ ]
++ },
++ {
++ "required": [
++ "image_url"
++ ]
++ }
++ ]
++ },
++ "vision.face-cast-match.v1": {
++ "title": "VisionFaceCastMatch",
++ "description": "vision.face-cast-match@1: the matches inside one channel's consented roster, each with a score and box. No other subject is named and no non-roster face is described.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "channel",
++ "matches",
++ "liveness"
++ ],
++ "properties": {
++ "channel": {
++ "type": "string"
++ },
++ "matches": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "subject",
++ "score"
++ ],
++ "properties": {
++ "subject": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "score": {
++ "type": "number",
++ "minimum": 0,
++ "maximum": 1
++ },
++ "box": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "x",
++ "y",
++ "width",
++ "height"
++ ],
++ "properties": {
++ "x": {
++ "type": "number"
++ },
++ "y": {
++ "type": "number"
++ },
++ "width": {
++ "type": "number"
++ },
++ "height": {
++ "type": "number"
++ }
++ }
++ }
++ }
++ }
++ },
++ "liveness": {
++ "enum": [
++ "not_checked",
++ "checked"
++ ]
++ }
++ }
++ },
++ "vision.face-detection.v1": {
++ "title": "VisionFaceDetection",
++ "description": "vision.face-detection@1: POST /api/v1/faces/detect, an identity-free YuNet privacy tool. It counts faces and returns their boxes and scores; it stores no template, names no one and needs no consent. Anyone may use it.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "count",
++ "boxes"
++ ],
++ "properties": {
++ "count": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "boxes": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "x",
++ "y",
++ "width",
++ "height",
++ "score"
++ ],
++ "properties": {
++ "x": {
++ "type": "number"
++ },
++ "y": {
++ "type": "number"
++ },
++ "width": {
++ "type": "number"
++ },
++ "height": {
++ "type": "number"
++ },
++ "score": {
++ "type": "number",
++ "minimum": 0,
++ "maximum": 1
++ }
++ }
++ }
++ }
++ }
++ },
++ "vision.face-enrol-request.v1": {
++ "title": "VisionFaceEnrolRequest",
++ "description": "vision.face-enrol-request@1: POST /api/v1/faces/enrol/:feature. `feature` is verify (1:1 verification of yourself), find (search your own uploads) or cast (an adult tagging named streams); each needs its own signed consent first, and cast also names the channel scope.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "feature"
++ ],
++ "properties": {
++ "image": {
++ "type": "string",
++ "pattern": "^data:image/",
++ "description": "The image as a data URL (base64)."
++ },
++ "image_url": {
++ "type": "string",
++ "format": "uri",
++ "pattern": "^https?://",
++ "description": "A public image URL, fetched through Vision's private-address guard."
++ },
++ "feature": {
++ "enum": [
++ "verify",
++ "find",
++ "cast"
++ ]
++ },
++ "channel": {
++ "type": "string",
++ "pattern": "^[a-zA-Z0-9][a-zA-Z0-9:_-]{1,119}$"
++ }
++ },
++ "anyOf": [
++ {
++ "required": [
++ "image"
++ ]
++ },
++ {
++ "required": [
++ "image_url"
++ ]
++ }
++ ]
++ },
++ "vision.face-find-result.v1": {
++ "title": "VisionFaceFindResult",
++ "description": "vision.face-find-result@1: POST /api/v1/faces/find lists the caller's own private uploads that match their enrolled self-search template, as private links and scores. It requires find consent; without it the answer is faces.consent_required whether or not any media exists.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "results"
++ ],
++ "properties": {
++ "results": {
++ "type": "array",
++ "maxItems": 500,
++ "items": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "private_link",
++ "score"
++ ],
++ "properties": {
++ "private_link": {
++ "type": "string",
++ "pattern": "^/api/v1/faces/media/"
++ },
++ "score": {
++ "type": "number",
++ "minimum": 0,
++ "maximum": 1
++ }
++ }
++ }
++ }
++ }
++ },
++ "vision.face-library-group.v1": {
++ "title": "VisionFaceLibraryGroup",
++ "description": "vision.face-library-group@1: one private face cluster in a person's own Vision library: the media it came from, the group it was merged into, an optional label and whether the person excluded it. It never leaves that person's library.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "id",
++ "media_id",
++ "group_id",
++ "excluded",
++ "created_at"
++ ],
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^fcl_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "media_id": {
++ "type": "string",
++ "pattern": "^fme_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "group_id": {
++ "type": "string",
++ "pattern": "^fgr_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "label": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 120
++ },
++ "excluded": {
++ "type": "boolean"
++ },
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ },
++ "vision.face-library-request.v1": {
++ "title": "VisionFaceLibraryRequest",
++ "description": "vision.face-library-request@1: POST /api/v1/faces/library/cluster (region-gated), GET /library/groups and POST /library/correct. `action` is cluster (default), exclude, merge or split; `ids` names up to 100 of the caller's own clusters; `region` is a jurisdiction code the host allows. Unknown faces stay unlabelled.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "action"
++ ],
++ "properties": {
++ "action": {
++ "enum": [
++ "cluster",
++ "exclude",
++ "merge",
++ "split"
++ ]
++ },
++ "region": {
++ "type": "string",
++ "maxLength": 10
++ },
++ "ids": {
++ "type": "array",
++ "items": {
++ "type": "string",
++ "maxLength": 40
++ },
++ "minItems": 1,
++ "maxItems": 100
++ }
++ }
++ },
++ "vision.face-library-result.v1": {
++ "title": "VisionFaceLibraryResult",
++ "description": "vision.face-library-result@1: clustering and group reads answer the caller's own private face groups as an array; a correction answers the action and how many clusters it updated.",
++ "oneOf": [
++ {
++ "type": "array",
++ "items": {
++ "$ref": "#/components/schemas/vision.face-library-group.v1"
++ }
++ },
++ {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "action",
++ "updated"
++ ],
++ "properties": {
++ "action": {
++ "enum": [
++ "exclude",
++ "merge",
++ "split"
++ ]
++ },
++ "updated": {
++ "type": "integer",
++ "minimum": 0
++ }
++ }
++ }
++ ]
++ },
++ "vision.face-tag-list.v1": {
++ "title": "VisionFaceTagList",
++ "description": "vision.face-tag-list@1: GET /api/v1/faces/cast/tags answers the caller's own cast tags, newest first, as a bare array. Empty when they have not consented to cast matching.",
++ "type": "array",
++ "items": {
++ "$ref": "#/components/schemas/vision.face-tag.v1"
++ }
++ },
++ "vision.face-tag.v1": {
++ "title": "VisionFaceTag",
++ "description": "vision.face-tag@1: one cast tag on a stream: which subject, which channel, which private media reference and when. GET /api/v1/faces/cast/tags lists the caller's own tags; revoking the cast consent removes them.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "id",
++ "channel",
++ "media_ref",
++ "created_at"
++ ],
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^ftg_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "channel": {
++ "type": "string"
++ },
++ "media_ref": {
++ "type": "string"
++ },
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ },
++ "vision.face-verify.v1": {
++ "title": "VisionFaceVerify",
++ "description": "vision.face-verify@1: POST /api/v1/faces/verify compares this person's own verification attempt with their enrolled template. It always requires that person's verify consent; without it the answer is faces.consent_required whether or not a template exists. A match alone never unlocks anything: liveness and a second factor are separate.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "score",
++ "match",
++ "threshold",
++ "liveness"
++ ],
++ "properties": {
++ "score": {
++ "type": "number",
++ "minimum": 0,
++ "maximum": 1
++ },
++ "match": {
++ "type": "boolean"
++ },
++ "threshold": {
++ "type": "number",
++ "minimum": 0,
++ "maximum": 1
++ },
++ "liveness": {
++ "enum": [
++ "not_checked",
++ "checked"
++ ]
++ }
++ }
++ },
++ "vision.find-request.v1": {
++ "title": "VisionFindRequest",
++ "description": "vision.find-request@1: the body of POST /api/v1/index and of GET or POST /api/v1/find. `index` stores a scoped CLIP embedding for one of the caller's own items (`item_id`, optional `result_url`); `find` searches only the caller's own private index, by words (`q`) or by an example image. No public image index is ever built.",
++ "type": "object",
++ "additionalProperties": false,
++ "properties": {
++ "image": {
++ "type": "string",
++ "pattern": "^data:image/",
++ "description": "The image as a data URL (base64)."
++ },
++ "image_url": {
++ "type": "string",
++ "format": "uri",
++ "pattern": "^https?://",
++ "description": "A public image URL, fetched through Vision's private-address guard."
++ },
++ "q": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 500
++ },
++ "item_id": {
++ "type": "string",
++ "pattern": "^[\\w:.-]{1,200}$"
++ },
++ "result_url": {
++ "type": "string",
++ "format": "uri",
++ "pattern": "^https://"
++ }
++ },
++ "anyOf": [
++ {
++ "required": [
++ "q"
++ ]
++ },
++ {
++ "required": [
++ "image"
++ ]
++ },
++ {
++ "required": [
++ "image_url"
++ ]
++ },
++ {
++ "required": [
++ "item_id",
++ "image"
++ ]
++ },
++ {
++ "required": [
++ "item_id",
++ "image_url"
++ ]
++ }
++ ]
++ },
++ "vision.find-result.v1": {
++ "title": "VisionFindResult",
++ "description": "vision.find-result@1: POST /api/v1/index and GET/POST /api/v1/find answer a scoped private search over the caller's own indexed items: `results` (item_id, result_url, score) only, no one else's rows and no public index.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "answer"
++ ],
++ "properties": {
++ "answer": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "results",
++ "receipt"
++ ],
++ "properties": {
++ "results": {
++ "type": "array",
++ "maxItems": 5000,
++ "items": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "item_id",
++ "score"
++ ],
++ "properties": {
++ "item_id": {
++ "type": "string"
++ },
++ "result_url": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "uri",
++ "pattern": "^https://"
++ },
++ "score": {
++ "type": "number"
++ }
++ }
++ }
++ },
++ "receipt": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "engine",
++ "model",
++ "cost_usd",
++ "ms",
++ "confidence",
++ "retention"
++ ],
++ "properties": {
++ "engine": {
++ "enum": [
++ "local",
++ "routed",
++ "local+routed",
++ "local+routed description"
++ ]
++ },
++ "model": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "cost_usd": {
++ "type": "number",
++ "minimum": 0
++ },
++ "ms": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "confidence": {
++ "type": [
++ "number",
++ "null"
++ ]
++ },
++ "retention": {
++ "type": "string"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "vision.image-request.v1": {
++ "title": "VisionImageRequest",
++ "description": "vision.image-request@1: one image for OpenVibe.Vision's one-shot tools, as a data URL or a public URL. The multipart form field `image` carries the same bytes. Private tools (find, index, watch, moments) accept only uploaded bytes or a data URL, never an unverified source URL.",
++ "type": "object",
++ "additionalProperties": false,
++ "properties": {
++ "image": {
++ "type": "string",
++ "pattern": "^data:image/",
++ "description": "The image as a data URL (base64)."
++ },
++ "image_url": {
++ "type": "string",
++ "format": "uri",
++ "pattern": "^https?://",
++ "description": "A public image URL, fetched through Vision's private-address guard."
++ }
++ },
++ "anyOf": [
++ {
++ "required": [
++ "image"
++ ]
++ },
++ {
++ "required": [
++ "image_url"
++ ]
++ }
++ ]
++ },
++ "vision.models.v1": {
++ "title": "VisionModels",
++ "description": "vision.models@1: GET /api/v1/models names the AI route's offered provider:model hints, the configured hints and the local engines present, without any subject data. OpenVibe.AI's router remains the authority: this is a menu, not a promise.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "ai",
++ "configured",
++ "offered",
++ "local",
++ "note"
++ ],
++ "properties": {
++ "ai": {
++ "type": "boolean"
++ },
++ "configured": {
++ "type": "array",
++ "items": {
++ "type": "string"
++ }
++ },
++ "offered": {
++ "type": "array",
++ "items": {
++ "type": "object"
++ }
++ },
++ "local": {
++ "type": "array",
++ "items": {
++ "type": "string"
++ }
++ },
++ "note": {
++ "type": "string"
++ }
++ }
++ },
++ "vision.moments-request.v1": {
++ "title": "VisionMomentsRequest",
++ "description": "vision.moments-request@1: the body of POST /api/v1/moments. Up to 30 timestamped frames are compared with CLIP prompt embeddings; a histogram jump marks a scene change. VOD and stream chapters come from this.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "frames"
++ ],
++ "properties": {
++ "frames": {
++ "type": "array",
++ "minItems": 1,
++ "maxItems": 30,
++ "items": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "timestamp_ms"
++ ],
++ "properties": {
++ "timestamp_ms": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "image": {
++ "type": "string",
++ "pattern": "^data:image/"
++ },
++ "image_url": {
++ "type": "string",
++ "format": "uri",
++ "pattern": "^https?://"
++ }
++ },
++ "anyOf": [
++ {
++ "required": [
++ "image"
++ ]
++ },
++ {
++ "required": [
++ "image_url"
++ ]
++ }
++ ]
++ }
++ },
++ "prompts": {
++ "type": "array",
++ "items": {
++ "type": "string",
++ "maxLength": 120
++ },
++ "maxItems": 10
++ }
++ }
++ },
++ "vision.moments-result.v1": {
++ "title": "VisionMomentsResult",
++ "description": "vision.moments-result@1: POST /api/v1/moments answers scene changes and candidate moments (timestamp, matching prompt, score) for up to 30 frames. A moment is a suggestion: the room decides whether it becomes a clip or a chapter.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "answer"
++ ],
++ "properties": {
++ "answer": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "scene_changes",
++ "candidate_moments",
++ "receipt"
++ ],
++ "properties": {
++ "scene_changes": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "timestamp_ms",
++ "kind"
++ ],
++ "properties": {
++ "timestamp_ms": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "kind": {
++ "const": "scene_change"
++ }
++ }
++ }
++ },
++ "candidate_moments": {
++ "type": "array",
++ "maxItems": 30,
++ "items": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "timestamp_ms",
++ "prompt",
++ "score"
++ ],
++ "properties": {
++ "timestamp_ms": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "prompt": {
++ "type": "string"
++ },
++ "score": {
++ "type": "number"
++ }
++ }
++ }
++ },
++ "receipt": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "engine",
++ "model",
++ "cost_usd",
++ "ms",
++ "confidence",
++ "retention"
++ ],
++ "properties": {
++ "engine": {
++ "enum": [
++ "local",
++ "routed",
++ "local+routed",
++ "local+routed description"
++ ]
++ },
++ "model": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "cost_usd": {
++ "type": "number",
++ "minimum": 0
++ },
++ "ms": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "confidence": {
++ "type": [
++ "number",
++ "null"
++ ]
++ },
++ "retention": {
++ "type": "string"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "vision.ocr-request.v1": {
++ "title": "VisionOcrRequest",
++ "description": "vision.ocr-request@1: the body of POST /api/v1/ocr. The local Tesseract 5 reader runs on Vision's host and costs nothing; `fallback` asks OpenVibe.AI only when the local pass found no usable text, which needs a signed-in person or a service token holding vision.ocr.",
++ "type": "object",
++ "additionalProperties": false,
++ "properties": {
++ "image": {
++ "type": "string",
++ "pattern": "^data:image/",
++ "description": "The image as a data URL (base64)."
++ },
++ "image_url": {
++ "type": "string",
++ "format": "uri",
++ "pattern": "^https?://",
++ "description": "A public image URL, fetched through Vision's private-address guard."
++ },
++ "lang": {
++ "type": "string",
++ "maxLength": 40
++ },
++ "fallback": {
++ "type": "boolean"
++ },
++ "model": {
++ "type": "string",
++ "maxLength": 120
++ }
++ },
++ "anyOf": [
++ {
++ "required": [
++ "image"
++ ]
++ },
++ {
++ "required": [
++ "image_url"
++ ]
++ }
++ ]
++ },
++ "vision.product-request.v1": {
++ "title": "VisionProductRequest",
++ "description": "vision.product-request@1: the body of POST /api/v1/product. The image is embedded locally with CLIP; `allow_rerank` lets one routed model reorder the candidate offers. Candidates come from OpenVibe.Deals and open.shoes; Vision never invents a price.",
++ "type": "object",
++ "additionalProperties": false,
++ "properties": {
++ "image": {
++ "type": "string",
++ "pattern": "^data:image/",
++ "description": "The image as a data URL (base64)."
++ },
++ "image_url": {
++ "type": "string",
++ "format": "uri",
++ "pattern": "^https?://",
++ "description": "A public image URL, fetched through Vision's private-address guard."
++ },
++ "allow_rerank": {
++ "type": "boolean"
++ }
++ },
++ "anyOf": [
++ {
++ "required": [
++ "image"
++ ]
++ },
++ {
++ "required": [
++ "image_url"
++ ]
++ }
++ ]
++ },
++ "vision.product-result.v1": {
++ "title": "VisionProductResult",
++ "description": "vision.product-result@1: POST /api/v1/product answers a description of the product only (never a person), the CLIP embedding, and candidate offers from OpenVibe.Deals and open.shoes with their score, price and URL. A room must not treat a candidate as a price it verified.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "answer"
++ ],
++ "properties": {
++ "answer": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "description",
++ "embedding",
++ "offers",
++ "warnings",
++ "receipt"
++ ],
++ "properties": {
++ "description": {
++ "type": "string",
++ "maxLength": 300
++ },
++ "embedding": {
++ "type": "array",
++ "items": {
++ "type": "number"
++ }
++ },
++ "offers": {
++ "type": "array",
++ "maxItems": 10,
++ "items": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "source",
++ "title",
++ "url"
++ ],
++ "properties": {
++ "source": {
++ "type": "string"
++ },
++ "title": {
++ "type": "string",
++ "maxLength": 200
++ },
++ "url": {
++ "type": "string",
++ "format": "uri",
++ "pattern": "^https://"
++ },
++ "price": {
++ "type": [
++ "number",
++ "null"
++ ]
++ },
++ "score": {
++ "type": "number"
++ }
++ }
++ }
++ },
++ "warnings": {
++ "type": "array",
++ "items": {
++ "type": "string"
++ }
++ },
++ "receipt": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "engine",
++ "model",
++ "cost_usd",
++ "ms",
++ "confidence",
++ "retention"
++ ],
++ "properties": {
++ "engine": {
++ "enum": [
++ "local",
++ "routed",
++ "local+routed",
++ "local+routed description"
++ ]
++ },
++ "model": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "cost_usd": {
++ "type": "number",
++ "minimum": 0
++ },
++ "ms": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "confidence": {
++ "type": [
++ "number",
++ "null"
++ ]
++ },
++ "retention": {
++ "type": "string"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "vision.receipt-request.v1": {
++ "title": "VisionReceiptRequest",
++ "description": "vision.receipt-request@1: the body of POST /api/v1/receipt. The local OCR reader extracts line items and a total; `fallback` asks OpenVibe.AI only when the local pass could not produce both.",
++ "type": "object",
++ "additionalProperties": false,
++ "properties": {
++ "image": {
++ "type": "string",
++ "pattern": "^data:image/",
++ "description": "The image as a data URL (base64)."
++ },
++ "image_url": {
++ "type": "string",
++ "format": "uri",
++ "pattern": "^https?://",
++ "description": "A public image URL, fetched through Vision's private-address guard."
++ },
++ "fallback": {
++ "type": "boolean"
}
-- ],
-- "x-openvibe-input": {
-- "$ref": "#/components/schemas/vision.ui-check-request.v1"
},
-- "parameters": [
++ "anyOf": [
{
-- "name": "id",
-- "in": "path",
-- "required": true,
-- "schema": {
-- "type": "string"
-- }
-- }
-- ],
-- "responses": {
-- "2XX": {
-- "description": "Success",
-- "content": {
-- "application/json": {
-- "schema": {
-- "$ref": "#/components/schemas/vision.ui-check-result.v1"
-- }
-- }
-- }
++ "required": [
++ "image"
++ ]
},
-- "default": {
-- "description": "An error (RFC 9457 problem details)",
-- "content": {
-- "application/problem+json": {
-- "schema": {
-- "$ref": "#/components/schemas/errors.problem.v1"
-- }
-- }
-- }
++ {
++ "required": [
++ "image_url"
++ ]
}
-- }
-- }
-- }
-- },
-- "components": {
-- "schemas": {
-- "errors.problem.v1": {
-- "title": "Problem",
-- "description": "RFC 9457 problem details (application/problem+json) with a stable OpenVibe error code and trace identifiers. The legacy 'error' string is allowed during migration so existing clients keep working.",
++ ]
++ },
++ "vision.receipt-result.v1": {
++ "title": "VisionReceiptResult",
++ "description": "vision.receipt-result@1: POST /api/v1/receipt answers the store, the line items and the total the local OCR reader read (or the routed fallback read), with `raw_text` when the local pass produced it. OpenVibe.Food decides what enters a pantry; Vision returns the reading.",
"type": "object",
++ "additionalProperties": false,
"required": [
-- "type",
-- "title",
-- "status",
-- "code"
++ "answer"
],
-- "additionalProperties": true,
"properties": {
-- "type": {
-- "type": "string",
-- "format": "uri-reference"
-- },
-- "title": {
-- "type": "string",
-- "minLength": 1
-- },
-- "status": {
-- "type": "integer",
-- "minimum": 400,
-- "maximum": 599
-- },
-- "detail": {
-- "type": "string"
-- },
-- "instance": {
-- "type": "string",
-- "format": "uri-reference"
-- },
-- "code": {
-- "type": "string",
-- "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+)*$"
-- },
-- "request_id": {
-- "type": "string"
-- },
-- "trace_id": {
-- "type": "string",
-- "pattern": "^[0-9a-f]{32}$"
-- },
-- "errors": {
-- "type": "array",
-- "items": {
-- "type": "object",
-- "required": [
-- "message"
-- ],
-- "properties": {
-- "path": {
-- "type": "string"
-- },
-- "message": {
-- "type": "string"
++ "answer": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "store",
++ "items",
++ "total",
++ "receipt"
++ ],
++ "properties": {
++ "store": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 150
++ },
++ "items": {
++ "type": "array",
++ "maxItems": 100,
++ "items": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "name",
++ "qty",
++ "price"
++ ],
++ "properties": {
++ "name": {
++ "type": "string",
++ "maxLength": 200
++ },
++ "qty": {
++ "type": "number",
++ "minimum": 0
++ },
++ "price": {
++ "type": "number",
++ "minimum": 0
++ }
++ }
++ }
++ },
++ "total": {
++ "type": [
++ "number",
++ "null"
++ ],
++ "minimum": 0
++ },
++ "raw_text": {
++ "type": "string",
++ "maxLength": 20000
++ },
++ "receipt": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "engine",
++ "model",
++ "cost_usd",
++ "ms",
++ "confidence",
++ "retention"
++ ],
++ "properties": {
++ "engine": {
++ "enum": [
++ "local",
++ "routed",
++ "local+routed",
++ "local+routed description"
++ ]
++ },
++ "model": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "cost_usd": {
++ "type": "number",
++ "minimum": 0
++ },
++ "ms": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "confidence": {
++ "type": [
++ "number",
++ "null"
++ ]
++ },
++ "retention": {
++ "type": "string"
++ }
}
}
}
-- },
-- "error": {
-- "type": "string",
-- "description": "Deprecated compatibility field: same text as title/detail for clients that read {error}."
}
}
},
@@@ -417,6 +4173,167 @@
}
}
}
++ },
++ "vision.watch-request.v1": {
++ "title": "VisionWatchRequest",
++ "description": "vision.watch-request@1: the body of POST /api/v1/watch (owner-granted camera alerts). `classes` names 1–20 things to look for (snow, delivery, package, a DETR label, or \"scene change\"); `previous_image` is required for a scene change and is compared by histogram.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "classes"
++ ],
++ "properties": {
++ "image": {
++ "type": "string",
++ "pattern": "^data:image/",
++ "description": "The image as a data URL (base64)."
++ },
++ "image_url": {
++ "type": "string",
++ "format": "uri",
++ "pattern": "^https?://",
++ "description": "A public image URL, fetched through Vision's private-address guard."
++ },
++ "classes": {
++ "type": "array",
++ "items": {
++ "type": "string",
++ "maxLength": 60
++ },
++ "minItems": 1,
++ "maxItems": 20
++ },
++ "previous_image": {
++ "type": "string",
++ "pattern": "^data:image/"
++ }
++ },
++ "anyOf": [
++ {
++ "required": [
++ "image"
++ ]
++ },
++ {
++ "required": [
++ "image_url"
++ ]
++ }
++ ]
++ },
++ "vision.watch-result.v1": {
++ "title": "VisionWatchResult",
++ "description": "vision.watch-result@1: POST /api/v1/watch answers the requested classes found in a camera still (DETR labels, snow/delivery/package through CLIP, or a histogram scene change), each with a confidence and, for an object, a box.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "answer"
++ ],
++ "properties": {
++ "answer": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "detections",
++ "receipt"
++ ],
++ "properties": {
++ "detections": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "class",
++ "confidence"
++ ],
++ "properties": {
++ "class": {
++ "type": "string"
++ },
++ "confidence": {
++ "type": "number",
++ "minimum": 0,
++ "maximum": 1
++ },
++ "box": {
++ "type": [
++ "object",
++ "null"
++ ],
++ "additionalProperties": false,
++ "required": [
++ "x",
++ "y",
++ "width",
++ "height"
++ ],
++ "properties": {
++ "x": {
++ "type": "number"
++ },
++ "y": {
++ "type": "number"
++ },
++ "width": {
++ "type": "number"
++ },
++ "height": {
++ "type": "number"
++ }
++ }
++ }
++ }
++ }
++ },
++ "receipt": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "engine",
++ "model",
++ "cost_usd",
++ "ms",
++ "confidence",
++ "retention"
++ ],
++ "properties": {
++ "engine": {
++ "enum": [
++ "local",
++ "routed",
++ "local+routed",
++ "local+routed description"
++ ]
++ },
++ "model": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "cost_usd": {
++ "type": "number",
++ "minimum": 0
++ },
++ "ms": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "confidence": {
++ "type": [
++ "number",
++ "null"
++ ]
++ },
++ "retention": {
++ "type": "string"
++ }
++ }
++ }
++ }
++ }
++ }
}
},
"securitySchemes": {
ddiff --git a/generated/openapi/watch.json b/generated/openapi/watch.json
iindex c68688ab..9ce7d600 100644
---- a/generated/openapi/watch.json
++++ b/generated/openapi/watch.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Watch API",
-- "version": "0.239.0",
-- "description": "Routes of OpenVibe.Watch as the OpenVibe contracts describe them (openvibe-contracts 0.239.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.240.0",
++ "description": "Routes of OpenVibe.Watch as the OpenVibe contracts describe them (openvibe-contracts 0.240.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/wiki.json b/generated/openapi/wiki.json
iindex 22c13fb5..b26e5151 100644
---- a/generated/openapi/wiki.json
++++ b/generated/openapi/wiki.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Wiki API",
-- "version": "0.239.0",
-- "description": "Routes of OpenVibe.Wiki as the OpenVibe contracts describe them (openvibe-contracts 0.239.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.240.0",
++ "description": "Routes of OpenVibe.Wiki as the OpenVibe contracts describe them (openvibe-contracts 0.240.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/work.json b/generated/openapi/work.json
iindex 8bfc21d7..35c9db7b 100644
---- a/generated/openapi/work.json
++++ b/generated/openapi/work.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Work API",
-- "version": "0.239.0",
-- "description": "Routes of OpenVibe.Work as the OpenVibe contracts describe them (openvibe-contracts 0.239.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.240.0",
++ "description": "Routes of OpenVibe.Work as the OpenVibe contracts describe them (openvibe-contracts 0.240.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/zone.json b/generated/openapi/zone.json
iindex 2f431c74..e37a7555 100644
---- a/generated/openapi/zone.json
++++ b/generated/openapi/zone.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Zone API",
-- "version": "0.239.0",
-- "description": "Routes of OpenVibe.Zone as the OpenVibe contracts describe them (openvibe-contracts 0.239.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.240.0",
++ "description": "Routes of OpenVibe.Zone as the OpenVibe contracts describe them (openvibe-contracts 0.240.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/typescript/index.d.ts b/generated/typescript/index.d.ts
iindex 2199a4c9..31921bf5 100644
---- a/generated/typescript/index.d.ts
++++ b/generated/typescript/index.d.ts
@@@ -213431,6 +213431,1406 @@ export interface VisionUiCheckResult {
};
}
++/** [email protected] (owner: vision) */
++/**
++ * vision.image-request@1: one image for OpenVibe.Vision's one-shot tools, as a data URL or a public URL. The multipart form field `image` carries the same bytes. Private tools (find, index, watch, moments) accept only uploaded bytes or a data URL, never an unverified source URL.
++ */
++export type VisionImageRequest = {
++ [k: string]: unknown | undefined;
++} & {
++ /**
++ * The image as a data URL (base64).
++ */
++ image?: string;
++ /**
++ * A public image URL, fetched through Vision's private-address guard.
++ */
++ image_url?: string;
++};
++
++/** [email protected] (owner: vision) */
++/**
++ * vision.ask-request@1: the body of POST /api/v1/ask and /api/v1/describe on OpenVibe.Vision. `task` is what the caller wants (ask, describe, identify, count, translate, ocr); `mode` is auto (local OCR first, then OpenVibe.AI), local (never a model) or ai. A read question (text, sign, receipt, label) is answered by the local OCR reader first in auto mode. `lang` is the Tesseract language and `model` is a provider:model hint AI may ignore.
++ */
++export type VisionAskRequest = {
++ [k: string]: unknown | undefined;
++} & {
++ /**
++ * The image as a data URL (base64).
++ */
++ image?: string;
++ /**
++ * A public image URL, fetched through Vision's private-address guard.
++ */
++ image_url?: string;
++ question?: string;
++ task?: "ask" | "describe" | "identify" | "count" | "translate" | "ocr";
++ mode?: "auto" | "local" | "ai";
++ lang?: string;
++ model?: string;
++};
++
++/** [email protected] (owner: vision) */
++/**
++ * vision.ocr-request@1: the body of POST /api/v1/ocr. The local Tesseract 5 reader runs on Vision's host and costs nothing; `fallback` asks OpenVibe.AI only when the local pass found no usable text, which needs a signed-in person or a service token holding vision.ocr.
++ */
++export type VisionOcrRequest = {
++ [k: string]: unknown | undefined;
++} & {
++ /**
++ * The image as a data URL (base64).
++ */
++ image?: string;
++ /**
++ * A public image URL, fetched through Vision's private-address guard.
++ */
++ image_url?: string;
++ lang?: string;
++ fallback?: boolean;
++ model?: string;
++};
++
++/** [email protected] (owner: vision) */
++/**
++ * vision.compare-request@1: the body of POST /api/v1/compare. One image runs through the local reader and up to three AI hints; the requested model is a hint only, and each row reports the provider and model OpenVibe.AI says answered.
++ */
++export type VisionCompareRequest = {
++ [k: string]: unknown | undefined;
++} & {
++ /**
++ * The image as a data URL (base64).
++ */
++ image?: string;
++ /**
++ * A public image URL, fetched through Vision's private-address guard.
++ */
++ image_url?: string;
++ task?: "ask" | "describe" | "identify" | "count" | "translate" | "ocr";
++ question?: string;
++ /**
++ * @maxItems 3
++ */
++ models?: [] | [string] | [string, string] | [string, string, string];
++ lang?: string;
++};
++
++/** [email protected] (owner: vision) */
++/**
++ * vision.find-request@1: the body of POST /api/v1/index and of GET or POST /api/v1/find. `index` stores a scoped CLIP embedding for one of the caller's own items (`item_id`, optional `result_url`); `find` searches only the caller's own private index, by words (`q`) or by an example image. No public image index is ever built.
++ */
++export type VisionFindRequest = {
++ [k: string]: unknown | undefined;
++} & {
++ /**
++ * The image as a data URL (base64).
++ */
++ image?: string;
++ /**
++ * A public image URL, fetched through Vision's private-address guard.
++ */
++ image_url?: string;
++ q?: string;
++ item_id?: string;
++ result_url?: string;
++};
++
++/** [email protected] (owner: vision) */
++/**
++ * vision.receipt-request@1: the body of POST /api/v1/receipt. The local OCR reader extracts line items and a total; `fallback` asks OpenVibe.AI only when the local pass could not produce both.
++ */
++export type VisionReceiptRequest = {
++ [k: string]: unknown | undefined;
++} & {
++ /**
++ * The image as a data URL (base64).
++ */
++ image?: string;
++ /**
++ * A public image URL, fetched through Vision's private-address guard.
++ */
++ image_url?: string;
++ fallback?: boolean;
++};
++
++/** [email protected] (owner: vision) */
++/**
++ * vision.product-request@1: the body of POST /api/v1/product. The image is embedded locally with CLIP; `allow_rerank` lets one routed model reorder the candidate offers. Candidates come from OpenVibe.Deals and open.shoes; Vision never invents a price.
++ */
++export type VisionProductRequest = {
++ [k: string]: unknown | undefined;
++} & {
++ /**
++ * The image as a data URL (base64).
++ */
++ image?: string;
++ /**
++ * A public image URL, fetched through Vision's private-address guard.
++ */
++ image_url?: string;
++ allow_rerank?: boolean;
++};
++
++/** [email protected] (owner: vision) */
++/**
++ * vision.watch-request@1: the body of POST /api/v1/watch (owner-granted camera alerts). `classes` names 1–20 things to look for (snow, delivery, package, a DETR label, or "scene change"); `previous_image` is required for a scene change and is compared by histogram.
++ */
++export type VisionWatchRequest = {
++ [k: string]: unknown | undefined;
++} & {
++ /**
++ * The image as a data URL (base64).
++ */
++ image?: string;
++ /**
++ * A public image URL, fetched through Vision's private-address guard.
++ */
++ image_url?: string;
++ /**
++ * @minItems 1
++ * @maxItems 20
++ */
++ classes:
++ | [string]
++ | [string, string]
++ | [string, string, string]
++ | [string, string, string, string]
++ | [string, string, string, string, string]
++ | [string, string, string, string, string, string]
++ | [string, string, string, string, string, string, string]
++ | [string, string, string, string, string, string, string, string]
++ | [string, string, string, string, string, string, string, string, string]
++ | [string, string, string, string, string, string, string, string, string, string]
++ | [string, string, string, string, string, string, string, string, string, string, string]
++ | [string, string, string, string, string, string, string, string, string, string, string, string]
++ | [string, string, string, string, string, string, string, string, string, string, string, string, string]
++ | [string, string, string, string, string, string, string, string, string, string, string, string, string, string]
++ | [
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string
++ ]
++ | [
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string
++ ]
++ | [
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string
++ ]
++ | [
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string
++ ]
++ | [
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string
++ ]
++ | [
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string
++ ];
++ previous_image?: string;
++};
++
++/** [email protected] (owner: vision) */
++/**
++ * vision.moments-request@1: the body of POST /api/v1/moments. Up to 30 timestamped frames are compared with CLIP prompt embeddings; a histogram jump marks a scene change. VOD and stream chapters come from this.
++ */
++export interface VisionMomentsRequest {
++ /**
++ * @minItems 1
++ * @maxItems 30
++ */
++ frames: [
++ (
++ | {
++ [k: string]: unknown | undefined;
++ }
++ | {
++ [k: string]: unknown | undefined;
++ }
++ ),
++ ...(
++ | {
++ [k: string]: unknown | undefined;
++ }
++ | {
++ [k: string]: unknown | undefined;
++ }
++ )[]
++ ];
++ /**
++ * @maxItems 10
++ */
++ prompts?:
++ | []
++ | [string]
++ | [string, string]
++ | [string, string, string]
++ | [string, string, string, string]
++ | [string, string, string, string, string]
++ | [string, string, string, string, string, string]
++ | [string, string, string, string, string, string, string]
++ | [string, string, string, string, string, string, string, string]
++ | [string, string, string, string, string, string, string, string, string]
++ | [string, string, string, string, string, string, string, string, string, string];
++}
++
++/** [email protected] (owner: vision) */
++/**
++ * vision.answer@1: one saved answer on OpenVibe.Vision. It names the engine, model, provider and route that actually answered (never a model hint), the reason it was chosen, the cost OpenVibe.AI reported, the measured latency and the confidence. No image bytes are stored: only a sha256 and the declared media type.
++ */
++export interface VisionAnswer {
++ id: string;
++ task: "ask" | "describe" | "identify" | "count" | "translate" | "ocr" | "compare";
++ question?: string | null;
++ mode: "auto" | "local" | "ai";
++ source: "upload" | "url" | "data";
++ source_url?: string | null;
++ image_sha256: string;
++ image_mime: string;
++ image_bytes: number;
++ text?: string | null;
++ /**
++ * Engine details, OCR words, provenance and any compare rows; never an image.
++ */
++ answer: {};
++ model?: string | null;
++ provider?: string | null;
++ route?: string | null;
++ reason?: string | null;
++ engine?: string | null;
++ confidence?: number | null;
++ cost_usd: number;
++ latency_ms: number;
++ created_at: string;
++}
++
++/** [email protected] (owner: vision) */
++/**
++ * vision.answer-result@1: what POST /api/v1/ask, /describe and /ocr answer (`{ answer }`), and POST /api/v1/compare answers with `compare` rows and the local reader's summary added. Every AI-written text has passed noLooks: a look-naming answer is replaced, never repaired word by word.
++ */
++export interface VisionAnswerResult {
++ answer: VisionAnswer;
++ compare?: {
++ engine: "local" | "ai";
++ requested_model?: string | null;
++ model?: string | null;
++ provider?: string | null;
++ route?: string | null;
++ ok: boolean;
++ text?: string | null;
++ cost_usd: number;
++ latency_ms: number;
++ error_code?: string | null;
++ }[];
++ local?: {
++ ok: boolean;
++ model: string | null;
++ confidence: number | null;
++ error_code: string | null;
++ };
++}
++
++/** [email protected] (owner: vision) */
++/**
++ * vision.alt-result@1: POST /api/v1/alt answers one short, editable alt-text sentence about the scene, objects and action, never about a person. The text has passed noLooks; the receipt says which engine answered and at what cost.
++ */
++export interface VisionAltResult {
++ answer: {
++ alt_text: string;
++ editable: true;
++ receipt: {
++ engine: "local" | "routed" | "local+routed" | "local+routed description";
++ model: string | null;
++ cost_usd: number;
++ ms: number;
++ confidence: number | null;
++ retention: string;
++ };
++ };
++}
++
++/** [email protected] (owner: vision) */
++/**
++ * vision.find-result@1: POST /api/v1/index and GET/POST /api/v1/find answer a scoped private search over the caller's own indexed items: `results` (item_id, result_url, score) only, no one else's rows and no public index.
++ */
++export interface VisionFindResult {
++ answer: {
++ /**
++ * @maxItems 5000
++ */
++ results: {
++ item_id: string;
++ result_url?: string | null;
++ score: number;
++ }[];
++ receipt: {
++ engine: "local" | "routed" | "local+routed" | "local+routed description";
++ model: string | null;
++ cost_usd: number;
++ ms: number;
++ confidence: number | null;
++ retention: string;
++ };
++ };
++}
++
++/** [email protected] (owner: vision) */
++/**
++ * vision.receipt-result@1: POST /api/v1/receipt answers the store, the line items and the total the local OCR reader read (or the routed fallback read), with `raw_text` when the local pass produced it. OpenVibe.Food decides what enters a pantry; Vision returns the reading.
++ */
++export interface VisionReceiptResult {
++ answer: {
++ store: string | null;
++ /**
++ * @maxItems 100
++ */
++ items: {
++ name: string;
++ qty: number;
++ price: number;
++ }[];
++ total: number | null;
++ raw_text?: string;
++ receipt: {
++ engine: "local" | "routed" | "local+routed" | "local+routed description";
++ model: string | null;
++ cost_usd: number;
++ ms: number;
++ confidence: number | null;
++ retention: string;
++ };
++ };
++}
++
++/** [email protected] (owner: vision) */
++/**
++ * vision.product-result@1: POST /api/v1/product answers a description of the product only (never a person), the CLIP embedding, and candidate offers from OpenVibe.Deals and open.shoes with their score, price and URL. A room must not treat a candidate as a price it verified.
++ */
++export interface VisionProductResult {
++ answer: {
++ description: string;
++ embedding: number[];
++ /**
++ * @maxItems 10
++ */
++ offers:
++ | []
++ | [
++ {
++ source: string;
++ title: string;
++ url: string;
++ price?: number | null;
++ score?: number;
++ }
++ ]
++ | [
++ {
++ source: string;
++ title: string;
++ url: string;
++ price?: number | null;
++ score?: number;
++ },
++ {
++ source: string;
++ title: string;
++ url: string;
++ price?: number | null;
++ score?: number;
++ }
++ ]
++ | [
++ {
++ source: string;
++ title: string;
++ url: string;
++ price?: number | null;
++ score?: number;
++ },
++ {
++ source: string;
++ title: string;
++ url: string;
++ price?: number | null;
++ score?: number;
++ },
++ {
++ source: string;
++ title: string;
++ url: string;
++ price?: number | null;
++ score?: number;
++ }
++ ]
++ | [
++ {
++ source: string;
++ title: string;
++ url: string;
++ price?: number | null;
++ score?: number;
++ },
++ {
++ source: string;
++ title: string;
++ url: string;
++ price?: number | null;
++ score?: number;
++ },
++ {
++ source: string;
++ title: string;
++ url: string;
++ price?: number | null;
++ score?: number;
++ },
++ {
++ source: string;
++ title: string;
++ url: string;
++ price?: number | null;
++ score?: number;
++ }
++ ]
++ | [
++ {
++ source: string;
++ title: string;
++ url: string;
++ price?: number | null;
++ score?: number;
++ },
++ {
++ source: string;
++ title: string;
++ url: string;
++ price?: number | null;
++ score?: number;
++ },
++ {
++ source: string;
++ title: string;
++ url: string;
++ price?: number | null;
++ score?: number;
++ },
++ {
++ source: string;
++ title: string;
++ url: string;
++ price?: number | null;
++ score?: number;
++ },
++ {
++ source: string;
++ title: string;
++ url: string;
++ price?: number | null;
++ score?: number;
++ }
++ ]
++ | [
++ {
++ source: string;
++ title: string;
++ url: string;
++ price?: number | null;
++ score?: number;
++ },
++ {
++ source: string;
++ title: string;
++ url: string;
++ price?: number | null;
++ score?: number;
++ },
++ {
++ source: string;
++ title: string;
++ url: string;
++ price?: number | null;
++ score?: number;
++ },
++ {
++ source: string;
++ title: string;
++ url: string;
++ price?: number | null;
++ score?: number;
++ },
++ {
++ source: string;
++ title: string;
++ url: string;
++ price?: number | null;
++ score?: number;
++ },
++ {
++ source: string;
++ title: string;
++ url: string;
++ price?: number | null;
++ score?: number;
++ }
++ ]
++ | [
++ {
++ source: string;
++ title: string;
++ url: string;
++ price?: number | null;
++ score?: number;
++ },
++ {
++ source: string;
++ title: string;
++ url: string;
++ price?: number | null;
++ score?: number;
++ },
++ {
++ source: string;
++ title: string;
++ url: string;
++ price?: number | null;
++ score?: number;
++ },
++ {
++ source: string;
++ title: string;
++ url: string;
++ price?: number | null;
++ score?: number;
++ },
++ {
++ source: string;
++ title: string;
++ url: string;
++ price?: number | null;
++ score?: number;
++ },
++ {
++ source: string;
++ title: string;
++ url: string;
++ price?: number | null;
++ score?: number;
++ },
++ {
++ source: string;
++ title: string;
++ url: string;
++ price?: number | null;
++ score?: number;
++ }
++ ]
++ | [
++ {
++ source: string;
++ title: string;
++ url: string;
++ price?: number | null;
++ score?: number;
++ },
++ {
++ source: string;
++ title: string;
++ url: string;
++ price?: number | null;
++ score?: number;
++ },
++ {
++ source: string;
++ title: string;
++ url: string;
++ price?: number | null;
++ score?: number;
++ },
++ {
++ source: string;
++ title: string;
++ url: string;
++ price?: number | null;
++ score?: number;
++ },
++ {
++ source: string;
++ title: string;
++ url: string;
++ price?: number | null;
++ score?: number;
++ },
++ {
++ source: string;
++ title: string;
++ url: string;
++ price?: number | null;
++ score?: number;
++ },
++ {
++ source: string;
++ title: string;
++ url: string;
++ price?: number | null;
++ score?: number;
++ },
++ {
++ source: string;
++ title: string;
++ url: string;
++ price?: number | null;
++ score?: number;
++ }
++ ]
++ | [
++ {
++ source: string;
++ title: string;
++ url: string;
++ price?: number | null;
++ score?: number;
++ },
++ {
++ source: string;
++ title: string;
++ url: string;
++ price?: number | null;
++ score?: number;
++ },
++ {
++ source: string;
++ title: string;
++ url: string;
++ price?: number | null;
++ score?: number;
++ },
++ {
++ source: string;
++ title: string;
++ url: string;
++ price?: number | null;
++ score?: number;
++ },
++ {
++ source: string;
++ title: string;
++ url: string;
++ price?: number | null;
++ score?: number;
++ },
++ {
++ source: string;
++ title: string;
++ url: string;
++ price?: number | null;
++ score?: number;
++ },
++ {
++ source: string;
++ title: string;
++ url: string;
++ price?: number | null;
++ score?: number;
++ },
++ {
++ source: string;
++ title: string;
++ url: string;
++ price?: number | null;
++ score?: number;
++ },
++ {
++ source: string;
++ title: string;
++ url: string;
++ price?: number | null;
++ score?: number;
++ }
++ ]
++ | [
++ {
++ source: string;
++ title: string;
++ url: string;
++ price?: number | null;
++ score?: number;
++ },
++ {
++ source: string;
++ title: string;
++ url: string;
++ price?: number | null;
++ score?: number;
++ },
++ {
++ source: string;
++ title: string;
++ url: string;
++ price?: number | null;
++ score?: number;
++ },
++ {
++ source: string;
++ title: string;
++ url: string;
++ price?: number | null;
++ score?: number;
++ },
++ {
++ source: string;
++ title: string;
++ url: string;
++ price?: number | null;
++ score?: number;
++ },
++ {
++ source: string;
++ title: string;
++ url: string;
++ price?: number | null;
++ score?: number;
++ },
++ {
++ source: string;
++ title: string;
++ url: string;
++ price?: number | null;
++ score?: number;
++ },
++ {
++ source: string;
++ title: string;
++ url: string;
++ price?: number | null;
++ score?: number;
++ },
++ {
++ source: string;
++ title: string;
++ url: string;
++ price?: number | null;
++ score?: number;
++ },
++ {
++ source: string;
++ title: string;
++ url: string;
++ price?: number | null;
++ score?: number;
++ }
++ ];
++ warnings: string[];
++ receipt: {
++ engine: "local" | "routed" | "local+routed" | "local+routed description";
++ model: string | null;
++ cost_usd: number;
++ ms: number;
++ confidence: number | null;
++ retention: string;
++ };
++ };
++}
++
++/** [email protected] (owner: vision) */
++/**
++ * vision.watch-result@1: POST /api/v1/watch answers the requested classes found in a camera still (DETR labels, snow/delivery/package through CLIP, or a histogram scene change), each with a confidence and, for an object, a box.
++ */
++export interface VisionWatchResult {
++ answer: {
++ detections: {
++ class: string;
++ confidence: number;
++ box?: {
++ x: number;
++ y: number;
++ width: number;
++ height: number;
++ } | null;
++ }[];
++ receipt: {
++ engine: "local" | "routed" | "local+routed" | "local+routed description";
++ model: string | null;
++ cost_usd: number;
++ ms: number;
++ confidence: number | null;
++ retention: string;
++ };
++ };
++}
++
++/** [email protected] (owner: vision) */
++/**
++ * vision.moments-result@1: POST /api/v1/moments answers scene changes and candidate moments (timestamp, matching prompt, score) for up to 30 frames. A moment is a suggestion: the room decides whether it becomes a clip or a chapter.
++ */
++export interface VisionMomentsResult {
++ answer: {
++ scene_changes: {
++ timestamp_ms: number;
++ kind: "scene_change";
++ }[];
++ /**
++ * @maxItems 30
++ */
++ candidate_moments: {
++ timestamp_ms: number;
++ prompt: string;
++ score: number;
++ }[];
++ receipt: {
++ engine: "local" | "routed" | "local+routed" | "local+routed description";
++ model: string | null;
++ cost_usd: number;
++ ms: number;
++ confidence: number | null;
++ retention: string;
++ };
++ };
++}
++
++/** [email protected] (owner: vision) */
++/**
++ * vision.face-detection@1: POST /api/v1/faces/detect, an identity-free YuNet privacy tool. It counts faces and returns their boxes and scores; it stores no template, names no one and needs no consent. Anyone may use it.
++ */
++export interface VisionFaceDetection {
++ count: number;
++ boxes: {
++ x: number;
++ y: number;
++ width: number;
++ height: number;
++ score: number;
++ }[];
++}
++
++/** [email protected] (owner: vision) */
++/**
++ * vision.face-blur@1: POST /api/v1/faces/blur answers a redacted PNG derivative and the number of faces blurred in X-Faces-Blur. Identity-free, no template, and the original is never stored.
++ */
++export interface VisionFaceBlur {
++ media_type: "image/png";
++ face_count: number;
++}
++
++/** [email protected] (owner: vision) */
++/**
++ * vision.face-enrol-request@1: POST /api/v1/faces/enrol/:feature. `feature` is verify (1:1 verification of yourself), find (search your own uploads) or cast (an adult tagging named streams); each needs its own signed consent first, and cast also names the channel scope.
++ */
++export type VisionFaceEnrolRequest = {
++ [k: string]: unknown | undefined;
++} & {
++ /**
++ * The image as a data URL (base64).
++ */
++ image?: string;
++ /**
++ * A public image URL, fetched through Vision's private-address guard.
++ */
++ image_url?: string;
++ feature: "verify" | "find" | "cast";
++ channel?: string;
++};
++
++/** [email protected] (owner: vision) */
++/**
++ * vision.face-verify@1: POST /api/v1/faces/verify compares this person's own verification attempt with their enrolled template. It always requires that person's verify consent; without it the answer is faces.consent_required whether or not a template exists. A match alone never unlocks anything: liveness and a second factor are separate.
++ */
++export interface VisionFaceVerify {
++ score: number;
++ match: boolean;
++ threshold: number;
++ liveness: "not_checked" | "checked";
++}
++
++/** [email protected] (owner: vision) */
++/**
++ * vision.face-find-result@1: POST /api/v1/faces/find lists the caller's own private uploads that match their enrolled self-search template, as private links and scores. It requires find consent; without it the answer is faces.consent_required whether or not any media exists.
++ */
++export interface VisionFaceFindResult {
++ /**
++ * @maxItems 500
++ */
++ results: {
++ private_link: string;
++ score: number;
++ }[];
++}
++
++/** [email protected] (owner: vision) */
++/**
++ * vision.face-cast-match-request@1: POST /api/v1/faces/cast/match, called by OpenVibe.Live with a service token scoped to one channel (vision.face.cast.match). It matches only that channel's opted-in adult roster; a person who has not enrolled for this channel cannot appear, and the answer does not say whether they exist.
++ */
++export type VisionFaceCastMatchRequest = {
++ [k: string]: unknown | undefined;
++} & {
++ /**
++ * The image as a data URL (base64).
++ */
++ image?: string;
++ /**
++ * A public image URL, fetched through Vision's private-address guard.
++ */
++ image_url?: string;
++ channel: string;
++ media_ref: string;
++};
++
++/** [email protected] (owner: vision) */
++/**
++ * vision.face-cast-match@1: the matches inside one channel's consented roster, each with a score and box. No other subject is named and no non-roster face is described.
++ */
++export interface VisionFaceCastMatch {
++ channel: string;
++ matches: {
++ subject: string;
++ score: number;
++ box?: {
++ x: number;
++ y: number;
++ width: number;
++ height: number;
++ };
++ }[];
++ liveness: "not_checked" | "checked";
++}
++
++/** [email protected] (owner: vision) */
++/**
++ * vision.face-tag@1: one cast tag on a stream: which subject, which channel, which private media reference and when. GET /api/v1/faces/cast/tags lists the caller's own tags; revoking the cast consent removes them.
++ */
++export interface VisionFaceTag {
++ id: string;
++ channel: string;
++ media_ref: string;
++ created_at: string;
++}
++
++/** [email protected] (owner: vision) */
++/**
++ * vision.face-tag-list@1: GET /api/v1/faces/cast/tags answers the caller's own cast tags, newest first, as a bare array. Empty when they have not consented to cast matching.
++ */
++export type VisionFaceTagList = VisionFaceTag[];
++
++/** [email protected] (owner: vision) */
++/**
++ * vision.face-library-request@1: POST /api/v1/faces/library/cluster (region-gated), GET /library/groups and POST /library/correct. `action` is cluster (default), exclude, merge or split; `ids` names up to 100 of the caller's own clusters; `region` is a jurisdiction code the host allows. Unknown faces stay unlabelled.
++ */
++export interface VisionFaceLibraryRequest {
++ action: "cluster" | "exclude" | "merge" | "split";
++ region?: string;
++ /**
++ * @minItems 1
++ * @maxItems 100
++ */
++ ids?: [string, ...string[]];
++}
++
++/** [email protected] (owner: vision) */
++/**
++ * vision.face-library-result@1: clustering and group reads answer the caller's own private face groups as an array; a correction answers the action and how many clusters it updated.
++ */
++export type VisionFaceLibraryResult =
++ | VisionFaceLibraryGroup[]
++ | {
++ action: "exclude" | "merge" | "split";
++ updated: number;
++ };
++
++/** [email protected] (owner: vision) */
++/**
++ * vision.face-library-group@1: one private face cluster in a person's own Vision library: the media it came from, the group it was merged into, an optional label and whether the person excluded it. It never leaves that person's library.
++ */
++export interface VisionFaceLibraryGroup {
++ id: string;
++ media_id: string;
++ group_id: string;
++ label?: string | null;
++ excluded: boolean;
++ created_at: string;
++}
++
++/** [email protected] (owner: vision) */
++/**
++ * vision.consent@1: one consent record on OpenVibe.Vision. It says whose face or voice it is (`subject`), the modality, the purpose-scoped feature and scope (a channel or a library), who signed which text version, when it was granted, when it expires, and how and when it was revoked. Face matching tools refuse without an active record for that person, with the same answer whether or not the person exists. Templates are deleted when the purpose ends and at the latest three years after the last use (BIPA); a revoked audit record is deleted one year after the purpose ends (CUBI).
++ */
++export interface VisionConsent {
++ id: string;
++ /**
++ * The person whose face or voice this consent covers.
++ */
++ subject: string;
++ modality: "face" | "voice";
++ feature: "verify" | "find" | "cast" | "library" | "voice";
++ /**
++ * Empty means the person's own account; a cast grant names one channel, a library grant the library it applies to.
++ */
++ scope: string;
++ purpose: string;
++ text_version: string;
++ /**
++ * The person who signed; the subject themself, or a guardian where the law allows.
++ */
++ granted_by: string;
++ granted_at: string;
++ /**
++ * When the consent stops applying on its own; null when it lasts until revoked.
++ */
++ expires_at: string | null;
++ last_used_at: string;
++ revoked_at: string | null;
++ /**
++ * How it was revoked: by the person, by the retention job, or with the account.
++ */
++ revoked_by: "subject" | "retention" | "account_deletion" | null;
++ purpose_ends_at: string | null;
++ /**
++ * The outer limit the record and derived data may exist: three years after last use, or one year after the purpose ended.
++ */
++ retention_until: string;
++ status: "active" | "revoked" | "expired";
++ /**
++ * True once the template and derived results this consent covered are gone.
++ */
++ erased: boolean;
++}
++
++/** [email protected] (owner: vision) */
++/**
++ * vision.consent-request@1: POST /api/v1/faces/consents/:feature. The caller signs the current text (`text_version` must match this server's), names themself (`signed_name`), and for cast must confirm they are an adult; library grouping is refused outside the host's allowed regions. A repeat of an active consent changes nothing.
++ */
++export interface VisionConsentRequest {
++ signed_name: string;
++ text_version: string;
++ modality?: "face" | "voice";
++ /**
++ * Echo of the :feature path segment.
++ */
++ feature?: "verify" | "find" | "cast" | "library" | "voice";
++ channel?: string;
++ adult?: boolean;
++ region?: string;
++}
++
++/** [email protected] (owner: vision) */
++/**
++ * vision.consent-revocation@1: DELETE /api/v1/faces/consents/:feature answers with the revoked feature and scope, when, and that the template and derived data were erased in the same transaction. Revocation removes past cast tags and cannot be undone by reusing an old text version.
++ */
++export interface VisionConsentRevocation {
++ feature: "verify" | "find" | "cast" | "library" | "voice";
++ scope: string;
++ revoked_at: string;
++ revoked_by: "subject";
++ erased: true;
++}
++
++/** [email protected] (owner: vision) */
++/**
++ * vision.models@1: GET /api/v1/models names the AI route's offered provider:model hints, the configured hints and the local engines present, without any subject data. OpenVibe.AI's router remains the authority: this is a menu, not a promise.
++ */
++export interface VisionModels {
++ ai: boolean;
++ configured: string[];
++ offered: {}[];
++ local: string[];
++ note: string;
++}
++
++/** [email protected] (owner: vision) */
++/**
++ * vision.benchmarks@1: GET /api/v1/benchmarks aggregates compare samples per task, engine, provider and model with sample counts and mean latency and cost. No subject, image or answer text is included.
++ */
++export interface VisionBenchmarks {
++ count: number;
++ benchmarks: {
++ task: string;
++ engine: "local" | "ai";
++ provider: string | null;
++ model: string | null;
++ samples: number;
++ avg_latency_ms: number;
++ avg_cost_usd: number;
++ min_cost_usd: number;
++ max_latency_ms: number;
++ }[];
++}
++
++/** [email protected] (owner: vision) */
++/**
++ * vision.result.ready v1 (OpenVibe.Vision): a visual result is ready for its owner or caller. Envelope: source vision, subject { type: user, id: <requester> } (or { type: service, id: <caller> } when no person asked), visibility subject for a person and internal otherwise. The payload carries the identifiers, the engine and provenance, cost, latency and retention, never an image, a template or an answer text.
++ */
++export interface VisionResultReadyPayload {
++ result_id: string | null;
++ task:
++ | "ask"
++ | "describe"
++ | "identify"
++ | "count"
++ | "translate"
++ | "ocr"
++ | "compare"
++ | "alt"
++ | "find"
++ | "receipt"
++ | "product"
++ | "watch"
++ | "moments"
++ | "ui_check"
++ | "face_detect"
++ | "face_blur";
++ source: "upload" | "url" | "data" | "html" | "screenshot" | "none";
++ model: string | null;
++ provider: string | null;
++ route: string | null;
++ cost_usd: number;
++ latency_ms: number;
++ confidence: number | null;
++ retention: string;
++ consent_id: string | null;
++ created_at: string;
++}
++
++/** [email protected] (owner: vision) */
++/**
++ * vision.alert v1 (OpenVibe.Vision): an owner-granted camera alert. Envelope: source vision, subject { type: user, id: <source owner> }, visibility subject. The payload names the watched source reference and the classes found with their confidence; it carries no image bytes and no person's identity.
++ */
++export interface VisionAlertPayload {
++ kind: "object" | "scene_change" | "face";
++ source_ref: string;
++ /**
++ * @minItems 1
++ * @maxItems 20
++ */
++ classes:
++ | [string]
++ | [string, string]
++ | [string, string, string]
++ | [string, string, string, string]
++ | [string, string, string, string, string]
++ | [string, string, string, string, string, string]
++ | [string, string, string, string, string, string, string]
++ | [string, string, string, string, string, string, string, string]
++ | [string, string, string, string, string, string, string, string, string]
++ | [string, string, string, string, string, string, string, string, string, string]
++ | [string, string, string, string, string, string, string, string, string, string, string]
++ | [string, string, string, string, string, string, string, string, string, string, string, string]
++ | [string, string, string, string, string, string, string, string, string, string, string, string, string]
++ | [string, string, string, string, string, string, string, string, string, string, string, string, string, string]
++ | [
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string
++ ]
++ | [
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string
++ ]
++ | [
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string
++ ]
++ | [
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string
++ ]
++ | [
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string
++ ]
++ | [
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string
++ ];
++ confidence: number | null;
++ previous_ref?: string | null;
++ detected_at: string;
++}
++
/** [email protected] (owner: bench) */
/**
* bench.kind@1: a benchmark kind on OpenVibe.Bench. ROOM is the room that owns the kind, ENTRY the shape a submitted entry takes, JUDGE the default judge (pair, rubric, automatic_grader, test_suite or game_outcome), NAME the display name, SCHEMA the entry JSON Schema, and METRICS the board metrics (elo, pareto, cost, or a room's own).
ddiff --git a/manifests/capabilities/vision.alt.write.json b/manifests/capabilities/vision.alt.write.json
nnew file mode 100644
iindex 00000000..2c35e946
---- /dev/null
++++ b/manifests/capabilities/vision.alt.write.json
@@@ -0,0 +1,23 @@
++{
++ "id": "vision.alt.write",
++ "version": "1.0",
++ "owner": "vision",
++ "status": "active",
++ "visibility": "first-party",
++ "description": "Write editable alt text for an image on OpenVibe.Vision: POST /api/v1/alt returns one short sentence about the scene, objects and action, never about a person. The answer has passed noLooks; the receipt names the engine, cost and latency. Rooms use it for Pics, Media and Chat uploads.",
++ "permissions": [
++ "vision:alt"
++ ],
++ "resourceConstraints": [
++ "none"
++ ],
++ "quotaClass": "vision-alt",
++ "events": [
++ "vision.result.ready"
++ ],
++ "implementedBy": [
++ "POST /api/v1/alt"
++ ],
++ "inputSchema": "vision.image-request@1",
++ "outputSchema": "vision.alt-result@1"
++}
ddiff --git a/manifests/capabilities/vision.benchmarks.read.json b/manifests/capabilities/vision.benchmarks.read.json
nnew file mode 100644
iindex 00000000..acd3dcca
---- /dev/null
++++ b/manifests/capabilities/vision.benchmarks.read.json
@@@ -0,0 +1,21 @@
++{
++ "id": "vision.benchmarks.read",
++ "version": "1.0",
++ "owner": "vision",
++ "status": "active",
++ "visibility": "first-party",
++ "description": "Read OpenVibe.Vision's aggregate comparison results: GET /api/v1/benchmarks groups compare samples by task, engine, provider and model with sample counts, mean latency and mean cost, with no subject, image or answer text. Public.",
++ "permissions": [
++ "vision:benchmarks:read"
++ ],
++ "resourceConstraints": [
++ "none"
++ ],
++ "quotaClass": "vision-read",
++ "events": [],
++ "implementedBy": [
++ "GET /api/v1/benchmarks"
++ ],
++ "inputSchema": "common.no-body@1",
++ "outputSchema": "vision.benchmarks@1"
++}
ddiff --git a/manifests/capabilities/vision.camera.watch.json b/manifests/capabilities/vision.camera.watch.json
nnew file mode 100644
iindex 00000000..cb2da894
---- /dev/null
++++ b/manifests/capabilities/vision.camera.watch.json
@@@ -0,0 +1,24 @@
++{
++ "id": "vision.camera.watch",
++ "version": "1.0",
++ "owner": "vision",
++ "status": "active",
++ "visibility": "first-party",
++ "description": "Watch a camera still on OpenVibe.Vision: POST /api/v1/watch finds the requested classes (DETR labels, snow, delivery, package through CLIP, or a histogram scene change against previous_image) and returns detections with confidence and boxes. Owner-granted camera alerting only; a watched class can raise vision.alert.",
++ "permissions": [
++ "vision:watch"
++ ],
++ "resourceConstraints": [
++ "owner"
++ ],
++ "quotaClass": "vision-watch",
++ "events": [
++ "vision.result.ready",
++ "vision.alert"
++ ],
++ "implementedBy": [
++ "POST /api/v1/watch"
++ ],
++ "inputSchema": "vision.watch-request@1",
++ "outputSchema": "vision.watch-result@1"
++}
ddiff --git a/manifests/capabilities/vision.face.blur.json b/manifests/capabilities/vision.face.blur.json
nnew file mode 100644
iindex 00000000..f32eeb82
---- /dev/null
++++ b/manifests/capabilities/vision.face.blur.json
@@@ -0,0 +1,23 @@
++{
++ "id": "vision.face.blur",
++ "version": "1.0",
++ "owner": "vision",
++ "status": "active",
++ "visibility": "first-party",
++ "description": "Identity-free face blurring on OpenVibe.Vision: POST /api/v1/faces/blur returns a redacted PNG derivative and the blurred count in X-Faces-Blur. No template is stored and the original is discarded; anyone may use it.",
++ "permissions": [
++ "vision:face:blur"
++ ],
++ "resourceConstraints": [
++ "none"
++ ],
++ "quotaClass": "vision-face",
++ "events": [
++ "vision.result.ready"
++ ],
++ "implementedBy": [
++ "POST /api/v1/faces/blur"
++ ],
++ "inputSchema": "vision.image-request@1",
++ "outputSchema": "vision.face-blur@1"
++}
ddiff --git a/manifests/capabilities/vision.face.cast.match.json b/manifests/capabilities/vision.face.cast.match.json
nnew file mode 100644
iindex 00000000..05ff4bb4
---- /dev/null
++++ b/manifests/capabilities/vision.face.cast.match.json
@@@ -0,0 +1,23 @@
++{
++ "id": "vision.face.cast.match",
++ "version": "1.0",
++ "owner": "vision",
++ "status": "active",
++ "visibility": "first-party",
++ "description": "Match faces against one channel's opted-in adult cast roster for OpenVibe.Live: POST /api/v1/faces/cast/match. The caller is the Live service token scoped to that channel (its channel_id claim must equal the body's channel) and must hold this capability. A non-consented person is never matched and the answer does not say whether they exist.",
++ "permissions": [
++ "vision:face:cast:match"
++ ],
++ "resourceConstraints": [
++ "none"
++ ],
++ "quotaClass": "vision-face",
++ "events": [
++ "vision.result.ready"
++ ],
++ "implementedBy": [
++ "POST /api/v1/faces/cast/match"
++ ],
++ "inputSchema": "vision.face-cast-match-request@1",
++ "outputSchema": "vision.face-cast-match@1"
++}
ddiff --git a/manifests/capabilities/vision.face.cast.read.json b/manifests/capabilities/vision.face.cast.read.json
nnew file mode 100644
iindex 00000000..b568d403
---- /dev/null
++++ b/manifests/capabilities/vision.face.cast.read.json
@@@ -0,0 +1,21 @@
++{
++ "id": "vision.face.cast.read",
++ "version": "1.0",
++ "owner": "vision",
++ "status": "active",
++ "visibility": "first-party",
++ "description": "Read your own cast tags on OpenVibe.Vision: GET /api/v1/faces/cast/tags lists the named channel and media reference for each tag the person's own cast consent produced. Revoking the consent removes them.",
++ "permissions": [
++ "vision:face:cast:read"
++ ],
++ "resourceConstraints": [
++ "owner"
++ ],
++ "quotaClass": "vision-face",
++ "events": [],
++ "implementedBy": [
++ "GET /api/v1/faces/cast/tags"
++ ],
++ "inputSchema": "common.no-body@1",
++ "outputSchema": "vision.face-tag-list@1"
++}
ddiff --git a/manifests/capabilities/vision.face.consent.manage.json b/manifests/capabilities/vision.face.consent.manage.json
nnew file mode 100644
iindex 00000000..850a283d
---- /dev/null
++++ b/manifests/capabilities/vision.face.consent.manage.json
@@@ -0,0 +1,24 @@
++{
++ "id": "vision.face.consent.manage",
++ "version": "1.0",
++ "owner": "vision",
++ "status": "active",
++ "visibility": "first-party",
++ "description": "Read, grant and revoke purpose-scoped face or voice consent on OpenVibe.Vision: GET /api/v1/faces/consents and /consents/text, POST /consents/:feature with the signed current text, and DELETE /consents/:feature, which revokes and erases the template and derived data in one transaction. A person manages only their own record; the record says whose face or voice, the scope, expiry and how it was revoked (vision.consent@1).",
++ "permissions": [
++ "vision:face:consent"
++ ],
++ "resourceConstraints": [
++ "owner"
++ ],
++ "quotaClass": "vision-face",
++ "events": [],
++ "implementedBy": [
++ "GET /api/v1/faces/consents",
++ "GET /api/v1/faces/consents/text",
++ "POST /api/v1/faces/consents/:feature",
++ "DELETE /api/v1/faces/consents/:feature"
++ ],
++ "inputSchema": "vision.consent-request@1",
++ "outputSchema": "vision.consent@1"
++}
ddiff --git a/manifests/capabilities/vision.face.detect.json b/manifests/capabilities/vision.face.detect.json
nnew file mode 100644
iindex 00000000..6b324145
---- /dev/null
++++ b/manifests/capabilities/vision.face.detect.json
@@@ -0,0 +1,23 @@
++{
++ "id": "vision.face.detect",
++ "version": "1.0",
++ "owner": "vision",
++ "status": "active",
++ "visibility": "first-party",
++ "description": "Identity-free face detection on OpenVibe.Vision: POST /api/v1/faces/detect counts faces and returns their boxes and scores with local YuNet. It stores no template, names no one and needs no consent; anyone may use it.",
++ "permissions": [
++ "vision:face:detect"
++ ],
++ "resourceConstraints": [
++ "none"
++ ],
++ "quotaClass": "vision-face",
++ "events": [
++ "vision.result.ready"
++ ],
++ "implementedBy": [
++ "POST /api/v1/faces/detect"
++ ],
++ "inputSchema": "vision.image-request@1",
++ "outputSchema": "vision.face-detection@1"
++}
ddiff --git a/manifests/capabilities/vision.face.find.self.json b/manifests/capabilities/vision.face.find.self.json
nnew file mode 100644
iindex 00000000..f5020a6f
---- /dev/null
++++ b/manifests/capabilities/vision.face.find.self.json
@@@ -0,0 +1,25 @@
++{
++ "id": "vision.face.find.self",
++ "version": "1.0",
++ "owner": "vision",
++ "status": "active",
++ "visibility": "first-party",
++ "description": "Find yourself in your own private uploads on OpenVibe.Vision: POST /api/v1/faces/media, GET /api/v1/faces/media/:id and POST /api/v1/faces/find. Refused with faces.consent_required without find consent, the same answer whether or not any media exists; results are private links to the caller's own rows.",
++ "permissions": [
++ "vision:face:find"
++ ],
++ "resourceConstraints": [
++ "owner"
++ ],
++ "quotaClass": "vision-face",
++ "events": [
++ "vision.result.ready"
++ ],
++ "implementedBy": [
++ "POST /api/v1/faces/media",
++ "GET /api/v1/faces/media/:id",
++ "POST /api/v1/faces/find"
++ ],
++ "inputSchema": "vision.find-request@1",
++ "outputSchema": "vision.face-find-result@1"
++}
ddiff --git a/manifests/capabilities/vision.face.library.json b/manifests/capabilities/vision.face.library.json
nnew file mode 100644
iindex 00000000..41f6038a
---- /dev/null
++++ b/manifests/capabilities/vision.face.library.json
@@@ -0,0 +1,25 @@
++{
++ "id": "vision.face.library",
++ "version": "1.0",
++ "owner": "vision",
++ "status": "active",
++ "visibility": "first-party",
++ "description": "Group faces in your own Vision library: POST /api/v1/faces/library/cluster (region-gated), GET /api/v1/faces/library/groups and POST /api/v1/faces/library/correct (exclude, merge, split). Requires library consent; unknown faces stay unlabelled and nothing leaves that person's library.",
++ "permissions": [
++ "vision:face:library"
++ ],
++ "resourceConstraints": [
++ "owner"
++ ],
++ "quotaClass": "vision-face",
++ "events": [
++ "vision.result.ready"
++ ],
++ "implementedBy": [
++ "POST /api/v1/faces/library/cluster",
++ "GET /api/v1/faces/library/groups",
++ "POST /api/v1/faces/library/correct"
++ ],
++ "inputSchema": "vision.face-library-request@1",
++ "outputSchema": "vision.face-library-result@1"
++}
ddiff --git a/manifests/capabilities/vision.face.verify.self.json b/manifests/capabilities/vision.face.verify.self.json
nnew file mode 100644
iindex 00000000..7448837a
---- /dev/null
++++ b/manifests/capabilities/vision.face.verify.self.json
@@@ -0,0 +1,24 @@
++{
++ "id": "vision.face.verify.self",
++ "version": "1.0",
++ "owner": "vision",
++ "status": "active",
++ "visibility": "first-party",
++ "description": "Verify yourself against your own enrolled template on OpenVibe.Vision: POST /api/v1/faces/enrol/verify and /api/v1/faces/verify. Refused with faces.consent_required without that person's verify consent, the same answer whether or not a template exists; a match alone unlocks nothing.",
++ "permissions": [
++ "vision:face:verify"
++ ],
++ "resourceConstraints": [
++ "owner"
++ ],
++ "quotaClass": "vision-face",
++ "events": [
++ "vision.result.ready"
++ ],
++ "implementedBy": [
++ "POST /api/v1/faces/enrol/:feature",
++ "POST /api/v1/faces/verify"
++ ],
++ "inputSchema": "vision.face-enrol-request@1",
++ "outputSchema": "vision.face-verify@1"
++}
ddiff --git a/manifests/capabilities/vision.image.ask.json b/manifests/capabilities/vision.image.ask.json
nnew file mode 100644
iindex 00000000..bc0dcb9c
---- /dev/null
++++ b/manifests/capabilities/vision.image.ask.json
@@@ -0,0 +1,24 @@
++{
++ "id": "vision.image.ask",
++ "version": "1.0",
++ "owner": "vision",
++ "status": "active",
++ "visibility": "first-party",
++ "description": "Ask or describe an image on OpenVibe.Vision: POST /api/v1/ask and /api/v1/describe take an image (multipart, data URL or public URL), a question, a task (ask, describe, identify, count, translate, ocr) and a mode (auto, local, ai). A read question is answered by the local OCR reader first; everything else goes through OpenVibe.AI, whose provenance the answer reports. In auto mode a read question also works anonymously; any other task needs a signed-in person or this capability.",
++ "permissions": [
++ "vision:ask"
++ ],
++ "resourceConstraints": [
++ "none"
++ ],
++ "quotaClass": "vision-ask",
++ "events": [
++ "vision.result.ready"
++ ],
++ "implementedBy": [
++ "POST /api/v1/ask",
++ "POST /api/v1/describe"
++ ],
++ "inputSchema": "vision.ask-request@1",
++ "outputSchema": "vision.answer-result@1"
++}
ddiff --git a/manifests/capabilities/vision.media.find.json b/manifests/capabilities/vision.media.find.json
nnew file mode 100644
iindex 00000000..90136c65
---- /dev/null
++++ b/manifests/capabilities/vision.media.find.json
@@@ -0,0 +1,25 @@
++{
++ "id": "vision.media.find",
++ "version": "1.0",
++ "owner": "vision",
++ "status": "active",
++ "visibility": "first-party",
++ "description": "Index and search a person's own images by CLIP embedding on OpenVibe.Vision: POST /api/v1/index stores one scoped embedding per item and GET/POST /api/v1/find searches only that person's own private index, by words or by an example image. No public image index is made; every read is owner-checked.",
++ "permissions": [
++ "vision:find"
++ ],
++ "resourceConstraints": [
++ "owner"
++ ],
++ "quotaClass": "vision-find",
++ "events": [
++ "vision.result.ready"
++ ],
++ "implementedBy": [
++ "POST /api/v1/index",
++ "GET /api/v1/find",
++ "POST /api/v1/find"
++ ],
++ "inputSchema": "vision.find-request@1",
++ "outputSchema": "vision.find-result@1"
++}
ddiff --git a/manifests/capabilities/vision.model.compare.json b/manifests/capabilities/vision.model.compare.json
nnew file mode 100644
iindex 00000000..a981f986
---- /dev/null
++++ b/manifests/capabilities/vision.model.compare.json
@@@ -0,0 +1,23 @@
++{
++ "id": "vision.model.compare",
++ "version": "1.0",
++ "owner": "vision",
++ "status": "active",
++ "visibility": "first-party",
++ "description": "Run OpenVibe.Vision's side-by-side comparison: POST /api/v1/compare runs the local reader and up to three AI model hints on the same image and returns each row with its success, text, cost and latency. The requested model is a hint; OpenVibe.AI's own provenance is what each row reports.",
++ "permissions": [
++ "vision:compare"
++ ],
++ "resourceConstraints": [
++ "none"
++ ],
++ "quotaClass": "vision-compare",
++ "events": [
++ "vision.result.ready"
++ ],
++ "implementedBy": [
++ "POST /api/v1/compare"
++ ],
++ "inputSchema": "vision.compare-request@1",
++ "outputSchema": "vision.answer-result@1"
++}
ddiff --git a/manifests/capabilities/vision.models.read.json b/manifests/capabilities/vision.models.read.json
nnew file mode 100644
iindex 00000000..6db1f30e
---- /dev/null
++++ b/manifests/capabilities/vision.models.read.json
@@@ -0,0 +1,21 @@
++{
++ "id": "vision.models.read",
++ "version": "1.0",
++ "owner": "vision",
++ "status": "active",
++ "visibility": "first-party",
++ "description": "Read OpenVibe.Vision's model menu: GET /api/v1/models returns the AI route's offered provider:model hints, the configured hints and the local engines present, with no subject data. Public; OpenVibe.AI's router is still the authority.",
++ "permissions": [
++ "vision:models:read"
++ ],
++ "resourceConstraints": [
++ "none"
++ ],
++ "quotaClass": "vision-read",
++ "events": [],
++ "implementedBy": [
++ "GET /api/v1/models"
++ ],
++ "inputSchema": "common.no-body@1",
++ "outputSchema": "vision.models@1"
++}
ddiff --git a/manifests/capabilities/vision.product.match.json b/manifests/capabilities/vision.product.match.json
nnew file mode 100644
iindex 00000000..67a29c84
---- /dev/null
++++ b/manifests/capabilities/vision.product.match.json
@@@ -0,0 +1,23 @@
++{
++ "id": "vision.product.match",
++ "version": "1.0",
++ "owner": "vision",
++ "status": "active",
++ "visibility": "first-party",
++ "description": "Match a product photo to offers on OpenVibe.Vision: POST /api/v1/product describes the product only (never a person), embeds it with CLIP and returns candidate offers from OpenVibe.Deals and open.shoes with their score and URL. A candidate is not a verified price.",
++ "permissions": [
++ "vision:product"
++ ],
++ "resourceConstraints": [
++ "none"
++ ],
++ "quotaClass": "vision-product",
++ "events": [
++ "vision.result.ready"
++ ],
++ "implementedBy": [
++ "POST /api/v1/product"
++ ],
++ "inputSchema": "vision.product-request@1",
++ "outputSchema": "vision.product-result@1"
++}
ddiff --git a/manifests/capabilities/vision.receipt.read.json b/manifests/capabilities/vision.receipt.read.json
nnew file mode 100644
iindex 00000000..84c4a8c8
---- /dev/null
++++ b/manifests/capabilities/vision.receipt.read.json
@@@ -0,0 +1,23 @@
++{
++ "id": "vision.receipt.read",
++ "version": "1.0",
++ "owner": "vision",
++ "status": "active",
++ "visibility": "first-party",
++ "description": "Read a receipt on OpenVibe.Vision: POST /api/v1/receipt returns the store, line items and total the local OCR reader extracted; `fallback` asks OpenVibe.AI only when the local pass could not produce both. OpenVibe.Food decides what enters a pantry.",
++ "permissions": [
++ "vision:receipt"
++ ],
++ "resourceConstraints": [
++ "none"
++ ],
++ "quotaClass": "vision-receipt",
++ "events": [
++ "vision.result.ready"
++ ],
++ "implementedBy": [
++ "POST /api/v1/receipt"
++ ],
++ "inputSchema": "vision.receipt-request@1",
++ "outputSchema": "vision.receipt-result@1"
++}
ddiff --git a/manifests/capabilities/vision.text.read.json b/manifests/capabilities/vision.text.read.json
nnew file mode 100644
iindex 00000000..69fcb757
---- /dev/null
++++ b/manifests/capabilities/vision.text.read.json
@@@ -0,0 +1,23 @@
++{
++ "id": "vision.text.read",
++ "version": "1.0",
++ "owner": "vision",
++ "status": "active",
++ "visibility": "first-party",
++ "description": "Read text from an image with OpenVibe.Vision's local Tesseract 5 reader: POST /api/v1/ocr returns the text, mean confidence and word boxes at zero cost, anonymously. `fallback` asks OpenVibe.AI only when the local pass found no usable text, and that fallback needs a signed-in person or this capability.",
++ "permissions": [
++ "vision:ocr"
++ ],
++ "resourceConstraints": [
++ "none"
++ ],
++ "quotaClass": "vision-ocr",
++ "events": [
++ "vision.result.ready"
++ ],
++ "implementedBy": [
++ "POST /api/v1/ocr"
++ ],
++ "inputSchema": "vision.ocr-request@1",
++ "outputSchema": "vision.answer-result@1"
++}
ddiff --git a/manifests/capabilities/vision.video.moments.json b/manifests/capabilities/vision.video.moments.json
nnew file mode 100644
iindex 00000000..ddfce477
---- /dev/null
++++ b/manifests/capabilities/vision.video.moments.json
@@@ -0,0 +1,23 @@
++{
++ "id": "vision.video.moments",
++ "version": "1.0",
++ "owner": "vision",
++ "status": "active",
++ "visibility": "first-party",
++ "description": "Find moments in a stream or VOD on OpenVibe.Vision: POST /api/v1/moments compares up to 30 timestamped frames with CLIP prompt embeddings and a histogram scene-change pass, returning scene changes and candidate moments. A candidate is a suggestion for Live or Media to accept.",
++ "permissions": [
++ "vision:moments"
++ ],
++ "resourceConstraints": [
++ "owner"
++ ],
++ "quotaClass": "vision-moments",
++ "events": [
++ "vision.result.ready"
++ ],
++ "implementedBy": [
++ "POST /api/v1/moments"
++ ],
++ "inputSchema": "vision.moments-request@1",
++ "outputSchema": "vision.moments-result@1"
++}
ddiff --git a/manifests/services/vision.json b/manifests/services/vision.json
iindex 9ae70270..61c9a6e1 100644
---- a/manifests/services/vision.json
++++ b/manifests/services/vision.json
@@@ -1,7 +1,7 @@
{
"version": "0.1.0",
"contractRanges": {
-- "openvibe-contracts": ">=0.229.0 <1.0.0"
++ "openvibe-contracts": ">=0.240.0 <1.0.0"
},
"id": "vision",
"name": "OpenVibe.Vision",
@@@ -16,9 +16,31 @@
"publicSite": "service"
},
"capabilities": [
-- "vision.ui.check"
++ "vision.ui.check",
++ "vision.image.ask",
++ "vision.text.read",
++ "vision.model.compare",
++ "vision.alt.write",
++ "vision.media.find",
++ "vision.receipt.read",
++ "vision.product.match",
++ "vision.camera.watch",
++ "vision.video.moments",
++ "vision.face.detect",
++ "vision.face.blur",
++ "vision.face.consent.manage",
++ "vision.face.verify.self",
++ "vision.face.find.self",
++ "vision.face.library",
++ "vision.face.cast.match",
++ "vision.face.cast.read",
++ "vision.models.read",
++ "vision.benchmarks.read"
++ ],
++ "eventsProduced": [
++ "vision.result.ready",
++ "vision.alert"
],
-- "eventsProduced": [],
"eventsConsumed": [
"network.account.export_requested",
"network.account.deleted"
@@@ -37,6 +59,8 @@
"drains": [
"HTTP server closed and in-flight requests drained",
"JWKS refresher stopped",
++ "face retention timer stopped",
++ "outbox publisher stopped (its send in progress awaited; unsent rows stay for the next start)",
"telemetry interval and retention stopped, then the telemetry buffer flushed",
"PostgreSQL pool closed, then the Events subscriptions stopped"
]
@@@ -47,6 +71,10 @@
"kind": "state",
"what": "answers, compare samples and UI checks live in PostgreSQL; a restart reopens the pool and clears only the in-process rate limits and telemetry buffer, so saved rows stay readable by id"
},
++ {
++ "kind": "outbox",
++ "what": "unsent vision.result.ready and vision.alert events in the outbox table go to OpenVibe.Events once the publisher starts (at least once; Events dedupes by event_id)"
++ },
{
"kind": "consumer",
"what": "OpenVibe.Events redelivers network.account.export_requested and network.account.deleted that were pending during the stop; account_data_events applies each once, so a redelivery changes nothing"
@@@ -60,17 +88,17 @@
],
"window": "automatic only while ovhost waits for readiness after the restart; manual afterwards, as long as no blocker applies",
"blockers": {
-- "none": "migration 0001 only creates Vision's own tables (answers, compare samples, UI checks, account-data receipts, telemetry); an earlier release reads none of them"
++ "none": "migration 0001 only creates Vision's own tables (answers, compare samples, UI checks, account-data receipts, telemetry); migration 0003 adds the face tables and 0004 adds consent expiry/revocation columns and the event outbox, all additive, so an earlier release reads none of them"
}
},
"contracts": {
-- "range": ">=0.229.0 <1.0.0"
++ "range": ">=0.240.0 <1.0.0"
},
"leases": {
"none": "no claims: one process serves the site and the API"
}
},
-- "notes": "Port 5130, unit openvibe-vision.service, database ov_vision. The Vision room (plan 2026-10-28 §5.8): ask a picture, read text with the local Tesseract 5 engine first, compare the local reader with up to three AI hints, and check HTML, a public URL or a screenshot for overflow, cut-off, low contrast, overlap and layering faults. OpenVibe.AI is the only model gateway (ai.run.create; the OpenAI-compatible POST /v1/chat/completions is workflow ai.complete); no provider key is held, no face recognition is used and no image bytes are stored. OAuth client `vision`; answers account export and deletion (ADR-033) and subscribes to the two account Events topics. v1 has no public records, so it has no Search outbox. Of Vision's four machine capabilities (vision.ask, vision.ocr, vision.compare, vision.ui_check), 0.235.0 registers only vision.ui.check, the route Design's Arena calls today; the other three are served but wait for a caller.",
++ "notes": "Port 5130, unit openvibe-vision.service, database ov_vision. The Vision room (plan 2026-10-28 §5.8b): ask a picture, read text with the local Tesseract 5 engine first, compare the local reader with up to three AI hints, check HTML, a public URL or a screenshot for UI faults, write alt text, search only your own images by CLIP, read receipts, match products to offers, watch camera stills and find VOD moments. Faces are local (YuNet/SFace): detect and blur are identity-free, and verify, find, cast and library grouping each need a separate signed consent record (vision.consent@1) with a scope, expiry and a revocation that erases the derived data. Refusal is the same whether or not the person exists. OpenVibe.AI is the only model gateway (ai.run.create); no provider key is held, no image bytes are stored for general answers and no face template enters Search, a prompt or Bench. Public AI text passes noLooks. OAuth client `vision`; MCP at POST /mcp (RFC 9728 protected-resource metadata) exposes the image tools and never enrolment, verification or face-group mutation. Answers account export and deletion (ADR-033), subscribes to the two account Events topics and publishes vision.result.ready and vision.alert through its outbox. v1 has no public records, so it has no Search outbox.",
"health": "/api/health",
"ready": "/api/ready",
"internalOrigin": "http://127.0.0.1:5130",
ddiff --git a/package.json b/package.json
iindex b7df3ccd..5f6dcc52 100644
---- a/package.json
++++ b/package.json
@@@ -1,6 +1,6 @@
{
"name": "openvibe-contracts",
-- "version": "0.239.0",
++ "version": "0.240.0",
"description": "Machine-readable contracts for the OpenVibe network: schemas, manifests, ids, error envelope, capability checks.",
"main": "index.js",
"types": "generated/typescript/index.d.ts",
ddiff --git a/test/run.js b/test/run.js
iindex 657ff465..99317f9c 100644
---- a/test/run.js
++++ b/test/run.js
@@@ -121,7 +121,7 @@ for (const id of ['events.event.publish', 'events.event.read', 'events.subscript
for (const c of payloads) {
const major = Number(c.version.split('.')[0]);
ok(c.schema === `events/payloads/${c.id}.v${major}.json`, `${c.id} lives at events/payloads/<event_type>.v<major>.json`);
-- ok(/^[a-z][a-z0-9_]*(\.[a-z0-9_]+){2,}$/.test(c.id), `${c.id} is an event type`);
++ ok(/^[a-z][a-z0-9_]*(\.[a-z0-9_]+){1,}$/.test(c.id), `${c.id} is an event type`);
ok(services.get(c.owner) && (c.status === 'retired' || services.get(c.owner).eventsProduced.includes(c.id)), `${c.owner} manifest lists current ${c.id} in eventsProduced`);
ok(['active', 'planned', 'retired'].includes(c.status), `${c.id} status ${c.status}`);
if (c.status === 'retired') ok(!services.get(c.owner).eventsProduced.includes(c.id), `${c.owner} no longer produces retired ${c.id}`);