dexverse-cli 0.0.2__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.
- dexverse/__init__.py +54 -0
- dexverse/cli.py +422 -0
- dexverse/client.py +128 -0
- dexverse/config.py +85 -0
- dexverse/convert/__init__.py +38 -0
- dexverse/convert/hdf5_to_raw.py +498 -0
- dexverse/convert/image_video_encode.py +449 -0
- dexverse/downloader/__init__.py +26 -0
- dexverse/downloader/base.py +101 -0
- dexverse/downloader/http_backend.py +508 -0
- dexverse/downloader/manager.py +365 -0
- dexverse/downloader/python_s3_backend.py +137 -0
- dexverse/downloader/s5cmd_backend.py +170 -0
- dexverse/downloader/state.py +263 -0
- dexverse/downloader/verify.py +114 -0
- dexverse/manifest.py +284 -0
- dexverse/mock/__init__.py +2 -0
- dexverse/mock/mock_tasks.json +22 -0
- dexverse/mock/server.py +211 -0
- dexverse_cli-0.0.2.dist-info/METADATA +198 -0
- dexverse_cli-0.0.2.dist-info/RECORD +23 -0
- dexverse_cli-0.0.2.dist-info/WHEEL +4 -0
- dexverse_cli-0.0.2.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
# ----------------------------------------------------------------------------
|
|
2
|
+
# Copyright (c) 2021-2025 DexForce Technology Co., Ltd.
|
|
3
|
+
#
|
|
4
|
+
# All rights reserved.
|
|
5
|
+
# ----------------------------------------------------------------------------
|
|
6
|
+
"""image_video_encode.py — RGB / depth 图像与视频(MP4 / AVI)编解码工具。
|
|
7
|
+
|
|
8
|
+
由 ``dexverse.convert.hdf5_to_raw`` 复用,提供把 MP4 字节流解码回 RGB 帧的
|
|
9
|
+
能力(``decode_mp4_bytes_to_rgb_images``)。
|
|
10
|
+
"""
|
|
11
|
+
import numpy as np
|
|
12
|
+
from PIL import Image
|
|
13
|
+
from typing import List, Optional
|
|
14
|
+
import av
|
|
15
|
+
from fractions import Fraction
|
|
16
|
+
import io
|
|
17
|
+
import os
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
import logging
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
# List of hardware encoders to probe for auto-selection. Availability depends on the platform and FFmpeg build.
|
|
24
|
+
# Determines the order of preference for auto-selection when vcodec="auto" is used.
|
|
25
|
+
HW_ENCODERS = [
|
|
26
|
+
"h264_videotoolbox", # macOS
|
|
27
|
+
"hevc_videotoolbox", # macOS
|
|
28
|
+
"h264_nvenc", # NVIDIA GPU
|
|
29
|
+
"hevc_nvenc", # NVIDIA GPU
|
|
30
|
+
"h264_vaapi", # Linux Intel/AMD
|
|
31
|
+
"h264_qsv", # Intel Quick Sync
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
VALID_VIDEO_CODECS = {"h264", "hevc", "libsvtav1", "auto"} | set(HW_ENCODERS)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _get_codec_options(
|
|
38
|
+
vcodec: str,
|
|
39
|
+
g: Optional[int] = 2,
|
|
40
|
+
crf: Optional[int] = 30,
|
|
41
|
+
preset: Optional[int] = None,
|
|
42
|
+
) -> dict:
|
|
43
|
+
"""Build codec-specific options dict for video encoding."""
|
|
44
|
+
options = {}
|
|
45
|
+
|
|
46
|
+
# GOP size (keyframe interval) - supported by VideoToolbox and software encoders
|
|
47
|
+
if g is not None and (vcodec in ("h264_videotoolbox", "hevc_videotoolbox") or vcodec not in HW_ENCODERS):
|
|
48
|
+
options["g"] = str(g)
|
|
49
|
+
|
|
50
|
+
# Quality control (codec-specific parameter names)
|
|
51
|
+
if crf is not None:
|
|
52
|
+
if vcodec in ("h264", "hevc", "libsvtav1"):
|
|
53
|
+
options["crf"] = str(crf)
|
|
54
|
+
elif vcodec in ("h264_videotoolbox", "hevc_videotoolbox"):
|
|
55
|
+
quality = max(1, min(100, int(100 - crf * 2)))
|
|
56
|
+
options["q:v"] = str(quality)
|
|
57
|
+
elif vcodec in ("h264_nvenc", "hevc_nvenc"):
|
|
58
|
+
options["rc"] = "constqp"
|
|
59
|
+
options["qp"] = str(crf)
|
|
60
|
+
elif vcodec in ("h264_vaapi",):
|
|
61
|
+
options["qp"] = str(crf)
|
|
62
|
+
elif vcodec in ("h264_qsv",):
|
|
63
|
+
options["global_quality"] = str(crf)
|
|
64
|
+
|
|
65
|
+
# Preset (only for libsvtav1)
|
|
66
|
+
if vcodec == "libsvtav1":
|
|
67
|
+
options["preset"] = str(preset) if preset is not None else "12"
|
|
68
|
+
|
|
69
|
+
return options
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def detect_available_hw_encoders() -> List[str]:
|
|
73
|
+
"""Probe PyAV/FFmpeg for available hardware video encoders."""
|
|
74
|
+
available = []
|
|
75
|
+
for codec_name in HW_ENCODERS:
|
|
76
|
+
try:
|
|
77
|
+
av.codec.Codec(codec_name, "w")
|
|
78
|
+
available.append(codec_name)
|
|
79
|
+
except Exception: # nosec B110
|
|
80
|
+
logger.debug("HW encoder '%s' not available", codec_name) # nosec B110
|
|
81
|
+
return available
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def resolve_vcodec(vcodec: str) -> str:
|
|
85
|
+
"""Validate vcodec and resolve 'auto' to best available HW encoder, fallback to libsvtav1."""
|
|
86
|
+
if vcodec not in VALID_VIDEO_CODECS:
|
|
87
|
+
raise ValueError(f"Invalid vcodec '{vcodec}'. Must be one of: {sorted(VALID_VIDEO_CODECS)}")
|
|
88
|
+
if vcodec != "auto":
|
|
89
|
+
logger.info(f"Using video codec: {vcodec}")
|
|
90
|
+
return vcodec
|
|
91
|
+
available = detect_available_hw_encoders()
|
|
92
|
+
for encoder in HW_ENCODERS:
|
|
93
|
+
if encoder in available:
|
|
94
|
+
logger.info(f"Auto-selected video codec: {encoder}")
|
|
95
|
+
return encoder
|
|
96
|
+
logger.info("No hardware encoder available, falling back to software encoder 'libsvtav1'")
|
|
97
|
+
return "libsvtav1"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def encode_rgb_images_as_mp4_bytes_impl(
|
|
101
|
+
images: List['Image.Image'],
|
|
102
|
+
fps: int = 30,
|
|
103
|
+
vcodec: str = "libsvtav1",
|
|
104
|
+
pix_fmt: str = "yuv420p",
|
|
105
|
+
g: Optional[int] = 2,
|
|
106
|
+
crf: Optional[int] = 30,
|
|
107
|
+
fast_decode: int = 0,
|
|
108
|
+
log_level: Optional[int] = av.logging.WARNING,
|
|
109
|
+
preset: Optional[int] = None
|
|
110
|
+
) -> Optional[bytes]:
|
|
111
|
+
if not images:
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
# Resolve vcodec
|
|
115
|
+
vcodec = resolve_vcodec(vcodec)
|
|
116
|
+
|
|
117
|
+
# Get image size from the first image
|
|
118
|
+
width, height = images[0].size
|
|
119
|
+
|
|
120
|
+
# Encoders/pixel formats incompatibility check
|
|
121
|
+
if (vcodec == "libsvtav1" or vcodec == "hevc") and pix_fmt == "yuv444p":
|
|
122
|
+
logger.warning(
|
|
123
|
+
f"Incompatible pixel format 'yuv444p' for codec {vcodec}, auto-selecting format 'yuv420p'"
|
|
124
|
+
)
|
|
125
|
+
pix_fmt = "yuv420p"
|
|
126
|
+
|
|
127
|
+
# Define video codec options
|
|
128
|
+
video_options = _get_codec_options(vcodec, g, crf, preset)
|
|
129
|
+
|
|
130
|
+
if fast_decode:
|
|
131
|
+
key = "svtav1-params" if vcodec == "libsvtav1" else "tune"
|
|
132
|
+
value = f"fast-decode={fast_decode}" if vcodec == "libsvtav1" else "fastdecode"
|
|
133
|
+
video_options[key] = value
|
|
134
|
+
|
|
135
|
+
# Set logging level
|
|
136
|
+
if log_level is not None:
|
|
137
|
+
logging.getLogger("libav").setLevel(log_level)
|
|
138
|
+
|
|
139
|
+
# Create a BytesIO object to hold the MP4 data
|
|
140
|
+
output_buffer = io.BytesIO()
|
|
141
|
+
|
|
142
|
+
# Create container with the BytesIO object
|
|
143
|
+
container = av.open(output_buffer, 'w', format='mp4')
|
|
144
|
+
|
|
145
|
+
# Add video stream
|
|
146
|
+
stream = container.add_stream(vcodec, rate=fps, options=video_options)
|
|
147
|
+
stream.width = width
|
|
148
|
+
stream.height = height
|
|
149
|
+
stream.pix_fmt = pix_fmt
|
|
150
|
+
stream.time_base = Fraction(1, fps)
|
|
151
|
+
|
|
152
|
+
# Encode frames
|
|
153
|
+
for i, img in enumerate(images):
|
|
154
|
+
# Convert PIL Image to AV VideoFrame
|
|
155
|
+
frame = av.VideoFrame.from_image(img.convert('RGB'))
|
|
156
|
+
frame.pts = i
|
|
157
|
+
frame.time_base = Fraction(1, fps)
|
|
158
|
+
|
|
159
|
+
# Encode frame
|
|
160
|
+
for packet in stream.encode(frame):
|
|
161
|
+
container.mux(packet)
|
|
162
|
+
|
|
163
|
+
# Flush encoder
|
|
164
|
+
for packet in stream.encode():
|
|
165
|
+
container.mux(packet)
|
|
166
|
+
|
|
167
|
+
# Reset logging level
|
|
168
|
+
if log_level is not None:
|
|
169
|
+
av.logging.restore_default_callback()
|
|
170
|
+
|
|
171
|
+
# Close container to ensure all data is written
|
|
172
|
+
container.close()
|
|
173
|
+
|
|
174
|
+
# Get the bytes from the buffer
|
|
175
|
+
output_buffer.seek(0)
|
|
176
|
+
output_bytes = output_buffer.getvalue()
|
|
177
|
+
|
|
178
|
+
return output_bytes
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@dataclass(frozen=True)
|
|
182
|
+
class EncodeImageIntoMp4Option(object):
|
|
183
|
+
fps: int = 30
|
|
184
|
+
vcodec: str = "libsvtav1"
|
|
185
|
+
pix_fmt: str = "yuv420p"
|
|
186
|
+
g: Optional[int] = 2
|
|
187
|
+
crf: Optional[int] = 30
|
|
188
|
+
fast_decode: int = 0
|
|
189
|
+
log_level: Optional[int] = av.logging.WARNING
|
|
190
|
+
preset: Optional[int] = None
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def encode_rgb_images_as_mp4_bytes(
|
|
194
|
+
images: List['Image.Image'],
|
|
195
|
+
option_in: Optional[EncodeImageIntoMp4Option] = None
|
|
196
|
+
) -> Optional[bytes]:
|
|
197
|
+
"""Encode a list of RGB images into MP4 bytes.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
images: List of PIL Image objects to encode.
|
|
201
|
+
option_in: Optional EncodeImageIntoMp4Option object with encoding parameters.
|
|
202
|
+
If None, default values will be used.
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
MP4-encoded bytes, or None if no images were provided.
|
|
206
|
+
"""
|
|
207
|
+
if option_in is None:
|
|
208
|
+
option_in = EncodeImageIntoMp4Option()
|
|
209
|
+
|
|
210
|
+
return encode_rgb_images_as_mp4_bytes_impl(
|
|
211
|
+
images=images,
|
|
212
|
+
fps=option_in.fps,
|
|
213
|
+
vcodec=option_in.vcodec,
|
|
214
|
+
pix_fmt=option_in.pix_fmt,
|
|
215
|
+
g=option_in.g,
|
|
216
|
+
crf=option_in.crf,
|
|
217
|
+
fast_decode=option_in.fast_decode,
|
|
218
|
+
log_level=option_in.log_level,
|
|
219
|
+
preset=option_in.preset
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def decode_mp4_bytes_to_rgb_images(encoded_bytes: bytes) -> List['Image.Image']:
|
|
224
|
+
# Create a BytesIO object from the encoded bytes
|
|
225
|
+
input_buffer = io.BytesIO(encoded_bytes)
|
|
226
|
+
|
|
227
|
+
# Open the buffer as a container
|
|
228
|
+
container = av.open(input_buffer, 'r')
|
|
229
|
+
|
|
230
|
+
# Iterate through frames
|
|
231
|
+
images: List['Image.Image'] = list()
|
|
232
|
+
for frame in container.decode(video=0):
|
|
233
|
+
# Convert AV VideoFrame to PIL Image
|
|
234
|
+
img = frame.to_image()
|
|
235
|
+
images.append(img)
|
|
236
|
+
|
|
237
|
+
# Done
|
|
238
|
+
container.close()
|
|
239
|
+
return images
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def encode_depth_images_as_avi_bytes_impl(
|
|
243
|
+
images: List[np.ndarray],
|
|
244
|
+
fps: int = 30,
|
|
245
|
+
vcodec: str = "ffv1",
|
|
246
|
+
pix_fmt: str = "gray16le",
|
|
247
|
+
log_level: Optional[int] = av.logging.WARNING,
|
|
248
|
+
) -> Optional[bytes]:
|
|
249
|
+
"""Encode a list of uint16 depth images into a video bytes.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
images: List of uint16 numpy arrays (depth images).
|
|
253
|
+
fps: Frames per second for the video.
|
|
254
|
+
vcodec: Video codec to use. Default is 'ffv1' (lossless).
|
|
255
|
+
pix_fmt: Pixel format. Default is 'gray16le' (16-bit grayscale, little-endian).
|
|
256
|
+
log_level: PyAV logging level.
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
Encoded video bytes, or None if no images were provided.
|
|
260
|
+
"""
|
|
261
|
+
if images is None or len(images) == 0:
|
|
262
|
+
return None
|
|
263
|
+
|
|
264
|
+
# Check data type
|
|
265
|
+
for img in images:
|
|
266
|
+
if img.dtype != np.uint16:
|
|
267
|
+
return None
|
|
268
|
+
|
|
269
|
+
# Input checked
|
|
270
|
+
height, width = images[0].shape
|
|
271
|
+
if log_level is not None:
|
|
272
|
+
logging.getLogger("libav").setLevel(log_level)
|
|
273
|
+
|
|
274
|
+
output_buffer = io.BytesIO()
|
|
275
|
+
container = av.open(output_buffer, 'w', format='avi')
|
|
276
|
+
|
|
277
|
+
stream = container.add_stream(vcodec, rate=fps)
|
|
278
|
+
stream.width = width
|
|
279
|
+
stream.height = height
|
|
280
|
+
stream.pix_fmt = pix_fmt
|
|
281
|
+
|
|
282
|
+
for i, img in enumerate(images):
|
|
283
|
+
assert img.dtype == np.uint16
|
|
284
|
+
frame = av.VideoFrame.from_ndarray(img, format=pix_fmt)
|
|
285
|
+
frame.pts = i
|
|
286
|
+
frame.time_base = Fraction(1, fps)
|
|
287
|
+
|
|
288
|
+
for packet in stream.encode(frame):
|
|
289
|
+
container.mux(packet)
|
|
290
|
+
|
|
291
|
+
for packet in stream.encode():
|
|
292
|
+
container.mux(packet)
|
|
293
|
+
|
|
294
|
+
if log_level is not None:
|
|
295
|
+
av.logging.restore_default_callback()
|
|
296
|
+
|
|
297
|
+
container.close()
|
|
298
|
+
output_buffer.seek(0)
|
|
299
|
+
return output_buffer.getvalue()
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
@dataclass(frozen=True)
|
|
303
|
+
class EncodeDepthImageIntoAVIOption(object):
|
|
304
|
+
fps: int = 30
|
|
305
|
+
vcodec: str = "ffv1"
|
|
306
|
+
pix_fmt: str = "gray16le"
|
|
307
|
+
log_level: Optional[int] = av.logging.WARNING
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def encode_depth_images_as_avi_bytes(
|
|
311
|
+
images: List[np.ndarray],
|
|
312
|
+
option_in: Optional[EncodeDepthImageIntoAVIOption] = None) -> Optional[bytes]:
|
|
313
|
+
"""Encode a list of depht images into avi bytes.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
images: List of depth images objects to encode.
|
|
317
|
+
option_in: Optional EncodeDepthImageIntoAVIOption object with encoding parameters.
|
|
318
|
+
If None, default values will be used.
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
avi-encoded bytes, or None if no images were provided.
|
|
322
|
+
"""
|
|
323
|
+
if option_in is None:
|
|
324
|
+
option_in = EncodeDepthImageIntoAVIOption()
|
|
325
|
+
return encode_depth_images_as_avi_bytes_impl(images=images, fps=option_in.fps, vcodec=option_in.vcodec,
|
|
326
|
+
pix_fmt=option_in.pix_fmt, log_level=option_in.log_level)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def encode_metric_depth_images_as_avi_bytes(
|
|
330
|
+
images: List[np.ndarray],
|
|
331
|
+
option_in: Optional[EncodeDepthImageIntoAVIOption] = None) -> Optional[bytes]:
|
|
332
|
+
# Convert the input
|
|
333
|
+
depth_images_uint16: List[np.ndarray] = list()
|
|
334
|
+
for img in images:
|
|
335
|
+
if img.dtype == np.float32:
|
|
336
|
+
img_mm = img * 1000.0
|
|
337
|
+
depth_images_uint16.append(img_mm.astype(np.uint16))
|
|
338
|
+
elif img.dtype == np.uint16:
|
|
339
|
+
depth_images_uint16.append(img)
|
|
340
|
+
else:
|
|
341
|
+
return None
|
|
342
|
+
|
|
343
|
+
return encode_depth_images_as_avi_bytes(depth_images_uint16, option_in)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def decode_avi_bytes_to_depth_images(
|
|
347
|
+
encoded_bytes: bytes,
|
|
348
|
+
pix_fmt: str = "gray16le",
|
|
349
|
+
) -> List[np.ndarray]:
|
|
350
|
+
"""Decode video bytes back to uint16 depth images.
|
|
351
|
+
|
|
352
|
+
Args:
|
|
353
|
+
encoded_bytes: Encoded video bytes.
|
|
354
|
+
pix_fmt: Pixel format used during encoding. Default is 'gray16le'.
|
|
355
|
+
|
|
356
|
+
Returns:
|
|
357
|
+
List of uint16 numpy arrays (depth images).
|
|
358
|
+
"""
|
|
359
|
+
# Make input buffer
|
|
360
|
+
input_buffer = io.BytesIO(encoded_bytes)
|
|
361
|
+
container = av.open(input_buffer, 'r')
|
|
362
|
+
|
|
363
|
+
# Load the images
|
|
364
|
+
images: List[np.ndarray] = []
|
|
365
|
+
for frame in container.decode(video=0):
|
|
366
|
+
img = frame.to_ndarray(format=pix_fmt)
|
|
367
|
+
images.append(img)
|
|
368
|
+
|
|
369
|
+
# Done
|
|
370
|
+
container.close()
|
|
371
|
+
return images
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def encode_rgb_image_path_into_mp4(
|
|
375
|
+
image_path: List[str],
|
|
376
|
+
mp4_output_path: str,
|
|
377
|
+
option_in: Optional[EncodeImageIntoMp4Option] = None):
|
|
378
|
+
"""Encode images from file paths into an MP4 file.
|
|
379
|
+
|
|
380
|
+
Args:
|
|
381
|
+
image_path: List of paths to image files.
|
|
382
|
+
mp4_output_path: Path to save the output MP4 file.
|
|
383
|
+
option_in: Optional EncodeImageIntoMp4Option object with encoding parameters.
|
|
384
|
+
If None, default values will be used.
|
|
385
|
+
"""
|
|
386
|
+
# Read images from file paths
|
|
387
|
+
images = []
|
|
388
|
+
for path in image_path:
|
|
389
|
+
with Image.open(path) as img:
|
|
390
|
+
images.append(img.convert('RGB'))
|
|
391
|
+
|
|
392
|
+
# Encode images to MP4 bytes
|
|
393
|
+
encoded_bytes = encode_rgb_images_as_mp4_bytes(images, option_in)
|
|
394
|
+
|
|
395
|
+
if encoded_bytes is None:
|
|
396
|
+
logger.warning("No images to encode")
|
|
397
|
+
return
|
|
398
|
+
|
|
399
|
+
# Create the output directory if it doesn't exist
|
|
400
|
+
output_dir = os.path.dirname(mp4_output_path)
|
|
401
|
+
if output_dir:
|
|
402
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
403
|
+
|
|
404
|
+
# Write the MP4 bytes to file
|
|
405
|
+
with open(mp4_output_path, 'wb') as f:
|
|
406
|
+
f.write(encoded_bytes)
|
|
407
|
+
|
|
408
|
+
logger.info(f"Encoded {len(image_path)} images into {mp4_output_path}")
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def decode_mp4_file_into_images_directory(mp4_file_path: str, save_image_dir: str):
|
|
412
|
+
"""Decode an MP4 file into a directory of images.
|
|
413
|
+
|
|
414
|
+
Args:
|
|
415
|
+
mp4_file_path: Path to the MP4 file to decode.
|
|
416
|
+
save_image_dir: Directory to save the decoded images.
|
|
417
|
+
"""
|
|
418
|
+
# Create the save directory if it doesn't exist
|
|
419
|
+
os.makedirs(save_image_dir, exist_ok=True)
|
|
420
|
+
|
|
421
|
+
# Read the MP4 file as bytes
|
|
422
|
+
with open(mp4_file_path, 'rb') as f:
|
|
423
|
+
mp4_bytes = f.read()
|
|
424
|
+
|
|
425
|
+
# Decode the MP4 bytes to images
|
|
426
|
+
images = decode_mp4_bytes_to_rgb_images(mp4_bytes)
|
|
427
|
+
|
|
428
|
+
# Save each image with a sequential filename
|
|
429
|
+
for i, img in enumerate(images):
|
|
430
|
+
img_path = os.path.join(save_image_dir, f"frame_{i:06d}.png")
|
|
431
|
+
img.save(img_path)
|
|
432
|
+
|
|
433
|
+
# Done
|
|
434
|
+
logger.info(f"Decoded MP4 file {mp4_file_path} into {save_image_dir}")
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
__all__ = [
|
|
438
|
+
"encode_rgb_images_as_mp4_bytes_impl",
|
|
439
|
+
"EncodeImageIntoMp4Option",
|
|
440
|
+
"encode_rgb_images_as_mp4_bytes",
|
|
441
|
+
"decode_mp4_bytes_to_rgb_images",
|
|
442
|
+
"encode_depth_images_as_avi_bytes_impl",
|
|
443
|
+
"EncodeDepthImageIntoAVIOption",
|
|
444
|
+
"encode_depth_images_as_avi_bytes",
|
|
445
|
+
"encode_metric_depth_images_as_avi_bytes",
|
|
446
|
+
"decode_avi_bytes_to_depth_images",
|
|
447
|
+
"encode_rgb_image_path_into_mp4",
|
|
448
|
+
"decode_mp4_file_into_images_directory",
|
|
449
|
+
]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Download core: manager, backends, verify, state."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from .manager import DownloadManager, DownloadReport, get_backend, repair
|
|
5
|
+
from .base import Backend, Credentials, FileResult
|
|
6
|
+
from .s5cmd_backend import S5CmdBackend, S5CmdNotAvailable, generate_s5cmd_lines
|
|
7
|
+
from .python_s3_backend import PythonS3Backend
|
|
8
|
+
from .http_backend import HttpBackend
|
|
9
|
+
from .verify import verify_files, summarize
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"DownloadManager",
|
|
13
|
+
"DownloadReport",
|
|
14
|
+
"get_backend",
|
|
15
|
+
"repair",
|
|
16
|
+
"Backend",
|
|
17
|
+
"Credentials",
|
|
18
|
+
"FileResult",
|
|
19
|
+
"S5CmdBackend",
|
|
20
|
+
"S5CmdNotAvailable",
|
|
21
|
+
"generate_s5cmd_lines",
|
|
22
|
+
"PythonS3Backend",
|
|
23
|
+
"HttpBackend",
|
|
24
|
+
"verify_files",
|
|
25
|
+
"summarize",
|
|
26
|
+
]
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Backend contract + shared credential resolution.
|
|
2
|
+
|
|
3
|
+
A *backend* takes the manifest, the subset of files that still need fetching,
|
|
4
|
+
the out root, and returns a per-object success/failure outcome. The download
|
|
5
|
+
manager owns skip/resume/verify; backends only move bytes.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Dict, List, Optional, Protocol
|
|
13
|
+
|
|
14
|
+
from ..manifest import FileSpec, Manifest
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class Credentials:
|
|
19
|
+
access_key: Optional[str]
|
|
20
|
+
secret_key: Optional[str]
|
|
21
|
+
session_token: Optional[str]
|
|
22
|
+
region: Optional[str]
|
|
23
|
+
endpoint: Optional[str]
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def from_manifest(cls, manifest: Manifest) -> "Credentials":
|
|
27
|
+
"""Prefer manifest credentials; fall back to AWS_* env vars."""
|
|
28
|
+
storage = manifest.storage
|
|
29
|
+
endpoint = storage.endpoint if storage else None
|
|
30
|
+
region = (storage.region if storage else None) or os.getenv("AWS_REGION") \
|
|
31
|
+
or os.getenv("AWS_DEFAULT_REGION") or "us-east-1"
|
|
32
|
+
|
|
33
|
+
ak = sk = st = None
|
|
34
|
+
if storage and storage.credentials and storage.credentials.is_populated():
|
|
35
|
+
ak = storage.credentials.accessKey
|
|
36
|
+
sk = storage.credentials.secretKey
|
|
37
|
+
st = storage.credentials.sessionToken or None
|
|
38
|
+
else:
|
|
39
|
+
ak = os.getenv("AWS_ACCESS_KEY_ID")
|
|
40
|
+
sk = os.getenv("AWS_SECRET_ACCESS_KEY")
|
|
41
|
+
st = os.getenv("AWS_SESSION_TOKEN") or None
|
|
42
|
+
|
|
43
|
+
return cls(
|
|
44
|
+
access_key=ak, secret_key=sk, session_token=st,
|
|
45
|
+
region=region, endpoint=endpoint,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def as_env(self) -> Dict[str, str]:
|
|
49
|
+
"""A dict suitable for merging into a subprocess environment."""
|
|
50
|
+
env: Dict[str, str] = {}
|
|
51
|
+
if self.access_key:
|
|
52
|
+
env["AWS_ACCESS_KEY_ID"] = self.access_key
|
|
53
|
+
if self.secret_key:
|
|
54
|
+
env["AWS_SECRET_ACCESS_KEY"] = self.secret_key
|
|
55
|
+
if self.session_token:
|
|
56
|
+
env["AWS_SESSION_TOKEN"] = self.session_token
|
|
57
|
+
if self.region:
|
|
58
|
+
env["AWS_REGION"] = self.region
|
|
59
|
+
env["AWS_DEFAULT_REGION"] = self.region
|
|
60
|
+
return env
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class FileResult:
|
|
65
|
+
objectKey: str
|
|
66
|
+
ok: bool
|
|
67
|
+
error: Optional[str] = None
|
|
68
|
+
localSize: Optional[int] = None
|
|
69
|
+
expectedSize: Optional[int] = None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class ProgressReporter(Protocol):
|
|
73
|
+
"""Optional byte-progress sink supplied by interactive callers."""
|
|
74
|
+
|
|
75
|
+
def start(self, *, total_bytes: Optional[int], total_files: int) -> None:
|
|
76
|
+
...
|
|
77
|
+
|
|
78
|
+
def advance(self, bytes_count: int) -> None:
|
|
79
|
+
...
|
|
80
|
+
|
|
81
|
+
def complete_file(self) -> None:
|
|
82
|
+
...
|
|
83
|
+
|
|
84
|
+
def finish(self) -> None:
|
|
85
|
+
...
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class Backend(Protocol):
|
|
89
|
+
name: str
|
|
90
|
+
|
|
91
|
+
def download(
|
|
92
|
+
self,
|
|
93
|
+
manifest: Manifest,
|
|
94
|
+
files: List[FileSpec],
|
|
95
|
+
out: Path,
|
|
96
|
+
*,
|
|
97
|
+
parallel: int,
|
|
98
|
+
log_path: Optional[Path] = None,
|
|
99
|
+
progress: Optional[ProgressReporter] = None,
|
|
100
|
+
) -> Dict[str, FileResult]:
|
|
101
|
+
...
|