hflow 0.2.2__tar.gz → 0.2.3__tar.gz
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.
- {hflow-0.2.2 → hflow-0.2.3}/PKG-INFO +8 -9
- {hflow-0.2.2 → hflow-0.2.3}/README.md +3 -4
- {hflow-0.2.2 → hflow-0.2.3}/pyproject.toml +19 -13
- {hflow-0.2.2 → hflow-0.2.3}/pyproject.toml.orig +16 -16
- hflow-0.2.3/src/hflow/__main__.py +3 -0
- {hflow-0.2.2 → hflow-0.2.3}/src/hflow/_grouped_mcap_writer/_writer.py +16 -0
- {hflow-0.2.2 → hflow-0.2.3}/src/hflow/_video_measurements/_camera_motion.py +3 -0
- hflow-0.2.3/src/hflow/_video_measurements/_field_guards.py +30 -0
- {hflow-0.2.2 → hflow-0.2.3}/src/hflow/_video_measurements/_frame_statistics.py +16 -1
- {hflow-0.2.2 → hflow-0.2.3}/src/hflow/app.py +214 -44
- {hflow-0.2.2 → hflow-0.2.3}/src/hflow/batching.py +29 -4
- {hflow-0.2.2 → hflow-0.2.3}/src/hflow/behavior.py +11 -1
- {hflow-0.2.2 → hflow-0.2.3}/src/hflow/catalog.py +15 -0
- {hflow-0.2.2 → hflow-0.2.3}/src/hflow/checks.py +689 -288
- {hflow-0.2.2 → hflow-0.2.3}/src/hflow/cli.py +32 -0
- {hflow-0.2.2 → hflow-0.2.3}/src/hflow/curation.py +10 -2
- {hflow-0.2.2 → hflow-0.2.3}/src/hflow/doctor.py +51 -5
- {hflow-0.2.2 → hflow-0.2.3}/src/hflow/ffmpeg/_binary.py +1 -1
- {hflow-0.2.2 → hflow-0.2.3}/src/hflow/ffmpeg/_contact_sheet.py +11 -3
- {hflow-0.2.2 → hflow-0.2.3}/src/hflow/format.py +7 -0
- {hflow-0.2.2 → hflow-0.2.3}/src/hflow/reader.py +3 -1
- {hflow-0.2.2 → hflow-0.2.3}/src/hflow/runtime/_bundle.py +25 -9
- {hflow-0.2.2 → hflow-0.2.3}/src/hflow/runtime/_client.py +2 -0
- {hflow-0.2.2 → hflow-0.2.3}/src/hflow/runtime/_templates.py +89 -10
- {hflow-0.2.2 → hflow-0.2.3}/src/hflow/stage_execution.py +26 -0
- {hflow-0.2.2 → hflow-0.2.3}/src/hflow/stage_planning.py +128 -13
- {hflow-0.2.2 → hflow-0.2.3}/src/hflow/steps.py +17 -1
- {hflow-0.2.2 → hflow-0.2.3}/src/hflow/storage.py +8 -1
- {hflow-0.2.2 → hflow-0.2.3}/src/hflow/testing.py +57 -0
- {hflow-0.2.2 → hflow-0.2.3}/src/hflow/transform.py +142 -29
- {hflow-0.2.2 → hflow-0.2.3}/src/hflow/video.py +120 -10
- {hflow-0.2.2 → hflow-0.2.3}/LICENSE +0 -0
- {hflow-0.2.2 → hflow-0.2.3}/src/hflow/__init__.py +0 -0
- {hflow-0.2.2 → hflow-0.2.3}/src/hflow/_grouped_mcap_writer/__init__.py +0 -0
- {hflow-0.2.2 → hflow-0.2.3}/src/hflow/_pinned_asset.py +0 -0
- {hflow-0.2.2 → hflow-0.2.3}/src/hflow/_video_measurement_toolchain.py +0 -0
- {hflow-0.2.2 → hflow-0.2.3}/src/hflow/_video_measurements/__init__.py +0 -0
- {hflow-0.2.2 → hflow-0.2.3}/src/hflow/_video_measurements/_raw_frames.py +0 -0
- {hflow-0.2.2 → hflow-0.2.3}/src/hflow/_video_measurements/_toolchain.py +0 -0
- {hflow-0.2.2 → hflow-0.2.3}/src/hflow/dataset.py +0 -0
- {hflow-0.2.2 → hflow-0.2.3}/src/hflow/episode.py +0 -0
- {hflow-0.2.2 → hflow-0.2.3}/src/hflow/ffmpeg/__init__.py +0 -0
- {hflow-0.2.2 → hflow-0.2.3}/src/hflow/ingest_ledger.py +0 -0
- {hflow-0.2.2 → hflow-0.2.3}/src/hflow/manifest.py +0 -0
- {hflow-0.2.2 → hflow-0.2.3}/src/hflow/mediapipe_hands.py +0 -0
- {hflow-0.2.2 → hflow-0.2.3}/src/hflow/project.py +0 -0
- {hflow-0.2.2 → hflow-0.2.3}/src/hflow/providers.py +0 -0
- {hflow-0.2.2 → hflow-0.2.3}/src/hflow/py.typed +0 -0
- {hflow-0.2.2 → hflow-0.2.3}/src/hflow/resample.py +0 -0
- {hflow-0.2.2 → hflow-0.2.3}/src/hflow/runtime/__init__.py +0 -0
- {hflow-0.2.2 → hflow-0.2.3}/src/hflow/runtime/_compose.py +0 -0
- {hflow-0.2.2 → hflow-0.2.3}/src/hflow/runtime/_deploy.py +0 -0
- {hflow-0.2.2 → hflow-0.2.3}/src/hflow/runtime/_endpoint.py +0 -0
- {hflow-0.2.2 → hflow-0.2.3}/src/hflow/runtime/_lifecycle.py +0 -0
- {hflow-0.2.2 → hflow-0.2.3}/src/hflow/runtime/_topology.py +0 -0
- {hflow-0.2.2 → hflow-0.2.3}/src/hflow/snapshot.py +0 -0
- {hflow-0.2.2 → hflow-0.2.3}/src/hflow/workspace.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hflow
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: Open source SDK for building Physical AI data pipelines
|
|
5
5
|
Keywords: airflow,data-pipeline,data-quality,dataset-curation,mcap,physical-ai,robotics
|
|
6
6
|
Author: Hebbian Robotics, Kingston Kuan, Brandon Ong
|
|
@@ -14,10 +14,10 @@ Requires-Dist: mcap-ros2-support>=0.5.7
|
|
|
14
14
|
Requires-Dist: numpy>=2.4.6
|
|
15
15
|
Requires-Dist: zstandard>=0.25.0
|
|
16
16
|
Requires-Dist: pyarrow>=25.0.1 ; extra == 'arrow'
|
|
17
|
-
Requires-Dist: obstore>=0.
|
|
18
|
-
Requires-Dist: mediapipe>=1.0.
|
|
19
|
-
Requires-Dist: opencv-python-headless>=
|
|
20
|
-
Requires-Dist: openai>=3.
|
|
17
|
+
Requires-Dist: obstore>=0.11.1 ; extra == 'bucket'
|
|
18
|
+
Requires-Dist: mediapipe>=1.0.1 ; extra == 'mediapipe'
|
|
19
|
+
Requires-Dist: opencv-python-headless>=5.0.0.93 ; extra == 'motion'
|
|
20
|
+
Requires-Dist: openai>=3.3.1 ; extra == 'openai'
|
|
21
21
|
Requires-Python: >=3.11
|
|
22
22
|
Project-URL: Repository, https://github.com/Hebbian-Robotics/hflow
|
|
23
23
|
Project-URL: Documentation, https://github.com/Hebbian-Robotics/hflow/tree/main/docs
|
|
@@ -87,7 +87,7 @@ episode.
|
|
|
87
87
|
|
|
88
88
|
| | HFlow's boundary |
|
|
89
89
|
| --- | --- |
|
|
90
|
-
| **Input** | One multimodal episode per standard MCAP file (`hflow doctor`
|
|
90
|
+
| **Input** | One multimodal episode per supported standard MCAP file (`hflow doctor` reports canonical gaps) |
|
|
91
91
|
| **Processing** | Your Python transforms, checks, labels, and enrichments |
|
|
92
92
|
| **Execution** | In-process for development; generated Airflow 3 DAGs for scheduled runs |
|
|
93
93
|
| **Durable output** | Canonical MCAP episodes, provenance, artifacts, and a Parquet catalog |
|
|
@@ -135,10 +135,9 @@ the trust model, and the current limits.
|
|
|
135
135
|
|
|
136
136
|
## Community and hosted interest
|
|
137
137
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
- **Hosted version interest:** Google Form coming soon.
|
|
138
|
+
- **Hosted platform waitlist:** [tell us about your workflow](https://forms.gle/EZpQpGGF3eJomx498).
|
|
141
139
|
- **Community Discord:** [join us](https://discord.gg/vacepQvjmg) for questions, feedback, and contribution discussion.
|
|
140
|
+
- **Code of conduct:** review our [community standards](./CODE_OF_CONDUCT.md) and report concerns privately.
|
|
142
141
|
|
|
143
142
|
For reproducible bugs and scoped feature requests, use
|
|
144
143
|
[GitHub issues](https://github.com/Hebbian-Robotics/hflow/issues).
|
|
@@ -56,7 +56,7 @@ episode.
|
|
|
56
56
|
|
|
57
57
|
| | HFlow's boundary |
|
|
58
58
|
| --- | --- |
|
|
59
|
-
| **Input** | One multimodal episode per standard MCAP file (`hflow doctor`
|
|
59
|
+
| **Input** | One multimodal episode per supported standard MCAP file (`hflow doctor` reports canonical gaps) |
|
|
60
60
|
| **Processing** | Your Python transforms, checks, labels, and enrichments |
|
|
61
61
|
| **Execution** | In-process for development; generated Airflow 3 DAGs for scheduled runs |
|
|
62
62
|
| **Durable output** | Canonical MCAP episodes, provenance, artifacts, and a Parquet catalog |
|
|
@@ -104,10 +104,9 @@ the trust model, and the current limits.
|
|
|
104
104
|
|
|
105
105
|
## Community and hosted interest
|
|
106
106
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
- **Hosted version interest:** Google Form coming soon.
|
|
107
|
+
- **Hosted platform waitlist:** [tell us about your workflow](https://forms.gle/EZpQpGGF3eJomx498).
|
|
110
108
|
- **Community Discord:** [join us](https://discord.gg/vacepQvjmg) for questions, feedback, and contribution discussion.
|
|
109
|
+
- **Code of conduct:** review our [community standards](./CODE_OF_CONDUCT.md) and report concerns privately.
|
|
111
110
|
|
|
112
111
|
For reproducible bugs and scoped feature requests, use
|
|
113
112
|
[GitHub issues](https://github.com/Hebbian-Robotics/hflow/issues).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "hflow"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.3"
|
|
4
4
|
description = "Open source SDK for building Physical AI data pipelines"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "Apache-2.0"
|
|
@@ -36,10 +36,10 @@ name = "Brandon Ong"
|
|
|
36
36
|
|
|
37
37
|
[project.optional-dependencies]
|
|
38
38
|
arrow = ["pyarrow>=25.0.1"]
|
|
39
|
-
bucket = ["obstore>=0.
|
|
40
|
-
mediapipe = ["mediapipe>=1.0.
|
|
41
|
-
motion = ["opencv-python-headless>=
|
|
42
|
-
openai = ["openai>=3.
|
|
39
|
+
bucket = ["obstore>=0.11.1"]
|
|
40
|
+
mediapipe = ["mediapipe>=1.0.1"]
|
|
41
|
+
motion = ["opencv-python-headless>=5.0.0.93"]
|
|
42
|
+
openai = ["openai>=3.3.1"]
|
|
43
43
|
|
|
44
44
|
[project.urls]
|
|
45
45
|
Repository = "https://github.com/Hebbian-Robotics/hflow"
|
|
@@ -52,25 +52,28 @@ hflow = "hflow.cli:main"
|
|
|
52
52
|
[dependency-groups]
|
|
53
53
|
dev = [
|
|
54
54
|
"hflow-server",
|
|
55
|
-
"httpx2>=2.
|
|
56
|
-
"obstore>=0.
|
|
57
|
-
"opencv-python-headless>=
|
|
55
|
+
"httpx2>=2.12.0",
|
|
56
|
+
"obstore>=0.11.1",
|
|
57
|
+
"opencv-python-headless>=5.0.0.93",
|
|
58
58
|
"pyarrow>=25.0.1",
|
|
59
59
|
"pytest>=9.1.1",
|
|
60
60
|
"pyyaml>=6.0.3",
|
|
61
|
-
"ruff>=0.16.
|
|
62
|
-
"ty>=0.0.
|
|
61
|
+
"ruff>=0.16.4",
|
|
62
|
+
"ty>=0.0.74",
|
|
63
63
|
]
|
|
64
64
|
|
|
65
65
|
[build-system]
|
|
66
|
-
requires = ["uv_build>=0.
|
|
66
|
+
requires = ["uv_build>=0.12.5,<0.13"]
|
|
67
67
|
build-backend = "uv_build"
|
|
68
68
|
|
|
69
69
|
[tool.uv]
|
|
70
70
|
exclude-newer = "5 days"
|
|
71
71
|
|
|
72
72
|
[tool.uv.workspace]
|
|
73
|
-
members = [
|
|
73
|
+
members = [
|
|
74
|
+
"packages/*",
|
|
75
|
+
"examples/build_ai_evaluation",
|
|
76
|
+
]
|
|
74
77
|
|
|
75
78
|
[tool.uv.sources.hflow-server]
|
|
76
79
|
workspace = true
|
|
@@ -120,4 +123,7 @@ unused-ignore-comment = "error"
|
|
|
120
123
|
possibly-unresolved-reference = "error"
|
|
121
124
|
|
|
122
125
|
[tool.ty.src]
|
|
123
|
-
exclude = [
|
|
126
|
+
exclude = [
|
|
127
|
+
"examples/build_ai_evaluation/",
|
|
128
|
+
"examples/openai_vision/",
|
|
129
|
+
]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "hflow"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.3"
|
|
4
4
|
description = "Open source SDK for building Physical AI data pipelines"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "Apache-2.0"
|
|
@@ -35,21 +35,21 @@ arrow = ["pyarrow>=25.0.1"]
|
|
|
35
35
|
# Native object-store data roots (gs://, s3://, az://): obstore is the Rust
|
|
36
36
|
# object_store crate as a single dependency-free wheel; local-path roots
|
|
37
37
|
# never import it.
|
|
38
|
-
bucket = ["obstore>=0.
|
|
38
|
+
bucket = ["obstore>=0.11.1"]
|
|
39
39
|
# Hand detection with MediaPipe's Hand Landmarker (hflow.mediapipe_hands).
|
|
40
40
|
# A MODEL, not a signal statistic, so it is opt-in twice over: outside the core
|
|
41
41
|
# install, and outside the dev group as well (unlike motion and bucket, which
|
|
42
42
|
# are mirrored into dev so the suite exercises them). It adds ~430 MB and its
|
|
43
43
|
# own OpenCV; a dev environment opts in with `--extra mediapipe`, exactly as
|
|
44
44
|
# the openai example does.
|
|
45
|
-
mediapipe = ["mediapipe>=1.0.
|
|
45
|
+
mediapipe = ["mediapipe>=1.0.1"]
|
|
46
46
|
# Camera-shake measurement: pyramidal Lucas-Kanade optical flow and a RANSAC
|
|
47
47
|
# similarity fit. No numpy-only equivalent preserves the instrument, and
|
|
48
48
|
# reimplementing them would make it a different, unvalidated one -- so the
|
|
49
49
|
# dependency is real, and confined to the one check that needs it. Headless
|
|
50
50
|
# because a data pipeline runs where there is no display.
|
|
51
|
-
motion = ["opencv-python-headless>=
|
|
52
|
-
openai = ["openai>=3.
|
|
51
|
+
motion = ["opencv-python-headless>=5.0.0.93"]
|
|
52
|
+
openai = ["openai>=3.3.1"]
|
|
53
53
|
|
|
54
54
|
[project.urls]
|
|
55
55
|
Repository = "https://github.com/Hebbian-Robotics/hflow"
|
|
@@ -64,20 +64,20 @@ dev = [
|
|
|
64
64
|
"hflow-server", # the workspace API package, present in dev so the suite tests it
|
|
65
65
|
# Starlette's TestClient transport for the hflow-server suite. httpx2, not
|
|
66
66
|
# httpx: starlette 1.6 deprecates the httpx transport in its favour.
|
|
67
|
-
"httpx2>=2.
|
|
68
|
-
"obstore>=0.
|
|
67
|
+
"httpx2>=2.12.0",
|
|
68
|
+
"obstore>=0.11.1", # the bucket extra, present in dev so the suite tests it
|
|
69
69
|
# the motion extra, present in dev so the suite tests it rather than
|
|
70
70
|
# skipping the one check that cannot be verified any other way
|
|
71
|
-
"opencv-python-headless>=
|
|
71
|
+
"opencv-python-headless>=5.0.0.93",
|
|
72
72
|
"pyarrow>=25.0.1", # the arrow extra, present in dev so the suite tests it
|
|
73
73
|
"pytest>=9.1.1",
|
|
74
74
|
"pyyaml>=6.0.3",
|
|
75
|
-
"ruff>=0.16.
|
|
76
|
-
"ty>=0.0.
|
|
75
|
+
"ruff>=0.16.4",
|
|
76
|
+
"ty>=0.0.74",
|
|
77
77
|
]
|
|
78
78
|
|
|
79
79
|
[build-system]
|
|
80
|
-
requires = ["uv_build>=0.
|
|
80
|
+
requires = ["uv_build>=0.12.5,<0.13"]
|
|
81
81
|
build-backend = "uv_build"
|
|
82
82
|
|
|
83
83
|
[tool.uv]
|
|
@@ -87,7 +87,7 @@ build-backend = "uv_build"
|
|
|
87
87
|
exclude-newer = "5 days"
|
|
88
88
|
|
|
89
89
|
[tool.uv.workspace]
|
|
90
|
-
members = ["packages/*"]
|
|
90
|
+
members = ["packages/*", "examples/build_ai_evaluation"]
|
|
91
91
|
|
|
92
92
|
[tool.uv.sources]
|
|
93
93
|
hflow-server = { workspace = true }
|
|
@@ -128,7 +128,7 @@ unused-ignore-comment = "error"
|
|
|
128
128
|
possibly-unresolved-reference = "error"
|
|
129
129
|
|
|
130
130
|
[tool.ty.src]
|
|
131
|
-
# examples
|
|
132
|
-
#
|
|
133
|
-
#
|
|
134
|
-
exclude = ["examples/openai_vision/"]
|
|
131
|
+
# Workspace examples typecheck themselves with their own dependencies.
|
|
132
|
+
# openai_vision imports the user's optional client, so the root project does
|
|
133
|
+
# not typecheck it either.
|
|
134
|
+
exclude = ["examples/build_ai_evaluation/", "examples/openai_vision/"]
|
|
@@ -236,6 +236,7 @@ class GroupedMcapWriter:
|
|
|
236
236
|
self._flush()
|
|
237
237
|
|
|
238
238
|
def register_schema(self, name: str, encoding: str, data: bytes) -> SchemaId:
|
|
239
|
+
"""Register a schema and return its id for :meth:`register_channel`."""
|
|
239
240
|
self._raise_if_not_active("register a schema")
|
|
240
241
|
schema_id = SchemaId(len(self._schemas_by_id) + 1)
|
|
241
242
|
schema = Schema(id=schema_id, data=data, encoding=encoding, name=name)
|
|
@@ -255,6 +256,11 @@ class GroupedMcapWriter:
|
|
|
255
256
|
group: str,
|
|
256
257
|
metadata: dict[str, str] | None = None,
|
|
257
258
|
) -> ChannelId:
|
|
259
|
+
"""Register a topic on a named group and return its id for :meth:`write_message`.
|
|
260
|
+
|
|
261
|
+
``schema_id`` must be a prior :meth:`register_schema` return value, or
|
|
262
|
+
``NO_SCHEMA_ID`` for schemaless channels.
|
|
263
|
+
"""
|
|
258
264
|
self._raise_if_not_active("register a channel")
|
|
259
265
|
# schema_id 0 is the MCAP spec's "no schema" sentinel.
|
|
260
266
|
if schema_id != NO_SCHEMA_ID and schema_id not in self._schemas_by_id:
|
|
@@ -288,6 +294,11 @@ class GroupedMcapWriter:
|
|
|
288
294
|
publish_time: int | None = None,
|
|
289
295
|
sequence: int = 0,
|
|
290
296
|
) -> None:
|
|
297
|
+
"""Write one message on a registered channel.
|
|
298
|
+
|
|
299
|
+
``log_time`` and ``publish_time`` are nanoseconds since epoch; when
|
|
300
|
+
``publish_time`` is omitted it defaults to ``log_time``.
|
|
301
|
+
"""
|
|
291
302
|
self._raise_if_not_active("write a message")
|
|
292
303
|
group_name = self._group_name_by_channel_id.get(channel_id)
|
|
293
304
|
if group_name is None:
|
|
@@ -318,6 +329,7 @@ class GroupedMcapWriter:
|
|
|
318
329
|
self._finalize_group_chunk(group_name)
|
|
319
330
|
|
|
320
331
|
def add_metadata(self, name: str, data: dict[str, str]) -> None:
|
|
332
|
+
"""Append an MCAP Metadata record (e.g. episode or provenance stamps)."""
|
|
321
333
|
self._raise_if_not_active("add metadata")
|
|
322
334
|
self._flush()
|
|
323
335
|
offset = self._stream.tell()
|
|
@@ -337,6 +349,7 @@ class GroupedMcapWriter:
|
|
|
337
349
|
log_time: int = 0,
|
|
338
350
|
create_time: int = 0,
|
|
339
351
|
) -> None:
|
|
352
|
+
"""Append an MCAP Attachment record; times are nanoseconds since epoch."""
|
|
340
353
|
self._raise_if_not_active("add an attachment")
|
|
341
354
|
self._flush()
|
|
342
355
|
offset = self._stream.tell()
|
|
@@ -363,6 +376,9 @@ class GroupedMcapWriter:
|
|
|
363
376
|
self._flush()
|
|
364
377
|
|
|
365
378
|
def finish(self) -> None:
|
|
379
|
+
"""Finalize the MCAP file. Must be the last mutating call; safe to call
|
|
380
|
+
again (idempotent). Further writes raise :class:`RuntimeError`.
|
|
381
|
+
"""
|
|
366
382
|
if self._state is _WriterState.FINISHED:
|
|
367
383
|
return
|
|
368
384
|
self._raise_if_not_active("finish")
|
|
@@ -7,6 +7,7 @@ from types import ModuleType
|
|
|
7
7
|
|
|
8
8
|
import numpy as np
|
|
9
9
|
|
|
10
|
+
from ._field_guards import require_float
|
|
10
11
|
from ._raw_frames import LUMA_FRAME_FILTER_GRAPH, luma_frames
|
|
11
12
|
from ._toolchain import VideoMeasurementToolchain
|
|
12
13
|
|
|
@@ -51,6 +52,8 @@ class CameraMotionSettings:
|
|
|
51
52
|
horizontal_field_of_view_degrees: float = DEFAULT_HORIZONTAL_FIELD_OF_VIEW_DEGREES
|
|
52
53
|
|
|
53
54
|
def __post_init__(self) -> None:
|
|
55
|
+
require_float(self.frames_per_second, "frames_per_second")
|
|
56
|
+
require_float(self.horizontal_field_of_view_degrees, "horizontal_field_of_view_degrees")
|
|
54
57
|
if not math.isfinite(self.frames_per_second) or self.frames_per_second <= 0:
|
|
55
58
|
raise ValueError(
|
|
56
59
|
f"frames_per_second must be finite and positive, got {self.frames_per_second}"
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Shared numeric-field validation for the video-measurement settings dataclasses.
|
|
2
|
+
|
|
3
|
+
Range checks alone (``0 <= x <= 100``) do not reject the wrong *type*: ``bool``
|
|
4
|
+
subclasses ``int``, so ``True``/``False`` satisfy every numeric comparison and
|
|
5
|
+
range test the settings below already run, and a ``str``/``None`` would raise
|
|
6
|
+
a bare ``TypeError`` from the comparison itself instead of a clear message
|
|
7
|
+
naming the field. This closes that hole the same way ``catalog.py`` already
|
|
8
|
+
does for interval bounds and measurement values.
|
|
9
|
+
|
|
10
|
+
These settings are caller-constructed configuration, not measurement-pipeline
|
|
11
|
+
output, so unlike ``catalog.py``'s NumPy-scalar coercion, no NumPy handling is
|
|
12
|
+
added here: a caller building a ``np.float64`` threshold can call ``.item()``
|
|
13
|
+
itself, the same way any other non-native-Python value would need to.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def require_int(value: object, name: str) -> None:
|
|
18
|
+
"""Refuse anything but a plain ``int``, ``bool`` included."""
|
|
19
|
+
if isinstance(value, bool) or not isinstance(value, int):
|
|
20
|
+
raise ValueError(f"{name} must be an int, got {type(value).__name__}")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def require_float(value: object, name: str) -> None:
|
|
24
|
+
"""Refuse anything but a plain ``int`` or ``float``, ``bool`` included.
|
|
25
|
+
|
|
26
|
+
An ``int`` is accepted for a float-declared field: it is a perfectly good
|
|
27
|
+
float value (``0`` is a real, falsy value that must still pass).
|
|
28
|
+
"""
|
|
29
|
+
if isinstance(value, bool) or not isinstance(value, int | float):
|
|
30
|
+
raise ValueError(f"{name} must be an int or float, got {type(value).__name__}")
|
|
@@ -14,6 +14,7 @@ from io import StringIO
|
|
|
14
14
|
from pathlib import Path
|
|
15
15
|
from typing import Protocol
|
|
16
16
|
|
|
17
|
+
from ._field_guards import require_float, require_int
|
|
17
18
|
from ._toolchain import VideoMeasurementToolchain
|
|
18
19
|
|
|
19
20
|
FRAME_STATISTICS_DEFINITION_VERSION = "video-frame-statistics/v1"
|
|
@@ -54,6 +55,8 @@ class VideoTimeInterval:
|
|
|
54
55
|
end_seconds: float
|
|
55
56
|
|
|
56
57
|
def __post_init__(self) -> None:
|
|
58
|
+
require_float(self.start_seconds, "start_seconds")
|
|
59
|
+
require_float(self.end_seconds, "end_seconds")
|
|
57
60
|
if not math.isfinite(self.start_seconds) or self.start_seconds < 0:
|
|
58
61
|
raise ValueError(f"start_seconds must be finite and nonnegative: {self.start_seconds}")
|
|
59
62
|
if not math.isfinite(self.end_seconds) or self.end_seconds < self.start_seconds:
|
|
@@ -74,6 +77,14 @@ class FrameStatisticsSettings:
|
|
|
74
77
|
overexposed_average_luma_threshold: float = 235.0
|
|
75
78
|
|
|
76
79
|
def __post_init__(self) -> None:
|
|
80
|
+
require_int(
|
|
81
|
+
self.black_frame_minimum_pixel_share_percent,
|
|
82
|
+
"black_frame_minimum_pixel_share_percent",
|
|
83
|
+
)
|
|
84
|
+
require_int(self.black_pixel_luma_threshold, "black_pixel_luma_threshold")
|
|
85
|
+
require_float(self.freeze_noise_tolerance_decibels, "freeze_noise_tolerance_decibels")
|
|
86
|
+
require_float(self.freeze_minimum_duration_seconds, "freeze_minimum_duration_seconds")
|
|
87
|
+
require_float(self.overexposed_average_luma_threshold, "overexposed_average_luma_threshold")
|
|
77
88
|
if not 0 <= self.black_frame_minimum_pixel_share_percent <= 100:
|
|
78
89
|
raise ValueError("black_frame_minimum_pixel_share_percent must be between 0 and 100")
|
|
79
90
|
if not 0 <= self.black_pixel_luma_threshold <= 255:
|
|
@@ -371,7 +382,11 @@ class _FrameStatisticsAccumulator:
|
|
|
371
382
|
overexposed_frame_percent=100.0 * self.overexposed_frame_count / frame_count,
|
|
372
383
|
freeze_intervals=tuple(self.freeze_intervals),
|
|
373
384
|
freeze_total_seconds=sum(
|
|
374
|
-
|
|
385
|
+
(
|
|
386
|
+
interval.end_seconds - interval.start_seconds
|
|
387
|
+
for interval in self.freeze_intervals
|
|
388
|
+
),
|
|
389
|
+
0.0,
|
|
375
390
|
),
|
|
376
391
|
average_luma_mean=self.average_luma.mean(),
|
|
377
392
|
average_luma_minimum=self.average_luma.minimum(),
|