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.
Files changed (28) hide show
  1. hive_video-0.1.0/.gitignore +48 -0
  2. hive_video-0.1.0/LICENSE +22 -0
  3. hive_video-0.1.0/PKG-INFO +94 -0
  4. hive_video-0.1.0/docs/agent-generated/package-readme.md +74 -0
  5. hive_video-0.1.0/pyproject.toml +77 -0
  6. hive_video-0.1.0/src/hive_video/__init__.py +3 -0
  7. hive_video-0.1.0/src/hive_video/__main__.py +6 -0
  8. hive_video-0.1.0/src/hive_video/_binaries.py +108 -0
  9. hive_video-0.1.0/src/hive_video/cli.py +151 -0
  10. hive_video-0.1.0/src/hive_video/download.py +853 -0
  11. hive_video-0.1.0/src/hive_video/fragment.py +383 -0
  12. hive_video-0.1.0/src/hive_video/progress.py +155 -0
  13. hive_video-0.1.0/src/hive_video/resequence/__init__.py +1 -0
  14. hive_video-0.1.0/src/hive_video/resequence/__main__.py +5 -0
  15. hive_video-0.1.0/src/hive_video/resequence/build_segments_from_jumps.py +302 -0
  16. hive_video-0.1.0/src/hive_video/resequence/cli.py +131 -0
  17. hive_video-0.1.0/src/hive_video/resequence/compress_resequenced.py +449 -0
  18. hive_video-0.1.0/src/hive_video/resequence/detect_video_discontinuities.py +495 -0
  19. hive_video-0.1.0/src/hive_video/resequence/diagnostics/__init__.py +1 -0
  20. hive_video-0.1.0/src/hive_video/resequence/diagnostics/approve_manual_join_qc.py +165 -0
  21. hive_video-0.1.0/src/hive_video/resequence/diagnostics/auto_qc_segment_joins.py +1035 -0
  22. hive_video-0.1.0/src/hive_video/resequence/diagnostics/diagnose_segment_discontinuities.py +239 -0
  23. hive_video-0.1.0/src/hive_video/resequence/diagnostics/make_join_review_video.py +667 -0
  24. hive_video-0.1.0/src/hive_video/resequence/order_video_segments.py +376 -0
  25. hive_video-0.1.0/src/hive_video/resequence/prepare_cut_review.py +84 -0
  26. hive_video-0.1.0/src/hive_video/resequence/reassemble_video_from_segments.py +870 -0
  27. hive_video-0.1.0/src/hive_video/resequence/summarize_jump_events.py +182 -0
  28. 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/
@@ -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,3 @@
1
+ """Portable tools for working with honey bee hive videos."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,6 @@
1
+ """Run the hive-video command with ``python -m hive_video``."""
2
+
3
+ from hive_video.cli import main
4
+
5
+ if __name__ == "__main__":
6
+ raise SystemExit(main())
@@ -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