marm-behavior 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.
- marm_behavior/__init__.py +25 -0
- marm_behavior/__main__.py +435 -0
- marm_behavior/_data_files.py +458 -0
- marm_behavior/_tf_quiet.py +71 -0
- marm_behavior/data/README.txt +60 -0
- marm_behavior/data/__init__.py +1 -0
- marm_behavior/data/nn_reference/README.txt +57 -0
- marm_behavior/depths/__init__.py +0 -0
- marm_behavior/depths/depths_1.py +291 -0
- marm_behavior/dlc_inference.py +369 -0
- marm_behavior/el_to_csv.py +131 -0
- marm_behavior/extract/__init__.py +0 -0
- marm_behavior/extract/extract_1.py +331 -0
- marm_behavior/extract/extract_2.py +666 -0
- marm_behavior/extract/extract_3.py +285 -0
- marm_behavior/features/__init__.py +0 -0
- marm_behavior/features/labels.py +1300 -0
- marm_behavior/io/__init__.py +0 -0
- marm_behavior/io/csv_io.py +43 -0
- marm_behavior/io/mat_io.py +249 -0
- marm_behavior/nn_postprocess.py +945 -0
- marm_behavior/numerics/__init__.py +0 -0
- marm_behavior/numerics/helpers.py +284 -0
- marm_behavior/numerics/hull.py +252 -0
- marm_behavior/pipeline/__init__.py +0 -0
- marm_behavior/pipeline/orchestrators.py +508 -0
- marm_behavior/process/__init__.py +0 -0
- marm_behavior/process/postures.py +153 -0
- marm_behavior/process/process_1.py +99 -0
- marm_behavior/process/process_2.py +292 -0
- marm_behavior/process/process_3.py +323 -0
- marm_behavior/process/process_4.py +502 -0
- marm_behavior/run.py +525 -0
- marm_behavior-0.1.0.dist-info/LICENSE +21 -0
- marm_behavior-0.1.0.dist-info/METADATA +378 -0
- marm_behavior-0.1.0.dist-info/RECORD +39 -0
- marm_behavior-0.1.0.dist-info/WHEEL +5 -0
- marm_behavior-0.1.0.dist-info/entry_points.txt +2 -0
- marm_behavior-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""
|
|
2
|
+
marm_behavior
|
|
3
|
+
=============
|
|
4
|
+
|
|
5
|
+
Multi-animal marmoset behavioral analysis pipeline. Takes a video,
|
|
6
|
+
runs DeepLabCut pose estimation, extracts per-animal body-part tracks,
|
|
7
|
+
computes per-frame behavioral features, and projects the features into
|
|
8
|
+
a learned cluster space.
|
|
9
|
+
|
|
10
|
+
Quickstart
|
|
11
|
+
----------
|
|
12
|
+
|
|
13
|
+
>>> from marm_behavior import run
|
|
14
|
+
>>> result = run("my_video.avi")
|
|
15
|
+
>>> result["descriptions"]
|
|
16
|
+
{'w': Path('w_description_my_video.csv'), ...}
|
|
17
|
+
|
|
18
|
+
See :func:`marm_behavior.run.run` for all options.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from .run import run
|
|
24
|
+
|
|
25
|
+
__all__ = ["run"]
|
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command-line entry point for marm_behavior.
|
|
3
|
+
|
|
4
|
+
Invoke as::
|
|
5
|
+
|
|
6
|
+
python -m marm_behavior # process all .avi in CWD
|
|
7
|
+
python -m marm_behavior path/to/video.avi # process one video
|
|
8
|
+
python -m marm_behavior path/to/video_dir/ # process all .avi in dir
|
|
9
|
+
|
|
10
|
+
See ``python -m marm_behavior --help`` for all options.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
# Silence TensorFlow's chatty C++ logger BEFORE any import path
|
|
16
|
+
# that could pull in tensorflow (.run -> .nn_postprocess -> tf, or
|
|
17
|
+
# .run -> .dlc_inference -> tf via deeplabcut). Must run before
|
|
18
|
+
# `import tensorflow` happens anywhere in the process.
|
|
19
|
+
from ._tf_quiet import silence_tensorflow_logging
|
|
20
|
+
silence_tensorflow_logging()
|
|
21
|
+
|
|
22
|
+
import argparse
|
|
23
|
+
import sys
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
from .run import run
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
#: Short colour keys and their full names, used for the per-animal
|
|
30
|
+
#: buddy-override CLI flags.
|
|
31
|
+
_COLOR_FLAGS = (
|
|
32
|
+
("red", "r"),
|
|
33
|
+
("white", "w"),
|
|
34
|
+
("blue", "b"),
|
|
35
|
+
("yellow", "y"),
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
40
|
+
p = argparse.ArgumentParser(
|
|
41
|
+
prog="python -m marm_behavior",
|
|
42
|
+
description=(
|
|
43
|
+
"Run the marmoset behavioral analysis pipeline on one or "
|
|
44
|
+
"more videos. With no positional argument, processes every "
|
|
45
|
+
"``.avi`` in the current working directory. Outputs land "
|
|
46
|
+
"next to each input video (or in --output-dir)."
|
|
47
|
+
),
|
|
48
|
+
)
|
|
49
|
+
p.add_argument(
|
|
50
|
+
"video",
|
|
51
|
+
type=Path,
|
|
52
|
+
nargs="?",
|
|
53
|
+
default=None,
|
|
54
|
+
help=(
|
|
55
|
+
"Path to an input ``.avi`` file, or a directory containing "
|
|
56
|
+
"``.avi`` files. If omitted, uses the current working "
|
|
57
|
+
"directory."
|
|
58
|
+
),
|
|
59
|
+
)
|
|
60
|
+
p.add_argument(
|
|
61
|
+
"--single-csv",
|
|
62
|
+
type=Path,
|
|
63
|
+
default=None,
|
|
64
|
+
help=(
|
|
65
|
+
"Path to the DLC *single.csv file. Default: auto-discovered "
|
|
66
|
+
"next to the video via glob '<video_stem>*single.csv'. "
|
|
67
|
+
"Ignored in multi-video (batch) mode."
|
|
68
|
+
),
|
|
69
|
+
)
|
|
70
|
+
p.add_argument(
|
|
71
|
+
"--multi-csv",
|
|
72
|
+
type=Path,
|
|
73
|
+
default=None,
|
|
74
|
+
help=(
|
|
75
|
+
"Path to the DLC *multi.csv file. Default: auto-discovered "
|
|
76
|
+
"next to the video via glob '<video_stem>*multi.csv'. "
|
|
77
|
+
"Ignored in multi-video (batch) mode."
|
|
78
|
+
),
|
|
79
|
+
)
|
|
80
|
+
p.add_argument(
|
|
81
|
+
"-o",
|
|
82
|
+
"--output-dir",
|
|
83
|
+
type=Path,
|
|
84
|
+
default=None,
|
|
85
|
+
help="Directory to write artifacts into. Default: each video's parent folder.",
|
|
86
|
+
)
|
|
87
|
+
p.add_argument(
|
|
88
|
+
"--ground-normalized",
|
|
89
|
+
type=Path,
|
|
90
|
+
default=None,
|
|
91
|
+
help=(
|
|
92
|
+
"Path to a ground_normalized.mat or .npz file. Default: the copy "
|
|
93
|
+
"bundled inside the package — no external file needed."
|
|
94
|
+
),
|
|
95
|
+
)
|
|
96
|
+
p.add_argument(
|
|
97
|
+
"--dlc-config",
|
|
98
|
+
type=Path,
|
|
99
|
+
default=None,
|
|
100
|
+
help=(
|
|
101
|
+
"Path to a user-supplied DLC config.yaml. Default: use the "
|
|
102
|
+
"trained model bundled inside the package. Only matters if "
|
|
103
|
+
"the 'dlc' stage runs."
|
|
104
|
+
),
|
|
105
|
+
)
|
|
106
|
+
p.add_argument(
|
|
107
|
+
"--nn-reference-dir",
|
|
108
|
+
type=Path,
|
|
109
|
+
default=None,
|
|
110
|
+
help=(
|
|
111
|
+
"Override the folder containing the NN reference files "
|
|
112
|
+
"(out_inner_mean1.csv, out_inner_std1.csv, "
|
|
113
|
+
"tsne_temp1_1.csv, dbscan_temp1_1.csv, plus the native "
|
|
114
|
+
"t-SNE cache files). Default: the canonical reference "
|
|
115
|
+
"data bundled inside the package at "
|
|
116
|
+
"marm_behavior/data/nn_reference/. You normally don't "
|
|
117
|
+
"need this flag — only override if you have your own "
|
|
118
|
+
"reference set (e.g. for a different cohort). Only "
|
|
119
|
+
"matters if the 'nn' stage runs."
|
|
120
|
+
),
|
|
121
|
+
)
|
|
122
|
+
p.add_argument(
|
|
123
|
+
"--prefetch-data",
|
|
124
|
+
action="store_true",
|
|
125
|
+
help=(
|
|
126
|
+
"Download all bundled data (DLC model, NN encoder, NN "
|
|
127
|
+
"reference set, ground_normalized) from the Hugging Face "
|
|
128
|
+
"Hub and exit, without running any pipeline stages. "
|
|
129
|
+
"Mostly redundant — every pipeline invocation already "
|
|
130
|
+
"auto-downloads any missing files before starting work — "
|
|
131
|
+
"but useful if you want to warm the cache up-front or "
|
|
132
|
+
"verify the download succeeds without running on a real "
|
|
133
|
+
"video. Files are cached under ~/.cache/huggingface/hub/ "
|
|
134
|
+
"(override with MARM_BEHAVIOR_DATA_DIR=/path/for/shared/install)."
|
|
135
|
+
),
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# Per-colour buddy overrides for the NN stage.
|
|
139
|
+
for long_name, short_key in _COLOR_FLAGS:
|
|
140
|
+
p.add_argument(
|
|
141
|
+
f"--{long_name}-buddy",
|
|
142
|
+
nargs="+",
|
|
143
|
+
choices=("r", "w", "b", "y"),
|
|
144
|
+
metavar="COLOR",
|
|
145
|
+
default=None,
|
|
146
|
+
help=(
|
|
147
|
+
f"Override which other animal's description CSV is "
|
|
148
|
+
f"paired with {long_name.capitalize()}'s during NN "
|
|
149
|
+
f"encoding. Takes one or more short colour keys "
|
|
150
|
+
f"(r/w/b/y) in preference order. Default chains: "
|
|
151
|
+
f"Red→Yellow/Blue, White→Blue/Red, Blue→White/Red, "
|
|
152
|
+
f"Yellow→Red/Blue."
|
|
153
|
+
),
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# Per-colour "present" flags: default True, --no-<color> disables.
|
|
157
|
+
for long_name, _ in _COLOR_FLAGS:
|
|
158
|
+
p.add_argument(
|
|
159
|
+
f"--no-{long_name}",
|
|
160
|
+
dest=f"{long_name}_present",
|
|
161
|
+
action="store_false",
|
|
162
|
+
help=f"Mark {long_name} as not present in the video.",
|
|
163
|
+
)
|
|
164
|
+
p.set_defaults(**{f"{long_name}_present": True})
|
|
165
|
+
|
|
166
|
+
p.add_argument(
|
|
167
|
+
"--c-thresh",
|
|
168
|
+
type=float,
|
|
169
|
+
default=0.1,
|
|
170
|
+
help="DLC confidence threshold for the extract stage. Default: 0.1.",
|
|
171
|
+
)
|
|
172
|
+
p.add_argument(
|
|
173
|
+
"--target-coverage",
|
|
174
|
+
type=float,
|
|
175
|
+
default=1.0,
|
|
176
|
+
help="Alpha-shape hull coverage target for the extract stage. Default: 1.0.",
|
|
177
|
+
)
|
|
178
|
+
p.add_argument(
|
|
179
|
+
"--stages",
|
|
180
|
+
nargs="+",
|
|
181
|
+
choices=("dlc", "extract", "process", "depths", "labels", "nn"),
|
|
182
|
+
default=None,
|
|
183
|
+
metavar="STAGE",
|
|
184
|
+
help=(
|
|
185
|
+
"Which pipeline stages to run. Default: all six "
|
|
186
|
+
"(dlc, extract, process, depths, labels, nn). Use e.g. "
|
|
187
|
+
"'--stages labels' to re-run only the labels stage, or "
|
|
188
|
+
"'--stages extract process depths labels' to skip both "
|
|
189
|
+
"DLC inference and the NN postprocessing stage."
|
|
190
|
+
),
|
|
191
|
+
)
|
|
192
|
+
p.add_argument(
|
|
193
|
+
"-q",
|
|
194
|
+
"--quiet",
|
|
195
|
+
action="store_true",
|
|
196
|
+
help="Suppress the per-stage progress messages.",
|
|
197
|
+
)
|
|
198
|
+
return p
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _discover_videos(target: "Path | None") -> "list[Path]":
|
|
202
|
+
"""Resolve the positional argument into a list of video files.
|
|
203
|
+
|
|
204
|
+
* ``None`` → glob ``*.avi`` (and ``*.AVI``) in the current working
|
|
205
|
+
directory.
|
|
206
|
+
* a file path → ``[that file]``.
|
|
207
|
+
* a directory path → glob ``*.avi`` / ``*.AVI`` inside it.
|
|
208
|
+
|
|
209
|
+
Returns a sorted list. Raises :class:`FileNotFoundError` if no
|
|
210
|
+
videos are found.
|
|
211
|
+
"""
|
|
212
|
+
if target is None:
|
|
213
|
+
search = Path.cwd()
|
|
214
|
+
else:
|
|
215
|
+
target = target.expanduser().resolve()
|
|
216
|
+
if target.is_file():
|
|
217
|
+
return [target]
|
|
218
|
+
if not target.exists():
|
|
219
|
+
raise FileNotFoundError(f"path not found: {target}")
|
|
220
|
+
search = target
|
|
221
|
+
|
|
222
|
+
matches = sorted(
|
|
223
|
+
set(search.glob("*.avi")) | set(search.glob("*.AVI"))
|
|
224
|
+
)
|
|
225
|
+
if not matches:
|
|
226
|
+
raise FileNotFoundError(
|
|
227
|
+
f"no .avi files found in {search}. Pass an explicit path "
|
|
228
|
+
f"or cd to a folder containing videos."
|
|
229
|
+
)
|
|
230
|
+
return matches
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _build_nn_buddies(args: argparse.Namespace) -> "dict[str, list[str]] | None":
|
|
234
|
+
"""Collect the four per-colour ``--<colour>-buddy`` flags into a
|
|
235
|
+
dict suitable for :func:`marm_behavior.run.run`'s ``nn_buddies``
|
|
236
|
+
argument.
|
|
237
|
+
|
|
238
|
+
Returns ``None`` if no overrides were passed, so the downstream
|
|
239
|
+
code path uses the built-in default chains.
|
|
240
|
+
"""
|
|
241
|
+
overrides: dict[str, list[str]] = {}
|
|
242
|
+
for long_name, short_key in _COLOR_FLAGS:
|
|
243
|
+
flag_value = getattr(args, f"{long_name}_buddy")
|
|
244
|
+
if flag_value:
|
|
245
|
+
overrides[short_key] = list(flag_value)
|
|
246
|
+
return overrides or None
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _print_video_summary(result: dict, quiet: bool) -> None:
|
|
250
|
+
if quiet:
|
|
251
|
+
return
|
|
252
|
+
print()
|
|
253
|
+
print(f"done. stages run: {', '.join(result['stages_run'])}")
|
|
254
|
+
if result.get("tracks") is not None:
|
|
255
|
+
print(f" tracks: {result['tracks']}")
|
|
256
|
+
if result.get("edges") is not None:
|
|
257
|
+
print(f" edges: {result['edges']}")
|
|
258
|
+
if result.get("depths") is not None:
|
|
259
|
+
print(f" depths: {result['depths']}")
|
|
260
|
+
for key, path in result.get("descriptions", {}).items():
|
|
261
|
+
print(f" description {key}: {path}")
|
|
262
|
+
for color, (hcoord, hlabel) in result.get("nn", {}).items():
|
|
263
|
+
print(f" nn {color}: {hcoord.name} + {hlabel.name}")
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def main(argv: "list[str] | None" = None) -> int:
|
|
267
|
+
parser = _build_parser()
|
|
268
|
+
args = parser.parse_args(argv)
|
|
269
|
+
|
|
270
|
+
# Auto-download any missing bundled data files before doing
|
|
271
|
+
# anything else. On a fresh install this triggers a one-time
|
|
272
|
+
# ~310 MB download from the Hugging Face Hub; on every
|
|
273
|
+
# subsequent run this returns in milliseconds because every
|
|
274
|
+
# file is already cached. We do this up front (instead of
|
|
275
|
+
# inside each stage) so the user sees the "downloading..."
|
|
276
|
+
# notice before any stage logs scroll past, and so a network
|
|
277
|
+
# failure aborts cleanly instead of mid-pipeline.
|
|
278
|
+
from ._data_files import ensure_all, _hf_repo_id
|
|
279
|
+
|
|
280
|
+
if args.prefetch_data:
|
|
281
|
+
# --prefetch-data is now mostly redundant since ensure_all
|
|
282
|
+
# runs unconditionally, but we keep the flag as an explicit
|
|
283
|
+
# "download and exit" mode for users who want to warm the
|
|
284
|
+
# cache without running a pipeline.
|
|
285
|
+
print(
|
|
286
|
+
f"[marm_behavior] prefetching data files from "
|
|
287
|
+
f"https://huggingface.co/datasets/{_hf_repo_id()} ..."
|
|
288
|
+
)
|
|
289
|
+
try:
|
|
290
|
+
ensure_all(verbose=False)
|
|
291
|
+
except Exception as err:
|
|
292
|
+
print(
|
|
293
|
+
f"error: prefetch failed: "
|
|
294
|
+
f"{type(err).__name__}: {err}",
|
|
295
|
+
file=sys.stderr,
|
|
296
|
+
)
|
|
297
|
+
return 2
|
|
298
|
+
print("[marm_behavior] prefetch complete.")
|
|
299
|
+
return 0
|
|
300
|
+
|
|
301
|
+
try:
|
|
302
|
+
ensure_all(verbose=not args.quiet)
|
|
303
|
+
except Exception as err:
|
|
304
|
+
print(
|
|
305
|
+
f"error: failed to fetch bundled data: "
|
|
306
|
+
f"{type(err).__name__}: {err}\n"
|
|
307
|
+
f"Pipeline cannot run without the data files. Check your "
|
|
308
|
+
f"network connection, or set MARM_BEHAVIOR_DATA_DIR to a "
|
|
309
|
+
f"folder containing a manual copy.",
|
|
310
|
+
file=sys.stderr,
|
|
311
|
+
)
|
|
312
|
+
return 2
|
|
313
|
+
|
|
314
|
+
try:
|
|
315
|
+
videos = _discover_videos(args.video)
|
|
316
|
+
except FileNotFoundError as err:
|
|
317
|
+
print(f"error: {err}", file=sys.stderr)
|
|
318
|
+
return 2
|
|
319
|
+
|
|
320
|
+
nn_buddies = _build_nn_buddies(args)
|
|
321
|
+
batch_mode = len(videos) > 1
|
|
322
|
+
|
|
323
|
+
if batch_mode and not args.quiet:
|
|
324
|
+
print(f"[marm_behavior] batch mode: {len(videos)} videos found")
|
|
325
|
+
for v in videos:
|
|
326
|
+
print(f" - {v.name}")
|
|
327
|
+
print()
|
|
328
|
+
|
|
329
|
+
# In batch mode, --single-csv and --multi-csv don't make sense
|
|
330
|
+
# because they'd point at a single video's files. Warn and ignore.
|
|
331
|
+
if batch_mode and (args.single_csv or args.multi_csv):
|
|
332
|
+
print(
|
|
333
|
+
"warning: --single-csv / --multi-csv are ignored in batch "
|
|
334
|
+
"mode; each video uses its own auto-discovered CSVs.",
|
|
335
|
+
file=sys.stderr,
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
failures: list[tuple[Path, str]] = []
|
|
339
|
+
successes: list[Path] = []
|
|
340
|
+
|
|
341
|
+
# In batch mode, prebuild the nn-stage artifacts once so the ~5
|
|
342
|
+
# min openTSNE fit doesn't get repeated per video. Skip the
|
|
343
|
+
# prebuild if the user explicitly asked to omit the nn stage, or
|
|
344
|
+
# if it's a single-video run (no batching benefit).
|
|
345
|
+
nn_artifacts = None
|
|
346
|
+
nn_will_run = (
|
|
347
|
+
args.stages is None or "nn" in args.stages
|
|
348
|
+
)
|
|
349
|
+
if batch_mode and nn_will_run:
|
|
350
|
+
try:
|
|
351
|
+
from .nn_postprocess import prepare_nn_artifacts
|
|
352
|
+
if not args.quiet:
|
|
353
|
+
print(
|
|
354
|
+
"[marm_behavior] batch mode: building shared "
|
|
355
|
+
"nn artifacts (~5 min openTSNE fit, done once "
|
|
356
|
+
"for all videos)"
|
|
357
|
+
)
|
|
358
|
+
nn_artifacts = prepare_nn_artifacts(
|
|
359
|
+
reference_dir=args.nn_reference_dir,
|
|
360
|
+
verbose=not args.quiet,
|
|
361
|
+
)
|
|
362
|
+
except Exception as err:
|
|
363
|
+
# If artifact prep fails we don't want to take the whole
|
|
364
|
+
# batch down — fall back to per-video prep, which will
|
|
365
|
+
# surface the same error per video and let the per-video
|
|
366
|
+
# try/except below handle it.
|
|
367
|
+
print(
|
|
368
|
+
f"warning: failed to prebuild nn artifacts "
|
|
369
|
+
f"({type(err).__name__}: {err}); falling back to "
|
|
370
|
+
f"per-video setup",
|
|
371
|
+
file=sys.stderr,
|
|
372
|
+
)
|
|
373
|
+
nn_artifacts = None
|
|
374
|
+
|
|
375
|
+
for i, video in enumerate(videos, start=1):
|
|
376
|
+
if batch_mode and not args.quiet:
|
|
377
|
+
print(
|
|
378
|
+
f"[marm_behavior] === [{i}/{len(videos)}] "
|
|
379
|
+
f"{video.name} ==="
|
|
380
|
+
)
|
|
381
|
+
try:
|
|
382
|
+
result = run(
|
|
383
|
+
video_path=video,
|
|
384
|
+
single_csv_path=args.single_csv if not batch_mode else None,
|
|
385
|
+
multi_csv_path=args.multi_csv if not batch_mode else None,
|
|
386
|
+
output_dir=args.output_dir,
|
|
387
|
+
ground_normalized_path=args.ground_normalized,
|
|
388
|
+
dlc_config=args.dlc_config,
|
|
389
|
+
nn_reference_dir=args.nn_reference_dir,
|
|
390
|
+
nn_artifacts=nn_artifacts,
|
|
391
|
+
nn_buddies=nn_buddies,
|
|
392
|
+
red_present=args.red_present,
|
|
393
|
+
white_present=args.white_present,
|
|
394
|
+
blue_present=args.blue_present,
|
|
395
|
+
yellow_present=args.yellow_present,
|
|
396
|
+
c_thresh=args.c_thresh,
|
|
397
|
+
target_coverage=args.target_coverage,
|
|
398
|
+
stages=args.stages,
|
|
399
|
+
verbose=not args.quiet,
|
|
400
|
+
)
|
|
401
|
+
except FileNotFoundError as err:
|
|
402
|
+
print(f"error: {video.name}: {err}", file=sys.stderr)
|
|
403
|
+
failures.append((video, f"FileNotFoundError: {err}"))
|
|
404
|
+
if not batch_mode:
|
|
405
|
+
return 2
|
|
406
|
+
continue
|
|
407
|
+
except Exception as err: # pragma: no cover
|
|
408
|
+
print(
|
|
409
|
+
f"error: {video.name}: {type(err).__name__}: {err}",
|
|
410
|
+
file=sys.stderr,
|
|
411
|
+
)
|
|
412
|
+
failures.append((video, f"{type(err).__name__}: {err}"))
|
|
413
|
+
if not batch_mode:
|
|
414
|
+
return 1
|
|
415
|
+
continue
|
|
416
|
+
|
|
417
|
+
_print_video_summary(result, args.quiet)
|
|
418
|
+
successes.append(video)
|
|
419
|
+
|
|
420
|
+
if batch_mode and not args.quiet:
|
|
421
|
+
print()
|
|
422
|
+
print(
|
|
423
|
+
f"[marm_behavior] batch done: "
|
|
424
|
+
f"{len(successes)}/{len(videos)} succeeded"
|
|
425
|
+
)
|
|
426
|
+
if failures:
|
|
427
|
+
print("failures:")
|
|
428
|
+
for v, reason in failures:
|
|
429
|
+
print(f" - {v.name}: {reason}")
|
|
430
|
+
|
|
431
|
+
return 0 if not failures else 1
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
if __name__ == "__main__":
|
|
435
|
+
raise SystemExit(main())
|