Spaces:
Running on Zero
Running on Zero
File size: 13,685 Bytes
2001557 c36db5c d346bdc 4c983fd d346bdc c36db5c d346bdc fd37678 c36db5c 2001557 c36db5c 2001557 c36db5c 2001557 c36db5c 2001557 c36db5c 2001557 c36db5c 2001557 c36db5c 2001557 9d01809 2001557 d346bdc 4c983fd c57dcae d346bdc 9d01809 2001557 9d01809 2001557 9d01809 4c983fd 326f401 4c983fd 326f401 2001557 326f401 2001557 4c983fd 326f401 4c983fd 2001557 4c983fd 2001557 326f401 2001557 4c983fd 2001557 4c983fd 2001557 4c983fd 326f401 9d01809 2001557 9d01809 2001557 9d01809 c36db5c fd37678 c36db5c 2001557 fd37678 982b332 fd37678 d346bdc 2001557 d346bdc df58091 d346bdc df58091 d346bdc df58091 d346bdc df58091 d346bdc df58091 d346bdc df58091 d346bdc df58091 d346bdc b84a490 9ae2cb2 b84a490 9ae2cb2 db4dfa4 9ae2cb2 db4dfa4 b84a490 c57dcae 9ae2cb2 d346bdc 2001557 d346bdc | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 | """Unit tests for streaming helpers and the Gradio fork link. No GPU or weights."""
from __future__ import annotations
import os
import pickle
import threading
import traceback
import uuid
from collections.abc import Callable
from fractions import Fraction
from pathlib import Path
import numpy as np
import pytest
import rerun as rr
import rerun.blueprint as rrb
import torch
os.environ.setdefault("FDANYONE_SKIP_LOAD", "1")
from fdanyone.config import INFERENCE # noqa: E402
from fdanyone.errors import FourDAnyoneError # noqa: E402
from fdanyone.model.inference import _tensor_frames # noqa: E402
import fdanyone.rerun_streaming as streaming # noqa: E402
import fdanyone.viz as viz # noqa: E402
import fdanyone_app # noqa: E402
EXAMPLE: Path = Path(__file__).resolve().parents[1] / "examples" / "jump-rope.mp4"
def test_preview_slice_plan_covers_every_fourth_latent_frame() -> None:
plan: tuple[tuple[int, int], ...] = streaming.preview_slice_plan(31)
assert plan == (
(0, 0),
(4, 16),
(8, 32),
(12, 48),
(16, 64),
(20, 80),
(24, 96),
(28, 112),
)
# Eight previews per view, and none past the frozen 121-frame contract.
assert len(plan) == 8
assert max(source for _, source in plan) < INFERENCE.num_frames
def test_preview_slice_plan_rejects_empty_latents() -> None:
with pytest.raises(ValueError):
streaming.preview_slice_plan(0)
def test_tensor_frames_maps_the_signed_range_onto_uint8() -> None:
"""The preview reuses the pipeline's own truncation, so pin its endpoints."""
video: torch.Tensor = torch.tensor([-1.0, 0.0, 1.0]).reshape(3, 1, 1, 1).expand(3, 1, 1, 1)
frame: np.ndarray = next(iter(_tensor_frames(video)))
assert frame.dtype == np.uint8
assert frame.shape == (1, 1, 3)
assert frame.reshape(3).tolist() == [0, 127, 255]
def test_probe_clip_accepts_the_bundled_example() -> None:
info: streaming.ClipInfo = streaming.probe_clip(EXAMPLE, 0.0)
assert info.fps > 0
assert info.duration_seconds >= info.required_seconds
assert info.required_seconds == pytest.approx(float(Fraction(120, 1) / info.fps))
def test_probe_clip_rejects_a_start_time_past_the_end() -> None:
with pytest.raises(FourDAnyoneError, match="need"):
streaming.probe_clip(EXAMPLE, 3600.0)
def test_probe_clip_rejects_a_missing_file(tmp_path: Path) -> None:
with pytest.raises(FourDAnyoneError, match="does not exist"):
streaming.probe_clip(tmp_path / "absent.mp4", 0.0)
def _pump_session(data_dir: Path = Path("/nonexistent")) -> streaming.Session:
"""A Session with only the fields ``pump`` touches."""
return streaming.Session(
streaming.RunSpec(
token="0" * 32,
video_path=EXAMPLE,
start_time=0.0,
seed=0,
fps=Fraction(25, 1),
data_dir=data_dir,
created=0.0,
)
)
def test_pump_yields_hook_labels_and_returns_the_work_result() -> None:
session: streaming.Session = _pump_session()
def work() -> str:
for label in ("bboxes", "smplx"):
session.events.put(label)
return "prepared"
pump = streaming.pump(session, work)
seen: list[str] = []
while True:
try:
seen.append(next(pump))
except StopIteration as done:
assert done.value == "prepared"
break
assert seen == ["bboxes", "smplx"]
def test_stop_sentinel_unwinds_a_running_pump_worker(tmp_path: Path) -> None:
"""Stop must reach the pipeline thread itself, not just Gradio's generator.
``cancels`` closes the request generator, but the worker thread (or the
forked ZeroGPU child) never hears it; the sentinel checked by the hooks is
the only thing that actually frees the GPU. The barrier makes the order
deterministic: the worker is provably mid-run when Stop arrives.
"""
assert streaming.request_stop(None) == "Nothing is running."
session: streaming.Session = _pump_session(tmp_path)
stop_requested: threading.Event = threading.Event()
def work() -> str:
# A pipeline hook checks the sentinel once per stage; model the stage
# boundary the run is inside when the user presses Stop.
streaming.check_stop(session.spec)
session.events.put("running")
assert stop_requested.wait(timeout=5.0)
streaming.check_stop(session.spec)
return "finished"
pump = streaming.pump(session, work)
assert next(pump) == "running"
streaming.request_stop(session.spec)
assert (tmp_path / "stop-requested").exists()
stop_requested.set()
with pytest.raises(streaming.RunCancelled):
list(pump)
def test_pump_reraises_a_worker_failure_on_the_caller_thread() -> None:
"""A pipeline error must surface where Gradio can turn it into a message."""
session: streaming.Session = _pump_session()
def work() -> str:
raise FourDAnyoneError("boom")
with pytest.raises(FourDAnyoneError, match="boom"):
list(streaming.pump(session, work))
def test_blueprints_build_for_every_phase() -> None:
"""Blueprints expose every phase in the approved camera-centered layout."""
assert streaming.APPLICATION_ID == "4danyone-rerun-v3"
assert viz.APPLICATION_ID == "4danyone-rerun-v3"
motion: rrb.Blueprint = viz.motion_blueprint()
source: rrb.Spatial2DView = motion.root_container.contents[0] # pyrefly: ignore
detections: rrb.Spatial2DView = motion.root_container.contents[1] # pyrefly: ignore
assert (source.origin, source.contents) == ("world/camera", ["$origin/video"])
assert detections.origin == "world/camera"
assert viz.diffusion_blueprint() is not None
body: rrb.Blueprint = viz.body_blueprint()
body_world: rrb.Spatial3DView = body.root_container.contents[0] # pyrefly: ignore
body_source: rrb.Spatial2DView = body.root_container.contents[1] # pyrefly: ignore
assert (body_world.origin, body_world.name) == ("world", "Canonical world")
assert body_source.contents == ["$origin/**", "/world/body/**"]
assert body.time_panel.play_state == "playing"
result: rrb.Blueprint = viz.result_blueprint(Fraction(25, 1))
top: rrb.Horizontal = result.root_container.contents[0] # pyrefly: ignore
dense: rrb.Horizontal = result.root_container.contents[1] # pyrefly: ignore
world: rrb.Spatial3DView = top.contents[0] # pyrefly: ignore
result_source: rrb.Spatial2DView = top.contents[1] # pyrefly: ignore
assert result.root_container.row_shares == [2.0, 1.0]
assert top.column_shares == [3.0, 1.0]
assert (world.origin, world.name) == ("world", "Canonical world")
assert (result_source.origin, result_source.contents) == (
"world/camera",
["$origin/**", "/world/body/**"],
)
assert type(dense).__name__ == "Horizontal"
assert tuple(view.origin for view in dense.contents) == tuple(
f"world/cameras/dense/{index:02d}/image" for index in range(viz.VIEWS)
)
assert result.time_panel.play_state == "playing"
assert result.time_panel.loop_mode == "all"
# ---------------------------------------------------------------------------
# Fork safety — the one thing that only misbehaves on ZeroGPU
# ---------------------------------------------------------------------------
OK: int = 0
"""Child exit code meaning the behaviour under test held."""
WRONG: int = 3
"""Child exit code meaning it did not; distinct from any interpreter failure."""
def _in_child(body: Callable[[], int]) -> int:
"""Run ``body`` in a forked child and return its exit code.
``os._exit`` skips every atexit handler and any pytest teardown the child
inherited, so only what ``body`` decides reaches the parent.
"""
pid: int = os.fork()
if pid == 0:
try:
code: int = body()
except BaseException: # noqa: BLE001 - a traceback here is the diagnosis
traceback.print_exc()
code = WRONG
os._exit(code)
_, status = os.waitpid(pid, 0)
return os.waitstatus_to_exitcode(status)
def test_a_recording_made_before_a_fork_cannot_be_flushed_after_it() -> None:
"""Pin the ZeroGPU failure the per-link recording exists to avoid.
``@spaces.GPU`` runs its callback in a forked child, so a recording opened
by an earlier link is a parent's recording. The SDK notices the pid change
and refuses: "Fork detected during flush". Everything ran fine locally
before this was understood, because ``spaces`` never forks off a Space.
"""
recording = rr.RecordingStream(streaming.APPLICATION_ID, recording_id=uuid.uuid4().hex)
stream = recording.binary_stream()
recording.log("log", rr.TextLog("parent"))
assert stream.read()
def body() -> int:
recording.log("log", rr.TextLog("child"))
try:
stream.read()
except RuntimeError as exc:
return OK if "fork" in str(exc).lower() else WRONG
return WRONG
assert _in_child(body) == OK, (
"A pre-fork recording flushed cleanly from the child. If the Rerun SDK "
"now supports this, the per-link recording is merely unnecessary, not wrong."
)
def test_open_link_gives_a_forked_child_a_recording_it_can_flush(tmp_path: Path) -> None:
"""The fix: each link builds its own stream, in whatever process it runs in.
The token is the ``recording_id``, so the parent's rows and the child's are
one recording to the viewer even though no object crossed the fork.
"""
token: str = uuid.uuid4().hex
parent: fdanyone_app.Link = fdanyone_app.open_link(token, tmp_path / "parent.rrd")
parent.recording.log("log", rr.TextLog("parent"))
assert parent.read()
parent.close()
def body() -> int:
child: fdanyone_app.Link = fdanyone_app.open_link(token, tmp_path / "child.rrd")
assert child.recording.get_recording_id() == token
child.recording.log("log", rr.TextLog("child"))
payload: bytes | None = child.read()
return OK if payload else WRONG
assert _in_child(body) == OK
# The dual sink teed the parent's rows into its part file as well.
assert (tmp_path / "parent.rrd").stat().st_size > 0
RRD_MAGIC: bytes = b"RRF2"
"""First four bytes of every RRD document the SDK emits."""
def test_a_stream_per_link_adds_no_framing_the_viewer_did_not_already_get(tmp_path: Path) -> None:
"""Why splitting one stream into four costs the browser viewer nothing.
Each ``read`` already returns a whole RRD document, magic bytes and manifest
included, so what the app sends the viewer has always been a concatenation
of documents rather than one long one. Opening a stream per link only
changes how many of them carry the same store id. If this ever stops
holding — if a later read became a bare continuation of the first — then
per-link streams would start sending headers mid-stream, and this test is
where that shows up rather than in a blank viewer on the Space.
"""
link: fdanyone_app.Link = fdanyone_app.open_link(uuid.uuid4().hex, tmp_path / "part.rrd")
for row in range(3):
link.recording.log("log", rr.TextLog(f"row {row}"))
payload: bytes | None = link.read()
assert payload is not None
assert payload.startswith(RRD_MAGIC), f"read {row} is not a self-contained document"
def test_coco17_annotation_context_matches_the_vitpose_layout() -> None:
"""The context carries simplecv's COCO-17 body joints and their links."""
from simplecv.data.skeleton.coco_133 import COCO_17_IDX, COCO_133_ID2NAME
assert len(COCO_17_IDX) == 17
assert COCO_133_ID2NAME[0] == "nose"
assert COCO_133_ID2NAME[16] == "right_ankle"
context: rr.AnnotationContext = viz.coco17_annotation_context()
assert context is not None
def test_format_status_counts_known_stages_and_passes_failures_through() -> None:
"""The banner shows x/x plus elapsed for the fixed run shape, nothing else."""
spec: streaming.RunSpec = streaming.new_spec(EXAMPLE, 0.0, 0)
first: str = streaming.format_status(spec, streaming.STAGES[0])
assert first.startswith(f"{streaming.STAGES[0]} (1/{len(streaming.STAGES)}")
last: str = streaming.format_status(spec, streaming.STAGES[-1])
assert f"({len(streaming.STAGES)}/{len(streaming.STAGES)}" in last
assert streaming.format_status(spec, "Failed: boom") == "Failed: boom"
def test_stage_sequence_is_complete_and_unique() -> None:
"""One count per stage: every emitted label indexes, none twice.
``STAGES`` is assembled from the same registries the hooks emit from, so
this pins the whole contract: 19 distinct stages, every registry label
present, and the denoise labels formatted by the shared helper.
"""
assert len(streaming.STAGES) == 19
assert len(set(streaming.STAGES)) == len(streaming.STAGES)
for label in streaming.MOTION_LABELS.values():
assert label in streaming.STAGE_INDEX
for label in streaming.GENERATE_LABELS.values():
assert label in streaming.STAGE_INDEX
for step in range(1, streaming.SETTINGS.num_inference_steps + 1):
assert streaming.denoise_step_label(step) in streaming.STAGE_INDEX
def test_a_run_spec_survives_the_pickle_into_a_zerogpu_worker() -> None:
"""Arguments are all a GPU callback gets; unpicklable state never arrives."""
spec: streaming.RunSpec = streaming.new_spec(EXAMPLE, 0.0, 7)
restored: streaming.RunSpec = pickle.loads(pickle.dumps(spec))
assert restored == spec
assert restored.data_dir.name == spec.token
|