mirror of
https://github.com/huggingface/lerobot.git
synced 2026-07-26 11:16:00 +00:00
189 lines
5.6 KiB
Python
189 lines
5.6 KiB
Python
#!/usr/bin/env python
|
|
|
|
# Copyright 2026 The HuggingFace Inc. team. All rights reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
from __future__ import annotations
|
|
|
|
import shutil
|
|
import threading
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
import pytest
|
|
from filelock import FileLock
|
|
|
|
from lerobot.streaming.manifest import EpisodeVideoManifest, VideoFileRecord
|
|
from lerobot.streaming.mp4 import Mp4Index
|
|
from lerobot.streaming.sidecar import (
|
|
SidecarLockTimeoutError,
|
|
SidecarSpec,
|
|
ensure_mp4_sidecar,
|
|
sidecar_cache_path,
|
|
)
|
|
|
|
|
|
def _record(path: str = "videos/camera/chunk-000/file-000.mp4", size: int = 128) -> VideoFileRecord:
|
|
arrays = np.array([0], dtype=np.int64)
|
|
index = Mp4Index(
|
|
file_path=path,
|
|
file_size=size,
|
|
ftyp=b"",
|
|
moov_offset=0,
|
|
mdat_offset=0,
|
|
mdat_payload_offset=0,
|
|
mdat_payload_size=size,
|
|
faststart=True,
|
|
codec="avc1",
|
|
timescale=1,
|
|
duration=1,
|
|
track_id=1,
|
|
width=1,
|
|
height=1,
|
|
stsd_body=b"",
|
|
sample_pts=np.array([0.0]),
|
|
sample_durations=arrays,
|
|
sample_sizes=arrays,
|
|
sample_offsets=arrays,
|
|
sync_samples=arrays,
|
|
)
|
|
return VideoFileRecord(path, size, index)
|
|
|
|
|
|
def _spec(revision: str = "rev-a", size: int = 128) -> SidecarSpec:
|
|
return SidecarSpec(
|
|
repo_id="owner/dataset",
|
|
revision=revision,
|
|
data_root="hf://datasets/owner/dataset",
|
|
source_files=(("videos/camera/chunk-000/file-000.mp4", size),),
|
|
)
|
|
|
|
|
|
def _write_valid(path: Path, spec: SidecarSpec) -> None:
|
|
EpisodeVideoManifest.save_file_sidecar(path, [_record(size=spec.source_files[0][1])], spec=spec)
|
|
|
|
|
|
def test_sidecar_cache_path_is_revision_keyed(tmp_path: Path) -> None:
|
|
first = sidecar_cache_path(tmp_path, _spec("rev-a"))
|
|
second = sidecar_cache_path(tmp_path, _spec("rev-b"))
|
|
|
|
assert first != second
|
|
assert first.parent == second.parent
|
|
|
|
|
|
def test_ensure_reuses_valid_local_sidecar(tmp_path: Path) -> None:
|
|
spec = _spec()
|
|
path = sidecar_cache_path(tmp_path, spec)
|
|
_write_valid(path, spec)
|
|
build_calls = 0
|
|
|
|
def build(_path: Path, _spec: SidecarSpec) -> None:
|
|
nonlocal build_calls
|
|
build_calls += 1
|
|
|
|
resolved = ensure_mp4_sidecar(spec, tmp_path, build=build)
|
|
|
|
assert resolved == path
|
|
assert build_calls == 0
|
|
|
|
|
|
def test_ensure_downloads_valid_published_sidecar(tmp_path: Path) -> None:
|
|
spec = _spec()
|
|
published = tmp_path / "published.npz"
|
|
_write_valid(published, spec)
|
|
build_calls = 0
|
|
|
|
def download(path: Path, _spec: SidecarSpec) -> bool:
|
|
shutil.copyfile(published, path)
|
|
return True
|
|
|
|
def build(_path: Path, _spec: SidecarSpec) -> None:
|
|
nonlocal build_calls
|
|
build_calls += 1
|
|
|
|
resolved = ensure_mp4_sidecar(spec, tmp_path / "cache", build=build, download=download)
|
|
|
|
assert EpisodeVideoManifest.validate_file_sidecar(resolved, spec)
|
|
assert build_calls == 0
|
|
|
|
|
|
@pytest.mark.parametrize("invalid_kind", ["corrupt", "stale"])
|
|
def test_ensure_rebuilds_invalid_local_sidecar(tmp_path: Path, invalid_kind: str) -> None:
|
|
spec = _spec()
|
|
path = sidecar_cache_path(tmp_path, spec)
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
if invalid_kind == "corrupt":
|
|
path.write_bytes(b"not-an-npz")
|
|
else:
|
|
_write_valid(path, _spec(revision="other-revision"))
|
|
build_calls = 0
|
|
|
|
def build(target: Path, target_spec: SidecarSpec) -> None:
|
|
nonlocal build_calls
|
|
build_calls += 1
|
|
_write_valid(target, target_spec)
|
|
|
|
resolved = ensure_mp4_sidecar(spec, tmp_path, build=build)
|
|
|
|
assert EpisodeVideoManifest.validate_file_sidecar(resolved, spec)
|
|
assert build_calls == 1
|
|
|
|
|
|
def test_concurrent_ensure_builds_once(tmp_path: Path) -> None:
|
|
spec = _spec()
|
|
start = threading.Barrier(2)
|
|
build_calls = 0
|
|
build_lock = threading.Lock()
|
|
|
|
def build(path: Path, target_spec: SidecarSpec) -> None:
|
|
nonlocal build_calls
|
|
with build_lock:
|
|
build_calls += 1
|
|
_write_valid(path, target_spec)
|
|
|
|
def ensure() -> Path:
|
|
start.wait()
|
|
return ensure_mp4_sidecar(spec, tmp_path, build=build)
|
|
|
|
with ThreadPoolExecutor(max_workers=2) as pool:
|
|
paths = list(pool.map(lambda _: ensure(), range(2)))
|
|
|
|
assert paths[0] == paths[1]
|
|
assert build_calls == 1
|
|
|
|
|
|
def test_failed_build_does_not_replace_existing_file(tmp_path: Path) -> None:
|
|
spec = _spec()
|
|
path = sidecar_cache_path(tmp_path, spec)
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_bytes(b"old-corrupt-file")
|
|
|
|
def build(target: Path, _spec: SidecarSpec) -> None:
|
|
target.write_bytes(b"partial")
|
|
raise RuntimeError("build failed")
|
|
|
|
with pytest.raises(RuntimeError, match="build failed"):
|
|
ensure_mp4_sidecar(spec, tmp_path, build=build)
|
|
|
|
assert path.read_bytes() == b"old-corrupt-file"
|
|
assert not list(path.parent.glob(f".{path.name}.*.tmp.npz"))
|
|
|
|
|
|
def test_lock_timeout_is_actionable(tmp_path: Path) -> None:
|
|
spec = _spec()
|
|
path = sidecar_cache_path(tmp_path, spec)
|
|
lock_path = path.with_suffix(f"{path.suffix}.lock")
|
|
lock_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
with (
|
|
FileLock(lock_path),
|
|
pytest.raises(SidecarLockTimeoutError, match="Timed out waiting"),
|
|
):
|
|
ensure_mp4_sidecar(spec, tmp_path, build=_write_valid, lock_timeout_s=0.01)
|