Buckets:
| """ | |
| Card 2: Service Contracts (Pydantic Schemas) | |
| Defines strict request/response contracts for: | |
| 1. Qwen Planner API (story prompt → validated motion script) | |
| 2. Kimodo Generator API (motion script → motion generation) | |
| All schemas include defensive validation, error codes, and examples. | |
| """ | |
| from enum import Enum | |
| from typing import List, Optional, Dict, Any | |
| from pydantic import BaseModel, Field, field_validator | |
| import json | |
| import logging | |
| LOGGER = logging.getLogger(__name__) | |
| # ============================================================================ | |
| # Enums | |
| # ============================================================================ | |
| class TransitionPolicy(str, Enum): | |
| """How to transition between motion segments.""" | |
| SMOOTH = "smooth" # Blend final frame of A with initial frame of B | |
| CUT = "cut" # Hard cut from A to B | |
| HOLD = "hold" # Hold final pose of A before B starts | |
| OVERLAP = "overlap" # Overlap A and B for N frames | |
| class ConstraintType(str, Enum): | |
| """Types of kinematic constraints.""" | |
| POSITIONAL = "positional" # XYZ position constraints | |
| ROTATIONAL = "rotational" # Joint angle constraints | |
| VELOCITY = "velocity" # Movement speed limits | |
| CONTACT = "contact" # Foot contact, hand placement | |
| NONE = "none" | |
| # ============================================================================ | |
| # Planner API Schemas (Qwen LLM → Motion Script) | |
| # ============================================================================ | |
| class CharacterDefinition(BaseModel): | |
| """Definition of a character in the scene.""" | |
| character_id: str = Field( | |
| ..., | |
| min_length=1, | |
| max_length=50, | |
| pattern="^[a-zA-Z0-9_-]+$", | |
| description="Unique identifier for this character (alphanumeric + _ -)." | |
| ) | |
| skeleton_type: str = Field( | |
| default="soma", | |
| description="Skeleton rig type (soma, g1, smpl-x, etc.)" | |
| ) | |
| description: Optional[str] = Field( | |
| default=None, | |
| max_length=200, | |
| description="Brief character description (e.g., 'tall female dancer')." | |
| ) | |
| def validate_char_id(cls, v): | |
| if not v or len(v) > 50: | |
| raise ValueError("character_id must be 1-50 chars") | |
| return v.strip() | |
| class MotionSegment(BaseModel): | |
| """A single motion segment for one character.""" | |
| segment_id: int = Field( | |
| ..., | |
| ge=0, | |
| description="Sequence order (0-based) within character's script." | |
| ) | |
| action_text: str = Field( | |
| ..., | |
| min_length=3, | |
| max_length=500, | |
| description="Natural language action description (e.g., 'walk forward with arms raised')." | |
| ) | |
| duration_sec: float = Field( | |
| default=2.0, | |
| ge=0.5, | |
| le=30.0, | |
| description="Duration of this motion segment in seconds (0.5-30s)." | |
| ) | |
| transition_policy: TransitionPolicy = Field( | |
| default=TransitionPolicy.SMOOTH, | |
| description="How to transition to the next segment." | |
| ) | |
| interaction_target: Optional[str] = Field( | |
| default=None, | |
| description="Another character_id to interact with (optional)." | |
| ) | |
| constraints: Optional[Dict[str, Any]] = Field( | |
| default_factory=dict, | |
| description="Kinematic constraints dict (e.g., {'floor_contact': True})." | |
| ) | |
| def validate_action_text(cls, v): | |
| if not v or len(v) < 3: | |
| raise ValueError("action_text must be >= 3 chars") | |
| return v.strip() | |
| def validate_duration(cls, v): | |
| if not (0.5 <= v <= 30.0): | |
| raise ValueError("duration_sec must be between 0.5 and 30.0 seconds") | |
| return v | |
| class PlannerRequest(BaseModel): | |
| """Request from frontend to Qwen planner.""" | |
| scene_id: str = Field( | |
| ..., | |
| min_length=1, | |
| max_length=100, | |
| description="Unique identifier for this scene/request." | |
| ) | |
| user_prompt: str = Field( | |
| ..., | |
| min_length=10, | |
| max_length=2000, | |
| description="High-level story or interaction prompt from user (10-2000 chars)." | |
| ) | |
| characters: List[CharacterDefinition] = Field( | |
| ..., | |
| min_length=1, | |
| max_length=10, | |
| description="List of characters in the scene (1-10 characters)." | |
| ) | |
| duration_limit_sec: float = Field( | |
| default=60.0, | |
| ge=10.0, | |
| le=600.0, | |
| description="Maximum total duration for the scene (10-600 seconds)." | |
| ) | |
| interactive_mode: bool = Field( | |
| default=False, | |
| description="If True, planner may request user input for interactions." | |
| ) | |
| def validate_scene_id(cls, v): | |
| if not v or len(v) > 100: | |
| raise ValueError("scene_id must be 1-100 chars") | |
| return v.strip() | |
| def validate_prompt(cls, v): | |
| if not v or len(v) < 10: | |
| raise ValueError("user_prompt must be >= 10 chars") | |
| return v.strip() | |
| def validate_characters(cls, v): | |
| if not v or len(v) > 10: | |
| raise ValueError("Must have 1-10 characters") | |
| # Check for duplicate character_ids | |
| ids = [c.character_id for c in v] | |
| if len(ids) != len(set(ids)): | |
| raise ValueError("Duplicate character_id found") | |
| return v | |
| class PlannerResponse(BaseModel): | |
| """Response from Qwen planner (validated motion script).""" | |
| scene_id: str = Field( | |
| ..., | |
| description="Echo of request scene_id." | |
| ) | |
| status: str = Field( | |
| default="success", | |
| description="Planner status (success/partial/error).", | |
| json_schema_extra={"enum": ["success", "partial", "error"]} | |
| ) | |
| error_message: Optional[str] = Field( | |
| default=None, | |
| description="Error details if status != success." | |
| ) | |
| scripts: Dict[str, List[MotionSegment]] = Field( | |
| default_factory=dict, | |
| description="Per-character motion scripts: {character_id: [segments]}." | |
| ) | |
| metadata: Dict[str, Any] = Field( | |
| default_factory=dict, | |
| description="Optional metadata (model name, version, timestamp, etc.)." | |
| ) | |
| total_duration_sec: float = Field( | |
| default=0.0, | |
| ge=0.0, | |
| description="Computed total duration of all characters combined." | |
| ) | |
| def validate_total_duration(cls, v): | |
| if v < 0: | |
| raise ValueError("total_duration_sec must be non-negative") | |
| return v | |
| # ============================================================================ | |
| # Generator API Schemas (Motion Script → Kimodo Generation) | |
| # ============================================================================ | |
| class GenerationConstraint(BaseModel): | |
| """Per-character generation constraint.""" | |
| constraint_type: ConstraintType = Field( | |
| default=ConstraintType.NONE, | |
| description="Type of constraint to apply." | |
| ) | |
| params: Dict[str, Any] = Field( | |
| default_factory=dict, | |
| description="Constraint-specific parameters (e.g., target position, velocity limit)." | |
| ) | |
| priority: int = Field( | |
| default=1, | |
| ge=1, | |
| le=10, | |
| description="Priority level (1-10, higher = enforced more strictly)." | |
| ) | |
| class CharacterGenerationState(BaseModel): | |
| """Per-character state for generation.""" | |
| character_id: str = Field( | |
| ..., | |
| description="Character identifier." | |
| ) | |
| skeleton_type: str = Field( | |
| default="soma", | |
| description="Skeleton rig type." | |
| ) | |
| segments: List[MotionSegment] = Field( | |
| ..., | |
| description="Motion segments for this character." | |
| ) | |
| initial_pose: Optional[Dict[str, Any]] = Field( | |
| default=None, | |
| description="Optional initial pose (joint angles or transformation)." | |
| ) | |
| constraints: Optional[List[GenerationConstraint]] = Field( | |
| default=None, | |
| description="List of kinematic constraints." | |
| ) | |
| def validate_id(cls, v): | |
| if not v: | |
| raise ValueError("character_id required") | |
| return v.strip() | |
| class GeneratorRequest(BaseModel): | |
| """Request to Kimodo generator (from planner response).""" | |
| scene_id: str = Field( | |
| ..., | |
| description="Scene identifier." | |
| ) | |
| characters: List[CharacterGenerationState] = Field( | |
| ..., | |
| min_length=1, | |
| max_length=10, | |
| description="Character states and motion segments." | |
| ) | |
| seed: int = Field( | |
| default=42, | |
| ge=0, | |
| description="Random seed for deterministic generation." | |
| ) | |
| num_samples: int = Field( | |
| default=1, | |
| ge=1, | |
| le=5, | |
| description="Number of motion samples to generate (1-5)." | |
| ) | |
| device: Optional[str] = Field( | |
| default=None, | |
| description="Compute device (cuda, rocm, cpu). If None, auto-detect." | |
| ) | |
| def validate_chars(cls, v): | |
| if not v or len(v) > 10: | |
| raise ValueError("Must have 1-10 characters") | |
| return v | |
| class MotionOutput(BaseModel): | |
| """Generated motion output for a single character.""" | |
| character_id: str = Field( | |
| ..., | |
| description="Character identifier." | |
| ) | |
| motion_data: Dict[str, Any] = Field( | |
| ..., | |
| description="Motion data (NPZ converted to dict: posed_joints, rotation_mats, foot_contacts, etc.)." | |
| ) | |
| duration_sec: float = Field( | |
| default=0.0, | |
| description="Actual duration of generated motion." | |
| ) | |
| frame_count: int = Field( | |
| default=0, | |
| description="Number of frames in motion." | |
| ) | |
| fps: int = Field( | |
| default=30, | |
| description="Frames per second." | |
| ) | |
| quality_score: Optional[float] = Field( | |
| default=None, | |
| description="Optional quality metric (0-1)." | |
| ) | |
| class GeneratorResponse(BaseModel): | |
| """Response from Kimodo generator.""" | |
| scene_id: str = Field( | |
| ..., | |
| description="Scene identifier." | |
| ) | |
| status: str = Field( | |
| default="success", | |
| description="Generation status.", | |
| json_schema_extra={"enum": ["success", "partial", "error"]} | |
| ) | |
| error_message: Optional[str] = Field( | |
| default=None, | |
| description="Error details if status != success." | |
| ) | |
| motions: List[MotionOutput] = Field( | |
| default_factory=list, | |
| description="Generated motions per character." | |
| ) | |
| total_frames: int = Field( | |
| default=0, | |
| description="Total frames across all characters." | |
| ) | |
| generation_time_sec: float = Field( | |
| default=0.0, | |
| description="Wall-clock time to generate (seconds)." | |
| ) | |
| metadata: Dict[str, Any] = Field( | |
| default_factory=dict, | |
| description="Metadata (model version, device, seed, etc.)." | |
| ) | |
| # ============================================================================ | |
| # Validation Examples | |
| # ============================================================================ | |
| def example_planner_request() -> PlannerRequest: | |
| """Example valid planner request.""" | |
| return PlannerRequest( | |
| scene_id="scene_001", | |
| user_prompt="Two dancers interact in the middle of a stage, one leads a waltz while the other follows.", | |
| characters=[ | |
| CharacterDefinition(character_id="dancer1", skeleton_type="soma", description="Lead dancer"), | |
| CharacterDefinition(character_id="dancer2", skeleton_type="soma", description="Follow dancer"), | |
| ], | |
| duration_limit_sec=60.0, | |
| interactive_mode=False | |
| ) | |
| def example_planner_response() -> PlannerResponse: | |
| """Example valid planner response.""" | |
| return PlannerResponse( | |
| scene_id="scene_001", | |
| status="success", | |
| scripts={ | |
| "dancer1": [ | |
| MotionSegment( | |
| segment_id=0, | |
| action_text="Walk forward with arms extended in waltz position", | |
| duration_sec=5.0, | |
| transition_policy=TransitionPolicy.SMOOTH, | |
| interaction_target="dancer2" | |
| ), | |
| MotionSegment( | |
| segment_id=1, | |
| action_text="Turn left while leading dancer2 in circular motion", | |
| duration_sec=5.0, | |
| transition_policy=TransitionPolicy.SMOOTH | |
| ), | |
| ], | |
| "dancer2": [ | |
| MotionSegment( | |
| segment_id=0, | |
| action_text="Follow dancer1 with arms extended, matching their tempo", | |
| duration_sec=5.0, | |
| transition_policy=TransitionPolicy.SMOOTH, | |
| interaction_target="dancer1" | |
| ), | |
| MotionSegment( | |
| segment_id=1, | |
| action_text="Turn right while being led by dancer1", | |
| duration_sec=5.0, | |
| transition_policy=TransitionPolicy.SMOOTH | |
| ), | |
| ] | |
| }, | |
| metadata={"model": "Qwen2.5-7B-Instruct", "created_at": "2026-05-09T12:00:00Z"}, | |
| total_duration_sec=10.0 | |
| ) | |
| def example_generator_request() -> GeneratorRequest: | |
| """Example valid generator request.""" | |
| planner_resp = example_planner_response() | |
| resp_dict = planner_resp.model_dump() | |
| chars = [ | |
| CharacterGenerationState( | |
| character_id="dancer1", | |
| skeleton_type="soma", | |
| segments=resp_dict["scripts"]["dancer1"] | |
| ), | |
| CharacterGenerationState( | |
| character_id="dancer2", | |
| skeleton_type="soma", | |
| segments=resp_dict["scripts"]["dancer2"] | |
| ), | |
| ] | |
| return GeneratorRequest( | |
| scene_id="scene_001", | |
| characters=chars, | |
| seed=42, | |
| num_samples=1 | |
| ) | |
| # ============================================================================ | |
| # Test & Validation Functions | |
| # ============================================================================ | |
| def validate_schema_examples(): | |
| """Test all schemas with valid and invalid payloads.""" | |
| LOGGER.info("schemas.validate_schema_examples.start") | |
| print("=== Card 2: Schema Validation Tests ===\n") | |
| # Test 1: Valid Planner Request | |
| try: | |
| req = example_planner_request() | |
| print("✓ Valid Planner Request: PASS") | |
| except Exception as e: | |
| print(f"✗ Valid Planner Request: FAIL - {e}") | |
| # Test 2: Valid Planner Response | |
| try: | |
| resp = example_planner_response() | |
| print("✓ Valid Planner Response: PASS") | |
| except Exception as e: | |
| print(f"✗ Valid Planner Response: FAIL - {e}") | |
| # Test 3: Valid Generator Request | |
| try: | |
| req = example_generator_request() | |
| print("✓ Valid Generator Request: PASS") | |
| except Exception as e: | |
| print(f"✗ Valid Generator Request: FAIL - {e}") | |
| print() | |
| # Test 4: Invalid Planner Request (missing required field) | |
| try: | |
| PlannerRequest( | |
| scene_id="test", | |
| # Missing user_prompt - should fail | |
| characters=[ | |
| CharacterDefinition(character_id="c1") | |
| ] | |
| ) | |
| print("✗ Invalid Request (missing user_prompt): FAIL - should have raised error") | |
| except Exception as e: | |
| print("✓ Invalid Request (missing user_prompt): PASS - correctly rejected") | |
| # Test 5: Invalid Planner Request (user_prompt too short) | |
| try: | |
| PlannerRequest( | |
| scene_id="test", | |
| user_prompt="short", # < 10 chars | |
| characters=[ | |
| CharacterDefinition(character_id="c1") | |
| ] | |
| ) | |
| print("✗ Invalid Request (short prompt): FAIL - should have raised error") | |
| except Exception as e: | |
| print("✓ Invalid Request (short prompt): PASS - correctly rejected") | |
| # Test 6: Invalid character_id (special chars not allowed) | |
| try: | |
| CharacterDefinition(character_id="c1@invalid") | |
| print("✗ Invalid character_id: FAIL - should have raised error") | |
| except Exception as e: | |
| print("✓ Invalid character_id: PASS - correctly rejected") | |
| # Test 7: Invalid duration_sec (out of range) | |
| try: | |
| MotionSegment( | |
| segment_id=0, | |
| action_text="test action", | |
| duration_sec=60.0 # > 30.0 max | |
| ) | |
| print("✗ Invalid duration_sec: FAIL - should have raised error") | |
| except Exception as e: | |
| print("✓ Invalid duration_sec: PASS - correctly rejected") | |
| # Test 8: Duplicate character_ids | |
| try: | |
| PlannerRequest( | |
| scene_id="test", | |
| user_prompt="this is a long enough prompt", | |
| characters=[ | |
| CharacterDefinition(character_id="char1"), | |
| CharacterDefinition(character_id="char1"), # Duplicate | |
| ] | |
| ) | |
| print("✗ Duplicate character_ids: FAIL - should have raised error") | |
| except Exception as e: | |
| print("✓ Duplicate character_ids: PASS - correctly rejected") | |
| print() | |
| print("=== All Schema Tests Complete ===") | |
| LOGGER.info("schemas.validate_schema_examples.exit") | |
| if __name__ == "__main__": | |
| validate_schema_examples() | |
Xet Storage Details
- Size:
- 17.9 kB
- Xet hash:
- 4ac76362c7dc86a18a519e6295605df971fd72ae95e652138483764dcf64d30a
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.