Files
NODEDC_MISSION_CORE/simulation/ai-polygon/register_paired_scene.py
T

106 lines
4.1 KiB
Python

"""Admit validated Worker-local Gaussian + collision assets, sending metadata only.
Run with the prepared Isaac Python environment on the Worker. The descriptor
declares hashes, provenance, size and scene settings; registration does not
assert that AI has completed a route. No model/profile is changed here.
"""
import argparse
import hashlib
import json
import os
import shutil
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "src"))
from core_client import CoreClient
from local_state import read_json, write_json
from terrain import load_glb
from k1link.simulation.ai_polygon.contracts import WorkerWorldCreate
from k1link.simulation.ai_polygon.terrain_contract import terrain_matches
from k1link.simulation.ai_polygon.worlds import inspect_gaussian_ply
def digest(path):
with path.open("rb") as stream:
return hashlib.file_digest(stream, "sha256").hexdigest()
def preserve(source, target, expected):
if target.exists():
if digest(target) != expected:
raise ValueError("Existing immutable asset identity differs")
return
temporary = target.with_suffix(target.suffix + ".part")
try:
os.link(source, temporary)
except OSError:
shutil.copyfile(source, temporary)
if digest(temporary) != expected:
raise ValueError("Asset identity changed while staging")
temporary.replace(target)
def stage(root, source, collider, descriptor):
if (root / "state/active.json").exists():
raise RuntimeError("Finish the active simulation before importing a scene")
request = WorkerWorldCreate.model_validate(descriptor)
if (
source.stat().st_size != request.byte_length
or digest(source) != request.sha256
or inspect_gaussian_ply(source) != request.splat_count
or digest(collider) != request.collider_sha256
):
raise ValueError("Paired asset does not match its declared identity")
load_glb(collider) # Reject unsupported transforms, external buffers and non-finite data.
cache = root / "state/worlds"
cache.mkdir(parents=True, exist_ok=True)
preserve(source, cache / (request.sha256 + ".ply"), request.sha256)
directory = root / "assets/prepared-worlds" / request.sha256
directory.mkdir(parents=True, exist_ok=True)
destination = directory / "collision.glb"
preserve(collider, destination, request.collider_sha256)
manifest = {
"schema_version": "missioncore.ai-polygon-terrain/v1",
"source_sha256": request.sha256,
"settings": request.settings.model_dump(mode="json"),
"generator": "paired-source",
"collider": str(destination),
"collider_sha256": request.collider_sha256,
"source_url": str(request.source_url) if request.source_url else None,
"qualification": "paired-source-requires-contact-and-route-validation",
}
path = directory / "terrain.json"
if path.exists():
existing = read_json(path)
if existing.get("collider_sha256") != request.collider_sha256 or not terrain_matches(
existing, request.model_dump(mode="json")
):
raise ValueError("Existing paired-scene calibration differs")
else:
write_json(path, manifest)
return request
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--root", type=Path, required=True)
parser.add_argument("--source", type=Path, required=True)
parser.add_argument("--collider", type=Path, required=True)
parser.add_argument("--descriptor", type=Path, required=True)
parser.add_argument("--core", default="http://127.0.0.1:18080")
args = parser.parse_args()
request = stage(args.root, args.source, args.collider, read_json(args.descriptor))
identity = read_json(args.root / "state/realtime-identity.json")
client = CoreClient(args.core, args.root / "private/worker.token", identity["instance_id"])
result = client.request("/worker/worlds", request.model_dump(mode="json"))
write_json(args.root / "assets/prepared-worlds" / request.sha256 / "registration.json", result)
print(json.dumps(result, ensure_ascii=True))
if __name__ == "__main__":
main()