Spaces:
Runtime error
Runtime error
import gradio as gr | |
import json | |
import os | |
import uuid | |
from datetime import datetime | |
from typing import Dict, List, Optional, Tuple | |
import hashlib | |
import hmac | |
import time | |
from pathlib import Path | |
# Create data directories | |
DATA_DIR = Path("data") | |
PICLETS_DIR = DATA_DIR / "piclets" | |
COLLECTIONS_DIR = DATA_DIR / "collections" | |
IMAGES_DIR = DATA_DIR / "images" | |
for dir_path in [DATA_DIR, PICLETS_DIR, COLLECTIONS_DIR, IMAGES_DIR]: | |
dir_path.mkdir(exist_ok=True) | |
# Secret key for verification (in production, use environment variable) | |
SECRET_KEY = os.getenv("PICLET_SECRET_KEY", "piclets-dev-key-change-in-production") | |
class PicletVerification: | |
"""Handles Piclet authenticity verification""" | |
def create_signature(piclet_data: dict, timestamp: int, generation_data: dict = None) -> str: | |
""" | |
Create a cryptographic signature for a Piclet | |
Uses HMAC-SHA256 with the secret key | |
""" | |
# Create deterministic string from core Piclet data | |
core_data = { | |
"name": piclet_data.get("name", ""), | |
"primaryType": piclet_data.get("primaryType", ""), | |
"baseStats": piclet_data.get("baseStats", {}), | |
"movepool": piclet_data.get("movepool", []), | |
"timestamp": timestamp | |
} | |
# Add generation metadata if provided | |
if generation_data: | |
core_data["generation_data"] = generation_data | |
# Create deterministic JSON string (sorted keys) | |
data_string = json.dumps(core_data, sort_keys=True, separators=(',', ':')) | |
# Create HMAC signature | |
signature = hmac.new( | |
SECRET_KEY.encode('utf-8'), | |
data_string.encode('utf-8'), | |
hashlib.sha256 | |
).hexdigest() | |
return signature | |
def verify_signature(piclet_data: dict, provided_signature: str, timestamp: int, generation_data: dict = None) -> bool: | |
""" | |
Verify a Piclet's signature | |
Returns True if signature is valid | |
""" | |
try: | |
expected_signature = PicletVerification.create_signature(piclet_data, timestamp, generation_data) | |
return hmac.compare_digest(expected_signature, provided_signature) | |
except Exception as e: | |
print(f"Signature verification error: {e}") | |
return False | |
def create_verification_data(piclet_data: dict, image_caption: str = None, concept_string: str = None) -> dict: | |
""" | |
Create complete verification data for a Piclet | |
Includes signature, timestamp, and generation metadata | |
""" | |
timestamp = int(time.time()) | |
# Generation metadata | |
generation_data = { | |
"image_caption": image_caption or "", | |
"concept_string": concept_string or "", | |
"generation_method": "official_app" | |
} | |
# Create signature | |
signature = PicletVerification.create_signature(piclet_data, timestamp, generation_data) | |
return { | |
"signature": signature, | |
"timestamp": timestamp, | |
"generation_data": generation_data, | |
"verification_version": "1.0" | |
} | |
def is_verified_piclet(piclet_data: dict) -> Tuple[bool, str]: | |
""" | |
Check if a Piclet has valid verification | |
Returns: (is_verified, status_message) | |
""" | |
try: | |
# Check for verification data | |
if "verification" not in piclet_data: | |
return False, "No verification data found" | |
verification = piclet_data["verification"] | |
required_fields = ["signature", "timestamp", "generation_data"] | |
for field in required_fields: | |
if field not in verification: | |
return False, f"Missing verification field: {field}" | |
# Verify signature | |
is_valid = PicletVerification.verify_signature( | |
piclet_data, | |
verification["signature"], | |
verification["timestamp"], | |
verification["generation_data"] | |
) | |
if not is_valid: | |
return False, "Invalid signature - Piclet may be modified or fake" | |
# Check timestamp (reject if too old - 24 hours) | |
current_time = int(time.time()) | |
piclet_time = verification["timestamp"] | |
# Allow some flexibility for testing (24 hours) | |
if current_time - piclet_time > 86400: # 24 hours | |
return False, "Piclet signature is too old (>24 hours)" | |
return True, "Verified authentic Piclet" | |
except Exception as e: | |
return False, f"Verification error: {str(e)}" | |
class PicletStorage: | |
"""Handles saving and retrieving Piclet data using file-based storage""" | |
def save_piclet(piclet_data: dict, image_file=None) -> Tuple[bool, str]: | |
""" | |
Save a Piclet with optional image file | |
Returns: (success, piclet_id_or_error_message) | |
""" | |
try: | |
# Generate unique ID | |
piclet_id = str(uuid.uuid4()) | |
# Add metadata | |
piclet_data["id"] = piclet_id | |
piclet_data["created_at"] = datetime.now().isoformat() | |
# Handle image file if provided | |
if image_file is not None: | |
# Generate image filename | |
image_ext = Path(image_file.name).suffix if hasattr(image_file, 'name') else '.png' | |
image_filename = f"{piclet_id}{image_ext}" | |
image_path = IMAGES_DIR / image_filename | |
# Save image file | |
if hasattr(image_file, 'save'): | |
image_file.save(str(image_path)) | |
else: | |
# Handle different image input types | |
with open(image_path, 'wb') as f: | |
if hasattr(image_file, 'read'): | |
f.write(image_file.read()) | |
else: | |
f.write(image_file) | |
piclet_data["image_filename"] = image_filename | |
# Save Piclet data | |
piclet_file = PICLETS_DIR / f"{piclet_id}.json" | |
with open(piclet_file, 'w') as f: | |
json.dump(piclet_data, f, indent=2) | |
return True, piclet_id | |
except Exception as e: | |
return False, f"Error saving Piclet: {str(e)}" | |
def get_piclet(piclet_id: str) -> Tuple[bool, dict]: | |
""" | |
Retrieve a Piclet by ID | |
Returns: (success, piclet_data_or_error_message) | |
""" | |
try: | |
piclet_file = PICLETS_DIR / f"{piclet_id}.json" | |
if not piclet_file.exists(): | |
return False, {"error": "Piclet not found"} | |
with open(piclet_file, 'r') as f: | |
piclet_data = json.load(f) | |
return True, piclet_data | |
except Exception as e: | |
return False, {"error": f"Error retrieving Piclet: {str(e)}"} | |
def list_piclets(limit: int = 50) -> Tuple[bool, List[dict]]: | |
""" | |
List all saved Piclets with basic info | |
Returns: (success, list_of_piclet_summaries) | |
""" | |
try: | |
piclets = [] | |
piclet_files = list(PICLETS_DIR.glob("*.json")) | |
# Sort by creation time (newest first) | |
piclet_files.sort(key=lambda x: x.stat().st_mtime, reverse=True) | |
for piclet_file in piclet_files[:limit]: | |
try: | |
with open(piclet_file, 'r') as f: | |
data = json.load(f) | |
# Create summary | |
summary = { | |
"id": data.get("id"), | |
"name": data.get("name", "Unnamed Piclet"), | |
"primaryType": data.get("primaryType"), | |
"tier": data.get("tier"), | |
"created_at": data.get("created_at"), | |
"has_image": "image_filename" in data, | |
"verified": data.get("verified", False) | |
} | |
piclets.append(summary) | |
except Exception as e: | |
print(f"Error reading {piclet_file}: {e}") | |
continue | |
return True, piclets | |
except Exception as e: | |
return False, [{"error": f"Error listing Piclets: {str(e)}"}] | |
def get_piclet_image(piclet_id: str): | |
"""Get the image file for a Piclet""" | |
try: | |
# First get the Piclet data to find image filename | |
success, piclet_data = PicletStorage.get_piclet(piclet_id) | |
if not success: | |
return None | |
if "image_filename" not in piclet_data: | |
return None | |
image_path = IMAGES_DIR / piclet_data["image_filename"] | |
if image_path.exists(): | |
return str(image_path) | |
return None | |
except Exception as e: | |
print(f"Error getting image for {piclet_id}: {e}") | |
return None | |
def save_piclet_api(piclet_json: str, signature: str = "", image_file=None) -> str: | |
""" | |
API endpoint to save a Piclet | |
""" | |
try: | |
# Parse the JSON data | |
piclet_data = json.loads(piclet_json) | |
# Validate required fields | |
required_fields = ["name", "primaryType", "baseStats", "movepool"] | |
for field in required_fields: | |
if field not in piclet_data: | |
return json.dumps({ | |
"success": False, | |
"error": f"Missing required field: {field}" | |
}) | |
# Verify Piclet authenticity | |
is_verified, verification_message = PicletVerification.is_verified_piclet(piclet_data) | |
if not is_verified: | |
return json.dumps({ | |
"success": False, | |
"error": f"Verification failed: {verification_message}", | |
"verification_required": True | |
}) | |
# Add verification status to stored data | |
piclet_data["verified"] = True | |
piclet_data["verification_message"] = verification_message | |
# Save the Piclet | |
success, result = PicletStorage.save_piclet(piclet_data, image_file) | |
if success: | |
return json.dumps({ | |
"success": True, | |
"piclet_id": result, | |
"message": "Verified Piclet saved successfully", | |
"verified": True | |
}) | |
else: | |
return json.dumps({ | |
"success": False, | |
"error": result | |
}) | |
except json.JSONDecodeError: | |
return json.dumps({ | |
"success": False, | |
"error": "Invalid JSON format" | |
}) | |
except Exception as e: | |
return json.dumps({ | |
"success": False, | |
"error": f"Unexpected error: {str(e)}" | |
}) | |
def get_piclet_api(piclet_id: str) -> str: | |
""" | |
API endpoint to retrieve a Piclet by ID | |
""" | |
try: | |
if not piclet_id.strip(): | |
return json.dumps({ | |
"success": False, | |
"error": "Piclet ID is required" | |
}) | |
success, result = PicletStorage.get_piclet(piclet_id.strip()) | |
if success: | |
return json.dumps({ | |
"success": True, | |
"piclet": result | |
}) | |
else: | |
return json.dumps({ | |
"success": False, | |
"error": result.get("error", "Unknown error") | |
}) | |
except Exception as e: | |
return json.dumps({ | |
"success": False, | |
"error": f"Unexpected error: {str(e)}" | |
}) | |
def list_piclets_api(limit: int = 50) -> str: | |
""" | |
API endpoint to list all saved Piclets | |
""" | |
try: | |
limit = max(1, min(limit, 100)) # Limit between 1 and 100 | |
success, result = PicletStorage.list_piclets(limit) | |
if success: | |
return json.dumps({ | |
"success": True, | |
"piclets": result, | |
"count": len(result) | |
}) | |
else: | |
return json.dumps({ | |
"success": False, | |
"error": "Failed to list Piclets" | |
}) | |
except Exception as e: | |
return json.dumps({ | |
"success": False, | |
"error": f"Unexpected error: {str(e)}" | |
}) | |
def get_piclet_image_api(piclet_id: str): | |
""" | |
API endpoint to get a Piclet's image | |
""" | |
try: | |
if not piclet_id.strip(): | |
return None | |
image_path = PicletStorage.get_piclet_image(piclet_id.strip()) | |
return image_path | |
except Exception as e: | |
print(f"Error in get_piclet_image_api: {e}") | |
return None | |
def sign_piclet_api(piclet_json: str, image_caption: str = "", concept_string: str = "") -> str: | |
""" | |
API endpoint to sign a Piclet (for static frontend integration) | |
This allows static sites to generate verified Piclets without exposing the secret key | |
""" | |
try: | |
# Parse the JSON data | |
piclet_data = json.loads(piclet_json) | |
# Validate required fields | |
required_fields = ["name", "primaryType", "baseStats", "movepool"] | |
for field in required_fields: | |
if field not in piclet_data: | |
return json.dumps({ | |
"success": False, | |
"error": f"Missing required field: {field}" | |
}) | |
# Create verification data using server's secret key | |
verification_data = PicletVerification.create_verification_data( | |
piclet_data, | |
image_caption=image_caption, | |
concept_string=concept_string | |
) | |
# Add verification to Piclet data | |
verified_piclet = {**piclet_data, "verification": verification_data} | |
return json.dumps({ | |
"success": True, | |
"verified_piclet": verified_piclet, | |
"signature": verification_data["signature"], | |
"message": "Piclet signed successfully - ready for storage" | |
}) | |
except json.JSONDecodeError: | |
return json.dumps({ | |
"success": False, | |
"error": "Invalid JSON format" | |
}) | |
except Exception as e: | |
return json.dumps({ | |
"success": False, | |
"error": f"Signing error: {str(e)}" | |
}) | |
def sign_and_save_piclet_api(piclet_json: str, image_caption: str = "", concept_string: str = "", image_file=None) -> str: | |
""" | |
API endpoint to sign AND save a Piclet in one step (convenience method) | |
Perfect for static frontends - no secret key needed on client side | |
""" | |
try: | |
# First, sign the Piclet | |
sign_result_json = sign_piclet_api(piclet_json, image_caption, concept_string) | |
sign_result = json.loads(sign_result_json) | |
if not sign_result["success"]: | |
return sign_result_json | |
# Then save the verified Piclet | |
verified_piclet_json = json.dumps(sign_result["verified_piclet"]) | |
save_result_json = save_piclet_api(verified_piclet_json, sign_result["signature"], image_file) | |
save_result = json.loads(save_result_json) | |
if save_result["success"]: | |
return json.dumps({ | |
"success": True, | |
"piclet_id": save_result["piclet_id"], | |
"message": "Piclet signed and saved successfully", | |
"verified": True, | |
"signature": sign_result["signature"] | |
}) | |
else: | |
return save_result_json | |
except Exception as e: | |
return json.dumps({ | |
"success": False, | |
"error": f"Sign and save error: {str(e)}" | |
}) | |
# Create example Piclet data for testing | |
def create_example_piclet() -> str: | |
"""Create an example Piclet for testing""" | |
example_piclet = { | |
"name": "Stellar Wolf", | |
"description": "A majestic wolf creature infused with cosmic energy", | |
"tier": "medium", | |
"primaryType": "space", | |
"secondaryType": "beast", | |
"baseStats": { | |
"hp": 85, | |
"attack": 90, | |
"defense": 70, | |
"speed": 80 | |
}, | |
"nature": "Bold", | |
"specialAbility": { | |
"name": "Cosmic Howl", | |
"description": "Increases attack when health is low" | |
}, | |
"movepool": [ | |
{ | |
"name": "Star Strike", | |
"type": "space", | |
"power": 75, | |
"accuracy": 90, | |
"pp": 15, | |
"priority": 0, | |
"flags": [], | |
"effects": [ | |
{ | |
"type": "damage", | |
"target": "opponent", | |
"amount": "normal" | |
} | |
] | |
}, | |
{ | |
"name": "Lunar Bite", | |
"type": "beast", | |
"power": 80, | |
"accuracy": 85, | |
"pp": 12, | |
"priority": 0, | |
"flags": ["contact", "bite"], | |
"effects": [ | |
{ | |
"type": "damage", | |
"target": "opponent", | |
"amount": "normal" | |
} | |
] | |
} | |
] | |
} | |
return json.dumps(example_piclet, indent=2) | |
# Create the Gradio interface | |
with gr.Blocks(title="Piclets Server API", theme=gr.themes.Soft()) as app: | |
gr.Markdown(""" | |
# 🦀 Piclets Server API | |
Backend server for saving and sharing Piclets (AI-generated creatures) from the Piclets game. | |
## Quick Start | |
1. **Save a Piclet**: Use the JSON format below with optional image upload | |
2. **Retrieve a Piclet**: Enter a Piclet ID to get the full data | |
3. **List Piclets**: See all saved Piclets with summaries | |
4. **Get Images**: Retrieve the original image for any Piclet | |
""") | |
with gr.Tabs(): | |
# Sign and Save Tab (For Static Frontends) | |
with gr.Tab("✍️ Sign & Save Piclet"): | |
gr.Markdown("### Sign and Save a Piclet (One Step - Perfect for Static Sites)") | |
gr.Markdown("🌟 **For Static Frontends**: No secret key needed! Server signs your Piclet automatically.") | |
with gr.Row(): | |
with gr.Column(scale=2): | |
unsigned_json_input = gr.Textbox( | |
label="Unsigned Piclet JSON Data", | |
placeholder="Paste your unsigned Piclet JSON here...", | |
lines=12, | |
value=create_example_piclet() | |
) | |
with gr.Row(): | |
caption_input = gr.Textbox( | |
label="Image Caption", | |
placeholder="AI-generated image description...", | |
lines=2 | |
) | |
concept_input = gr.Textbox( | |
label="Concept String", | |
placeholder="Creature concept from AI...", | |
lines=2 | |
) | |
with gr.Column(scale=1): | |
sign_save_image_input = gr.File( | |
label="Piclet Image (Optional)", | |
file_types=["image"] | |
) | |
sign_save_btn = gr.Button("✍️ Sign & Save Piclet", variant="primary") | |
sign_save_output = gr.JSON(label="Sign & Save Result") | |
sign_save_btn.click( | |
fn=sign_and_save_piclet_api, | |
inputs=[unsigned_json_input, caption_input, concept_input, sign_save_image_input], | |
outputs=sign_save_output | |
) | |
# Sign Only Tab | |
with gr.Tab("🔏 Sign Only"): | |
gr.Markdown("### Sign a Piclet (Two-Step Process)") | |
gr.Markdown("📝 **Advanced**: Sign first, then save separately. Useful for batch operations.") | |
with gr.Row(): | |
with gr.Column(scale=2): | |
sign_json_input = gr.Textbox( | |
label="Unsigned Piclet JSON Data", | |
placeholder="Paste your unsigned Piclet JSON here...", | |
lines=10, | |
value=create_example_piclet() | |
) | |
with gr.Row(): | |
sign_caption_input = gr.Textbox( | |
label="Image Caption", | |
placeholder="AI-generated image description..." | |
) | |
sign_concept_input = gr.Textbox( | |
label="Concept String", | |
placeholder="Creature concept from AI..." | |
) | |
with gr.Column(scale=2): | |
sign_btn = gr.Button("🔏 Sign Piclet", variant="secondary") | |
sign_output = gr.JSON(label="Signing Result") | |
sign_btn.click( | |
fn=sign_piclet_api, | |
inputs=[sign_json_input, sign_caption_input, sign_concept_input], | |
outputs=sign_output | |
) | |
# Save Piclet Tab (For Pre-Signed) | |
with gr.Tab("💾 Save Pre-Signed"): | |
gr.Markdown("### Save a Pre-Signed Piclet") | |
gr.Markdown("⚠️ **For Advanced Users**: Save a Piclet that was already signed elsewhere.") | |
with gr.Row(): | |
with gr.Column(scale=2): | |
piclet_json_input = gr.Textbox( | |
label="Piclet JSON Data", | |
placeholder="Paste your Piclet JSON here...", | |
lines=15, | |
value=create_example_piclet() | |
) | |
with gr.Column(scale=1): | |
image_input = gr.File( | |
label="Piclet Image (Optional)", | |
file_types=["image"] | |
) | |
save_btn = gr.Button("💾 Save Piclet", variant="primary") | |
save_output = gr.JSON(label="Save Result") | |
save_btn.click( | |
fn=save_piclet_api, | |
inputs=[piclet_json_input, image_input], | |
outputs=save_output | |
) | |
# Get Piclet Tab | |
with gr.Tab("🔍 Get Piclet"): | |
gr.Markdown("### Retrieve a Piclet by ID") | |
with gr.Row(): | |
with gr.Column(scale=2): | |
piclet_id_input = gr.Textbox( | |
label="Piclet ID", | |
placeholder="Enter Piclet ID here..." | |
) | |
get_btn = gr.Button("🔍 Get Piclet", variant="primary") | |
with gr.Column(scale=2): | |
get_output = gr.JSON(label="Piclet Data") | |
get_btn.click( | |
fn=get_piclet_api, | |
inputs=piclet_id_input, | |
outputs=get_output | |
) | |
# List Piclets Tab | |
with gr.Tab("📋 List Piclets"): | |
gr.Markdown("### Browse all saved Piclets") | |
with gr.Row(): | |
with gr.Column(scale=1): | |
limit_input = gr.Slider( | |
label="Max Results", | |
minimum=1, | |
maximum=100, | |
value=20, | |
step=1 | |
) | |
list_btn = gr.Button("📋 List Piclets", variant="primary") | |
with gr.Column(scale=3): | |
list_output = gr.JSON(label="Piclets List") | |
list_btn.click( | |
fn=list_piclets_api, | |
inputs=limit_input, | |
outputs=list_output | |
) | |
# Get Image Tab | |
with gr.Tab("🖼️ Get Image"): | |
gr.Markdown("### Retrieve a Piclet's image") | |
with gr.Row(): | |
with gr.Column(scale=1): | |
image_id_input = gr.Textbox( | |
label="Piclet ID", | |
placeholder="Enter Piclet ID here..." | |
) | |
get_image_btn = gr.Button("🖼️ Get Image", variant="primary") | |
with gr.Column(scale=2): | |
image_output = gr.Image(label="Piclet Image") | |
get_image_btn.click( | |
fn=get_piclet_image_api, | |
inputs=image_id_input, | |
outputs=image_output | |
) | |
# Generate Verified Example Tab | |
with gr.Tab("✅ Generate Verified Example"): | |
gr.Markdown(""" | |
### Generate a Verified Example Piclet | |
This creates a properly signed example Piclet that will pass verification. | |
Use this to test the verification system or as a template. | |
""") | |
generate_btn = gr.Button("✅ Generate Verified Example", variant="primary") | |
verified_output = gr.Textbox( | |
label="Verified Piclet JSON", | |
lines=20, | |
show_copy_button=True | |
) | |
generate_btn.click( | |
fn=create_verified_example_api, | |
outputs=verified_output | |
) | |
# API Documentation Tab | |
with gr.Tab("📖 API Documentation & Verification"): | |
gr.Markdown(""" | |
## 🔐 Verification System | |
**All Piclets must be verified to prevent fake or modified creatures.** | |
### How Verification Works: | |
1. **HMAC-SHA256 Signature**: Each Piclet is signed with a secret key | |
2. **Timestamp Validation**: Signatures expire after 24 hours | |
3. **Generation Metadata**: Includes image caption and concept string | |
4. **Tamper Detection**: Any modification invalidates the signature | |
### Required Verification Format: | |
```json | |
{ | |
"verification": { | |
"signature": "abc123...", | |
"timestamp": 1690123456, | |
"generation_data": { | |
"image_caption": "AI-generated description", | |
"concept_string": "Creature concept", | |
"generation_method": "official_app" | |
}, | |
"verification_version": "1.0" | |
} | |
} | |
``` | |
## API Endpoints | |
### 1. Sign & Save Piclet (Recommended for Static Sites) | |
- **Function**: `sign_and_save_piclet_api(piclet_json, image_caption, concept_string, image_file=None)` | |
- **Input**: Unsigned Piclet JSON, image caption, concept string, optional image file | |
- **Output**: JSON response with success status and Piclet ID | |
- **✅ Perfect for**: Static sites (no secret key needed) | |
### 2. Sign Only | |
- **Function**: `sign_piclet_api(piclet_json, image_caption, concept_string)` | |
- **Input**: Unsigned Piclet JSON, image caption, concept string | |
- **Output**: JSON response with verified Piclet and signature | |
- **Use case**: Two-step process, batch operations | |
### 3. Save Pre-Signed Piclet | |
- **Function**: `save_piclet_api(piclet_json, signature, image_file=None)` | |
- **Input**: Signed Piclet JSON, signature string, optional image file | |
- **Output**: JSON response with success status and Piclet ID | |
- **⚠️ Requires**: Valid verification signature | |
### 2. Get Piclet | |
- **Function**: `get_piclet_api(piclet_id)` | |
- **Input**: Piclet ID string | |
- **Output**: JSON response with full Piclet data | |
### 3. List Piclets | |
- **Function**: `list_piclets_api(limit=50)` | |
- **Input**: Maximum number of results (1-100) | |
- **Output**: JSON response with Piclet summaries | |
### 4. Get Image | |
- **Function**: `get_piclet_image_api(piclet_id)` | |
- **Input**: Piclet ID string | |
- **Output**: Image file path or None | |
## Required JSON Format | |
```json | |
{ | |
"name": "Piclet Name", | |
"description": "Description of the creature", | |
"tier": "low|medium|high|legendary", | |
"primaryType": "beast|bug|aquatic|flora|mineral|space|machina|structure|culture|cuisine", | |
"secondaryType": "same options as primaryType or null", | |
"baseStats": { | |
"hp": 85, | |
"attack": 90, | |
"defense": 70, | |
"speed": 80 | |
}, | |
"nature": "Personality trait", | |
"specialAbility": { | |
"name": "Ability Name", | |
"description": "What the ability does" | |
}, | |
"movepool": [ | |
{ | |
"name": "Move Name", | |
"type": "attack type", | |
"power": 75, | |
"accuracy": 90, | |
"pp": 15, | |
"priority": 0, | |
"flags": ["contact", "bite", etc.], | |
"effects": [ | |
{ | |
"type": "damage|heal|modifyStats|etc.", | |
"target": "opponent|self|all", | |
"amount": "weak|normal|strong|extreme" | |
} | |
] | |
} | |
] | |
} | |
``` | |
## Frontend Integration (JavaScript) | |
### Include Verification Helper | |
```html | |
<!-- Include crypto-js for HMAC generation --> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"></script> | |
<script src="verification_helper.js"></script> | |
``` | |
### Static Site Usage (Recommended) | |
```javascript | |
// Connect to this Gradio space | |
const client = await window.gradioClient.Client.connect("your-space-name"); | |
// Sign and save in one step (NO SECRET KEY NEEDED!) | |
const result = await client.predict("/sign_and_save_piclet_api", [ | |
JSON.stringify(picletData), // Unsigned Piclet data | |
imageCaption, // AI-generated caption | |
conceptString, // Creature concept | |
imageFile // Optional image | |
]); | |
const response = JSON.parse(result.data[0]); | |
if (response.success) { | |
console.log(`Piclet saved with ID: ${response.piclet_id}`); | |
} else { | |
console.error(`Save failed: ${response.error}`); | |
} | |
// Two-step process (sign first, then save) | |
const signResult = await client.predict("/sign_piclet_api", [ | |
JSON.stringify(picletData), | |
imageCaption, | |
conceptString | |
]); | |
const signResponse = JSON.parse(signResult.data[0]); | |
if (signResponse.success) { | |
const saveResult = await client.predict("/save_piclet_api", [ | |
JSON.stringify(signResponse.verified_piclet), | |
signResponse.signature, | |
imageFile | |
]); | |
} | |
``` | |
### Secret Key Management (Server Only) | |
```bash | |
# Set environment variable on server (HuggingFace Spaces) | |
PICLET_SECRET_KEY=your-super-secret-64-char-hex-key | |
# Generate secure key: | |
openssl rand -hex 32 | |
# ✅ Frontend doesn't need the secret key anymore! | |
# Server handles all signing securely | |
``` | |
## Storage Structure | |
- **Data Directory**: `./data/` | |
- **Piclets**: `./data/piclets/*.json` | |
- **Images**: `./data/images/*` | |
- **Collections**: `./data/collections/*.json` (future feature) | |
All data is stored locally in the Hugging Face Space persistent storage. | |
## Security Features | |
### ✅ Verified Piclets | |
- Generated through official app only | |
- Cryptographically signed with HMAC-SHA256 | |
- Include generation metadata (image caption, concept) | |
- Tamper-proof (any modification breaks signature) | |
### ❌ Rejected Piclets | |
- Missing verification data | |
- Invalid or expired signatures | |
- Modified after generation | |
- Created outside official app | |
### Environment Variables | |
- `PICLET_SECRET_KEY`: Secret key for verification (change in production) | |
### Files Included | |
- `app.py`: Main server application | |
- `verification_helper.js`: Frontend helper functions | |
- `requirements.txt`: Python dependencies | |
- `API_DOCUMENTATION.md`: Complete documentation | |
""") | |
# Launch the app | |
if __name__ == "__main__": | |
app.launch( | |
server_name="0.0.0.0", | |
server_port=7860, | |
share=False | |
) |