fix(viewer): gate recorded perception readiness
This commit is contained in:
@@ -105,16 +105,10 @@ class _CuboidPresentationState:
|
||||
colors: np.ndarray,
|
||||
) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, list[str]] | None:
|
||||
accepted = [
|
||||
item
|
||||
for item in objects
|
||||
if str(item.get("cuboid_status", "")).startswith("accepted-")
|
||||
item for item in objects if str(item.get("cuboid_status", "")).startswith("accepted-")
|
||||
]
|
||||
if not (
|
||||
len(accepted)
|
||||
== len(centers)
|
||||
== len(half_sizes)
|
||||
== len(quaternions)
|
||||
== len(colors)
|
||||
len(accepted) == len(centers) == len(half_sizes) == len(quaternions) == len(colors)
|
||||
):
|
||||
raise RecordedPerceptionOverlayError(
|
||||
"integrated perception cuboid presentation arrays are inconsistent"
|
||||
@@ -343,6 +337,8 @@ class IntegratedPerceptionOverlayStore:
|
||||
self.cache_root = cache_root.expanduser().absolute()
|
||||
self.ffmpeg_path = ffmpeg_path.expanduser().absolute()
|
||||
self._lock = threading.Lock()
|
||||
self._catalog_signature: tuple[Any, ...] | None = None
|
||||
self._latest_by_session: dict[str, IntegratedPerceptionResult | None] = {}
|
||||
|
||||
def render(
|
||||
self,
|
||||
@@ -402,10 +398,21 @@ class IntegratedPerceptionOverlayStore:
|
||||
return payload
|
||||
|
||||
def _latest(self, session_id: str) -> IntegratedPerceptionResult | None:
|
||||
signature = _directory_catalog_signature(
|
||||
self.jobs_root,
|
||||
self.results_root,
|
||||
self.lidar_packs_root,
|
||||
)
|
||||
if signature != self._catalog_signature:
|
||||
self._catalog_signature = signature
|
||||
self._latest_by_session.clear()
|
||||
if session_id in self._latest_by_session:
|
||||
return self._latest_by_session[session_id]
|
||||
try:
|
||||
jobs = sorted(self.jobs_root.iterdir())
|
||||
candidates = sorted(self.results_root.iterdir())
|
||||
except FileNotFoundError:
|
||||
self._latest_by_session[session_id] = None
|
||||
return None
|
||||
if len(jobs) > MAX_SCAN or len(candidates) > MAX_SCAN:
|
||||
raise RecordedPerceptionOverlayError("integrated perception catalog is outside bounds")
|
||||
@@ -436,8 +443,57 @@ class IntegratedPerceptionOverlayStore:
|
||||
):
|
||||
matches.append(value)
|
||||
if not matches:
|
||||
self._latest_by_session[session_id] = None
|
||||
return None
|
||||
return max(matches, key=lambda value: (value.created_at_utc, value.result_id))
|
||||
latest = max(matches, key=lambda value: (value.created_at_utc, value.result_id))
|
||||
self._latest_by_session[session_id] = latest
|
||||
return latest
|
||||
|
||||
|
||||
def _directory_catalog_signature(*roots: Path) -> tuple[Any, ...]:
|
||||
"""Track immutable lab catalogs without rehashing every large artifact per request.
|
||||
|
||||
Accepted result directories are append-only. Their directory mtimes still
|
||||
change when an atomic manifest replacement occurs, while a new experiment
|
||||
changes the parent listing. This cheap signature therefore invalidates the
|
||||
process-local validation memo without weakening the first full integrity
|
||||
validation of every catalog generation.
|
||||
"""
|
||||
|
||||
signature: list[Any] = []
|
||||
for root in roots:
|
||||
try:
|
||||
root_stat = root.stat()
|
||||
entries = sorted(root.iterdir(), key=lambda path: path.name)
|
||||
except FileNotFoundError:
|
||||
signature.append((str(root), None))
|
||||
continue
|
||||
if len(entries) > MAX_SCAN:
|
||||
raise RecordedPerceptionOverlayError("integrated perception catalog is outside bounds")
|
||||
entry_signature = []
|
||||
for entry in entries:
|
||||
try:
|
||||
metadata = entry.lstat()
|
||||
except FileNotFoundError:
|
||||
# A concurrent atomic publication will alter the parent mtime
|
||||
# and be observed on the next request.
|
||||
continue
|
||||
entry_signature.append(
|
||||
(
|
||||
entry.name,
|
||||
metadata.st_mode,
|
||||
metadata.st_size,
|
||||
metadata.st_mtime_ns,
|
||||
)
|
||||
)
|
||||
signature.append(
|
||||
(
|
||||
str(root),
|
||||
root_stat.st_mtime_ns,
|
||||
tuple(entry_signature),
|
||||
)
|
||||
)
|
||||
return tuple(signature)
|
||||
|
||||
|
||||
def _render(
|
||||
@@ -630,9 +686,7 @@ def _camera_proxy_file(
|
||||
os.close(descriptor)
|
||||
path = Path(name)
|
||||
path.unlink(missing_ok=True)
|
||||
frame_filter = (
|
||||
f"select=between(n\\,{start_frame_index}\\,{end_frame_index}),setpts=N/(10*TB)"
|
||||
)
|
||||
frame_filter = f"select=between(n\\,{start_frame_index}\\,{end_frame_index}),setpts=N/(10*TB)"
|
||||
try:
|
||||
completed = subprocess.run(
|
||||
[
|
||||
@@ -900,9 +954,7 @@ def _canonical_json(value: object) -> bytes:
|
||||
|
||||
def _finite(value: object) -> TypeGuard[int | float]:
|
||||
return (
|
||||
isinstance(value, int | float)
|
||||
and not isinstance(value, bool)
|
||||
and bool(np.isfinite(value))
|
||||
isinstance(value, int | float) and not isinstance(value, bool) and bool(np.isfinite(value))
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user