reshot 0.3.7__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.
- reshot/__init__.py +58 -0
- reshot/_version.py +1 -0
- reshot/backends/__init__.py +23 -0
- reshot/backends/base.py +34 -0
- reshot/backends/fake.py +28 -0
- reshot/backends/vda.py +140 -0
- reshot/cli.py +169 -0
- reshot/config.py +62 -0
- reshot/errors.py +45 -0
- reshot/io.py +196 -0
- reshot/pipeline.py +435 -0
- reshot/planning.py +81 -0
- reshot/postprocess.py +73 -0
- reshot/py.typed +0 -0
- reshot/reporter.py +27 -0
- reshot/targets.py +53 -0
- reshot/third_party/__init__.py +0 -0
- reshot/third_party/video_depth_anything/LICENSE +201 -0
- reshot/third_party/video_depth_anything/__init__.py +0 -0
- reshot/third_party/video_depth_anything/dinov2.py +415 -0
- reshot/third_party/video_depth_anything/dinov2_layers/__init__.py +11 -0
- reshot/third_party/video_depth_anything/dinov2_layers/attention.py +83 -0
- reshot/third_party/video_depth_anything/dinov2_layers/block.py +252 -0
- reshot/third_party/video_depth_anything/dinov2_layers/drop_path.py +35 -0
- reshot/third_party/video_depth_anything/dinov2_layers/layer_scale.py +28 -0
- reshot/third_party/video_depth_anything/dinov2_layers/mlp.py +41 -0
- reshot/third_party/video_depth_anything/dinov2_layers/patch_embed.py +89 -0
- reshot/third_party/video_depth_anything/dinov2_layers/swiglu_ffn.py +63 -0
- reshot/third_party/video_depth_anything/dpt.py +160 -0
- reshot/third_party/video_depth_anything/dpt_temporal.py +125 -0
- reshot/third_party/video_depth_anything/motion_module/__init__.py +0 -0
- reshot/third_party/video_depth_anything/motion_module/attention.py +429 -0
- reshot/third_party/video_depth_anything/motion_module/motion_module.py +321 -0
- reshot/third_party/video_depth_anything/util/__init__.py +0 -0
- reshot/third_party/video_depth_anything/util/align.py +74 -0
- reshot/third_party/video_depth_anything/util/blocks.py +162 -0
- reshot/third_party/video_depth_anything/util/transform.py +158 -0
- reshot/third_party/video_depth_anything/video_depth.py +163 -0
- reshot/third_party/video_depth_anything/video_depth_stream.py +161 -0
- reshot-0.3.7.dist-info/METADATA +62 -0
- reshot-0.3.7.dist-info/RECORD +46 -0
- reshot-0.3.7.dist-info/WHEEL +5 -0
- reshot-0.3.7.dist-info/entry_points.txt +2 -0
- reshot-0.3.7.dist-info/licenses/LICENSE +202 -0
- reshot-0.3.7.dist-info/licenses/NOTICE +12 -0
- reshot-0.3.7.dist-info/top_level.txt +1 -0
reshot/__init__.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""ReShot — turn any video into a depth-map video for video-generation control.
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from reshot import RunConfig, run
|
|
5
|
+
run(RunConfig(input=Path("in.mp4"), output=Path("out.mp4"), target="h3"))
|
|
6
|
+
|
|
7
|
+
from reshot import extract, to_gray, write_gray_video
|
|
8
|
+
depths, fps = extract("in.mp4") # float32 [T, H, W], larger = closer
|
|
9
|
+
write_gray_video(to_gray(depths), "out.mp4", fps)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
|
|
16
|
+
# Apple Silicon: cap the MPS allocator at 60 % of the recommended working set. Unified
|
|
17
|
+
# memory means GPU allocations count against the same RAM as everything else; without a
|
|
18
|
+
# cap a long clip grows until the OS kills processes (seen 2026-09-11). Must be set
|
|
19
|
+
# before torch is imported, which is why it lives here.
|
|
20
|
+
os.environ.setdefault("PYTORCH_MPS_HIGH_WATERMARK_RATIO", "0.6")
|
|
21
|
+
|
|
22
|
+
from ._version import __version__
|
|
23
|
+
from .config import RunConfig
|
|
24
|
+
from .errors import BackendError, InputError, RamBudgetError, ReshotError, ToolMissingError
|
|
25
|
+
from .io import probe_video, read_video, write_gray_video
|
|
26
|
+
from .pipeline import Plan, RunResult, extract, model_input_resolution, plan, resolve_input_size, run, run_many
|
|
27
|
+
from .planning import memory_verdict, processing_max_res
|
|
28
|
+
from .postprocess import to_gray, upsample_frames
|
|
29
|
+
from .targets import TARGETS, Target, center_crop, fit_dimensions
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"TARGETS",
|
|
33
|
+
"BackendError",
|
|
34
|
+
"InputError",
|
|
35
|
+
"Plan",
|
|
36
|
+
"RamBudgetError",
|
|
37
|
+
"ReshotError",
|
|
38
|
+
"RunConfig",
|
|
39
|
+
"RunResult",
|
|
40
|
+
"Target",
|
|
41
|
+
"ToolMissingError",
|
|
42
|
+
"__version__",
|
|
43
|
+
"center_crop",
|
|
44
|
+
"extract",
|
|
45
|
+
"fit_dimensions",
|
|
46
|
+
"memory_verdict",
|
|
47
|
+
"model_input_resolution",
|
|
48
|
+
"plan",
|
|
49
|
+
"probe_video",
|
|
50
|
+
"processing_max_res",
|
|
51
|
+
"read_video",
|
|
52
|
+
"resolve_input_size",
|
|
53
|
+
"run",
|
|
54
|
+
"run_many",
|
|
55
|
+
"to_gray",
|
|
56
|
+
"upsample_frames",
|
|
57
|
+
"write_gray_video",
|
|
58
|
+
]
|
reshot/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.7"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Backend registry. `get_backend("vda", model="small", device="auto")`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .base import BackendInfo, DepthBackend
|
|
8
|
+
from .fake import FakeBackend
|
|
9
|
+
from .vda import VDABackend, pick_device
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_backend(name: str, **kwargs: Any) -> DepthBackend:
|
|
13
|
+
"""Instantiate a backend by name. Unknown names raise `BackendError`."""
|
|
14
|
+
from ..errors import BackendError
|
|
15
|
+
|
|
16
|
+
if name == "vda":
|
|
17
|
+
return VDABackend(**kwargs)
|
|
18
|
+
if name == "fake":
|
|
19
|
+
return FakeBackend()
|
|
20
|
+
raise BackendError(f"unknown backend {name!r}")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
__all__ = ["BackendInfo", "DepthBackend", "FakeBackend", "VDABackend", "get_backend", "pick_device"]
|
reshot/backends/base.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Backend protocol: a depth model that turns a frame stack into per-frame depth.
|
|
2
|
+
|
|
3
|
+
Every backend returns *relative inverse depth* (larger = closer to camera) as float32
|
|
4
|
+
`[T, H, W]` at the input frame resolution, already temporally aligned across the whole
|
|
5
|
+
clip. Normalisation to 8-bit grey happens later in :mod:`reshot.postprocess`,
|
|
6
|
+
so backends never decide what "white" means.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Protocol
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class BackendInfo:
|
|
19
|
+
name: str
|
|
20
|
+
variant: str
|
|
21
|
+
license: str
|
|
22
|
+
commercial_ok: bool
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class DepthBackend(Protocol):
|
|
26
|
+
info: BackendInfo
|
|
27
|
+
|
|
28
|
+
def infer(self, frames: np.ndarray, fps: float, *, input_size: int = 518) -> np.ndarray:
|
|
29
|
+
"""`frames`: uint8 RGB `[T, H, W, 3]` → float32 inverse depth `[T, H, W]`."""
|
|
30
|
+
...
|
|
31
|
+
|
|
32
|
+
def peak_memory_bytes(self) -> dict[str, int]:
|
|
33
|
+
"""Accelerator memory peak of the last `infer()`; `{}` when there is none to report."""
|
|
34
|
+
...
|
reshot/backends/fake.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""A backend that returns a plausible depth field without loading a model.
|
|
2
|
+
|
|
3
|
+
For exercising everything *around* inference — I/O, encoding, presets, metrics,
|
|
4
|
+
remote job runners — in CI or on a laptop, in milliseconds and without downloads.
|
|
5
|
+
Select with `--backend fake` (or the env var `RESHOT_FAKE_BACKEND=1`)."""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
from .base import BackendInfo
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class FakeBackend:
|
|
15
|
+
info = BackendInfo(name="fake", variant="none", license="n/a", commercial_ok=True)
|
|
16
|
+
fp32 = True
|
|
17
|
+
device = "cpu"
|
|
18
|
+
|
|
19
|
+
def infer(self, frames: np.ndarray, fps: float, *, input_size: int = 518) -> np.ndarray:
|
|
20
|
+
t, h, w = frames.shape[:3]
|
|
21
|
+
yy, xx = np.mgrid[0:h, 0:w].astype(np.float32)
|
|
22
|
+
# a soft radial "subject" in the middle that brightens over time
|
|
23
|
+
base = 1.0 / (1.0 + ((xx - w / 2) ** 2 + (yy - h / 2) ** 2) / (0.15 * w * h))
|
|
24
|
+
ramp = np.linspace(0.8, 1.0, num=max(t, 1), dtype=np.float32)
|
|
25
|
+
return (base[None] * ramp[:, None, None]).astype(np.float32)
|
|
26
|
+
|
|
27
|
+
def peak_memory_bytes(self) -> dict[str, int]:
|
|
28
|
+
return {}
|
reshot/backends/vda.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Video Depth Anything backend (default).
|
|
2
|
+
|
|
3
|
+
Why this model: it is the only open video-depth model that ships a temporal module
|
|
4
|
+
*and* an Apache-2.0 checkpoint (Small). Per-frame models (Depth Anything V2/3) flicker
|
|
5
|
+
on video unless you bolt on your own smoothing; diffusion models (DepthCrafter) are
|
|
6
|
+
~100× slower. VDA-Small at 28M params runs on an 8 GB consumer GPU or an Apple M-series.
|
|
7
|
+
|
|
8
|
+
Precision policy (measured 2026-09-11 on M2 Max, 32 frames @ 736×1280):
|
|
9
|
+
mps fp32 → 7.0 s mps fp16 → did not finish in 3 min (autocast pathological)
|
|
10
|
+
cpu fp32 → 59 s
|
|
11
|
+
Full 294-frame clip end-to-end on mps fp32: 147 s (~500 ms/frame incl. alignment).
|
|
12
|
+
So fp16 is enabled on CUDA only. Do not "optimise" this by turning fp16 on for MPS.
|
|
13
|
+
|
|
14
|
+
Licence policy: `small` is Apache-2.0. `base`/`large` are CC-BY-NC-4.0 — loading them
|
|
15
|
+
prints a warning and sets ``info.commercial_ok = False``; the CLI surfaces this.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import logging
|
|
21
|
+
import warnings
|
|
22
|
+
|
|
23
|
+
import numpy as np
|
|
24
|
+
import torch
|
|
25
|
+
|
|
26
|
+
from .base import BackendInfo
|
|
27
|
+
|
|
28
|
+
log = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
_VARIANTS = {
|
|
31
|
+
"small": {"encoder": "vits", "features": 64, "out_channels": [48, 96, 192, 384]},
|
|
32
|
+
"base": {"encoder": "vitb", "features": 128, "out_channels": [96, 192, 384, 768]},
|
|
33
|
+
"large": {"encoder": "vitl", "features": 256, "out_channels": [256, 512, 1024, 1024]},
|
|
34
|
+
}
|
|
35
|
+
_LICENSES = {"small": "Apache-2.0", "base": "CC-BY-NC-4.0", "large": "CC-BY-NC-4.0"}
|
|
36
|
+
_HF_REPOS = {
|
|
37
|
+
"small": "depth-anything/Video-Depth-Anything-Small",
|
|
38
|
+
"base": "depth-anything/Video-Depth-Anything-Base",
|
|
39
|
+
"large": "depth-anything/Video-Depth-Anything-Large",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def pick_device(requested: str = "auto") -> str:
|
|
44
|
+
if requested != "auto":
|
|
45
|
+
return requested
|
|
46
|
+
if torch.cuda.is_available():
|
|
47
|
+
return "cuda"
|
|
48
|
+
if getattr(torch.backends, "mps", None) and torch.backends.mps.is_available():
|
|
49
|
+
return "mps"
|
|
50
|
+
return "cpu"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _weights_path(variant: str, checkpoint: str | None) -> str:
|
|
54
|
+
if checkpoint:
|
|
55
|
+
return checkpoint
|
|
56
|
+
# huggingface_hub honours HF_ENDPOINT, so users in China can point it at a mirror
|
|
57
|
+
# (e.g. https://hf-mirror.com) without us special-casing anything.
|
|
58
|
+
from huggingface_hub import hf_hub_download
|
|
59
|
+
|
|
60
|
+
filename = f"video_depth_anything_{_VARIANTS[variant]['encoder']}.pth"
|
|
61
|
+
return hf_hub_download(_HF_REPOS[variant], filename)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class VDABackend:
|
|
65
|
+
def __init__(
|
|
66
|
+
self,
|
|
67
|
+
model: str = "small",
|
|
68
|
+
device: str = "auto",
|
|
69
|
+
checkpoint: str | None = None,
|
|
70
|
+
cuda_memory_fraction: float | None = None,
|
|
71
|
+
) -> None:
|
|
72
|
+
variant = model
|
|
73
|
+
if variant not in _VARIANTS:
|
|
74
|
+
from ..errors import BackendError
|
|
75
|
+
|
|
76
|
+
raise BackendError(f"unknown model variant {variant!r}; choose from {sorted(_VARIANTS)}")
|
|
77
|
+
from ..third_party.video_depth_anything.video_depth import VideoDepthAnything
|
|
78
|
+
|
|
79
|
+
self.variant = variant
|
|
80
|
+
self.device = pick_device(device)
|
|
81
|
+
# fp16 only where it is known to be fast; see module docstring for the measurement.
|
|
82
|
+
self.fp32 = self.device != "cuda"
|
|
83
|
+
self.info = BackendInfo(
|
|
84
|
+
name="video-depth-anything",
|
|
85
|
+
variant=variant,
|
|
86
|
+
license=_LICENSES[variant],
|
|
87
|
+
commercial_ok=_LICENSES[variant].startswith("Apache"),
|
|
88
|
+
)
|
|
89
|
+
if not self.info.commercial_ok:
|
|
90
|
+
warnings.warn(
|
|
91
|
+
f"Video Depth Anything '{variant}' weights are {_LICENSES[variant]}: "
|
|
92
|
+
"non-commercial use only. Use 'small' for commercial projects.",
|
|
93
|
+
stacklevel=2,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
path = _weights_path(variant, checkpoint)
|
|
98
|
+
except Exception as exc: # hub / network / offline-cache errors all land here
|
|
99
|
+
from ..errors import BackendError
|
|
100
|
+
|
|
101
|
+
raise BackendError(
|
|
102
|
+
f"could not obtain weights for '{variant}': {exc}. "
|
|
103
|
+
"Set HF_ENDPOINT to a mirror, or pass --checkpoint with a local .pth."
|
|
104
|
+
) from exc
|
|
105
|
+
if cuda_memory_fraction is not None and self.device == "cuda":
|
|
106
|
+
# Emulate a smaller card: allocations beyond the fraction raise OOM instead of
|
|
107
|
+
# silently using the whole GPU. Used to verify the "runs on N GB" claim.
|
|
108
|
+
torch.cuda.set_per_process_memory_fraction(cuda_memory_fraction)
|
|
109
|
+
log.info("cuda memory capped at %.0f%% of the card", cuda_memory_fraction * 100)
|
|
110
|
+
log.info("loading %s from %s on %s (fp32=%s)", variant, path, self.device, self.fp32)
|
|
111
|
+
model = VideoDepthAnything(**_VARIANTS[variant])
|
|
112
|
+
model.load_state_dict(torch.load(path, map_location="cpu", weights_only=True), strict=True)
|
|
113
|
+
self.model = model.to(self.device).eval()
|
|
114
|
+
|
|
115
|
+
def infer(self, frames: np.ndarray, fps: float, *, input_size: int = 518) -> np.ndarray:
|
|
116
|
+
if frames.ndim != 4 or frames.shape[-1] != 3:
|
|
117
|
+
raise ValueError(f"expected [T, H, W, 3] uint8 RGB, got {frames.shape}")
|
|
118
|
+
# Upstream's infer_video_depth already does 32-frame windows with 10-frame overlap,
|
|
119
|
+
# keyframe-based scale/shift alignment across windows and interpolation over the
|
|
120
|
+
# seam. That is the temporal-consistency machinery — do not re-chunk outside it.
|
|
121
|
+
if self.device == "cuda":
|
|
122
|
+
torch.cuda.reset_peak_memory_stats()
|
|
123
|
+
with torch.inference_mode(): # upstream uses no_grad; inference_mode also skips version counters
|
|
124
|
+
depths, _ = self.model.infer_video_depth(
|
|
125
|
+
frames, fps, input_size=input_size, device=self.device, fp32=self.fp32
|
|
126
|
+
)
|
|
127
|
+
return np.asarray(depths, dtype=np.float32)
|
|
128
|
+
|
|
129
|
+
def peak_memory_bytes(self) -> dict[str, int]:
|
|
130
|
+
"""Peak VRAM of the last `infer()`: what the model actually touched (`allocated`)
|
|
131
|
+
and what the caching allocator held (`reserved`, ≈ what nvidia-smi shows)."""
|
|
132
|
+
if self.device != "cuda":
|
|
133
|
+
return {}
|
|
134
|
+
return {
|
|
135
|
+
"gpu_peak_allocated_bytes": int(torch.cuda.max_memory_allocated()),
|
|
136
|
+
"gpu_peak_reserved_bytes": int(torch.cuda.max_memory_reserved()),
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
__all__ = ["VDABackend", "pick_device"]
|
reshot/cli.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""`reshot` command line: argv → RunConfig(s) → pipeline.run() / run_many().
|
|
2
|
+
|
|
3
|
+
reshot in.mp4 -o out.mp4 # Apache-2.0 Small model, auto device
|
|
4
|
+
reshot in.mp4 -o out.mp4 --target h3 # 24 fps, ×32 dims, ≤15 s for MiniMax H3
|
|
5
|
+
reshot in.mp4 -o out.mp4 --npz d.npz --metrics run.json
|
|
6
|
+
reshot clips/*.mp4 -o depth/ --target seedance # batch: one model load, out/<name>_depth.mp4
|
|
7
|
+
|
|
8
|
+
Batch mode (several inputs, or `-o` naming a directory): `--npz` / `--metrics`, if given,
|
|
9
|
+
are directories too and get `<name>.npz` / `<name>.json` per clip. One bad clip is
|
|
10
|
+
reported and skipped; the exit code is that of the first failure.
|
|
11
|
+
|
|
12
|
+
Exit codes: 0 ok · 1 unexpected · 2 bad input/arguments · 3 over RAM budget ·
|
|
13
|
+
4 ffmpeg missing · 5 backend/weights problem.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
import logging
|
|
20
|
+
import sys
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
from ._version import __version__
|
|
24
|
+
from .config import BACKENDS, MODEL_VARIANTS, QUALITIES, RunConfig
|
|
25
|
+
from .errors import ReshotError
|
|
26
|
+
from .pipeline import run, run_many
|
|
27
|
+
from .reporter import StderrReporter
|
|
28
|
+
from .targets import TARGETS
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
32
|
+
p = argparse.ArgumentParser(
|
|
33
|
+
prog="reshot",
|
|
34
|
+
description="Video → depth-map video for video-generation ControlNets.",
|
|
35
|
+
epilog="Docs: https://github.com/maosika-ai/reshot",
|
|
36
|
+
)
|
|
37
|
+
p.add_argument("input", type=Path, nargs="+", help="input video(s) (anything ffmpeg/OpenCV can read)")
|
|
38
|
+
p.add_argument(
|
|
39
|
+
"-o", "--output", type=Path, required=True, help="output .mp4, or a directory when there are several inputs"
|
|
40
|
+
)
|
|
41
|
+
g = p.add_argument_group("model")
|
|
42
|
+
g.add_argument(
|
|
43
|
+
"--model",
|
|
44
|
+
default="small",
|
|
45
|
+
choices=MODEL_VARIANTS,
|
|
46
|
+
help="Video Depth Anything variant; only 'small' is Apache-2.0 (default)",
|
|
47
|
+
)
|
|
48
|
+
g.add_argument("--backend", default="vda", choices=BACKENDS, help=argparse.SUPPRESS)
|
|
49
|
+
g.add_argument("--device", default="auto", help="auto | cuda | mps | cpu")
|
|
50
|
+
g.add_argument(
|
|
51
|
+
"--quality",
|
|
52
|
+
default="fast",
|
|
53
|
+
choices=QUALITIES,
|
|
54
|
+
help="what the model sees: fast = 644x364 for 16:9 / 364x644 for 9:16, ~3 GB VRAM (default); "
|
|
55
|
+
"full = 924x518 / 518x924, ~11 GB VRAM, 2.5x slower, sharper fine detail",
|
|
56
|
+
)
|
|
57
|
+
g.add_argument(
|
|
58
|
+
"--input-size",
|
|
59
|
+
type=int,
|
|
60
|
+
default=None,
|
|
61
|
+
help="expert: model short side in px, multiple of 14; overrides --quality",
|
|
62
|
+
)
|
|
63
|
+
g.add_argument("--checkpoint", type=Path, help="local .pth instead of the Hugging Face download")
|
|
64
|
+
g = p.add_argument_group("output")
|
|
65
|
+
g.add_argument(
|
|
66
|
+
"--target",
|
|
67
|
+
default="none",
|
|
68
|
+
choices=sorted(TARGETS),
|
|
69
|
+
help="fps / frame-size preset for a generator: " + ", ".join(f"{k}={v.note}" for k, v in TARGETS.items()),
|
|
70
|
+
)
|
|
71
|
+
g.add_argument("--fps", type=float, help="override output fps (default: preset or source)")
|
|
72
|
+
g.add_argument(
|
|
73
|
+
"--max-res", type=int, default=1280, help="cap the OUTPUT's longer side (inference runs at model resolution)"
|
|
74
|
+
)
|
|
75
|
+
g.add_argument("--max-frames", type=int, help="stop after N frames")
|
|
76
|
+
g.add_argument("--invert", action="store_true", help="far = white instead of near = white")
|
|
77
|
+
g.add_argument(
|
|
78
|
+
"--clip", type=float, default=0.0, metavar="PCT", help="percentile clip on both tails before scaling"
|
|
79
|
+
)
|
|
80
|
+
g.add_argument("--gamma", type=float, default=1.0, help=">1 darkens mid-tones")
|
|
81
|
+
g.add_argument("--crf", type=int, default=12, help="x264 CRF; lower = larger, cleaner (default 12)")
|
|
82
|
+
g.add_argument("--npz", type=Path, help="also save raw float depth (processing resolution)")
|
|
83
|
+
g.add_argument("--metrics", type=Path, help="write timing / memory / size metrics as JSON")
|
|
84
|
+
g = p.add_argument_group("safety")
|
|
85
|
+
g.add_argument("--force", action="store_true", help="run even if the RAM estimate exceeds the budget")
|
|
86
|
+
# Verification aid, not a user feature: makes a big card behave like a small one so a
|
|
87
|
+
# "runs on N GB" claim can be tested (torch.cuda.set_per_process_memory_fraction).
|
|
88
|
+
g.add_argument("--cuda-memory-fraction", type=float, default=None, help=argparse.SUPPRESS)
|
|
89
|
+
p.add_argument("-v", "--verbose", action="store_true")
|
|
90
|
+
p.add_argument("--version", action="version", version=f"reshot {__version__}")
|
|
91
|
+
return p
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _is_batch(args: argparse.Namespace) -> bool:
|
|
95
|
+
"""Several inputs, or an output that is (or is spelled like) a directory."""
|
|
96
|
+
o = args.output
|
|
97
|
+
return len(args.input) > 1 or o.is_dir() or str(o).endswith(("/", "\\"))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def configs_from_args(args: argparse.Namespace) -> list[RunConfig]:
|
|
101
|
+
"""One RunConfig per input. In batch mode the outputs are derived from the input names."""
|
|
102
|
+
batch = _is_batch(args)
|
|
103
|
+
|
|
104
|
+
def per_clip(option: Path | None, stem: str, ext: str) -> Path | None:
|
|
105
|
+
if option is None:
|
|
106
|
+
return None
|
|
107
|
+
return option / f"{stem}{ext}" if batch else option
|
|
108
|
+
|
|
109
|
+
cfgs = []
|
|
110
|
+
for src in args.input:
|
|
111
|
+
stem = src.stem
|
|
112
|
+
cfgs.append(
|
|
113
|
+
RunConfig(
|
|
114
|
+
input=src,
|
|
115
|
+
output=args.output / f"{stem}_depth.mp4" if batch else args.output,
|
|
116
|
+
model=args.model,
|
|
117
|
+
backend=args.backend,
|
|
118
|
+
device=args.device,
|
|
119
|
+
target=args.target,
|
|
120
|
+
fps=args.fps,
|
|
121
|
+
max_res=args.max_res,
|
|
122
|
+
max_frames=args.max_frames,
|
|
123
|
+
quality=args.quality,
|
|
124
|
+
input_size=args.input_size,
|
|
125
|
+
invert=args.invert,
|
|
126
|
+
clip_percent=args.clip,
|
|
127
|
+
gamma=args.gamma,
|
|
128
|
+
crf=args.crf,
|
|
129
|
+
npz=per_clip(args.npz, stem, ".npz"),
|
|
130
|
+
metrics=per_clip(args.metrics, stem, ".json"),
|
|
131
|
+
checkpoint=args.checkpoint,
|
|
132
|
+
force=args.force,
|
|
133
|
+
cuda_memory_fraction=args.cuda_memory_fraction,
|
|
134
|
+
)
|
|
135
|
+
)
|
|
136
|
+
return cfgs
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def config_from_args(args: argparse.Namespace) -> RunConfig:
|
|
140
|
+
"""Single-clip form, kept for callers that build a Namespace themselves."""
|
|
141
|
+
return configs_from_args(args)[0]
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def main(argv: list[str] | None = None) -> int:
|
|
145
|
+
args = build_parser().parse_args(argv)
|
|
146
|
+
logging.basicConfig(
|
|
147
|
+
level=logging.INFO if args.verbose else logging.WARNING, format="%(levelname)s %(name)s: %(message)s"
|
|
148
|
+
)
|
|
149
|
+
try:
|
|
150
|
+
cfgs = configs_from_args(args)
|
|
151
|
+
if len(cfgs) == 1 and not _is_batch(args):
|
|
152
|
+
run(cfgs[0], StderrReporter())
|
|
153
|
+
return 0
|
|
154
|
+
results = run_many(cfgs, StderrReporter())
|
|
155
|
+
failures = [r for r in results if isinstance(r, ReshotError)]
|
|
156
|
+
if failures:
|
|
157
|
+
print(f"reshot: {len(failures)} of {len(results)} clips failed", file=sys.stderr)
|
|
158
|
+
return failures[0].exit_code
|
|
159
|
+
return 0
|
|
160
|
+
except ReshotError as exc:
|
|
161
|
+
print(f"reshot: {exc}", file=sys.stderr)
|
|
162
|
+
return exc.exit_code
|
|
163
|
+
except KeyboardInterrupt:
|
|
164
|
+
print("reshot: interrupted", file=sys.stderr)
|
|
165
|
+
return 130
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
if __name__ == "__main__": # pragma: no cover
|
|
169
|
+
sys.exit(main())
|
reshot/config.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Everything a run needs, in one immutable object. Built by the CLI from argv, or by
|
|
2
|
+
library users directly. Validation that does not need I/O happens in `__post_init__`
|
|
3
|
+
so mistakes surface before any work starts."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from .errors import InputError
|
|
11
|
+
from .targets import TARGETS
|
|
12
|
+
|
|
13
|
+
MODEL_VARIANTS = ("small", "base", "large")
|
|
14
|
+
QUALITIES = ("fast", "full") # model working size: fast = 644×364 for 16:9 (default), full = 924×518
|
|
15
|
+
BACKENDS = ("vda", "fake")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class RunConfig:
|
|
20
|
+
input: Path
|
|
21
|
+
output: Path
|
|
22
|
+
model: str = "small"
|
|
23
|
+
backend: str = "vda"
|
|
24
|
+
device: str = "auto"
|
|
25
|
+
target: str = "none"
|
|
26
|
+
fps: float | None = None # None = preset or source
|
|
27
|
+
max_res: int = 1280 # cap on the OUTPUT's longer side
|
|
28
|
+
max_frames: int | None = None
|
|
29
|
+
quality: str = "fast" # fast | full — see QUALITIES; `input_size` overrides it
|
|
30
|
+
input_size: int | None = None # model short side (multiple of 14); expert override of `quality`
|
|
31
|
+
invert: bool = False
|
|
32
|
+
clip_percent: float = 0.0
|
|
33
|
+
gamma: float = 1.0
|
|
34
|
+
crf: int = 12
|
|
35
|
+
npz: Path | None = None
|
|
36
|
+
metrics: Path | None = None
|
|
37
|
+
checkpoint: Path | None = None
|
|
38
|
+
force: bool = False # ignore the RAM budget
|
|
39
|
+
cuda_memory_fraction: float | None = None # cap VRAM to this share of the card (verification aid)
|
|
40
|
+
extra: dict = field(default_factory=dict) # free-form, echoed into metrics
|
|
41
|
+
|
|
42
|
+
def __post_init__(self) -> None:
|
|
43
|
+
if self.target not in TARGETS:
|
|
44
|
+
raise InputError(f"unknown --target {self.target!r}; choose from {', '.join(sorted(TARGETS))}")
|
|
45
|
+
if self.model not in MODEL_VARIANTS:
|
|
46
|
+
raise InputError(f"unknown --model {self.model!r}; choose from {', '.join(MODEL_VARIANTS)}")
|
|
47
|
+
if self.backend not in BACKENDS:
|
|
48
|
+
raise InputError(f"unknown backend {self.backend!r}; choose from {', '.join(BACKENDS)}")
|
|
49
|
+
if self.quality not in QUALITIES:
|
|
50
|
+
raise InputError(f"unknown --quality {self.quality!r}; choose from {', '.join(QUALITIES)}")
|
|
51
|
+
if self.input_size is not None and (self.input_size < 14 or self.input_size % 14):
|
|
52
|
+
raise InputError("--input-size must be a positive multiple of 14 (ViT patch size)")
|
|
53
|
+
if not 0 <= self.clip_percent < 50:
|
|
54
|
+
raise InputError("--clip must be in [0, 50)")
|
|
55
|
+
if self.gamma <= 0:
|
|
56
|
+
raise InputError("--gamma must be > 0")
|
|
57
|
+
if not 0 <= self.crf <= 51:
|
|
58
|
+
raise InputError("--crf must be in [0, 51]")
|
|
59
|
+
if self.max_frames is not None and self.max_frames < 1:
|
|
60
|
+
raise InputError("--max-frames must be ≥ 1")
|
|
61
|
+
if self.cuda_memory_fraction is not None and not 0 < self.cuda_memory_fraction <= 1:
|
|
62
|
+
raise InputError("--cuda-memory-fraction must be in (0, 1]")
|
reshot/errors.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""User-facing errors. The CLI prints `str(exc)` and maps each to an exit code; library
|
|
2
|
+
users can catch `ReshotError` for anything the tool considers a *user* problem
|
|
3
|
+
(bad input, over budget, missing tool) as opposed to a bug."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ReshotError(Exception):
|
|
9
|
+
"""Base class; `exit_code` is what the CLI returns."""
|
|
10
|
+
|
|
11
|
+
exit_code = 1
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class InputError(ReshotError):
|
|
15
|
+
"""The input video could not be opened or decoded."""
|
|
16
|
+
|
|
17
|
+
exit_code = 2
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class RamBudgetError(ReshotError):
|
|
21
|
+
"""The planned run would exceed the host-RAM budget (see planning.py)."""
|
|
22
|
+
|
|
23
|
+
exit_code = 3
|
|
24
|
+
|
|
25
|
+
def __init__(self, estimate: int, budget: int, max_frames_ok: int) -> None:
|
|
26
|
+
self.estimate, self.budget, self.max_frames_ok = estimate, budget, max_frames_ok
|
|
27
|
+
from .planning import gib
|
|
28
|
+
|
|
29
|
+
super().__init__(
|
|
30
|
+
f"estimated host RAM {gib(estimate)} exceeds the budget {gib(budget)}. "
|
|
31
|
+
f"Try --max-frames {max_frames_ok} (or split the clip), a smaller --input-size, "
|
|
32
|
+
"or --force if you know the machine can take it."
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ToolMissingError(ReshotError):
|
|
37
|
+
"""A required external tool (ffmpeg) is missing."""
|
|
38
|
+
|
|
39
|
+
exit_code = 4
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class BackendError(ReshotError):
|
|
43
|
+
"""Unknown backend / model variant, or weights could not be loaded."""
|
|
44
|
+
|
|
45
|
+
exit_code = 5
|