refactor(lab): объединить RAV004 в единый Rerun replay

This commit is contained in:
DCCONSTRUCTIONS
2026-08-30 16:40:12 +03:00
parent 9c5259dbc9
commit 81fdf6904a
18 changed files with 750 additions and 186 deletions
+278 -12
View File
@@ -1,10 +1,12 @@
"""Native Rerun sidecar for immutable recorded laboratory evidence.
"""Native Rerun evidence for an immutable recorded laboratory replay.
The sidecar deliberately contains only evidence missing from the canonical K1
recording: camera video, semantic images and diagnostic 2D boxes. The base RRD
continues to own poses, point clouds and trajectory. Both files use the same
Rerun recording id and ``session_time`` timeline, so the upstream viewer is the
only playback clock and the only spatial renderer.
only playback clock and the only spatial renderer. The browser-facing LAB
artifact is a cached merge of both files, so the viewer opens one immutable
source instead of racing two independent HTTP receivers.
"""
from __future__ import annotations
@@ -31,9 +33,87 @@ from PIL import Image
APPLICATION_ID: Final = "nodedc_mission_core_recorded"
SESSION_TIMELINE: Final = "session_time"
RENDERER_VERSION: Final = "upstream-rerun-0.36.3-encoded-optimized-v4"
RENDERER_VERSION: Final = "upstream-rerun-0.36.3-encoded-optimized-v6"
REPLAY_RENDERER_VERSION: Final = "upstream-rerun-0.36.3-canonical-replay-v1"
MAX_SOURCE_BYTES: Final = 768 * 1024 * 1024
MAX_OVERLAY_BYTES: Final = 256 * 1024 * 1024
MAX_REPLAY_BYTES: Final = 1024 * 1024 * 1024
SEMANTIC_LABELS_RU: Final = {
"outside_valid_fov": "вне поля зрения",
"undefined": "не определено",
"person": "человек",
"bicycle": "велосипед",
"motorcycle": "мотоцикл",
"car": "автомобиль",
"heavy_vehicle": "тяжёлый транспорт",
"truck": "грузовик",
"bus": "автобус",
"building_structure": "здание или сооружение",
"building": "здание",
"wall": "стена",
"paved_road": "дорога с покрытием",
"asphalt": "асфальт",
"bikeway": "велодорожка",
"sidewalk": "тротуар",
"sidewalk_curb": "тротуар и бордюр",
"curb": "бордюр",
"ground_dirt": "грунт",
"soil": "почва",
"gravel": "гравий",
"cobble": "булыжник",
"grass_low_vegetation": "трава и низкая растительность",
"low_grass": "низкая трава",
"high_grass": "высокая трава",
"scenery_vegetation": "растительность",
"forest": "лес",
"bush": "куст",
"hedge": "живая изгородь",
"moss": "мох",
"leaves": "листва",
"crops": "посевы",
"tree_woody_vegetation": "деревья и древесная растительность",
"tree_crown": "крона дерева",
"tree_trunk": "ствол дерева",
"tree_root": "корни дерева",
"sky": "небо",
"water": "вода",
"snow": "снег",
"rock": "камень",
"static_obstacle": "неподвижное препятствие",
"obstacle": "препятствие",
"debris": "обломки",
"animal": "животное",
"rider": "водитель двухколёсного транспорта",
"traffic_cone": "дорожный конус",
"traffic_light": "светофор",
"street_light": "уличный фонарь",
"traffic_sign": "дорожный знак",
"misc_sign": "прочий знак",
"road_block": "перекрытие дороги",
"road_marking": "дорожная разметка",
"pedestrian_crossing": "пешеходный переход",
"boom_barrier": "шлагбаум",
"barrier_tape": "сигнальная лента",
"fence": "ограждение",
"guard_rail": "дорожное ограждение",
"bridge": "мост",
"tunnel": "тоннель",
"pole": "столб",
"rail_track": "железнодорожный путь",
"ego_vehicle": "носитель камеры",
"kick_scooter": "самокат",
"on_rails": "рельсовый транспорт",
"caravan": "автодом",
"trailer": "прицеп",
"heavy_machinery": "тяжёлая техника",
"military_vehicle": "военная техника",
"container": "контейнер",
"barrel": "бочка",
"pipe": "труба",
"wire": "провод",
"other_background": "прочий фон",
"outlier": "выброс",
}
class CanonicalLabOverlayError(RuntimeError):
@@ -47,8 +127,19 @@ class CanonicalLabOverlayArtifact:
sha256: str
@dataclass(frozen=True, slots=True)
class CanonicalLabReplayArtifact:
path: Path
byte_length: int
sha256: str
_render_lock = threading.Lock()
_memory_cache: dict[tuple[str, str, str], CanonicalLabOverlayArtifact] = {}
_replay_lock = threading.Lock()
_replay_memory_cache: dict[
tuple[str, str, str, str], CanonicalLabReplayArtifact
] = {}
def canonical_recording_id(path: Path) -> str:
@@ -172,6 +263,139 @@ def canonical_lab_overlay(
source.unlink(missing_ok=True)
def canonical_lab_replay(
base_recording_path: Path,
*,
base_generation_sha256: str,
overlay: CanonicalLabOverlayArtifact,
result_id: str,
recording_id: str,
cache_root: Path,
) -> CanonicalLabReplayArtifact:
"""Merge base geometry and LAB perception into one cached native RRD."""
base = base_recording_path.expanduser().resolve(strict=True)
cache = cache_root.expanduser().absolute()
if (
base.is_symlink()
or not base.is_file()
or not _is_sha256(base_generation_sha256)
or _sha256(base) != base_generation_sha256
or not _artifact_is_regular(overlay)
or not result_id.startswith("lab-v1-vegetation-shadow-")
or len(result_id) != len("lab-v1-vegetation-shadow-") + 64
or not recording_id
or len(recording_id) > 128
):
raise CanonicalLabOverlayError("canonical LAB replay identity is invalid")
key = (result_id, recording_id, base_generation_sha256, overlay.sha256)
cached = _replay_memory_cache.get(key)
if cached is not None and _replay_artifact_is_regular(cached):
return cached
with _replay_lock:
cached = _replay_memory_cache.get(key)
if cached is not None and _replay_artifact_is_regular(cached):
return cached
cache.mkdir(parents=True, exist_ok=True)
if cache.is_symlink() or not cache.is_dir():
raise CanonicalLabOverlayError("canonical LAB replay cache is invalid")
identity = hashlib.sha256(
"\0".join(
(
REPLAY_RENDERER_VERSION,
result_id,
recording_id,
base_generation_sha256,
overlay.sha256,
)
).encode()
).hexdigest()
output = cache / f"{identity}.replay.rrd"
sidecar = cache / f"{identity}.replay.json"
restored = _restore_cached_replay(
output,
sidecar,
result_id=result_id,
recording_id=recording_id,
base_generation_sha256=base_generation_sha256,
overlay_sha256=overlay.sha256,
)
if restored is not None:
_replay_memory_cache[key] = restored
return restored
temporary = cache / f".{identity}.{uuid4().hex}.replay.rrd"
try:
completed = subprocess.run(
[
sys.executable,
"-m",
"rerun",
"rrd",
"optimize",
"--profile",
"object-store",
"--max-size",
"4MiB",
"--max-rows",
"512",
"--num-pass",
"20",
str(base),
str(overlay.path),
"-o",
str(temporary),
],
check=False,
capture_output=True,
timeout=180,
)
if (
completed.returncode != 0
or not temporary.is_file()
or temporary.is_symlink()
or temporary.stat().st_size < 4
or temporary.stat().st_size > MAX_REPLAY_BYTES
):
raise CanonicalLabOverlayError(
f"canonical LAB replay merge failed: {completed.stderr[-1000:]!r}"
)
with temporary.open("rb") as stream:
if stream.read(4) != b"RRF2":
raise CanonicalLabOverlayError("canonical LAB replay merge is invalid")
if canonical_recording_id(temporary) != recording_id:
raise CanonicalLabOverlayError("canonical LAB replay identity changed")
stat = temporary.stat()
digest = _sha256(temporary)
os.chmod(temporary, 0o600)
os.replace(temporary, output)
_write_json_atomic(
sidecar,
{
"schema_version": "missioncore.canonical-lab-rerun-replay/v1",
"renderer_version": REPLAY_RENDERER_VERSION,
"result_id": result_id,
"recording_id": recording_id,
"base_generation_sha256": base_generation_sha256,
"overlay_sha256": overlay.sha256,
"byte_length": stat.st_size,
"sha256": digest,
},
)
artifact = CanonicalLabReplayArtifact(output, stat.st_size, digest)
_replay_memory_cache[key] = artifact
return artifact
except subprocess.TimeoutExpired as exc:
raise CanonicalLabOverlayError("canonical LAB replay merge timed out") from exc
except CanonicalLabOverlayError:
raise
except Exception as exc:
raise CanonicalLabOverlayError("failed to merge canonical LAB replay") from exc
finally:
temporary.unlink(missing_ok=True)
def _full_route(manifest: dict[str, Any]) -> dict[str, Any]:
route = manifest.get("route_full_review")
layers = route.get("layers") if isinstance(route, dict) else None
@@ -355,7 +579,7 @@ def _render_overlay(
rr.ClassDescription(
info=rr.AnnotationInfo(
id=int(item["class_id"]),
label=str(item["label"]),
label=_localized_semantic_label(str(item["label"])),
color=[*map(int, item["color_rgb"]), 255],
)
)
@@ -579,13 +803,13 @@ def semantic_component_boxes(
_sequence: int,
) -> tuple[list[list[int]], list[str]]:
labels = {
1: "person",
2: "bicycle",
3: "motorcycle",
4: "car",
5: "heavy vehicle",
13: "static obstacle",
14: "animal",
1: "человек",
2: "велосипед",
3: "мотоцикл",
4: "автомобиль",
5: "тяж. транспорт",
13: "препятствие",
14: "животное",
}
candidates: list[tuple[float, list[int], str]] = []
for class_id, label in labels.items():
@@ -595,13 +819,17 @@ def semantic_component_boxes(
)[:12]:
score = min(0.99, 0.5 + pixels / 20_000)
candidates.append(
(score, [left, top, right, bottom], f"{label} · {score:.0%} · semantic-only")
(score, [left, top, right, bottom], f"{label} · {score:.0%}")
)
candidates.sort(key=lambda row: (-row[0], row[1][1], row[1][0]))
selected = candidates[:32]
return [row[1] for row in selected], [row[2] for row in selected]
def _localized_semantic_label(label: str) -> str:
return SEMANTIC_LABELS_RU.get(label, label)
def _mask_component_boxes(
mask: np.ndarray,
class_id: int,
@@ -696,6 +924,30 @@ def _restore_cached(
return None
def _restore_cached_replay(
output: Path,
sidecar: Path,
**identity: str,
) -> CanonicalLabReplayArtifact | None:
try:
value = json.loads(sidecar.read_text(encoding="utf-8"))
stat = output.stat()
if (
output.is_symlink()
or sidecar.is_symlink()
or value.get("schema_version") != "missioncore.canonical-lab-rerun-replay/v1"
or value.get("renderer_version") != REPLAY_RENDERER_VERSION
or any(value.get(key) != expected for key, expected in identity.items())
or value.get("byte_length") != stat.st_size
or not _is_sha256(value.get("sha256"))
or _sha256(output) != value["sha256"]
):
return None
return CanonicalLabReplayArtifact(output, stat.st_size, value["sha256"])
except (OSError, ValueError, json.JSONDecodeError):
return None
def _artifact_is_regular(artifact: CanonicalLabOverlayArtifact) -> bool:
try:
with artifact.path.open("rb") as stream:
@@ -710,6 +962,20 @@ def _artifact_is_regular(artifact: CanonicalLabOverlayArtifact) -> bool:
return False
def _replay_artifact_is_regular(artifact: CanonicalLabReplayArtifact) -> bool:
try:
with artifact.path.open("rb") as stream:
magic = stream.read(4)
return (
not artifact.path.is_symlink()
and magic == b"RRF2"
and artifact.path.stat().st_size == artifact.byte_length
and _sha256(artifact.path) == artifact.sha256
)
except OSError:
return False
def _write_json_atomic(path: Path, value: dict[str, Any]) -> None:
temporary = path.with_name(f".{path.name}.{uuid4().hex}.tmp")
try:
+103
View File
@@ -24,8 +24,10 @@ from pydantic import BaseModel, ConfigDict, Field
from k1link.laboratory.canonical_rerun_overlay import (
CanonicalLabOverlayArtifact,
CanonicalLabOverlayError,
CanonicalLabReplayArtifact,
_mask_component_boxes,
canonical_lab_overlay,
canonical_lab_replay,
canonical_recording_id,
)
from k1link.laboratory.evidence_registry import LaboratoryEvidenceDefinition
@@ -352,6 +354,59 @@ def _build_vegetation_lab_router(
) from exc
return artifact
async def canonical_rerun_replay_artifact(
result_id: str,
*,
expected_base_generation_sha256: str | None = None,
) -> CanonicalLabReplayArtifact:
"""Return one immutable RRD containing base geometry and LAB perception."""
if (
canonical_recording_provider is None
or jobs_root is None
or rerun_overlay_cache_root is None
or ffmpeg_path is None
):
raise HTTPException(status_code=503, detail="Canonical LAB Rerun replay unavailable")
candidate = _resolve_candidate(root_provider, definition, result_id)
manifest = _read_verified(candidate, definition)
route, _ = _full_route_context(candidate, manifest)
recording = canonical_recording_provider(str(route["session_id"]))
if recording is None:
raise HTTPException(status_code=409, detail="Canonical spatial recording is not ready")
recording_path, generation_sha256 = recording
if (
expected_base_generation_sha256 is not None
and expected_base_generation_sha256 != generation_sha256
):
raise HTTPException(status_code=412, detail="Canonical recording generation changed")
try:
recording_id = await run_in_threadpool(canonical_recording_id, recording_path)
overlay = await run_in_threadpool(
canonical_lab_overlay,
candidate,
manifest,
recording_id=recording_id,
base_generation_sha256=generation_sha256,
jobs_root=jobs_root,
cache_root=rerun_overlay_cache_root,
ffmpeg_path=ffmpeg_path,
)
return await run_in_threadpool(
canonical_lab_replay,
recording_path,
base_generation_sha256=generation_sha256,
overlay=overlay,
result_id=result_id,
recording_id=recording_id,
cache_root=rerun_overlay_cache_root,
)
except CanonicalLabOverlayError as exc:
raise HTTPException(
status_code=503,
detail="Canonical LAB Rerun replay failed verification",
) from exc
def canonical_rerun_overlay_file_response(
artifact: CanonicalLabOverlayArtifact,
) -> FileResponse:
@@ -365,6 +420,19 @@ def _build_vegetation_lab_router(
},
)
def canonical_rerun_replay_file_response(
artifact: CanonicalLabReplayArtifact,
) -> FileResponse:
return FileResponse(
artifact.path,
media_type="application/vnd.rerun.rrd",
headers={
"Cache-Control": "private, max-age=31536000, immutable",
"ETag": f'"{artifact.sha256}"',
"X-Content-Type-Options": "nosniff",
},
)
@router.post("/{result_id}/canonical-overlay.rrd")
async def get_canonical_rerun_overlay(
result_id: str,
@@ -446,6 +514,41 @@ def _build_vegetation_lab_router(
raise HTTPException(status_code=412, detail="Canonical overlay generation changed")
return canonical_rerun_overlay_file_response(artifact)
@router.head("/{result_id}/canonical-replay.rrd")
async def describe_canonical_rerun_replay(
result_id: str,
base_generation: Annotated[str, Query(pattern=r"^[a-f0-9]{64}$")],
) -> Response:
"""Build once and describe the single RRD consumed by the LAB viewer."""
artifact = await canonical_rerun_replay_artifact(
result_id,
expected_base_generation_sha256=base_generation,
)
return Response(
status_code=200,
media_type="application/vnd.rerun.rrd",
headers={
"Cache-Control": "private, no-store",
"Content-Length": str(artifact.byte_length),
"ETag": f'"{artifact.sha256}"',
"X-Content-Type-Options": "nosniff",
"X-Rerun-Format": "RRF2",
},
)
@router.get("/{result_id}/canonical-replay.rrd")
async def stream_canonical_rerun_replay(
result_id: str,
generation: Annotated[str, Query(pattern=r"^[a-f0-9]{64}$")],
) -> FileResponse:
"""Stream the digest-bound merged replay through one native receiver."""
artifact = await canonical_rerun_replay_artifact(result_id)
if generation != artifact.sha256:
raise HTTPException(status_code=412, detail="Canonical replay generation changed")
return canonical_rerun_replay_file_response(artifact)
@router.get("/{result_id}/timeline")
def get_canonical_route_timeline(result_id: str) -> dict[str, object]:
candidate = _resolve_candidate(root_provider, definition, result_id)