Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 55 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,18 @@ blocksmith generate "a castle" -o castle.glb
![Castle Example](castle-example-model.jpg)
> **Tip:** You can view your generated GLB/GLTF files for free at [sandbox.babylonjs.com](https://sandbox.babylonjs.com/).

**Bring it to life:**
```bash
# Generate a character
blocksmith generate "a blocky robot" -o robot.py

# Animate it
blocksmith animate "walk cycle" -m robot.py -o walk.py

# Link together
blocksmith link -m robot.py -a walk.py -o robot_animated.glb
```

```bash
# Image to 3D model
blocksmith generate "blocky version" --image photo.jpg -o model.glb
Expand Down Expand Up @@ -90,6 +102,12 @@ source .venv/bin/activate # On Windows: .venv\Scripts\activate

# Install in editable mode
pip install -e .

# Updating to latest version:
# git pull origin main
# pip install -e . # Re-run if dependencies changed
# If you hit weird import errors:
# deactivate && source .venv/bin/activate
```

### 2. Set Up API Key
Expand Down Expand Up @@ -172,6 +190,10 @@ blocksmith generate "turn this into blocks" --image photo.jpg -o model.glb

# Convert between formats
blocksmith convert castle.bbmodel castle.glb

# Animation Workflow
blocksmith animate "wave hand" -m steve.py -o wave.py
blocksmith link -m steve.py -a wave.py -o steve_animated.glb
```

### Python SDK
Expand Down Expand Up @@ -220,8 +242,39 @@ blocksmith convert tree.bbmodel tree.json
```bash
blocksmith --help
blocksmith generate --help
blocksmith --help
blocksmith generate --help
blocksmith convert --help
blocksmith animate --help
blocksmith link --help
```
### Animation Workflow

**1. Generate Animation Code:**
```bash
# Creates a Python file containing just the animation logic
blocksmith animate "make it run" -m model.py -o run.py
```

**2. Link to Model:**
```bash
# Merges the model and animation(s) into a GLB
blocksmith link -m model.py -a run.py -o final.glb

# You can stick multiple animations together!
blocksmith link -m model.py -a walk.py -a run.py -a jump.py -o final.glb
```

### ⚠️ Animation Caveats & Best Practices

1. **Centered Pivots:** If you want an object to rotate around its center (like a floating cube), the **Model** must have a Group pivot at its geometric center.
* *Bad:* A cube at `[0,0,0]` will rotate around the bottom-left corner.
* *Good:* A cube inside a Group at `[0, 0.5, 0]` will rotate around its center.
* *Fix:* Ask the generator for "a cube centered at 0,0,0 ready for rotation".
2. **Speed**: LLMs tend to generate very fast animations (1.0s). For smooth loops, try asking for "slow, smooth rotation" or specifically "2 to 4 seconds duration".
3. **Forward Direction**: In BlockSmith, **North is -Z**.
* Arms raising "Forward" will rotate towards -Z.
* The LLM knows this, but sometimes needs reminders for complex moves.

### Python SDK Usage

Expand Down Expand Up @@ -517,7 +570,8 @@ This is an alpha release focused on core features. Current limitations:
- ✅ Format conversion API

**v0.2 (Planned)**
- [ ] Animation support
**v0.2 (Planned)**
- [x] Animation support (Basic)
- [ ] Blockbench plugin
- [ ] Web UI

Expand Down
133 changes: 133 additions & 0 deletions blocksmith/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,62 @@ def generate(prompt, output, model, image, verbose):
traceback.print_exc()
sys.exit(1)

@cli.command()
@click.argument("prompt")
@click.option("--model-file", "-m", required=True, type=click.Path(exists=True), help="Path to existing model Python file")
@click.option("--output", "-o", default="anim.py", help="Output file path (default: anim.py)")
@click.option("--model", help="LLM model override")
@click.option('--verbose', '-v', is_flag=True, help='Show detailed statistics')
def animate(prompt, model_file, output, model, verbose):
"""
Generate animations for an existing model.

Examples:
blocksmith animate "walk cycle" -m steve.py -o walk.py
blocksmith animate "wave hand" -m robot.py -o wave.py
"""
try:
# Initialize client
bs = Blocksmith(default_model=model) if model else Blocksmith()

if verbose:
click.echo(f"Animating: {prompt}")
click.echo(f"Base Model: {model_file}")

# Read model code
with open(model_file, 'r') as f:
model_code = f.read()

# Generate animation
result = bs.animate(prompt, model_code, model=model)

# Save output
click.echo(f"Saving animation to: {output}")
# Force saving as python file
if not output.endswith('.py'):
output += '.py'

with open(output, 'w') as f:
f.write(result.dsl)

# Show stats if verbose
if verbose:
click.echo("\nGeneration Statistics:")
click.echo(f" Tokens: {result.tokens.total_tokens}")
if result.cost is not None:
click.echo(f" Cost: ${result.cost:.4f}")
click.echo(f" Model: {result.model}")

click.secho(f"✓ Success! Animation saved to {output}", fg='green')

except Exception as e:
click.secho(f"Error: {e}", fg='red', err=True)
if verbose:
import traceback
traceback.print_exc()
sys.exit(1)



@cli.command()
@click.argument('input_path')
Expand Down Expand Up @@ -131,6 +187,83 @@ def convert(input_path, output_path, verbose):
sys.exit(1)


@cli.command()
@click.option('-m', '--model', 'model_path', required=True, help='Path to the base model file (Python DSL).')
@click.option('-a', '--animation', 'anim_paths', multiple=True, help='Path to animation Python file(s). Can be specified multiple times.')
@click.option('-o', '--output', required=True, help='Output file path (.glb only supported for now).')
@click.option('--verbose', '-v', is_flag=True, help='Show detailed linking info')
def link(model_path, anim_paths, output, verbose):
"""
Link a base model with one or more animation files.

This allows you to generate a static model first, generate animations separately,
and then combine them into a single animated GLB file.

Example:
blocksmith link -m robot.py -a walk.py -a wave.py -o robot.glb
"""
try:
from blocksmith.converters.python.importer import import_python_from_file, import_animation_only
from blocksmith.converters.gltf.exporter import export_glb

# 1. Load Base Model
if verbose:
click.echo(f"Loading base model: {model_path}")

if not Path(model_path).exists():
raise FileNotFoundError(f"Model file not found: {model_path}")

# We primarily support Python DSL for the linker as per design
if not model_path.endswith('.py'):
click.secho("Warning: Linker is designed for .py model files. Other formats might not work as expected.", fg='yellow')

# Load model structure
model_data = import_python_from_file(model_path)

# Initialize animations list if missing
if 'animations' not in model_data or model_data['animations'] is None:
model_data['animations'] = []

# 2. Load Animations
for anim_path in anim_paths:
if verbose:
click.echo(f"Linking animation file: {anim_path}")

if not Path(anim_path).exists():
raise FileNotFoundError(f"Animation file not found: {anim_path}")

with open(anim_path, 'r', encoding='utf-8') as f:
code = f.read()

# Extract animations using the helper
anims = import_animation_only(code)

if verbose:
click.echo(f" Found {len(anims)} animations: {[a['name'] for a in anims]}")

model_data['animations'].extend(anims)

# 3. Export
if verbose:
click.echo(f"Exporting combined model to: {output}")

if output.endswith('.glb'):
glb_bytes = export_glb(model_data)
with open(output, 'wb') as f:
f.write(glb_bytes)
else:
raise ValueError("Linker currently only supports .glb output.")

click.secho(f"✓ Success! Linked model saved to {output}", fg='green')

except Exception as e:
click.secho(f"Error linking model: {e}", fg='red', err=True)
if verbose:
import traceback
traceback.print_exc()
sys.exit(1)


def main():
"""Entry point for the CLI"""
cli()
Expand Down
43 changes: 43 additions & 0 deletions blocksmith/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class for handling conversions and saving.
from blocksmith.generator.engine import ModelGenerator
from blocksmith.converters import import_python, export_glb, export_gltf, export_bbmodel
from blocksmith.llm.client import TokenUsage
from blocksmith.generator.prompts import ANIMATION_SYSTEM_PROMPT


@dataclass
Expand Down Expand Up @@ -180,6 +181,48 @@ def generate(
_bs=self
)

def animate(
self,
prompt: str,
model_code: str,
model: Optional[str] = None
) -> GenerationResult:
"""
Generate animations for an existing model structure.

Args:
prompt: Description of the animation (e.g., "walk cycle")
model_code: The Python source code of the existing model (defines IDs)
model: Optional LLM model override

Returns:
GenerationResult: Contains the generated create_animations() code
"""
# Construct specific prompt for animation
# We embed the model code so the LLM knows the IDs
full_prompt = f"""# Animation Request
{prompt}

# Existing Model Structure
Use the Group IDs defined in this code:
{model_code}
"""

# Call generator with specific system prompt
gen_response = self.generator.generate(
prompt=full_prompt,
model=model,
system_prompt=ANIMATION_SYSTEM_PROMPT
)

return GenerationResult(
dsl=gen_response.code,
tokens=gen_response.tokens,
cost=gen_response.cost,
model=gen_response.model,
_bs=self
)

def get_stats(self):
"""
Get session statistics across all generations.
Expand Down
12 changes: 6 additions & 6 deletions blocksmith/converters/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
"""Format converters for BlockSmith models"""

from blocksmith.converters.python.importer import import_python
from blocksmith.converters.python.exporter import export_python
from blocksmith.converters.gltf.exporter import export_glb, export_gltf
from blocksmith.converters.gltf.importer_wrapper import import_gltf, import_glb
from blocksmith.converters.bbmodel.exporter import export_bbmodel
from blocksmith.converters.bbmodel.importer import import_bbmodel
from .python.importer import import_python
from .python.exporter import export_python
from .gltf.exporter import export_glb, export_gltf
from .gltf.importer_wrapper import import_gltf, import_glb
from .bbmodel.exporter import export_bbmodel
from .bbmodel.importer import import_bbmodel

__all__ = [
"import_python",
Expand Down
2 changes: 1 addition & 1 deletion blocksmith/converters/bbmodel/exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

# Import BlockJSON schema
from blocksmith.schema.blockjson import ModelDefinition, CuboidEntity, GroupEntity
from blocksmith.converters.uv_mapper import to_bbmodel
from ..uv_mapper import to_bbmodel

logger = logging.getLogger(__name__)

Expand Down
Loading