refactor(lab): объединить RAV004 в единый Rerun replay
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user