Add Sketchfab integration
This commit is contained in:
parent
972096e9dc
commit
8b7e89fbcd
271
addon.py
271
addon.py
@ -11,6 +11,7 @@ import tempfile
|
||||
import traceback
|
||||
import os
|
||||
import shutil
|
||||
import zipfile
|
||||
from bpy.props import StringProperty, IntProperty, BoolProperty, EnumProperty
|
||||
import io
|
||||
from contextlib import redirect_stdout
|
||||
@ -200,6 +201,7 @@ class BlenderMCPServer:
|
||||
"execute_code": self.execute_code,
|
||||
"get_polyhaven_status": self.get_polyhaven_status,
|
||||
"get_hyper3d_status": self.get_hyper3d_status,
|
||||
"get_sketchfab_status": self.get_sketchfab_status,
|
||||
}
|
||||
|
||||
# Add Polyhaven handlers only if enabled
|
||||
@ -220,6 +222,14 @@ class BlenderMCPServer:
|
||||
"import_generated_asset": self.import_generated_asset,
|
||||
}
|
||||
handlers.update(polyhaven_handlers)
|
||||
|
||||
# Add Sketchfab handlers only if enabled
|
||||
if bpy.context.scene.blendermcp_use_sketchfab:
|
||||
sketchfab_handlers = {
|
||||
"search_sketchfab_models": self.search_sketchfab_models,
|
||||
"download_sketchfab_model": self.download_sketchfab_model,
|
||||
}
|
||||
handlers.update(sketchfab_handlers)
|
||||
|
||||
handler = handlers.get(cmd_type)
|
||||
if handler:
|
||||
@ -1373,6 +1383,248 @@ class BlenderMCPServer:
|
||||
return {"succeed": False, "error": str(e)}
|
||||
#endregion
|
||||
|
||||
#region Sketchfab API
|
||||
def get_sketchfab_status(self):
|
||||
"""Get the current status of Sketchfab integration"""
|
||||
enabled = bpy.context.scene.blendermcp_use_sketchfab
|
||||
api_key = bpy.context.scene.blendermcp_sketchfab_api_key
|
||||
|
||||
# Test the API key if present
|
||||
if api_key:
|
||||
try:
|
||||
headers = {
|
||||
"Authorization": f"Token {api_key}"
|
||||
}
|
||||
|
||||
response = requests.get(
|
||||
"https://api.sketchfab.com/v3/me",
|
||||
headers=headers
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
user_data = response.json()
|
||||
username = user_data.get("username", "Unknown user")
|
||||
return {
|
||||
"enabled": True,
|
||||
"message": f"Sketchfab integration is enabled and ready to use. Logged in as: {username}"
|
||||
}
|
||||
else:
|
||||
print(f"API key test failed with status code {response.status_code}: {response.text}")
|
||||
return {
|
||||
"enabled": False,
|
||||
"message": f"Sketchfab API key seems invalid. Status code: {response.status_code}"
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"Error testing Sketchfab API key: {str(e)}")
|
||||
return {
|
||||
"enabled": False,
|
||||
"message": f"Error testing Sketchfab API key: {str(e)}"
|
||||
}
|
||||
|
||||
if enabled and api_key:
|
||||
return {"enabled": True, "message": "Sketchfab integration is enabled and ready to use."}
|
||||
elif enabled and not api_key:
|
||||
return {
|
||||
"enabled": False,
|
||||
"message": """Sketchfab integration is currently enabled, but API key is not given. To enable it:
|
||||
1. In the 3D Viewport, find the BlenderMCP panel in the sidebar (press N if hidden)
|
||||
2. Keep the 'Use Sketchfab' checkbox checked
|
||||
3. Enter your Sketchfab API Key
|
||||
4. Restart the connection to Claude"""
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"enabled": False,
|
||||
"message": """Sketchfab integration is currently disabled. To enable it:
|
||||
1. In the 3D Viewport, find the BlenderMCP panel in the sidebar (press N if hidden)
|
||||
2. Check the 'Use assets from Sketchfab' checkbox
|
||||
3. Enter your Sketchfab API Key
|
||||
4. Restart the connection to Claude"""
|
||||
}
|
||||
|
||||
def search_sketchfab_models(self, query, categories=None, count=20, downloadable=True):
|
||||
"""Search for models on Sketchfab based on query and optional filters"""
|
||||
try:
|
||||
api_key = bpy.context.scene.blendermcp_sketchfab_api_key
|
||||
if not api_key:
|
||||
return {"error": "Sketchfab API key is not configured"}
|
||||
|
||||
print(f"Searching Sketchfab with query: {query}, categories: {categories}, count: {count}, downloadable: {downloadable}")
|
||||
|
||||
# Build search parameters with exact fields from Sketchfab API docs
|
||||
params = {
|
||||
"type": "models",
|
||||
"q": query,
|
||||
"count": count,
|
||||
"downloadable": downloadable,
|
||||
"archives_flavours": False
|
||||
}
|
||||
|
||||
if categories:
|
||||
params["categories"] = categories
|
||||
|
||||
# Make API request to Sketchfab search endpoint
|
||||
# The proper format according to Sketchfab API docs for API key auth
|
||||
headers = {
|
||||
"Authorization": f"Token {api_key}"
|
||||
}
|
||||
|
||||
print(f"Making request to Sketchfab API search endpoint with params: {params}")
|
||||
|
||||
# Use the search endpoint as specified in the API documentation
|
||||
response = requests.get(
|
||||
"https://api.sketchfab.com/v3/search",
|
||||
headers=headers,
|
||||
params=params
|
||||
)
|
||||
|
||||
if response.status_code == 401:
|
||||
print(f"Authentication failed with status code 401. Check your API key.")
|
||||
return {"error": f"Authentication failed (401). Check your API key."}
|
||||
|
||||
if response.status_code != 200:
|
||||
print(f"API request failed with status code {response.status_code}: {response.text}")
|
||||
return {"error": f"API request failed with status code {response.status_code}: {response.text}"}
|
||||
|
||||
response_data = response.json()
|
||||
print(f"API response received with status code {response.status_code}")
|
||||
|
||||
# Safety check on the response structure
|
||||
if response_data is None:
|
||||
return {"error": "Received empty response from Sketchfab API"}
|
||||
|
||||
# Handle 'results' potentially missing from response
|
||||
results = response_data.get("results", [])
|
||||
if not isinstance(results, list):
|
||||
return {"error": f"Unexpected response format from Sketchfab API: {response_data}"}
|
||||
|
||||
return response_data
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"JSON decoding error: {str(e)}. Response text: {response.text if 'response' in locals() else 'No response'}")
|
||||
return {"error": f"Invalid JSON response from Sketchfab API: {str(e)}"}
|
||||
except Exception as e:
|
||||
print(f"Error in search_sketchfab_models: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return {"error": str(e)}
|
||||
|
||||
def download_sketchfab_model(self, uid):
|
||||
"""Download a model from Sketchfab by its UID"""
|
||||
try:
|
||||
api_key = bpy.context.scene.blendermcp_sketchfab_api_key
|
||||
if not api_key:
|
||||
return {"error": "Sketchfab API key is not configured"}
|
||||
|
||||
print(f"Attempting to download Sketchfab model with UID: {uid}")
|
||||
|
||||
# Use proper authorization header for API key auth
|
||||
headers = {
|
||||
"Authorization": f"Token {api_key}"
|
||||
}
|
||||
|
||||
print(f"Making download request to Sketchfab API with UID: {uid}")
|
||||
|
||||
# Request download URL using the exact endpoint from the documentation
|
||||
download_endpoint = f"https://api.sketchfab.com/v3/models/{uid}/download"
|
||||
print(f"Download endpoint: {download_endpoint}")
|
||||
|
||||
response = requests.get(
|
||||
download_endpoint,
|
||||
headers=headers
|
||||
)
|
||||
|
||||
if response.status_code == 401:
|
||||
print(f"Authentication failed with status code 401. Check your API key.")
|
||||
return {"error": f"Authentication failed (401). Check your API key."}
|
||||
|
||||
if response.status_code != 200:
|
||||
print(f"Download request failed with status code {response.status_code}: {response.text}")
|
||||
return {"error": f"Download request failed with status code {response.status_code}: {response.text}"}
|
||||
|
||||
data = response.json()
|
||||
|
||||
# Safety check for None data
|
||||
if data is None:
|
||||
return {"error": "Received empty response from Sketchfab API for download request"}
|
||||
|
||||
print(f"Download response data: {data}")
|
||||
|
||||
# Extract download URL with safety checks
|
||||
gltf_data = data.get("gltf")
|
||||
if not gltf_data:
|
||||
print(f"No gltf data in response: {data}")
|
||||
return {"error": "No gltf download URL available for this model. Response: " + str(data)}
|
||||
|
||||
download_url = gltf_data.get("url")
|
||||
if not download_url:
|
||||
print(f"No URL in gltf data: {gltf_data}")
|
||||
return {"error": "No download URL available for this model. Make sure the model is downloadable and you have access."}
|
||||
|
||||
print(f"Download URL obtained: {download_url}")
|
||||
|
||||
# Download the model
|
||||
model_response = requests.get(download_url)
|
||||
|
||||
if model_response.status_code != 200:
|
||||
print(f"Model download failed with status code {model_response.status_code}")
|
||||
return {"error": f"Model download failed with status code {model_response.status_code}"}
|
||||
|
||||
# Save to temporary file
|
||||
temp_dir = tempfile.mkdtemp()
|
||||
file_path = os.path.join(temp_dir, f"{uid}.zip")
|
||||
|
||||
print(f"Saving downloaded model to temporary file: {file_path}")
|
||||
|
||||
with open(file_path, "wb") as f:
|
||||
f.write(model_response.content)
|
||||
|
||||
# Extract the zip file
|
||||
print(f"Extracting zip file to: {temp_dir}")
|
||||
with zipfile.ZipFile(file_path, 'r') as zip_ref:
|
||||
zip_ref.extractall(temp_dir)
|
||||
|
||||
# Find the main glTF file
|
||||
gltf_files = [f for f in os.listdir(temp_dir) if f.endswith('.gltf') or f.endswith('.glb')]
|
||||
|
||||
if not gltf_files:
|
||||
print(f"No glTF file found in the downloaded model. Directory contents: {os.listdir(temp_dir)}")
|
||||
return {"error": "No glTF file found in the downloaded model"}
|
||||
|
||||
main_file = os.path.join(temp_dir, gltf_files[0])
|
||||
print(f"Found main glTF file: {main_file}")
|
||||
|
||||
# Import the model
|
||||
print(f"Importing model using Blender's glTF importer")
|
||||
bpy.ops.import_scene.gltf(filepath=main_file)
|
||||
|
||||
# Get the names of imported objects
|
||||
imported_objects = [obj.name for obj in bpy.context.selected_objects]
|
||||
print(f"Imported objects: {', '.join(imported_objects)}")
|
||||
|
||||
# Clean up temporary files
|
||||
try:
|
||||
shutil.rmtree(temp_dir)
|
||||
print(f"Temporary directory cleaned up: {temp_dir}")
|
||||
except Exception as e:
|
||||
print(f"Failed to clean up temporary directory: {temp_dir}. Error: {str(e)}")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Model imported successfully",
|
||||
"imported_objects": imported_objects
|
||||
}
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"JSON decoding error: {str(e)}. Response text: {response.text if 'response' in locals() else 'No response'}")
|
||||
return {"error": f"Invalid JSON response from Sketchfab API: {str(e)}"}
|
||||
except Exception as e:
|
||||
print(f"Error in download_sketchfab_model: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return {"error": f"Failed to download model: {str(e)}"}
|
||||
#endregion
|
||||
|
||||
# Blender UI Panel
|
||||
class BLENDERMCP_PT_Panel(bpy.types.Panel):
|
||||
bl_label = "Blender MCP"
|
||||
@ -1394,6 +1646,10 @@ class BLENDERMCP_PT_Panel(bpy.types.Panel):
|
||||
layout.prop(scene, "blendermcp_hyper3d_api_key", text="API Key")
|
||||
layout.operator("blendermcp.set_hyper3d_free_trial_api_key", text="Set Free Trial API Key")
|
||||
|
||||
layout.prop(scene, "blendermcp_use_sketchfab", text="Use assets from Sketchfab")
|
||||
if scene.blendermcp_use_sketchfab:
|
||||
layout.prop(scene, "blendermcp_sketchfab_api_key", text="API Key")
|
||||
|
||||
if not scene.blendermcp_server_running:
|
||||
layout.operator("blendermcp.start_server", text="Connect to MCP server")
|
||||
else:
|
||||
@ -1492,6 +1748,19 @@ def register():
|
||||
default=""
|
||||
)
|
||||
|
||||
bpy.types.Scene.blendermcp_use_sketchfab = bpy.props.BoolProperty(
|
||||
name="Use Sketchfab",
|
||||
description="Enable Sketchfab asset integration",
|
||||
default=False
|
||||
)
|
||||
|
||||
bpy.types.Scene.blendermcp_sketchfab_api_key = bpy.props.StringProperty(
|
||||
name="Sketchfab API Key",
|
||||
subtype="PASSWORD",
|
||||
description="API Key provided by Sketchfab",
|
||||
default=""
|
||||
)
|
||||
|
||||
bpy.utils.register_class(BLENDERMCP_PT_Panel)
|
||||
bpy.utils.register_class(BLENDERMCP_OT_SetFreeTrialHyper3DAPIKey)
|
||||
bpy.utils.register_class(BLENDERMCP_OT_StartServer)
|
||||
@ -1516,6 +1785,8 @@ def unregister():
|
||||
del bpy.types.Scene.blendermcp_use_hyper3d
|
||||
del bpy.types.Scene.blendermcp_hyper3d_mode
|
||||
del bpy.types.Scene.blendermcp_hyper3d_api_key
|
||||
del bpy.types.Scene.blendermcp_use_sketchfab
|
||||
del bpy.types.Scene.blendermcp_sketchfab_api_key
|
||||
|
||||
print("BlenderMCP addon unregistered")
|
||||
|
||||
|
||||
@ -515,6 +515,144 @@ def get_hyper3d_status(ctx: Context) -> str:
|
||||
logger.error(f"Error checking Hyper3D status: {str(e)}")
|
||||
return f"Error checking Hyper3D status: {str(e)}"
|
||||
|
||||
@mcp.tool()
|
||||
def get_sketchfab_status(ctx: Context) -> str:
|
||||
"""
|
||||
Check if Sketchfab integration is enabled in Blender.
|
||||
Returns a message indicating whether Sketchfab features are available.
|
||||
"""
|
||||
try:
|
||||
blender = get_blender_connection()
|
||||
result = blender.send_command("get_sketchfab_status")
|
||||
enabled = result.get("enabled", False)
|
||||
message = result.get("message", "")
|
||||
|
||||
return message
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking Sketchfab status: {str(e)}")
|
||||
return f"Error checking Sketchfab status: {str(e)}"
|
||||
|
||||
@mcp.tool()
|
||||
def search_sketchfab_models(
|
||||
ctx: Context,
|
||||
query: str,
|
||||
categories: str = None,
|
||||
count: int = 20,
|
||||
downloadable: bool = True
|
||||
) -> str:
|
||||
"""
|
||||
Search for models on Sketchfab with optional filtering.
|
||||
|
||||
Parameters:
|
||||
- query: Text to search for
|
||||
- categories: Optional comma-separated list of categories
|
||||
- count: Maximum number of results to return (default 20)
|
||||
- downloadable: Whether to include only downloadable models (default True)
|
||||
|
||||
Returns a formatted list of matching models.
|
||||
"""
|
||||
try:
|
||||
|
||||
blender = get_blender_connection()
|
||||
logger.info(f"Searching Sketchfab models with query: {query}, categories: {categories}, count: {count}, downloadable: {downloadable}")
|
||||
|
||||
result = blender.send_command("search_sketchfab_models", {
|
||||
"query": query,
|
||||
"categories": categories,
|
||||
"count": count,
|
||||
"downloadable": downloadable
|
||||
})
|
||||
|
||||
if "error" in result:
|
||||
logger.error(f"Error from Sketchfab search: {result['error']}")
|
||||
return f"Error: {result['error']}"
|
||||
|
||||
# Safely get results with fallbacks for None
|
||||
if result is None:
|
||||
logger.error("Received None result from Sketchfab search")
|
||||
return "Error: Received no response from Sketchfab search"
|
||||
|
||||
# Format the results
|
||||
models = result.get("results", []) or []
|
||||
if not models:
|
||||
return f"No models found matching '{query}'"
|
||||
|
||||
formatted_output = f"Found {len(models)} models matching '{query}':\n\n"
|
||||
|
||||
for model in models:
|
||||
if model is None:
|
||||
continue
|
||||
|
||||
model_name = model.get("name", "Unnamed model")
|
||||
model_uid = model.get("uid", "Unknown ID")
|
||||
formatted_output += f"- {model_name} (UID: {model_uid})\n"
|
||||
|
||||
# Get user info with safety checks
|
||||
user = model.get("user") or {}
|
||||
username = user.get("username", "Unknown author") if isinstance(user, dict) else "Unknown author"
|
||||
formatted_output += f" Author: {username}\n"
|
||||
|
||||
# Get license info with safety checks
|
||||
license_data = model.get("license") or {}
|
||||
license_label = license_data.get("label", "Unknown") if isinstance(license_data, dict) else "Unknown"
|
||||
formatted_output += f" License: {license_label}\n"
|
||||
|
||||
# Add face count and downloadable status
|
||||
face_count = model.get("faceCount", "Unknown")
|
||||
is_downloadable = "Yes" if model.get("isDownloadable") else "No"
|
||||
formatted_output += f" Face count: {face_count}\n"
|
||||
formatted_output += f" Downloadable: {is_downloadable}\n\n"
|
||||
|
||||
return formatted_output
|
||||
except Exception as e:
|
||||
logger.error(f"Error searching Sketchfab models: {str(e)}")
|
||||
import traceback
|
||||
logger.error(traceback.format_exc())
|
||||
return f"Error searching Sketchfab models: {str(e)}"
|
||||
|
||||
@mcp.tool()
|
||||
def download_sketchfab_model(
|
||||
ctx: Context,
|
||||
uid: str
|
||||
) -> str:
|
||||
"""
|
||||
Download and import a Sketchfab model by its UID.
|
||||
|
||||
Parameters:
|
||||
- uid: The unique identifier of the Sketchfab model
|
||||
|
||||
Returns a message indicating success or failure.
|
||||
The model must be downloadable and you must have proper access rights.
|
||||
"""
|
||||
try:
|
||||
|
||||
blender = get_blender_connection()
|
||||
logger.info(f"Attempting to download Sketchfab model with UID: {uid}")
|
||||
|
||||
result = blender.send_command("download_sketchfab_model", {
|
||||
"uid": uid
|
||||
})
|
||||
|
||||
if result is None:
|
||||
logger.error("Received None result from Sketchfab download")
|
||||
return "Error: Received no response from Sketchfab download request"
|
||||
|
||||
if "error" in result:
|
||||
logger.error(f"Error from Sketchfab download: {result['error']}")
|
||||
return f"Error: {result['error']}"
|
||||
|
||||
if result.get("success"):
|
||||
imported_objects = result.get("imported_objects", [])
|
||||
object_names = ", ".join(imported_objects) if imported_objects else "none"
|
||||
return f"Successfully imported model. Created objects: {object_names}"
|
||||
else:
|
||||
return f"Failed to download model: {result.get('message', 'Unknown error')}"
|
||||
except Exception as e:
|
||||
logger.error(f"Error downloading Sketchfab model: {str(e)}")
|
||||
import traceback
|
||||
logger.error(traceback.format_exc())
|
||||
return f"Error downloading Sketchfab model: {str(e)}"
|
||||
|
||||
def _process_bbox(original_bbox: list[float] | list[int] | None) -> list[int] | None:
|
||||
if original_bbox is None:
|
||||
return None
|
||||
@ -705,7 +843,14 @@ def asset_creation_strategy() -> str:
|
||||
- For objects/models: Use download_polyhaven_asset() with asset_type="models"
|
||||
- For materials/textures: Use download_polyhaven_asset() with asset_type="textures"
|
||||
- For environment lighting: Use download_polyhaven_asset() with asset_type="hdris"
|
||||
2. Hyper3D(Rodin)
|
||||
2. Sketchfab
|
||||
Use get_sketchfab_status() to verify its status
|
||||
If Sketchfab is enabled:
|
||||
- For objects/models: First search using search_sketchfab_models() with your query
|
||||
- Then download specific models using download_sketchfab_model() with the UID
|
||||
- Note that only downloadable models can be accessed, and API key must be properly configured
|
||||
- Sketchfab has a wider variety of models than PolyHaven, especially for specific subjects
|
||||
3. Hyper3D(Rodin)
|
||||
Hyper3D Rodin is good at generating 3D models for single item.
|
||||
So don't try to:
|
||||
1. Generate the whole scene with one shot
|
||||
@ -735,11 +880,17 @@ def asset_creation_strategy() -> str:
|
||||
- Ensure that all objects that should not be clipping are not clipping.
|
||||
- Items have right spatial relationship.
|
||||
|
||||
4. Recommended asset source priority:
|
||||
- For specific existing objects: First try Sketchfab, then PolyHaven
|
||||
- For generic objects/furniture: First try PolyHaven, then Sketchfab
|
||||
- For custom or unique items not available in libraries: Use Hyper3D Rodin
|
||||
- For environment lighting: Use PolyHaven HDRIs
|
||||
- For materials/textures: Use PolyHaven textures
|
||||
|
||||
Only fall back to scripting when:
|
||||
- PolyHaven and Hyper3D are disabled
|
||||
- PolyHaven, Sketchfab, and Hyper3D are all disabled
|
||||
- A simple primitive is explicitly requested
|
||||
- No suitable PolyHaven asset exists
|
||||
- No suitable asset exists in any of the libraries
|
||||
- Hyper3D Rodin failed to generate the desired asset
|
||||
- The task specifically requires a basic material/color
|
||||
"""
|
||||
|
||||
Loading…
Reference in New Issue
Block a user