hyperstudy-perception 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. hyperstudy_perception-0.1.0.dist-info/METADATA +53 -0
  2. hyperstudy_perception-0.1.0.dist-info/RECORD +82 -0
  3. hyperstudy_perception-0.1.0.dist-info/WHEEL +4 -0
  4. hyperstudy_perception-0.1.0.dist-info/entry_points.txt +2 -0
  5. hyperstudy_perception-0.1.0.dist-info/licenses/LICENSE +21 -0
  6. perception/__init__.py +0 -0
  7. perception/_pipeline/RELEASE.json +59 -0
  8. perception/_pipeline/cloud/runpod/guard.sh +37 -0
  9. perception/_pipeline/cloud/runpod/image/Dockerfile +39 -0
  10. perception/_pipeline/cloud/runpod/image/start.sh +11 -0
  11. perception/_pipeline/cloud/runpod/image/tag.py +40 -0
  12. perception/_pipeline/cloud/skypilot/gpu-faces.yaml +39 -0
  13. perception/_pipeline/cloud/skypilot/gpu-tune.yaml +55 -0
  14. perception/_pipeline/cloud/skypilot/guard.sh +20 -0
  15. perception/_pipeline/cloud/skypilot/iam-perception-node.json +18 -0
  16. perception/_pipeline/cloud/skypilot/launch.sh +63 -0
  17. perception/_pipeline/cloud/skypilot/node-gpu.yaml +25 -0
  18. perception/_pipeline/cloud/skypilot/node-setup.sh +22 -0
  19. perception/_pipeline/cloud/skypilot/sky.yaml +7 -0
  20. perception/_pipeline/cloud/skypilot/smoke.yaml +28 -0
  21. perception/_pipeline/envs/api/pyproject.toml +12 -0
  22. perception/_pipeline/envs/api/uv.lock +276 -0
  23. perception/_pipeline/envs/audio/pyproject.toml +12 -0
  24. perception/_pipeline/envs/audio/uv.lock +575 -0
  25. perception/_pipeline/envs/core/pyproject.toml +12 -0
  26. perception/_pipeline/envs/core/uv.lock +157 -0
  27. perception/_pipeline/envs/faces/pyproject.toml +12 -0
  28. perception/_pipeline/envs/faces/uv.lock +1180 -0
  29. perception/_pipeline/graphs/decode.yaml +11 -0
  30. perception/_pipeline/graphs/faces.yaml +14 -0
  31. perception/_pipeline/graphs/full.yaml +31 -0
  32. perception/_pipeline/graphs/mvp-captions.yaml +27 -0
  33. perception/_pipeline/graphs/mvp.yaml +17 -0
  34. perception/_pipeline/nodes/audio/asr/manifest.yaml +20 -0
  35. perception/_pipeline/nodes/audio/asr/node.py +74 -0
  36. perception/_pipeline/nodes/audio/asr/prepare.py +12 -0
  37. perception/_pipeline/nodes/audio/asr/test_wav.py +38 -0
  38. perception/_pipeline/nodes/audio/asr/wav.py +19 -0
  39. perception/_pipeline/nodes/conftest.py +64 -0
  40. perception/_pipeline/nodes/core/decode/manifest.yaml +19 -0
  41. perception/_pipeline/nodes/core/decode/media.py +104 -0
  42. perception/_pipeline/nodes/core/decode/node.py +56 -0
  43. perception/_pipeline/nodes/core/decode/test_media.py +65 -0
  44. perception/_pipeline/nodes/testdata/README.md +10 -0
  45. perception/_pipeline/nodes/testdata/rotated-90.mp4 +0 -0
  46. perception/_pipeline/nodes/vision/face-track/frames.py +88 -0
  47. perception/_pipeline/nodes/vision/face-track/loader.py +18 -0
  48. perception/_pipeline/nodes/vision/face-track/manifest.yaml +30 -0
  49. perception/_pipeline/nodes/vision/face-track/node.py +117 -0
  50. perception/_pipeline/nodes/vision/face-track/prepare.py +8 -0
  51. perception/_pipeline/nodes/vision/face-track/test_frames.py +46 -0
  52. perception/_pipeline/nodes/vision/face-track/test_loader.py +16 -0
  53. perception/_pipeline/nodes/vision/face-track/test_thumbnails.py +66 -0
  54. perception/_pipeline/nodes/vision/face-track/thumbnails.py +68 -0
  55. perception/_pipeline/nodes/vision/face-track/tracking.py +150 -0
  56. perception/_pipeline/nodes/vlm/shot-captions/manifest.yaml +29 -0
  57. perception/_pipeline/nodes/vlm/shot-captions/node.py +185 -0
  58. perception/_pipeline/schema/README.md +54 -0
  59. perception/_pipeline/schema/bundle-0.1.schema.json +371 -0
  60. perception/_pipeline/sdk/pyproject.toml +13 -0
  61. perception/_pipeline/sdk/src/perception_node/__init__.py +157 -0
  62. perception/artifacts.py +180 -0
  63. perception/backends/__init__.py +0 -0
  64. perception/backends/base.py +179 -0
  65. perception/backends/cloud.py +214 -0
  66. perception/backends/native.py +140 -0
  67. perception/backends/probe.py +44 -0
  68. perception/backends/remote.py +140 -0
  69. perception/cli.py +780 -0
  70. perception/estimates.py +68 -0
  71. perception/export.py +100 -0
  72. perception/hosts.py +201 -0
  73. perception/ir.py +245 -0
  74. perception/keys.py +259 -0
  75. perception/manifests.py +169 -0
  76. perception/plan.py +170 -0
  77. perception/release.py +61 -0
  78. perception/remote_driver.py +990 -0
  79. perception/runpod.py +100 -0
  80. perception/scheduler.py +552 -0
  81. perception/setup.py +72 -0
  82. perception/store.py +219 -0
@@ -0,0 +1,185 @@
1
+ """vlm.shot-captions: one description per shot from its keyframes and speech, with the face roster.
2
+
3
+ Ported from pipeline/bakeoff/caption.py (Claude Haiku 5.5 with the face roster: 126 of 135 boxed people named on
4
+ OOO, about $0.04 a clip). Each shot sees its three keyframes, with tracked faces boxed and labelled, the speech that
5
+ starts inside it, the people seen so far and the previous shot's description; never anything from later shots.
6
+ That chaining is why the calls run in order rather than as a Message Batch.
7
+
8
+ Writes the shots layer with captions (it refines the decode node's shots layer) and the on-screen text layer.
9
+ """
10
+
11
+ import base64
12
+ import io
13
+ import json
14
+ import time
15
+
16
+ from perception_node import Node, NodeError
17
+
18
+ PRICE_PER_MTOK = {"claude-haiku-5-5": (0.10, 0.50), "claude-opus-5-5": (4.00, 20.00)} # input, output (USD)
19
+
20
+ SCHEMA = {
21
+ "type": "object",
22
+ "properties": {
23
+ "description": {"type": "string"},
24
+ "setting": {"type": "string"},
25
+ "people": {"type": "array", "items": {
26
+ "type": "object",
27
+ "properties": {"label": {"type": "string"}, "appearance": {"type": "string"}, "action": {"type": "string"},
28
+ "speaking": {"type": "boolean"}},
29
+ "required": ["label", "appearance", "action", "speaking"], "additionalProperties": False}},
30
+ "onscreen_text": {"type": "array", "items": {"type": "string"}},
31
+ "mood": {"type": "string"},
32
+ },
33
+ "required": ["description", "setting", "people", "onscreen_text", "mood"],
34
+ "additionalProperties": False,
35
+ }
36
+
37
+ INSTRUCTIONS = """You are writing one entry of a time-coded description track for a short film. \
38
+ An AI participant will read these entries while watching alongside people, so each entry must \
39
+ say what someone watching would see and hear during THIS shot only.
40
+
41
+ Rules:
42
+ - Describe only what is visible in the keyframes or audible in the transcript lines for this shot. \
43
+ Do not guess what happens later or why, and do not describe earlier shots again.
44
+ - people: everyone visible. Reuse a label from the roster when it is clearly the same person; \
45
+ otherwise use the next unused letter ("Person A", "Person B", ...). Never use a real name unless \
46
+ the name is spoken in the transcript or written on screen.
47
+ - onscreen_text: legible text in the frames, verbatim; [] if none.
48
+ - description: 1-3 plain present-tense sentences. setting and mood: a short phrase each.
49
+ Return only the JSON object."""
50
+
51
+ FACES_INSTRUCTIONS = """Face tracking has already identified people. Labelled boxes are drawn on the \
52
+ keyframes. Use exactly these labels for the boxed people and the roster below; do not invent new \
53
+ labels for them. Someone visible but not boxed (back turned, too small) may be described without a \
54
+ label, or as "Unlabelled person" in people. The boxes and labels were added for you; they are not \
55
+ part of the film, so never describe or mention them."""
56
+
57
+ BOX_COLOURS = ["#e6194b", "#3cb44b", "#4363d8", "#f58231", "#911eb4", "#42d4f4", "#f032e6", "#bfef45"]
58
+ MAX_WORD_SECONDS = 2.0 # a longer "word" is an alignment failure that absorbed preceding silence or music
59
+
60
+
61
+ def label(person_id: str) -> str:
62
+ return person_id.split(":", 1)[1]
63
+
64
+
65
+ def shot_speech(shot, words) -> str:
66
+ lines = []
67
+ for w in words:
68
+ onset = w["end"] - 0.4 if w["end"] - w["start"] > MAX_WORD_SECONDS else w["start"]
69
+ if not shot["start"] <= onset < shot["end"]:
70
+ continue
71
+ speaker = label(w["voice"]) if w.get("voice") else "SPEAKER"
72
+ if lines and lines[-1]["speaker"] == speaker:
73
+ lines[-1]["words"].append(w["w"])
74
+ else:
75
+ lines.append({"t": onset, "speaker": speaker, "words": [w["w"]]})
76
+ return "\n".join(f"[{l['t']:.1f}s {l['speaker']}] {' '.join(l['words'])}" for l in lines) or "(no speech)"
77
+
78
+
79
+ def labelled_keyframes(shot, keyframes_dir, faces, width) -> list[bytes]:
80
+ """Each keyframe with every tracked, assigned face within 0.2 s of it boxed and labelled."""
81
+ from PIL import Image, ImageDraw
82
+
83
+ people = [p["id"] for p in faces["people"]]
84
+ colour = {p: BOX_COLOURS[i % len(BOX_COLOURS)] for i, p in enumerate(people)}
85
+ out = []
86
+ root = keyframes_dir.resolve()
87
+ for kf in shot["keyframes"]:
88
+ path = (keyframes_dir / kf["file"].removeprefix("assets/")).resolve() # follows links: one out is caught too
89
+ if not path.is_relative_to(root):
90
+ raise NodeError("input", f"keyframe {kf['file']} is outside the keyframes input; only those images are sent")
91
+ image = Image.open(path).convert("RGB")
92
+ scale = image.width / width
93
+ draw = ImageDraw.Draw(image)
94
+ for track in faces["tracks"]:
95
+ if track["person"] is None: # unattributed faces are not labelled for the model
96
+ continue
97
+ near = [s for s in track["samples"] if abs(s["t"] - kf["t"]) <= 0.2]
98
+ if not near:
99
+ continue
100
+ x, y, w, h = (v * scale for v in min(near, key=lambda s: abs(s["t"] - kf["t"]))["box"])
101
+ name, c = label(track["person"]), colour[track["person"]]
102
+ draw.rectangle([x, y, x + w, y + h], outline=c, width=3)
103
+ top = max(0, y - 18)
104
+ draw.rectangle([x, top, x + 8 * len(name) + 6, top + 18], fill=c)
105
+ draw.text((x + 3, top + 2), name, fill="white")
106
+ buf = io.BytesIO()
107
+ image.save(buf, format="JPEG", quality=90)
108
+ out.append(buf.getvalue())
109
+ return out
110
+
111
+
112
+ def caption(client, model, effort, prompt, images, spend):
113
+ import anthropic
114
+
115
+ content = [{"type": "image", "source": {"type": "base64", "media_type": "image/jpeg", "data": base64.standard_b64encode(b).decode()}}
116
+ for b in images] + [{"type": "text", "text": prompt}]
117
+ try:
118
+ response = client.messages.create(model=model, max_tokens=4000, messages=[{"role": "user", "content": content}],
119
+ output_config={"effort": effort, "format": {"type": "json_schema", "schema": SCHEMA}})
120
+ except (anthropic.RateLimitError, anthropic.InternalServerError, anthropic.APIConnectionError) as e:
121
+ raise NodeError("transient", f"{type(e).__name__}: {e}") from None # the SDK already retried twice
122
+ except anthropic.AuthenticationError:
123
+ raise NodeError("input", "the Anthropic API rejected the key (ANTHROPIC_API_KEY)") from None
124
+ spend(response.usage.input_tokens, response.usage.output_tokens) # billed even if the reply is then rejected
125
+ if response.stop_reason == "refusal":
126
+ raise NodeError("input", f"refusal: {response.stop_details}")
127
+ text = next((b.text for b in response.content if b.type == "text"), None)
128
+ if text is None:
129
+ raise NodeError("internal", f"no text block in the response (stop_reason {response.stop_reason})")
130
+ return json.loads(text)
131
+
132
+
133
+ def spent(model, tokens_in, tokens_out):
134
+ price = PRICE_PER_MTOK[model]
135
+ return round((tokens_in * price[0] + tokens_out * price[1]) / 1e6, 4)
136
+
137
+
138
+ with Node() as node:
139
+ import anthropic
140
+
141
+ c = node.config
142
+ model, effort = c["model"], c["effort"]
143
+ client = anthropic.Anthropic(api_key=node.secret("ANTHROPIC_API_KEY"))
144
+ shots_doc = node.input_json("shots")
145
+ words = node.input_json("transcript")["words"]
146
+ faces = node.input_json("faces")
147
+ width = node.input_json("media")["width"]
148
+ keyframes_dir = node.input("keyframes")
149
+ first_seen = {p["id"]: p["first_seen"] for p in faces["people"]}
150
+
151
+ roster, previous, t0 = {}, None, time.time()
152
+ shots, texts, totals = [], [], {"in": 0, "out": 0, "calls": 0}
153
+
154
+ def spend(n_in, n_out):
155
+ """The running total after every billed call: if a later shot fails, the response still reports it."""
156
+ totals.update({"in": totals["in"] + n_in, "out": totals["out"] + n_out, "calls": totals["calls"] + 1})
157
+ node.usage(usd=spent(model, totals["in"], totals["out"]), tokens_in=totals["in"], tokens_out=totals["out"], calls=totals["calls"])
158
+
159
+ for shot in shots_doc["shots"]:
160
+ seen = [label(p) for p, t in first_seen.items() if t is not None and t < shot["end"]] # never people who appear later
161
+ roster_text = "\n".join(f"- {k}: {v}" for k, v in roster.items()) or "(nobody yet)"
162
+ prompt = (f"{INSTRUCTIONS}\n\nShot {shot['index']} runs {shot['start']:.1f}s to {shot['end']:.1f}s. "
163
+ f"Keyframes are at {', '.join(str(k['t']) + 's' for k in shot['keyframes'])}.\n\n"
164
+ f"Speech during this shot:\n{shot_speech(shot, words)}\n\nPeople seen in earlier shots:\n{roster_text}\n\n"
165
+ f"Previous shot: {previous or '(this is the first shot)'}\n\n{FACES_INSTRUCTIONS}\n\nFace roster so far:\n"
166
+ + ("\n".join(f"- {s}" for s in seen) or "(nobody yet)"))
167
+ cap = caption(client, model, effort, prompt, labelled_keyframes(shot, keyframes_dir, faces, width), spend)
168
+ for person in cap["people"]:
169
+ roster.setdefault(person["label"], person["appearance"])
170
+ previous = cap["description"]
171
+ shots.append({**shot, "caption": {"description": cap["description"], "setting": cap["setting"], "mood": cap["mood"],
172
+ "people": cap["people"], "model": model}})
173
+ for string in cap["onscreen_text"]: # the same string in consecutive shots is one item
174
+ same = next((t for t in reversed(texts) if t["text"] == string and t["end"] == shot["start"]), None)
175
+ if same:
176
+ same["end"] = shot["end"]
177
+ else:
178
+ texts.append({"id": f"text:{len(texts)}", "text": string, "start": shot["start"], "end": shot["end"], "box": None})
179
+
180
+ node.model(model, licence_mode="api-terms", licence="Anthropic API terms")
181
+ node.model("PySceneDetect AdaptiveDetector", licence_mode="commercial-ok", licence="BSD-3-Clause") # the shots it refines
182
+ timings = {"captions_s": round(time.time() - t0, 1)}
183
+ node.write_json("shots", "shots.json", {**shots_doc, "producer": node.producer(timings=timings, device="Anthropic API"), "shots": shots})
184
+ node.write_json("text", "text.json", {"layer": "text", "schema_version": "0.1.0",
185
+ "producer": node.producer(timings=timings, device="Anthropic API"), "items": texts})
@@ -0,0 +1,54 @@
1
+ # Track bundle format, v0.1 (draft)
2
+
3
+ The contract between the pipeline (this repo), the annotator app and HyperStudy. A bundle is a directory:
4
+
5
+ ```
6
+ manifest.json what the bundle is: video identity, pipeline, layers with checksums and licence modes
7
+ layers/<name>.json one file per layer (shots, transcript, speakers, faces, bodies, hands, gestures, facing,
8
+ gaze, objects, sounds, text, prosody); every layer is optional
9
+ layers/faces_detail.json heavy per-sample arrays live in side files the layer points to
10
+ assets/ optional small images (keyframes, person and object crops) referenced by relative path
11
+ ```
12
+
13
+ `bundle-0.1.schema.json` validates the manifest (the root schema) and each layer file
14
+ (`#/$defs/layer/<name>`). `pipeline/bundle/validate.py` checks a bundle directory against both, plus
15
+ cross-references the schema cannot express (every id a layer points at exists).
16
+
17
+ ## Conventions
18
+
19
+ - **Video identity.** A bundle never contains the video. `manifest.video.sha256` identifies it; readers ask
20
+ for the file and check the hash. Stimuli are often copyrighted, and films are large.
21
+ - **Time** is seconds from the start of the video (float, millisecond precision). Spans are `start`/`end`,
22
+ samples are `t`.
23
+ - **Space** is source-video pixels. Boxes are `[x, y, w, h]`. Keypoints are `[x, y, score]`, where each
24
+ layer states its score scale and the threshold its producer used (`score_threshold`).
25
+ - **IDs** are strings `"<kind>:<local>"`, unique within a bundle: `shot:3`, `utt:17`, `voice:SPEAKER_01`,
26
+ `face:126` (a face track), `person:Person A` (a machine identity cluster), `body:41`, `hand:125`, `obj:47`
27
+ (an object track), `objid:white box 1` (an object identity), `sound:9`, `text:2`, `gesture:4`,
28
+ `facing:7`, `gaze:31`. References between layers use these ids. Ids are stable for a given pipeline run,
29
+ and they change when a stage re-runs; corrections re-attach by time and geometry, not by id alone.
30
+ - **Persons are machine clusters.** The pipeline over-splits on purpose (a wrong merge misleads agents, a split
31
+ is fixed in review); names and merges are corrections, never written into a bundle.
32
+ - **Producer.** Every layer file carries `producer`: stage name and version, the models used (id, source,
33
+ revision, licence, licence mode), the configuration that affects outputs, run time, device and timings.
34
+ - **Licence mode** per model and per layer: `academic-only`, `commercial-ok`, `needs-review`, or `api-terms`
35
+ (output of a commercial API, governed by its terms). A layer's mode is the most restrictive of its models'.
36
+ A commercial deployment drops layers that are not `commercial-ok` or `api-terms`.
37
+ - **Extensibility.** Core fields are required and typed; objects allow extra, producer-specific fields
38
+ (`additionalProperties` is open), so stages can add detail without breaking readers. Readers ignore fields they
39
+ do not know.
40
+ - **Versioning.** `schema_version` is semver. Minor versions only add optional fields or layers; a major version
41
+ may change required fields. Readers accept bundles whose major version they know.
42
+
43
+ ## Corrections and added data (not part of a bundle)
44
+
45
+ The annotator stores corrections as an append-only op log over a bundle (`perception-corrections`), plus the
46
+ user's own annotation types, notes and imported layers. They reference bundle ids and the video sha256, and are
47
+ exported as their own files. The prototype's op log (v1) uses bare track numbers and person labels; mapping it to
48
+ v0.1 ids is mechanical (`track 126` -> `face:126`, `Person O` -> `person:Person O`).
49
+
50
+ ## Open for v0.2
51
+
52
+ - Arrow/Parquet side files for heavy per-frame arrays (a 2-hour film's face detail would be ~1 GB of JSON).
53
+ - A `social` layer once the social-signal rules are settled (today: gaze events and facing).
54
+ - Per-utterance speaker evidence from more than one detector (today: LR-ASD candidates + TalkNet agreement).
@@ -0,0 +1,371 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://hyperstudy.io/schemas/perception-bundle/0.1/bundle.schema.json",
4
+ "title": "HyperStudy perception track bundle, v0.1",
5
+ "description": "Root: manifest.json. Layer files validate against #/$defs/layer/<name>. Conventions in schema/README.md.",
6
+ "type": "object",
7
+ "required": ["format", "schema_version", "created_at", "video", "pipeline", "layers"],
8
+ "additionalProperties": false,
9
+ "properties": {
10
+ "format": { "const": "hyperstudy-perception-bundle" },
11
+ "schema_version": { "$ref": "#/$defs/semver" },
12
+ "created_at": { "type": "string", "format": "date-time" },
13
+ "video": {
14
+ "type": "object",
15
+ "required": ["sha256", "duration", "fps", "width", "height"],
16
+ "properties": {
17
+ "sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
18
+ "duration": { "type": "number", "exclusiveMinimum": 0 },
19
+ "fps": { "type": "number", "exclusiveMinimum": 0 },
20
+ "width": { "type": "integer", "minimum": 1 },
21
+ "height": { "type": "integer", "minimum": 1 },
22
+ "filename": { "type": "string", "description": "Name of the source file when the bundle was made; a hint only." }
23
+ }
24
+ },
25
+ "pipeline": {
26
+ "type": "object",
27
+ "required": ["name", "version"],
28
+ "properties": {
29
+ "name": { "type": "string" },
30
+ "version": { "type": "string" },
31
+ "git_commit": { "type": ["string", "null"] },
32
+ "source": { "type": "string", "description": "How the bundle was made, e.g. 'pipeline run' or 'converted from bake-off run'." }
33
+ }
34
+ },
35
+ "layers": {
36
+ "type": "object",
37
+ "propertyNames": { "$ref": "#/$defs/layerName" },
38
+ "additionalProperties": {
39
+ "type": "object",
40
+ "required": ["file", "sha256", "bytes", "licence_mode", "stage"],
41
+ "properties": {
42
+ "file": { "type": "string", "pattern": "^layers/[a-z_]+\\.json$" },
43
+ "sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
44
+ "bytes": { "type": "integer", "minimum": 0 },
45
+ "licence_mode": { "$ref": "#/$defs/licenceMode" },
46
+ "stage": { "type": "string" },
47
+ "side_files": {
48
+ "type": "array",
49
+ "items": {
50
+ "type": "object", "required": ["file", "sha256", "bytes"],
51
+ "properties": { "file": { "type": "string" }, "sha256": { "type": "string" }, "bytes": { "type": "integer" } }
52
+ }
53
+ }
54
+ }
55
+ }
56
+ },
57
+ "assets": {
58
+ "type": "array",
59
+ "description": "Small images under assets/ (person, face-track and object crops), referenced by relative path from layers.",
60
+ "items": {
61
+ "type": "object", "required": ["file", "sha256", "bytes"],
62
+ "properties": {
63
+ "file": { "type": "string", "pattern": "^assets/[A-Za-z0-9_./-]+$" },
64
+ "sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
65
+ "bytes": { "type": "integer", "minimum": 0 }
66
+ }
67
+ }
68
+ }
69
+ },
70
+
71
+ "$defs": {
72
+ "semver": { "type": "string", "pattern": "^\\d+\\.\\d+\\.\\d+$" },
73
+ "layerName": { "enum": ["shots", "transcript", "speakers", "faces", "bodies", "hands", "gestures", "facing", "gaze", "objects", "sounds", "text", "prosody", "waveform"] },
74
+ "licenceMode": { "enum": ["academic-only", "commercial-ok", "needs-review", "api-terms"] },
75
+ "time": { "type": "number", "minimum": 0 },
76
+ "box": { "type": "array", "items": { "type": "number" }, "minItems": 4, "maxItems": 4, "description": "[x, y, w, h] in source pixels" },
77
+ "point": { "type": "array", "items": { "type": "number" }, "minItems": 2, "maxItems": 2 },
78
+ "keypoints": { "type": "array", "items": { "type": "array", "items": { "type": ["number", "null"] }, "minItems": 3, "maxItems": 3 }, "description": "[[x, y, score], ...]" },
79
+ "span": {
80
+ "type": "object", "required": ["start", "end"],
81
+ "properties": { "start": { "$ref": "#/$defs/time" }, "end": { "$ref": "#/$defs/time" } }
82
+ },
83
+ "id": {
84
+ "shot": { "type": "string", "pattern": "^shot:\\d+$" },
85
+ "utt": { "type": "string", "pattern": "^utt:\\d+$" },
86
+ "voice": { "type": "string", "pattern": "^voice:.+$" },
87
+ "face": { "type": "string", "pattern": "^face:\\d+$" },
88
+ "person": { "type": "string", "pattern": "^person:.+$" },
89
+ "body": { "type": "string", "pattern": "^body:\\d+$" },
90
+ "hand": { "type": "string", "pattern": "^hand:\\d+$" },
91
+ "obj": { "type": "string", "pattern": "^obj:\\d+$" },
92
+ "objid": { "type": "string", "pattern": "^objid:.+$" },
93
+ "event": { "type": "string", "pattern": "^(sound|text|gesture|facing|gaze):\\d+$" }
94
+ },
95
+ "model": {
96
+ "type": "object", "required": ["id", "licence_mode"],
97
+ "properties": {
98
+ "id": { "type": "string", "description": "Model name, e.g. 'facebook/sam3' or 'LR-ASD TalkSet finetune'." },
99
+ "source": { "type": ["string", "null"], "description": "hf://repo@revision[/file] or a URL." },
100
+ "revision": { "type": ["string", "null"] },
101
+ "licence": { "type": ["string", "null"] },
102
+ "licence_mode": { "$ref": "#/$defs/licenceMode" }
103
+ }
104
+ },
105
+ "producer": {
106
+ "type": "object", "required": ["stage", "stage_version", "models"],
107
+ "properties": {
108
+ "stage": { "type": "string" },
109
+ "stage_version": { "type": "string" },
110
+ "models": { "type": "array", "items": { "$ref": "#/$defs/model" } },
111
+ "config": { "type": "object" },
112
+ "device": { "type": ["string", "null"] },
113
+ "run_at": { "type": ["string", "null"] },
114
+ "timings": { "type": "object" }
115
+ }
116
+ },
117
+ "layerBase": {
118
+ "type": "object", "required": ["layer", "schema_version", "producer"],
119
+ "properties": {
120
+ "layer": { "$ref": "#/$defs/layerName" },
121
+ "schema_version": { "$ref": "#/$defs/semver" },
122
+ "producer": { "$ref": "#/$defs/producer" }
123
+ }
124
+ },
125
+
126
+ "layer": {
127
+ "shots": {
128
+ "allOf": [{ "$ref": "#/$defs/layerBase" }, { "properties": { "layer": { "const": "shots" } } }],
129
+ "required": ["shots"],
130
+ "properties": {
131
+ "shots": { "type": "array", "items": {
132
+ "type": "object", "required": ["id", "index", "start", "end"],
133
+ "properties": {
134
+ "id": { "$ref": "#/$defs/id/shot" }, "index": { "type": "integer", "minimum": 0 },
135
+ "start": { "$ref": "#/$defs/time" }, "end": { "$ref": "#/$defs/time" },
136
+ "keyframes": { "type": "array", "items": { "type": "object", "required": ["t"], "properties": { "t": { "$ref": "#/$defs/time" }, "file": { "type": ["string", "null"] } } } },
137
+ "caption": { "type": ["object", "null"], "properties": {
138
+ "description": { "type": ["string", "null"] }, "setting": { "type": ["string", "null"] }, "mood": { "type": ["string", "null"] },
139
+ "people": { "type": "array" }, "model": { "type": "string" } } }
140
+ } } }
141
+ }
142
+ },
143
+
144
+ "transcript": {
145
+ "allOf": [{ "$ref": "#/$defs/layerBase" }, { "properties": { "layer": { "const": "transcript" } } }],
146
+ "required": ["words", "utterances", "voices"],
147
+ "properties": {
148
+ "voices": { "type": "array", "items": { "type": "object", "required": ["id"], "properties": { "id": { "$ref": "#/$defs/id/voice" } } } },
149
+ "words": { "type": "array", "items": {
150
+ "type": "object", "required": ["w", "start", "end"],
151
+ "properties": { "w": { "type": "string" }, "start": { "$ref": "#/$defs/time" }, "end": { "$ref": "#/$defs/time" },
152
+ "voice": { "oneOf": [{ "$ref": "#/$defs/id/voice" }, { "type": "null" }] } } } },
153
+ "utterances": { "type": "array", "items": {
154
+ "type": "object", "required": ["id", "start", "end", "text", "shot", "words"],
155
+ "properties": { "id": { "$ref": "#/$defs/id/utt" }, "start": { "$ref": "#/$defs/time" }, "end": { "$ref": "#/$defs/time" },
156
+ "text": { "type": "string" }, "shot": { "$ref": "#/$defs/id/shot" },
157
+ "words": { "type": "array", "items": { "type": "integer", "minimum": 0 }, "minItems": 2, "maxItems": 2, "description": "[first, last] word index, inclusive" },
158
+ "voice": { "oneOf": [{ "$ref": "#/$defs/id/voice" }, { "type": "null" }] } } } }
159
+ }
160
+ },
161
+
162
+ "speakers": {
163
+ "allOf": [{ "$ref": "#/$defs/layerBase" }, { "properties": { "layer": { "const": "speakers" } } }],
164
+ "required": ["utterances"],
165
+ "properties": {
166
+ "utterances": { "type": "array", "items": {
167
+ "type": "object", "required": ["utt", "predicted", "candidates"],
168
+ "properties": {
169
+ "utt": { "$ref": "#/$defs/id/utt" },
170
+ "predicted": { "type": "object", "required": ["kind"], "properties": {
171
+ "kind": { "enum": ["face", "person", "offscreen"], "description": "face: a visible face track; person: a known identity not visible (trusted voice link); offscreen: an unidentified voice" },
172
+ "face": { "$ref": "#/$defs/id/face" }, "person": { "$ref": "#/$defs/id/person" },
173
+ "voice": { "oneOf": [{ "$ref": "#/$defs/id/voice" }, { "type": "null" }] },
174
+ "via": { "enum": ["asd", "voice_link", "none"] } } },
175
+ "candidates": { "type": "array", "items": { "type": "object", "required": ["face", "score"],
176
+ "properties": { "face": { "$ref": "#/$defs/id/face" }, "score": { "type": "number" } } } },
177
+ "margin": { "type": ["number", "null"] },
178
+ "second_opinion_agrees": { "type": ["boolean", "null"] }
179
+ } } },
180
+ "voice_links": { "type": "array", "items": { "type": "object", "required": ["voice", "person", "word_share"],
181
+ "properties": { "voice": { "$ref": "#/$defs/id/voice" }, "person": { "$ref": "#/$defs/id/person" },
182
+ "word_share": { "type": "number" }, "trusted": { "type": "boolean" } } } }
183
+ }
184
+ },
185
+
186
+ "faces": {
187
+ "allOf": [{ "$ref": "#/$defs/layerBase" }, { "properties": { "layer": { "const": "faces" } } }],
188
+ "required": ["sample_fps", "people", "tracks"],
189
+ "properties": {
190
+ "sample_fps": { "type": "number" },
191
+ "angles": { "type": "string", "description": "Units and sign convention of head pose and gaze." },
192
+ "people": { "type": "array", "items": { "type": "object", "required": ["id"],
193
+ "properties": { "id": { "$ref": "#/$defs/id/person" }, "first_seen": { "type": ["number", "null"] },
194
+ "seconds_on_screen": { "type": ["number", "null"] }, "crop": { "type": ["string", "null"] } } } },
195
+ "tracks": { "type": "array", "items": {
196
+ "type": "object", "required": ["id", "person", "shot", "start", "end", "samples"],
197
+ "properties": {
198
+ "id": { "$ref": "#/$defs/id/face" }, "shot": { "$ref": "#/$defs/id/shot" },
199
+ "person": { "oneOf": [{ "$ref": "#/$defs/id/person" }, { "type": "null" }], "description": "null: not assigned to any identity cluster (the clusterer's Unknown)" },
200
+ "start": { "$ref": "#/$defs/time" }, "end": { "$ref": "#/$defs/time" },
201
+ "linked_body": { "type": ["boolean", "null"], "description": "Ever linked to a posed body; false is a strong false-face hint." },
202
+ "crop": { "type": ["string", "null"], "description": "assets/ path of a padded crop from the track's largest detection." },
203
+ "samples": { "type": "array", "items": { "type": "object", "required": ["t", "box"],
204
+ "properties": { "t": { "$ref": "#/$defs/time" }, "box": { "$ref": "#/$defs/box" },
205
+ "mouth": { "type": ["number", "null"] }, "gaze": { "oneOf": [{ "$ref": "#/$defs/point" }, { "type": "null" }] } } } }
206
+ } } },
207
+ "person_pairs": { "type": "array", "items": { "type": "object", "required": ["a", "b", "distance"],
208
+ "properties": { "a": { "$ref": "#/$defs/id/person" }, "b": { "$ref": "#/$defs/id/person" },
209
+ "distance": { "type": "number" }, "co_visible": { "type": "boolean" } } } },
210
+ "detail": { "type": ["object", "null"], "required": ["file"], "properties": {
211
+ "file": { "type": "string" }, "fields": { "type": "array", "items": { "type": "string" } } } }
212
+ }
213
+ },
214
+
215
+ "bodies": {
216
+ "allOf": [{ "$ref": "#/$defs/layerBase" }, { "properties": { "layer": { "const": "bodies" } } }],
217
+ "required": ["keypoint_names", "score_threshold", "fps", "tracks"],
218
+ "properties": {
219
+ "keypoint_names": { "type": "array", "items": { "type": "string" } },
220
+ "skeleton": { "type": "array", "items": { "type": "array", "items": { "type": "integer" }, "minItems": 2, "maxItems": 2 } },
221
+ "score_threshold": { "type": "number" }, "fps": { "type": "number" },
222
+ "tracks": { "type": "array", "items": { "type": "object", "required": ["id", "shot", "start", "end", "samples"],
223
+ "properties": { "id": { "$ref": "#/$defs/id/body" }, "shot": { "$ref": "#/$defs/id/shot" },
224
+ "face": { "oneOf": [{ "$ref": "#/$defs/id/face" }, { "type": "null" }] },
225
+ "start": { "$ref": "#/$defs/time" }, "end": { "$ref": "#/$defs/time" },
226
+ "samples": { "type": "array", "items": { "type": "object", "required": ["t", "box", "kpts"],
227
+ "properties": { "t": { "$ref": "#/$defs/time" }, "box": { "$ref": "#/$defs/box" }, "kpts": { "$ref": "#/$defs/keypoints" } } } } } } }
228
+ }
229
+ },
230
+
231
+ "hands": {
232
+ "allOf": [{ "$ref": "#/$defs/layerBase" }, { "properties": { "layer": { "const": "hands" } } }],
233
+ "required": ["keypoint_names", "fps", "hands"],
234
+ "properties": {
235
+ "keypoint_names": { "type": "array", "items": { "type": "string" } },
236
+ "skeleton": { "type": "array" }, "score_threshold": { "type": ["number", "null"] }, "fps": { "type": "number" },
237
+ "hands": { "type": "array", "items": { "type": "object", "required": ["id", "shot", "side", "start", "end", "samples"],
238
+ "properties": { "id": { "$ref": "#/$defs/id/hand" }, "shot": { "$ref": "#/$defs/id/shot" },
239
+ "side": { "enum": ["left", "right", null] },
240
+ "body": { "oneOf": [{ "$ref": "#/$defs/id/body" }, { "type": "null" }] },
241
+ "face": { "oneOf": [{ "$ref": "#/$defs/id/face" }, { "type": "null" }] },
242
+ "box_source": { "type": "string" },
243
+ "start": { "$ref": "#/$defs/time" }, "end": { "$ref": "#/$defs/time" },
244
+ "samples": { "type": "array", "items": { "type": "object", "required": ["t", "box", "kpts"],
245
+ "properties": { "t": { "$ref": "#/$defs/time" }, "box": { "$ref": "#/$defs/box" }, "kpts": { "$ref": "#/$defs/keypoints" } } } } } } }
246
+ }
247
+ },
248
+
249
+ "gestures": {
250
+ "allOf": [{ "$ref": "#/$defs/layerBase" }, { "properties": { "layer": { "const": "gestures" } } }],
251
+ "required": ["events"],
252
+ "properties": { "events": { "type": "array", "items": { "type": "object", "required": ["id", "kind", "start", "end"],
253
+ "properties": { "id": { "$ref": "#/$defs/id/event" }, "kind": { "type": "string" },
254
+ "start": { "$ref": "#/$defs/time" }, "end": { "$ref": "#/$defs/time" },
255
+ "face": { "oneOf": [{ "$ref": "#/$defs/id/face" }, { "type": "null" }] },
256
+ "hand": { "oneOf": [{ "$ref": "#/$defs/id/hand" }, { "type": "null" }] },
257
+ "score": { "type": ["number", "null"] }, "detail": { "type": "object" } } } } }
258
+ },
259
+
260
+ "facing": {
261
+ "allOf": [{ "$ref": "#/$defs/layerBase" }, { "properties": { "layer": { "const": "facing" } } }],
262
+ "required": ["events"],
263
+ "properties": { "events": { "type": "array", "items": { "type": "object", "required": ["id", "from", "to", "start", "end"],
264
+ "properties": { "id": { "$ref": "#/$defs/id/event" }, "from": { "$ref": "#/$defs/id/face" }, "to": { "$ref": "#/$defs/id/face" },
265
+ "start": { "$ref": "#/$defs/time" }, "end": { "$ref": "#/$defs/time" },
266
+ "mutual": { "type": "boolean" }, "score": { "type": ["number", "null"] }, "detail": { "type": "object" } } } } }
267
+ },
268
+
269
+ "gaze": {
270
+ "allOf": [{ "$ref": "#/$defs/layerBase" }, { "properties": { "layer": { "const": "gaze" } } }],
271
+ "required": ["fps", "tracks", "events"],
272
+ "properties": {
273
+ "fps": { "type": "number" },
274
+ "tracks": { "type": "array", "items": { "type": "object", "required": ["face", "samples"],
275
+ "properties": { "face": { "$ref": "#/$defs/id/face" },
276
+ "samples": { "type": "array", "items": { "type": "object", "required": ["t"],
277
+ "properties": { "t": { "$ref": "#/$defs/time" },
278
+ "peak": { "oneOf": [{ "$ref": "#/$defs/point" }, { "type": "null" }] },
279
+ "in_frame": { "type": ["number", "null"] },
280
+ "target": { "oneOf": [{ "$ref": "#/$defs/id/face" }, { "$ref": "#/$defs/id/obj" }, { "enum": ["out", "camera", null] }] },
281
+ "target_score": { "type": ["number", "null"] } } } } } } },
282
+ "events": { "type": "array", "items": { "type": "object", "required": ["id", "kind", "from", "start", "end"],
283
+ "properties": { "id": { "$ref": "#/$defs/id/event" }, "kind": { "enum": ["looks_at", "mutual_gaze", "joint_attention"] },
284
+ "from": { "$ref": "#/$defs/id/face" },
285
+ "to": { "oneOf": [{ "$ref": "#/$defs/id/face" }, { "type": "null" }] },
286
+ "object": { "oneOf": [{ "$ref": "#/$defs/id/obj" }, { "type": "null" }] },
287
+ "also": { "oneOf": [{ "$ref": "#/$defs/id/face" }, { "type": "null" }] },
288
+ "start": { "$ref": "#/$defs/time" }, "end": { "$ref": "#/$defs/time" }, "score": { "type": ["number", "null"] } } } }
289
+ }
290
+ },
291
+
292
+ "objects": {
293
+ "allOf": [{ "$ref": "#/$defs/layerBase" }, { "properties": { "layer": { "const": "objects" } } }],
294
+ "required": ["fps", "tracks", "identities"],
295
+ "properties": {
296
+ "fps": { "type": "number" },
297
+ "tracks": { "type": "array", "items": { "type": "object", "required": ["id", "label", "identity", "shot", "start", "end", "samples"],
298
+ "properties": { "id": { "$ref": "#/$defs/id/obj" }, "label": { "type": "string" }, "identity": { "$ref": "#/$defs/id/objid" },
299
+ "shot": { "$ref": "#/$defs/id/shot" }, "start": { "$ref": "#/$defs/time" }, "end": { "$ref": "#/$defs/time" },
300
+ "wearer": { "oneOf": [{ "$ref": "#/$defs/id/person" }, { "type": "null" }] },
301
+ "verdict": { "enum": ["object", "graphic", "absent", null] },
302
+ "samples": { "type": "array", "items": { "type": "object", "required": ["t", "box"],
303
+ "properties": { "t": { "$ref": "#/$defs/time" }, "box": { "$ref": "#/$defs/box" },
304
+ "poly": { "type": "array", "items": { "$ref": "#/$defs/point" } }, "score": { "type": ["number", "null"] } } } } } } },
305
+ "identities": { "type": "array", "items": { "type": "object", "required": ["id", "label", "tracks"],
306
+ "properties": { "id": { "$ref": "#/$defs/id/objid" }, "label": { "type": "string" }, "family": { "type": ["string", "null"] },
307
+ "worn_by": { "type": "array", "items": { "$ref": "#/$defs/id/person" } },
308
+ "tracks": { "type": "array", "items": { "$ref": "#/$defs/id/obj" } }, "crop": { "type": ["string", "null"] } } } },
309
+ "rejected_tracks": { "type": "array", "items": { "type": "object", "required": ["id", "label", "verdict", "start", "end"],
310
+ "properties": { "id": { "$ref": "#/$defs/id/obj" }, "verdict": { "enum": ["graphic", "absent"] } } } }
311
+ }
312
+ },
313
+
314
+ "sounds": {
315
+ "allOf": [{ "$ref": "#/$defs/layerBase" }, { "properties": { "layer": { "const": "sounds" } } }],
316
+ "required": ["events"],
317
+ "properties": { "events": { "type": "array", "items": { "type": "object", "required": ["id", "label", "start", "end", "sources"],
318
+ "properties": { "id": { "$ref": "#/$defs/id/event" }, "label": { "type": "string" },
319
+ "start": { "$ref": "#/$defs/time" }, "end": { "$ref": "#/$defs/time" },
320
+ "sources": { "type": "array", "items": { "type": "string" } },
321
+ "agreement": { "type": ["boolean", "null"], "description": "For query-model events: did a tagger agree? null for tagger events." },
322
+ "score": { "type": ["number", "null"] } } } } }
323
+ },
324
+
325
+ "text": {
326
+ "allOf": [{ "$ref": "#/$defs/layerBase" }, { "properties": { "layer": { "const": "text" } } }],
327
+ "required": ["items"],
328
+ "properties": { "items": { "type": "array", "items": { "type": "object", "required": ["id", "text", "start", "end"],
329
+ "properties": { "id": { "$ref": "#/$defs/id/event" }, "text": { "type": "string" },
330
+ "start": { "$ref": "#/$defs/time" }, "end": { "$ref": "#/$defs/time" },
331
+ "box": { "oneOf": [{ "$ref": "#/$defs/box" }, { "type": "null" }] } } } } }
332
+ },
333
+
334
+ "prosody": {
335
+ "allOf": [{ "$ref": "#/$defs/layerBase" }, { "properties": { "layer": { "const": "prosody" } } }],
336
+ "required": ["contour", "utterances"],
337
+ "properties": {
338
+ "contour": { "type": "object", "required": ["step", "t0", "speakers", "f0_st", "speaker"],
339
+ "properties": { "step": { "type": "number" }, "t0": { "type": "number" },
340
+ "speakers": { "type": "array", "items": { "type": "string" } },
341
+ "f0_st": { "type": "array", "items": { "type": ["number", "null"] }, "description": "Semitones relative to each speaker's median" },
342
+ "speaker": { "type": "array", "items": { "type": ["integer", "null"] } } } },
343
+ "default_dimensional": { "type": ["string", "null"], "description": "The emotion_models key readers show by default." },
344
+ "emotion_models": { "type": "object", "description": "Per emotion key: the model that wrote it.",
345
+ "additionalProperties": { "type": "object", "required": ["model", "licence_mode", "kind"],
346
+ "properties": { "model": { "type": "string" }, "licence_mode": { "$ref": "#/$defs/licenceMode" },
347
+ "kind": { "enum": ["dimensional", "categorical"] } } } },
348
+ "caption_models": { "type": "object", "description": "Per caption key: the model that wrote it.",
349
+ "additionalProperties": { "type": "object", "required": ["model", "licence_mode"],
350
+ "properties": { "model": { "type": "string" }, "licence_mode": { "$ref": "#/$defs/licenceMode" } } } },
351
+ "utterances": { "type": "array", "items": { "type": "object", "required": ["utt"],
352
+ "properties": { "utt": { "$ref": "#/$defs/id/utt" }, "speaker": { "type": ["string", "null"] },
353
+ "measures": { "type": "object" },
354
+ "words": { "type": "array" },
355
+ "emotion": { "type": "object", "description": "Per emotion_models key: {arousal, valence, dominance} or categorical scores." },
356
+ "captions": { "type": "object", "description": "Per caption_models key: {description, tags, emphasized_words}." } } } }
357
+ }
358
+ },
359
+
360
+ "waveform": {
361
+ "allOf": [{ "$ref": "#/$defs/layerBase" }, { "properties": { "layer": { "const": "waveform" } } }],
362
+ "required": ["hz", "peaks"],
363
+ "properties": {
364
+ "hz": { "type": "number", "exclusiveMinimum": 0, "description": "Bins per second." },
365
+ "peaks": { "type": "array", "items": { "type": "integer", "minimum": 0, "maximum": 100 },
366
+ "description": "Peak absolute amplitude per bin, 0-100 relative to the loudest bin." }
367
+ }
368
+ }
369
+ }
370
+ }
371
+ }