Buckets:

rydlrKE's picture
download
raw
17.9 kB
"""
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')."
)
@field_validator("character_id")
@classmethod
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})."
)
@field_validator("action_text")
@classmethod
def validate_action_text(cls, v):
if not v or len(v) < 3:
raise ValueError("action_text must be >= 3 chars")
return v.strip()
@field_validator("duration_sec")
@classmethod
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."
)
@field_validator("scene_id")
@classmethod
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()
@field_validator("user_prompt")
@classmethod
def validate_prompt(cls, v):
if not v or len(v) < 10:
raise ValueError("user_prompt must be >= 10 chars")
return v.strip()
@field_validator("characters")
@classmethod
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."
)
@field_validator("total_duration_sec")
@classmethod
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."
)
@field_validator("character_id")
@classmethod
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."
)
@field_validator("characters")
@classmethod
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.