hive-video 0.1.0__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.
- hive_video-0.1.0/.gitignore +48 -0
- hive_video-0.1.0/LICENSE +22 -0
- hive_video-0.1.0/PKG-INFO +94 -0
- hive_video-0.1.0/docs/agent-generated/package-readme.md +74 -0
- hive_video-0.1.0/pyproject.toml +77 -0
- hive_video-0.1.0/src/hive_video/__init__.py +3 -0
- hive_video-0.1.0/src/hive_video/__main__.py +6 -0
- hive_video-0.1.0/src/hive_video/_binaries.py +108 -0
- hive_video-0.1.0/src/hive_video/cli.py +151 -0
- hive_video-0.1.0/src/hive_video/download.py +853 -0
- hive_video-0.1.0/src/hive_video/fragment.py +383 -0
- hive_video-0.1.0/src/hive_video/progress.py +155 -0
- hive_video-0.1.0/src/hive_video/resequence/__init__.py +1 -0
- hive_video-0.1.0/src/hive_video/resequence/__main__.py +5 -0
- hive_video-0.1.0/src/hive_video/resequence/build_segments_from_jumps.py +302 -0
- hive_video-0.1.0/src/hive_video/resequence/cli.py +131 -0
- hive_video-0.1.0/src/hive_video/resequence/compress_resequenced.py +449 -0
- hive_video-0.1.0/src/hive_video/resequence/detect_video_discontinuities.py +495 -0
- hive_video-0.1.0/src/hive_video/resequence/diagnostics/__init__.py +1 -0
- hive_video-0.1.0/src/hive_video/resequence/diagnostics/approve_manual_join_qc.py +165 -0
- hive_video-0.1.0/src/hive_video/resequence/diagnostics/auto_qc_segment_joins.py +1035 -0
- hive_video-0.1.0/src/hive_video/resequence/diagnostics/diagnose_segment_discontinuities.py +239 -0
- hive_video-0.1.0/src/hive_video/resequence/diagnostics/make_join_review_video.py +667 -0
- hive_video-0.1.0/src/hive_video/resequence/order_video_segments.py +376 -0
- hive_video-0.1.0/src/hive_video/resequence/prepare_cut_review.py +84 -0
- hive_video-0.1.0/src/hive_video/resequence/reassemble_video_from_segments.py +870 -0
- hive_video-0.1.0/src/hive_video/resequence/summarize_jump_events.py +182 -0
- hive_video-0.1.0/src/hive_video/sources.py +134 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Local Python environments and caches
|
|
2
|
+
.venv/
|
|
3
|
+
__pycache__/
|
|
4
|
+
*.py[cod]
|
|
5
|
+
.ipynb_checkpoints/
|
|
6
|
+
|
|
7
|
+
# Local configuration and stop files
|
|
8
|
+
.env
|
|
9
|
+
.env.*
|
|
10
|
+
.safeword
|
|
11
|
+
|
|
12
|
+
# OS/editor noise
|
|
13
|
+
.DS_Store
|
|
14
|
+
|
|
15
|
+
# Slurm opens these scheduler logs before a worker can verify its Git revision.
|
|
16
|
+
slurm.*.out
|
|
17
|
+
slurm.*.err
|
|
18
|
+
|
|
19
|
+
# Large generated pipeline outputs.
|
|
20
|
+
#
|
|
21
|
+
# Ignore raw source data by default. Keep only the named small seed sample.
|
|
22
|
+
data/raw/**
|
|
23
|
+
!data/raw/
|
|
24
|
+
!data/raw/.gitkeep
|
|
25
|
+
|
|
26
|
+
# Always keep our pet sample video.
|
|
27
|
+
!data/raw/start04_sample_5s.mp4
|
|
28
|
+
|
|
29
|
+
data/qc/**
|
|
30
|
+
!data/qc/
|
|
31
|
+
!data/qc/.gitkeep
|
|
32
|
+
!data/qc/README.md
|
|
33
|
+
|
|
34
|
+
data/artifacts/**
|
|
35
|
+
!data/artifacts/
|
|
36
|
+
!data/artifacts/.gitkeep
|
|
37
|
+
!data/artifacts/README.md
|
|
38
|
+
|
|
39
|
+
data/experiments/*
|
|
40
|
+
|
|
41
|
+
# Always keep our pet experiment
|
|
42
|
+
!data/experiments/experiment_example_5s/
|
|
43
|
+
|
|
44
|
+
# A no-sync folder
|
|
45
|
+
data/no-sync/**
|
|
46
|
+
|
|
47
|
+
# Local distribution staging output
|
|
48
|
+
distribution-1/
|
hive_video-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Peter Dresslar
|
|
4
|
+
Copyright (c) 2026 Collective Logic Lab, School of Complex Adaptive Systems, Arizona State University
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: hive-video
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Portable download, resequencing, and fragment tools for honey bee hive video.
|
|
5
|
+
Project-URL: Documentation, https://github.com/Collective-Logic-Lab/honeybee-hive-video/blob/main/docs/agent-generated/package-interface.md
|
|
6
|
+
Project-URL: Source, https://github.com/Collective-Logic-Lab/honeybee-hive-video
|
|
7
|
+
Project-URL: Issues, https://github.com/Collective-Logic-Lab/honeybee-hive-video/issues
|
|
8
|
+
Project-URL: Releases, https://github.com/Collective-Logic-Lab/honeybee-hive-video/releases
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Python: >=3.12
|
|
12
|
+
Requires-Dist: certifi>=2026.4.22
|
|
13
|
+
Requires-Dist: filelock>=3.20
|
|
14
|
+
Requires-Dist: portable-ffmpeg==0.3.0
|
|
15
|
+
Provides-Extra: resequence
|
|
16
|
+
Requires-Dist: numpy>=1.26; extra == 'resequence'
|
|
17
|
+
Requires-Dist: opencv-python>=4.13.0.92; extra == 'resequence'
|
|
18
|
+
Requires-Dist: pillow>=12.2.0; extra == 'resequence'
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# Hive Video Tools
|
|
22
|
+
|
|
23
|
+
`hive-video` provides command-line tools and Python APIs for downloading, resequencing, and extracting fragments from honey bee hive videos. It is part of the [Collective Logic Lab's honeybee-hive-video project](https://github.com/Collective-Logic-Lab/honeybee-hive-video).
|
|
24
|
+
|
|
25
|
+
- `download` selects a recording from the Edmond 2019 honey bee video archive, resumes partial transfers, and verifies the archive MD5 checksum.
|
|
26
|
+
- `fragment` extracts a PNG frame or an MP4 clip from a local video and writes a JSON sidecar with provenance and an output checksum.
|
|
27
|
+
- `resequence` provides individual reconstruction, inspection, quality-control, and rendering stages. Its dependencies are optional.
|
|
28
|
+
|
|
29
|
+
Version 0.1.0 is the first public package release. These tools have been used in research; the packaged API is still evolving. During 0.x, incompatible interface changes receive a minor version increment and release notes. Retain a dependency lockfile for reproducible work.
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
Use Python 3.12 or newer. To install the command-line tools with `uv`:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
uv tool install hive-video
|
|
37
|
+
hive-video --help
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
To include resequencing support, install with the optional extra:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
uv tool install 'hive-video[resequence]'
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
FFmpeg and ffprobe are resolved automatically. The tools use an explicitly configured pair or a complete pair on `PATH`; otherwise, the included provider downloads and caches platform-specific builds from the FFmpeg 8 family. Prepare them before offline work with:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
hive-video setup-ffmpeg
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
This reports their executable paths, versions, and checksums. Help and archive downloading do not need FFmpeg.
|
|
53
|
+
|
|
54
|
+
## Extract a fragment
|
|
55
|
+
|
|
56
|
+
Given a local `source.mp4`, extract its first 25 frames:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
hive-video fragment --video source.mp4 --start-frame 0 --duration-frames 25 --out clip.mp4
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Omit the duration and use a `.png` output to extract one frame. Existing output files are refused. The bee progress display can be changed with `--progress plain` or disabled with `--progress off`.
|
|
63
|
+
|
|
64
|
+
## Use from Python
|
|
65
|
+
|
|
66
|
+
Add the library to the project that will import it:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
uv add hive-video
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Use `uv add 'hive-video[resequence]'` if you also need resequencing. Then download a recording and extract a clip:
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
from pathlib import Path
|
|
76
|
+
|
|
77
|
+
from hive_video.download import download_video
|
|
78
|
+
from hive_video.fragment import create_fragment
|
|
79
|
+
|
|
80
|
+
my_dir = Path("data/day22")
|
|
81
|
+
source = download_video(day=22, side=0, panel="top", target=my_dir)
|
|
82
|
+
clip = create_fragment(source, my_dir / "clip.mp4", start=0, duration=30, unit="seconds")
|
|
83
|
+
print(clip)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`day=22` selects the archive's sequential `start22` capture identifier; the filename gives its calendar timestamp. Downloading retrieves the entire source recording, which can be tens of gigabytes. Seconds use the source's nominal frame clock. See the [package interface guide](https://github.com/Collective-Logic-Lab/honeybee-hive-video/blob/main/docs/agent-generated/package-interface.md) for selectors, transfer settings, progress callbacks, and fragment conventions.
|
|
87
|
+
|
|
88
|
+
## Resequencing and scientific scope
|
|
89
|
+
|
|
90
|
+
Resequencing is computationally expensive and includes human inspection and join quality control. Follow the [stage workflow](https://github.com/Collective-Logic-Lab/honeybee-hive-video/blob/main/docs/agent-generated/resequencing.md) and [methods record](https://github.com/Collective-Logic-Lab/honeybee-hive-video/blob/main/docs/agent-generated/METHODS.md). Reconstruction does not independently establish absolute recording chronology or validate a biological interpretation.
|
|
91
|
+
|
|
92
|
+
This distribution contains the reusable video tools. Internal analyses, experimental recipes, Slurm launchers, and research data remain in the source project.
|
|
93
|
+
|
|
94
|
+
The package source is MIT licensed. FFmpeg executables are supplied separately by the selected system installation or binary provider.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Hive Video Tools
|
|
2
|
+
|
|
3
|
+
`hive-video` provides command-line tools and Python APIs for downloading, resequencing, and extracting fragments from honey bee hive videos. It is part of the [Collective Logic Lab's honeybee-hive-video project](https://github.com/Collective-Logic-Lab/honeybee-hive-video).
|
|
4
|
+
|
|
5
|
+
- `download` selects a recording from the Edmond 2019 honey bee video archive, resumes partial transfers, and verifies the archive MD5 checksum.
|
|
6
|
+
- `fragment` extracts a PNG frame or an MP4 clip from a local video and writes a JSON sidecar with provenance and an output checksum.
|
|
7
|
+
- `resequence` provides individual reconstruction, inspection, quality-control, and rendering stages. Its dependencies are optional.
|
|
8
|
+
|
|
9
|
+
Version 0.1.0 is the first public package release. These tools have been used in research; the packaged API is still evolving. During 0.x, incompatible interface changes receive a minor version increment and release notes. Retain a dependency lockfile for reproducible work.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
Use Python 3.12 or newer. To install the command-line tools with `uv`:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
uv tool install hive-video
|
|
17
|
+
hive-video --help
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
To include resequencing support, install with the optional extra:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
uv tool install 'hive-video[resequence]'
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
FFmpeg and ffprobe are resolved automatically. The tools use an explicitly configured pair or a complete pair on `PATH`; otherwise, the included provider downloads and caches platform-specific builds from the FFmpeg 8 family. Prepare them before offline work with:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
hive-video setup-ffmpeg
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
This reports their executable paths, versions, and checksums. Help and archive downloading do not need FFmpeg.
|
|
33
|
+
|
|
34
|
+
## Extract a fragment
|
|
35
|
+
|
|
36
|
+
Given a local `source.mp4`, extract its first 25 frames:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
hive-video fragment --video source.mp4 --start-frame 0 --duration-frames 25 --out clip.mp4
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Omit the duration and use a `.png` output to extract one frame. Existing output files are refused. The bee progress display can be changed with `--progress plain` or disabled with `--progress off`.
|
|
43
|
+
|
|
44
|
+
## Use from Python
|
|
45
|
+
|
|
46
|
+
Add the library to the project that will import it:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
uv add hive-video
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Use `uv add 'hive-video[resequence]'` if you also need resequencing. Then download a recording and extract a clip:
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from pathlib import Path
|
|
56
|
+
|
|
57
|
+
from hive_video.download import download_video
|
|
58
|
+
from hive_video.fragment import create_fragment
|
|
59
|
+
|
|
60
|
+
my_dir = Path("data/day22")
|
|
61
|
+
source = download_video(day=22, side=0, panel="top", target=my_dir)
|
|
62
|
+
clip = create_fragment(source, my_dir / "clip.mp4", start=0, duration=30, unit="seconds")
|
|
63
|
+
print(clip)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`day=22` selects the archive's sequential `start22` capture identifier; the filename gives its calendar timestamp. Downloading retrieves the entire source recording, which can be tens of gigabytes. Seconds use the source's nominal frame clock. See the [package interface guide](https://github.com/Collective-Logic-Lab/honeybee-hive-video/blob/main/docs/agent-generated/package-interface.md) for selectors, transfer settings, progress callbacks, and fragment conventions.
|
|
67
|
+
|
|
68
|
+
## Resequencing and scientific scope
|
|
69
|
+
|
|
70
|
+
Resequencing is computationally expensive and includes human inspection and join quality control. Follow the [stage workflow](https://github.com/Collective-Logic-Lab/honeybee-hive-video/blob/main/docs/agent-generated/resequencing.md) and [methods record](https://github.com/Collective-Logic-Lab/honeybee-hive-video/blob/main/docs/agent-generated/METHODS.md). Reconstruction does not independently establish absolute recording chronology or validate a biological interpretation.
|
|
71
|
+
|
|
72
|
+
This distribution contains the reusable video tools. Internal analyses, experimental recipes, Slurm launchers, and research data remain in the source project.
|
|
73
|
+
|
|
74
|
+
The package source is MIT licensed. FFmpeg executables are supplied separately by the selected system installation or binary provider.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "hive-video"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Portable download, resequencing, and fragment tools for honey bee hive video."
|
|
5
|
+
readme = "docs/agent-generated/package-readme.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
requires-python = ">=3.12"
|
|
9
|
+
dependencies = ["certifi>=2026.4.22", "filelock>=3.20", "portable-ffmpeg==0.3.0"]
|
|
10
|
+
|
|
11
|
+
[project.urls]
|
|
12
|
+
Documentation = "https://github.com/Collective-Logic-Lab/honeybee-hive-video/blob/main/docs/agent-generated/package-interface.md"
|
|
13
|
+
Source = "https://github.com/Collective-Logic-Lab/honeybee-hive-video"
|
|
14
|
+
Issues = "https://github.com/Collective-Logic-Lab/honeybee-hive-video/issues"
|
|
15
|
+
Releases = "https://github.com/Collective-Logic-Lab/honeybee-hive-video/releases"
|
|
16
|
+
|
|
17
|
+
[project.optional-dependencies]
|
|
18
|
+
resequence = [
|
|
19
|
+
"numpy>=1.26",
|
|
20
|
+
"opencv-python>=4.13.0.92",
|
|
21
|
+
"pillow>=12.2.0",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.scripts]
|
|
25
|
+
hive-video = "hive_video.cli:main"
|
|
26
|
+
|
|
27
|
+
[build-system]
|
|
28
|
+
requires = ["hatchling>=1.27,<2"]
|
|
29
|
+
build-backend = "hatchling.build"
|
|
30
|
+
|
|
31
|
+
[tool.hatch.build.targets.wheel]
|
|
32
|
+
packages = ["src/hive_video"]
|
|
33
|
+
|
|
34
|
+
[tool.hatch.build.targets.sdist]
|
|
35
|
+
only-include = ["pyproject.toml", "LICENSE", "docs/agent-generated/package-readme.md", "src/hive_video"]
|
|
36
|
+
|
|
37
|
+
[dependency-groups]
|
|
38
|
+
dev = [
|
|
39
|
+
"ruff>=0.5",
|
|
40
|
+
]
|
|
41
|
+
research = [
|
|
42
|
+
"certifi>=2026.4.22",
|
|
43
|
+
"hf>=1.14.0",
|
|
44
|
+
"ipykernel>=6.29",
|
|
45
|
+
"jupyterlab>=4.2",
|
|
46
|
+
"matplotlib>=3.8",
|
|
47
|
+
"numpy>=1.26",
|
|
48
|
+
"opencv-python>=4.13.0.92",
|
|
49
|
+
"pandas>=2.2",
|
|
50
|
+
"pillow>=12.2.0",
|
|
51
|
+
"scikit-learn>=1.5",
|
|
52
|
+
"scipy>=1.13",
|
|
53
|
+
"seaborn>=0.13",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
[tool.uv]
|
|
57
|
+
default-groups = ["dev", "research"]
|
|
58
|
+
|
|
59
|
+
[tool.ruff]
|
|
60
|
+
line-length = 100
|
|
61
|
+
target-version = "py312"
|
|
62
|
+
|
|
63
|
+
[tool.ruff.lint]
|
|
64
|
+
select = [
|
|
65
|
+
"E", # pycodestyle errors
|
|
66
|
+
"W", # pycodestyle warnings
|
|
67
|
+
"F", # Pyflakes
|
|
68
|
+
"I", # isort (import sorting)
|
|
69
|
+
"NPY", # NumPy-specific rules
|
|
70
|
+
]
|
|
71
|
+
ignore = [
|
|
72
|
+
"E501", # Do not enforce line length (supports unwrapped prose in comments/docstrings)
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
[tool.ruff.lint.per-file-ignores]
|
|
76
|
+
"notebooks/**" = ["F401", "F841"]
|
|
77
|
+
"experiments/**" = ["E402"]
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Resolve the FFmpeg pair without changing PATH; see method HV-P002."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import contextlib
|
|
6
|
+
import hashlib
|
|
7
|
+
import os
|
|
8
|
+
import platform
|
|
9
|
+
import shutil
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
from functools import lru_cache
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
_NAMES = ("ffmpeg", "ffprobe")
|
|
16
|
+
_OVERRIDES = ("HIVE_VIDEO_FFMPEG", "HIVE_VIDEO_FFPROBE")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _executable(value: str | Path, name: str) -> str:
|
|
20
|
+
path = Path(value).expanduser()
|
|
21
|
+
if not path.is_absolute():
|
|
22
|
+
raise ValueError(f"{name} must name an absolute executable path, got {value!r}")
|
|
23
|
+
if not path.is_file():
|
|
24
|
+
raise FileNotFoundError(f"{name} executable is missing or not a file: {path}")
|
|
25
|
+
if not os.access(path, os.X_OK):
|
|
26
|
+
raise PermissionError(f"{name} is not executable: {path}")
|
|
27
|
+
return str(path.resolve())
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@lru_cache(maxsize=1)
|
|
31
|
+
def _managed_binaries() -> tuple[str, str]:
|
|
32
|
+
# Only request a native build on explicitly supported platforms.
|
|
33
|
+
machine = platform.machine().lower()
|
|
34
|
+
supported = {
|
|
35
|
+
"darwin": {"arm64", "aarch64", "x86_64", "amd64"},
|
|
36
|
+
"linux": {"arm64", "aarch64", "x86_64", "amd64"},
|
|
37
|
+
"win32": {"x86_64", "amd64"},
|
|
38
|
+
}
|
|
39
|
+
if machine not in supported.get(sys.platform, set()):
|
|
40
|
+
raise RuntimeError(
|
|
41
|
+
f"Managed FFmpeg is unsupported on {sys.platform}/{machine}; "
|
|
42
|
+
"set HIVE_VIDEO_FFMPEG and HIVE_VIDEO_FFPROBE to a compatible executable pair."
|
|
43
|
+
)
|
|
44
|
+
# Lazy import keeps help and archive downloads independent of provisioning.
|
|
45
|
+
from filelock import FileLock
|
|
46
|
+
from portable_ffmpeg import FFmpegVersions, core, get_ffmpeg
|
|
47
|
+
|
|
48
|
+
# The provider reports download progress to stdout. Keep CLI stdout usable
|
|
49
|
+
# for the output path or JSON and surface provisioning on stderr instead.
|
|
50
|
+
# The provider has a thread lock; serialize preparation across processes too.
|
|
51
|
+
# A timeout fails rather than breaking another process's installation lock.
|
|
52
|
+
with FileLock(str(core.CACHE_DIR.parent / "hive-video.lock"), timeout=60):
|
|
53
|
+
with contextlib.redirect_stdout(sys.stderr):
|
|
54
|
+
paths = get_ffmpeg(version=FFmpegVersions.V8)
|
|
55
|
+
return tuple(_executable(path, name) for name, path in zip(_NAMES, paths, strict=True))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _resolve_pair() -> tuple[str, str]:
|
|
59
|
+
explicit = tuple(os.environ.get(name) for name in _OVERRIDES)
|
|
60
|
+
if any(value is not None for value in explicit):
|
|
61
|
+
if not all(explicit):
|
|
62
|
+
raise ValueError(
|
|
63
|
+
"Set both HIVE_VIDEO_FFMPEG and HIVE_VIDEO_FFPROBE to absolute executable paths."
|
|
64
|
+
)
|
|
65
|
+
return tuple(
|
|
66
|
+
_executable(value, name) for name, value in zip(_OVERRIDES, explicit, strict=True)
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
found = tuple(shutil.which(name) for name in _NAMES)
|
|
70
|
+
if all(found):
|
|
71
|
+
return tuple(
|
|
72
|
+
_executable(Path(path).absolute(), name)
|
|
73
|
+
for name, path in zip(_NAMES, found, strict=True)
|
|
74
|
+
)
|
|
75
|
+
if "SLURM_JOB_ID" in os.environ:
|
|
76
|
+
raise RuntimeError(
|
|
77
|
+
"FFmpeg and ffprobe are unavailable in this Slurm task. Prepare them before "
|
|
78
|
+
"submission and set HIVE_VIDEO_FFMPEG and HIVE_VIDEO_FFPROBE to their absolute paths."
|
|
79
|
+
)
|
|
80
|
+
return _managed_binaries()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def resolve_binary(name: str) -> str:
|
|
84
|
+
"""Return one executable from the selected pair, provisioning it if needed."""
|
|
85
|
+
if name not in _NAMES:
|
|
86
|
+
raise ValueError(f"Expected 'ffmpeg' or 'ffprobe', got {name!r}")
|
|
87
|
+
return _resolve_pair()[_NAMES.index(name)]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def setup_ffmpeg() -> dict[str, dict[str, str]]:
|
|
91
|
+
"""Prepare and inspect both executables before offline or parallel work."""
|
|
92
|
+
result = {}
|
|
93
|
+
for name, executable in zip(_NAMES, _resolve_pair(), strict=True):
|
|
94
|
+
completed = subprocess.run(
|
|
95
|
+
[executable, "-version"], capture_output=True, text=True, check=False
|
|
96
|
+
)
|
|
97
|
+
if completed.returncode:
|
|
98
|
+
raise RuntimeError(
|
|
99
|
+
f"{name} version check failed (exit {completed.returncode}) for {executable}: "
|
|
100
|
+
f"{completed.stderr.strip()[-4000:]}"
|
|
101
|
+
)
|
|
102
|
+
lines = completed.stdout.splitlines()
|
|
103
|
+
if not lines or not lines[0].startswith(f"{name} version "):
|
|
104
|
+
raise RuntimeError(f"Unexpected {name} version output from {executable}: {lines!r}")
|
|
105
|
+
with Path(executable).open("rb") as handle:
|
|
106
|
+
checksum = hashlib.file_digest(handle, "sha256").hexdigest()
|
|
107
|
+
result[name] = {"path": executable, "version": lines[0], "sha256": checksum}
|
|
108
|
+
return result
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""Command-line entry point for portable hive-video utilities."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Sequence
|
|
10
|
+
|
|
11
|
+
from .fragment import create_fragment
|
|
12
|
+
from .progress import BeeProgress
|
|
13
|
+
from .sources import fragment_filename, resolve_source
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
17
|
+
parser = argparse.ArgumentParser(
|
|
18
|
+
prog="hive-video",
|
|
19
|
+
description="Local, reproducible utilities for honey bee hive video.",
|
|
20
|
+
)
|
|
21
|
+
commands = parser.add_subparsers(dest="command", required=True)
|
|
22
|
+
fragment = commands.add_parser(
|
|
23
|
+
"fragment",
|
|
24
|
+
help="Extract a frame as PNG or an interval as MP4.",
|
|
25
|
+
description=(
|
|
26
|
+
"Extract a frame or interval from a local video using FFmpeg and ffprobe. "
|
|
27
|
+
"Frame indices are zero-based. Seconds use the nominal frame clock n/fps "
|
|
28
|
+
"and select [ceil(start * fps), ceil((start + duration) * fps)); a still "
|
|
29
|
+
"selects ceil(start * fps). Omitted or zero duration writes one PNG. "
|
|
30
|
+
"Every output receives a JSON provenance sidecar; existing files are refused."
|
|
31
|
+
),
|
|
32
|
+
)
|
|
33
|
+
sources = fragment.add_mutually_exclusive_group(required=True)
|
|
34
|
+
sources.add_argument("--video", type=Path, help="Explicit local source video path.")
|
|
35
|
+
sources.add_argument(
|
|
36
|
+
"--locator", help="Archive shorthand such as start4_side0_top; requires --data-dir."
|
|
37
|
+
)
|
|
38
|
+
fragment.add_argument(
|
|
39
|
+
"--data-dir",
|
|
40
|
+
type=Path,
|
|
41
|
+
help="Explicit local search root for --locator; multiple matching copies are an error.",
|
|
42
|
+
)
|
|
43
|
+
start = fragment.add_mutually_exclusive_group(required=True)
|
|
44
|
+
start.add_argument("--start-seconds", metavar="DECIMAL", help="Non-negative start in seconds.")
|
|
45
|
+
start.add_argument(
|
|
46
|
+
"--start-frame", type=int, metavar="INT", help="Zero-based start frame index."
|
|
47
|
+
)
|
|
48
|
+
duration = fragment.add_mutually_exclusive_group()
|
|
49
|
+
duration.add_argument("--duration-seconds", metavar="DECIMAL", help="Duration in seconds.")
|
|
50
|
+
duration.add_argument("--duration-frames", type=int, metavar="INT", help="Number of frames.")
|
|
51
|
+
destination = fragment.add_mutually_exclusive_group()
|
|
52
|
+
destination.add_argument("--out", type=Path, help="Explicit .png or .mp4 output path.")
|
|
53
|
+
destination.add_argument(
|
|
54
|
+
"--out-dir",
|
|
55
|
+
type=Path,
|
|
56
|
+
help="Directory for a generated filename (default: ./data/artifacts/fragments).",
|
|
57
|
+
)
|
|
58
|
+
fragment.add_argument(
|
|
59
|
+
"--threads", type=int, default=1, help="FFmpeg thread limit (default: 1)."
|
|
60
|
+
)
|
|
61
|
+
fragment.add_argument(
|
|
62
|
+
"--progress",
|
|
63
|
+
choices=("auto", "plain", "off"),
|
|
64
|
+
default="auto",
|
|
65
|
+
help="Progress on stderr: animated on terminals, plain in logs, or off (default: auto).",
|
|
66
|
+
)
|
|
67
|
+
commands.add_parser("download", add_help=False, help="Resolve and download archive videos.")
|
|
68
|
+
commands.add_parser(
|
|
69
|
+
"resequence", add_help=False, help="Run individual reconstruction and review stages."
|
|
70
|
+
)
|
|
71
|
+
commands.add_parser(
|
|
72
|
+
"setup-ffmpeg",
|
|
73
|
+
help="Prepare FFmpeg and ffprobe for media operations, including offline work.",
|
|
74
|
+
description=(
|
|
75
|
+
"Resolve or download FFmpeg and ffprobe, verify both executables, and print "
|
|
76
|
+
"their paths, versions, and SHA-256 checksums as JSON. Provider progress goes "
|
|
77
|
+
"to stderr. Help does not download binaries."
|
|
78
|
+
),
|
|
79
|
+
)
|
|
80
|
+
return parser
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
84
|
+
arguments = list(sys.argv[1:] if argv is None else argv)
|
|
85
|
+
# Each tool owns its argument contract. Dispatch before parsing so stage
|
|
86
|
+
# help and all existing downloader flags reach their own parsers intact.
|
|
87
|
+
try:
|
|
88
|
+
if arguments and arguments[0] == "setup-ffmpeg":
|
|
89
|
+
build_parser().parse_args(arguments)
|
|
90
|
+
from ._binaries import setup_ffmpeg
|
|
91
|
+
|
|
92
|
+
print(json.dumps(setup_ffmpeg(), indent=2))
|
|
93
|
+
return 0
|
|
94
|
+
if arguments and arguments[0] == "download":
|
|
95
|
+
from . import download
|
|
96
|
+
|
|
97
|
+
return download.main(arguments[1:])
|
|
98
|
+
if arguments and arguments[0] == "resequence":
|
|
99
|
+
from .resequence.cli import main as resequence_main
|
|
100
|
+
|
|
101
|
+
return resequence_main(arguments[1:])
|
|
102
|
+
except KeyboardInterrupt:
|
|
103
|
+
print("hive-video: cancelled", file=sys.stderr)
|
|
104
|
+
return 130
|
|
105
|
+
except (OSError, ValueError, RuntimeError) as error:
|
|
106
|
+
print(f"hive-video: error: {error}", file=sys.stderr)
|
|
107
|
+
return 1
|
|
108
|
+
parser = build_parser()
|
|
109
|
+
args = parser.parse_args(arguments)
|
|
110
|
+
if args.locator is not None and args.data_dir is None:
|
|
111
|
+
parser.error("--locator requires an explicit --data-dir")
|
|
112
|
+
if args.video is not None and args.data_dir is not None:
|
|
113
|
+
parser.error("--data-dir applies only to --locator; --video already selects a source")
|
|
114
|
+
unit = "seconds" if args.start_seconds is not None else "frames"
|
|
115
|
+
if (unit == "seconds" and args.duration_frames is not None) or (
|
|
116
|
+
unit == "frames" and args.duration_seconds is not None
|
|
117
|
+
):
|
|
118
|
+
parser.error("start and duration must use the same units (seconds or frames)")
|
|
119
|
+
if args.threads < 1:
|
|
120
|
+
parser.error(f"--threads must be a positive integer; observed {args.threads}")
|
|
121
|
+
start = args.start_seconds if unit == "seconds" else args.start_frame
|
|
122
|
+
duration = args.duration_seconds if unit == "seconds" else args.duration_frames
|
|
123
|
+
try:
|
|
124
|
+
source = (
|
|
125
|
+
resolve_source(args.locator, args.data_dir)
|
|
126
|
+
if args.locator is not None
|
|
127
|
+
else args.video.expanduser().resolve(strict=True)
|
|
128
|
+
)
|
|
129
|
+
filename = fragment_filename(source, start=start, duration=duration, unit=unit)
|
|
130
|
+
output_dir = args.out_dir or Path.cwd() / "data" / "artifacts" / "fragments"
|
|
131
|
+
output = args.out if args.out is not None else output_dir / filename
|
|
132
|
+
if args.progress != "off":
|
|
133
|
+
print(f"Source: {source}", file=sys.stderr)
|
|
134
|
+
with BeeProgress(args.progress) as progress:
|
|
135
|
+
result = create_fragment(
|
|
136
|
+
source,
|
|
137
|
+
output,
|
|
138
|
+
start=start,
|
|
139
|
+
duration=duration,
|
|
140
|
+
unit=unit,
|
|
141
|
+
threads=args.threads,
|
|
142
|
+
on_progress=None if args.progress == "off" else progress.update,
|
|
143
|
+
)
|
|
144
|
+
except KeyboardInterrupt:
|
|
145
|
+
print(f"{parser.prog}: cancelled", file=sys.stderr)
|
|
146
|
+
return 130
|
|
147
|
+
except (OSError, ValueError, RuntimeError) as error:
|
|
148
|
+
print(f"{parser.prog}: error: {error}", file=sys.stderr)
|
|
149
|
+
return 1
|
|
150
|
+
print(result)
|
|
151
|
+
return 0
|