Spaces:
Running
Running
| """Shared data contract between vision analysis and scene generation.""" | |
| from __future__ import annotations | |
| import math | |
| from typing import Any, Literal | |
| MotionType = Literal["rotate", "translate", "oscillate", "static", "screw", "orbit", "pulse"] | |
| EXAMPLE_ANALYSIS: dict[str, Any] = { | |
| "component": "ratchet wrench head", | |
| "confidence": 0.72, | |
| "summary": ( | |
| "A ratchet head converts a back-and-forth handle motion into one-way " | |
| "socket rotation. A pawl locks against the gear teeth during the drive " | |
| "stroke and slips over the teeth during the return stroke." | |
| ), | |
| "trigger": "handle swings clockwise and counterclockwise", | |
| "motion_sequence": [ | |
| "handle applies torque to the outer head", | |
| "spring-loaded pawl locks into the ratchet gear", | |
| "gear and socket rotate on the drive stroke", | |
| "pawl rides over gear teeth on the return stroke", | |
| ], | |
| "parts": [ | |
| { | |
| "id": "housing", | |
| "name": "head housing", | |
| "role": "keeps the mechanism aligned", | |
| "geometry": { | |
| "shape": "cylinder", | |
| "size": [3.2, 0.55, 3.2], | |
| "position": [0, 0, 0], | |
| "rotation": [1.5708, 0, 0], | |
| "color": "steel", | |
| }, | |
| "motion": {"type": "static"}, | |
| "annotation": { | |
| "point": [0.5, 0.55], | |
| "label": "head housing", | |
| "note": "visible outer shell around the ratchet mechanism", | |
| }, | |
| }, | |
| { | |
| "id": "ratchet_gear", | |
| "name": "ratchet gear", | |
| "role": "carries the socket and teeth", | |
| "geometry": { | |
| "shape": "gear", | |
| "teeth": 24, | |
| "size": [1.45, 0.4, 1.45], | |
| "position": [0, 0.15, 0], | |
| "color": "cyan", | |
| }, | |
| "motion": { | |
| "type": "rotate", | |
| "axis": [0, 1, 0], | |
| "speed": 0.95, | |
| "phase": 0, | |
| }, | |
| "annotation": { | |
| "point": [0.5, 0.48], | |
| "label": "ratchet gear", | |
| "note": "central rotating socket carrier", | |
| }, | |
| }, | |
| { | |
| "id": "pawl", | |
| "name": "spring pawl", | |
| "role": "locks and releases against gear teeth", | |
| "geometry": { | |
| "shape": "cone", | |
| "size": [0.34, 0.62, 0.34], | |
| "position": [1.25, 0.3, 0.18], | |
| "rotation": [0.55, 0, -0.75], | |
| "color": "amber", | |
| }, | |
| "motion": { | |
| "type": "oscillate", | |
| "axis": [0, 1, 0], | |
| "amplitude": 0.28, | |
| "speed": 3.2, | |
| "phase": 0.6, | |
| }, | |
| "annotation": { | |
| "point": [0.67, 0.44], | |
| "label": "spring pawl", | |
| "note": "locking tooth that alternates grip and slip", | |
| }, | |
| }, | |
| { | |
| "id": "pawl_spring", | |
| "name": "pawl return spring", | |
| "role": "pushes the pawl back into the gear teeth", | |
| "geometry": { | |
| "shape": "spring", | |
| "coils": 6, | |
| "wire": 0.035, | |
| "size": [0.34, 0.9, 0.34], | |
| "position": [1.55, 0.32, -0.2], | |
| "rotation": [0.2, 0, 0.45], | |
| "color": "orange", | |
| }, | |
| "motion": {"type": "pulse", "speed": 2.4, "amplitude": 0.08}, | |
| "annotation": { | |
| "point": [0.72, 0.5], | |
| "label": "return spring", | |
| "note": "small spring biasing the locking pawl", | |
| }, | |
| }, | |
| { | |
| "id": "selector_pin", | |
| "name": "selector pin", | |
| "role": "slides the selector and changes pawl bias direction", | |
| "geometry": { | |
| "shape": "capsule", | |
| "size": [0.22, 0.85, 0.22], | |
| "position": [-0.95, 0.56, -0.95], | |
| "rotation": [1.5708, 0, 0.25], | |
| "color": "steel", | |
| }, | |
| "motion": { | |
| "type": "translate", | |
| "axis": [1, 0, 0], | |
| "range": [-0.08, 0.08], | |
| "speed": 1.3, | |
| }, | |
| "annotation": { | |
| "point": [0.38, 0.3], | |
| "label": "selector pin", | |
| "note": "rounded pin under the direction selector", | |
| }, | |
| }, | |
| { | |
| "id": "retaining_ring", | |
| "name": "retaining ring", | |
| "role": "keeps the socket gear captured in the head", | |
| "geometry": { | |
| "shape": "torus", | |
| "size": [1.9, 0.18, 1.9], | |
| "position": [0, 0.43, 0], | |
| "color": "cyan", | |
| }, | |
| "motion": {"type": "static"}, | |
| "annotation": { | |
| "point": [0.5, 0.36], | |
| "label": "retaining ring", | |
| "note": "circular clip around the socket drive", | |
| }, | |
| }, | |
| ], | |
| } | |
| ANALYSIS_SCHEMA: dict[str, Any] = { | |
| "type": "object", | |
| "required": ["component", "summary", "trigger", "motion_sequence", "parts"], | |
| "properties": { | |
| "component": {"type": "string"}, | |
| "confidence": {"type": "number", "minimum": 0, "maximum": 1}, | |
| "render_mode": {"type": "string", "enum": ["three", "annotate", "unavailable"]}, | |
| "summary": {"type": "string"}, | |
| "trigger": {"type": "string"}, | |
| "motion_sequence": {"type": "array", "items": {"type": "string"}}, | |
| "parts": { | |
| "type": "array", | |
| "maxItems": 6, | |
| "items": { | |
| "type": "object", | |
| "required": ["id", "name", "role"], | |
| "properties": { | |
| "id": {"type": "string"}, | |
| "name": {"type": "string"}, | |
| "role": {"type": "string"}, | |
| "geometry": { | |
| "type": "object", | |
| "required": ["shape", "size", "position"], | |
| "properties": { | |
| "shape": { | |
| "type": "string", | |
| "enum": [ | |
| "box", | |
| "cylinder", | |
| "sphere", | |
| "gear", | |
| "rod", | |
| "cone", | |
| "capsule", | |
| "torus", | |
| "spring", | |
| ], | |
| }, | |
| "size": { | |
| "type": "array", | |
| "items": {"type": "number"}, | |
| "minItems": 3, | |
| "maxItems": 3, | |
| }, | |
| "position": { | |
| "type": "array", | |
| "items": {"type": "number"}, | |
| "minItems": 3, | |
| "maxItems": 3, | |
| }, | |
| "rotation": { | |
| "type": "array", | |
| "items": {"type": "number"}, | |
| "minItems": 3, | |
| "maxItems": 3, | |
| }, | |
| "teeth": {"type": "integer", "minimum": 6, "maximum": 80}, | |
| "coils": {"type": "integer", "minimum": 2, "maximum": 12}, | |
| "wire": {"type": "number"}, | |
| "color": {"type": "string"}, | |
| }, | |
| }, | |
| "motion": { | |
| "type": "object", | |
| "required": ["type"], | |
| "properties": { | |
| "type": { | |
| "type": "string", | |
| "enum": [ | |
| "rotate", | |
| "translate", | |
| "oscillate", | |
| "static", | |
| "screw", | |
| "orbit", | |
| "pulse", | |
| ], | |
| }, | |
| "axis": { | |
| "type": "array", | |
| "items": {"type": "number"}, | |
| "minItems": 3, | |
| "maxItems": 3, | |
| }, | |
| "speed": {"type": "number"}, | |
| "amplitude": {"type": "number"}, | |
| "phase": {"type": "number"}, | |
| "range": { | |
| "type": "array", | |
| "items": {"type": "number"}, | |
| "minItems": 2, | |
| "maxItems": 2, | |
| }, | |
| "pitch": {"type": "number"}, | |
| "pivot": { | |
| "type": "array", | |
| "items": {"type": "number"}, | |
| "minItems": 3, | |
| "maxItems": 3, | |
| }, | |
| }, | |
| }, | |
| "annotation": { | |
| "type": "object", | |
| "required": ["point"], | |
| "properties": { | |
| "point": { | |
| "type": "array", | |
| "items": {"type": "number", "minimum": 0, "maximum": 1}, | |
| "minItems": 2, | |
| "maxItems": 2, | |
| }, | |
| "box": { | |
| "type": "array", | |
| "items": {"type": "number", "minimum": 0, "maximum": 1}, | |
| "minItems": 4, | |
| "maxItems": 4, | |
| }, | |
| "label": {"type": "string"}, | |
| "note": {"type": "string"}, | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| } | |
| _SHAPES = {"box", "cylinder", "sphere", "gear", "rod", "cone", "capsule", "torus", "spring"} | |
| _MOTIONS = {"rotate", "translate", "oscillate", "static", "screw", "orbit", "pulse"} | |
| _RENDER_MODES = {"three", "annotate", "unavailable"} | |
| DEFAULT_CONFIDENCE_THRESHOLD = 0.5 | |
| def normalize_confidence_threshold(threshold: Any = DEFAULT_CONFIDENCE_THRESHOLD) -> float: | |
| if isinstance(threshold, bool) or threshold is None: | |
| return DEFAULT_CONFIDENCE_THRESHOLD | |
| try: | |
| value = float(threshold) | |
| except (TypeError, ValueError): | |
| return DEFAULT_CONFIDENCE_THRESHOLD | |
| if not math.isfinite(value): | |
| return DEFAULT_CONFIDENCE_THRESHOLD | |
| return max(0.0, min(1.0, value)) | |
| def select_render_mode( | |
| analysis: dict[str, Any], | |
| threshold: Any = DEFAULT_CONFIDENCE_THRESHOLD, | |
| ) -> str: | |
| """Pick the safest advisory renderer for a validated analysis payload.""" | |
| threshold = normalize_confidence_threshold(threshold) | |
| explicit_mode = analysis.get("render_mode") | |
| parts = analysis.get("parts") if isinstance(analysis, dict) else [] | |
| has_geometry = any(isinstance(part, dict) and isinstance(part.get("geometry"), dict) for part in parts) | |
| has_annotation = any(isinstance(part, dict) and isinstance(part.get("annotation"), dict) for part in parts) | |
| confidence = analysis.get("confidence", 1) | |
| low_confidence = _is_number(confidence) and float(confidence) < threshold | |
| if has_annotation and (low_confidence or not has_geometry): | |
| mode = "annotate" | |
| elif has_geometry and not low_confidence: | |
| mode = "three" | |
| elif has_annotation: | |
| mode = "annotate" | |
| else: | |
| mode = "unavailable" | |
| if explicit_mode in _RENDER_MODES: | |
| return _lower_render_mode(str(explicit_mode), mode) | |
| return mode | |
| def _lower_render_mode(left: str, right: str) -> str: | |
| rank = {"unavailable": 0, "annotate": 1, "three": 2} | |
| return left if rank[left] <= rank[right] else right | |
| def validate_analysis(payload: dict[str, Any]) -> dict[str, Any]: | |
| """Validate a model analysis payload against the scene-generation contract.""" | |
| if not isinstance(payload, dict): | |
| raise ValueError("Analysis payload must be a JSON object.") | |
| for key in ["component", "summary", "trigger"]: | |
| _require_string(payload, key, key) | |
| _require_string_list(payload, "motion_sequence", "motion_sequence") | |
| if "render_mode" in payload and payload["render_mode"] not in _RENDER_MODES: | |
| raise ValueError("Invalid analysis payload at render_mode: unsupported mode") | |
| parts = payload.get("parts") | |
| if not isinstance(parts, list) or not parts: | |
| raise ValueError("Invalid analysis payload at parts: expected a non-empty list") | |
| if len(parts) > 6: | |
| raise ValueError("Invalid analysis payload at parts: expected no more than 6 items") | |
| for index, part in enumerate(parts): | |
| path = f"parts.{index}" | |
| if not isinstance(part, dict): | |
| raise ValueError(f"Invalid analysis payload at {path}: expected an object") | |
| for key in ["id", "name", "role"]: | |
| _require_string(part, key, f"{path}.{key}") | |
| has_geometry = False | |
| geometry = part.get("geometry") | |
| if geometry is not None: | |
| if not isinstance(geometry, dict): | |
| raise ValueError(f"Invalid analysis payload at {path}.geometry: expected an object") | |
| shape = geometry.get("shape") | |
| if shape not in _SHAPES: | |
| raise ValueError(f"Invalid analysis payload at {path}.geometry.shape: unsupported shape") | |
| _require_number_list(geometry, "size", f"{path}.geometry.size", 3) | |
| _require_number_list(geometry, "position", f"{path}.geometry.position", 3) | |
| if "rotation" in geometry: | |
| _require_number_list(geometry, "rotation", f"{path}.geometry.rotation", 3) | |
| if "teeth" in geometry and not isinstance(geometry["teeth"], int): | |
| raise ValueError(f"Invalid analysis payload at {path}.geometry.teeth: expected an integer") | |
| if "coils" in geometry and not isinstance(geometry["coils"], int): | |
| raise ValueError(f"Invalid analysis payload at {path}.geometry.coils: expected an integer") | |
| if "wire" in geometry and not _is_number(geometry["wire"]): | |
| raise ValueError(f"Invalid analysis payload at {path}.geometry.wire: expected a number") | |
| has_geometry = True | |
| has_annotation = False | |
| annotation = part.get("annotation") | |
| if annotation is not None: | |
| if not isinstance(annotation, dict): | |
| raise ValueError(f"Invalid analysis payload at {path}.annotation: expected an object") | |
| _require_unit_number_list(annotation, "point", f"{path}.annotation.point", 2) | |
| if "box" in annotation: | |
| _require_unit_number_list(annotation, "box", f"{path}.annotation.box", 4) | |
| for key in ["label", "note"]: | |
| if key in annotation and not isinstance(annotation[key], str): | |
| raise ValueError(f"Invalid analysis payload at {path}.annotation.{key}: expected a string") | |
| has_annotation = True | |
| if not has_geometry and not has_annotation: | |
| raise ValueError( | |
| f"Invalid analysis payload at {path}: expected geometry or annotation" | |
| ) | |
| motion = part.get("motion") | |
| if motion is not None: | |
| if not isinstance(motion, dict): | |
| raise ValueError(f"Invalid analysis payload at {path}.motion: expected an object") | |
| motion_type = motion.get("type") | |
| if motion_type not in _MOTIONS: | |
| raise ValueError(f"Invalid analysis payload at {path}.motion.type: unsupported motion") | |
| if "axis" in motion: | |
| _require_number_list(motion, "axis", f"{path}.motion.axis", 3) | |
| if "range" in motion: | |
| _require_number_list(motion, "range", f"{path}.motion.range", 2) | |
| if "pivot" in motion: | |
| _require_number_list(motion, "pivot", f"{path}.motion.pivot", 3) | |
| for key in ["speed", "amplitude", "phase", "pitch"]: | |
| if key in motion and not _is_number(motion[key]): | |
| raise ValueError(f"Invalid analysis payload at {path}.motion.{key}: expected a number") | |
| elif has_geometry: | |
| raise ValueError(f"Invalid analysis payload at {path}.motion: expected an object") | |
| return payload | |
| def _require_string(payload: dict[str, Any], key: str, path: str) -> None: | |
| if not isinstance(payload.get(key), str) or not payload[key].strip(): | |
| raise ValueError(f"Invalid analysis payload at {path}: expected a non-empty string") | |
| def _require_string_list(payload: dict[str, Any], key: str, path: str) -> None: | |
| value = payload.get(key) | |
| if not isinstance(value, list) or not value or not all(isinstance(item, str) and item.strip() for item in value): | |
| raise ValueError(f"Invalid analysis payload at {path}: expected a non-empty list of strings") | |
| def _require_number_list(payload: dict[str, Any], key: str, path: str, length: int) -> None: | |
| value = payload.get(key) | |
| if not isinstance(value, list) or len(value) != length or not all(_is_number(item) for item in value): | |
| raise ValueError(f"Invalid analysis payload at {path}: expected {length} numbers") | |
| def _require_unit_number_list(payload: dict[str, Any], key: str, path: str, length: int) -> None: | |
| value = payload.get(key) | |
| if ( | |
| not isinstance(value, list) | |
| or len(value) != length | |
| or not all(_is_number(item) and 0 <= item <= 1 for item in value) | |
| ): | |
| raise ValueError(f"Invalid analysis payload at {path}: expected {length} numbers from 0 to 1") | |
| def _is_number(value: Any) -> bool: | |
| return isinstance(value, (int, float)) and not isinstance(value, bool) | |