feat(api): publish immutable perception lab evidence
This commit is contained in:
@@ -56,14 +56,11 @@ from k1link.compute.e40_perception_product_gate import (
|
||||
read_e40_perception_product_gate,
|
||||
)
|
||||
from k1link.web.l3_pointpillars_visual_api import latest_l3_visual_identity
|
||||
from k1link.web.l31_pointpillars_ravnoves_api import latest_l31_identity
|
||||
from k1link.web.l32_pointpillars_camera_review_api import latest_l32_identity
|
||||
from k1link.web.l33_camera_first_detector_review_api import latest_l33_identity
|
||||
|
||||
LABORATORY_ADVANCED_CATALOG_SCHEMA: Final = (
|
||||
"missioncore.laboratory-advanced-catalog/v1"
|
||||
)
|
||||
LABORATORY_ADVANCED_INDEX_SCHEMA: Final = (
|
||||
"missioncore.laboratory-advanced-index/v1"
|
||||
)
|
||||
LABORATORY_ADVANCED_CATALOG_SCHEMA: Final = "missioncore.laboratory-advanced-catalog/v1"
|
||||
LABORATORY_ADVANCED_INDEX_SCHEMA: Final = "missioncore.laboratory-advanced-index/v1"
|
||||
_INDEX_DOCUMENT_MAX_BYTES: Final = 64 * 1024
|
||||
|
||||
_E31_RESULT_ID = re.compile(r"^e31-source-qualification-[a-f0-9]{64}$")
|
||||
@@ -75,6 +72,24 @@ _E37_RESULT_ID = re.compile(r"^e37-ravnoves-acceptance-[a-f0-9]{64}$")
|
||||
_E38_RESULT_ID = re.compile(r"^e38-perception-baseline-[a-f0-9]{64}$")
|
||||
_E39_RESULT_ID = re.compile(r"^e39-perception-refinement-[a-f0-9]{64}$")
|
||||
_E40_RESULT_ID = re.compile(r"^e40-perception-product-gate-[a-f0-9]{64}$")
|
||||
_E46_RESULT_ID = re.compile(r"^e46-detector-truth-island-[a-f0-9]{64}$")
|
||||
_E46A_RESULT_ID = re.compile(r"^e46a-ai-engineering-preannotation-[a-f0-9]{64}$")
|
||||
_E46B_RESULT_ID = re.compile(r"^e46b-temporal-motion-[a-f0-9]{64}$")
|
||||
_E46C_RESULT_ID = re.compile(r"^e46c-full-replay-world-tracks-[a-f0-9]{64}$")
|
||||
_E46D_RESULT_ID = re.compile(r"^e46d-temporal-failure-audit-[a-f0-9]{64}$")
|
||||
_E46E_RESULT_ID = re.compile(r"^e46e-ready-stack-[a-f0-9]{64}$")
|
||||
_E46F_RESULT_ID = re.compile(r"^e46f-dashcam-bakeoff-[a-f0-9]{64}$")
|
||||
_E46G_RESULT_ID = re.compile(r"^e46g-rectified-detector-bakeoff-[a-f0-9]{64}$")
|
||||
_E46H_RESULT_ID = re.compile(r"^e46h-full-rectified-front-replay-[a-f0-9]{64}$")
|
||||
_E46I_RESULT_ID = re.compile(r"^e46i-grounding-dino-full-replay-[a-f0-9]{64}$")
|
||||
_E46J_RESULT_ID = re.compile(r"^e46j-raw-fisheye-realtime-[a-f0-9]{64}$")
|
||||
_L34_RESULT_ID = re.compile(r"^l34-right-yolox-truth-island-freeze-[a-f0-9]{64}$")
|
||||
_L34A_RESULT_ID = re.compile(r"^l34a-assisted-yolox-error-audit-[a-f0-9]{64}$")
|
||||
_L34B_RESULT_ID = re.compile(r"^l34b-nested-box-consolidation-shadow-[a-f0-9]{64}$")
|
||||
_L34C_RESULT_ID = re.compile(r"^l34c-tile-seam-stitch-shadow-[a-f0-9]{64}$")
|
||||
_L34D_RESULT_ID = re.compile(r"^l34d-cumulative-postprocessing-candidate-[a-f0-9]{64}$")
|
||||
_L34E_RESULT_ID = re.compile(r"^l34e-self-review-diagnostic-[a-f0-9]{64}$")
|
||||
_L34F_RESULT_ID = re.compile(r"^l34f-adjudicated-reference-[a-f0-9]{64}$")
|
||||
|
||||
RootProvider = Callable[[], Path | None]
|
||||
|
||||
@@ -328,12 +343,8 @@ def _project_e31(result: E31SourceQualification) -> dict[str, object]:
|
||||
"correspondence_count": offset.get("correspondence_count"),
|
||||
"supported_fraction": offset.get("baseline_supported_fraction"),
|
||||
"semantic_self_sample_count": semantic_mask.get("source_sample_count"),
|
||||
"semantic_self_collateral_count": semantic_mask.get(
|
||||
"collateral_item_count"
|
||||
),
|
||||
"exact_geometry_correction_count": len(
|
||||
self_mask.get("exact_correction_item_ids", [])
|
||||
),
|
||||
"semantic_self_collateral_count": semantic_mask.get("collateral_item_count"),
|
||||
"exact_geometry_correction_count": len(self_mask.get("exact_correction_item_ids", [])),
|
||||
"geometry_point_mask_status": geometry_mask.get("status"),
|
||||
},
|
||||
"limitations": copy.deepcopy(result.report.get("limitations")),
|
||||
@@ -375,13 +386,9 @@ def _project_e32(result: E32TrackGeometryReplay) -> dict[str, object]:
|
||||
"semantic_masked": objects.get("semantic_masked"),
|
||||
"geometry_published": objects.get("geometry_published"),
|
||||
"observations_arbitrated": ownership.get("observations_arbitrated"),
|
||||
"overlapping_claims_removed": ownership.get(
|
||||
"overlapping_claims_removed"
|
||||
),
|
||||
"overlapping_claims_removed": ownership.get("overlapping_claims_removed"),
|
||||
"qualified_points_published": point_rows.get("e32_published"),
|
||||
"qualified_points_withheld": point_rows.get(
|
||||
"unqualified_ranges_withheld"
|
||||
),
|
||||
"qualified_points_withheld": point_rows.get("unqualified_ranges_withheld"),
|
||||
"frame_processing_p95_ms": frame_processing.get("p95"),
|
||||
"build_elapsed_ms": runtime.get("build_elapsed_ms"),
|
||||
},
|
||||
@@ -444,8 +451,7 @@ def _project_e33(
|
||||
"source_frames": accounting.get("source_frames"),
|
||||
"source_available_frames": linked_frames.get("source_available"),
|
||||
"source_unavailable_frames": (
|
||||
int(accounting["source_frames"])
|
||||
- int(linked_frames["source_available"])
|
||||
int(accounting["source_frames"]) - int(linked_frames["source_available"])
|
||||
),
|
||||
"delivered_frames": accounting.get("delivered"),
|
||||
"input_superseded": accounting.get("input_superseded"),
|
||||
@@ -460,9 +466,7 @@ def _project_e33(
|
||||
"result_deadline_ms": deadlines.get("result_ms"),
|
||||
"process_rss_p95_mib": process_rss.get("p95"),
|
||||
"process_rss_max_mib": process_rss.get("maximum"),
|
||||
"process_rss_limit_mib": profile_acceptance.get(
|
||||
"maximum_process_rss_mib"
|
||||
),
|
||||
"process_rss_limit_mib": profile_acceptance.get("maximum_process_rss_mib"),
|
||||
"gpu_utilization_p95_percent": gpu_utilization.get("p95"),
|
||||
"gpu_visible": resources.get("gpu_visible"),
|
||||
"work_queue_capacity": work_queue.get("capacity"),
|
||||
@@ -507,15 +511,9 @@ def _project_e34(result: E34TemporalOccupiedReplay) -> dict[str, object]:
|
||||
"configuration": {
|
||||
"voxel_size_m": layer.get("voxel_size_m"),
|
||||
"occupied_ttl_seconds": layer.get("occupied_ttl_seconds"),
|
||||
"maximum_active_components": layer.get(
|
||||
"maximum_active_components"
|
||||
),
|
||||
"maximum_cells_per_component": layer.get(
|
||||
"maximum_cells_per_component"
|
||||
),
|
||||
"geometry_maximum_gap_seconds": association.get(
|
||||
"geometry_maximum_gap_seconds"
|
||||
),
|
||||
"maximum_active_components": layer.get("maximum_active_components"),
|
||||
"maximum_cells_per_component": layer.get("maximum_cells_per_component"),
|
||||
"geometry_maximum_gap_seconds": association.get("geometry_maximum_gap_seconds"),
|
||||
"geometry_maximum_centroid_distance_m": association.get(
|
||||
"geometry_maximum_centroid_distance_m"
|
||||
),
|
||||
@@ -529,44 +527,26 @@ def _project_e34(result: E34TemporalOccupiedReplay) -> dict[str, object]:
|
||||
"created_components": components.get("created"),
|
||||
"camera_created_components": components.get("camera_created"),
|
||||
"geometry_created_components": components.get("geometry_created"),
|
||||
"geometry_reassociated_components": components.get(
|
||||
"geometry_reassociated_components"
|
||||
),
|
||||
"exact_camera_associations": components.get(
|
||||
"exact_camera_associations"
|
||||
),
|
||||
"geometry_spatial_reassociations": components.get(
|
||||
"geometry_spatial_reassociations"
|
||||
),
|
||||
"geometry_reassociated_components": components.get("geometry_reassociated_components"),
|
||||
"exact_camera_associations": components.get("exact_camera_associations"),
|
||||
"geometry_spatial_reassociations": components.get("geometry_spatial_reassociations"),
|
||||
"held_publications": components.get("held_publications"),
|
||||
"expired_components": components.get("expired"),
|
||||
"peak_active_components": components.get("peak_active"),
|
||||
"maximum_observed_span_seconds": components.get(
|
||||
"maximum_observed_span_seconds"
|
||||
),
|
||||
"maximum_observed_span_seconds": components.get("maximum_observed_span_seconds"),
|
||||
"continuity_fraction": components.get("continuity_fraction"),
|
||||
"geometry_only_reassociation_fraction": components.get(
|
||||
"geometry_only_reassociation_fraction"
|
||||
),
|
||||
"e32_current_point_rows": occupancy.get(
|
||||
"e32_current_point_rows"
|
||||
),
|
||||
"e34_consumed_current_point_rows": occupancy.get(
|
||||
"e34_consumed_current_point_rows"
|
||||
),
|
||||
"e32_current_point_rows": occupancy.get("e32_current_point_rows"),
|
||||
"e34_consumed_current_point_rows": occupancy.get("e34_consumed_current_point_rows"),
|
||||
"current_cell_rows": occupancy.get("current_cell_rows"),
|
||||
"held_cell_rows": occupancy.get("held_cell_rows"),
|
||||
"stored_cell_rows": occupancy.get("stored_cell_rows"),
|
||||
"free_cell_rows": occupancy.get("free_cell_rows"),
|
||||
"peak_cells_per_component": occupancy.get(
|
||||
"peak_cells_per_component"
|
||||
),
|
||||
"maximum_held_age_seconds": aging.get(
|
||||
"maximum_held_age_seconds"
|
||||
),
|
||||
"maximum_expiry_delay_seconds": aging.get(
|
||||
"maximum_expiry_delay_seconds"
|
||||
),
|
||||
"peak_cells_per_component": occupancy.get("peak_cells_per_component"),
|
||||
"maximum_held_age_seconds": aging.get("maximum_held_age_seconds"),
|
||||
"maximum_expiry_delay_seconds": aging.get("maximum_expiry_delay_seconds"),
|
||||
"maximum_replay_materialization_delay_seconds": aging.get(
|
||||
"maximum_replay_materialization_delay_seconds"
|
||||
),
|
||||
@@ -611,19 +591,13 @@ def _project_e35(result: E35DegradationReplay) -> dict[str, object]:
|
||||
"pipeline_id": identity.get("pipeline"),
|
||||
"coordinate_frame": identity.get("coordinate_frame"),
|
||||
"configuration": {
|
||||
"maximum_recovery_seconds": acceptance_profile.get(
|
||||
"maximum_recovery_seconds"
|
||||
),
|
||||
"scenario_count": acceptance_profile.get(
|
||||
"expected_scenario_count"
|
||||
),
|
||||
"maximum_recovery_seconds": acceptance_profile.get("maximum_recovery_seconds"),
|
||||
"scenario_count": acceptance_profile.get("expected_scenario_count"),
|
||||
},
|
||||
"metrics": {
|
||||
"source_frames": accounting.get("source_frames"),
|
||||
"variant_count": accounting.get("variant_count"),
|
||||
"variant_frame_outcomes": accounting.get(
|
||||
"variant_frame_outcomes"
|
||||
),
|
||||
"variant_frame_outcomes": accounting.get("variant_frame_outcomes"),
|
||||
"injection_records": accounting.get("injection_records"),
|
||||
"variant_frame_processing_p95_ms": processing.get("p95"),
|
||||
"build_elapsed_ms": runtime.get("build_elapsed_ms"),
|
||||
@@ -669,18 +643,12 @@ def _project_e37(result: E37AcceptanceContract) -> dict[str, object]:
|
||||
"accounting_fraction": metrics.get("accounting_fraction"),
|
||||
"false_free_claims": metrics.get("false_free_claims"),
|
||||
},
|
||||
"dimension_distributions": copy.deepcopy(
|
||||
result.contract.get("dimension_distributions")
|
||||
),
|
||||
"severity_distribution": copy.deepcopy(
|
||||
result.contract.get("severity_distribution")
|
||||
),
|
||||
"dimension_distributions": copy.deepcopy(result.contract.get("dimension_distributions")),
|
||||
"severity_distribution": copy.deepcopy(result.contract.get("severity_distribution")),
|
||||
"label_provenance": {
|
||||
"engineering_items": provenance.get("engineering_items"),
|
||||
"human_exception_items": provenance.get("human_exception_items"),
|
||||
"independent_ground_truth": provenance.get(
|
||||
"independent_ground_truth"
|
||||
),
|
||||
"independent_ground_truth": provenance.get("independent_ground_truth"),
|
||||
},
|
||||
"split": copy.deepcopy(result.contract.get("split")),
|
||||
"targets": copy.deepcopy(result.contract.get("targets")),
|
||||
@@ -827,8 +795,28 @@ def build_advanced_laboratory_router(
|
||||
e38_root_provider: RootProvider = lambda: None,
|
||||
e39_root_provider: RootProvider = lambda: None,
|
||||
e40_root_provider: RootProvider = lambda: None,
|
||||
e46_root_provider: RootProvider = lambda: None,
|
||||
e46a_root_provider: RootProvider = lambda: None,
|
||||
e46b_root_provider: RootProvider = lambda: None,
|
||||
e46c_root_provider: RootProvider = lambda: None,
|
||||
e46d_root_provider: RootProvider = lambda: None,
|
||||
e46e_root_provider: RootProvider = lambda: None,
|
||||
e46f_root_provider: RootProvider = lambda: None,
|
||||
e46g_root_provider: RootProvider = lambda: None,
|
||||
e46h_root_provider: RootProvider = lambda: None,
|
||||
e46i_root_provider: RootProvider = lambda: None,
|
||||
e46j_root_provider: RootProvider = lambda: None,
|
||||
l34_root_provider: RootProvider = lambda: None,
|
||||
l34a_root_provider: RootProvider = lambda: None,
|
||||
l34b_root_provider: RootProvider = lambda: None,
|
||||
l34c_root_provider: RootProvider = lambda: None,
|
||||
l34d_root_provider: RootProvider = lambda: None,
|
||||
l34e_root_provider: RootProvider = lambda: None,
|
||||
l34f_root_provider: RootProvider = lambda: None,
|
||||
l3_visual_root_provider: RootProvider = lambda: None,
|
||||
l31_ravnoves_root_provider: RootProvider = lambda: None,
|
||||
l32_camera_review_root_provider: RootProvider = lambda: None,
|
||||
l33_camera_first_review_root_provider: RootProvider = lambda: None,
|
||||
) -> APIRouter:
|
||||
router = APIRouter(prefix="/api/v1/laboratory", tags=["laboratory"])
|
||||
|
||||
@@ -899,23 +887,161 @@ def build_advanced_laboratory_router(
|
||||
"manifest.json",
|
||||
"missioncore.e40-perception-product-gate/v1",
|
||||
),
|
||||
(
|
||||
"e46-detector-truth-island",
|
||||
e46_root_provider,
|
||||
_E46_RESULT_ID,
|
||||
"manifest.json",
|
||||
"missioncore.e46-detector-truth-island/v1",
|
||||
),
|
||||
(
|
||||
"e46a-ai-engineering-preannotation",
|
||||
e46a_root_provider,
|
||||
_E46A_RESULT_ID,
|
||||
"manifest.json",
|
||||
"missioncore.e46a-ai-engineering-preannotation/v1",
|
||||
),
|
||||
(
|
||||
"e46b-temporal-motion",
|
||||
e46b_root_provider,
|
||||
_E46B_RESULT_ID,
|
||||
"manifest.json",
|
||||
"missioncore.e46b-temporal-motion/v1",
|
||||
),
|
||||
(
|
||||
"e46c-full-replay-world-tracks",
|
||||
e46c_root_provider,
|
||||
_E46C_RESULT_ID,
|
||||
"manifest.json",
|
||||
"missioncore.e46c-full-replay-world-tracks/v1",
|
||||
),
|
||||
(
|
||||
"e46d-temporal-failure-audit",
|
||||
e46d_root_provider,
|
||||
_E46D_RESULT_ID,
|
||||
"manifest.json",
|
||||
"missioncore.e46d-temporal-failure-audit/v1",
|
||||
),
|
||||
(
|
||||
"e46e-ready-stack",
|
||||
e46e_root_provider,
|
||||
_E46E_RESULT_ID,
|
||||
"manifest.json",
|
||||
"missioncore.e46e-ready-stack-result/v1",
|
||||
),
|
||||
(
|
||||
"e46f-dashcam-bakeoff",
|
||||
e46f_root_provider,
|
||||
_E46F_RESULT_ID,
|
||||
"manifest.json",
|
||||
"missioncore.e46f-dashcam-bakeoff-result/v1",
|
||||
),
|
||||
(
|
||||
"e46g-rectified-detector-bakeoff",
|
||||
e46g_root_provider,
|
||||
_E46G_RESULT_ID,
|
||||
"manifest.json",
|
||||
"missioncore.e46g-rectified-detector-bakeoff-result/v1",
|
||||
),
|
||||
(
|
||||
"e46h-full-rectified-front-replay",
|
||||
e46h_root_provider,
|
||||
_E46H_RESULT_ID,
|
||||
"manifest.json",
|
||||
"missioncore.e46h-full-rectified-front-replay-result/v1",
|
||||
),
|
||||
(
|
||||
"e46i-grounding-dino-full-replay",
|
||||
e46i_root_provider,
|
||||
_E46I_RESULT_ID,
|
||||
"manifest.json",
|
||||
"missioncore.e46i-grounding-dino-full-replay-result/v1",
|
||||
),
|
||||
(
|
||||
"e46j-raw-fisheye-realtime",
|
||||
e46j_root_provider,
|
||||
_E46J_RESULT_ID,
|
||||
"manifest.json",
|
||||
"missioncore.e46j-raw-fisheye-realtime-result/v1",
|
||||
),
|
||||
(
|
||||
"l34-right-yolox-truth-island-freeze",
|
||||
l34_root_provider,
|
||||
_L34_RESULT_ID,
|
||||
"manifest.json",
|
||||
"missioncore.l34-right-yolox-truth-island-freeze/v1",
|
||||
),
|
||||
(
|
||||
"l34a-assisted-yolox-error-audit",
|
||||
l34a_root_provider,
|
||||
_L34A_RESULT_ID,
|
||||
"manifest.json",
|
||||
"missioncore.l34a-assisted-yolox-error-audit/v1",
|
||||
),
|
||||
(
|
||||
"l34b-nested-box-consolidation-shadow",
|
||||
l34b_root_provider,
|
||||
_L34B_RESULT_ID,
|
||||
"manifest.json",
|
||||
"missioncore.l34b-nested-box-consolidation-shadow/v1",
|
||||
),
|
||||
(
|
||||
"l34c-tile-seam-stitch-shadow",
|
||||
l34c_root_provider,
|
||||
_L34C_RESULT_ID,
|
||||
"manifest.json",
|
||||
"missioncore.l34c-tile-seam-stitch-shadow/v1",
|
||||
),
|
||||
(
|
||||
"l34d-cumulative-postprocessing-candidate",
|
||||
l34d_root_provider,
|
||||
_L34D_RESULT_ID,
|
||||
"manifest.json",
|
||||
"missioncore.l34d-cumulative-postprocessing-candidate/v1",
|
||||
),
|
||||
(
|
||||
"l34e-self-review-diagnostic",
|
||||
l34e_root_provider,
|
||||
_L34E_RESULT_ID,
|
||||
"manifest.json",
|
||||
"missioncore.l34e-self-review-diagnostic/v1",
|
||||
),
|
||||
(
|
||||
"l34f-adjudicated-reference",
|
||||
l34f_root_provider,
|
||||
_L34F_RESULT_ID,
|
||||
"manifest.json",
|
||||
"missioncore.l34f-adjudicated-reference/v1",
|
||||
),
|
||||
)
|
||||
)
|
||||
items = result["items"]
|
||||
if not isinstance(items, list):
|
||||
raise RuntimeError("advanced LAB index items are invalid")
|
||||
l3_identity = latest_l3_visual_identity(l3_visual_root_provider)
|
||||
if l3_identity is not None:
|
||||
result["items"].append(
|
||||
items.append(
|
||||
{
|
||||
"work_id": "l3-pointpillars-visual-audit",
|
||||
**l3_identity,
|
||||
"access": "read-only",
|
||||
}
|
||||
)
|
||||
l31_identity = latest_l31_identity(l31_ravnoves_root_provider)
|
||||
if l31_identity is not None:
|
||||
result["items"].append(
|
||||
l32_identity = latest_l32_identity(l32_camera_review_root_provider)
|
||||
if l32_identity is not None:
|
||||
items.append(
|
||||
{
|
||||
"work_id": "l31-pointpillars-ravnoves",
|
||||
**l31_identity,
|
||||
"work_id": "l32-pointpillars-camera-review",
|
||||
**l32_identity,
|
||||
"access": "read-only",
|
||||
}
|
||||
)
|
||||
l33_identity = latest_l33_identity(l33_camera_first_review_root_provider)
|
||||
if l33_identity is not None:
|
||||
items.append(
|
||||
{
|
||||
"work_id": "l33-camera-first-detector-review",
|
||||
**l33_identity,
|
||||
"access": "read-only",
|
||||
}
|
||||
)
|
||||
@@ -972,10 +1098,7 @@ def build_advanced_laboratory_router(
|
||||
str(candidate.resolve()),
|
||||
_result_signature(candidate),
|
||||
)
|
||||
if (
|
||||
result.report.get("status")
|
||||
!= "accepted-diagnostic-track-geometry-replay"
|
||||
):
|
||||
if result.report.get("status") != "accepted-diagnostic-track-geometry-replay":
|
||||
raise ValueError("E32 result is not accepted")
|
||||
if len(items) < limit:
|
||||
items.append(_project_e32(result))
|
||||
|
||||
+544
-172
@@ -41,6 +41,31 @@ from k1link.web.e30_engineering_api import build_e30_engineering_router
|
||||
from k1link.web.e30_human_review_api import build_e30_human_review_router
|
||||
from k1link.web.e30_review_api import build_e30_review_router
|
||||
from k1link.web.e40_case_review_api import build_e40_case_review_router
|
||||
from k1link.web.e46_blind_review_api import build_e46_blind_review_router
|
||||
from k1link.web.e46a_ai_engineering_preannotation_api import (
|
||||
build_e46a_ai_engineering_preannotation_router,
|
||||
)
|
||||
from k1link.web.e46b_temporal_motion_api import build_e46b_temporal_motion_router
|
||||
from k1link.web.e46c_full_replay_world_tracks_api import (
|
||||
build_e46c_full_replay_world_tracks_router,
|
||||
)
|
||||
from k1link.web.e46d_temporal_failure_audit_api import (
|
||||
build_e46d_temporal_failure_audit_router,
|
||||
)
|
||||
from k1link.web.e46e_ready_stack_api import build_e46e_ready_stack_router
|
||||
from k1link.web.e46f_dashcam_bakeoff_api import build_e46f_dashcam_bakeoff_router
|
||||
from k1link.web.e46g_rectified_detector_bakeoff_api import (
|
||||
build_e46g_rectified_detector_bakeoff_router,
|
||||
)
|
||||
from k1link.web.e46h_full_rectified_front_replay_api import (
|
||||
build_e46h_full_rectified_front_replay_router,
|
||||
)
|
||||
from k1link.web.e46i_grounding_dino_full_replay_api import (
|
||||
build_e46i_grounding_dino_full_replay_router,
|
||||
)
|
||||
from k1link.web.e46j_raw_fisheye_realtime_api import (
|
||||
build_e46j_raw_fisheye_realtime_router,
|
||||
)
|
||||
from k1link.web.environment_api import build_environment_router
|
||||
from k1link.web.l3_pointpillars_visual_api import (
|
||||
build_l3_pointpillars_visual_router,
|
||||
@@ -48,6 +73,35 @@ from k1link.web.l3_pointpillars_visual_api import (
|
||||
from k1link.web.l31_pointpillars_ravnoves_api import (
|
||||
build_l31_pointpillars_ravnoves_router,
|
||||
)
|
||||
from k1link.web.l32_pointpillars_camera_review_api import (
|
||||
build_l32_pointpillars_camera_review_router,
|
||||
)
|
||||
from k1link.web.l33_camera_first_detector_review_api import (
|
||||
build_l33_camera_first_detector_review_router,
|
||||
)
|
||||
from k1link.web.l34_annotation_api import (
|
||||
build_l34_annotation_router,
|
||||
build_l34d_blind_annotation_router,
|
||||
)
|
||||
from k1link.web.l34_right_yolox_truth_island_api import (
|
||||
build_l34_right_yolox_truth_island_freeze_router,
|
||||
)
|
||||
from k1link.web.l34a_assisted_yolox_error_audit_api import (
|
||||
build_l34a_assisted_yolox_error_audit_router,
|
||||
)
|
||||
from k1link.web.l34b_nested_box_consolidation_api import (
|
||||
build_l34b_nested_box_consolidation_router,
|
||||
)
|
||||
from k1link.web.l34c_tile_seam_stitch_api import (
|
||||
build_l34c_tile_seam_stitch_router,
|
||||
)
|
||||
from k1link.web.l34d_cumulative_postprocessing_api import (
|
||||
build_l34d_cumulative_postprocessing_router,
|
||||
)
|
||||
from k1link.web.l34e_self_review_diagnostic_api import (
|
||||
build_l34e_self_review_diagnostic_router,
|
||||
)
|
||||
from k1link.web.l34f_adjudication_api import build_l34f_adjudication_router
|
||||
from k1link.web.laboratory_api import build_laboratory_router
|
||||
from k1link.web.lidar_api import build_lidar_router
|
||||
from k1link.web.lidar_local_surface_service import K1LocalSurfaceReadService
|
||||
@@ -139,18 +193,10 @@ session_integrated_perception_store = (
|
||||
IntegratedPerceptionOverlayStore(
|
||||
jobs_root=REPOSITORY_ROOT / ".runtime" / "compute-jobs",
|
||||
results_root=(
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "e10"
|
||||
/ "worker-results"
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e10" / "worker-results"
|
||||
),
|
||||
lidar_packs_root=(
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "e10"
|
||||
/ "lidar-packs"
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e10" / "lidar-packs"
|
||||
),
|
||||
cache_root=session_store.data_dir / "integrated-perception-overlays",
|
||||
ffmpeg_path=_ffmpeg,
|
||||
@@ -304,9 +350,7 @@ async def _recording_preparation_reconciler() -> None:
|
||||
async def app_lifespan(_: FastAPI) -> AsyncIterator[None]:
|
||||
reconciler: asyncio.Task[None] | None = None
|
||||
try:
|
||||
configure_scanner_diagnostics(
|
||||
REPOSITORY_ROOT / ".runtime" / "mission-core" / "logs"
|
||||
)
|
||||
configure_scanner_diagnostics(REPOSITORY_ROOT / ".runtime" / "mission-core" / "logs")
|
||||
session_recording_preparation_manager.start()
|
||||
# Recovery is intentionally a one-shot startup phase. The archive
|
||||
# helper owns a cross-process lease, while ordinary catalog requests
|
||||
@@ -441,9 +485,7 @@ app.include_router(
|
||||
)
|
||||
)
|
||||
app.include_router(
|
||||
build_environment_router(
|
||||
root_provider=lambda: session_store.data_dir / "ui-environment"
|
||||
)
|
||||
build_environment_router(root_provider=lambda: session_store.data_dir / "ui-environment")
|
||||
)
|
||||
app.include_router(build_map_router(map_gateway_proxy))
|
||||
app.include_router(
|
||||
@@ -453,25 +495,18 @@ app.include_router(
|
||||
)
|
||||
app.include_router(
|
||||
build_polygon_router(
|
||||
root_provider=lambda: configured_polygon_runs_root()
|
||||
or REPOSITORY_ROOT / ".runtime" / "polygon-runs"
|
||||
root_provider=lambda: (
|
||||
configured_polygon_runs_root() or REPOSITORY_ROOT / ".runtime" / "polygon-runs"
|
||||
)
|
||||
)
|
||||
)
|
||||
app.include_router(
|
||||
build_lidar_router(
|
||||
root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "lidar-replay-v2"
|
||||
/ "packs"
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "lidar-replay-v2" / "packs"
|
||||
),
|
||||
ground_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "lidar-ground-v1"
|
||||
/ "benchmarks"
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "lidar-ground-v1" / "benchmarks"
|
||||
),
|
||||
field_review_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
@@ -481,18 +516,10 @@ app.include_router(
|
||||
/ "reviews"
|
||||
),
|
||||
local_surface_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "k1-local-surface-v1"
|
||||
/ "models"
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "k1-local-surface-v1" / "models"
|
||||
),
|
||||
e10_source_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "e10"
|
||||
/ "lidar-packs"
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e10" / "lidar-packs"
|
||||
),
|
||||
dataset_admission_provider=lambda: (
|
||||
REPOSITORY_ROOT / ".runtime" / "dataset-gateway" / "admission.json"
|
||||
@@ -518,221 +545,574 @@ app.include_router(
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e29" / "results"
|
||||
),
|
||||
local_surface_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "k1-local-surface-v1"
|
||||
/ "models"
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "k1-local-surface-v1" / "models"
|
||||
),
|
||||
source_pack_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "e10"
|
||||
/ "lidar-packs"
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e10" / "lidar-packs"
|
||||
),
|
||||
source_result_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "e10"
|
||||
/ "worker-results"
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e10" / "worker-results"
|
||||
),
|
||||
)
|
||||
)
|
||||
app.include_router(
|
||||
build_advanced_laboratory_router(
|
||||
e31_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "e31"
|
||||
/ "source-qualifications"
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e31" / "source-qualifications"
|
||||
),
|
||||
e32_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "e32"
|
||||
/ "results"
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e32" / "results"
|
||||
),
|
||||
e33_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "e33"
|
||||
/ "results"
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e33" / "results"
|
||||
),
|
||||
e34_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "e34"
|
||||
/ "results"
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e34" / "results"
|
||||
),
|
||||
e35_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "e35"
|
||||
/ "results"
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e35" / "results"
|
||||
),
|
||||
e37_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "e37"
|
||||
/ "results"
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e37" / "results"
|
||||
),
|
||||
e38_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "e38"
|
||||
/ "results"
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e38" / "results"
|
||||
),
|
||||
e39_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "e39"
|
||||
/ "results"
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e39" / "results"
|
||||
),
|
||||
e40_root_provider=lambda: (
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e40" / "results"
|
||||
),
|
||||
e46_root_provider=lambda: (
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e46" / "results"
|
||||
),
|
||||
e46a_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "e40"
|
||||
/ "results"
|
||||
/ "e46a"
|
||||
/ "ai-engineering-preannotations"
|
||||
),
|
||||
e46b_root_provider=lambda: (
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e46b" / "temporal-motion"
|
||||
),
|
||||
e46c_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "e46c"
|
||||
/ "full-replay-world-tracks"
|
||||
),
|
||||
e46d_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "e46d"
|
||||
/ "temporal-failure-audits"
|
||||
),
|
||||
e46e_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "e46e"
|
||||
/ "ready-stack-results"
|
||||
),
|
||||
e46f_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "e46f"
|
||||
/ "dashcam-bakeoff-results"
|
||||
),
|
||||
e46g_root_provider=lambda: (
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e46g" / "results"
|
||||
),
|
||||
e46h_root_provider=lambda: (
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e46h" / "results"
|
||||
),
|
||||
e46i_root_provider=lambda: (
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e46i" / "results"
|
||||
),
|
||||
e46j_root_provider=lambda: (
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e46j" / "results"
|
||||
),
|
||||
l34_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "l3"
|
||||
/ "right-yolox-truth-island-freeze"
|
||||
),
|
||||
l34a_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "l3"
|
||||
/ "assisted-yolox-error-audits"
|
||||
),
|
||||
l34b_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "l3"
|
||||
/ "nested-box-consolidation-shadows"
|
||||
),
|
||||
l34c_root_provider=lambda: (
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "l3" / "tile-seam-stitch-shadows"
|
||||
),
|
||||
l34d_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "l3"
|
||||
/ "cumulative-postprocessing-candidates"
|
||||
),
|
||||
l34e_root_provider=lambda: (
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "l3" / "self-review-diagnostics"
|
||||
),
|
||||
l34f_root_provider=lambda: (
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "l3" / "adjudicated-references"
|
||||
),
|
||||
l3_visual_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "l3"
|
||||
/ "visual-audits"
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "l3" / "visual-audits"
|
||||
),
|
||||
l31_ravnoves_root_provider=lambda: (
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "l3" / "pointpillars-ravnoves"
|
||||
),
|
||||
l32_camera_review_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "l3"
|
||||
/ "pointpillars-ravnoves"
|
||||
/ "pointpillars-camera-review"
|
||||
),
|
||||
l33_camera_first_review_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "l3"
|
||||
/ "camera-first-detector-review"
|
||||
),
|
||||
)
|
||||
)
|
||||
app.include_router(
|
||||
build_l3_pointpillars_visual_router(
|
||||
root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "l3"
|
||||
/ "visual-audits"
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "l3" / "visual-audits"
|
||||
)
|
||||
)
|
||||
)
|
||||
app.include_router(
|
||||
build_l31_pointpillars_ravnoves_router(
|
||||
root_provider=lambda: (
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "l3" / "pointpillars-ravnoves"
|
||||
)
|
||||
)
|
||||
)
|
||||
app.include_router(
|
||||
build_l32_pointpillars_camera_review_router(
|
||||
root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "l3"
|
||||
/ "pointpillars-ravnoves"
|
||||
/ "pointpillars-camera-review"
|
||||
)
|
||||
)
|
||||
)
|
||||
app.include_router(
|
||||
build_l33_camera_first_detector_review_router(
|
||||
root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "l3"
|
||||
/ "camera-first-detector-review"
|
||||
)
|
||||
)
|
||||
)
|
||||
app.include_router(
|
||||
build_l34_right_yolox_truth_island_freeze_router(
|
||||
root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "l3"
|
||||
/ "right-yolox-truth-island-freeze"
|
||||
),
|
||||
truth_root_provider=lambda: (
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e46" / "results"
|
||||
),
|
||||
evaluation_pack_root_provider=lambda: (
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e2" / "evaluation-packs"
|
||||
),
|
||||
)
|
||||
)
|
||||
app.include_router(
|
||||
build_e46_blind_review_router(
|
||||
truth_root_provider=lambda: (
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e46" / "results"
|
||||
),
|
||||
evaluation_pack_root_provider=lambda: (
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e2" / "evaluation-packs"
|
||||
),
|
||||
annotation_root_provider=lambda: (
|
||||
REPOSITORY_ROOT / ".runtime" / "laboratory-annotations" / "e46-blind"
|
||||
),
|
||||
submission_root_provider=lambda: (
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e48" / "review-submissions"
|
||||
),
|
||||
)
|
||||
)
|
||||
app.include_router(
|
||||
build_e46a_ai_engineering_preannotation_router(
|
||||
root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "e46a"
|
||||
/ "ai-engineering-preannotations"
|
||||
),
|
||||
annotation_root_provider=lambda: (
|
||||
REPOSITORY_ROOT / ".runtime" / "laboratory-annotations" / "e46a-correction"
|
||||
),
|
||||
)
|
||||
)
|
||||
app.include_router(
|
||||
build_e46b_temporal_motion_router(
|
||||
root_provider=lambda: (
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e46b" / "temporal-motion"
|
||||
),
|
||||
)
|
||||
)
|
||||
app.include_router(
|
||||
build_e46c_full_replay_world_tracks_router(
|
||||
root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "e46c"
|
||||
/ "full-replay-world-tracks"
|
||||
),
|
||||
e26_root_provider=lambda: (
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e10" / "worker-results"
|
||||
),
|
||||
)
|
||||
)
|
||||
app.include_router(
|
||||
build_e46d_temporal_failure_audit_router(
|
||||
root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "e46d"
|
||||
/ "temporal-failure-audits"
|
||||
),
|
||||
)
|
||||
)
|
||||
app.include_router(
|
||||
build_e46e_ready_stack_router(
|
||||
root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "e46e"
|
||||
/ "ready-stack-results"
|
||||
),
|
||||
)
|
||||
)
|
||||
app.include_router(
|
||||
build_e46f_dashcam_bakeoff_router(
|
||||
root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "e46f"
|
||||
/ "dashcam-bakeoff-results"
|
||||
),
|
||||
e46e_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "e46e"
|
||||
/ "ready-stack-results"
|
||||
),
|
||||
)
|
||||
)
|
||||
app.include_router(
|
||||
build_e46g_rectified_detector_bakeoff_router(
|
||||
root_provider=lambda: (
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e46g" / "results"
|
||||
)
|
||||
)
|
||||
)
|
||||
app.include_router(
|
||||
build_e46h_full_rectified_front_replay_router(
|
||||
root_provider=lambda: (
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e46h" / "results"
|
||||
)
|
||||
)
|
||||
)
|
||||
app.include_router(
|
||||
build_e46i_grounding_dino_full_replay_router(
|
||||
root_provider=lambda: (
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e46i" / "results"
|
||||
)
|
||||
)
|
||||
)
|
||||
app.include_router(
|
||||
build_e46j_raw_fisheye_realtime_router(
|
||||
root_provider=lambda: (
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e46j" / "results"
|
||||
)
|
||||
)
|
||||
)
|
||||
app.include_router(
|
||||
build_l34_annotation_router(
|
||||
root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "l3"
|
||||
/ "right-yolox-truth-island-freeze"
|
||||
),
|
||||
truth_root_provider=lambda: (
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e46" / "results"
|
||||
),
|
||||
evaluation_pack_root_provider=lambda: (
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e2" / "evaluation-packs"
|
||||
),
|
||||
annotation_root_provider=lambda: (
|
||||
REPOSITORY_ROOT / ".runtime" / "laboratory-annotations" / "l34"
|
||||
),
|
||||
)
|
||||
)
|
||||
app.include_router(
|
||||
build_l34a_assisted_yolox_error_audit_router(
|
||||
audit_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "l3"
|
||||
/ "assisted-yolox-error-audits"
|
||||
),
|
||||
l34_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "l3"
|
||||
/ "right-yolox-truth-island-freeze"
|
||||
),
|
||||
truth_root_provider=lambda: (
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e46" / "results"
|
||||
),
|
||||
evaluation_pack_root_provider=lambda: (
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e2" / "evaluation-packs"
|
||||
),
|
||||
)
|
||||
)
|
||||
app.include_router(
|
||||
build_l34b_nested_box_consolidation_router(
|
||||
shadow_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "l3"
|
||||
/ "nested-box-consolidation-shadows"
|
||||
),
|
||||
l34_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "l3"
|
||||
/ "right-yolox-truth-island-freeze"
|
||||
),
|
||||
truth_root_provider=lambda: (
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e46" / "results"
|
||||
),
|
||||
evaluation_pack_root_provider=lambda: (
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e2" / "evaluation-packs"
|
||||
),
|
||||
)
|
||||
)
|
||||
app.include_router(
|
||||
build_l34c_tile_seam_stitch_router(
|
||||
shadow_root_provider=lambda: (
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "l3" / "tile-seam-stitch-shadows"
|
||||
),
|
||||
l34_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "l3"
|
||||
/ "right-yolox-truth-island-freeze"
|
||||
),
|
||||
truth_root_provider=lambda: (
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e46" / "results"
|
||||
),
|
||||
evaluation_pack_root_provider=lambda: (
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e2" / "evaluation-packs"
|
||||
),
|
||||
)
|
||||
)
|
||||
app.include_router(
|
||||
build_l34d_cumulative_postprocessing_router(
|
||||
candidate_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "l3"
|
||||
/ "cumulative-postprocessing-candidates"
|
||||
),
|
||||
l34_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "l3"
|
||||
/ "right-yolox-truth-island-freeze"
|
||||
),
|
||||
truth_root_provider=lambda: (
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e46" / "results"
|
||||
),
|
||||
evaluation_pack_root_provider=lambda: (
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e2" / "evaluation-packs"
|
||||
),
|
||||
)
|
||||
)
|
||||
app.include_router(
|
||||
build_l34d_blind_annotation_router(
|
||||
candidate_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "l3"
|
||||
/ "cumulative-postprocessing-candidates"
|
||||
),
|
||||
l34_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "l3"
|
||||
/ "right-yolox-truth-island-freeze"
|
||||
),
|
||||
truth_root_provider=lambda: (
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e46" / "results"
|
||||
),
|
||||
evaluation_pack_root_provider=lambda: (
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e2" / "evaluation-packs"
|
||||
),
|
||||
annotation_root_provider=lambda: (
|
||||
REPOSITORY_ROOT / ".runtime" / "laboratory-annotations" / "l34d-blind"
|
||||
),
|
||||
)
|
||||
)
|
||||
app.include_router(
|
||||
build_l34e_self_review_diagnostic_router(
|
||||
diagnostic_root_provider=lambda: (
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "l3" / "self-review-diagnostics"
|
||||
),
|
||||
candidate_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "l3"
|
||||
/ "cumulative-postprocessing-candidates"
|
||||
),
|
||||
l34_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "l3"
|
||||
/ "right-yolox-truth-island-freeze"
|
||||
),
|
||||
truth_root_provider=lambda: (
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e46" / "results"
|
||||
),
|
||||
evaluation_pack_root_provider=lambda: (
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e2" / "evaluation-packs"
|
||||
),
|
||||
)
|
||||
)
|
||||
app.include_router(
|
||||
build_l34f_adjudication_router(
|
||||
diagnostic_root_provider=lambda: (
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "l3" / "self-review-diagnostics"
|
||||
),
|
||||
candidate_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "l3"
|
||||
/ "cumulative-postprocessing-candidates"
|
||||
),
|
||||
l34_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "l3"
|
||||
/ "right-yolox-truth-island-freeze"
|
||||
),
|
||||
truth_root_provider=lambda: (
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e46" / "results"
|
||||
),
|
||||
evaluation_pack_root_provider=lambda: (
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e2" / "evaluation-packs"
|
||||
),
|
||||
adjudication_root_provider=lambda: (
|
||||
REPOSITORY_ROOT / ".runtime" / "laboratory-annotations" / "l34f-adjudication"
|
||||
),
|
||||
result_root_provider=lambda: (
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "l3" / "adjudicated-references"
|
||||
),
|
||||
)
|
||||
)
|
||||
app.include_router(
|
||||
build_e30_review_router(
|
||||
materialization_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "e30"
|
||||
/ "materializations"
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e30" / "materializations"
|
||||
),
|
||||
review_pack_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "e30"
|
||||
/ "review-packs"
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e30" / "review-packs"
|
||||
),
|
||||
)
|
||||
)
|
||||
app.include_router(
|
||||
build_e40_case_review_router(
|
||||
e40_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "e40"
|
||||
/ "results"
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e40" / "results"
|
||||
),
|
||||
operator_review_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "e40"
|
||||
/ "operator-reviews"
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e40" / "operator-reviews"
|
||||
),
|
||||
)
|
||||
)
|
||||
app.include_router(
|
||||
build_e30_engineering_router(
|
||||
generation_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "e30"
|
||||
/ "engineering-generations"
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e30" / "engineering-generations"
|
||||
),
|
||||
materialization_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "e30"
|
||||
/ "materializations"
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e30" / "materializations"
|
||||
),
|
||||
review_pack_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "e30"
|
||||
/ "review-packs"
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e30" / "review-packs"
|
||||
),
|
||||
)
|
||||
)
|
||||
app.include_router(
|
||||
build_e30_human_review_router(
|
||||
materialization_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "e30"
|
||||
/ "materializations"
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e30" / "materializations"
|
||||
),
|
||||
review_pack_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "e30"
|
||||
/ "review-packs"
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e30" / "review-packs"
|
||||
),
|
||||
engineering_generation_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "e30"
|
||||
/ "engineering-generations"
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e30" / "engineering-generations"
|
||||
),
|
||||
draft_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "e30"
|
||||
/ "human-review-drafts"
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e30" / "human-review-drafts"
|
||||
),
|
||||
generation_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
@@ -752,18 +1132,10 @@ app.include_router(
|
||||
build_artifact_health_router(
|
||||
runtime_root_provider=lambda: REPOSITORY_ROOT / ".runtime",
|
||||
e44_results_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "e44"
|
||||
/ "results"
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e44" / "results"
|
||||
),
|
||||
e50_results_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "e50"
|
||||
/ "results"
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e50" / "results"
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -0,0 +1,610 @@
|
||||
"""Source-only E46 review collection for two independent human slots."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import secrets
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path, PurePosixPath
|
||||
from typing import Annotated, Any, Final, Literal
|
||||
|
||||
from fastapi import APIRouter, Header, HTTPException, Query
|
||||
from fastapi import Path as ApiPath
|
||||
from fastapi.responses import FileResponse
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from k1link.artifacts import write_json_atomic
|
||||
from k1link.compute.e46_detector_truth_island import (
|
||||
E46_REFERENCES_NAME,
|
||||
E46DetectorTruthIsland,
|
||||
E46DetectorTruthIslandError,
|
||||
read_e46_detector_truth_island,
|
||||
)
|
||||
from k1link.compute.e46_lab_review_submission import (
|
||||
E46LabReviewSubmissionError,
|
||||
build_e46_lab_review_submission,
|
||||
read_e46_lab_review_submission,
|
||||
)
|
||||
from k1link.web.l34_annotation_api import (
|
||||
L34_ANNOTATION_LABEL_CATALOG_SCHEMA,
|
||||
L34_ANNOTATION_SESSION_CATALOG_SCHEMA,
|
||||
L34_ANNOTATION_SOURCE_CATALOG_SCHEMA,
|
||||
L34_ANNOTATION_SOURCE_SCHEMA,
|
||||
L34AnnotationCreateRequest,
|
||||
L34AnnotationSaveRequest,
|
||||
_L34AnnotationSessionStore,
|
||||
_normalize_object,
|
||||
_png_dimensions,
|
||||
_source_projection,
|
||||
)
|
||||
|
||||
_TRUTH_ID = re.compile(r"^e46-detector-truth-island-[a-f0-9]{64}$")
|
||||
_SESSION_ID = re.compile(r"^l34-annotation-session-[a-f0-9]{64}$")
|
||||
_SUBMISSION_ID = re.compile(r"^e46-lab-review-submission-[a-f0-9]{64}$")
|
||||
_PACK_ID = re.compile(r"^evaluation-pack-[a-f0-9]{64}$")
|
||||
_REVIEWER_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{1,63}$")
|
||||
_TARGET_CLASSES: Final = (
|
||||
"person",
|
||||
"bicycle",
|
||||
"motorcycle",
|
||||
"car",
|
||||
"heavy_vehicle",
|
||||
"static_obstacle",
|
||||
"animal",
|
||||
)
|
||||
_AUTHORITY: Final = {
|
||||
"ground_truth": False,
|
||||
"independent_truth": False,
|
||||
"metric_grade_reference": False,
|
||||
"candidate_accepted": False,
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
}
|
||||
|
||||
RootProvider = Callable[[], Path | None]
|
||||
|
||||
|
||||
class E46ReviewFreezeRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
expected_revision: int = Field(ge=1)
|
||||
reviewer_id: str = Field(min_length=2, max_length=64)
|
||||
independent_attestation: Literal[True]
|
||||
candidate_identity_not_seen: Literal[True]
|
||||
model_material_not_seen: Literal[True]
|
||||
|
||||
|
||||
def build_e46_blind_review_router(
|
||||
*,
|
||||
truth_root_provider: RootProvider = lambda: None,
|
||||
evaluation_pack_root_provider: RootProvider = lambda: None,
|
||||
annotation_root_provider: RootProvider = lambda: None,
|
||||
submission_root_provider: RootProvider = lambda: None,
|
||||
) -> APIRouter:
|
||||
router = APIRouter(prefix="/api/v1/laboratory/e46", tags=["laboratory"])
|
||||
store = _L34AnnotationSessionStore(
|
||||
annotation_root_provider,
|
||||
result_id_pattern=_TRUTH_ID,
|
||||
lab_label="E46",
|
||||
initial_assistance_mode="prediction-free-manual",
|
||||
allowed_assistance_modes=frozenset({"prediction-free-manual"}),
|
||||
)
|
||||
|
||||
def require_capability(
|
||||
result_id: str,
|
||||
session_id: str,
|
||||
capability: str | None,
|
||||
) -> None:
|
||||
sidecar = store.session_path(result_id, session_id).with_suffix(
|
||||
".capability.json"
|
||||
)
|
||||
try:
|
||||
document = json.loads(sidecar.read_text(encoding="utf-8"))
|
||||
except (OSError, ValueError):
|
||||
raise HTTPException(status_code=403, detail="E46 reviewer slot недоступен") from None
|
||||
expected = document.get("capability") if isinstance(document, dict) else None
|
||||
if (
|
||||
not isinstance(capability, str)
|
||||
or not isinstance(expected, str)
|
||||
or not secrets.compare_digest(capability, expected)
|
||||
):
|
||||
raise HTTPException(status_code=403, detail="E46 reviewer slot недоступен")
|
||||
|
||||
def truth(result_id: str) -> tuple[E46DetectorTruthIsland, tuple[dict[str, Any], ...]]:
|
||||
if _TRUTH_ID.fullmatch(result_id) is None:
|
||||
raise HTTPException(status_code=404, detail="E46 truth island не найден")
|
||||
root = _configured_root(truth_root_provider)
|
||||
if root is None:
|
||||
raise HTTPException(status_code=404, detail="E46 truth island не найден")
|
||||
path = (root / result_id).resolve()
|
||||
if path.parent != root or path.is_symlink():
|
||||
raise HTTPException(status_code=404, detail="E46 truth island не найден")
|
||||
try:
|
||||
result = read_e46_detector_truth_island(path)
|
||||
references = tuple(_read_jsonl(result.result_root / E46_REFERENCES_NAME))
|
||||
except (E46DetectorTruthIslandError, OSError, ValueError):
|
||||
raise HTTPException(status_code=404, detail="E46 truth island не найден") from None
|
||||
return result, references
|
||||
|
||||
def source(
|
||||
result_id: str,
|
||||
sequence: int,
|
||||
) -> tuple[E46DetectorTruthIsland, dict[str, Any], Path]:
|
||||
result, references = truth(result_id)
|
||||
reference = next(
|
||||
(item for item in references if item.get("truth_island_sequence") == sequence),
|
||||
None,
|
||||
)
|
||||
if reference is None:
|
||||
raise HTTPException(status_code=404, detail="E46 source frame не найден")
|
||||
identity = result.manifest.get("identity")
|
||||
source_identity = identity.get("source") if isinstance(identity, dict) else None
|
||||
pack_id = (
|
||||
source_identity.get("evaluation_pack_id")
|
||||
if isinstance(source_identity, dict)
|
||||
else None
|
||||
)
|
||||
pack_root = _configured_root(evaluation_pack_root_provider)
|
||||
if (
|
||||
pack_root is None
|
||||
or not isinstance(pack_id, str)
|
||||
or _PACK_ID.fullmatch(pack_id) is None
|
||||
):
|
||||
raise HTTPException(status_code=404, detail="E46 image source не найден")
|
||||
pack = (pack_root / pack_id).resolve()
|
||||
relative = reference.get("source_path")
|
||||
if not isinstance(relative, str):
|
||||
raise HTTPException(status_code=404, detail="E46 image source не найден")
|
||||
pure = PurePosixPath(relative)
|
||||
if (
|
||||
pack.parent != pack_root
|
||||
or pack.is_symlink()
|
||||
or not pack.is_dir()
|
||||
or pure.is_absolute()
|
||||
or ".." in pure.parts
|
||||
or pure.parts[:2] != ("images", "valid-fov-fill")
|
||||
or pure.suffix.lower() != ".png"
|
||||
):
|
||||
raise HTTPException(status_code=404, detail="E46 image source не найден")
|
||||
image = (pack / Path(*pure.parts)).resolve()
|
||||
if (
|
||||
not image.is_relative_to(pack)
|
||||
or image.is_symlink()
|
||||
or not image.is_file()
|
||||
or image.stat().st_size != reference.get("byte_length")
|
||||
or _sha256(image) != reference.get("sha256")
|
||||
):
|
||||
raise HTTPException(status_code=404, detail="E46 image source изменён")
|
||||
return result, reference, image
|
||||
|
||||
@router.get("/results")
|
||||
def list_results(limit: int = Query(default=1, ge=1, le=10)) -> dict[str, object]:
|
||||
candidates = _truth_candidates(truth_root_provider)
|
||||
items: list[dict[str, object]] = []
|
||||
invalid_total = 0
|
||||
for path in candidates:
|
||||
try:
|
||||
result, references = truth(path.name)
|
||||
if len(items) < limit:
|
||||
items.append(
|
||||
_project_result(
|
||||
result,
|
||||
references,
|
||||
sessions=store.list(result.result_id),
|
||||
submissions=_submissions_for_truth(
|
||||
submission_root_provider,
|
||||
result.result_root,
|
||||
result.result_id,
|
||||
),
|
||||
)
|
||||
)
|
||||
except HTTPException:
|
||||
invalid_total += 1
|
||||
return {
|
||||
"schema_version": "missioncore.e46-detector-truth-island-catalog/v1",
|
||||
"configured": _configured_root(truth_root_provider) is not None,
|
||||
"items": items,
|
||||
"candidate_total": len(candidates),
|
||||
"invalid_total": invalid_total,
|
||||
"access": "read-only",
|
||||
}
|
||||
|
||||
@router.get("/results/{result_id}/annotation-source")
|
||||
def get_source_catalog(result_id: str) -> dict[str, object]:
|
||||
result, references = truth(result_id)
|
||||
frames: list[dict[str, object]] = []
|
||||
for reference in references:
|
||||
_, _, image = source(result_id, int(reference["truth_island_sequence"]))
|
||||
width, height = _png_dimensions(image)
|
||||
frames.append(_source_projection(reference, width=width, height=height))
|
||||
return {
|
||||
"schema_version": L34_ANNOTATION_SOURCE_CATALOG_SCHEMA,
|
||||
"result_id": result.result_id,
|
||||
"truth_island_id": result.result_id,
|
||||
"contract": _review_contract(),
|
||||
"frames": frames,
|
||||
"frame_count": len(frames),
|
||||
"model_material_included": False,
|
||||
"candidate_identity_included": False,
|
||||
"candidate_predictions_included": False,
|
||||
"prelabels_included": False,
|
||||
"reviewer_slots": ["A", "B"],
|
||||
"access": "prediction-free-independent-review-source-read-only",
|
||||
}
|
||||
|
||||
@router.get("/results/{result_id}/annotation-source/frames/{sequence}")
|
||||
def get_source_frame(result_id: str, sequence: int) -> dict[str, object]:
|
||||
_, reference, image = source(result_id, sequence)
|
||||
width, height = _png_dimensions(image)
|
||||
return {
|
||||
"schema_version": L34_ANNOTATION_SOURCE_SCHEMA,
|
||||
"result_id": result_id,
|
||||
**_source_projection(reference, width=width, height=height),
|
||||
"camera_url": (
|
||||
f"/api/v1/laboratory/e46/results/{result_id}"
|
||||
f"/annotation-source/frames/{sequence}/camera"
|
||||
),
|
||||
"model_material_included": False,
|
||||
"candidate_identity_included": False,
|
||||
"candidate_predictions_included": False,
|
||||
"prelabels_included": False,
|
||||
"access": "prediction-free-independent-review-source-read-only",
|
||||
}
|
||||
|
||||
@router.get("/results/{result_id}/annotation-source/frames/{sequence}/camera")
|
||||
def get_source_camera(result_id: str, sequence: int) -> FileResponse:
|
||||
_, _, image = source(result_id, sequence)
|
||||
return FileResponse(image, media_type="image/png")
|
||||
|
||||
@router.get("/results/{result_id}/annotation-sessions")
|
||||
def list_sessions(result_id: str) -> dict[str, object]:
|
||||
truth(result_id)
|
||||
items = store.list(result_id)
|
||||
return {
|
||||
"schema_version": L34_ANNOTATION_SESSION_CATALOG_SCHEMA,
|
||||
"result_id": result_id,
|
||||
"items": items,
|
||||
"total": len(items),
|
||||
"maximum_sessions": 2,
|
||||
"access": "prediction-free-independent-review-summaries",
|
||||
}
|
||||
|
||||
@router.get("/results/{result_id}/annotation-labels")
|
||||
def get_labels(result_id: str) -> dict[str, object]:
|
||||
truth(result_id)
|
||||
return {
|
||||
"schema_version": L34_ANNOTATION_LABEL_CATALOG_SCHEMA,
|
||||
"result_id": result_id,
|
||||
"items": [],
|
||||
"total": 0,
|
||||
"access": "prediction-free-fixed-taxonomy",
|
||||
}
|
||||
|
||||
@router.post("/results/{result_id}/annotation-sessions")
|
||||
def create_session(
|
||||
result_id: str,
|
||||
request: L34AnnotationCreateRequest,
|
||||
) -> dict[str, object]:
|
||||
result, _ = truth(result_id)
|
||||
expected = "l34-annotation-session-" + hashlib.sha256(
|
||||
f"{result_id}\n{request.idempotency_key}".encode()
|
||||
).hexdigest()
|
||||
existing = store.list(result_id)
|
||||
if len(existing) >= 2 and all(item["session_id"] != expected for item in existing):
|
||||
raise HTTPException(status_code=409, detail="E46 допускает только Reviewer A и B")
|
||||
created = store.create(
|
||||
result_id=result_id,
|
||||
truth_island_id=result.result_id,
|
||||
idempotency_key=request.idempotency_key,
|
||||
)
|
||||
capability_path = store.session_path(
|
||||
result_id,
|
||||
str(created["session_id"]),
|
||||
).with_suffix(".capability.json")
|
||||
if capability_path.exists():
|
||||
try:
|
||||
capability_document = json.loads(
|
||||
capability_path.read_text(encoding="utf-8")
|
||||
)
|
||||
capability = capability_document["capability"]
|
||||
except (KeyError, OSError, TypeError, ValueError):
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail="E46 reviewer slot не прошёл проверку",
|
||||
) from None
|
||||
else:
|
||||
capability = secrets.token_urlsafe(32)
|
||||
write_json_atomic(
|
||||
capability_path,
|
||||
{
|
||||
"schema_version": "missioncore.e46-review-capability/v1",
|
||||
"result_id": result_id,
|
||||
"session_id": created["session_id"],
|
||||
"capability": capability,
|
||||
},
|
||||
)
|
||||
capability_path.chmod(0o600)
|
||||
return {**created, "review_capability": capability}
|
||||
|
||||
@router.get("/results/{result_id}/annotation-sessions/{session_id}")
|
||||
def get_session(
|
||||
result_id: str,
|
||||
session_id: Annotated[str, ApiPath(pattern=_SESSION_ID.pattern)],
|
||||
x_e46_review_capability: Annotated[
|
||||
str | None,
|
||||
Header(alias="X-E46-Review-Capability"),
|
||||
] = None,
|
||||
) -> dict[str, object]:
|
||||
truth(result_id)
|
||||
require_capability(result_id, session_id, x_e46_review_capability)
|
||||
return store.get(result_id, session_id)
|
||||
|
||||
@router.put("/results/{result_id}/annotation-sessions/{session_id}")
|
||||
def save_session(
|
||||
result_id: str,
|
||||
session_id: Annotated[str, ApiPath(pattern=_SESSION_ID.pattern)],
|
||||
request: L34AnnotationSaveRequest,
|
||||
x_e46_review_capability: Annotated[
|
||||
str | None,
|
||||
Header(alias="X-E46-Review-Capability"),
|
||||
] = None,
|
||||
) -> dict[str, object]:
|
||||
require_capability(result_id, session_id, x_e46_review_capability)
|
||||
result, _ = truth(result_id)
|
||||
if any(
|
||||
item["annotation_session_id"] == session_id
|
||||
for item in _submissions_for_truth(
|
||||
submission_root_provider,
|
||||
result.result_root,
|
||||
result.result_id,
|
||||
)
|
||||
):
|
||||
raise HTTPException(status_code=409, detail="E46 reviewer slot уже заморожен")
|
||||
if request.assistance_mode != "prediction-free-manual":
|
||||
raise HTTPException(
|
||||
status_code=422,
|
||||
detail="E46 принимает только prediction-free разметку",
|
||||
)
|
||||
sequences: set[int] = set()
|
||||
normalized: list[dict[str, object]] = []
|
||||
for frame in request.frames:
|
||||
sequence = frame.truth_island_sequence
|
||||
if sequence in sequences:
|
||||
raise HTTPException(status_code=422, detail="Кадр размечен дважды")
|
||||
sequences.add(sequence)
|
||||
_, reference, image = source(result_id, sequence)
|
||||
width, height = _png_dimensions(image)
|
||||
if any(
|
||||
item.origin != "manual" or item.category == "unmapped"
|
||||
for item in frame.objects
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=422,
|
||||
detail="E46 принимает только ручные canonical-классы",
|
||||
)
|
||||
objects = [
|
||||
_normalize_object(item, width=width, height=height)
|
||||
for item in frame.objects
|
||||
]
|
||||
if frame.hard_negative != (len(objects) == 0):
|
||||
raise HTTPException(status_code=422, detail="Пустой кадр должен быть явно отмечен")
|
||||
normalized.append(
|
||||
{
|
||||
"truth_island_sequence": sequence,
|
||||
"image_id": int(reference["image_id"]),
|
||||
"frame_index": int(reference["frame_index"]),
|
||||
"source_sha256": str(reference["sha256"]),
|
||||
"reviewed": True,
|
||||
"hard_negative": frame.hard_negative,
|
||||
"objects": objects,
|
||||
}
|
||||
)
|
||||
normalized.sort(key=lambda item: int(item["truth_island_sequence"]))
|
||||
return store.save(
|
||||
result_id=result_id,
|
||||
session_id=session_id,
|
||||
request=request,
|
||||
frames=normalized,
|
||||
)
|
||||
|
||||
@router.post("/results/{result_id}/annotation-sessions/{session_id}/freeze")
|
||||
def freeze_session(
|
||||
result_id: str,
|
||||
session_id: Annotated[str, ApiPath(pattern=_SESSION_ID.pattern)],
|
||||
request: E46ReviewFreezeRequest,
|
||||
x_e46_review_capability: Annotated[
|
||||
str | None,
|
||||
Header(alias="X-E46-Review-Capability"),
|
||||
] = None,
|
||||
) -> dict[str, object]:
|
||||
result, _ = truth(result_id)
|
||||
require_capability(result_id, session_id, x_e46_review_capability)
|
||||
session = store.get(result_id, session_id)
|
||||
if session["revision"] != request.expected_revision:
|
||||
raise HTTPException(status_code=409, detail="E46-сессия уже изменена")
|
||||
if not session["progress"]["complete"]:
|
||||
raise HTTPException(status_code=422, detail="Для freeze нужны 32/32 кадров")
|
||||
reviewer_id = request.reviewer_id.strip()
|
||||
if _REVIEWER_ID.fullmatch(reviewer_id) is None:
|
||||
raise HTTPException(status_code=422, detail="Некорректный reviewer id")
|
||||
root = submission_root_provider()
|
||||
if root is None:
|
||||
raise HTTPException(status_code=503, detail="Хранилище E46 review не настроено")
|
||||
existing = _submissions_for_truth(
|
||||
submission_root_provider,
|
||||
result.result_root,
|
||||
result.result_id,
|
||||
)
|
||||
for item in existing:
|
||||
if item["annotation_session_id"] == session_id:
|
||||
if item["reviewer_id"] == reviewer_id:
|
||||
return item
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail="E46 reviewer slot уже заморожен другим reviewer id",
|
||||
)
|
||||
if item["reviewer_id"] == reviewer_id and item["annotation_session_id"] != session_id:
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail="Reviewer id уже использован другим слотом",
|
||||
)
|
||||
try:
|
||||
frozen = build_e46_lab_review_submission(
|
||||
truth_island_root=result.result_root,
|
||||
annotation_session_path=store.session_path(result_id, session_id),
|
||||
reviewer_id=reviewer_id,
|
||||
output_root=root,
|
||||
)
|
||||
except (E46LabReviewSubmissionError, OSError) as exc:
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail="E46 review freeze не прошёл проверку",
|
||||
) from exc
|
||||
return _project_submission(frozen)
|
||||
|
||||
return router
|
||||
|
||||
|
||||
def _project_result(
|
||||
result: E46DetectorTruthIsland,
|
||||
references: tuple[dict[str, Any], ...],
|
||||
*,
|
||||
sessions: list[dict[str, object]],
|
||||
submissions: list[dict[str, object]],
|
||||
) -> dict[str, object]:
|
||||
selection = result.report["selection"]
|
||||
source = result.manifest["identity"]["source"]
|
||||
return {
|
||||
"result_id": result.result_id,
|
||||
"created_at_utc": result.manifest["created_at_utc"],
|
||||
"status": result.report["status"],
|
||||
"source_session_id": source["session_id"],
|
||||
"camera_source_id": source["source_id"],
|
||||
"metrics": {
|
||||
"frame_count": len(references),
|
||||
"anchor_count": selection["anchor_count"],
|
||||
"temporal_frame_count": selection["temporal_frame_count"],
|
||||
"temporal_group_count": selection["temporal_group_count"],
|
||||
"review_session_count": len(sessions),
|
||||
"completed_submission_count": len(submissions),
|
||||
"required_submission_count": 2,
|
||||
},
|
||||
"review_sessions": copy.deepcopy(sessions),
|
||||
"submissions": copy.deepcopy(submissions),
|
||||
"decision": {
|
||||
"collection_ready": True,
|
||||
"two_independent_reviews_complete": len(submissions) == 2,
|
||||
"adjudication_ready": len(submissions) == 2,
|
||||
"e48_sealed": False,
|
||||
"l35_open": False,
|
||||
"next_action": (
|
||||
"complete two distinct prediction-free reviewer submissions"
|
||||
if len(submissions) < 2
|
||||
else "adjudicate both frozen reviews before E48 seal"
|
||||
),
|
||||
},
|
||||
"blindness": {
|
||||
"candidate_identity_included": False,
|
||||
"model_prelabels_included": False,
|
||||
"model_predictions_included": False,
|
||||
"model_scores_included": False,
|
||||
},
|
||||
"authority": _AUTHORITY,
|
||||
"ground_truth": False,
|
||||
"access": "review-collection-read-only-summary",
|
||||
}
|
||||
|
||||
|
||||
def _project_submission(result: dict[str, Any]) -> dict[str, object]:
|
||||
identity = result["manifest"]["identity"]
|
||||
return {
|
||||
"result_id": result["result_id"],
|
||||
"created_at_utc": result["manifest"]["created_at_utc"],
|
||||
"truth_island_id": identity["truth_island_id"],
|
||||
"annotation_session_id": identity["annotation_session_id"],
|
||||
"reviewer_id": identity["reviewer_id"],
|
||||
"state": result["manifest"]["state"],
|
||||
"frame_count": len(result["review"]["images"]),
|
||||
"object_count": sum(len(item["objects"]) for item in result["review"]["images"]),
|
||||
"blindness": copy.deepcopy(identity["blindness"]),
|
||||
"authority": copy.deepcopy(identity["authority"]),
|
||||
"ground_truth": False,
|
||||
"access": "immutable-e48-review-input",
|
||||
}
|
||||
|
||||
|
||||
def _submissions_for_truth(
|
||||
provider: RootProvider,
|
||||
truth_root: Path,
|
||||
truth_id: str,
|
||||
) -> list[dict[str, object]]:
|
||||
root = _configured_root(provider)
|
||||
if root is None:
|
||||
return []
|
||||
items: list[dict[str, object]] = []
|
||||
for path in sorted(root.iterdir(), key=lambda item: item.stat().st_mtime_ns):
|
||||
if not path.is_dir() or path.is_symlink() or _SUBMISSION_ID.fullmatch(path.name) is None:
|
||||
continue
|
||||
try:
|
||||
result = read_e46_lab_review_submission(path, truth_island_root=truth_root)
|
||||
if result["manifest"]["identity"]["truth_island_id"] == truth_id:
|
||||
items.append(_project_submission(result))
|
||||
except (E46LabReviewSubmissionError, OSError):
|
||||
continue
|
||||
return items
|
||||
|
||||
|
||||
def _review_contract() -> dict[str, object]:
|
||||
return {
|
||||
"contract_id": "e46-detector-blind-review/v1",
|
||||
"classes": list(_TARGET_CLASSES),
|
||||
"unmapped_class": None,
|
||||
"box_format": "xyxy-pixels-800x600",
|
||||
"all_identifiable_instances_required": True,
|
||||
"inside_valid_fov_only": True,
|
||||
"hard_negative_frame_flag_required": True,
|
||||
"occluded_and_truncated_flags_required": True,
|
||||
}
|
||||
|
||||
|
||||
def _truth_candidates(provider: RootProvider) -> list[Path]:
|
||||
root = _configured_root(provider)
|
||||
if root is None:
|
||||
return []
|
||||
return sorted(
|
||||
(
|
||||
path
|
||||
for path in root.iterdir()
|
||||
if path.is_dir() and not path.is_symlink() and _TRUTH_ID.fullmatch(path.name)
|
||||
),
|
||||
key=lambda path: path.stat().st_mtime_ns,
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
|
||||
def _configured_root(provider: RootProvider) -> Path | None:
|
||||
value = provider()
|
||||
if value is None:
|
||||
return None
|
||||
candidate = value.expanduser().absolute()
|
||||
if candidate.is_symlink():
|
||||
return None
|
||||
try:
|
||||
root = candidate.resolve(strict=True)
|
||||
except OSError:
|
||||
return None
|
||||
return root if root.is_dir() else None
|
||||
|
||||
|
||||
def _read_jsonl(path: Path) -> list[dict[str, Any]]:
|
||||
rows = [json.loads(line) for line in path.read_text(encoding="utf-8").splitlines() if line]
|
||||
if any(not isinstance(row, dict) for row in rows):
|
||||
raise ValueError("invalid E46 references")
|
||||
return rows
|
||||
|
||||
|
||||
def _sha256(path: Path) -> str:
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
@@ -0,0 +1,383 @@
|
||||
"""Read-only LAB projection for E46A AI engineering preannotations."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import re
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
from typing import Annotated, Any
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from fastapi import Path as ApiPath
|
||||
|
||||
from k1link.compute.e46a_ai_engineering_preannotation import (
|
||||
E46AAiEngineeringPreannotationError,
|
||||
read_e46a_ai_engineering_preannotation,
|
||||
)
|
||||
from k1link.web.l34_annotation_api import (
|
||||
L34_ANNOTATION_LABEL_CATALOG_SCHEMA,
|
||||
L34_ANNOTATION_SEED_SCHEMA,
|
||||
L34_ANNOTATION_SESSION_CATALOG_SCHEMA,
|
||||
L34_ANNOTATION_SOURCE_CATALOG_SCHEMA,
|
||||
L34_ANNOTATION_SOURCE_SCHEMA,
|
||||
L34AnnotationCreateRequest,
|
||||
L34AnnotationSaveRequest,
|
||||
_annotation_contract,
|
||||
_L34AnnotationSessionStore,
|
||||
_normalize_object,
|
||||
)
|
||||
|
||||
_RESULT_ID = re.compile(r"^e46a-ai-engineering-preannotation-[a-f0-9]{64}$")
|
||||
_SESSION_ID = re.compile(r"^l34-annotation-session-[a-f0-9]{64}$")
|
||||
RootProvider = Callable[[], Path | None]
|
||||
|
||||
|
||||
def build_e46a_ai_engineering_preannotation_router(
|
||||
*,
|
||||
root_provider: RootProvider = lambda: None,
|
||||
annotation_root_provider: RootProvider = lambda: None,
|
||||
) -> APIRouter:
|
||||
router = APIRouter(prefix="/api/v1/laboratory/e46a", tags=["laboratory"])
|
||||
store = _L34AnnotationSessionStore(
|
||||
annotation_root_provider,
|
||||
result_id_pattern=_RESULT_ID,
|
||||
lab_label="E46A correction",
|
||||
initial_assistance_mode="frozen-candidate-seeded",
|
||||
allowed_assistance_modes=frozenset({"frozen-candidate-seeded"}),
|
||||
contract_id_overrides={
|
||||
"frozen-candidate-seeded": "e46a-ai-engineering-correction/v1"
|
||||
},
|
||||
)
|
||||
|
||||
def result(result_id: str) -> dict[str, Any]:
|
||||
if _RESULT_ID.fullmatch(result_id) is None:
|
||||
raise HTTPException(status_code=404, detail="E46A result не найден")
|
||||
root = _configured_root(root_provider)
|
||||
if root is None:
|
||||
raise HTTPException(status_code=404, detail="E46A result не найден")
|
||||
path = (root / result_id).resolve()
|
||||
if path.parent != root or path.is_symlink():
|
||||
raise HTTPException(status_code=404, detail="E46A result не найден")
|
||||
try:
|
||||
return read_e46a_ai_engineering_preannotation(path)
|
||||
except (E46AAiEngineeringPreannotationError, OSError, ValueError):
|
||||
raise HTTPException(status_code=404, detail="E46A result не найден") from None
|
||||
|
||||
@router.get("/results")
|
||||
def list_results(limit: int = Query(default=1, ge=1, le=10)) -> dict[str, object]:
|
||||
candidates = _candidates(root_provider)
|
||||
items: list[dict[str, object]] = []
|
||||
invalid_total = 0
|
||||
for path in candidates:
|
||||
try:
|
||||
frozen = result(path.name)
|
||||
if len(items) < limit:
|
||||
items.append(_project_result(frozen))
|
||||
except HTTPException:
|
||||
invalid_total += 1
|
||||
return {
|
||||
"schema_version": "missioncore.e46a-ai-engineering-preannotation-catalog/v1",
|
||||
"configured": _configured_root(root_provider) is not None,
|
||||
"items": items,
|
||||
"candidate_total": len(candidates),
|
||||
"invalid_total": invalid_total,
|
||||
"access": "read-only-non-truth",
|
||||
}
|
||||
|
||||
@router.get("/results/{result_id}/cases/{sequence}")
|
||||
def get_case(result_id: str, sequence: int) -> dict[str, object]:
|
||||
if sequence < 1 or sequence > 32:
|
||||
raise HTTPException(status_code=404, detail="E46A frame не найден")
|
||||
frozen = result(result_id)
|
||||
case = next(
|
||||
(
|
||||
item
|
||||
for item in frozen["cases"]
|
||||
if item.get("truth_island_sequence") == sequence
|
||||
),
|
||||
None,
|
||||
)
|
||||
if case is None:
|
||||
raise HTTPException(status_code=404, detail="E46A frame не найден")
|
||||
identity = frozen["manifest"].get("identity")
|
||||
e46_source = identity.get("e46_source") if isinstance(identity, dict) else None
|
||||
e46_result_id = (
|
||||
e46_source.get("result_id") if isinstance(e46_source, dict) else None
|
||||
)
|
||||
if not isinstance(e46_result_id, str):
|
||||
raise HTTPException(status_code=404, detail="E46A source не найден")
|
||||
return {
|
||||
**copy.deepcopy(case),
|
||||
"result_id": result_id,
|
||||
"camera_url": (
|
||||
f"/api/v1/laboratory/e46/results/{e46_result_id}"
|
||||
f"/annotation-source/frames/{sequence}/camera"
|
||||
),
|
||||
"candidate_identity_included": True,
|
||||
"model_predictions_seen_during_source_adjudication": True,
|
||||
"model_scores_included": False,
|
||||
"independent_review": False,
|
||||
"ground_truth": False,
|
||||
"access": "read-only-ai-engineering-preannotation-not-truth",
|
||||
}
|
||||
|
||||
def annotation_case(result_id: str, sequence: int) -> tuple[dict[str, Any], dict[str, Any]]:
|
||||
frozen = result(result_id)
|
||||
case = next(
|
||||
(
|
||||
item
|
||||
for item in frozen["cases"]
|
||||
if item.get("truth_island_sequence") == sequence
|
||||
),
|
||||
None,
|
||||
)
|
||||
if case is None:
|
||||
raise HTTPException(status_code=404, detail="E46A frame не найден")
|
||||
return frozen, case
|
||||
|
||||
def source_projection(case: dict[str, Any]) -> dict[str, object]:
|
||||
group_id = str(case["group_id"])
|
||||
return {
|
||||
"truth_island_sequence": int(case["truth_island_sequence"]),
|
||||
"image_id": int(case["image_id"]),
|
||||
"frame_index": int(case["frame_index"]),
|
||||
"group_id": group_id,
|
||||
"role": "temporal" if group_id.startswith("clip-") else "anchor",
|
||||
"session_seconds": float(case["session_seconds"]),
|
||||
"camera": {
|
||||
"width": 800,
|
||||
"height": 600,
|
||||
"sha256": str(case["source_image_sha256"]),
|
||||
},
|
||||
}
|
||||
|
||||
def e46_source_id(frozen: dict[str, Any]) -> str:
|
||||
identity = frozen["manifest"].get("identity")
|
||||
source = identity.get("e46_source") if isinstance(identity, dict) else None
|
||||
source_id = source.get("result_id") if isinstance(source, dict) else None
|
||||
if not isinstance(source_id, str):
|
||||
raise HTTPException(status_code=404, detail="E46A source не найден")
|
||||
return source_id
|
||||
|
||||
@router.get("/results/{result_id}/annotation-source")
|
||||
def get_annotation_source_catalog(result_id: str) -> dict[str, object]:
|
||||
frozen = result(result_id)
|
||||
frames = [source_projection(case) for case in frozen["cases"]]
|
||||
return {
|
||||
"schema_version": L34_ANNOTATION_SOURCE_CATALOG_SCHEMA,
|
||||
"result_id": result_id,
|
||||
"truth_island_id": e46_source_id(frozen),
|
||||
"contract": _annotation_contract(
|
||||
contract_id="e46a-ai-engineering-correction/v1"
|
||||
),
|
||||
"frames": frames,
|
||||
"frame_count": len(frames),
|
||||
"model_material_included": False,
|
||||
"candidate_identity_included": True,
|
||||
"candidate_predictions_included": False,
|
||||
"prelabels_included": False,
|
||||
"access": "ai-engineering-correction-source-read-only",
|
||||
}
|
||||
|
||||
@router.get("/results/{result_id}/annotation-source/frames/{sequence}")
|
||||
def get_annotation_source_frame(result_id: str, sequence: int) -> dict[str, object]:
|
||||
frozen, case = annotation_case(result_id, sequence)
|
||||
source_id = e46_source_id(frozen)
|
||||
return {
|
||||
"schema_version": L34_ANNOTATION_SOURCE_SCHEMA,
|
||||
"result_id": result_id,
|
||||
**source_projection(case),
|
||||
"camera_url": (
|
||||
f"/api/v1/laboratory/e46/results/{source_id}"
|
||||
f"/annotation-source/frames/{sequence}/camera"
|
||||
),
|
||||
"model_material_included": False,
|
||||
"candidate_identity_included": True,
|
||||
"candidate_predictions_included": False,
|
||||
"prelabels_included": False,
|
||||
"access": "ai-engineering-correction-source-read-only",
|
||||
}
|
||||
|
||||
@router.get("/results/{result_id}/annotation-seed/frames/{sequence}")
|
||||
def get_annotation_seed_frame(result_id: str, sequence: int) -> dict[str, object]:
|
||||
_, case = annotation_case(result_id, sequence)
|
||||
objects: list[dict[str, object]] = []
|
||||
for raw in case["objects"]:
|
||||
category = str(raw["category"])
|
||||
proposed_label: str | None = None
|
||||
if category == "stroller":
|
||||
category = "unmapped"
|
||||
proposed_label = "Коляска"
|
||||
elif category == "laptop":
|
||||
category = "unmapped"
|
||||
proposed_label = "Ноутбук"
|
||||
objects.append(
|
||||
{
|
||||
"object_id": str(raw["object_id"]),
|
||||
"category": category,
|
||||
"proposed_label": proposed_label,
|
||||
"origin": "frozen_candidate_seed",
|
||||
"box_xyxy": copy.deepcopy(raw["box_xyxy"]),
|
||||
"occluded": bool(raw["occluded"]),
|
||||
"truncated": bool(raw["truncated"]),
|
||||
}
|
||||
)
|
||||
return {
|
||||
"schema_version": L34_ANNOTATION_SEED_SCHEMA,
|
||||
"result_id": result_id,
|
||||
"truth_island_sequence": sequence,
|
||||
"source_sha256": str(case["source_image_sha256"]),
|
||||
"objects": objects,
|
||||
"assistance": {
|
||||
"mode": "frozen-candidate-seeded",
|
||||
"independent_truth_eligible": False,
|
||||
},
|
||||
"model_material_included": True,
|
||||
"access": "ai-engineering-preannotation-seed-read-only",
|
||||
}
|
||||
|
||||
@router.get("/results/{result_id}/annotation-sessions")
|
||||
def list_annotation_sessions(result_id: str) -> dict[str, object]:
|
||||
annotation_case(result_id, 1)
|
||||
items = store.list(result_id)
|
||||
return {
|
||||
"schema_version": L34_ANNOTATION_SESSION_CATALOG_SCHEMA,
|
||||
"result_id": result_id,
|
||||
"items": items,
|
||||
"total": len(items),
|
||||
"access": "ai-engineering-human-correction",
|
||||
}
|
||||
|
||||
@router.get("/results/{result_id}/annotation-labels")
|
||||
def get_annotation_labels(result_id: str) -> dict[str, object]:
|
||||
annotation_case(result_id, 1)
|
||||
values = {"коляска": "Коляска", "ноутбук": "Ноутбук"}
|
||||
for item in store.labels(result_id):
|
||||
label = str(item["value"])
|
||||
values[label.casefold()] = label
|
||||
items = [
|
||||
{"value": value, "normalization_state": "pending-adjudication"}
|
||||
for value in sorted(values.values(), key=str.casefold)
|
||||
]
|
||||
return {
|
||||
"schema_version": L34_ANNOTATION_LABEL_CATALOG_SCHEMA,
|
||||
"result_id": result_id,
|
||||
"items": items,
|
||||
"total": len(items),
|
||||
"access": "ai-engineering-correction-taxonomy",
|
||||
}
|
||||
|
||||
@router.post("/results/{result_id}/annotation-sessions")
|
||||
def create_annotation_session(
|
||||
result_id: str,
|
||||
request: L34AnnotationCreateRequest,
|
||||
) -> dict[str, object]:
|
||||
frozen, _ = annotation_case(result_id, 1)
|
||||
return store.create(
|
||||
result_id=result_id,
|
||||
truth_island_id=e46_source_id(frozen),
|
||||
idempotency_key=request.idempotency_key,
|
||||
)
|
||||
|
||||
@router.get("/results/{result_id}/annotation-sessions/{session_id}")
|
||||
def get_annotation_session(
|
||||
result_id: str,
|
||||
session_id: Annotated[str, ApiPath(pattern=_SESSION_ID.pattern)],
|
||||
) -> dict[str, object]:
|
||||
annotation_case(result_id, 1)
|
||||
return store.get(result_id, session_id)
|
||||
|
||||
@router.put("/results/{result_id}/annotation-sessions/{session_id}")
|
||||
def save_annotation_session(
|
||||
result_id: str,
|
||||
session_id: Annotated[str, ApiPath(pattern=_SESSION_ID.pattern)],
|
||||
request: L34AnnotationSaveRequest,
|
||||
) -> dict[str, object]:
|
||||
if request.assistance_mode != "frozen-candidate-seeded":
|
||||
raise HTTPException(status_code=422, detail="Режим E46A correction запрещён")
|
||||
sequences: set[int] = set()
|
||||
frames: list[dict[str, object]] = []
|
||||
for frame in request.frames:
|
||||
sequence = frame.truth_island_sequence
|
||||
if sequence in sequences:
|
||||
raise HTTPException(status_code=422, detail="Кадр размечен дважды")
|
||||
sequences.add(sequence)
|
||||
_, case = annotation_case(result_id, sequence)
|
||||
objects = [
|
||||
_normalize_object(item, width=800, height=600) for item in frame.objects
|
||||
]
|
||||
if frame.hard_negative != (len(objects) == 0):
|
||||
raise HTTPException(
|
||||
status_code=422,
|
||||
detail="Пустой кадр должен быть явно отмечен",
|
||||
)
|
||||
frames.append(
|
||||
{
|
||||
"truth_island_sequence": sequence,
|
||||
"image_id": int(case["image_id"]),
|
||||
"frame_index": int(case["frame_index"]),
|
||||
"source_sha256": str(case["source_image_sha256"]),
|
||||
"reviewed": True,
|
||||
"hard_negative": frame.hard_negative,
|
||||
"objects": objects,
|
||||
}
|
||||
)
|
||||
frames.sort(key=lambda item: int(item["truth_island_sequence"]))
|
||||
return store.save(
|
||||
result_id=result_id,
|
||||
session_id=session_id,
|
||||
request=request,
|
||||
frames=frames,
|
||||
)
|
||||
|
||||
return router
|
||||
|
||||
|
||||
def _project_result(frozen: dict[str, Any]) -> dict[str, object]:
|
||||
manifest = frozen["manifest"]
|
||||
report = frozen["report"]
|
||||
identity = manifest.get("identity")
|
||||
e46_source = identity.get("e46_source") if isinstance(identity, dict) else None
|
||||
return {
|
||||
"schema_version": "missioncore.e46a-ai-engineering-preannotation-view/v1",
|
||||
"result_id": frozen["result_id"],
|
||||
"created_at_utc": report.get("created_at_utc"),
|
||||
"source_session_id": report.get("source_session_id"),
|
||||
"camera_source_id": report.get("camera_source_id"),
|
||||
"source_e46_result_id": (
|
||||
e46_source.get("result_id") if isinstance(e46_source, dict) else None
|
||||
),
|
||||
"metrics": copy.deepcopy(report.get("metrics")),
|
||||
"assistance": copy.deepcopy(report.get("assistance")),
|
||||
"taxonomy": copy.deepcopy(report.get("taxonomy")),
|
||||
"decision": copy.deepcopy(report.get("decision")),
|
||||
"limitations": copy.deepcopy(report.get("limitations")),
|
||||
"authority": copy.deepcopy(report.get("authority")),
|
||||
"ground_truth": False,
|
||||
"access": "read-only-ai-engineering-preannotation-not-truth",
|
||||
}
|
||||
|
||||
|
||||
def _configured_root(provider: RootProvider) -> Path | None:
|
||||
configured = provider()
|
||||
if configured is None:
|
||||
return None
|
||||
return configured.expanduser().absolute()
|
||||
|
||||
|
||||
def _candidates(provider: RootProvider) -> list[Path]:
|
||||
root = _configured_root(provider)
|
||||
if root is None or not root.is_dir() or root.is_symlink():
|
||||
return []
|
||||
return sorted(
|
||||
(
|
||||
path
|
||||
for path in root.iterdir()
|
||||
if path.is_dir()
|
||||
and not path.is_symlink()
|
||||
and _RESULT_ID.fullmatch(path.name) is not None
|
||||
),
|
||||
key=lambda path: (path.stat().st_mtime_ns, path.name),
|
||||
reverse=True,
|
||||
)
|
||||
@@ -0,0 +1,133 @@
|
||||
"""Read-only LAB projection for E46B temporal tracks and motion state."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import re
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
|
||||
from k1link.compute.e46b_temporal_motion import (
|
||||
E46BTemporalMotionError,
|
||||
read_e46b_temporal_motion,
|
||||
)
|
||||
|
||||
_RESULT_ID = re.compile(r"^e46b-temporal-motion-[a-f0-9]{64}$")
|
||||
RootProvider = Callable[[], Path | None]
|
||||
|
||||
|
||||
def build_e46b_temporal_motion_router(
|
||||
*, root_provider: RootProvider = lambda: None,
|
||||
) -> APIRouter:
|
||||
router = APIRouter(prefix="/api/v1/laboratory/e46b", tags=["laboratory"])
|
||||
|
||||
def result(result_id: str) -> dict[str, Any]:
|
||||
if _RESULT_ID.fullmatch(result_id) is None:
|
||||
raise HTTPException(status_code=404, detail="E46B result не найден")
|
||||
root = _configured_root(root_provider)
|
||||
if root is None:
|
||||
raise HTTPException(status_code=404, detail="E46B result не найден")
|
||||
path = (root / result_id).resolve()
|
||||
if path.parent != root or path.is_symlink():
|
||||
raise HTTPException(status_code=404, detail="E46B result не найден")
|
||||
try:
|
||||
return read_e46b_temporal_motion(path)
|
||||
except (E46BTemporalMotionError, OSError, ValueError):
|
||||
raise HTTPException(status_code=404, detail="E46B result не найден") from None
|
||||
|
||||
@router.get("/results")
|
||||
def list_results(limit: int = Query(default=1, ge=1, le=10)) -> dict[str, object]:
|
||||
candidates = _candidates(root_provider)
|
||||
items: list[dict[str, object]] = []
|
||||
invalid_total = 0
|
||||
for path in candidates:
|
||||
try:
|
||||
frozen = result(path.name)
|
||||
if len(items) < limit:
|
||||
items.append(_project_result(frozen))
|
||||
except HTTPException:
|
||||
invalid_total += 1
|
||||
return {
|
||||
"schema_version": "missioncore.e46b-temporal-motion-catalog/v1",
|
||||
"configured": _configured_root(root_provider) is not None,
|
||||
"items": items,
|
||||
"candidate_total": len(candidates),
|
||||
"invalid_total": invalid_total,
|
||||
"access": "read-only-engineering-evidence",
|
||||
}
|
||||
|
||||
@router.get("/results/{result_id}/cases/{sequence}")
|
||||
def get_case(result_id: str, sequence: int) -> dict[str, object]:
|
||||
frozen = result(result_id)
|
||||
case = next(
|
||||
(item for item in frozen["cases"] if item.get("truth_island_sequence") == sequence),
|
||||
None,
|
||||
)
|
||||
if case is None:
|
||||
raise HTTPException(status_code=404, detail="E46B frame не найден")
|
||||
identity = frozen["manifest"]["identity"]
|
||||
e46_result_id = identity["e46_source"]["result_id"]
|
||||
return {
|
||||
**copy.deepcopy(case),
|
||||
"result_id": result_id,
|
||||
"camera_url": (
|
||||
f"/api/v1/laboratory/e46/results/{e46_result_id}"
|
||||
f"/annotation-source/frames/{sequence}/camera"
|
||||
),
|
||||
"ground_truth": False,
|
||||
"access": "read-only-source-scoped-temporal-motion",
|
||||
}
|
||||
|
||||
return router
|
||||
|
||||
|
||||
def _project_result(frozen: dict[str, Any]) -> dict[str, object]:
|
||||
report = frozen["report"]
|
||||
identity = frozen["manifest"]["identity"]
|
||||
return {
|
||||
"schema_version": "missioncore.e46b-temporal-motion-view/v1",
|
||||
"result_id": frozen["result_id"],
|
||||
"created_at_utc": report["created_at_utc"],
|
||||
"source_session_id": report["source_session_id"],
|
||||
"camera_source_id": report["camera_source_id"],
|
||||
"source_e46a_result_id": identity["e46a_source"]["result_id"],
|
||||
"source_e26_result_id": identity["e26_motion_source"]["result_id"],
|
||||
"status": report["status"],
|
||||
"metrics": copy.deepcopy(report["metrics"]),
|
||||
"tracks": copy.deepcopy(report["tracks"]),
|
||||
"decision": copy.deepcopy(report["decision"]),
|
||||
"limitations": copy.deepcopy(report["limitations"]),
|
||||
"ground_truth": False,
|
||||
"access": "read-only-engineering-evidence",
|
||||
}
|
||||
|
||||
|
||||
def _configured_root(provider: RootProvider) -> Path | None:
|
||||
value = provider()
|
||||
if value is None:
|
||||
return None
|
||||
candidate = value.expanduser().absolute()
|
||||
if candidate.is_symlink():
|
||||
return None
|
||||
try:
|
||||
root = candidate.resolve(strict=True)
|
||||
except OSError:
|
||||
return None
|
||||
return root if root.is_dir() else None
|
||||
|
||||
|
||||
def _candidates(provider: RootProvider) -> list[Path]:
|
||||
root = _configured_root(provider)
|
||||
if root is None:
|
||||
return []
|
||||
return sorted(
|
||||
(
|
||||
item for item in root.iterdir()
|
||||
if item.is_dir() and not item.is_symlink() and _RESULT_ID.fullmatch(item.name)
|
||||
),
|
||||
key=lambda item: item.stat().st_mtime_ns,
|
||||
reverse=True,
|
||||
)
|
||||
@@ -0,0 +1,396 @@
|
||||
"""Read-only LAB projection for E46C full-replay world tracks."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import re
|
||||
from collections.abc import Callable
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
|
||||
from k1link.compute.e46c_full_replay_world_tracks import (
|
||||
E46CFullReplayWorldTracksError,
|
||||
read_e46c_full_replay_world_tracks,
|
||||
)
|
||||
|
||||
_RESULT_ID = re.compile(r"^e46c-full-replay-world-tracks-[a-f0-9]{64}$")
|
||||
_E26_RESULT_ID = re.compile(r"^e10-integrated-perception-[a-f0-9]{64}$")
|
||||
_SAFE_SESSION_ID = re.compile(r"^[A-Za-z0-9._:-]{1,128}$")
|
||||
RootProvider = Callable[[], Path | None]
|
||||
|
||||
|
||||
def build_e46c_full_replay_world_tracks_router(
|
||||
*,
|
||||
root_provider: RootProvider = lambda: None,
|
||||
e26_root_provider: RootProvider = lambda: None,
|
||||
) -> APIRouter:
|
||||
router = APIRouter(prefix="/api/v1/laboratory/e46c", tags=["laboratory"])
|
||||
|
||||
def result(result_id: str) -> dict[str, Any]:
|
||||
if _RESULT_ID.fullmatch(result_id) is None:
|
||||
raise HTTPException(status_code=404, detail="E46C result не найден")
|
||||
root = _configured_root(root_provider)
|
||||
if root is None:
|
||||
raise HTTPException(status_code=404, detail="E46C result не найден")
|
||||
path = (root / result_id).resolve()
|
||||
if path.parent != root or path.is_symlink():
|
||||
raise HTTPException(status_code=404, detail="E46C result не найден")
|
||||
try:
|
||||
return read_e46c_full_replay_world_tracks(path)
|
||||
except (E46CFullReplayWorldTracksError, OSError, ValueError):
|
||||
raise HTTPException(status_code=404, detail="E46C result не найден") from None
|
||||
|
||||
@router.get("/results")
|
||||
def list_results(limit: int = Query(default=1, ge=1, le=10)) -> dict[str, object]:
|
||||
candidates = _candidates(root_provider)
|
||||
items: list[dict[str, object]] = []
|
||||
invalid_total = 0
|
||||
for path in candidates:
|
||||
try:
|
||||
frozen = result(path.name)
|
||||
if len(items) < limit:
|
||||
items.append(_project_result(frozen))
|
||||
except HTTPException:
|
||||
invalid_total += 1
|
||||
return {
|
||||
"schema_version": "missioncore.e46c-full-replay-world-tracks-catalog/v1",
|
||||
"configured": _configured_root(root_provider) is not None,
|
||||
"items": items,
|
||||
"candidate_total": len(candidates),
|
||||
"invalid_total": invalid_total,
|
||||
"access": "read-only-diagnostic",
|
||||
}
|
||||
|
||||
@router.get("/results/{result_id}/cases/{sequence}")
|
||||
def get_case(result_id: str, sequence: int) -> dict[str, object]:
|
||||
frozen = result(result_id)
|
||||
case = next(
|
||||
(item for item in frozen["cases"] if item.get("truth_island_sequence") == sequence),
|
||||
None,
|
||||
)
|
||||
if case is None:
|
||||
raise HTTPException(status_code=404, detail="E46C frame не найден")
|
||||
e46_result_id = frozen["manifest"]["identity"]["e46_source"]["result_id"]
|
||||
return {
|
||||
**copy.deepcopy(case),
|
||||
"result_id": result_id,
|
||||
"camera_url": (
|
||||
f"/api/v1/laboratory/e46/results/{e46_result_id}"
|
||||
f"/annotation-source/frames/{sequence}/camera"
|
||||
),
|
||||
"ground_truth": False,
|
||||
"access": "read-only-full-route-diagnostic-sample",
|
||||
}
|
||||
|
||||
@router.get("/results/{result_id}/video-overlay")
|
||||
def get_video_overlay(result_id: str) -> dict[str, object]:
|
||||
frozen = result(result_id)
|
||||
e26_root = _configured_root(e26_root_provider)
|
||||
if e26_root is None:
|
||||
raise HTTPException(status_code=404, detail="E46C video overlay не найден")
|
||||
identity = frozen["manifest"]["identity"]["e26_full_replay"]
|
||||
source_result_id = identity.get("result_id")
|
||||
expected_result_sha256 = identity.get("result_sha256")
|
||||
expected_fusion_sha256 = identity.get("fusion_frames_sha256")
|
||||
if (
|
||||
not isinstance(source_result_id, str)
|
||||
or _E26_RESULT_ID.fullmatch(source_result_id) is None
|
||||
or not isinstance(expected_result_sha256, str)
|
||||
or not isinstance(expected_fusion_sha256, str)
|
||||
):
|
||||
raise HTTPException(status_code=404, detail="E46C video overlay не найден")
|
||||
source_path = (e26_root / source_result_id).resolve()
|
||||
if source_path.parent != e26_root or source_path.is_symlink():
|
||||
raise HTTPException(status_code=404, detail="E46C video overlay не найден")
|
||||
try:
|
||||
return _cached_video_overlay(
|
||||
result_id,
|
||||
str(source_path),
|
||||
expected_result_sha256,
|
||||
expected_fusion_sha256,
|
||||
)
|
||||
except (OSError, ValueError, json.JSONDecodeError):
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="E46C video overlay не найден",
|
||||
) from None
|
||||
|
||||
return router
|
||||
|
||||
|
||||
@lru_cache(maxsize=4)
|
||||
def _cached_video_overlay(
|
||||
e46c_result_id: str,
|
||||
source_root_value: str,
|
||||
expected_result_sha256: str,
|
||||
expected_fusion_sha256: str,
|
||||
) -> dict[str, object]:
|
||||
source_root = Path(source_root_value).resolve(strict=True)
|
||||
if source_root.is_symlink() or not source_root.is_dir():
|
||||
raise ValueError("E26 source root is invalid")
|
||||
result_path = source_root / "result.json"
|
||||
if _sha256(result_path) != expected_result_sha256:
|
||||
raise ValueError("E26 result identity changed")
|
||||
result = _read_json(result_path)
|
||||
identity = result.get("identity")
|
||||
artifacts = result.get("artifacts")
|
||||
if (
|
||||
result.get("schema_version")
|
||||
!= "missioncore.e10-integrated-perception-result/v1"
|
||||
or result.get("acceptance_state") != "accepted"
|
||||
or not isinstance(identity, dict)
|
||||
or not isinstance(artifacts, list)
|
||||
):
|
||||
raise ValueError("E26 result contract is invalid")
|
||||
selection = identity.get("selection")
|
||||
configuration = identity.get("configuration")
|
||||
if not isinstance(selection, dict) or not isinstance(configuration, dict):
|
||||
raise ValueError("E26 timeline contract is invalid")
|
||||
recorded_session_id = configuration.get("source_session_id")
|
||||
if (
|
||||
not isinstance(recorded_session_id, str)
|
||||
or _SAFE_SESSION_ID.fullmatch(recorded_session_id) is None
|
||||
or identity.get("source_id") != "sensor.camera.right"
|
||||
or selection.get("frame_count") != 4489
|
||||
):
|
||||
raise ValueError("E26 recorded source binding is invalid")
|
||||
fusion_artifact = next(
|
||||
(
|
||||
item
|
||||
for item in artifacts
|
||||
if isinstance(item, dict) and item.get("path") == "fusion-frames.jsonl"
|
||||
),
|
||||
None,
|
||||
)
|
||||
fusion_path = _validated_source_artifact(
|
||||
source_root,
|
||||
fusion_artifact,
|
||||
expected_fusion_sha256,
|
||||
)
|
||||
|
||||
frames: list[dict[str, object]] = []
|
||||
previous_seconds = -math.inf
|
||||
for expected_frame_index, row in enumerate(_read_jsonl(fusion_path)):
|
||||
frame_index = row.get("source_frame_index")
|
||||
session_seconds = row.get("session_seconds")
|
||||
raw_objects = row.get("objects")
|
||||
if (
|
||||
frame_index != expected_frame_index
|
||||
or not isinstance(session_seconds, int | float)
|
||||
or isinstance(session_seconds, bool)
|
||||
or not math.isfinite(float(session_seconds))
|
||||
or float(session_seconds) <= previous_seconds
|
||||
or not isinstance(raw_objects, list)
|
||||
):
|
||||
raise ValueError("E26 video frame sequence is invalid")
|
||||
previous_seconds = float(session_seconds)
|
||||
frames.append(
|
||||
{
|
||||
"frame_index": frame_index,
|
||||
"session_seconds": float(session_seconds),
|
||||
"fusion_state": str(row.get("fusion_state", "unknown")),
|
||||
"objects": [
|
||||
_video_object(item)
|
||||
for item in raw_objects
|
||||
if isinstance(item, dict)
|
||||
],
|
||||
}
|
||||
)
|
||||
timeline_start = float(selection.get("timeline_start_seconds"))
|
||||
timeline_end = float(selection.get("timeline_end_seconds"))
|
||||
if (
|
||||
len(frames) != 4489
|
||||
or frames[0]["session_seconds"] != timeline_start
|
||||
or frames[-1]["session_seconds"] != timeline_end
|
||||
):
|
||||
raise ValueError("E26 video timeline changed")
|
||||
|
||||
benchmark = configuration.get("benchmark")
|
||||
benchmark_events = benchmark.get("events") if isinstance(benchmark, dict) else None
|
||||
review_windows = []
|
||||
if isinstance(benchmark_events, list):
|
||||
for event in benchmark_events:
|
||||
if not isinstance(event, dict):
|
||||
continue
|
||||
window = event.get("window_seconds")
|
||||
if (
|
||||
not isinstance(window, list)
|
||||
or len(window) != 2
|
||||
or not all(isinstance(item, int | float) for item in window)
|
||||
):
|
||||
continue
|
||||
review_windows.append(
|
||||
{
|
||||
"id": str(event.get("id", "review-window")),
|
||||
"kind": str(event.get("kind", "diagnostic")),
|
||||
"class_group": str(event.get("class_group", "all")),
|
||||
"start_seconds": float(window[0]),
|
||||
"end_seconds": float(window[1]),
|
||||
"target_source_track_ids": [
|
||||
int(value)
|
||||
for value in event.get("target_source_track_ids", [])
|
||||
if isinstance(value, int) and not isinstance(value, bool)
|
||||
],
|
||||
}
|
||||
)
|
||||
return {
|
||||
"schema_version": "missioncore.e46c-recorded-video-overlay/v1",
|
||||
"result_id": e46c_result_id,
|
||||
"recorded_source": {
|
||||
"session_id": recorded_session_id,
|
||||
"source_id": "sensor.camera.right",
|
||||
"input_sha256": identity.get("input_sha256"),
|
||||
"synchronization": "host-arrival-best-effort",
|
||||
},
|
||||
"image_width": 800,
|
||||
"image_height": 600,
|
||||
"timeline_start_seconds": timeline_start,
|
||||
"timeline_end_seconds": timeline_end,
|
||||
"frame_count": len(frames),
|
||||
"frames": frames,
|
||||
"review_windows": review_windows,
|
||||
"ground_truth": False,
|
||||
"access": "read-only-full-route-diagnostic-video",
|
||||
}
|
||||
|
||||
|
||||
def _video_object(item: dict[str, Any]) -> dict[str, object]:
|
||||
box = item.get("bbox_xyxy")
|
||||
if (
|
||||
not isinstance(box, list)
|
||||
or len(box) != 4
|
||||
or not all(
|
||||
isinstance(value, int | float)
|
||||
and not isinstance(value, bool)
|
||||
and math.isfinite(float(value))
|
||||
for value in box
|
||||
)
|
||||
):
|
||||
raise ValueError("E26 video object box is invalid")
|
||||
source_track_id = item.get("source_track_id")
|
||||
world_track_id = item.get("track_id")
|
||||
if not isinstance(source_track_id, int) or isinstance(source_track_id, bool):
|
||||
raise ValueError("E26 video object track is invalid")
|
||||
if not isinstance(world_track_id, int) or isinstance(world_track_id, bool):
|
||||
world_track_id = source_track_id
|
||||
return {
|
||||
"bbox_xyxy": [float(value) for value in box],
|
||||
"category": str(item.get("label", "unknown")),
|
||||
"score": float(item.get("score") or 0.0),
|
||||
"route_track_id": source_track_id,
|
||||
"world_track_id": (
|
||||
world_track_id if world_track_id != source_track_id else None
|
||||
),
|
||||
"motion_state": str(item.get("motion_state", "unknown")),
|
||||
"motion_confidence": float(item.get("motion_confidence") or 0.0),
|
||||
"track_hits": int(item.get("track_hits") or 0),
|
||||
"track_age_seconds": float(item.get("track_age_s") or 0.0),
|
||||
"camera_evidence_current": item.get("camera_evidence_current") is True,
|
||||
"world_evidence_current": item.get("occupancy_evidence_current") is True,
|
||||
}
|
||||
|
||||
|
||||
def _validated_source_artifact(
|
||||
root: Path,
|
||||
raw: object,
|
||||
expected_sha256: str,
|
||||
) -> Path:
|
||||
if not isinstance(raw, dict) or raw.get("sha256") != expected_sha256:
|
||||
raise ValueError("E26 fusion artifact identity changed")
|
||||
relative = raw.get("path")
|
||||
if not isinstance(relative, str):
|
||||
raise ValueError("E26 fusion artifact path is invalid")
|
||||
path = (root / relative).resolve(strict=True)
|
||||
expected_size = raw.get("byte_length", raw.get("size_bytes"))
|
||||
if (
|
||||
path.parent != root
|
||||
or path.is_symlink()
|
||||
or not path.is_file()
|
||||
or path.stat().st_size != expected_size
|
||||
or _sha256(path) != expected_sha256
|
||||
):
|
||||
raise ValueError("E26 fusion artifact changed")
|
||||
return path
|
||||
|
||||
|
||||
def _read_json(path: Path) -> dict[str, Any]:
|
||||
value = json.loads(path.read_text(encoding="utf-8"))
|
||||
if not isinstance(value, dict):
|
||||
raise ValueError("JSON document is invalid")
|
||||
return value
|
||||
|
||||
|
||||
def _read_jsonl(path: Path):
|
||||
with path.open("r", encoding="utf-8") as handle:
|
||||
for line in handle:
|
||||
if line.strip():
|
||||
value = json.loads(line)
|
||||
if not isinstance(value, dict):
|
||||
raise ValueError("JSONL row is invalid")
|
||||
yield value
|
||||
|
||||
|
||||
def _sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as handle:
|
||||
for block in iter(lambda: handle.read(1024 * 1024), b""):
|
||||
digest.update(block)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def _project_result(frozen: dict[str, Any]) -> dict[str, object]:
|
||||
report = frozen["report"]
|
||||
identity = frozen["manifest"]["identity"]
|
||||
return {
|
||||
"schema_version": "missioncore.e46c-full-replay-world-tracks-view/v1",
|
||||
"result_id": frozen["result_id"],
|
||||
"created_at_utc": report["created_at_utc"],
|
||||
"source_session_id": report["source_session_id"],
|
||||
"camera_source_id": report["camera_source_id"],
|
||||
"source_e46a_result_id": identity["e46a_visual_sample"]["result_id"],
|
||||
"source_e26_result_id": identity["e26_full_replay"]["result_id"],
|
||||
"status": report["status"],
|
||||
"metrics": copy.deepcopy(report["metrics"]),
|
||||
"acceptance": copy.deepcopy(report["acceptance"]),
|
||||
"decision": copy.deepcopy(report["decision"]),
|
||||
"limitations": copy.deepcopy(report["limitations"]),
|
||||
"ground_truth": False,
|
||||
"access": "read-only-diagnostic",
|
||||
}
|
||||
|
||||
|
||||
def _configured_root(provider: RootProvider) -> Path | None:
|
||||
value = provider()
|
||||
if value is None:
|
||||
return None
|
||||
candidate = value.expanduser().absolute()
|
||||
if candidate.is_symlink():
|
||||
return None
|
||||
try:
|
||||
root = candidate.resolve(strict=True)
|
||||
except OSError:
|
||||
return None
|
||||
return root if root.is_dir() else None
|
||||
|
||||
|
||||
def _candidates(provider: RootProvider) -> list[Path]:
|
||||
root = _configured_root(provider)
|
||||
if root is None:
|
||||
return []
|
||||
return sorted(
|
||||
(
|
||||
item
|
||||
for item in root.iterdir()
|
||||
if item.is_dir()
|
||||
and not item.is_symlink()
|
||||
and _RESULT_ID.fullmatch(item.name)
|
||||
),
|
||||
key=lambda item: item.stat().st_mtime_ns,
|
||||
reverse=True,
|
||||
)
|
||||
@@ -0,0 +1,114 @@
|
||||
"""Read-only LAB projection for the E46D full-route temporal audit."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import re
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
|
||||
from k1link.compute.e46d_temporal_failure_audit import (
|
||||
E46DTemporalFailureAuditError,
|
||||
read_e46d_temporal_failure_audit,
|
||||
)
|
||||
|
||||
_RESULT_ID = re.compile(r"^e46d-temporal-failure-audit-[a-f0-9]{64}$")
|
||||
RootProvider = Callable[[], Path | None]
|
||||
|
||||
|
||||
def build_e46d_temporal_failure_audit_router(
|
||||
*, root_provider: RootProvider = lambda: None
|
||||
) -> APIRouter:
|
||||
router = APIRouter(prefix="/api/v1/laboratory/e46d", tags=["laboratory"])
|
||||
|
||||
def result(result_id: str) -> dict[str, Any]:
|
||||
if _RESULT_ID.fullmatch(result_id) is None:
|
||||
raise HTTPException(status_code=404, detail="E46D result не найден")
|
||||
root = _configured_root(root_provider)
|
||||
if root is None:
|
||||
raise HTTPException(status_code=404, detail="E46D result не найден")
|
||||
path = (root / result_id).resolve()
|
||||
if path.parent != root or path.is_symlink():
|
||||
raise HTTPException(status_code=404, detail="E46D result не найден")
|
||||
try:
|
||||
return read_e46d_temporal_failure_audit(path)
|
||||
except (E46DTemporalFailureAuditError, OSError, ValueError):
|
||||
raise HTTPException(status_code=404, detail="E46D result не найден") from None
|
||||
|
||||
@router.get("/results")
|
||||
def list_results(limit: int = Query(default=1, ge=1, le=10)) -> dict[str, object]:
|
||||
candidates = _candidates(root_provider)
|
||||
items: list[dict[str, object]] = []
|
||||
invalid_total = 0
|
||||
for path in candidates:
|
||||
try:
|
||||
frozen = result(path.name)
|
||||
if len(items) < limit:
|
||||
items.append(_project_result(frozen))
|
||||
except HTTPException:
|
||||
invalid_total += 1
|
||||
return {
|
||||
"schema_version": "missioncore.e46d-temporal-failure-audit-catalog/v1",
|
||||
"configured": _configured_root(root_provider) is not None,
|
||||
"items": items,
|
||||
"candidate_total": len(candidates),
|
||||
"invalid_total": invalid_total,
|
||||
"access": "read-only-recorded-temporal-diagnostic",
|
||||
}
|
||||
|
||||
return router
|
||||
|
||||
|
||||
def _project_result(frozen: dict[str, Any]) -> dict[str, object]:
|
||||
report = frozen["report"]
|
||||
identity = frozen["manifest"]["identity"]
|
||||
return {
|
||||
"schema_version": "missioncore.e46d-temporal-failure-audit-view/v1",
|
||||
"result_id": frozen["result_id"],
|
||||
"created_at_utc": report["created_at_utc"],
|
||||
"source_session_id": report["source_session_id"],
|
||||
"camera_source_id": report["camera_source_id"],
|
||||
"source_e46c_result_id": identity["e46c_source"]["result_id"],
|
||||
"source_e26_result_id": identity["e26_source"]["result_id"],
|
||||
"status": report["status"],
|
||||
"metrics": copy.deepcopy(report["metrics"]),
|
||||
"acceptance": copy.deepcopy(report["acceptance"]),
|
||||
"decision": copy.deepcopy(report["decision"]),
|
||||
"method": copy.deepcopy(report["method"]),
|
||||
"limitations": copy.deepcopy(report["limitations"]),
|
||||
"review_clips": copy.deepcopy(frozen["clips"]),
|
||||
"ground_truth": False,
|
||||
"access": "read-only-recorded-temporal-diagnostic",
|
||||
}
|
||||
|
||||
|
||||
def _configured_root(provider: RootProvider) -> Path | None:
|
||||
value = provider()
|
||||
if value is None:
|
||||
return None
|
||||
candidate = value.expanduser().absolute()
|
||||
if candidate.is_symlink():
|
||||
return None
|
||||
try:
|
||||
root = candidate.resolve(strict=True)
|
||||
except OSError:
|
||||
return None
|
||||
return root if root.is_dir() else None
|
||||
|
||||
|
||||
def _candidates(provider: RootProvider) -> list[Path]:
|
||||
root = _configured_root(provider)
|
||||
if root is None:
|
||||
return []
|
||||
return sorted(
|
||||
(
|
||||
item
|
||||
for item in root.iterdir()
|
||||
if item.is_dir() and not item.is_symlink() and _RESULT_ID.fullmatch(item.name)
|
||||
),
|
||||
key=lambda item: item.stat().st_mtime_ns,
|
||||
reverse=True,
|
||||
)
|
||||
@@ -0,0 +1,149 @@
|
||||
"""Read-only LAB projection and video delivery for the E46E NVIDIA ready stack."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import re
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from fastapi.responses import FileResponse
|
||||
|
||||
from k1link.compute.e46e_ready_stack import (
|
||||
E46EReadyStackError,
|
||||
read_e46e_ready_stack,
|
||||
)
|
||||
|
||||
_RESULT_ID = re.compile(r"^e46e-ready-stack-[a-f0-9]{64}$")
|
||||
RootProvider = Callable[[], Path | None]
|
||||
|
||||
|
||||
def build_e46e_ready_stack_router(
|
||||
*, root_provider: RootProvider = lambda: None
|
||||
) -> APIRouter:
|
||||
router = APIRouter(prefix="/api/v1/laboratory/e46e", tags=["laboratory"])
|
||||
|
||||
def result(result_id: str) -> dict[str, Any]:
|
||||
if _RESULT_ID.fullmatch(result_id) is None:
|
||||
raise HTTPException(status_code=404, detail="E46E result не найден")
|
||||
root = _configured_root(root_provider)
|
||||
if root is None:
|
||||
raise HTTPException(status_code=404, detail="E46E result не найден")
|
||||
path = (root / result_id).resolve()
|
||||
if path.parent != root or path.is_symlink():
|
||||
raise HTTPException(status_code=404, detail="E46E result не найден")
|
||||
try:
|
||||
return read_e46e_ready_stack(path)
|
||||
except (E46EReadyStackError, OSError, ValueError):
|
||||
raise HTTPException(status_code=404, detail="E46E result не найден") from None
|
||||
|
||||
@router.get("/results")
|
||||
def list_results(limit: int = Query(default=1, ge=1, le=10)) -> dict[str, object]:
|
||||
candidates = _candidates(root_provider)
|
||||
items: list[dict[str, object]] = []
|
||||
invalid_total = 0
|
||||
for path in candidates:
|
||||
try:
|
||||
frozen = result(path.name)
|
||||
if len(items) < limit:
|
||||
items.append(_project_result(frozen))
|
||||
except HTTPException:
|
||||
invalid_total += 1
|
||||
return {
|
||||
"schema_version": "missioncore.e46e-ready-stack-catalog/v1",
|
||||
"configured": _configured_root(root_provider) is not None,
|
||||
"items": items,
|
||||
"candidate_total": len(candidates),
|
||||
"invalid_total": invalid_total,
|
||||
"access": "read-only-stock-nvidia-recorded-diagnostic",
|
||||
}
|
||||
|
||||
@router.get("/results/{result_id}/overlay.mp4")
|
||||
def get_overlay_video(result_id: str) -> FileResponse:
|
||||
frozen = result(result_id)
|
||||
overlay = frozen["overlay_path"]
|
||||
descriptor = next(
|
||||
(
|
||||
row
|
||||
for row in frozen["manifest"]["artifacts"]
|
||||
if row.get("role") == "visual-overlay-video"
|
||||
),
|
||||
None,
|
||||
)
|
||||
if not isinstance(descriptor, dict):
|
||||
raise HTTPException(status_code=404, detail="E46E video не найден")
|
||||
return FileResponse(
|
||||
overlay,
|
||||
media_type="video/mp4",
|
||||
headers={
|
||||
"Cache-Control": "public, max-age=31536000, immutable",
|
||||
"ETag": f'"{descriptor["sha256"]}"',
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
},
|
||||
)
|
||||
|
||||
return router
|
||||
|
||||
|
||||
def _project_result(frozen: dict[str, Any]) -> dict[str, object]:
|
||||
report = frozen["report"]
|
||||
overlay = next(
|
||||
row
|
||||
for row in frozen["manifest"]["artifacts"]
|
||||
if row.get("role") == "visual-overlay-video"
|
||||
)
|
||||
result_id = frozen["result_id"]
|
||||
return {
|
||||
"schema_version": "missioncore.e46e-ready-stack-view/v1",
|
||||
"result_id": result_id,
|
||||
"created_at_utc": report["created_at_utc"],
|
||||
"source_session_id": report["source_session_id"],
|
||||
"camera_source_id": report["camera_source_id"],
|
||||
"status": report["status"],
|
||||
"metrics": copy.deepcopy(report["metrics"]),
|
||||
"acceptance": copy.deepcopy(report["acceptance"]),
|
||||
"decision": copy.deepcopy(report["decision"]),
|
||||
"method": copy.deepcopy(report["method"]),
|
||||
"limitations": copy.deepcopy(report["limitations"]),
|
||||
"video": {
|
||||
"url": f"/api/v1/laboratory/e46e/results/{result_id}/overlay.mp4",
|
||||
"media_type": "video/mp4",
|
||||
"byte_length": overlay["byte_length"],
|
||||
"sha256": overlay["sha256"],
|
||||
"width": 800,
|
||||
"height": 600,
|
||||
},
|
||||
"ground_truth": False,
|
||||
"access": "read-only-stock-nvidia-recorded-diagnostic",
|
||||
}
|
||||
|
||||
|
||||
def _configured_root(provider: RootProvider) -> Path | None:
|
||||
value = provider()
|
||||
if value is None:
|
||||
return None
|
||||
candidate = value.expanduser().absolute()
|
||||
if candidate.is_symlink():
|
||||
return None
|
||||
try:
|
||||
root = candidate.resolve(strict=True)
|
||||
except OSError:
|
||||
return None
|
||||
return root if root.is_dir() else None
|
||||
|
||||
|
||||
def _candidates(provider: RootProvider) -> list[Path]:
|
||||
root = _configured_root(provider)
|
||||
if root is None:
|
||||
return []
|
||||
return sorted(
|
||||
(
|
||||
item
|
||||
for item in root.iterdir()
|
||||
if item.is_dir() and not item.is_symlink() and _RESULT_ID.fullmatch(item.name)
|
||||
),
|
||||
key=lambda item: item.stat().st_mtime_ns,
|
||||
reverse=True,
|
||||
)
|
||||
@@ -0,0 +1,230 @@
|
||||
"""Read-only LAB projection for the E46F DashCamNet detector bake-off."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import re
|
||||
from collections import Counter
|
||||
from collections.abc import Callable, Iterable
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from fastapi.responses import FileResponse
|
||||
|
||||
from k1link.compute.e46e_ready_stack import (
|
||||
E46EReadyStackError,
|
||||
read_e46e_ready_stack,
|
||||
)
|
||||
from k1link.compute.e46f_dashcam_bakeoff import (
|
||||
E46FDashCamBakeoffError,
|
||||
read_e46f_dashcam_bakeoff,
|
||||
)
|
||||
|
||||
_RESULT_ID = re.compile(r"^e46f-dashcam-bakeoff-[a-f0-9]{64}$")
|
||||
_BASELINE_ID = re.compile(r"^e46e-ready-stack-[a-f0-9]{64}$")
|
||||
RootProvider = Callable[[], Path | None]
|
||||
|
||||
|
||||
def build_e46f_dashcam_bakeoff_router(
|
||||
*,
|
||||
root_provider: RootProvider = lambda: None,
|
||||
e46e_root_provider: RootProvider = lambda: None,
|
||||
) -> APIRouter:
|
||||
router = APIRouter(prefix="/api/v1/laboratory/e46f", tags=["laboratory"])
|
||||
|
||||
def result(result_id: str) -> dict[str, Any]:
|
||||
if _RESULT_ID.fullmatch(result_id) is None:
|
||||
raise HTTPException(status_code=404, detail="E46F result не найден")
|
||||
root = _configured_root(root_provider)
|
||||
if root is None:
|
||||
raise HTTPException(status_code=404, detail="E46F result не найден")
|
||||
path = (root / result_id).resolve()
|
||||
if path.parent != root or path.is_symlink():
|
||||
raise HTTPException(status_code=404, detail="E46F result не найден")
|
||||
try:
|
||||
return read_e46f_dashcam_bakeoff(path)
|
||||
except (E46FDashCamBakeoffError, OSError, ValueError):
|
||||
raise HTTPException(status_code=404, detail="E46F result не найден") from None
|
||||
|
||||
@router.get("/results")
|
||||
def list_results(limit: int = Query(default=1, ge=1, le=10)) -> dict[str, object]:
|
||||
candidates = _candidates(root_provider)
|
||||
items: list[dict[str, object]] = []
|
||||
invalid_total = 0
|
||||
for path in candidates:
|
||||
try:
|
||||
frozen = result(path.name)
|
||||
if len(items) < limit:
|
||||
items.append(_project_result(frozen, e46e_root_provider))
|
||||
except HTTPException:
|
||||
invalid_total += 1
|
||||
return {
|
||||
"schema_version": "missioncore.e46f-dashcam-bakeoff-catalog/v1",
|
||||
"configured": _configured_root(root_provider) is not None,
|
||||
"items": items,
|
||||
"candidate_total": len(candidates),
|
||||
"invalid_total": invalid_total,
|
||||
"access": "read-only-rejected-detector-bakeoff",
|
||||
}
|
||||
|
||||
@router.get("/results/{result_id}/overlay.mp4")
|
||||
def get_overlay_video(result_id: str) -> FileResponse:
|
||||
frozen = result(result_id)
|
||||
descriptor = next(
|
||||
(
|
||||
row
|
||||
for row in frozen["manifest"]["artifacts"]
|
||||
if row.get("role") == "visual-overlay-video"
|
||||
),
|
||||
None,
|
||||
)
|
||||
if not isinstance(descriptor, dict):
|
||||
raise HTTPException(status_code=404, detail="E46F video не найден")
|
||||
return FileResponse(
|
||||
frozen["overlay_path"],
|
||||
media_type="video/mp4",
|
||||
headers={
|
||||
"Cache-Control": "public, max-age=31536000, immutable",
|
||||
"ETag": f'"{descriptor["sha256"]}"',
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
},
|
||||
)
|
||||
|
||||
return router
|
||||
|
||||
|
||||
def _project_result(frozen: dict[str, Any], e46e_root_provider: RootProvider) -> dict[str, object]:
|
||||
report = frozen["report"]
|
||||
overlay = next(
|
||||
row for row in frozen["manifest"]["artifacts"] if row.get("role") == "visual-overlay-video"
|
||||
)
|
||||
baseline_id = report["comparison_contract"]["baseline_result_id"]
|
||||
baseline = _read_baseline(e46e_root_provider, baseline_id)
|
||||
baseline_metrics = baseline["report"]["metrics"]
|
||||
metrics = report["metrics"]
|
||||
candidate_triage = _large_box_triage(frozen["frames"])
|
||||
baseline_triage = _large_box_triage(baseline["frames"])
|
||||
result_id = frozen["result_id"]
|
||||
return {
|
||||
"schema_version": "missioncore.e46f-dashcam-bakeoff-view/v1",
|
||||
"result_id": result_id,
|
||||
"created_at_utc": report["created_at_utc"],
|
||||
"source_session_id": report["source_session_id"],
|
||||
"camera_source_id": report["camera_source_id"],
|
||||
"status": report["status"],
|
||||
"metrics": copy.deepcopy(metrics),
|
||||
"acceptance": copy.deepcopy(report["acceptance"]),
|
||||
"decision": copy.deepcopy(report["decision"]),
|
||||
"method": copy.deepcopy(report["method"]),
|
||||
"limitations": copy.deepcopy(report["limitations"]),
|
||||
"comparison": {
|
||||
"controlled_change": "detector-only",
|
||||
"baseline_result_id": baseline_id,
|
||||
"baseline_metrics": copy.deepcopy(baseline_metrics),
|
||||
"delta": {
|
||||
"zero_track_frame_count": metrics["zero_track_frame_count"]
|
||||
- baseline_metrics["zero_track_frame_count"],
|
||||
"full_layer_blackout_event_count": metrics["full_layer_blackout_event_count"]
|
||||
- baseline_metrics["full_layer_blackout_event_count"],
|
||||
"unique_track_count": metrics["unique_track_count"]
|
||||
- baseline_metrics["unique_track_count"],
|
||||
"short_track_fraction": metrics["short_track_fraction"]
|
||||
- baseline_metrics["short_track_fraction"],
|
||||
},
|
||||
"large_box_visual_triage": {
|
||||
"area_ratio_threshold": 0.2,
|
||||
"candidate": candidate_triage,
|
||||
"baseline": baseline_triage,
|
||||
"interpretation": "diagnostic visual triage; not precision/recall",
|
||||
},
|
||||
"visual_review": {
|
||||
"status": "rejected-semantic-regression",
|
||||
"sample_video_seconds": [4.2, 9.0, 22.0, 44.0, 264.0, 418.0],
|
||||
"finding": (
|
||||
"DashCamNet repeatedly turns the fisheye black rim and edge geometry "
|
||||
"into very large person tracks; temporal continuity improves, but the "
|
||||
"semantic object layer is less trustworthy than E46E"
|
||||
),
|
||||
"next_action": (
|
||||
"insert one calibration-derived rectification and valid-FOV projection "
|
||||
"before any perspective ready detector, then repeat the controlled bake-off"
|
||||
),
|
||||
},
|
||||
"verdict": "reject-dashcamnet-on-unrectified-fisheye",
|
||||
},
|
||||
"video": {
|
||||
"url": f"/api/v1/laboratory/e46f/results/{result_id}/overlay.mp4",
|
||||
"media_type": "video/mp4",
|
||||
"byte_length": overlay["byte_length"],
|
||||
"sha256": overlay["sha256"],
|
||||
"width": 800,
|
||||
"height": 600,
|
||||
},
|
||||
"ground_truth": False,
|
||||
"access": "read-only-rejected-detector-bakeoff",
|
||||
}
|
||||
|
||||
|
||||
def _read_baseline(provider: RootProvider, result_id: str) -> dict[str, Any]:
|
||||
root = _configured_root(provider)
|
||||
if root is None or _BASELINE_ID.fullmatch(result_id) is None:
|
||||
raise HTTPException(status_code=404, detail="E46E baseline не найден")
|
||||
path = (root / result_id).resolve()
|
||||
if path.parent != root or path.is_symlink():
|
||||
raise HTTPException(status_code=404, detail="E46E baseline не найден")
|
||||
try:
|
||||
return read_e46e_ready_stack(path)
|
||||
except (E46EReadyStackError, OSError, ValueError):
|
||||
raise HTTPException(status_code=404, detail="E46E baseline не найден") from None
|
||||
|
||||
|
||||
def _large_box_triage(frames: Iterable[dict[str, Any]]) -> dict[str, object]:
|
||||
observation_count = 0
|
||||
frame_ids: set[int] = set()
|
||||
track_ids: set[int] = set()
|
||||
classes: Counter[str] = Counter()
|
||||
for frame in frames:
|
||||
for item in frame["objects"]:
|
||||
_, _, width, height = item["bbox"]
|
||||
if float(width) * float(height) < 800 * 600 * 0.2:
|
||||
continue
|
||||
observation_count += 1
|
||||
frame_ids.add(int(frame["frame_index"]))
|
||||
track_ids.add(int(item["source_track_id"]))
|
||||
classes.update((str(item["class_name"]),))
|
||||
return {
|
||||
"observation_count": observation_count,
|
||||
"frame_count": len(frame_ids),
|
||||
"track_id_count": len(track_ids),
|
||||
"class_observations": dict(sorted(classes.items())),
|
||||
}
|
||||
|
||||
|
||||
def _configured_root(provider: RootProvider) -> Path | None:
|
||||
value = provider()
|
||||
if value is None:
|
||||
return None
|
||||
candidate = value.expanduser().absolute()
|
||||
if candidate.is_symlink():
|
||||
return None
|
||||
try:
|
||||
root = candidate.resolve(strict=True)
|
||||
except OSError:
|
||||
return None
|
||||
return root if root.is_dir() else None
|
||||
|
||||
|
||||
def _candidates(provider: RootProvider) -> list[Path]:
|
||||
root = _configured_root(provider)
|
||||
if root is None:
|
||||
return []
|
||||
return sorted(
|
||||
(
|
||||
item
|
||||
for item in root.iterdir()
|
||||
if item.is_dir() and not item.is_symlink() and _RESULT_ID.fullmatch(item.name)
|
||||
),
|
||||
key=lambda item: item.stat().st_mtime_ns,
|
||||
reverse=True,
|
||||
)
|
||||
@@ -0,0 +1,185 @@
|
||||
"""Read-only LAB projection for the calibrated E46G ready-detector bake-off."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import re
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from fastapi.responses import FileResponse
|
||||
|
||||
from k1link.compute.e46g_rectified_detector_bakeoff import (
|
||||
E46GRectifiedDetectorBakeoffError,
|
||||
read_e46g_rectified_detector_bakeoff,
|
||||
)
|
||||
|
||||
_RESULT_ID = re.compile(r"^e46g-rectified-detector-bakeoff-[a-f0-9]{64}$")
|
||||
_CANDIDATES = ("trafficcamnet", "dashcamnet")
|
||||
RootProvider = Callable[[], Path | None]
|
||||
|
||||
|
||||
def build_e46g_rectified_detector_bakeoff_router(
|
||||
*, root_provider: RootProvider = lambda: None
|
||||
) -> APIRouter:
|
||||
router = APIRouter(prefix="/api/v1/laboratory/e46g", tags=["laboratory"])
|
||||
|
||||
def result(result_id: str) -> dict[str, Any]:
|
||||
if _RESULT_ID.fullmatch(result_id) is None:
|
||||
raise HTTPException(status_code=404, detail="E46G result не найден")
|
||||
root = _configured_root(root_provider)
|
||||
if root is None:
|
||||
raise HTTPException(status_code=404, detail="E46G result не найден")
|
||||
path = (root / result_id).resolve()
|
||||
if path.parent != root or path.is_symlink():
|
||||
raise HTTPException(status_code=404, detail="E46G result не найден")
|
||||
try:
|
||||
return read_e46g_rectified_detector_bakeoff(path)
|
||||
except (E46GRectifiedDetectorBakeoffError, OSError, ValueError):
|
||||
raise HTTPException(status_code=404, detail="E46G result не найден") from None
|
||||
|
||||
@router.get("/results")
|
||||
def list_results(limit: int = Query(default=1, ge=1, le=10)) -> dict[str, object]:
|
||||
candidates = _candidates(root_provider)
|
||||
items: list[dict[str, object]] = []
|
||||
invalid_total = 0
|
||||
for path in candidates:
|
||||
try:
|
||||
frozen = result(path.name)
|
||||
if len(items) < limit:
|
||||
items.append(_project_result(frozen))
|
||||
except HTTPException:
|
||||
invalid_total += 1
|
||||
return {
|
||||
"schema_version": "missioncore.e46g-rectified-detector-bakeoff-catalog/v1",
|
||||
"configured": _configured_root(root_provider) is not None,
|
||||
"items": items,
|
||||
"candidate_total": len(candidates),
|
||||
"invalid_total": invalid_total,
|
||||
"access": "read-only-diagnostic-selection",
|
||||
}
|
||||
|
||||
@router.get("/results/{result_id}/{candidate}.mp4")
|
||||
def get_comparison_video(result_id: str, candidate: str) -> FileResponse:
|
||||
if candidate not in _CANDIDATES:
|
||||
raise HTTPException(status_code=404, detail="E46G video не найден")
|
||||
frozen = result(result_id)
|
||||
descriptor = _video_descriptor(frozen, candidate)
|
||||
return FileResponse(
|
||||
frozen["comparison_paths"][candidate],
|
||||
media_type="video/mp4",
|
||||
headers={
|
||||
"Cache-Control": "public, max-age=31536000, immutable",
|
||||
"ETag": f'"{descriptor["sha256"]}"',
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
},
|
||||
)
|
||||
|
||||
return router
|
||||
|
||||
|
||||
def _project_result(frozen: dict[str, Any]) -> dict[str, object]:
|
||||
report = frozen["report"]
|
||||
result_id = frozen["result_id"]
|
||||
videos = {
|
||||
candidate: {
|
||||
"url": f"/api/v1/laboratory/e46g/results/{result_id}/{candidate}.mp4",
|
||||
"media_type": "video/mp4",
|
||||
"byte_length": descriptor["byte_length"],
|
||||
"sha256": descriptor["sha256"],
|
||||
"width": 2880,
|
||||
"height": 544,
|
||||
"duration_seconds": 60.0,
|
||||
"view_order": ["left", "front", "right"],
|
||||
}
|
||||
for candidate in _CANDIDATES
|
||||
for descriptor in (_video_descriptor(frozen, candidate),)
|
||||
}
|
||||
return {
|
||||
"schema_version": "missioncore.e46g-rectified-detector-bakeoff-view/v1",
|
||||
"result_id": result_id,
|
||||
"created_at_utc": report["created_at_utc"],
|
||||
"source_session_id": report["source_session_id"],
|
||||
"camera_source_id": report["camera_source_id"],
|
||||
"status": "selected-for-next-diagnostic-full-route",
|
||||
"selection": copy.deepcopy(report["selection"]),
|
||||
"rectification": copy.deepcopy(report["rectification"]),
|
||||
"metrics": copy.deepcopy(report["metrics"]),
|
||||
"acceptance": copy.deepcopy(report["acceptance"]),
|
||||
"method": copy.deepcopy(report["method"]),
|
||||
"limitations": copy.deepcopy(report["limitations"]),
|
||||
"comparison": {
|
||||
"controlled_change": "detector-provider-only",
|
||||
"visual_review": {
|
||||
"status": "selected-for-next-diagnostic",
|
||||
"reviewed_video_seconds": [0.0, 10.0, 20.0, 30.0, 40.0, 50.0],
|
||||
"selected_candidate": "trafficcamnet",
|
||||
"selected_view": "front",
|
||||
"excluded_views": ["left", "right"],
|
||||
"finding": (
|
||||
"The calibrated FRONT projection is useful. TrafficCamNet preserves "
|
||||
"substantially more visible vehicles and people than DashCamNet, while "
|
||||
"DashCamNet repeatedly leaves visible road objects undetected."
|
||||
),
|
||||
"risk": (
|
||||
"TrafficCamNet still produces duplicate or stale boxes around some "
|
||||
"vehicles. LEFT and RIGHT projections are dominated by the camera mount "
|
||||
"and trigger large semantic false tracks, so they are excluded."
|
||||
),
|
||||
"next_action": (
|
||||
"run TrafficCamNet with stock NvDCF over the complete retained FRONT "
|
||||
"projection and review the full temporal overlay before motion-state work"
|
||||
),
|
||||
},
|
||||
"verdict": "select-trafficcamnet-front-only-for-e46h",
|
||||
},
|
||||
"videos": videos,
|
||||
"ground_truth": False,
|
||||
"authority": copy.deepcopy(report["authority"]),
|
||||
"access": "read-only-diagnostic-selection",
|
||||
}
|
||||
|
||||
|
||||
def _video_descriptor(frozen: dict[str, Any], candidate: str) -> dict[str, Any]:
|
||||
descriptor = next(
|
||||
(
|
||||
row
|
||||
for row in frozen["manifest"]["artifacts"]
|
||||
if row.get("role") == f"comparison-video-{candidate}"
|
||||
),
|
||||
None,
|
||||
)
|
||||
if not isinstance(descriptor, dict):
|
||||
raise HTTPException(status_code=404, detail="E46G video не найден")
|
||||
return descriptor
|
||||
|
||||
|
||||
def _configured_root(provider: RootProvider) -> Path | None:
|
||||
value = provider()
|
||||
if value is None:
|
||||
return None
|
||||
candidate = value.expanduser().absolute()
|
||||
if candidate.is_symlink():
|
||||
return None
|
||||
try:
|
||||
root = candidate.resolve(strict=True)
|
||||
except OSError:
|
||||
return None
|
||||
return root if root.is_dir() else None
|
||||
|
||||
|
||||
def _candidates(provider: RootProvider) -> list[Path]:
|
||||
root = _configured_root(provider)
|
||||
if root is None:
|
||||
return []
|
||||
return sorted(
|
||||
(
|
||||
item
|
||||
for item in root.iterdir()
|
||||
if item.is_dir() and not item.is_symlink() and _RESULT_ID.fullmatch(item.name)
|
||||
),
|
||||
key=lambda item: item.stat().st_mtime_ns,
|
||||
reverse=True,
|
||||
)
|
||||
@@ -0,0 +1,224 @@
|
||||
"""Read-only LAB projection and video delivery for E46H."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import re
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from fastapi.responses import FileResponse
|
||||
|
||||
from k1link.compute.e46h_full_rectified_front_replay import (
|
||||
E46HFullRectifiedFrontReplayError,
|
||||
read_e46h_full_rectified_front_replay,
|
||||
)
|
||||
|
||||
_RESULT_ID = re.compile(r"^e46h-full-rectified-front-replay-[a-f0-9]{64}$")
|
||||
RootProvider = Callable[[], Path | None]
|
||||
|
||||
_REVIEW_WINDOWS = [
|
||||
{
|
||||
"id": "large-false-car-wall",
|
||||
"label": "6.0–10.9 с · false car на стене",
|
||||
"start_seconds": 6.0,
|
||||
"end_seconds": 10.9,
|
||||
"source_track_id": 6,
|
||||
"verdict": "semantic-false-positive",
|
||||
},
|
||||
{
|
||||
"id": "large-false-car-shrub",
|
||||
"label": "178.6–180.3 с · false car на кусте",
|
||||
"start_seconds": 178.6,
|
||||
"end_seconds": 180.3,
|
||||
"source_track_id": 371,
|
||||
"verdict": "semantic-false-positive",
|
||||
},
|
||||
{
|
||||
"id": "large-false-car-ground",
|
||||
"label": "250.7–265.6 с · false car на полотне",
|
||||
"start_seconds": 250.7,
|
||||
"end_seconds": 265.6,
|
||||
"source_track_id": 481,
|
||||
"verdict": "semantic-false-positive",
|
||||
},
|
||||
{
|
||||
"id": "large-false-car-road",
|
||||
"label": "392.2–400.6 с · false car на дороге",
|
||||
"start_seconds": 392.2,
|
||||
"end_seconds": 400.6,
|
||||
"source_track_id": 822,
|
||||
"verdict": "semantic-false-positive",
|
||||
},
|
||||
{
|
||||
"id": "empty-scene-not-blackout",
|
||||
"label": "419.4–426.9 с · пустая сцена, не сбой",
|
||||
"start_seconds": 419.4,
|
||||
"end_seconds": 426.9,
|
||||
"source_track_id": None,
|
||||
"verdict": "empty-scene-expected",
|
||||
},
|
||||
{
|
||||
"id": "large-false-car-terrace",
|
||||
"label": "440.8–448.4 с · false car на террасе",
|
||||
"start_seconds": 440.8,
|
||||
"end_seconds": 448.4,
|
||||
"source_track_id": 927,
|
||||
"verdict": "semantic-false-positive",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def build_e46h_full_rectified_front_replay_router(
|
||||
*, root_provider: RootProvider = lambda: None
|
||||
) -> APIRouter:
|
||||
router = APIRouter(prefix="/api/v1/laboratory/e46h", tags=["laboratory"])
|
||||
|
||||
def result(result_id: str) -> dict[str, Any]:
|
||||
if _RESULT_ID.fullmatch(result_id) is None:
|
||||
raise HTTPException(status_code=404, detail="E46H result не найден")
|
||||
root = _configured_root(root_provider)
|
||||
if root is None:
|
||||
raise HTTPException(status_code=404, detail="E46H result не найден")
|
||||
path = (root / result_id).resolve()
|
||||
if path.parent != root or path.is_symlink():
|
||||
raise HTTPException(status_code=404, detail="E46H result не найден")
|
||||
try:
|
||||
return read_e46h_full_rectified_front_replay(path)
|
||||
except (E46HFullRectifiedFrontReplayError, OSError, ValueError):
|
||||
raise HTTPException(status_code=404, detail="E46H result не найден") from None
|
||||
|
||||
@router.get("/results")
|
||||
def list_results(limit: int = Query(default=1, ge=1, le=10)) -> dict[str, object]:
|
||||
candidates = _candidates(root_provider)
|
||||
items: list[dict[str, object]] = []
|
||||
invalid_total = 0
|
||||
for path in candidates:
|
||||
try:
|
||||
frozen = result(path.name)
|
||||
if len(items) < limit:
|
||||
items.append(_project_result(frozen))
|
||||
except HTTPException:
|
||||
invalid_total += 1
|
||||
return {
|
||||
"schema_version": "missioncore.e46h-full-rectified-front-replay-catalog/v1",
|
||||
"configured": _configured_root(root_provider) is not None,
|
||||
"items": items,
|
||||
"candidate_total": len(candidates),
|
||||
"invalid_total": invalid_total,
|
||||
"access": "read-only-full-route-diagnostic",
|
||||
}
|
||||
|
||||
@router.get("/results/{result_id}/overlay.mp4")
|
||||
def get_overlay_video(result_id: str) -> FileResponse:
|
||||
frozen = result(result_id)
|
||||
descriptor = _video_descriptor(frozen)
|
||||
return FileResponse(
|
||||
frozen["overlay_path"],
|
||||
media_type="video/mp4",
|
||||
headers={
|
||||
"Cache-Control": "public, max-age=31536000, immutable",
|
||||
"ETag": f'"{descriptor["sha256"]}"',
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
},
|
||||
)
|
||||
|
||||
return router
|
||||
|
||||
|
||||
def _project_result(frozen: dict[str, Any]) -> dict[str, object]:
|
||||
report = frozen["report"]
|
||||
descriptor = _video_descriptor(frozen)
|
||||
result_id = frozen["result_id"]
|
||||
return {
|
||||
"schema_version": "missioncore.e46h-full-rectified-front-replay-view/v1",
|
||||
"result_id": result_id,
|
||||
"created_at_utc": report["created_at_utc"],
|
||||
"source_session_id": report["source_session_id"],
|
||||
"camera_source_id": report["camera_source_id"],
|
||||
"status": "diagnostic-regression-large-semantic-false-tracks",
|
||||
"baseline_result_id": report["baseline_result_id"],
|
||||
"selection": copy.deepcopy(report["selection"]),
|
||||
"rectification": copy.deepcopy(report["rectification"]),
|
||||
"metrics": copy.deepcopy(report["metrics"]),
|
||||
"acceptance": copy.deepcopy(report["acceptance"]),
|
||||
"decision": copy.deepcopy(report["decision"]),
|
||||
"method": copy.deepcopy(report["method"]),
|
||||
"limitations": copy.deepcopy(report["limitations"]),
|
||||
"visual_review": {
|
||||
"status": "full-continuous-and-targeted-review-completed",
|
||||
"complete_video_reviewed": True,
|
||||
"reviewed_video_range_seconds": [0.0, 448.8],
|
||||
"verdict": "useful-front-continuity-but-semantic-regression-blocks-promotion",
|
||||
"review_windows": copy.deepcopy(_REVIEW_WINDOWS),
|
||||
"finding": (
|
||||
"FRONT is readable and most route objects are tracked, but five large false "
|
||||
"car tracks attach to a wall, shrub, road surface and terrace."
|
||||
),
|
||||
"blackout_interpretation": (
|
||||
"The two zero-track intervals at 419.4..426.9 video seconds show an actually "
|
||||
"empty scene and are not object-layer failures."
|
||||
),
|
||||
"next_action": (
|
||||
"do not tune the detector to this route; isolate a provider-level semantic "
|
||||
"suppression or stronger ready model on the same immutable FRONT adapter"
|
||||
),
|
||||
},
|
||||
"video": {
|
||||
"url": f"/api/v1/laboratory/e46h/results/{result_id}/overlay.mp4",
|
||||
"media_type": "video/mp4",
|
||||
"byte_length": descriptor["byte_length"],
|
||||
"sha256": descriptor["sha256"],
|
||||
"width": 960,
|
||||
"height": 544,
|
||||
"duration_seconds": 448.8,
|
||||
},
|
||||
"ground_truth": False,
|
||||
"authority": copy.deepcopy(report["authority"]),
|
||||
"access": "read-only-full-route-diagnostic",
|
||||
}
|
||||
|
||||
|
||||
def _video_descriptor(frozen: dict[str, Any]) -> dict[str, Any]:
|
||||
descriptor = next(
|
||||
(
|
||||
row
|
||||
for row in frozen["manifest"]["artifacts"]
|
||||
if row.get("role") == "visual-overlay-video"
|
||||
),
|
||||
None,
|
||||
)
|
||||
if not isinstance(descriptor, dict):
|
||||
raise HTTPException(status_code=404, detail="E46H video не найден")
|
||||
return descriptor
|
||||
|
||||
|
||||
def _configured_root(provider: RootProvider) -> Path | None:
|
||||
value = provider()
|
||||
if value is None:
|
||||
return None
|
||||
candidate = value.expanduser().absolute()
|
||||
if candidate.is_symlink():
|
||||
return None
|
||||
try:
|
||||
root = candidate.resolve(strict=True)
|
||||
except OSError:
|
||||
return None
|
||||
return root if root.is_dir() else None
|
||||
|
||||
|
||||
def _candidates(provider: RootProvider) -> list[Path]:
|
||||
root = _configured_root(provider)
|
||||
if root is None:
|
||||
return []
|
||||
return sorted(
|
||||
(
|
||||
item
|
||||
for item in root.iterdir()
|
||||
if item.is_dir() and not item.is_symlink() and _RESULT_ID.fullmatch(item.name)
|
||||
),
|
||||
key=lambda item: item.stat().st_mtime_ns,
|
||||
reverse=True,
|
||||
)
|
||||
@@ -0,0 +1,194 @@
|
||||
"""Read-only LAB projection and visual evidence delivery for E46I."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import re
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from fastapi.responses import FileResponse
|
||||
|
||||
from k1link.compute.e46i_grounding_dino_full_replay import (
|
||||
E46IGroundingDinoFullReplayError,
|
||||
read_e46i_grounding_dino_full_replay,
|
||||
)
|
||||
|
||||
_RESULT_ID = re.compile(r"^e46i-grounding-dino-full-replay-[a-f0-9]{64}$")
|
||||
RootProvider = Callable[[], Path | None]
|
||||
|
||||
|
||||
def build_e46i_grounding_dino_full_replay_router(
|
||||
*, root_provider: RootProvider = lambda: None
|
||||
) -> APIRouter:
|
||||
router = APIRouter(prefix="/api/v1/laboratory/e46i", tags=["laboratory"])
|
||||
|
||||
def result(result_id: str) -> dict[str, Any]:
|
||||
if _RESULT_ID.fullmatch(result_id) is None:
|
||||
raise HTTPException(status_code=404, detail="E46I result не найден")
|
||||
root = _configured_root(root_provider)
|
||||
if root is None:
|
||||
raise HTTPException(status_code=404, detail="E46I result не найден")
|
||||
path = (root / result_id).resolve()
|
||||
if path.parent != root or path.is_symlink():
|
||||
raise HTTPException(status_code=404, detail="E46I result не найден")
|
||||
try:
|
||||
return read_e46i_grounding_dino_full_replay(path)
|
||||
except (E46IGroundingDinoFullReplayError, OSError, ValueError):
|
||||
raise HTTPException(status_code=404, detail="E46I result не найден") from None
|
||||
|
||||
@router.get("/results")
|
||||
def list_results(limit: int = Query(default=1, ge=1, le=10)) -> dict[str, object]:
|
||||
candidates = _candidates(root_provider)
|
||||
items: list[dict[str, object]] = []
|
||||
invalid_total = 0
|
||||
for path in candidates:
|
||||
try:
|
||||
frozen = result(path.name)
|
||||
if len(items) < limit:
|
||||
items.append(_project_result(frozen))
|
||||
except HTTPException:
|
||||
invalid_total += 1
|
||||
return {
|
||||
"schema_version": "missioncore.e46i-grounding-dino-full-replay-catalog/v1",
|
||||
"configured": _configured_root(root_provider) is not None,
|
||||
"items": items,
|
||||
"candidate_total": len(candidates),
|
||||
"invalid_total": invalid_total,
|
||||
"access": "read-only-full-route-diagnostic",
|
||||
}
|
||||
|
||||
@router.get("/results/{result_id}/overlay.mp4")
|
||||
def get_overlay_video(result_id: str) -> FileResponse:
|
||||
frozen = result(result_id)
|
||||
descriptor = _artifact_descriptor(frozen, "visual-overlay-video")
|
||||
return _file_response(frozen["overlay_path"], descriptor, "video/mp4")
|
||||
|
||||
@router.get("/results/{result_id}/visual/{role}.png")
|
||||
def get_visual(result_id: str, role: str) -> FileResponse:
|
||||
roles = {
|
||||
"shadow-gate": ("shadow-gate-contact-sheet", "shadow_sheet_path"),
|
||||
"full-route": ("full-route-contact-sheet", "route_sheet_path"),
|
||||
"targeted-windows": (
|
||||
"targeted-windows-contact-sheet",
|
||||
"targeted_sheet_path",
|
||||
),
|
||||
}
|
||||
selected = roles.get(role)
|
||||
if selected is None:
|
||||
raise HTTPException(status_code=404, detail="E46I visual не найден")
|
||||
frozen = result(result_id)
|
||||
descriptor = _artifact_descriptor(frozen, selected[0])
|
||||
return _file_response(frozen[selected[1]], descriptor, "image/png")
|
||||
|
||||
return router
|
||||
|
||||
|
||||
def _project_result(frozen: dict[str, Any]) -> dict[str, object]:
|
||||
report = frozen["report"]
|
||||
result_id = frozen["result_id"]
|
||||
video = _artifact_descriptor(frozen, "visual-overlay-video")
|
||||
visual_roles = {
|
||||
"shadow_gate": "shadow-gate-contact-sheet",
|
||||
"full_route": "full-route-contact-sheet",
|
||||
"targeted_windows": "targeted-windows-contact-sheet",
|
||||
}
|
||||
visuals = {}
|
||||
for key, role in visual_roles.items():
|
||||
descriptor = _artifact_descriptor(frozen, role)
|
||||
slug = role.removesuffix("-contact-sheet")
|
||||
visuals[key] = {
|
||||
"url": f"/api/v1/laboratory/e46i/results/{result_id}/visual/{slug}.png",
|
||||
"media_type": "image/png",
|
||||
"byte_length": descriptor["byte_length"],
|
||||
"sha256": descriptor["sha256"],
|
||||
}
|
||||
return {
|
||||
"schema_version": "missioncore.e46i-grounding-dino-full-replay-view/v1",
|
||||
"result_id": result_id,
|
||||
"created_at_utc": report["created_at_utc"],
|
||||
"status": report["status"],
|
||||
"baseline_result_id": report["baseline_result_id"],
|
||||
"source": copy.deepcopy(report["source"]),
|
||||
"provider": copy.deepcopy(report["provider"]),
|
||||
"inference": copy.deepcopy(report["inference"]),
|
||||
"metrics": copy.deepcopy(report["metrics"]),
|
||||
"shadow_gate": copy.deepcopy(report["shadow_gate"]),
|
||||
"visual_review": copy.deepcopy(report["visual_review"]),
|
||||
"acceptance": copy.deepcopy(report["acceptance"]),
|
||||
"decision": copy.deepcopy(report["decision"]),
|
||||
"method": copy.deepcopy(report["method"]),
|
||||
"limitations": copy.deepcopy(report["limitations"]),
|
||||
"video": {
|
||||
"url": f"/api/v1/laboratory/e46i/results/{result_id}/overlay.mp4",
|
||||
"media_type": "video/mp4",
|
||||
"byte_length": video["byte_length"],
|
||||
"sha256": video["sha256"],
|
||||
"width": 960,
|
||||
"height": 544,
|
||||
"frame_rate": 10.0,
|
||||
"frame_count": 4488,
|
||||
"duration_seconds": 448.8,
|
||||
},
|
||||
"visuals": visuals,
|
||||
"ground_truth": False,
|
||||
"authority": copy.deepcopy(report["authority"]),
|
||||
"access": "read-only-full-route-diagnostic",
|
||||
}
|
||||
|
||||
|
||||
def _artifact_descriptor(frozen: dict[str, Any], role: str) -> dict[str, Any]:
|
||||
descriptor = next(
|
||||
(
|
||||
row
|
||||
for row in frozen["manifest"]["artifacts"]
|
||||
if isinstance(row, dict) and row.get("role") == role
|
||||
),
|
||||
None,
|
||||
)
|
||||
if not isinstance(descriptor, dict):
|
||||
raise HTTPException(status_code=404, detail="E46I artifact не найден")
|
||||
return descriptor
|
||||
|
||||
|
||||
def _file_response(path: Path, descriptor: dict[str, Any], media_type: str) -> FileResponse:
|
||||
return FileResponse(
|
||||
path,
|
||||
media_type=media_type,
|
||||
headers={
|
||||
"Cache-Control": "public, max-age=31536000, immutable",
|
||||
"ETag": f'"{descriptor["sha256"]}"',
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _configured_root(provider: RootProvider) -> Path | None:
|
||||
value = provider()
|
||||
if value is None:
|
||||
return None
|
||||
candidate = value.expanduser().absolute()
|
||||
if candidate.is_symlink():
|
||||
return None
|
||||
try:
|
||||
root = candidate.resolve(strict=True)
|
||||
except OSError:
|
||||
return None
|
||||
return root if root.is_dir() else None
|
||||
|
||||
|
||||
def _candidates(provider: RootProvider) -> list[Path]:
|
||||
root = _configured_root(provider)
|
||||
if root is None:
|
||||
return []
|
||||
return sorted(
|
||||
(
|
||||
item
|
||||
for item in root.iterdir()
|
||||
if item.is_dir() and not item.is_symlink() and _RESULT_ID.fullmatch(item.name)
|
||||
),
|
||||
key=lambda item: item.stat().st_mtime_ns,
|
||||
reverse=True,
|
||||
)
|
||||
@@ -0,0 +1,196 @@
|
||||
"""Read-only LAB projection and visual evidence delivery for E46J."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import re
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from fastapi.responses import FileResponse
|
||||
|
||||
from k1link.compute.e46j_raw_fisheye_realtime import (
|
||||
E46JRawFisheyeRealtimeError,
|
||||
read_e46j_raw_fisheye_realtime,
|
||||
)
|
||||
|
||||
_RESULT_ID = re.compile(r"^e46j-raw-fisheye-realtime-[a-f0-9]{64}$")
|
||||
RootProvider = Callable[[], Path | None]
|
||||
|
||||
|
||||
def build_e46j_raw_fisheye_realtime_router(
|
||||
*, root_provider: RootProvider = lambda: None
|
||||
) -> APIRouter:
|
||||
router = APIRouter(prefix="/api/v1/laboratory/e46j", tags=["laboratory"])
|
||||
|
||||
def result(result_id: str) -> dict[str, Any]:
|
||||
if _RESULT_ID.fullmatch(result_id) is None:
|
||||
raise HTTPException(status_code=404, detail="E46J result не найден")
|
||||
root = _configured_root(root_provider)
|
||||
if root is None:
|
||||
raise HTTPException(status_code=404, detail="E46J result не найден")
|
||||
path = (root / result_id).resolve()
|
||||
if path.parent != root or path.is_symlink():
|
||||
raise HTTPException(status_code=404, detail="E46J result не найден")
|
||||
try:
|
||||
return read_e46j_raw_fisheye_realtime(path)
|
||||
except (E46JRawFisheyeRealtimeError, OSError, ValueError):
|
||||
raise HTTPException(status_code=404, detail="E46J result не найден") from None
|
||||
|
||||
@router.get("/results")
|
||||
def list_results(limit: int = Query(default=1, ge=1, le=10)) -> dict[str, object]:
|
||||
candidates = _candidates(root_provider)
|
||||
items: list[dict[str, object]] = []
|
||||
invalid_total = 0
|
||||
for path in candidates:
|
||||
try:
|
||||
frozen = result(path.name)
|
||||
if len(items) < limit:
|
||||
items.append(_project_result(frozen))
|
||||
except HTTPException:
|
||||
invalid_total += 1
|
||||
return {
|
||||
"schema_version": "missioncore.e46j-raw-fisheye-realtime-catalog/v1",
|
||||
"configured": _configured_root(root_provider) is not None,
|
||||
"items": items,
|
||||
"candidate_total": len(candidates),
|
||||
"invalid_total": invalid_total,
|
||||
"access": "read-only-full-route-diagnostic",
|
||||
}
|
||||
|
||||
@router.get("/results/{result_id}/overlay.mp4")
|
||||
def get_overlay_video(result_id: str) -> FileResponse:
|
||||
frozen = result(result_id)
|
||||
descriptor = _artifact_descriptor(frozen, "visual-overlay-video")
|
||||
return _file_response(frozen["overlay_path"], descriptor, "video/mp4")
|
||||
|
||||
@router.get("/results/{result_id}/visual/{role}.png")
|
||||
def get_visual(result_id: str, role: str) -> FileResponse:
|
||||
roles = {
|
||||
"full-route": ("full-route-contact-sheet", "route_sheet_path"),
|
||||
"targeted-windows": (
|
||||
"targeted-windows-contact-sheet",
|
||||
"targeted_sheet_path",
|
||||
),
|
||||
"operator-shadow": (
|
||||
"operator-shadow-contact-sheet",
|
||||
"shadow_sheet_path",
|
||||
),
|
||||
}
|
||||
selected = roles.get(role)
|
||||
if selected is None:
|
||||
raise HTTPException(status_code=404, detail="E46J visual не найден")
|
||||
frozen = result(result_id)
|
||||
descriptor = _artifact_descriptor(frozen, selected[0])
|
||||
return _file_response(frozen[selected[1]], descriptor, "image/png")
|
||||
|
||||
return router
|
||||
|
||||
|
||||
def _project_result(frozen: dict[str, Any]) -> dict[str, object]:
|
||||
report = frozen["report"]
|
||||
result_id = frozen["result_id"]
|
||||
video = _artifact_descriptor(frozen, "visual-overlay-video")
|
||||
visual_roles = {
|
||||
"full_route": "full-route-contact-sheet",
|
||||
"targeted_windows": "targeted-windows-contact-sheet",
|
||||
"operator_shadow": "operator-shadow-contact-sheet",
|
||||
}
|
||||
visuals = {}
|
||||
for key, role in visual_roles.items():
|
||||
descriptor = _artifact_descriptor(frozen, role)
|
||||
slug = role.removesuffix("-contact-sheet")
|
||||
visuals[key] = {
|
||||
"url": f"/api/v1/laboratory/e46j/results/{result_id}/visual/{slug}.png",
|
||||
"media_type": "image/png",
|
||||
"byte_length": descriptor["byte_length"],
|
||||
"sha256": descriptor["sha256"],
|
||||
}
|
||||
return {
|
||||
"schema_version": "missioncore.e46j-raw-fisheye-realtime-view/v1",
|
||||
"result_id": result_id,
|
||||
"created_at_utc": report["created_at_utc"],
|
||||
"status": report["status"],
|
||||
"source": copy.deepcopy(report["source"]),
|
||||
"detector": copy.deepcopy(report["detector"]),
|
||||
"preprocessing": copy.deepcopy(report["preprocessing"]),
|
||||
"detection": copy.deepcopy(report["detection"]),
|
||||
"metrics": copy.deepcopy(report["metrics"]),
|
||||
"visual_review": copy.deepcopy(report["visual_review"]),
|
||||
"acceptance": copy.deepcopy(report["acceptance"]),
|
||||
"decision": copy.deepcopy(report["decision"]),
|
||||
"method": copy.deepcopy(report["method"]),
|
||||
"limitations": copy.deepcopy(report["limitations"]),
|
||||
"video": {
|
||||
"url": f"/api/v1/laboratory/e46j/results/{result_id}/overlay.mp4",
|
||||
"media_type": "video/mp4",
|
||||
"byte_length": video["byte_length"],
|
||||
"sha256": video["sha256"],
|
||||
"width": 800,
|
||||
"height": 600,
|
||||
"frame_rate": 10.003944527024467,
|
||||
"frame_count": 4489,
|
||||
"duration_seconds": 448.723,
|
||||
},
|
||||
"visuals": visuals,
|
||||
"ground_truth": False,
|
||||
"authority": copy.deepcopy(report["authority"]),
|
||||
"access": "read-only-full-route-diagnostic",
|
||||
}
|
||||
|
||||
|
||||
def _artifact_descriptor(frozen: dict[str, Any], role: str) -> dict[str, Any]:
|
||||
descriptor = next(
|
||||
(
|
||||
row
|
||||
for row in frozen["manifest"]["artifacts"]
|
||||
if isinstance(row, dict) and row.get("role") == role
|
||||
),
|
||||
None,
|
||||
)
|
||||
if not isinstance(descriptor, dict):
|
||||
raise HTTPException(status_code=404, detail="E46J artifact не найден")
|
||||
return descriptor
|
||||
|
||||
|
||||
def _file_response(path: Path, descriptor: dict[str, Any], media_type: str) -> FileResponse:
|
||||
return FileResponse(
|
||||
path,
|
||||
media_type=media_type,
|
||||
headers={
|
||||
"Cache-Control": "public, max-age=31536000, immutable",
|
||||
"ETag": f'"{descriptor["sha256"]}"',
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _configured_root(provider: RootProvider) -> Path | None:
|
||||
value = provider()
|
||||
if value is None:
|
||||
return None
|
||||
candidate = value.expanduser().absolute()
|
||||
if candidate.is_symlink():
|
||||
return None
|
||||
try:
|
||||
root = candidate.resolve(strict=True)
|
||||
except OSError:
|
||||
return None
|
||||
return root if root.is_dir() else None
|
||||
|
||||
|
||||
def _candidates(provider: RootProvider) -> list[Path]:
|
||||
root = _configured_root(provider)
|
||||
if root is None:
|
||||
return []
|
||||
return sorted(
|
||||
(
|
||||
item
|
||||
for item in root.iterdir()
|
||||
if item.is_dir() and not item.is_symlink() and _RESULT_ID.fullmatch(item.name)
|
||||
),
|
||||
key=lambda item: item.stat().st_mtime_ns,
|
||||
reverse=True,
|
||||
)
|
||||
@@ -0,0 +1,323 @@
|
||||
"""Read-only API for sealed L3.2 camera-bound PointPillars evidence."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
from typing import Any, Final
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from fastapi.responses import FileResponse
|
||||
|
||||
RootProvider = Callable[[], Path | None]
|
||||
|
||||
RESULT_SCHEMA: Final = "missioncore.l32-pointpillars-camera-review/v1"
|
||||
CATALOG_SCHEMA: Final = "missioncore.l32-pointpillars-camera-review-catalog/v1"
|
||||
FRAME_SCHEMA: Final = "missioncore.l32-pointpillars-camera-review-frame/v1"
|
||||
RESULT_PROJECTION_SCHEMA: Final = (
|
||||
"missioncore.l32-pointpillars-camera-review-result/v1"
|
||||
)
|
||||
RESULT_CATALOG_SCHEMA: Final = (
|
||||
"missioncore.l32-pointpillars-camera-review-catalog-results/v1"
|
||||
)
|
||||
RESULT_ID: Final = re.compile(
|
||||
r"^l32-pointpillars-camera-review-[a-f0-9]{64}$"
|
||||
)
|
||||
FRAME_ID: Final = re.compile(r"^[0-9]{6}$")
|
||||
SHA256: Final = re.compile(r"^[a-f0-9]{64}$")
|
||||
MAX_JSON_BYTES: Final = 16 * 1024 * 1024
|
||||
MAX_FRAMES: Final = 18
|
||||
|
||||
|
||||
def build_l32_pointpillars_camera_review_router(
|
||||
*,
|
||||
root_provider: RootProvider = lambda: None,
|
||||
) -> APIRouter:
|
||||
router = APIRouter(
|
||||
prefix="/api/v1/laboratory/l32/pointpillars-camera-review",
|
||||
tags=["laboratory"],
|
||||
)
|
||||
|
||||
@router.get("/results")
|
||||
def list_results(
|
||||
limit: int = Query(default=1, ge=1, le=10),
|
||||
) -> dict[str, object]:
|
||||
root = _configured_root(root_provider)
|
||||
if root is None:
|
||||
return _empty_catalog(False)
|
||||
candidates = _candidates(root)
|
||||
items: list[dict[str, object]] = []
|
||||
invalid_total = 0
|
||||
for candidate in candidates:
|
||||
try:
|
||||
items.append(_project_result(candidate))
|
||||
except RuntimeError:
|
||||
invalid_total += 1
|
||||
items.sort(
|
||||
key=lambda item: (str(item["created_at_utc"]), str(item["result_id"])),
|
||||
reverse=True,
|
||||
)
|
||||
return {
|
||||
"schema_version": RESULT_CATALOG_SCHEMA,
|
||||
"configured": True,
|
||||
"items": items[:limit],
|
||||
"candidate_total": len(candidates),
|
||||
"invalid_total": invalid_total,
|
||||
"access": "read-only",
|
||||
}
|
||||
|
||||
@router.get("/{result_id}/frames/{frame_id}")
|
||||
def get_frame(result_id: str, frame_id: str) -> dict[str, object]:
|
||||
candidate, descriptor = _resolve_frame(
|
||||
root_provider, result_id=result_id, frame_id=frame_id
|
||||
)
|
||||
path = candidate / str(descriptor["detail_path"])
|
||||
payload = _read_json(path)
|
||||
if (
|
||||
payload.get("schema_version") != FRAME_SCHEMA
|
||||
or payload.get("frame_id") != frame_id
|
||||
or descriptor.get("detail_sha256") != _sha256(path)
|
||||
or descriptor.get("detail_byte_length") != path.stat().st_size
|
||||
or not _valid_frame_payload(payload)
|
||||
):
|
||||
raise HTTPException(status_code=404, detail="L3.2 frame not found")
|
||||
return {**copy.deepcopy(payload), "access": "read-only"}
|
||||
|
||||
@router.get("/{result_id}/frames/{frame_id}/camera")
|
||||
def get_camera(result_id: str, frame_id: str) -> FileResponse:
|
||||
candidate, descriptor = _resolve_frame(
|
||||
root_provider, result_id=result_id, frame_id=frame_id
|
||||
)
|
||||
path = (candidate / str(descriptor["camera_path"])).resolve()
|
||||
if (
|
||||
not path.is_relative_to(candidate)
|
||||
or path.is_symlink()
|
||||
or not path.is_file()
|
||||
or descriptor.get("camera_sha256") != _sha256(path)
|
||||
):
|
||||
raise HTTPException(status_code=404, detail="L3.2 camera not found")
|
||||
return FileResponse(path, media_type="image/jpeg")
|
||||
|
||||
return router
|
||||
|
||||
|
||||
def latest_l32_identity(root_provider: RootProvider) -> dict[str, str] | None:
|
||||
root = _configured_root(root_provider)
|
||||
if root is None:
|
||||
return None
|
||||
valid: list[dict[str, object]] = []
|
||||
for candidate in _candidates(root):
|
||||
try:
|
||||
valid.append(_project_result(candidate))
|
||||
except RuntimeError:
|
||||
continue
|
||||
if not valid:
|
||||
return None
|
||||
latest = max(
|
||||
valid,
|
||||
key=lambda item: (str(item["created_at_utc"]), str(item["result_id"])),
|
||||
)
|
||||
return {
|
||||
"result_id": str(latest["result_id"]),
|
||||
"created_at_utc": str(latest["created_at_utc"]),
|
||||
}
|
||||
|
||||
|
||||
def _resolve_frame(
|
||||
root_provider: RootProvider,
|
||||
*,
|
||||
result_id: str,
|
||||
frame_id: str,
|
||||
) -> tuple[Path, dict[str, Any]]:
|
||||
if not RESULT_ID.fullmatch(result_id) or not FRAME_ID.fullmatch(frame_id):
|
||||
raise HTTPException(status_code=404, detail="L3.2 frame not found")
|
||||
root = _configured_root(root_provider)
|
||||
if root is None:
|
||||
raise HTTPException(status_code=404, detail="L3.2 result not found")
|
||||
candidate = root / result_id
|
||||
try:
|
||||
result = _load_result(candidate)
|
||||
descriptor = next(
|
||||
item for item in result["catalog"]["frames"]
|
||||
if item["frame_id"] == frame_id
|
||||
)
|
||||
except (RuntimeError, StopIteration):
|
||||
raise HTTPException(status_code=404, detail="L3.2 frame not found") from None
|
||||
return candidate, descriptor
|
||||
|
||||
|
||||
def _project_result(candidate: Path) -> dict[str, object]:
|
||||
result = _load_result(candidate)
|
||||
manifest = result["manifest"]
|
||||
identity = manifest["identity"]
|
||||
return {
|
||||
"schema_version": RESULT_PROJECTION_SCHEMA,
|
||||
"result_id": manifest["result_id"],
|
||||
"created_at_utc": manifest["created_at_utc"],
|
||||
"status": "camera-bound-review-rejects-current-candidate",
|
||||
"source_session_id": identity["source_session_id"],
|
||||
"source_l31_result_id": identity["source_l31_result_id"],
|
||||
"source_e10_pack_id": identity["source_e10_pack_id"],
|
||||
"projection": copy.deepcopy(identity["projection"]),
|
||||
"metrics": copy.deepcopy(manifest["metrics"]),
|
||||
"frames": copy.deepcopy(result["catalog"]["frames"]),
|
||||
"limitations": copy.deepcopy(manifest["limitations"]),
|
||||
"authority": copy.deepcopy(manifest["authority"]),
|
||||
"access": "read-only",
|
||||
}
|
||||
|
||||
|
||||
def _load_result(candidate: Path) -> dict[str, Any]:
|
||||
if (
|
||||
not candidate.is_dir()
|
||||
or candidate.is_symlink()
|
||||
or not RESULT_ID.fullmatch(candidate.name)
|
||||
):
|
||||
raise RuntimeError("L3.2 candidate is invalid")
|
||||
manifest = _read_json(candidate / "manifest.json")
|
||||
identity = manifest.get("identity")
|
||||
authority = manifest.get("authority")
|
||||
catalog_descriptor = manifest.get("catalog")
|
||||
if (
|
||||
manifest.get("schema_version") != RESULT_SCHEMA
|
||||
or manifest.get("result_id") != candidate.name
|
||||
or manifest.get("status") != "camera-bound-review-rejects-current-candidate"
|
||||
or not isinstance(manifest.get("created_at_utc"), str)
|
||||
or not isinstance(identity, dict)
|
||||
or identity.get("schema_version") != RESULT_SCHEMA
|
||||
or identity.get("source_session_id") != "20260720T065719Z_viewer_live"
|
||||
or manifest.get("identity_sha256")
|
||||
!= hashlib.sha256(_canonical_json(identity)).hexdigest()
|
||||
or candidate.name
|
||||
!= f"l32-pointpillars-camera-review-{manifest.get('identity_sha256')}"
|
||||
or not isinstance(authority, dict)
|
||||
or authority.get("shadow_only") is not True
|
||||
or authority.get("accuracy_accepted") is not False
|
||||
or authority.get("commands_enabled") is not False
|
||||
or not isinstance(manifest.get("metrics"), dict)
|
||||
or not isinstance(manifest.get("limitations"), list)
|
||||
or not isinstance(catalog_descriptor, dict)
|
||||
or catalog_descriptor.get("path") != "catalog.json"
|
||||
):
|
||||
raise RuntimeError("L3.2 manifest is invalid")
|
||||
catalog_path = candidate / "catalog.json"
|
||||
if (
|
||||
catalog_descriptor.get("sha256") != _sha256(catalog_path)
|
||||
or catalog_descriptor.get("byte_length") != catalog_path.stat().st_size
|
||||
):
|
||||
raise RuntimeError("L3.2 catalog changed")
|
||||
catalog = _read_json(catalog_path)
|
||||
frames = catalog.get("frames")
|
||||
if (
|
||||
catalog.get("schema_version") != CATALOG_SCHEMA
|
||||
or catalog.get("result_id") != candidate.name
|
||||
or not isinstance(frames, list)
|
||||
or not 1 <= len(frames) <= MAX_FRAMES
|
||||
or catalog.get("frame_count") != len(frames)
|
||||
or len({item.get("frame_id") for item in frames if isinstance(item, dict)})
|
||||
!= len(frames)
|
||||
or any(not _valid_descriptor(item) for item in frames)
|
||||
):
|
||||
raise RuntimeError("L3.2 catalog is invalid")
|
||||
return {"manifest": manifest, "catalog": catalog}
|
||||
|
||||
|
||||
def _valid_descriptor(value: object) -> bool:
|
||||
if not isinstance(value, dict):
|
||||
return False
|
||||
frame_id = value.get("frame_id")
|
||||
return (
|
||||
isinstance(frame_id, str)
|
||||
and FRAME_ID.fullmatch(frame_id) is not None
|
||||
and value.get("detail_path") == f"frame-{frame_id}.json"
|
||||
and isinstance(value.get("detail_sha256"), str)
|
||||
and SHA256.fullmatch(value["detail_sha256"]) is not None
|
||||
and isinstance(value.get("detail_byte_length"), int)
|
||||
and 0 < value["detail_byte_length"] <= MAX_JSON_BYTES
|
||||
and isinstance(value.get("camera_path"), str)
|
||||
and value["camera_path"].startswith("frames/frame-")
|
||||
and isinstance(value.get("camera_sha256"), str)
|
||||
and SHA256.fullmatch(value["camera_sha256"]) is not None
|
||||
and isinstance(value.get("camera_delta_ms"), (int, float))
|
||||
and abs(float(value["camera_delta_ms"])) <= 100.0
|
||||
)
|
||||
|
||||
|
||||
def _valid_frame_payload(value: dict[str, Any]) -> bool:
|
||||
projection = value.get("camera_projection")
|
||||
camera = value.get("camera")
|
||||
interpretation = value.get("interpretation")
|
||||
return (
|
||||
isinstance(value.get("summary"), dict)
|
||||
and isinstance(value.get("points"), dict)
|
||||
and isinstance(value.get("prediction_boxes"), list)
|
||||
and isinstance(projection, dict)
|
||||
and projection.get("point_layout") == "flat-xy-depth-m"
|
||||
and isinstance(projection.get("point_count"), int)
|
||||
and isinstance(projection.get("point_values"), list)
|
||||
and len(projection["point_values"]) == projection["point_count"] * 3
|
||||
and isinstance(projection.get("boxes"), list)
|
||||
and isinstance(camera, dict)
|
||||
and camera.get("media_type") == "image/jpeg"
|
||||
and camera.get("exact_source_frame") is True
|
||||
and isinstance(interpretation, dict)
|
||||
and interpretation.get("camera_is_semantic_reference") is True
|
||||
and interpretation.get("boxes_are_model_hypotheses") is True
|
||||
and interpretation.get("accuracy_claim_allowed") is False
|
||||
)
|
||||
|
||||
|
||||
def _configured_root(provider: RootProvider) -> Path | None:
|
||||
configured = provider()
|
||||
if configured is None:
|
||||
return None
|
||||
root = configured.expanduser().resolve()
|
||||
return root if root.is_dir() and not root.is_symlink() else None
|
||||
|
||||
|
||||
def _candidates(root: Path) -> list[Path]:
|
||||
return [
|
||||
item for item in root.iterdir()
|
||||
if item.is_dir() and not item.is_symlink() and RESULT_ID.fullmatch(item.name)
|
||||
]
|
||||
|
||||
|
||||
def _empty_catalog(configured: bool) -> dict[str, object]:
|
||||
return {
|
||||
"schema_version": RESULT_CATALOG_SCHEMA,
|
||||
"configured": configured,
|
||||
"items": [],
|
||||
"candidate_total": 0,
|
||||
"invalid_total": 0,
|
||||
"access": "read-only",
|
||||
}
|
||||
|
||||
|
||||
def _read_json(path: Path) -> dict[str, Any]:
|
||||
if path.is_symlink() or not path.is_file() or path.stat().st_size > MAX_JSON_BYTES:
|
||||
raise RuntimeError("L3.2 JSON artifact is invalid")
|
||||
try:
|
||||
value = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError) as exc:
|
||||
raise RuntimeError("L3.2 JSON artifact is invalid") from exc
|
||||
if not isinstance(value, dict):
|
||||
raise RuntimeError("L3.2 JSON artifact is invalid")
|
||||
return value
|
||||
|
||||
|
||||
def _canonical_json(value: object) -> bytes:
|
||||
return json.dumps(
|
||||
value, ensure_ascii=False, sort_keys=True, separators=(",", ":")
|
||||
).encode("utf-8")
|
||||
|
||||
|
||||
def _sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
while chunk := stream.read(1024 * 1024):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
@@ -0,0 +1,394 @@
|
||||
"""Read-only API for sealed L3.3 camera-first detector evidence."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
from typing import Any, Final
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from fastapi.responses import FileResponse
|
||||
|
||||
from k1link.compute.l33_camera_first_admission import is_admitted_l33_identity
|
||||
|
||||
RootProvider = Callable[[], Path | None]
|
||||
|
||||
RESULT_SCHEMA: Final = "missioncore.l33-camera-first-detector-review/v1"
|
||||
CATALOG_SCHEMA: Final = "missioncore.l33-camera-first-detector-review-catalog/v1"
|
||||
FRAME_SCHEMA: Final = "missioncore.l33-camera-first-detector-review-frame/v1"
|
||||
RESULT_PROJECTION_SCHEMA: Final = "missioncore.l33-camera-first-detector-review-result/v1"
|
||||
RESULT_CATALOG_SCHEMA: Final = "missioncore.l33-camera-first-detector-review-catalog-results/v1"
|
||||
RESULT_ID: Final = re.compile(r"^l33-camera-first-detector-review-[a-f0-9]{64}$")
|
||||
FRAME_ID: Final = re.compile(r"^[0-9]{6}$")
|
||||
SHA256: Final = re.compile(r"^[a-f0-9]{64}$")
|
||||
MAX_JSON_BYTES: Final = 16 * 1024 * 1024
|
||||
MAX_FRAMES: Final = 18
|
||||
|
||||
|
||||
def build_l33_camera_first_detector_review_router(
|
||||
*,
|
||||
root_provider: RootProvider = lambda: None,
|
||||
) -> APIRouter:
|
||||
router = APIRouter(
|
||||
prefix="/api/v1/laboratory/l33/camera-first-detector-review",
|
||||
tags=["laboratory"],
|
||||
)
|
||||
|
||||
@router.get("/results")
|
||||
def list_results(limit: int = Query(default=1, ge=1, le=10)) -> dict[str, object]:
|
||||
root = _configured_root(root_provider)
|
||||
if root is None:
|
||||
return _empty_catalog(False)
|
||||
candidates = _candidates(root)
|
||||
items: list[dict[str, object]] = []
|
||||
invalid_total = 0
|
||||
for candidate in candidates:
|
||||
try:
|
||||
items.append(_project_result(candidate))
|
||||
except RuntimeError:
|
||||
invalid_total += 1
|
||||
items.sort(
|
||||
key=lambda item: (str(item["created_at_utc"]), str(item["result_id"])),
|
||||
reverse=True,
|
||||
)
|
||||
return {
|
||||
"schema_version": RESULT_CATALOG_SCHEMA,
|
||||
"configured": True,
|
||||
"items": items[:limit],
|
||||
"candidate_total": len(candidates),
|
||||
"invalid_total": invalid_total,
|
||||
"access": "read-only",
|
||||
}
|
||||
|
||||
@router.get("/{result_id}/frames/{frame_id}")
|
||||
def get_frame(result_id: str, frame_id: str) -> dict[str, object]:
|
||||
candidate, descriptor = _resolve_frame(
|
||||
root_provider, result_id=result_id, frame_id=frame_id
|
||||
)
|
||||
path = candidate / str(descriptor["detail_path"])
|
||||
payload = _read_json(path)
|
||||
if (
|
||||
payload.get("schema_version") != FRAME_SCHEMA
|
||||
or payload.get("frame_id") != frame_id
|
||||
or descriptor.get("detail_sha256") != _sha256(path)
|
||||
or descriptor.get("detail_byte_length") != path.stat().st_size
|
||||
or not _valid_frame_payload(payload)
|
||||
):
|
||||
raise HTTPException(status_code=404, detail="L3.3 frame not found")
|
||||
return {**copy.deepcopy(payload), "access": "read-only"}
|
||||
|
||||
@router.get("/{result_id}/frames/{frame_id}/camera")
|
||||
def get_camera(result_id: str, frame_id: str) -> FileResponse:
|
||||
candidate, descriptor = _resolve_frame(
|
||||
root_provider, result_id=result_id, frame_id=frame_id
|
||||
)
|
||||
path = (candidate / str(descriptor["camera_path"])).resolve()
|
||||
if (
|
||||
not path.is_relative_to(candidate)
|
||||
or path.is_symlink()
|
||||
or not path.is_file()
|
||||
or descriptor.get("camera_sha256") != _sha256(path)
|
||||
):
|
||||
raise HTTPException(status_code=404, detail="L3.3 camera not found")
|
||||
return FileResponse(path, media_type="image/jpeg")
|
||||
|
||||
return router
|
||||
|
||||
|
||||
def latest_l33_identity(root_provider: RootProvider) -> dict[str, str] | None:
|
||||
root = _configured_root(root_provider)
|
||||
if root is None:
|
||||
return None
|
||||
valid: list[dict[str, object]] = []
|
||||
for candidate in _candidates(root):
|
||||
try:
|
||||
valid.append(_project_result(candidate))
|
||||
except RuntimeError:
|
||||
continue
|
||||
if not valid:
|
||||
return None
|
||||
latest = max(
|
||||
valid,
|
||||
key=lambda item: (str(item["created_at_utc"]), str(item["result_id"])),
|
||||
)
|
||||
return {
|
||||
"result_id": str(latest["result_id"]),
|
||||
"created_at_utc": str(latest["created_at_utc"]),
|
||||
}
|
||||
|
||||
|
||||
def _resolve_frame(
|
||||
root_provider: RootProvider,
|
||||
*,
|
||||
result_id: str,
|
||||
frame_id: str,
|
||||
) -> tuple[Path, dict[str, Any]]:
|
||||
if not RESULT_ID.fullmatch(result_id) or not FRAME_ID.fullmatch(frame_id):
|
||||
raise HTTPException(status_code=404, detail="L3.3 frame not found")
|
||||
root = _configured_root(root_provider)
|
||||
if root is None:
|
||||
raise HTTPException(status_code=404, detail="L3.3 result not found")
|
||||
candidate = root / result_id
|
||||
try:
|
||||
result = _load_result(candidate)
|
||||
descriptor = next(
|
||||
item for item in result["catalog"]["frames"] if item["frame_id"] == frame_id
|
||||
)
|
||||
except (RuntimeError, StopIteration):
|
||||
raise HTTPException(status_code=404, detail="L3.3 frame not found") from None
|
||||
return candidate, descriptor
|
||||
|
||||
|
||||
def _project_result(candidate: Path) -> dict[str, object]:
|
||||
result = _load_result(candidate)
|
||||
manifest = result["manifest"]
|
||||
identity = manifest["identity"]
|
||||
return {
|
||||
"schema_version": RESULT_PROJECTION_SCHEMA,
|
||||
"result_id": manifest["result_id"],
|
||||
"created_at_utc": manifest["created_at_utc"],
|
||||
"status": manifest["status"],
|
||||
"source_session_id": identity["source_session_id"],
|
||||
"source_l32_result_id": identity["source_l32_result_id"],
|
||||
"source_e26_result_id": identity["source_e26_result_id"],
|
||||
"source_e29_result_id": identity["source_e29_result_id"],
|
||||
"source_e10_pack_id": identity["source_e10_pack_id"],
|
||||
"source_world_state_result_id": identity.get("source_world_state_result_id"),
|
||||
"detector": copy.deepcopy(identity["detector"]),
|
||||
"semantic_contract": copy.deepcopy(identity["semantic_contract"]),
|
||||
"geometry_contract": copy.deepcopy(identity["geometry_contract"]),
|
||||
"metrics": copy.deepcopy(manifest["metrics"]),
|
||||
"frames": copy.deepcopy(result["catalog"]["frames"]),
|
||||
"limitations": copy.deepcopy(manifest["limitations"]),
|
||||
"authority": copy.deepcopy(manifest["authority"]),
|
||||
"access": "read-only",
|
||||
}
|
||||
|
||||
|
||||
def _load_result(candidate: Path) -> dict[str, Any]:
|
||||
if (
|
||||
not candidate.is_dir()
|
||||
or candidate.is_symlink()
|
||||
or not RESULT_ID.fullmatch(candidate.name)
|
||||
):
|
||||
raise RuntimeError("L3.3 candidate is invalid")
|
||||
manifest = _read_json(candidate / "manifest.json")
|
||||
identity = manifest.get("identity")
|
||||
authority = manifest.get("authority")
|
||||
catalog_descriptor = manifest.get("catalog")
|
||||
if (
|
||||
manifest.get("schema_version") != RESULT_SCHEMA
|
||||
or manifest.get("result_id") != candidate.name
|
||||
or manifest.get("status") != "camera-first-boundary-restored-shadow-only"
|
||||
or not isinstance(manifest.get("created_at_utc"), str)
|
||||
or manifest.get("ground_truth") is not False
|
||||
or not isinstance(identity, dict)
|
||||
or identity.get("schema_version") != RESULT_SCHEMA
|
||||
or not is_admitted_l33_identity(identity)
|
||||
or manifest.get("identity_sha256")
|
||||
!= hashlib.sha256(_canonical_json(identity)).hexdigest()
|
||||
or candidate.name
|
||||
!= f"l33-camera-first-detector-review-{manifest.get('identity_sha256')}"
|
||||
or not isinstance(authority, dict)
|
||||
or authority.get("shadow_only") is not True
|
||||
or authority.get("accuracy_accepted") is not False
|
||||
or authority.get("commands_enabled") is not False
|
||||
or authority.get("navigation_or_safety_accepted") is not False
|
||||
or not isinstance(manifest.get("metrics"), dict)
|
||||
or not isinstance(manifest.get("limitations"), list)
|
||||
or not isinstance(catalog_descriptor, dict)
|
||||
or catalog_descriptor.get("path") != "catalog.json"
|
||||
):
|
||||
raise RuntimeError("L3.3 manifest is invalid")
|
||||
catalog_path = candidate / "catalog.json"
|
||||
if (
|
||||
catalog_descriptor.get("sha256") != _sha256(catalog_path)
|
||||
or catalog_descriptor.get("byte_length") != catalog_path.stat().st_size
|
||||
):
|
||||
raise RuntimeError("L3.3 catalog changed")
|
||||
catalog = _read_json(catalog_path)
|
||||
frames = catalog.get("frames")
|
||||
if (
|
||||
catalog.get("schema_version") != CATALOG_SCHEMA
|
||||
or catalog.get("result_id") != candidate.name
|
||||
or not isinstance(frames, list)
|
||||
or not 1 <= len(frames) <= MAX_FRAMES
|
||||
or catalog.get("frame_count") != len(frames)
|
||||
or len({item.get("frame_id") for item in frames if isinstance(item, dict)})
|
||||
!= len(frames)
|
||||
or any(not _valid_descriptor(item) for item in frames)
|
||||
):
|
||||
raise RuntimeError("L3.3 catalog is invalid")
|
||||
return {"manifest": manifest, "catalog": catalog}
|
||||
|
||||
|
||||
def _valid_descriptor(value: object) -> bool:
|
||||
if not isinstance(value, dict):
|
||||
return False
|
||||
frame_id = value.get("frame_id")
|
||||
return (
|
||||
isinstance(frame_id, str)
|
||||
and FRAME_ID.fullmatch(frame_id) is not None
|
||||
and value.get("detail_path") == f"frame-{frame_id}.json"
|
||||
and isinstance(value.get("detail_sha256"), str)
|
||||
and SHA256.fullmatch(value["detail_sha256"]) is not None
|
||||
and isinstance(value.get("detail_byte_length"), int)
|
||||
and 0 < value["detail_byte_length"] <= MAX_JSON_BYTES
|
||||
and isinstance(value.get("camera_path"), str)
|
||||
and value["camera_path"].startswith("frames/frame-")
|
||||
and isinstance(value.get("camera_sha256"), str)
|
||||
and SHA256.fullmatch(value["camera_sha256"]) is not None
|
||||
and value.get("semantic_provenance")
|
||||
in (
|
||||
"recorded-current",
|
||||
"bounded-track-interpolation",
|
||||
"rectified-detector-current",
|
||||
"e23-temporal-hold",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _valid_frame_payload(value: dict[str, Any]) -> bool:
|
||||
projection = value.get("camera_projection")
|
||||
camera = value.get("camera")
|
||||
detections = value.get("detections")
|
||||
prediction_boxes = value.get("prediction_boxes", [])
|
||||
interpretation = value.get("interpretation")
|
||||
return (
|
||||
isinstance(value.get("summary"), dict)
|
||||
and isinstance(value.get("points"), dict)
|
||||
and isinstance(projection, dict)
|
||||
and projection.get("point_layout") == "flat-xy-depth-m"
|
||||
and isinstance(projection.get("point_count"), int)
|
||||
and isinstance(projection.get("point_values"), list)
|
||||
and len(projection["point_values"]) == projection["point_count"] * 3
|
||||
and isinstance(camera, dict)
|
||||
and camera.get("media_type") == "image/jpeg"
|
||||
and camera.get("exact_source_frame") is True
|
||||
and isinstance(detections, list)
|
||||
and all(_valid_detection(item) for item in detections)
|
||||
and isinstance(prediction_boxes, list)
|
||||
and all(_valid_prediction_box(item) for item in prediction_boxes)
|
||||
and isinstance(interpretation, dict)
|
||||
and interpretation.get("camera_owns_semantic_identity") is True
|
||||
and interpretation.get("lidar_owns_metric_range") is True
|
||||
and interpretation.get("missing_lidar_erases_camera_detection") is False
|
||||
and interpretation.get("ground_truth_available") is False
|
||||
and interpretation.get("accuracy_claim_allowed") is False
|
||||
)
|
||||
|
||||
|
||||
def _valid_detection(value: object) -> bool:
|
||||
if not isinstance(value, dict):
|
||||
return False
|
||||
bbox = value.get("bbox_xyxy")
|
||||
camera_ray = value.get("camera_ray_lidar")
|
||||
geometry_anchor = value.get("geometry_anchor_xyz_m")
|
||||
return (
|
||||
isinstance(value.get("source_track_id"), int)
|
||||
and isinstance(value.get("label"), str)
|
||||
and isinstance(value.get("score"), (int, float))
|
||||
and 0.25 <= float(value["score"]) <= 1.0
|
||||
and isinstance(bbox, list)
|
||||
and len(bbox) == 4
|
||||
and all(isinstance(item, (int, float)) for item in bbox)
|
||||
and value.get("semantic_provenance")
|
||||
in (
|
||||
"recorded-current",
|
||||
"bounded-track-interpolation",
|
||||
"rectified-detector-current",
|
||||
"e23-temporal-hold",
|
||||
)
|
||||
and isinstance(value.get("geometry_status"), str)
|
||||
and (value.get("range_m") is None or isinstance(value.get("range_m"), (int, float)))
|
||||
and (
|
||||
value.get("source_e29_optical_depth_m") is None
|
||||
or isinstance(value.get("source_e29_optical_depth_m"), (int, float))
|
||||
)
|
||||
and isinstance(camera_ray, dict)
|
||||
and _valid_vector(camera_ray.get("origin_xyz_m"), 3)
|
||||
and _valid_vector(camera_ray.get("direction_xyz"), 3)
|
||||
and (geometry_anchor is None or _valid_vector(geometry_anchor, 3))
|
||||
and ((value.get("range_m") is None) == (geometry_anchor is None))
|
||||
)
|
||||
|
||||
|
||||
def _valid_prediction_box(value: object) -> bool:
|
||||
if not isinstance(value, dict):
|
||||
return False
|
||||
return (
|
||||
isinstance(value.get("benchmark_class"), str)
|
||||
and _valid_vector(value.get("center_xyz_m"), 3)
|
||||
and _valid_vector(value.get("size_lwh_m"), 3)
|
||||
and all(float(item) > 0 for item in value["size_lwh_m"])
|
||||
and isinstance(value.get("yaw_rad"), (int, float))
|
||||
and value.get("status") == "model-prediction"
|
||||
and isinstance(value.get("score"), (int, float))
|
||||
and 0 <= float(value["score"]) <= 1
|
||||
and isinstance(value.get("track_id"), int)
|
||||
and isinstance(value.get("temporal_status"), str)
|
||||
)
|
||||
|
||||
|
||||
def _valid_vector(value: object, length: int) -> bool:
|
||||
return (
|
||||
isinstance(value, list)
|
||||
and len(value) == length
|
||||
and all(isinstance(item, (int, float)) for item in value)
|
||||
)
|
||||
|
||||
|
||||
def _configured_root(provider: RootProvider) -> Path | None:
|
||||
configured = provider()
|
||||
if configured is None:
|
||||
return None
|
||||
root = configured.expanduser().resolve()
|
||||
return root if root.is_dir() and not root.is_symlink() else None
|
||||
|
||||
|
||||
def _candidates(root: Path) -> list[Path]:
|
||||
return [
|
||||
item for item in root.iterdir()
|
||||
if item.is_dir() and not item.is_symlink() and RESULT_ID.fullmatch(item.name)
|
||||
]
|
||||
|
||||
|
||||
def _empty_catalog(configured: bool) -> dict[str, object]:
|
||||
return {
|
||||
"schema_version": RESULT_CATALOG_SCHEMA,
|
||||
"configured": configured,
|
||||
"items": [],
|
||||
"candidate_total": 0,
|
||||
"invalid_total": 0,
|
||||
"access": "read-only",
|
||||
}
|
||||
|
||||
|
||||
def _read_json(path: Path) -> dict[str, Any]:
|
||||
if path.is_symlink() or not path.is_file() or path.stat().st_size > MAX_JSON_BYTES:
|
||||
raise RuntimeError("L3.3 JSON artifact is invalid")
|
||||
try:
|
||||
value = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError) as exc:
|
||||
raise RuntimeError("L3.3 JSON artifact is invalid") from exc
|
||||
if not isinstance(value, dict):
|
||||
raise RuntimeError("L3.3 JSON artifact is invalid")
|
||||
return value
|
||||
|
||||
|
||||
def _canonical_json(value: object) -> bytes:
|
||||
return json.dumps(
|
||||
value, ensure_ascii=False, sort_keys=True, separators=(",", ":")
|
||||
).encode("utf-8")
|
||||
|
||||
|
||||
def _sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,450 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import re
|
||||
import struct
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path, PurePosixPath
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from fastapi.responses import FileResponse
|
||||
|
||||
from k1link.compute.e46_detector_truth_island import (
|
||||
E46_MANIFEST_NAME,
|
||||
E46_REFERENCES_NAME,
|
||||
E46DetectorTruthIslandError,
|
||||
read_e46_detector_truth_island,
|
||||
)
|
||||
from k1link.compute.l34_right_yolox_truth_island_freeze import (
|
||||
L34_MANIFEST_NAME,
|
||||
L34_PREDICTION_SCHEMA,
|
||||
L34RightYoloxTruthIsland,
|
||||
L34RightYoloxTruthIslandError,
|
||||
read_l34_right_yolox_truth_island_freeze,
|
||||
)
|
||||
|
||||
RootProvider = Callable[[], Path | None]
|
||||
_RESULT_ID = re.compile(r"^l34-right-yolox-truth-island-freeze-[a-f0-9]{64}$")
|
||||
_TRUTH_ID = re.compile(r"^e46-detector-truth-island-[a-f0-9]{64}$")
|
||||
_PACK_ID = re.compile(r"^evaluation-pack-[a-f0-9]{64}$")
|
||||
_PNG_SIGNATURE = b"\x89PNG\r\n\x1a\n"
|
||||
_MAX_VISUAL_FRAMES = 32
|
||||
|
||||
|
||||
def latest_l34_identity(provider: RootProvider) -> dict[str, object] | None:
|
||||
for candidate in _candidates(provider):
|
||||
try:
|
||||
result = read_l34_right_yolox_truth_island_freeze(candidate)
|
||||
except L34RightYoloxTruthIslandError:
|
||||
continue
|
||||
created_at_utc = result.manifest.get("created_at_utc")
|
||||
if not isinstance(created_at_utc, str) or not created_at_utc.strip():
|
||||
continue
|
||||
return {
|
||||
"result_id": result.result_id,
|
||||
"created_at_utc": created_at_utc,
|
||||
"access": "read-only",
|
||||
}
|
||||
return None
|
||||
|
||||
|
||||
def build_l34_right_yolox_truth_island_freeze_router(
|
||||
*,
|
||||
root_provider: RootProvider = lambda: None,
|
||||
truth_root_provider: RootProvider = lambda: None,
|
||||
evaluation_pack_root_provider: RootProvider = lambda: None,
|
||||
) -> APIRouter:
|
||||
router = APIRouter(prefix="/api/v1/laboratory", tags=["laboratory"])
|
||||
|
||||
@router.get("/l34/results")
|
||||
def list_l34_results(
|
||||
limit: int = Query(default=1, ge=1, le=10),
|
||||
) -> dict[str, object]:
|
||||
items: list[dict[str, object]] = []
|
||||
invalid_total = 0
|
||||
for candidate in _candidates(root_provider):
|
||||
try:
|
||||
result = read_l34_right_yolox_truth_island_freeze(candidate)
|
||||
items.append(_project(result))
|
||||
except L34RightYoloxTruthIslandError:
|
||||
invalid_total += 1
|
||||
continue
|
||||
if len(items) >= limit:
|
||||
break
|
||||
return {
|
||||
"schema_version": (
|
||||
"missioncore.l34-right-yolox-truth-island-catalog/v1"
|
||||
),
|
||||
"configured": _configured_root(root_provider) is not None,
|
||||
"items": items,
|
||||
"candidate_total": len(items),
|
||||
"invalid_total": invalid_total,
|
||||
"access": "read-only",
|
||||
}
|
||||
|
||||
@router.get("/l34/results/{result_id}/frames/{sequence}")
|
||||
def get_l34_frame(result_id: str, sequence: int) -> dict[str, object]:
|
||||
result, prediction, reference, image_path = _resolve_visual_frame(
|
||||
root_provider=root_provider,
|
||||
truth_root_provider=truth_root_provider,
|
||||
evaluation_pack_root_provider=evaluation_pack_root_provider,
|
||||
result_id=result_id,
|
||||
sequence=sequence,
|
||||
)
|
||||
width, height = _png_dimensions(image_path)
|
||||
predictions = _validated_predictions(prediction, width=width, height=height)
|
||||
identity = result.manifest["identity"]
|
||||
candidate = identity["candidate"]
|
||||
return {
|
||||
"schema_version": "missioncore.l34-right-yolox-truth-island-frame/v1",
|
||||
"result_id": result.result_id,
|
||||
"truth_island_sequence": sequence,
|
||||
"image_id": prediction["image_id"],
|
||||
"frame_index": prediction["frame_index"],
|
||||
"group_id": prediction["group_id"],
|
||||
"role": reference["role"],
|
||||
"session_seconds": reference["session_seconds"],
|
||||
"camera": {
|
||||
"width": width,
|
||||
"height": height,
|
||||
"byte_length": image_path.stat().st_size,
|
||||
"sha256": reference["sha256"],
|
||||
},
|
||||
"prediction_rows_sha256": candidate["prediction_rows_sha256"],
|
||||
"predictions": predictions,
|
||||
"truth_labels_read": False,
|
||||
"access": "read-only",
|
||||
}
|
||||
|
||||
@router.get("/l34/results/{result_id}/frames/{sequence}/camera")
|
||||
def get_l34_camera(result_id: str, sequence: int) -> FileResponse:
|
||||
_, _, _, image_path = _resolve_visual_frame(
|
||||
root_provider=root_provider,
|
||||
truth_root_provider=truth_root_provider,
|
||||
evaluation_pack_root_provider=evaluation_pack_root_provider,
|
||||
result_id=result_id,
|
||||
sequence=sequence,
|
||||
)
|
||||
return FileResponse(image_path, media_type="image/png")
|
||||
|
||||
return router
|
||||
|
||||
|
||||
def _project(result: L34RightYoloxTruthIsland) -> dict[str, object]:
|
||||
identity = result.manifest["identity"]
|
||||
report = result.report
|
||||
return {
|
||||
"result_id": result.result_id,
|
||||
"created_at_utc": result.manifest["created_at_utc"],
|
||||
"status": report["status"],
|
||||
"profile_id": report["profile_id"],
|
||||
"pipeline_id": report["pipeline_id"],
|
||||
"source_session_id": report["source_session_id"],
|
||||
"camera_source_id": report["camera_source_id"],
|
||||
"candidate": {
|
||||
**copy.deepcopy(identity["profile"]["candidate"]),
|
||||
"l33_result_id": identity["candidate"]["l33_result_id"],
|
||||
},
|
||||
"truth_island": {
|
||||
"result_id": identity["truth_island"]["result_id"],
|
||||
"truth_state": identity["truth_island"]["truth_state"],
|
||||
},
|
||||
"metrics": copy.deepcopy(report["metrics"]),
|
||||
"frames": [_frame_summary(row) for row in result.predictions],
|
||||
"decision": copy.deepcopy(report["decision"]),
|
||||
"limitations": copy.deepcopy(report["limitations"]),
|
||||
"authority": copy.deepcopy(report["authority"]),
|
||||
"access": "read-only",
|
||||
}
|
||||
|
||||
|
||||
def _frame_summary(row: dict[str, Any]) -> dict[str, object]:
|
||||
sequence = _nonnegative_integer(row.get("truth_island_sequence"), "sequence")
|
||||
image_id = _nonnegative_integer(row.get("image_id"), "image id")
|
||||
frame_index = _nonnegative_integer(row.get("frame_index"), "frame index")
|
||||
group_id = row.get("group_id")
|
||||
predictions = row.get("predictions")
|
||||
if (
|
||||
row.get("schema_version") != L34_PREDICTION_SCHEMA
|
||||
or not 1 <= sequence <= _MAX_VISUAL_FRAMES
|
||||
or not isinstance(group_id, str)
|
||||
or not group_id.strip()
|
||||
or not isinstance(predictions, list)
|
||||
):
|
||||
raise L34RightYoloxTruthIslandError("L34 visual frame is invalid")
|
||||
scores = [
|
||||
float(prediction["score"])
|
||||
for prediction in predictions
|
||||
if isinstance(prediction, dict)
|
||||
and isinstance(prediction.get("score"), (int, float))
|
||||
and not isinstance(prediction.get("score"), bool)
|
||||
]
|
||||
if len(scores) != len(predictions) or any(not math.isfinite(score) for score in scores):
|
||||
raise L34RightYoloxTruthIslandError("L34 visual scores are invalid")
|
||||
return {
|
||||
"truth_island_sequence": sequence,
|
||||
"image_id": image_id,
|
||||
"frame_index": frame_index,
|
||||
"group_id": group_id,
|
||||
"prediction_count": len(predictions),
|
||||
"maximum_score": max(scores, default=0.0),
|
||||
}
|
||||
|
||||
|
||||
def _resolve_visual_frame(
|
||||
*,
|
||||
root_provider: RootProvider,
|
||||
truth_root_provider: RootProvider,
|
||||
evaluation_pack_root_provider: RootProvider,
|
||||
result_id: str,
|
||||
sequence: int,
|
||||
) -> tuple[L34RightYoloxTruthIsland, dict[str, Any], dict[str, Any], Path]:
|
||||
if _RESULT_ID.fullmatch(result_id) is None or not 1 <= sequence <= _MAX_VISUAL_FRAMES:
|
||||
raise HTTPException(status_code=404, detail="L34 frame not found")
|
||||
result_root = _configured_root(root_provider)
|
||||
if result_root is None:
|
||||
raise HTTPException(status_code=404, detail="L34 result not found")
|
||||
result_path = (result_root / result_id).resolve()
|
||||
if result_path.parent != result_root or result_path.is_symlink():
|
||||
raise HTTPException(status_code=404, detail="L34 result not found")
|
||||
try:
|
||||
result = read_l34_right_yolox_truth_island_freeze(result_path)
|
||||
except (L34RightYoloxTruthIslandError, OSError):
|
||||
raise HTTPException(status_code=404, detail="L34 result not found") from None
|
||||
prediction = next(
|
||||
(
|
||||
row
|
||||
for row in result.predictions
|
||||
if row.get("truth_island_sequence") == sequence
|
||||
),
|
||||
None,
|
||||
)
|
||||
if prediction is None:
|
||||
raise HTTPException(status_code=404, detail="L34 frame not found")
|
||||
|
||||
identity = result.manifest["identity"]
|
||||
truth_identity = identity["truth_island"]
|
||||
truth_id = truth_identity.get("result_id")
|
||||
truth_root = _configured_root(truth_root_provider)
|
||||
if (
|
||||
truth_root is None
|
||||
or not isinstance(truth_id, str)
|
||||
or _TRUTH_ID.fullmatch(truth_id) is None
|
||||
):
|
||||
raise HTTPException(status_code=404, detail="L34 truth island not found")
|
||||
truth_path = (truth_root / truth_id).resolve()
|
||||
if truth_path.parent != truth_root or truth_path.is_symlink():
|
||||
raise HTTPException(status_code=404, detail="L34 truth island not found")
|
||||
try:
|
||||
truth = read_e46_detector_truth_island(truth_path)
|
||||
except (E46DetectorTruthIslandError, OSError):
|
||||
raise HTTPException(status_code=404, detail="L34 truth island not found") from None
|
||||
if _sha256(truth.result_root / E46_MANIFEST_NAME) != truth_identity.get(
|
||||
"manifest_sha256"
|
||||
):
|
||||
raise HTTPException(status_code=404, detail="L34 truth island changed")
|
||||
references = _read_jsonl(truth.result_root / E46_REFERENCES_NAME)
|
||||
reference = next(
|
||||
(row for row in references if row.get("truth_island_sequence") == sequence),
|
||||
None,
|
||||
)
|
||||
if (
|
||||
reference is None
|
||||
or prediction.get("candidate_id") != "yolox-s-kb4-core3"
|
||||
or prediction.get("truth_joined") is not False
|
||||
or reference.get("frame_index") != prediction.get("frame_index")
|
||||
or reference.get("image_id") != prediction.get("image_id")
|
||||
or reference.get("group_id") != prediction.get("group_id")
|
||||
or reference.get("session_seconds")
|
||||
!= prediction.get("session_seconds")
|
||||
or reference.get("sha256")
|
||||
!= prediction.get("source_image_sha256")
|
||||
or reference.get("role") not in {"anchor", "temporal"}
|
||||
or not isinstance(reference.get("session_seconds"), (int, float))
|
||||
):
|
||||
raise HTTPException(status_code=404, detail="L34 frame binding is invalid")
|
||||
|
||||
truth_source = truth.manifest["identity"]["source"]
|
||||
pack_id = truth_source.get("evaluation_pack_id")
|
||||
pack_root = _configured_root(evaluation_pack_root_provider)
|
||||
if (
|
||||
pack_root is None
|
||||
or not isinstance(pack_id, str)
|
||||
or _PACK_ID.fullmatch(pack_id) is None
|
||||
):
|
||||
raise HTTPException(status_code=404, detail="L34 image source not found")
|
||||
pack_path = (pack_root / pack_id).resolve()
|
||||
if pack_path.parent != pack_root or pack_path.is_symlink() or not pack_path.is_dir():
|
||||
raise HTTPException(status_code=404, detail="L34 image source not found")
|
||||
relative = reference.get("source_path")
|
||||
if not isinstance(relative, str):
|
||||
raise HTTPException(status_code=404, detail="L34 image source not found")
|
||||
pure = PurePosixPath(relative)
|
||||
if (
|
||||
pure.is_absolute()
|
||||
or ".." in pure.parts
|
||||
or pure.parts[:2] != ("images", "valid-fov-fill")
|
||||
or pure.suffix.lower() != ".png"
|
||||
):
|
||||
raise HTTPException(status_code=404, detail="L34 image source not found")
|
||||
image_path = (pack_path / Path(*pure.parts)).resolve()
|
||||
if (
|
||||
not image_path.is_relative_to(pack_path)
|
||||
or image_path.is_symlink()
|
||||
or not image_path.is_file()
|
||||
or image_path.stat().st_size != reference.get("byte_length")
|
||||
or _sha256(image_path) != reference.get("sha256")
|
||||
):
|
||||
raise HTTPException(status_code=404, detail="L34 image source changed")
|
||||
return result, prediction, reference, image_path
|
||||
|
||||
|
||||
def resolve_l34_frame_source(
|
||||
*,
|
||||
root_provider: RootProvider,
|
||||
truth_root_provider: RootProvider,
|
||||
evaluation_pack_root_provider: RootProvider,
|
||||
result_id: str,
|
||||
sequence: int,
|
||||
) -> tuple[L34RightYoloxTruthIsland, dict[str, Any], dict[str, Any], Path]:
|
||||
"""Resolve one immutable L3.4 source frame for bounded sibling APIs.
|
||||
|
||||
The caller receives the server-side binding. Prediction-free source routes
|
||||
project only the source reference; explicitly assisted routes may project a
|
||||
bounded, provenance-marked subset of ``prediction``.
|
||||
"""
|
||||
|
||||
return _resolve_visual_frame(
|
||||
root_provider=root_provider,
|
||||
truth_root_provider=truth_root_provider,
|
||||
evaluation_pack_root_provider=evaluation_pack_root_provider,
|
||||
result_id=result_id,
|
||||
sequence=sequence,
|
||||
)
|
||||
|
||||
|
||||
def _validated_predictions(
|
||||
row: dict[str, Any],
|
||||
*,
|
||||
width: int,
|
||||
height: int,
|
||||
) -> list[dict[str, object]]:
|
||||
predictions = row.get("predictions")
|
||||
if not isinstance(predictions, list):
|
||||
raise HTTPException(status_code=404, detail="L34 predictions not found")
|
||||
projected: list[dict[str, object]] = []
|
||||
for prediction in predictions:
|
||||
if not isinstance(prediction, dict):
|
||||
raise HTTPException(status_code=404, detail="L34 prediction is invalid")
|
||||
label = prediction.get("label")
|
||||
score = prediction.get("score")
|
||||
bbox = prediction.get("bbox_xyxy")
|
||||
if (
|
||||
label not in {
|
||||
"person",
|
||||
"bicycle",
|
||||
"motorcycle",
|
||||
"car",
|
||||
"heavy_vehicle",
|
||||
"static_obstacle",
|
||||
"animal",
|
||||
}
|
||||
or not isinstance(score, (int, float))
|
||||
or isinstance(score, bool)
|
||||
or not math.isfinite(float(score))
|
||||
or not 0.25 <= float(score) <= 1.0
|
||||
or not isinstance(bbox, list)
|
||||
or len(bbox) != 4
|
||||
or any(
|
||||
not isinstance(value, (int, float))
|
||||
or isinstance(value, bool)
|
||||
or not math.isfinite(float(value))
|
||||
for value in bbox
|
||||
)
|
||||
):
|
||||
raise HTTPException(status_code=404, detail="L34 prediction is invalid")
|
||||
left, top, right, bottom = (float(value) for value in bbox)
|
||||
if not (0 <= left < right <= width and 0 <= top < bottom <= height):
|
||||
raise HTTPException(status_code=404, detail="L34 prediction box is invalid")
|
||||
projected.append(
|
||||
{
|
||||
"label": label,
|
||||
"score": float(score),
|
||||
"bbox_xyxy": [left, top, right, bottom],
|
||||
}
|
||||
)
|
||||
return projected
|
||||
|
||||
|
||||
def _png_dimensions(path: Path) -> tuple[int, int]:
|
||||
try:
|
||||
with path.open("rb") as stream:
|
||||
header = stream.read(24)
|
||||
except OSError:
|
||||
raise HTTPException(status_code=404, detail="L34 camera not found") from None
|
||||
if len(header) != 24 or header[:8] != _PNG_SIGNATURE or header[12:16] != b"IHDR":
|
||||
raise HTTPException(status_code=404, detail="L34 camera is invalid")
|
||||
width, height = struct.unpack(">II", header[16:24])
|
||||
if not 1 <= width <= 8192 or not 1 <= height <= 8192:
|
||||
raise HTTPException(status_code=404, detail="L34 camera is invalid")
|
||||
return width, height
|
||||
|
||||
|
||||
def _read_jsonl(path: Path) -> list[dict[str, Any]]:
|
||||
try:
|
||||
rows = [json.loads(line) for line in path.read_text(encoding="utf-8").splitlines() if line]
|
||||
except (json.JSONDecodeError, OSError):
|
||||
raise HTTPException(status_code=404, detail="L34 frame index not found") from None
|
||||
if not rows or len(rows) > _MAX_VISUAL_FRAMES or any(not isinstance(row, dict) for row in rows):
|
||||
raise HTTPException(status_code=404, detail="L34 frame index is invalid")
|
||||
return rows
|
||||
|
||||
|
||||
def _sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def _nonnegative_integer(value: object, label: str) -> int:
|
||||
if not isinstance(value, int) or isinstance(value, bool) or value < 0:
|
||||
raise L34RightYoloxTruthIslandError(f"L34 {label} is invalid")
|
||||
return value
|
||||
|
||||
|
||||
def _configured_root(provider: RootProvider) -> Path | None:
|
||||
value = provider()
|
||||
if value is None:
|
||||
return None
|
||||
candidate = value.expanduser().absolute()
|
||||
if candidate.is_symlink():
|
||||
return None
|
||||
try:
|
||||
resolved = candidate.resolve(strict=True)
|
||||
except OSError:
|
||||
return None
|
||||
return resolved if resolved.is_dir() else None
|
||||
|
||||
|
||||
def _candidates(provider: RootProvider) -> list[Path]:
|
||||
root = _configured_root(provider)
|
||||
if root is None:
|
||||
return []
|
||||
return sorted(
|
||||
(
|
||||
candidate
|
||||
for candidate in root.iterdir()
|
||||
if candidate.is_dir()
|
||||
and not candidate.is_symlink()
|
||||
and _RESULT_ID.fullmatch(candidate.name) is not None
|
||||
and (candidate / L34_MANIFEST_NAME).is_file()
|
||||
),
|
||||
key=lambda candidate: candidate.stat().st_mtime_ns,
|
||||
reverse=True,
|
||||
)
|
||||
@@ -0,0 +1,207 @@
|
||||
"""Read-only API for the L3.4A assisted YOLOX error audit."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import re
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from fastapi.responses import FileResponse
|
||||
|
||||
from k1link.compute.l34a_assisted_yolox_error_audit import (
|
||||
L34A_MANIFEST_NAME,
|
||||
L34AAssistedYoloxErrorAuditError,
|
||||
read_l34a_assisted_yolox_error_audit,
|
||||
)
|
||||
from k1link.web.l34_right_yolox_truth_island_api import (
|
||||
RootProvider,
|
||||
resolve_l34_frame_source,
|
||||
)
|
||||
|
||||
_RESULT_ID = re.compile(r"^l34a-assisted-yolox-error-audit-[a-f0-9]{64}$")
|
||||
AuditRootProvider = Callable[[], Path | None]
|
||||
|
||||
|
||||
def build_l34a_assisted_yolox_error_audit_router(
|
||||
*,
|
||||
audit_root_provider: AuditRootProvider = lambda: None,
|
||||
l34_root_provider: RootProvider = lambda: None,
|
||||
truth_root_provider: RootProvider = lambda: None,
|
||||
evaluation_pack_root_provider: RootProvider = lambda: None,
|
||||
) -> APIRouter:
|
||||
router = APIRouter(prefix="/api/v1/laboratory", tags=["laboratory"])
|
||||
|
||||
@router.get("/l34a/results")
|
||||
def list_l34a_results(
|
||||
limit: int = Query(default=1, ge=1, le=10),
|
||||
) -> dict[str, object]:
|
||||
items: list[dict[str, object]] = []
|
||||
invalid_total = 0
|
||||
candidates = _candidates(audit_root_provider)
|
||||
for candidate in candidates:
|
||||
try:
|
||||
result = read_l34a_assisted_yolox_error_audit(candidate)
|
||||
if len(items) < limit:
|
||||
items.append(_project(result))
|
||||
except (L34AAssistedYoloxErrorAuditError, OSError, ValueError):
|
||||
invalid_total += 1
|
||||
return {
|
||||
"schema_version": "missioncore.l34a-assisted-yolox-error-catalog/v1",
|
||||
"configured": _configured_root(audit_root_provider) is not None,
|
||||
"items": items,
|
||||
"candidate_total": len(candidates),
|
||||
"invalid_total": invalid_total,
|
||||
"access": "read-only",
|
||||
}
|
||||
|
||||
@router.get("/l34a/results/{result_id}/cases/{sequence}")
|
||||
def get_l34a_case(result_id: str, sequence: int) -> dict[str, object]:
|
||||
result, case, _, _ = _resolve_case(
|
||||
audit_root_provider=audit_root_provider,
|
||||
l34_root_provider=l34_root_provider,
|
||||
truth_root_provider=truth_root_provider,
|
||||
evaluation_pack_root_provider=evaluation_pack_root_provider,
|
||||
result_id=result_id,
|
||||
sequence=sequence,
|
||||
)
|
||||
return {
|
||||
**copy.deepcopy(case),
|
||||
"result_id": result["result_id"],
|
||||
"ground_truth": False,
|
||||
"access": "read-only",
|
||||
}
|
||||
|
||||
@router.get("/l34a/results/{result_id}/cases/{sequence}/camera")
|
||||
def get_l34a_case_camera(result_id: str, sequence: int) -> FileResponse:
|
||||
_, _, _, image_path = _resolve_case(
|
||||
audit_root_provider=audit_root_provider,
|
||||
l34_root_provider=l34_root_provider,
|
||||
truth_root_provider=truth_root_provider,
|
||||
evaluation_pack_root_provider=evaluation_pack_root_provider,
|
||||
result_id=result_id,
|
||||
sequence=sequence,
|
||||
)
|
||||
return FileResponse(image_path, media_type="image/png")
|
||||
|
||||
return router
|
||||
|
||||
|
||||
def _project(result: dict[str, Any]) -> dict[str, object]:
|
||||
report = result["report"]
|
||||
identity = result["manifest"]["identity"]
|
||||
annotation = identity["assisted_annotation"]
|
||||
return {
|
||||
"result_id": result["result_id"],
|
||||
"created_at_utc": report["created_at_utc"],
|
||||
"status": report["status"],
|
||||
"profile_id": report["profile"]["profile_id"],
|
||||
"pipeline_id": report["method"]["pipeline_id"],
|
||||
"source_session_id": report["source_session_id"],
|
||||
"camera_source_id": report["camera_source_id"],
|
||||
"assisted_annotation": copy.deepcopy(annotation),
|
||||
"metrics": copy.deepcopy(report["metrics"]),
|
||||
"cases": [_case_summary(case) for case in result["cases"]],
|
||||
"case_order": copy.deepcopy(report["case_order"]),
|
||||
"method": copy.deepcopy(report["method"]),
|
||||
"decision": copy.deepcopy(report["decision"]),
|
||||
"limitations": copy.deepcopy(report["limitations"]),
|
||||
"authority": copy.deepcopy(report["authority"]),
|
||||
"ground_truth": False,
|
||||
"access": "read-only",
|
||||
}
|
||||
|
||||
|
||||
def _case_summary(case: dict[str, Any]) -> dict[str, object]:
|
||||
return {
|
||||
"truth_island_sequence": case["truth_island_sequence"],
|
||||
"image_id": case["image_id"],
|
||||
"frame_index": case["frame_index"],
|
||||
"group_id": case["group_id"],
|
||||
"session_seconds": case["session_seconds"],
|
||||
"source_image_sha256": case["source_image_sha256"],
|
||||
"summary": copy.deepcopy(case["summary"]),
|
||||
}
|
||||
|
||||
|
||||
def _resolve_case(
|
||||
*,
|
||||
audit_root_provider: AuditRootProvider,
|
||||
l34_root_provider: RootProvider,
|
||||
truth_root_provider: RootProvider,
|
||||
evaluation_pack_root_provider: RootProvider,
|
||||
result_id: str,
|
||||
sequence: int,
|
||||
) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any], Path]:
|
||||
if _RESULT_ID.fullmatch(result_id) is None or not 1 <= sequence <= 32:
|
||||
raise HTTPException(status_code=404, detail="L3.4A case not found")
|
||||
root = _configured_root(audit_root_provider)
|
||||
if root is None:
|
||||
raise HTTPException(status_code=404, detail="L3.4A result not found")
|
||||
path = (root / result_id).resolve()
|
||||
if path.parent != root or path.is_symlink():
|
||||
raise HTTPException(status_code=404, detail="L3.4A result not found")
|
||||
try:
|
||||
result = read_l34a_assisted_yolox_error_audit(path)
|
||||
except (L34AAssistedYoloxErrorAuditError, OSError):
|
||||
raise HTTPException(status_code=404, detail="L3.4A result not found") from None
|
||||
case = next(
|
||||
(
|
||||
item
|
||||
for item in result["cases"]
|
||||
if item.get("truth_island_sequence") == sequence
|
||||
),
|
||||
None,
|
||||
)
|
||||
if case is None:
|
||||
raise HTTPException(status_code=404, detail="L3.4A case not found")
|
||||
l34_result_id = result["manifest"]["identity"]["l34_freeze"]["result_id"]
|
||||
_, prediction, reference, image_path = resolve_l34_frame_source(
|
||||
root_provider=l34_root_provider,
|
||||
truth_root_provider=truth_root_provider,
|
||||
evaluation_pack_root_provider=evaluation_pack_root_provider,
|
||||
result_id=l34_result_id,
|
||||
sequence=sequence,
|
||||
)
|
||||
if (
|
||||
prediction.get("source_image_sha256") != case.get("source_image_sha256")
|
||||
or reference.get("sha256") != case.get("source_image_sha256")
|
||||
or prediction.get("image_id") != case.get("image_id")
|
||||
or prediction.get("frame_index") != case.get("frame_index")
|
||||
):
|
||||
raise HTTPException(status_code=404, detail="L3.4A case binding changed")
|
||||
return result, case, prediction, image_path
|
||||
|
||||
|
||||
def _configured_root(provider: AuditRootProvider) -> Path | None:
|
||||
value = provider()
|
||||
if value is None:
|
||||
return None
|
||||
candidate = value.expanduser().absolute()
|
||||
if candidate.is_symlink():
|
||||
return None
|
||||
try:
|
||||
root = candidate.resolve(strict=True)
|
||||
except OSError:
|
||||
return None
|
||||
return root if root.is_dir() else None
|
||||
|
||||
|
||||
def _candidates(provider: AuditRootProvider) -> list[Path]:
|
||||
root = _configured_root(provider)
|
||||
if root is None:
|
||||
return []
|
||||
return sorted(
|
||||
(
|
||||
candidate
|
||||
for candidate in root.iterdir()
|
||||
if candidate.is_dir()
|
||||
and not candidate.is_symlink()
|
||||
and _RESULT_ID.fullmatch(candidate.name) is not None
|
||||
and (candidate / L34A_MANIFEST_NAME).is_file()
|
||||
),
|
||||
key=lambda candidate: candidate.stat().st_mtime_ns,
|
||||
reverse=True,
|
||||
)
|
||||
@@ -0,0 +1,202 @@
|
||||
"""Read-only API for the L3.4B nested-box consolidation shadow."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import re
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from fastapi.responses import FileResponse
|
||||
|
||||
from k1link.compute.l34b_nested_box_consolidation_shadow import (
|
||||
L34B_MANIFEST_NAME,
|
||||
L34BNestedBoxConsolidationError,
|
||||
read_l34b_nested_box_consolidation_shadow,
|
||||
)
|
||||
from k1link.web.l34_right_yolox_truth_island_api import (
|
||||
RootProvider,
|
||||
resolve_l34_frame_source,
|
||||
)
|
||||
|
||||
_RESULT_ID = re.compile(r"^l34b-nested-box-consolidation-shadow-[a-f0-9]{64}$")
|
||||
ShadowRootProvider = Callable[[], Path | None]
|
||||
|
||||
|
||||
def build_l34b_nested_box_consolidation_router(
|
||||
*,
|
||||
shadow_root_provider: ShadowRootProvider = lambda: None,
|
||||
l34_root_provider: RootProvider = lambda: None,
|
||||
truth_root_provider: RootProvider = lambda: None,
|
||||
evaluation_pack_root_provider: RootProvider = lambda: None,
|
||||
) -> APIRouter:
|
||||
router = APIRouter(prefix="/api/v1/laboratory", tags=["laboratory"])
|
||||
|
||||
@router.get("/l34b/results")
|
||||
def list_l34b_results(
|
||||
limit: int = Query(default=1, ge=1, le=10),
|
||||
) -> dict[str, object]:
|
||||
items: list[dict[str, object]] = []
|
||||
invalid_total = 0
|
||||
candidates = _candidates(shadow_root_provider)
|
||||
for candidate in candidates:
|
||||
try:
|
||||
result = read_l34b_nested_box_consolidation_shadow(candidate)
|
||||
if len(items) < limit:
|
||||
items.append(_project(result))
|
||||
except (L34BNestedBoxConsolidationError, OSError, ValueError):
|
||||
invalid_total += 1
|
||||
return {
|
||||
"schema_version": "missioncore.l34b-nested-box-consolidation-catalog/v1",
|
||||
"configured": _configured_root(shadow_root_provider) is not None,
|
||||
"items": items,
|
||||
"candidate_total": len(candidates),
|
||||
"invalid_total": invalid_total,
|
||||
"access": "read-only",
|
||||
}
|
||||
|
||||
@router.get("/l34b/results/{result_id}/cases/{sequence}")
|
||||
def get_l34b_case(result_id: str, sequence: int) -> dict[str, object]:
|
||||
result, case, _, _ = _resolve_case(
|
||||
shadow_root_provider=shadow_root_provider,
|
||||
l34_root_provider=l34_root_provider,
|
||||
truth_root_provider=truth_root_provider,
|
||||
evaluation_pack_root_provider=evaluation_pack_root_provider,
|
||||
result_id=result_id,
|
||||
sequence=sequence,
|
||||
)
|
||||
return {
|
||||
**copy.deepcopy(case),
|
||||
"result_id": result["result_id"],
|
||||
"ground_truth": False,
|
||||
"access": "read-only",
|
||||
}
|
||||
|
||||
@router.get("/l34b/results/{result_id}/cases/{sequence}/camera")
|
||||
def get_l34b_case_camera(result_id: str, sequence: int) -> FileResponse:
|
||||
_, _, _, image_path = _resolve_case(
|
||||
shadow_root_provider=shadow_root_provider,
|
||||
l34_root_provider=l34_root_provider,
|
||||
truth_root_provider=truth_root_provider,
|
||||
evaluation_pack_root_provider=evaluation_pack_root_provider,
|
||||
result_id=result_id,
|
||||
sequence=sequence,
|
||||
)
|
||||
return FileResponse(image_path, media_type="image/png")
|
||||
|
||||
return router
|
||||
|
||||
|
||||
def _project(result: dict[str, Any]) -> dict[str, object]:
|
||||
report = result["report"]
|
||||
return {
|
||||
"result_id": result["result_id"],
|
||||
"created_at_utc": report["created_at_utc"],
|
||||
"status": report["status"],
|
||||
"profile": copy.deepcopy(report["profile"]),
|
||||
"pipeline_id": report["method"]["pipeline_id"],
|
||||
"source_session_id": report["source_session_id"],
|
||||
"camera_source_id": report["camera_source_id"],
|
||||
"metrics": copy.deepcopy(report["metrics"]),
|
||||
"cases": [_case_summary(case) for case in result["cases"]],
|
||||
"case_order": copy.deepcopy(report["case_order"]),
|
||||
"method": copy.deepcopy(report["method"]),
|
||||
"decision": copy.deepcopy(report["decision"]),
|
||||
"limitations": copy.deepcopy(report["limitations"]),
|
||||
"authority": copy.deepcopy(report["authority"]),
|
||||
"ground_truth": False,
|
||||
"access": "read-only",
|
||||
}
|
||||
|
||||
|
||||
def _case_summary(case: dict[str, Any]) -> dict[str, object]:
|
||||
return {
|
||||
"truth_island_sequence": case["truth_island_sequence"],
|
||||
"image_id": case["image_id"],
|
||||
"frame_index": case["frame_index"],
|
||||
"group_id": case["group_id"],
|
||||
"session_seconds": case["session_seconds"],
|
||||
"source_image_sha256": case["source_image_sha256"],
|
||||
"before_summary": copy.deepcopy(case["before_summary"]),
|
||||
"after_summary": copy.deepcopy(case["after_summary"]),
|
||||
"consolidation_count": len(case["consolidations"]),
|
||||
}
|
||||
|
||||
|
||||
def _resolve_case(
|
||||
*,
|
||||
shadow_root_provider: ShadowRootProvider,
|
||||
l34_root_provider: RootProvider,
|
||||
truth_root_provider: RootProvider,
|
||||
evaluation_pack_root_provider: RootProvider,
|
||||
result_id: str,
|
||||
sequence: int,
|
||||
) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any], Path]:
|
||||
if _RESULT_ID.fullmatch(result_id) is None or not 1 <= sequence <= 32:
|
||||
raise HTTPException(status_code=404, detail="L3.4B case not found")
|
||||
root = _configured_root(shadow_root_provider)
|
||||
if root is None:
|
||||
raise HTTPException(status_code=404, detail="L3.4B result not found")
|
||||
path = (root / result_id).resolve()
|
||||
if path.parent != root or path.is_symlink():
|
||||
raise HTTPException(status_code=404, detail="L3.4B result not found")
|
||||
try:
|
||||
result = read_l34b_nested_box_consolidation_shadow(path)
|
||||
except (L34BNestedBoxConsolidationError, OSError):
|
||||
raise HTTPException(status_code=404, detail="L3.4B result not found") from None
|
||||
case = next(
|
||||
(item for item in result["cases"] if item.get("truth_island_sequence") == sequence),
|
||||
None,
|
||||
)
|
||||
if case is None:
|
||||
raise HTTPException(status_code=404, detail="L3.4B case not found")
|
||||
l34_result_id = result["manifest"]["identity"]["l34_freeze"]["result_id"]
|
||||
_, prediction, reference, image_path = resolve_l34_frame_source(
|
||||
root_provider=l34_root_provider,
|
||||
truth_root_provider=truth_root_provider,
|
||||
evaluation_pack_root_provider=evaluation_pack_root_provider,
|
||||
result_id=l34_result_id,
|
||||
sequence=sequence,
|
||||
)
|
||||
if (
|
||||
prediction.get("source_image_sha256") != case.get("source_image_sha256")
|
||||
or reference.get("sha256") != case.get("source_image_sha256")
|
||||
or prediction.get("image_id") != case.get("image_id")
|
||||
or prediction.get("frame_index") != case.get("frame_index")
|
||||
):
|
||||
raise HTTPException(status_code=404, detail="L3.4B case binding changed")
|
||||
return result, case, prediction, image_path
|
||||
|
||||
|
||||
def _configured_root(provider: ShadowRootProvider) -> Path | None:
|
||||
value = provider()
|
||||
if value is None:
|
||||
return None
|
||||
candidate = value.expanduser().absolute()
|
||||
if candidate.is_symlink():
|
||||
return None
|
||||
try:
|
||||
root = candidate.resolve(strict=True)
|
||||
except OSError:
|
||||
return None
|
||||
return root if root.is_dir() else None
|
||||
|
||||
|
||||
def _candidates(provider: ShadowRootProvider) -> list[Path]:
|
||||
root = _configured_root(provider)
|
||||
if root is None:
|
||||
return []
|
||||
return sorted(
|
||||
(
|
||||
candidate
|
||||
for candidate in root.iterdir()
|
||||
if candidate.is_dir()
|
||||
and not candidate.is_symlink()
|
||||
and _RESULT_ID.fullmatch(candidate.name) is not None
|
||||
and (candidate / L34B_MANIFEST_NAME).is_file()
|
||||
),
|
||||
key=lambda candidate: candidate.stat().st_mtime_ns,
|
||||
reverse=True,
|
||||
)
|
||||
@@ -0,0 +1,206 @@
|
||||
"""Read-only API for the L3.4C temporal tile-seam stitch shadow."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import re
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from fastapi.responses import FileResponse
|
||||
|
||||
from k1link.compute.l34c_tile_seam_stitch_shadow import (
|
||||
L34C_MANIFEST_NAME,
|
||||
L34CTileSeamStitchError,
|
||||
read_l34c_tile_seam_stitch_shadow,
|
||||
)
|
||||
from k1link.web.l34_right_yolox_truth_island_api import (
|
||||
RootProvider,
|
||||
resolve_l34_frame_source,
|
||||
)
|
||||
|
||||
_RESULT_ID = re.compile(r"^l34c-tile-seam-stitch-shadow-[a-f0-9]{64}$")
|
||||
ShadowRootProvider = Callable[[], Path | None]
|
||||
|
||||
|
||||
def build_l34c_tile_seam_stitch_router(
|
||||
*,
|
||||
shadow_root_provider: ShadowRootProvider = lambda: None,
|
||||
l34_root_provider: RootProvider = lambda: None,
|
||||
truth_root_provider: RootProvider = lambda: None,
|
||||
evaluation_pack_root_provider: RootProvider = lambda: None,
|
||||
) -> APIRouter:
|
||||
router = APIRouter(prefix="/api/v1/laboratory", tags=["laboratory"])
|
||||
|
||||
@router.get("/l34c/results")
|
||||
def list_l34c_results(
|
||||
limit: int = Query(default=1, ge=1, le=10),
|
||||
) -> dict[str, object]:
|
||||
items: list[dict[str, object]] = []
|
||||
invalid_total = 0
|
||||
candidates = _candidates(shadow_root_provider)
|
||||
for candidate in candidates:
|
||||
try:
|
||||
result = read_l34c_tile_seam_stitch_shadow(candidate)
|
||||
if len(items) < limit:
|
||||
items.append(_project(result))
|
||||
except (L34CTileSeamStitchError, OSError, ValueError):
|
||||
invalid_total += 1
|
||||
return {
|
||||
"schema_version": "missioncore.l34c-tile-seam-stitch-catalog/v1",
|
||||
"configured": _configured_root(shadow_root_provider) is not None,
|
||||
"items": items,
|
||||
"candidate_total": len(candidates),
|
||||
"invalid_total": invalid_total,
|
||||
"access": "read-only",
|
||||
}
|
||||
|
||||
@router.get("/l34c/results/{result_id}/cases/{sequence}")
|
||||
def get_l34c_case(result_id: str, sequence: int) -> dict[str, object]:
|
||||
result, case, _, _ = _resolve_case(
|
||||
shadow_root_provider=shadow_root_provider,
|
||||
l34_root_provider=l34_root_provider,
|
||||
truth_root_provider=truth_root_provider,
|
||||
evaluation_pack_root_provider=evaluation_pack_root_provider,
|
||||
result_id=result_id,
|
||||
sequence=sequence,
|
||||
)
|
||||
return {
|
||||
**copy.deepcopy(case),
|
||||
"result_id": result["result_id"],
|
||||
"ground_truth": False,
|
||||
"access": "read-only",
|
||||
}
|
||||
|
||||
@router.get("/l34c/results/{result_id}/cases/{sequence}/camera")
|
||||
def get_l34c_case_camera(result_id: str, sequence: int) -> FileResponse:
|
||||
_, _, _, image_path = _resolve_case(
|
||||
shadow_root_provider=shadow_root_provider,
|
||||
l34_root_provider=l34_root_provider,
|
||||
truth_root_provider=truth_root_provider,
|
||||
evaluation_pack_root_provider=evaluation_pack_root_provider,
|
||||
result_id=result_id,
|
||||
sequence=sequence,
|
||||
)
|
||||
return FileResponse(image_path, media_type="image/png")
|
||||
|
||||
return router
|
||||
|
||||
|
||||
def _project(result: dict[str, Any]) -> dict[str, object]:
|
||||
report = result["report"]
|
||||
return {
|
||||
"result_id": result["result_id"],
|
||||
"created_at_utc": report["created_at_utc"],
|
||||
"status": report["status"],
|
||||
"profile": copy.deepcopy(report["profile"]),
|
||||
"pipeline_id": report["method"]["pipeline_id"],
|
||||
"source_session_id": report["source_session_id"],
|
||||
"camera_source_id": report["camera_source_id"],
|
||||
"metrics": copy.deepcopy(report["metrics"]),
|
||||
"cases": [_case_summary(case) for case in result["cases"]],
|
||||
"case_order": copy.deepcopy(report["case_order"]),
|
||||
"method": copy.deepcopy(report["method"]),
|
||||
"decision": copy.deepcopy(report["decision"]),
|
||||
"limitations": copy.deepcopy(report["limitations"]),
|
||||
"authority": copy.deepcopy(report["authority"]),
|
||||
"ground_truth": False,
|
||||
"access": "read-only",
|
||||
}
|
||||
|
||||
|
||||
def _case_summary(case: dict[str, Any]) -> dict[str, object]:
|
||||
return {
|
||||
"truth_island_sequence": case["truth_island_sequence"],
|
||||
"image_id": case["image_id"],
|
||||
"frame_index": case["frame_index"],
|
||||
"group_id": case["group_id"],
|
||||
"session_seconds": case["session_seconds"],
|
||||
"source_image_sha256": case["source_image_sha256"],
|
||||
"before_summary": copy.deepcopy(case["before_summary"]),
|
||||
"after_summary": copy.deepcopy(case["after_summary"]),
|
||||
"stitch_count": len(case["stitches"]),
|
||||
}
|
||||
|
||||
|
||||
def _resolve_case(
|
||||
*,
|
||||
shadow_root_provider: ShadowRootProvider,
|
||||
l34_root_provider: RootProvider,
|
||||
truth_root_provider: RootProvider,
|
||||
evaluation_pack_root_provider: RootProvider,
|
||||
result_id: str,
|
||||
sequence: int,
|
||||
) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any], Path]:
|
||||
if _RESULT_ID.fullmatch(result_id) is None or not 1 <= sequence <= 32:
|
||||
raise HTTPException(status_code=404, detail="L3.4C case not found")
|
||||
root = _configured_root(shadow_root_provider)
|
||||
if root is None:
|
||||
raise HTTPException(status_code=404, detail="L3.4C result not found")
|
||||
path = (root / result_id).resolve()
|
||||
if path.parent != root or path.is_symlink():
|
||||
raise HTTPException(status_code=404, detail="L3.4C result not found")
|
||||
try:
|
||||
result = read_l34c_tile_seam_stitch_shadow(path)
|
||||
except (L34CTileSeamStitchError, OSError):
|
||||
raise HTTPException(status_code=404, detail="L3.4C result not found") from None
|
||||
case = next(
|
||||
(
|
||||
item
|
||||
for item in result["cases"]
|
||||
if item.get("truth_island_sequence") == sequence
|
||||
),
|
||||
None,
|
||||
)
|
||||
if case is None:
|
||||
raise HTTPException(status_code=404, detail="L3.4C case not found")
|
||||
l34_result_id = result["manifest"]["identity"]["l34_freeze"]["result_id"]
|
||||
_, prediction, reference, image_path = resolve_l34_frame_source(
|
||||
root_provider=l34_root_provider,
|
||||
truth_root_provider=truth_root_provider,
|
||||
evaluation_pack_root_provider=evaluation_pack_root_provider,
|
||||
result_id=l34_result_id,
|
||||
sequence=sequence,
|
||||
)
|
||||
if (
|
||||
prediction.get("source_image_sha256") != case.get("source_image_sha256")
|
||||
or reference.get("sha256") != case.get("source_image_sha256")
|
||||
or prediction.get("image_id") != case.get("image_id")
|
||||
or prediction.get("frame_index") != case.get("frame_index")
|
||||
):
|
||||
raise HTTPException(status_code=404, detail="L3.4C case binding changed")
|
||||
return result, case, prediction, image_path
|
||||
|
||||
|
||||
def _configured_root(provider: ShadowRootProvider) -> Path | None:
|
||||
value = provider()
|
||||
if value is None:
|
||||
return None
|
||||
candidate = value.expanduser().absolute()
|
||||
if candidate.is_symlink():
|
||||
return None
|
||||
try:
|
||||
root = candidate.resolve(strict=True)
|
||||
except OSError:
|
||||
return None
|
||||
return root if root.is_dir() else None
|
||||
|
||||
|
||||
def _candidates(provider: ShadowRootProvider) -> list[Path]:
|
||||
root = _configured_root(provider)
|
||||
if root is None:
|
||||
return []
|
||||
return sorted(
|
||||
(
|
||||
candidate
|
||||
for candidate in root.iterdir()
|
||||
if candidate.is_dir()
|
||||
and not candidate.is_symlink()
|
||||
and _RESULT_ID.fullmatch(candidate.name) is not None
|
||||
and (candidate / L34C_MANIFEST_NAME).is_file()
|
||||
),
|
||||
key=lambda candidate: candidate.stat().st_mtime_ns,
|
||||
reverse=True,
|
||||
)
|
||||
@@ -0,0 +1,207 @@
|
||||
"""Read-only API for the L3.4D cumulative post-processing candidate."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import re
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from fastapi.responses import FileResponse
|
||||
|
||||
from k1link.compute.l34d_cumulative_postprocessing_candidate import (
|
||||
L34D_MANIFEST_NAME,
|
||||
L34DCumulativeCandidateError,
|
||||
read_l34d_cumulative_postprocessing_candidate,
|
||||
)
|
||||
from k1link.web.l34_right_yolox_truth_island_api import (
|
||||
RootProvider,
|
||||
resolve_l34_frame_source,
|
||||
)
|
||||
|
||||
_RESULT_ID = re.compile(r"^l34d-cumulative-postprocessing-candidate-[a-f0-9]{64}$")
|
||||
CandidateRootProvider = Callable[[], Path | None]
|
||||
|
||||
|
||||
def build_l34d_cumulative_postprocessing_router(
|
||||
*,
|
||||
candidate_root_provider: CandidateRootProvider = lambda: None,
|
||||
l34_root_provider: RootProvider = lambda: None,
|
||||
truth_root_provider: RootProvider = lambda: None,
|
||||
evaluation_pack_root_provider: RootProvider = lambda: None,
|
||||
) -> APIRouter:
|
||||
router = APIRouter(prefix="/api/v1/laboratory", tags=["laboratory"])
|
||||
|
||||
@router.get("/l34d/results")
|
||||
def list_l34d_results(
|
||||
limit: int = Query(default=1, ge=1, le=10),
|
||||
) -> dict[str, object]:
|
||||
items: list[dict[str, object]] = []
|
||||
invalid_total = 0
|
||||
candidates = _candidates(candidate_root_provider)
|
||||
for candidate in candidates:
|
||||
try:
|
||||
result = read_l34d_cumulative_postprocessing_candidate(candidate)
|
||||
if len(items) < limit:
|
||||
items.append(_project(result))
|
||||
except (L34DCumulativeCandidateError, OSError, ValueError):
|
||||
invalid_total += 1
|
||||
return {
|
||||
"schema_version": ("missioncore.l34d-cumulative-postprocessing-catalog/v1"),
|
||||
"configured": _configured_root(candidate_root_provider) is not None,
|
||||
"items": items,
|
||||
"candidate_total": len(candidates),
|
||||
"invalid_total": invalid_total,
|
||||
"access": "read-only",
|
||||
}
|
||||
|
||||
@router.get("/l34d/results/{result_id}/cases/{sequence}")
|
||||
def get_l34d_case(result_id: str, sequence: int) -> dict[str, object]:
|
||||
result, case, _, _, _ = resolve_l34d_case_source(
|
||||
candidate_root_provider=candidate_root_provider,
|
||||
l34_root_provider=l34_root_provider,
|
||||
truth_root_provider=truth_root_provider,
|
||||
evaluation_pack_root_provider=evaluation_pack_root_provider,
|
||||
result_id=result_id,
|
||||
sequence=sequence,
|
||||
)
|
||||
return {
|
||||
**copy.deepcopy(case),
|
||||
"result_id": result["result_id"],
|
||||
"ground_truth": False,
|
||||
"access": "read-only",
|
||||
}
|
||||
|
||||
@router.get("/l34d/results/{result_id}/cases/{sequence}/camera")
|
||||
def get_l34d_case_camera(result_id: str, sequence: int) -> FileResponse:
|
||||
_, _, _, _, image_path = resolve_l34d_case_source(
|
||||
candidate_root_provider=candidate_root_provider,
|
||||
l34_root_provider=l34_root_provider,
|
||||
truth_root_provider=truth_root_provider,
|
||||
evaluation_pack_root_provider=evaluation_pack_root_provider,
|
||||
result_id=result_id,
|
||||
sequence=sequence,
|
||||
)
|
||||
return FileResponse(image_path, media_type="image/png")
|
||||
|
||||
return router
|
||||
|
||||
|
||||
def _project(result: dict[str, Any]) -> dict[str, object]:
|
||||
report = result["report"]
|
||||
return {
|
||||
"result_id": result["result_id"],
|
||||
"created_at_utc": report["created_at_utc"],
|
||||
"status": report["status"],
|
||||
"profile": copy.deepcopy(report["profile"]),
|
||||
"pipeline_id": report["method"]["pipeline_id"],
|
||||
"source_session_id": report["source_session_id"],
|
||||
"camera_source_id": report["camera_source_id"],
|
||||
"metrics": copy.deepcopy(report["metrics"]),
|
||||
"cases": [_case_summary(case) for case in result["cases"]],
|
||||
"case_order": copy.deepcopy(report["case_order"]),
|
||||
"method": copy.deepcopy(report["method"]),
|
||||
"decision": copy.deepcopy(report["decision"]),
|
||||
"limitations": copy.deepcopy(report["limitations"]),
|
||||
"authority": copy.deepcopy(report["authority"]),
|
||||
"ground_truth": False,
|
||||
"access": "read-only",
|
||||
}
|
||||
|
||||
|
||||
def _case_summary(case: dict[str, Any]) -> dict[str, object]:
|
||||
operations = case["operations"]
|
||||
return {
|
||||
"truth_island_sequence": case["truth_island_sequence"],
|
||||
"image_id": case["image_id"],
|
||||
"frame_index": case["frame_index"],
|
||||
"group_id": case["group_id"],
|
||||
"session_seconds": case["session_seconds"],
|
||||
"source_image_sha256": case["source_image_sha256"],
|
||||
"before_summary": copy.deepcopy(case["before_summary"]),
|
||||
"after_summary": copy.deepcopy(case["after_summary"]),
|
||||
"operation_count": len(operations),
|
||||
"operation_types": [item["operation_type"] for item in operations],
|
||||
}
|
||||
|
||||
|
||||
def resolve_l34d_case_source(
|
||||
*,
|
||||
candidate_root_provider: CandidateRootProvider,
|
||||
l34_root_provider: RootProvider,
|
||||
truth_root_provider: RootProvider,
|
||||
evaluation_pack_root_provider: RootProvider,
|
||||
result_id: str,
|
||||
sequence: int,
|
||||
) -> tuple[dict[str, Any], dict[str, Any], Any, dict[str, Any], Path]:
|
||||
if _RESULT_ID.fullmatch(result_id) is None or not 1 <= sequence <= 32:
|
||||
raise HTTPException(status_code=404, detail="L3.4D case not found")
|
||||
root = _configured_root(candidate_root_provider)
|
||||
if root is None:
|
||||
raise HTTPException(status_code=404, detail="L3.4D result not found")
|
||||
path = (root / result_id).resolve()
|
||||
if path.parent != root or path.is_symlink():
|
||||
raise HTTPException(status_code=404, detail="L3.4D result not found")
|
||||
try:
|
||||
result = read_l34d_cumulative_postprocessing_candidate(path)
|
||||
except (L34DCumulativeCandidateError, OSError):
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="L3.4D result not found",
|
||||
) from None
|
||||
case = next(
|
||||
(item for item in result["cases"] if item.get("truth_island_sequence") == sequence),
|
||||
None,
|
||||
)
|
||||
if case is None:
|
||||
raise HTTPException(status_code=404, detail="L3.4D case not found")
|
||||
l34_result_id = result["manifest"]["identity"]["l34_freeze"]["result_id"]
|
||||
l34_result, prediction, reference, image_path = resolve_l34_frame_source(
|
||||
root_provider=l34_root_provider,
|
||||
truth_root_provider=truth_root_provider,
|
||||
evaluation_pack_root_provider=evaluation_pack_root_provider,
|
||||
result_id=l34_result_id,
|
||||
sequence=sequence,
|
||||
)
|
||||
if (
|
||||
prediction.get("source_image_sha256") != case.get("source_image_sha256")
|
||||
or reference.get("sha256") != case.get("source_image_sha256")
|
||||
or prediction.get("image_id") != case.get("image_id")
|
||||
or prediction.get("frame_index") != case.get("frame_index")
|
||||
):
|
||||
raise HTTPException(status_code=404, detail="L3.4D case binding changed")
|
||||
return result, case, l34_result, reference, image_path
|
||||
|
||||
|
||||
def _configured_root(provider: CandidateRootProvider) -> Path | None:
|
||||
value = provider()
|
||||
if value is None:
|
||||
return None
|
||||
candidate = value.expanduser().absolute()
|
||||
if candidate.is_symlink():
|
||||
return None
|
||||
try:
|
||||
root = candidate.resolve(strict=True)
|
||||
except OSError:
|
||||
return None
|
||||
return root if root.is_dir() else None
|
||||
|
||||
|
||||
def _candidates(provider: CandidateRootProvider) -> list[Path]:
|
||||
root = _configured_root(provider)
|
||||
if root is None:
|
||||
return []
|
||||
return sorted(
|
||||
(
|
||||
candidate
|
||||
for candidate in root.iterdir()
|
||||
if candidate.is_dir()
|
||||
and not candidate.is_symlink()
|
||||
and _RESULT_ID.fullmatch(candidate.name) is not None
|
||||
and (candidate / L34D_MANIFEST_NAME).is_file()
|
||||
),
|
||||
key=lambda candidate: candidate.stat().st_mtime_ns,
|
||||
reverse=True,
|
||||
)
|
||||
@@ -0,0 +1,219 @@
|
||||
"""Read-only API for the L3.4E manual self-review diagnostic."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import re
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from fastapi.responses import FileResponse
|
||||
|
||||
from k1link.compute.l34e_self_review_diagnostic import (
|
||||
L34E_MANIFEST_NAME,
|
||||
L34ESelfReviewDiagnosticError,
|
||||
read_l34e_self_review_diagnostic,
|
||||
)
|
||||
from k1link.web.l34_right_yolox_truth_island_api import RootProvider
|
||||
from k1link.web.l34d_cumulative_postprocessing_api import (
|
||||
CandidateRootProvider,
|
||||
resolve_l34d_case_source,
|
||||
)
|
||||
|
||||
_RESULT_ID = re.compile(r"^l34e-self-review-diagnostic-[a-f0-9]{64}$")
|
||||
DiagnosticRootProvider = Callable[[], Path | None]
|
||||
|
||||
|
||||
def build_l34e_self_review_diagnostic_router(
|
||||
*,
|
||||
diagnostic_root_provider: DiagnosticRootProvider = lambda: None,
|
||||
candidate_root_provider: CandidateRootProvider = lambda: None,
|
||||
l34_root_provider: RootProvider = lambda: None,
|
||||
truth_root_provider: RootProvider = lambda: None,
|
||||
evaluation_pack_root_provider: RootProvider = lambda: None,
|
||||
) -> APIRouter:
|
||||
router = APIRouter(prefix="/api/v1/laboratory", tags=["laboratory"])
|
||||
|
||||
@router.get("/l34e/results")
|
||||
def list_l34e_results(
|
||||
limit: int = Query(default=1, ge=1, le=10),
|
||||
) -> dict[str, object]:
|
||||
items: list[dict[str, object]] = []
|
||||
invalid_total = 0
|
||||
candidates = _candidates(diagnostic_root_provider)
|
||||
for candidate in candidates:
|
||||
try:
|
||||
result = read_l34e_self_review_diagnostic(candidate)
|
||||
if len(items) < limit:
|
||||
items.append(_project(result))
|
||||
except (L34ESelfReviewDiagnosticError, OSError, ValueError):
|
||||
invalid_total += 1
|
||||
return {
|
||||
"schema_version": "missioncore.l34e-self-review-diagnostic-catalog/v1",
|
||||
"configured": _configured_root(diagnostic_root_provider) is not None,
|
||||
"items": items,
|
||||
"candidate_total": len(candidates),
|
||||
"invalid_total": invalid_total,
|
||||
"access": "read-only",
|
||||
}
|
||||
|
||||
@router.get("/l34e/results/{result_id}/cases/{sequence}")
|
||||
def get_l34e_case(result_id: str, sequence: int) -> dict[str, object]:
|
||||
result, case, _ = resolve_l34e_case_source(
|
||||
diagnostic_root_provider=diagnostic_root_provider,
|
||||
candidate_root_provider=candidate_root_provider,
|
||||
l34_root_provider=l34_root_provider,
|
||||
truth_root_provider=truth_root_provider,
|
||||
evaluation_pack_root_provider=evaluation_pack_root_provider,
|
||||
result_id=result_id,
|
||||
sequence=sequence,
|
||||
)
|
||||
return {
|
||||
**copy.deepcopy(case),
|
||||
"result_id": result["result_id"],
|
||||
"ground_truth": False,
|
||||
"access": "read-only",
|
||||
}
|
||||
|
||||
@router.get("/l34e/results/{result_id}/cases/{sequence}/camera")
|
||||
def get_l34e_case_camera(result_id: str, sequence: int) -> FileResponse:
|
||||
_, _, image_path = resolve_l34e_case_source(
|
||||
diagnostic_root_provider=diagnostic_root_provider,
|
||||
candidate_root_provider=candidate_root_provider,
|
||||
l34_root_provider=l34_root_provider,
|
||||
truth_root_provider=truth_root_provider,
|
||||
evaluation_pack_root_provider=evaluation_pack_root_provider,
|
||||
result_id=result_id,
|
||||
sequence=sequence,
|
||||
)
|
||||
return FileResponse(image_path, media_type="image/png")
|
||||
|
||||
return router
|
||||
|
||||
|
||||
def resolve_l34e_case_source(
|
||||
*,
|
||||
diagnostic_root_provider: DiagnosticRootProvider,
|
||||
candidate_root_provider: CandidateRootProvider,
|
||||
l34_root_provider: RootProvider,
|
||||
truth_root_provider: RootProvider,
|
||||
evaluation_pack_root_provider: RootProvider,
|
||||
result_id: str,
|
||||
sequence: int,
|
||||
) -> tuple[dict[str, Any], dict[str, Any], Path]:
|
||||
if _RESULT_ID.fullmatch(result_id) is None or not 1 <= sequence <= 32:
|
||||
raise HTTPException(status_code=404, detail="L3.4E case not found")
|
||||
root = _configured_root(diagnostic_root_provider)
|
||||
if root is None:
|
||||
raise HTTPException(status_code=404, detail="L3.4E result not found")
|
||||
path = (root / result_id).resolve()
|
||||
if path.parent != root or path.is_symlink():
|
||||
raise HTTPException(status_code=404, detail="L3.4E result not found")
|
||||
try:
|
||||
result = read_l34e_self_review_diagnostic(path)
|
||||
except (L34ESelfReviewDiagnosticError, OSError):
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="L3.4E result not found",
|
||||
) from None
|
||||
case = next(
|
||||
(
|
||||
item
|
||||
for item in result["cases"]
|
||||
if item.get("truth_island_sequence") == sequence
|
||||
),
|
||||
None,
|
||||
)
|
||||
if case is None:
|
||||
raise HTTPException(status_code=404, detail="L3.4E case not found")
|
||||
l34d_result_id = result["manifest"]["identity"]["l34d_candidate"][
|
||||
"result_id"
|
||||
]
|
||||
_, l34d_case, _, _, image_path = resolve_l34d_case_source(
|
||||
candidate_root_provider=candidate_root_provider,
|
||||
l34_root_provider=l34_root_provider,
|
||||
truth_root_provider=truth_root_provider,
|
||||
evaluation_pack_root_provider=evaluation_pack_root_provider,
|
||||
result_id=l34d_result_id,
|
||||
sequence=sequence,
|
||||
)
|
||||
if any(
|
||||
case.get(key) != l34d_case.get(key)
|
||||
for key in (
|
||||
"truth_island_sequence",
|
||||
"image_id",
|
||||
"frame_index",
|
||||
"source_image_sha256",
|
||||
)
|
||||
):
|
||||
raise HTTPException(status_code=404, detail="L3.4E case binding changed")
|
||||
return result, case, image_path
|
||||
|
||||
|
||||
def _project(result: dict[str, Any]) -> dict[str, object]:
|
||||
report = result["report"]
|
||||
return {
|
||||
"result_id": result["result_id"],
|
||||
"created_at_utc": report["created_at_utc"],
|
||||
"status": report["status"],
|
||||
"profile": copy.deepcopy(report["profile"]),
|
||||
"pipeline_id": report["method"]["pipeline_id"],
|
||||
"source_session_id": report["source_session_id"],
|
||||
"camera_source_id": report["camera_source_id"],
|
||||
"metrics": copy.deepcopy(report["metrics"]),
|
||||
"cases": [_case_summary(case) for case in result["cases"]],
|
||||
"case_order": copy.deepcopy(report["case_order"]),
|
||||
"method": copy.deepcopy(report["method"]),
|
||||
"decision": copy.deepcopy(report["decision"]),
|
||||
"limitations": copy.deepcopy(report["limitations"]),
|
||||
"authority": copy.deepcopy(report["authority"]),
|
||||
"ground_truth": False,
|
||||
"access": "read-only",
|
||||
}
|
||||
|
||||
|
||||
def _case_summary(case: dict[str, Any]) -> dict[str, object]:
|
||||
return {
|
||||
"truth_island_sequence": case["truth_island_sequence"],
|
||||
"image_id": case["image_id"],
|
||||
"frame_index": case["frame_index"],
|
||||
"group_id": case["group_id"],
|
||||
"session_seconds": case["session_seconds"],
|
||||
"source_image_sha256": case["source_image_sha256"],
|
||||
"strict_summary": copy.deepcopy(case["strict_summary"]),
|
||||
"diagnostic_summary": copy.deepcopy(case["diagnostic_summary"]),
|
||||
}
|
||||
|
||||
|
||||
def _configured_root(provider: DiagnosticRootProvider) -> Path | None:
|
||||
value = provider()
|
||||
if value is None:
|
||||
return None
|
||||
candidate = value.expanduser().absolute()
|
||||
if candidate.is_symlink():
|
||||
return None
|
||||
try:
|
||||
root = candidate.resolve(strict=True)
|
||||
except OSError:
|
||||
return None
|
||||
return root if root.is_dir() else None
|
||||
|
||||
|
||||
def _candidates(provider: DiagnosticRootProvider) -> list[Path]:
|
||||
root = _configured_root(provider)
|
||||
if root is None:
|
||||
return []
|
||||
return sorted(
|
||||
(
|
||||
candidate
|
||||
for candidate in root.iterdir()
|
||||
if candidate.is_dir()
|
||||
and not candidate.is_symlink()
|
||||
and _RESULT_ID.fullmatch(candidate.name) is not None
|
||||
and (candidate / L34E_MANIFEST_NAME).is_file()
|
||||
),
|
||||
key=lambda candidate: candidate.stat().st_mtime_ns,
|
||||
reverse=True,
|
||||
)
|
||||
@@ -0,0 +1,601 @@
|
||||
"""Revisioned L3.4F disagreement adjudication and immutable freeze API."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import re
|
||||
import threading
|
||||
from collections.abc import Callable
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Annotated, Any, Final, Literal
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from fastapi import Path as ApiPath
|
||||
from fastapi.responses import FileResponse
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from k1link.artifacts import write_json_atomic
|
||||
from k1link.compute.l34e_self_review_diagnostic import (
|
||||
L34ESelfReviewDiagnosticError,
|
||||
read_l34e_self_review_diagnostic,
|
||||
)
|
||||
from k1link.compute.l34f_adjudicated_reference import (
|
||||
L34F_MANIFEST_NAME,
|
||||
L34F_SESSION_SCHEMA,
|
||||
L34FAdjudicatedReferenceError,
|
||||
build_l34f_adjudicated_reference,
|
||||
read_l34f_adjudicated_reference,
|
||||
)
|
||||
from k1link.web.l34_right_yolox_truth_island_api import RootProvider
|
||||
from k1link.web.l34d_cumulative_postprocessing_api import CandidateRootProvider
|
||||
from k1link.web.l34e_self_review_diagnostic_api import (
|
||||
DiagnosticRootProvider,
|
||||
resolve_l34e_case_source,
|
||||
)
|
||||
|
||||
_DIAGNOSTIC_ID = re.compile(r"^l34e-self-review-diagnostic-[a-f0-9]{64}$")
|
||||
_SESSION_ID = re.compile(r"^l34f-adjudication-session-[a-f0-9]{64}$")
|
||||
_RESULT_ID = re.compile(r"^l34f-adjudicated-reference-[a-f0-9]{64}$")
|
||||
_OBJECT_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,95}$")
|
||||
_OPERATION_KEY = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$")
|
||||
_TARGET_CLASSES: Final = (
|
||||
"person",
|
||||
"bicycle",
|
||||
"motorcycle",
|
||||
"car",
|
||||
"heavy_vehicle",
|
||||
"static_obstacle",
|
||||
"animal",
|
||||
)
|
||||
_AUTHORITY: Final = {
|
||||
"ground_truth": False,
|
||||
"independent_truth": False,
|
||||
"metric_grade_reference": False,
|
||||
"candidate_accepted": False,
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
}
|
||||
_BLINDNESS: Final = {
|
||||
"candidate_identity_seen": True,
|
||||
"model_prelabels_seen": False,
|
||||
"model_predictions_seen": True,
|
||||
"model_scores_seen": False,
|
||||
"source_self_review_seen": True,
|
||||
}
|
||||
_ASSISTANCE: Final = {
|
||||
"mode": "candidate-visible-human-adjudication",
|
||||
"independent_truth_eligible": False,
|
||||
"metric_grade_reference": False,
|
||||
}
|
||||
|
||||
AdjudicationRootProvider = Callable[[], Path | None]
|
||||
ResultRootProvider = Callable[[], Path | None]
|
||||
|
||||
|
||||
class L34FCreateRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
idempotency_key: str = Field(min_length=1, max_length=128)
|
||||
|
||||
|
||||
class L34FObjectRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
object_id: str = Field(min_length=1, max_length=96)
|
||||
category: Literal[
|
||||
"person",
|
||||
"bicycle",
|
||||
"motorcycle",
|
||||
"car",
|
||||
"heavy_vehicle",
|
||||
"static_obstacle",
|
||||
"animal",
|
||||
"unmapped",
|
||||
]
|
||||
proposed_label: str | None = Field(default=None, max_length=80)
|
||||
origin: Literal["self_review_seed", "adjudicated_manual"]
|
||||
box_xyxy: list[float] = Field(min_length=4, max_length=4)
|
||||
occluded: bool
|
||||
truncated: bool
|
||||
|
||||
|
||||
class L34FFrameRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
truth_island_sequence: int = Field(ge=1, le=32)
|
||||
reviewed: bool
|
||||
objects: list[L34FObjectRequest] = Field(max_length=256)
|
||||
|
||||
|
||||
class L34FSaveRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
expected_revision: int = Field(ge=0)
|
||||
idempotency_key: str = Field(min_length=1, max_length=128)
|
||||
title: str = Field(min_length=1, max_length=160)
|
||||
frames: list[L34FFrameRequest] = Field(min_length=32, max_length=32)
|
||||
|
||||
|
||||
class L34FFreezeRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
expected_revision: int = Field(ge=1)
|
||||
|
||||
|
||||
class _Store:
|
||||
def __init__(self, provider: AdjudicationRootProvider) -> None:
|
||||
self.provider = provider
|
||||
self.lock = threading.Lock()
|
||||
|
||||
def list(self, diagnostic_id: str) -> list[dict[str, object]]:
|
||||
root = self._diagnostic_root(diagnostic_id, create=False)
|
||||
if root is None:
|
||||
return []
|
||||
items: list[dict[str, object]] = []
|
||||
for path in sorted(root.glob("l34f-adjudication-session-*.json")):
|
||||
if path.is_symlink() or _SESSION_ID.fullmatch(path.stem) is None:
|
||||
continue
|
||||
try:
|
||||
items.append(_project_session(self._read(path, diagnostic_id), False))
|
||||
except (OSError, ValueError):
|
||||
continue
|
||||
items.sort(key=lambda item: str(item["created_at_utc"]))
|
||||
return items
|
||||
|
||||
def create(self, diagnostic: dict[str, Any], key: str) -> dict[str, object]:
|
||||
diagnostic_id = diagnostic["result_id"]
|
||||
_validate_operation_key(key)
|
||||
digest = hashlib.sha256(f"{diagnostic_id}\n{key}".encode()).hexdigest()
|
||||
session_id = f"l34f-adjudication-session-{digest}"
|
||||
with self.lock:
|
||||
path = self._session_path(diagnostic_id, session_id, create=True)
|
||||
assert path is not None
|
||||
if path.exists():
|
||||
return _project_session(self._read(path, diagnostic_id), True)
|
||||
now = _utc_now()
|
||||
frames = []
|
||||
for case in sorted(diagnostic["cases"], key=lambda row: row["truth_island_sequence"]):
|
||||
frames.append(
|
||||
{
|
||||
"truth_island_sequence": int(case["truth_island_sequence"]),
|
||||
"image_id": int(case["image_id"]),
|
||||
"frame_index": int(case["frame_index"]),
|
||||
"source_sha256": str(case["source_image_sha256"]),
|
||||
"reviewed": False,
|
||||
"objects": [
|
||||
{
|
||||
"object_id": str(item["object_id"]),
|
||||
"category": str(item["category"]),
|
||||
"proposed_label": item.get("proposed_label"),
|
||||
"origin": "self_review_seed",
|
||||
"box_xyxy": [float(value) for value in item["box_xyxy"]],
|
||||
"occluded": bool(item["occluded"]),
|
||||
"truncated": bool(item["truncated"]),
|
||||
}
|
||||
for item in case["references"]
|
||||
],
|
||||
}
|
||||
)
|
||||
reviewer_slot = len(self.list(diagnostic_id)) + 1
|
||||
document: dict[str, object] = {
|
||||
"schema_version": L34F_SESSION_SCHEMA,
|
||||
"session_id": session_id,
|
||||
"diagnostic_result_id": diagnostic_id,
|
||||
"source_session_id": str(diagnostic["report"]["source_session_id"]),
|
||||
"source_review_session_id": str(
|
||||
diagnostic["manifest"]["identity"]["self_review"]["session_id"]
|
||||
),
|
||||
"contract_id": "l34e-candidate-visible-adjudication/v1",
|
||||
"title": f"Разбор конфликтов · LAB L3.4F · Рецензент {reviewer_slot}",
|
||||
"reviewer_slot": reviewer_slot,
|
||||
"revision": 0,
|
||||
"state": "draft",
|
||||
"created_at_utc": now,
|
||||
"updated_at_utc": now,
|
||||
"frames": frames,
|
||||
"blindness": dict(_BLINDNESS),
|
||||
"assistance": dict(_ASSISTANCE),
|
||||
"authority": dict(_AUTHORITY),
|
||||
"last_save_idempotency_key": None,
|
||||
}
|
||||
write_json_atomic(path, document)
|
||||
return _project_session(document, True)
|
||||
|
||||
def get(self, diagnostic_id: str, session_id: str) -> dict[str, object]:
|
||||
path = self._session_path(diagnostic_id, session_id, create=False)
|
||||
if path is None or not path.is_file() or path.is_symlink():
|
||||
raise HTTPException(status_code=404, detail="Сессия adjudication не найдена")
|
||||
try:
|
||||
return _project_session(self._read(path, diagnostic_id), True)
|
||||
except (OSError, ValueError) as exc:
|
||||
raise HTTPException(status_code=409, detail="Сессия adjudication повреждена") from exc
|
||||
|
||||
def save(
|
||||
self,
|
||||
diagnostic: dict[str, Any],
|
||||
session_id: str,
|
||||
request: L34FSaveRequest,
|
||||
) -> dict[str, object]:
|
||||
diagnostic_id = diagnostic["result_id"]
|
||||
_validate_operation_key(request.idempotency_key)
|
||||
path = self._session_path(diagnostic_id, session_id, create=False)
|
||||
if path is None or not path.is_file() or path.is_symlink():
|
||||
raise HTTPException(status_code=404, detail="Сессия adjudication не найдена")
|
||||
source_cases = {int(case["truth_island_sequence"]): case for case in diagnostic["cases"]}
|
||||
seen: set[int] = set()
|
||||
frames: list[dict[str, object]] = []
|
||||
for frame in request.frames:
|
||||
if frame.truth_island_sequence in seen:
|
||||
raise HTTPException(status_code=422, detail="Кадр передан дважды")
|
||||
seen.add(frame.truth_island_sequence)
|
||||
source = source_cases[frame.truth_island_sequence]
|
||||
frames.append(
|
||||
{
|
||||
"truth_island_sequence": frame.truth_island_sequence,
|
||||
"image_id": int(source["image_id"]),
|
||||
"frame_index": int(source["frame_index"]),
|
||||
"source_sha256": str(source["source_image_sha256"]),
|
||||
"reviewed": frame.reviewed,
|
||||
"objects": [_normalize_object(item) for item in frame.objects],
|
||||
}
|
||||
)
|
||||
if seen != set(range(1, 33)):
|
||||
raise HTTPException(status_code=422, detail="Требуются все 32 кадра")
|
||||
frames.sort(key=lambda item: int(item["truth_island_sequence"]))
|
||||
with self.lock:
|
||||
current = self._read(path, diagnostic_id)
|
||||
if current.get("last_save_idempotency_key") == request.idempotency_key:
|
||||
return _project_session(current, True)
|
||||
if current["revision"] != request.expected_revision:
|
||||
raise HTTPException(status_code=409, detail="Сессия уже изменена в другом окне")
|
||||
current.update(
|
||||
{
|
||||
"title": request.title.strip(),
|
||||
"revision": request.expected_revision + 1,
|
||||
"state": "saved",
|
||||
"updated_at_utc": _utc_now(),
|
||||
"frames": frames,
|
||||
"last_save_idempotency_key": request.idempotency_key,
|
||||
}
|
||||
)
|
||||
write_json_atomic(path, current)
|
||||
return _project_session(current, True)
|
||||
|
||||
def session_path(self, diagnostic_id: str, session_id: str) -> Path:
|
||||
path = self._session_path(diagnostic_id, session_id, create=False)
|
||||
if path is None or not path.is_file() or path.is_symlink():
|
||||
raise HTTPException(status_code=404, detail="Сессия adjudication не найдена")
|
||||
return path
|
||||
|
||||
def _diagnostic_root(self, diagnostic_id: str, *, create: bool) -> Path | None:
|
||||
if _DIAGNOSTIC_ID.fullmatch(diagnostic_id) is None:
|
||||
raise HTTPException(status_code=404, detail="L3.4E result не найден")
|
||||
value = self.provider()
|
||||
if value is None:
|
||||
raise HTTPException(status_code=503, detail="Хранилище adjudication не настроено")
|
||||
root = value.expanduser().absolute()
|
||||
if root.is_symlink():
|
||||
raise HTTPException(status_code=503, detail="Хранилище adjudication недоступно")
|
||||
if create:
|
||||
root.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
if not root.is_dir():
|
||||
return None
|
||||
root = root.resolve(strict=True)
|
||||
child = root / diagnostic_id
|
||||
if create:
|
||||
child.mkdir(mode=0o700, exist_ok=True)
|
||||
if not child.is_dir() or child.is_symlink():
|
||||
return None
|
||||
resolved = child.resolve(strict=True)
|
||||
if resolved.parent != root:
|
||||
raise HTTPException(status_code=503, detail="Хранилище adjudication недоступно")
|
||||
return resolved
|
||||
|
||||
def _session_path(self, diagnostic_id: str, session_id: str, *, create: bool) -> Path | None:
|
||||
if _SESSION_ID.fullmatch(session_id) is None:
|
||||
raise HTTPException(status_code=404, detail="Сессия adjudication не найдена")
|
||||
root = self._diagnostic_root(diagnostic_id, create=create)
|
||||
return None if root is None else root / f"{session_id}.json"
|
||||
|
||||
def _read(self, path: Path, diagnostic_id: str) -> dict[str, Any]:
|
||||
value = json.loads(path.read_text(encoding="utf-8"))
|
||||
if (
|
||||
not isinstance(value, dict)
|
||||
or value.get("schema_version") != L34F_SESSION_SCHEMA
|
||||
or value.get("session_id") != path.stem
|
||||
or value.get("diagnostic_result_id") != diagnostic_id
|
||||
or value.get("contract_id") != "l34e-candidate-visible-adjudication/v1"
|
||||
or value.get("blindness") != _BLINDNESS
|
||||
or value.get("assistance") != _ASSISTANCE
|
||||
or value.get("authority") != _AUTHORITY
|
||||
or not isinstance(value.get("revision"), int)
|
||||
or not isinstance(value.get("frames"), list)
|
||||
or len(value["frames"]) != 32
|
||||
):
|
||||
raise ValueError("invalid L3.4F session")
|
||||
return value
|
||||
|
||||
|
||||
def build_l34f_adjudication_router(
|
||||
*,
|
||||
diagnostic_root_provider: DiagnosticRootProvider = lambda: None,
|
||||
candidate_root_provider: CandidateRootProvider = lambda: None,
|
||||
l34_root_provider: RootProvider = lambda: None,
|
||||
truth_root_provider: RootProvider = lambda: None,
|
||||
evaluation_pack_root_provider: RootProvider = lambda: None,
|
||||
adjudication_root_provider: AdjudicationRootProvider = lambda: None,
|
||||
result_root_provider: ResultRootProvider = lambda: None,
|
||||
) -> APIRouter:
|
||||
router = APIRouter(prefix="/api/v1/laboratory/l34f", tags=["laboratory"])
|
||||
store = _Store(adjudication_root_provider)
|
||||
|
||||
def diagnostic(result_id: str) -> dict[str, Any]:
|
||||
if _DIAGNOSTIC_ID.fullmatch(result_id) is None:
|
||||
raise HTTPException(status_code=404, detail="L3.4E result не найден")
|
||||
root = _configured_root(diagnostic_root_provider)
|
||||
if root is None:
|
||||
raise HTTPException(status_code=404, detail="L3.4E result не найден")
|
||||
path = (root / result_id).resolve()
|
||||
if path.parent != root or path.is_symlink():
|
||||
raise HTTPException(status_code=404, detail="L3.4E result не найден")
|
||||
try:
|
||||
return read_l34e_self_review_diagnostic(path)
|
||||
except (L34ESelfReviewDiagnosticError, OSError):
|
||||
raise HTTPException(status_code=404, detail="L3.4E result не найден") from None
|
||||
|
||||
def case_source(result_id: str, sequence: int) -> tuple[dict[str, Any], dict[str, Any], Path]:
|
||||
return resolve_l34e_case_source(
|
||||
diagnostic_root_provider=diagnostic_root_provider,
|
||||
candidate_root_provider=candidate_root_provider,
|
||||
l34_root_provider=l34_root_provider,
|
||||
truth_root_provider=truth_root_provider,
|
||||
evaluation_pack_root_provider=evaluation_pack_root_provider,
|
||||
result_id=result_id,
|
||||
sequence=sequence,
|
||||
)
|
||||
|
||||
@router.get("/diagnostics/{diagnostic_id}/cases/{sequence}")
|
||||
def get_case(diagnostic_id: str, sequence: int) -> dict[str, object]:
|
||||
result, case, image_path = case_source(diagnostic_id, sequence)
|
||||
width, height = _png_dimensions(image_path)
|
||||
return {
|
||||
"schema_version": "missioncore.l34f-adjudication-case/v1",
|
||||
"diagnostic_result_id": result["result_id"],
|
||||
"truth_island_sequence": sequence,
|
||||
"image_id": case["image_id"],
|
||||
"frame_index": case["frame_index"],
|
||||
"group_id": case["group_id"],
|
||||
"session_seconds": case["session_seconds"],
|
||||
"source_image_sha256": case["source_image_sha256"],
|
||||
"camera": {"width": width, "height": height},
|
||||
"camera_url": (
|
||||
f"/api/v1/laboratory/l34f/diagnostics/{diagnostic_id}/cases/{sequence}/camera"
|
||||
),
|
||||
"candidate_objects": [
|
||||
{
|
||||
"candidate_id": f"candidate-{sequence}-{item['prediction_index']}",
|
||||
"prediction_index": item["prediction_index"],
|
||||
"category": item["category"],
|
||||
"box_xyxy": copy.deepcopy(item["box_xyxy"]),
|
||||
"diagnostic_verdict": item["diagnostic_verdict"],
|
||||
"associated_object_id": item["associated_object_id"],
|
||||
}
|
||||
for item in case["predictions"]
|
||||
],
|
||||
"diagnostic_summary": copy.deepcopy(case["diagnostic_summary"]),
|
||||
"model_scores_included": False,
|
||||
"ground_truth": False,
|
||||
"access": "candidate-visible-adjudication-read-only-source",
|
||||
}
|
||||
|
||||
@router.get("/diagnostics/{diagnostic_id}/cases/{sequence}/camera")
|
||||
def get_camera(diagnostic_id: str, sequence: int) -> FileResponse:
|
||||
_, _, image_path = case_source(diagnostic_id, sequence)
|
||||
return FileResponse(image_path, media_type="image/png")
|
||||
|
||||
@router.get("/diagnostics/{diagnostic_id}/sessions")
|
||||
def list_sessions(diagnostic_id: str) -> dict[str, object]:
|
||||
diagnostic(diagnostic_id)
|
||||
items = store.list(diagnostic_id)
|
||||
return {
|
||||
"schema_version": "missioncore.l34f-adjudication-session-catalog/v1",
|
||||
"diagnostic_result_id": diagnostic_id,
|
||||
"items": items,
|
||||
"total": len(items),
|
||||
"access": "human-adjudication",
|
||||
}
|
||||
|
||||
@router.post("/diagnostics/{diagnostic_id}/sessions")
|
||||
def create_session(diagnostic_id: str, request: L34FCreateRequest) -> dict[str, object]:
|
||||
return store.create(diagnostic(diagnostic_id), request.idempotency_key)
|
||||
|
||||
@router.get("/diagnostics/{diagnostic_id}/sessions/{session_id}")
|
||||
def get_session(
|
||||
diagnostic_id: str, session_id: Annotated[str, ApiPath(pattern=_SESSION_ID.pattern)]
|
||||
) -> dict[str, object]:
|
||||
diagnostic(diagnostic_id)
|
||||
return store.get(diagnostic_id, session_id)
|
||||
|
||||
@router.put("/diagnostics/{diagnostic_id}/sessions/{session_id}")
|
||||
def save_session(
|
||||
diagnostic_id: str,
|
||||
session_id: Annotated[str, ApiPath(pattern=_SESSION_ID.pattern)],
|
||||
request: L34FSaveRequest,
|
||||
) -> dict[str, object]:
|
||||
return store.save(diagnostic(diagnostic_id), session_id, request)
|
||||
|
||||
@router.post("/diagnostics/{diagnostic_id}/sessions/{session_id}/freeze")
|
||||
def freeze_session(
|
||||
diagnostic_id: str,
|
||||
session_id: Annotated[str, ApiPath(pattern=_SESSION_ID.pattern)],
|
||||
request: L34FFreezeRequest,
|
||||
) -> dict[str, object]:
|
||||
source = diagnostic(diagnostic_id)
|
||||
session = store.get(diagnostic_id, session_id)
|
||||
if session["revision"] != request.expected_revision:
|
||||
raise HTTPException(status_code=409, detail="Сессия уже изменена")
|
||||
if not session["progress"]["complete"]:
|
||||
raise HTTPException(status_code=422, detail="Для freeze нужны 32/32 проверенных кадров")
|
||||
output = result_root_provider()
|
||||
if output is None:
|
||||
raise HTTPException(status_code=503, detail="Хранилище L3.4F не настроено")
|
||||
try:
|
||||
frozen = build_l34f_adjudicated_reference(
|
||||
diagnostic_root=source["result_root"],
|
||||
adjudication_session_path=store.session_path(diagnostic_id, session_id),
|
||||
output_root=output,
|
||||
)
|
||||
except (L34FAdjudicatedReferenceError, OSError) as exc:
|
||||
raise HTTPException(status_code=409, detail="L3.4F freeze не прошёл проверку") from exc
|
||||
return _project_result(frozen)
|
||||
|
||||
@router.get("/results")
|
||||
def list_results(limit: int = Query(default=1, ge=1, le=10)) -> dict[str, object]:
|
||||
candidates = _result_candidates(result_root_provider)
|
||||
items: list[dict[str, object]] = []
|
||||
invalid_total = 0
|
||||
for path in candidates:
|
||||
try:
|
||||
result = read_l34f_adjudicated_reference(path)
|
||||
if len(items) < limit:
|
||||
items.append(_project_result(result))
|
||||
except (L34FAdjudicatedReferenceError, OSError):
|
||||
invalid_total += 1
|
||||
return {
|
||||
"schema_version": "missioncore.l34f-adjudicated-reference-catalog/v1",
|
||||
"configured": _configured_result_root(result_root_provider) is not None,
|
||||
"items": items,
|
||||
"candidate_total": len(candidates),
|
||||
"invalid_total": invalid_total,
|
||||
"access": "read-only",
|
||||
}
|
||||
|
||||
return router
|
||||
|
||||
|
||||
def _normalize_object(item: L34FObjectRequest) -> dict[str, object]:
|
||||
if _OBJECT_ID.fullmatch(item.object_id) is None:
|
||||
raise HTTPException(status_code=422, detail="Некорректный id рамки")
|
||||
if any(not math.isfinite(float(value)) for value in item.box_xyxy):
|
||||
raise HTTPException(status_code=422, detail="Некорректная геометрия рамки")
|
||||
left, top, right, bottom = (float(value) for value in item.box_xyxy)
|
||||
if not (0 <= left < right <= 800 and 0 <= top < bottom <= 600):
|
||||
raise HTTPException(status_code=422, detail="Рамка выходит за границы кадра")
|
||||
proposed = item.proposed_label.strip() if isinstance(item.proposed_label, str) else None
|
||||
if item.category == "unmapped" and not proposed:
|
||||
raise HTTPException(status_code=422, detail="Для другого объекта требуется название")
|
||||
if item.category != "unmapped" and proposed is not None:
|
||||
raise HTTPException(status_code=422, detail="Название допустимо только для другого объекта")
|
||||
return {
|
||||
"object_id": item.object_id,
|
||||
"category": item.category,
|
||||
"proposed_label": proposed,
|
||||
"origin": item.origin,
|
||||
"box_xyxy": [left, top, right, bottom],
|
||||
"occluded": item.occluded,
|
||||
"truncated": item.truncated,
|
||||
}
|
||||
|
||||
|
||||
def _project_session(document: dict[str, Any], include_frames: bool) -> dict[str, object]:
|
||||
frames = document["frames"]
|
||||
reviewed = sum(frame.get("reviewed") is True for frame in frames)
|
||||
objects = sum(len(frame.get("objects", [])) for frame in frames)
|
||||
value = {
|
||||
key: copy.deepcopy(document[key])
|
||||
for key in (
|
||||
"schema_version",
|
||||
"session_id",
|
||||
"diagnostic_result_id",
|
||||
"contract_id",
|
||||
"title",
|
||||
"reviewer_slot",
|
||||
"revision",
|
||||
"state",
|
||||
"created_at_utc",
|
||||
"updated_at_utc",
|
||||
"blindness",
|
||||
"assistance",
|
||||
"authority",
|
||||
)
|
||||
}
|
||||
value["progress"] = {
|
||||
"reviewed_frame_count": reviewed,
|
||||
"frame_count": 32,
|
||||
"object_count": objects,
|
||||
"complete": reviewed == 32,
|
||||
}
|
||||
if include_frames:
|
||||
value["frames"] = copy.deepcopy(frames)
|
||||
return value
|
||||
|
||||
|
||||
def _project_result(result: dict[str, Any]) -> dict[str, object]:
|
||||
report = result["report"]
|
||||
identity = result["manifest"]["identity"]
|
||||
return {
|
||||
"result_id": result["result_id"],
|
||||
"created_at_utc": report["created_at_utc"],
|
||||
"status": report["status"],
|
||||
"source_session_id": report["source_session_id"],
|
||||
"camera_source_id": report["camera_source_id"],
|
||||
"diagnostic_result_id": identity["l34e_diagnostic"]["result_id"],
|
||||
"adjudication_session_id": identity["adjudication_session"]["session_id"],
|
||||
"metrics": copy.deepcopy(report["metrics"]),
|
||||
"decision": copy.deepcopy(report["decision"]),
|
||||
"limitations": copy.deepcopy(report["limitations"]),
|
||||
"authority": copy.deepcopy(report["authority"]),
|
||||
"ground_truth": False,
|
||||
"access": "read-only",
|
||||
}
|
||||
|
||||
|
||||
def _configured_root(provider: DiagnosticRootProvider) -> Path | None:
|
||||
value = provider()
|
||||
if value is None:
|
||||
return None
|
||||
path = value.expanduser().absolute()
|
||||
if path.is_symlink():
|
||||
return None
|
||||
try:
|
||||
path = path.resolve(strict=True)
|
||||
except OSError:
|
||||
return None
|
||||
return path if path.is_dir() else None
|
||||
|
||||
|
||||
def _configured_result_root(provider: ResultRootProvider) -> Path | None:
|
||||
return _configured_root(provider)
|
||||
|
||||
|
||||
def _result_candidates(provider: ResultRootProvider) -> list[Path]:
|
||||
root = _configured_result_root(provider)
|
||||
if root is None:
|
||||
return []
|
||||
return sorted(
|
||||
(
|
||||
path
|
||||
for path in root.iterdir()
|
||||
if path.is_dir()
|
||||
and not path.is_symlink()
|
||||
and _RESULT_ID.fullmatch(path.name) is not None
|
||||
and (path / L34F_MANIFEST_NAME).is_file()
|
||||
),
|
||||
key=lambda path: path.stat().st_mtime_ns,
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
|
||||
def _validate_operation_key(value: str) -> None:
|
||||
if _OPERATION_KEY.fullmatch(value) is None:
|
||||
raise HTTPException(status_code=422, detail="Некорректный ключ операции")
|
||||
|
||||
|
||||
def _png_dimensions(path: Path) -> tuple[int, int]:
|
||||
header = path.read_bytes()[:24]
|
||||
if len(header) != 24 or header[:8] != b"\x89PNG\r\n\x1a\n" or header[12:16] != b"IHDR":
|
||||
raise HTTPException(status_code=404, detail="Кадр повреждён")
|
||||
return int.from_bytes(header[16:20], "big"), int.from_bytes(header[20:24], "big")
|
||||
|
||||
|
||||
def _utc_now() -> str:
|
||||
return datetime.now(UTC).isoformat(timespec="milliseconds").replace("+00:00", "Z")
|
||||
Reference in New Issue
Block a user