@hybridlabor-api/bdb-hardware-pcb 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +116 -0
- package/config/mcp/antigravity.json +22 -0
- package/config/mcp/claude.json +22 -0
- package/config/mcp/codex.toml +25 -0
- package/docs/adr/ADR-001-KICAD-OPENSCAD-MCP-STRATEGY.md +334 -0
- package/docs/review_documentation.md +121 -0
- package/installer.js +358 -0
- package/mcp_servers/kicad-mcp-server/.env.example +22 -0
- package/mcp_servers/kicad-mcp-server/.github/workflows/ci.yml +36 -0
- package/mcp_servers/kicad-mcp-server/CLAUDE.md +487 -0
- package/mcp_servers/kicad-mcp-server/README.md +316 -0
- package/mcp_servers/kicad-mcp-server/docs/DEVICE_TREE.md +416 -0
- package/mcp_servers/kicad-mcp-server/docs/INSTALLATION.md +332 -0
- package/mcp_servers/kicad-mcp-server/docs/PIN_ANALYSIS.md +332 -0
- package/mcp_servers/kicad-mcp-server/docs/README.md +240 -0
- package/mcp_servers/kicad-mcp-server/docs/TESTING.md +613 -0
- package/mcp_servers/kicad-mcp-server/docs/VALIDATION.md +268 -0
- package/mcp_servers/kicad-mcp-server/pyproject.toml +96 -0
- package/mcp_servers/kicad-mcp-server/requirements-dev.txt +16 -0
- package/mcp_servers/kicad-mcp-server/requirements-test.txt +24 -0
- package/mcp_servers/kicad-mcp-server/requirements.txt +13 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/__init__.py +3 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/__main__.py +17 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/config.py +46 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/models/__init__.py +1 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/models/types.py +87 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/parsers/__init__.py +1 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/parsers/netlist_parser.py +234 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/parsers/pcb_parser.py +375 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/parsers/pcb_parser_kicad.py +327 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/parsers/schematic_parser.py +902 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/server.py +71 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/__init__.py +1 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/arduino/connectivity_test.cpp.j2 +189 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/device_tree/atmega.dts.j2 +77 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/device_tree/esp32.dts.j2 +77 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/device_tree/nrf52.dts.j2 +77 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/device_tree/stm32f4.dts.j2 +89 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/esp_idf/test_suite.c.j2 +340 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/pytest/test_connectivity.py.j2 +147 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/st_hal/hal_test.c.j2 +313 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/pytest_gpio_test.py.j2 +99 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/pytest_i2c_test.py.j2 +117 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/pytest_pinmux_test.py.j2 +43 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/pytest_spi_test.py.j2 +94 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/unity_gpio_test.c.j2 +113 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/unity_i2c_test.c.j2 +101 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/unity_spi_test.c.j2 +94 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/unittest/test_schematic.py.j2 +172 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/__init__.py +36 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/device_tree.py +1187 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/hierarchical_analysis.py +211 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/netlist.py +320 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/parts_registry.py +142 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/pcb.py +955 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/pcb_layout.py +308 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/pin_analysis.py +765 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/project.py +196 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/schematic.py +319 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/schematic_editor.py +674 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/schematic_search.py +158 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/validation.py +866 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/visualization.py +225 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/utils/__init__.py +1 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/utils/file_handlers.py +65 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/utils/kicad_cli.py +103 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/utils/kicad_version.py +103 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/utils/parts_registry.py +197 -0
- package/mcp_servers/kicad-mcp-server/tests/__init__.py +1 -0
- package/mcp_servers/kicad-mcp-server/tests/examples/ESP32S3_TEST.md +219 -0
- package/mcp_servers/kicad-mcp-server/tests/fixtures/README.md +65 -0
- package/mcp_servers/kicad-mcp-server/tests/fixtures/__init__.py +1 -0
- package/mcp_servers/kicad-mcp-server/tests/fixtures/example_pcb.kicad_pcb +177 -0
- package/mcp_servers/kicad-mcp-server/tests/fixtures/example_schematic.kicad_sch +145 -0
- package/mcp_servers/kicad-mcp-server/tests/fixtures/hier/child.kicad_sch +24 -0
- package/mcp_servers/kicad-mcp-server/tests/fixtures/hier/root.kicad_sch +38 -0
- package/mcp_servers/kicad-mcp-server/tests/test_tools/__init__.py +1 -0
- package/mcp_servers/kicad-mcp-server/tests/test_tools/test_hierarchical_labels.py +195 -0
- package/mcp_servers/kicad-mcp-server/tests/test_tools/test_kicad_cli.py +116 -0
- package/mcp_servers/kicad-mcp-server/tests/test_tools/test_netlist_cache_path.py +29 -0
- package/mcp_servers/kicad-mcp-server/tests/test_tools/test_schematic.py +189 -0
- package/mcp_servers/kicad-mcp-server/tests/test_tools/test_schematic_hierarchy.py +69 -0
- package/mcp_servers/kicad-mcp-server/tests/test_tools/test_visualization.py +136 -0
- package/mcp_servers/kicad-mcp-server/uv.lock +2873 -0
- package/mcp_servers/openscad-mcp-server/.dockerignore +9 -0
- package/mcp_servers/openscad-mcp-server/.github/workflows/test.yml +40 -0
- package/mcp_servers/openscad-mcp-server/Dockerfile +29 -0
- package/mcp_servers/openscad-mcp-server/LICENSE +21 -0
- package/mcp_servers/openscad-mcp-server/README.md +154 -0
- package/mcp_servers/openscad-mcp-server/docs/audit.md +56 -0
- package/mcp_servers/openscad-mcp-server/docs/docker.md +66 -0
- package/mcp_servers/openscad-mcp-server/docs/issue-followup.md +19 -0
- package/mcp_servers/openscad-mcp-server/docs/jetson.md +17 -0
- package/mcp_servers/openscad-mcp-server/glama.json +4 -0
- package/mcp_servers/openscad-mcp-server/legacy/README.md +15 -0
- package/mcp_servers/openscad-mcp-server/legacy/README.original.md +294 -0
- package/mcp_servers/openscad-mcp-server/legacy/implementation_plan.md +100 -0
- package/mcp_servers/openscad-mcp-server/legacy/old/download_sam2_checkpoint.py +115 -0
- package/mcp_servers/openscad-mcp-server/legacy/old/src/ai/sam_segmentation.py +209 -0
- package/mcp_servers/openscad-mcp-server/legacy/old/src/models/threestudio_generator.py +231 -0
- package/mcp_servers/openscad-mcp-server/legacy/old/src/workflow/image_to_model_pipeline.py +260 -0
- package/mcp_servers/openscad-mcp-server/legacy/old/test_sam2_segmentation.py +96 -0
- package/mcp_servers/openscad-mcp-server/legacy/requirements.txt +57 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/README.md +39 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/decisions/ai-driven-code-generation.md +122 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/decisions/export-formats.md +76 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/files/src/ai/ai_service.py.md +51 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/files/src/main.py.md +63 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/files/src/models/code_generator.py.md +63 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/files/src/nlp/parameter_extractor.py.md +63 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/knowledge/ai/natural-language-processing.md +78 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/knowledge/nlp/parameter-extraction.md +173 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/knowledge/openscad/export-formats.md +91 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/knowledge/openscad/openscad-basics.md +66 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/knowledge/openscad/primitive-testing.md +79 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/__init__.py +0 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/ai/ai_service.py +257 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/ai/gemini_api.py +161 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/ai/venice_api.py +203 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/config.py +121 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/main.py +1456 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/main.py.new +404 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/main_remote.py +401 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/models/__init__.py +0 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/models/code_generator.py +321 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/models/cuda_mvs.py +209 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/models/scad_templates/basic_shapes.scad +144 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/nlp/__init__.py +0 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/nlp/parameter_extractor.py +388 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/openscad_wrapper/__init__.py +0 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/openscad_wrapper/wrapper.py +418 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/printer_discovery/__init__.py +1 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/printer_discovery/printer_discovery.py +471 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/remote/connection_manager.py +537 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/remote/cuda_mvs_client.py +435 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/remote/cuda_mvs_server.py +787 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/remote/error_handling.py +415 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/testing/__init__.py +0 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/testing/primitive_tester.py +203 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/testing/test_primitives.py +98 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/utils/__init__.py +1 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/utils/cad_exporter.py +241 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/utils/format_validator.py +206 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/utils/stl_exporter.py +140 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/utils/stl_repair.py +91 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/utils/stl_validator.py +123 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/visualization/__init__.py +0 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/visualization/headless_renderer.py +52 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/visualization/renderer.py +177 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/visualization/web_interface.py +639 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/workflow/image_approval.py +148 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/workflow/multi_view_to_model_pipeline.py +338 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_complete_workflow.py +374 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_cuda_mvs.py +191 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_gemini_api.py +168 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_image_approval.py +192 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_image_approval_workflow.py +251 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_image_to_model_pipeline.py +145 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_model_selection.py +41 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_multi_view_pipeline.py +290 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_primitives.sh +13 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_rabbit_direct.py +71 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_remote_cuda_mvs.py +283 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_venice_example.py +69 -0
- package/mcp_servers/openscad-mcp-server/pyproject.toml +33 -0
- package/mcp_servers/openscad-mcp-server/requirements.txt +2 -0
- package/mcp_servers/openscad-mcp-server/scad/simple_cube.scad +2 -0
- package/mcp_servers/openscad-mcp-server/scripts/test_docker.py +220 -0
- package/mcp_servers/openscad-mcp-server/src/openscad_mcp/__init__.py +3 -0
- package/mcp_servers/openscad-mcp-server/src/openscad_mcp/__main__.py +3 -0
- package/mcp_servers/openscad-mcp-server/src/openscad_mcp/engine.py +94 -0
- package/mcp_servers/openscad-mcp-server/src/openscad_mcp/geometry.py +242 -0
- package/mcp_servers/openscad-mcp-server/src/openscad_mcp/server.py +245 -0
- package/mcp_servers/openscad-mcp-server/src/openscad_mcp/service.py +190 -0
- package/mcp_servers/openscad-mcp-server/tests/conftest.py +17 -0
- package/mcp_servers/openscad-mcp-server/tests/test_engine.py +37 -0
- package/mcp_servers/openscad-mcp-server/tests/test_geometry.py +106 -0
- package/mcp_servers/openscad-mcp-server/tests/test_integration.py +172 -0
- package/mcp_servers/openscad-mcp-server/tests/test_transports.py +314 -0
- package/mcp_servers/openscad-mcp-server/uv.lock +1123 -0
- package/package.json +44 -0
- package/scripts/install_mcps.sh +86 -0
- package/scripts/openscad_wrapper.sh +62 -0
- package/scripts/run_kicad_mcp.sh +25 -0
- package/scripts/run_openscad_mcp.sh +41 -0
- package/scripts/test_mcp_connection.py +626 -0
- package/scripts/test_mcp_connection.sh +168 -0
- package/skills/code-first-hardware-design/SKILL.md +260 -0
- package/skills/pcb-constraint-definition/SKILL.md +236 -0
- package/skills/pcb-layout-routing-automation/SKILL.md +153 -0
- package/skills/pcb-validation-dfm-signoff/SKILL.md +194 -0
- package/skills/schematic-datasheet-analysis/SKILL.md +202 -0
|
@@ -0,0 +1,1456 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import logging
|
|
3
|
+
import uuid
|
|
4
|
+
import json
|
|
5
|
+
from typing import Dict, Any, List, Optional, Tuple
|
|
6
|
+
from fastapi import FastAPI, Request, Response, HTTPException
|
|
7
|
+
from fastapi.responses import JSONResponse, FileResponse
|
|
8
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
9
|
+
from fastapi.staticfiles import StaticFiles
|
|
10
|
+
from fastapi.templating import Jinja2Templates
|
|
11
|
+
import uvicorn
|
|
12
|
+
from mcp import MCPServer, MCPTool, MCPToolCall, MCPToolCallResult
|
|
13
|
+
|
|
14
|
+
# Import configuration
|
|
15
|
+
from src.config import *
|
|
16
|
+
|
|
17
|
+
# Import components
|
|
18
|
+
from src.nlp.parameter_extractor import ParameterExtractor
|
|
19
|
+
from src.models.code_generator import CodeGenerator
|
|
20
|
+
from src.openscad_wrapper.wrapper import OpenSCADWrapper
|
|
21
|
+
from src.utils.cad_exporter import CADExporter
|
|
22
|
+
from src.visualization.headless_renderer import HeadlessRenderer
|
|
23
|
+
from src.printer_discovery.printer_discovery import PrinterDiscovery, PrinterInterface
|
|
24
|
+
from src.ai.venice_api import VeniceImageGenerator
|
|
25
|
+
from src.ai.sam_segmentation import SAMSegmenter
|
|
26
|
+
|
|
27
|
+
# Configure logging
|
|
28
|
+
logging.basicConfig(
|
|
29
|
+
level=logging.INFO,
|
|
30
|
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
31
|
+
)
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
# Create FastAPI app
|
|
35
|
+
app = FastAPI(title="OpenSCAD MCP Server")
|
|
36
|
+
|
|
37
|
+
# Add CORS middleware
|
|
38
|
+
app.add_middleware(
|
|
39
|
+
CORSMiddleware,
|
|
40
|
+
allow_origins=["*"],
|
|
41
|
+
allow_credentials=True,
|
|
42
|
+
allow_methods=["*"],
|
|
43
|
+
allow_headers=["*"],
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Create directories
|
|
47
|
+
os.makedirs("scad", exist_ok=True)
|
|
48
|
+
os.makedirs("output", exist_ok=True)
|
|
49
|
+
os.makedirs("output/models", exist_ok=True)
|
|
50
|
+
os.makedirs("output/preview", exist_ok=True)
|
|
51
|
+
os.makedirs("templates", exist_ok=True)
|
|
52
|
+
os.makedirs("static", exist_ok=True)
|
|
53
|
+
|
|
54
|
+
# Initialize components
|
|
55
|
+
parameter_extractor = ParameterExtractor()
|
|
56
|
+
code_generator = CodeGenerator("scad", "output")
|
|
57
|
+
openscad_wrapper = OpenSCADWrapper("scad", "output")
|
|
58
|
+
cad_exporter = CADExporter()
|
|
59
|
+
headless_renderer = HeadlessRenderer()
|
|
60
|
+
printer_discovery = PrinterDiscovery()
|
|
61
|
+
|
|
62
|
+
# Initialize AI components
|
|
63
|
+
venice_generator = VeniceImageGenerator(VENICE_API_KEY, IMAGES_DIR)
|
|
64
|
+
gemini_generator = GeminiImageGenerator(GEMINI_API_KEY, IMAGES_DIR)
|
|
65
|
+
cuda_mvs = CUDAMultiViewStereo(CUDA_MVS_PATH, MODELS_DIR, use_gpu=CUDA_MVS_USE_GPU)
|
|
66
|
+
image_approval = ImageApprovalTool(APPROVED_IMAGES_DIR)
|
|
67
|
+
|
|
68
|
+
# Initialize remote processing components if enabled
|
|
69
|
+
remote_connection_manager = None
|
|
70
|
+
if REMOTE_CUDA_MVS["ENABLED"]:
|
|
71
|
+
logger.info("Initializing remote CUDA MVS connection manager")
|
|
72
|
+
remote_connection_manager = CUDAMVSConnectionManager(
|
|
73
|
+
api_key=REMOTE_CUDA_MVS["API_KEY"],
|
|
74
|
+
discovery_port=REMOTE_CUDA_MVS["DISCOVERY_PORT"],
|
|
75
|
+
use_lan_discovery=REMOTE_CUDA_MVS["USE_LAN_DISCOVERY"],
|
|
76
|
+
server_url=REMOTE_CUDA_MVS["SERVER_URL"] if REMOTE_CUDA_MVS["SERVER_URL"] else None
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Initialize workflow pipeline
|
|
80
|
+
multi_view_pipeline = MultiViewToModelPipeline(
|
|
81
|
+
gemini_generator=gemini_generator,
|
|
82
|
+
cuda_mvs=cuda_mvs,
|
|
83
|
+
approval_tool=image_approval,
|
|
84
|
+
output_dir=OUTPUT_DIR
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# SAM2 segmenter will be initialized on first use to avoid loading the model unnecessarily
|
|
88
|
+
sam_segmenter = None
|
|
89
|
+
|
|
90
|
+
def get_sam_segmenter():
|
|
91
|
+
"""
|
|
92
|
+
Get or initialize the SAM2 segmenter.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
SAMSegmenter instance
|
|
96
|
+
"""
|
|
97
|
+
global sam_segmenter
|
|
98
|
+
if sam_segmenter is None:
|
|
99
|
+
logger.info("Initializing SAM2 segmenter")
|
|
100
|
+
sam_segmenter = SAMSegmenter(
|
|
101
|
+
model_type=SAM2_MODEL_TYPE,
|
|
102
|
+
checkpoint_path=SAM2_CHECKPOINT_PATH,
|
|
103
|
+
use_gpu=SAM2_USE_GPU,
|
|
104
|
+
output_dir=MASKS_DIR
|
|
105
|
+
)
|
|
106
|
+
return sam_segmenter
|
|
107
|
+
|
|
108
|
+
# Store models in memory
|
|
109
|
+
models = {}
|
|
110
|
+
printers = {}
|
|
111
|
+
approved_images = {}
|
|
112
|
+
remote_jobs = {}
|
|
113
|
+
|
|
114
|
+
# Create MCP server
|
|
115
|
+
mcp_server = MCPServer()
|
|
116
|
+
|
|
117
|
+
# Mount static files
|
|
118
|
+
app.mount("/static", StaticFiles(directory="static"), name="static")
|
|
119
|
+
|
|
120
|
+
# Create Jinja2 templates
|
|
121
|
+
templates = Jinja2Templates(directory="templates")
|
|
122
|
+
|
|
123
|
+
# Create model preview template
|
|
124
|
+
with open("templates/preview.html", "w") as f:
|
|
125
|
+
f.write("""
|
|
126
|
+
<!DOCTYPE html>
|
|
127
|
+
<html lang="en">
|
|
128
|
+
<head>
|
|
129
|
+
<meta charset="UTF-8">
|
|
130
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
131
|
+
<title>OpenSCAD Model Preview</title>
|
|
132
|
+
<style>
|
|
133
|
+
body {
|
|
134
|
+
font-family: Arial, sans-serif;
|
|
135
|
+
margin: 0;
|
|
136
|
+
padding: 0;
|
|
137
|
+
background-color: #f5f5f5;
|
|
138
|
+
}
|
|
139
|
+
.container {
|
|
140
|
+
max-width: 1200px;
|
|
141
|
+
margin: 0 auto;
|
|
142
|
+
padding: 20px;
|
|
143
|
+
}
|
|
144
|
+
h1 {
|
|
145
|
+
color: #333;
|
|
146
|
+
margin-bottom: 20px;
|
|
147
|
+
}
|
|
148
|
+
.preview-container {
|
|
149
|
+
display: flex;
|
|
150
|
+
flex-wrap: wrap;
|
|
151
|
+
gap: 20px;
|
|
152
|
+
margin-top: 20px;
|
|
153
|
+
}
|
|
154
|
+
.preview-image {
|
|
155
|
+
border: 1px solid #ddd;
|
|
156
|
+
border-radius: 5px;
|
|
157
|
+
padding: 10px;
|
|
158
|
+
background-color: white;
|
|
159
|
+
}
|
|
160
|
+
.preview-image img {
|
|
161
|
+
max-width: 100%;
|
|
162
|
+
height: auto;
|
|
163
|
+
}
|
|
164
|
+
.preview-image h3 {
|
|
165
|
+
margin-top: 10px;
|
|
166
|
+
margin-bottom: 5px;
|
|
167
|
+
color: #555;
|
|
168
|
+
}
|
|
169
|
+
.parameters {
|
|
170
|
+
margin-top: 20px;
|
|
171
|
+
background-color: #f9f9f9;
|
|
172
|
+
padding: 15px;
|
|
173
|
+
border-radius: 5px;
|
|
174
|
+
border: 1px solid #eee;
|
|
175
|
+
}
|
|
176
|
+
.parameters h2 {
|
|
177
|
+
margin-top: 0;
|
|
178
|
+
color: #333;
|
|
179
|
+
}
|
|
180
|
+
.parameters table {
|
|
181
|
+
width: 100%;
|
|
182
|
+
border-collapse: collapse;
|
|
183
|
+
}
|
|
184
|
+
.parameters table th, .parameters table td {
|
|
185
|
+
padding: 8px;
|
|
186
|
+
text-align: left;
|
|
187
|
+
border-bottom: 1px solid #ddd;
|
|
188
|
+
}
|
|
189
|
+
.parameters table th {
|
|
190
|
+
background-color: #f2f2f2;
|
|
191
|
+
}
|
|
192
|
+
.actions {
|
|
193
|
+
margin-top: 20px;
|
|
194
|
+
display: flex;
|
|
195
|
+
gap: 10px;
|
|
196
|
+
}
|
|
197
|
+
.actions button {
|
|
198
|
+
padding: 10px 15px;
|
|
199
|
+
background-color: #4CAF50;
|
|
200
|
+
color: white;
|
|
201
|
+
border: none;
|
|
202
|
+
border-radius: 4px;
|
|
203
|
+
cursor: pointer;
|
|
204
|
+
}
|
|
205
|
+
.actions button:hover {
|
|
206
|
+
background-color: #45a049;
|
|
207
|
+
}
|
|
208
|
+
</style>
|
|
209
|
+
</head>
|
|
210
|
+
<body>
|
|
211
|
+
<div class="container">
|
|
212
|
+
<h1>OpenSCAD Model Preview: {{ model_id }}</h1>
|
|
213
|
+
|
|
214
|
+
<div class="parameters">
|
|
215
|
+
<h2>Parameters</h2>
|
|
216
|
+
<table>
|
|
217
|
+
<tr>
|
|
218
|
+
<th>Parameter</th>
|
|
219
|
+
<th>Value</th>
|
|
220
|
+
</tr>
|
|
221
|
+
{% for key, value in parameters.items() %}
|
|
222
|
+
<tr>
|
|
223
|
+
<td>{{ key }}</td>
|
|
224
|
+
<td>{{ value }}</td>
|
|
225
|
+
</tr>
|
|
226
|
+
{% endfor %}
|
|
227
|
+
</table>
|
|
228
|
+
</div>
|
|
229
|
+
|
|
230
|
+
<div class="preview-container">
|
|
231
|
+
{% for view, image_path in previews.items() %}
|
|
232
|
+
<div class="preview-image">
|
|
233
|
+
<h3>{{ view|title }} View</h3>
|
|
234
|
+
<img src="{{ image_path }}" alt="{{ view }} view">
|
|
235
|
+
</div>
|
|
236
|
+
{% endfor %}
|
|
237
|
+
</div>
|
|
238
|
+
|
|
239
|
+
<div class="actions">
|
|
240
|
+
<button onclick="window.location.href='/download/{{ model_id }}'">Download Model</button>
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
</body>
|
|
244
|
+
</html>
|
|
245
|
+
""")
|
|
246
|
+
|
|
247
|
+
# Define MCP tools
|
|
248
|
+
@mcp_server.tool
|
|
249
|
+
def create_3d_model(description: str) -> Dict[str, Any]:
|
|
250
|
+
"""
|
|
251
|
+
Create a 3D model from a natural language description.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
description: Natural language description of the 3D model
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
Dictionary with model information
|
|
258
|
+
"""
|
|
259
|
+
# Extract parameters from description
|
|
260
|
+
model_type, parameters = parameter_extractor.extract_parameters(description)
|
|
261
|
+
|
|
262
|
+
# Generate a unique model ID
|
|
263
|
+
model_id = str(uuid.uuid4())
|
|
264
|
+
|
|
265
|
+
# Generate OpenSCAD code
|
|
266
|
+
scad_code = code_generator.generate_code(model_type, parameters)
|
|
267
|
+
|
|
268
|
+
# Save the SCAD file
|
|
269
|
+
scad_file = openscad_wrapper.generate_scad(scad_code, model_id)
|
|
270
|
+
|
|
271
|
+
# Generate preview images
|
|
272
|
+
previews = openscad_wrapper.generate_multi_angle_previews(scad_file, parameters)
|
|
273
|
+
|
|
274
|
+
# Export to parametric format (CSG by default)
|
|
275
|
+
success, model_file, error = cad_exporter.export_model(
|
|
276
|
+
scad_file,
|
|
277
|
+
"csg",
|
|
278
|
+
parameters,
|
|
279
|
+
metadata={
|
|
280
|
+
"description": description,
|
|
281
|
+
"model_type": model_type,
|
|
282
|
+
}
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
# Store model information
|
|
286
|
+
models[model_id] = {
|
|
287
|
+
"id": model_id,
|
|
288
|
+
"type": model_type,
|
|
289
|
+
"parameters": parameters,
|
|
290
|
+
"description": description,
|
|
291
|
+
"scad_file": scad_file,
|
|
292
|
+
"model_file": model_file if success else None,
|
|
293
|
+
"previews": previews,
|
|
294
|
+
"format": "csg"
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
# Create response
|
|
298
|
+
response = {
|
|
299
|
+
"model_id": model_id,
|
|
300
|
+
"model_type": model_type,
|
|
301
|
+
"parameters": parameters,
|
|
302
|
+
"preview_url": f"/ui/preview/{model_id}",
|
|
303
|
+
"supported_formats": cad_exporter.get_supported_formats()
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return response
|
|
307
|
+
|
|
308
|
+
@mcp_server.tool
|
|
309
|
+
def modify_3d_model(model_id: str, modifications: str) -> Dict[str, Any]:
|
|
310
|
+
"""
|
|
311
|
+
Modify an existing 3D model.
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
model_id: ID of the model to modify
|
|
315
|
+
modifications: Natural language description of the modifications
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
Dictionary with updated model information
|
|
319
|
+
"""
|
|
320
|
+
# Check if model exists
|
|
321
|
+
if model_id not in models:
|
|
322
|
+
raise ValueError(f"Model with ID {model_id} not found")
|
|
323
|
+
|
|
324
|
+
# Get existing model information
|
|
325
|
+
model_info = models[model_id]
|
|
326
|
+
|
|
327
|
+
# Extract parameters from modifications
|
|
328
|
+
_, new_parameters = parameter_extractor.extract_parameters(
|
|
329
|
+
modifications,
|
|
330
|
+
model_type=model_info["type"],
|
|
331
|
+
existing_parameters=model_info["parameters"]
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
# Generate OpenSCAD code with updated parameters
|
|
335
|
+
scad_code = code_generator.generate_code(model_info["type"], new_parameters)
|
|
336
|
+
|
|
337
|
+
# Save the SCAD file
|
|
338
|
+
scad_file = openscad_wrapper.generate_scad(scad_code, model_id)
|
|
339
|
+
|
|
340
|
+
# Generate preview images
|
|
341
|
+
previews = openscad_wrapper.generate_multi_angle_previews(scad_file, new_parameters)
|
|
342
|
+
|
|
343
|
+
# Export to parametric format (same as original)
|
|
344
|
+
success, model_file, error = cad_exporter.export_model(
|
|
345
|
+
scad_file,
|
|
346
|
+
model_info["format"],
|
|
347
|
+
new_parameters,
|
|
348
|
+
metadata={
|
|
349
|
+
"description": model_info["description"] + " | " + modifications,
|
|
350
|
+
"model_type": model_info["type"],
|
|
351
|
+
}
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
# Update model information
|
|
355
|
+
models[model_id] = {
|
|
356
|
+
"id": model_id,
|
|
357
|
+
"type": model_info["type"],
|
|
358
|
+
"parameters": new_parameters,
|
|
359
|
+
"description": model_info["description"] + " | " + modifications,
|
|
360
|
+
"scad_file": scad_file,
|
|
361
|
+
"model_file": model_file if success else None,
|
|
362
|
+
"previews": previews,
|
|
363
|
+
"format": model_info["format"]
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
# Create response
|
|
367
|
+
response = {
|
|
368
|
+
"model_id": model_id,
|
|
369
|
+
"model_type": model_info["type"],
|
|
370
|
+
"parameters": new_parameters,
|
|
371
|
+
"preview_url": f"/ui/preview/{model_id}",
|
|
372
|
+
"supported_formats": cad_exporter.get_supported_formats()
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return response
|
|
376
|
+
|
|
377
|
+
@mcp_server.tool
|
|
378
|
+
def export_model(model_id: str, format: str = "csg") -> Dict[str, Any]:
|
|
379
|
+
"""
|
|
380
|
+
Export a 3D model to a specific format.
|
|
381
|
+
|
|
382
|
+
Args:
|
|
383
|
+
model_id: ID of the model to export
|
|
384
|
+
format: Format to export to (csg, stl, obj, etc.)
|
|
385
|
+
|
|
386
|
+
Returns:
|
|
387
|
+
Dictionary with export information
|
|
388
|
+
"""
|
|
389
|
+
# Check if model exists
|
|
390
|
+
if model_id not in models:
|
|
391
|
+
raise ValueError(f"Model with ID {model_id} not found")
|
|
392
|
+
|
|
393
|
+
# Get model information
|
|
394
|
+
model_info = models[model_id]
|
|
395
|
+
|
|
396
|
+
# Check if format is supported
|
|
397
|
+
supported_formats = cad_exporter.get_supported_formats()
|
|
398
|
+
if format not in supported_formats:
|
|
399
|
+
raise ValueError(f"Format {format} not supported. Supported formats: {', '.join(supported_formats)}")
|
|
400
|
+
|
|
401
|
+
# Export model
|
|
402
|
+
success, model_file, error = cad_exporter.export_model(
|
|
403
|
+
model_info["scad_file"],
|
|
404
|
+
format,
|
|
405
|
+
model_info["parameters"],
|
|
406
|
+
metadata={
|
|
407
|
+
"description": model_info["description"],
|
|
408
|
+
"model_type": model_info["type"],
|
|
409
|
+
}
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
if not success:
|
|
413
|
+
raise ValueError(f"Failed to export model: {error}")
|
|
414
|
+
|
|
415
|
+
# Update model information
|
|
416
|
+
models[model_id]["model_file"] = model_file
|
|
417
|
+
models[model_id]["format"] = format
|
|
418
|
+
|
|
419
|
+
# Create response
|
|
420
|
+
response = {
|
|
421
|
+
"model_id": model_id,
|
|
422
|
+
"format": format,
|
|
423
|
+
"model_file": model_file,
|
|
424
|
+
"download_url": f"/download/{model_id}"
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return response
|
|
428
|
+
|
|
429
|
+
@mcp_server.tool
|
|
430
|
+
def discover_printers() -> Dict[str, Any]:
|
|
431
|
+
"""
|
|
432
|
+
Discover 3D printers on the network.
|
|
433
|
+
|
|
434
|
+
Returns:
|
|
435
|
+
Dictionary with discovered printers
|
|
436
|
+
"""
|
|
437
|
+
# Discover printers
|
|
438
|
+
discovered_printers = printer_discovery.discover_printers()
|
|
439
|
+
|
|
440
|
+
# Store printers
|
|
441
|
+
for printer in discovered_printers:
|
|
442
|
+
printers[printer["id"]] = printer
|
|
443
|
+
|
|
444
|
+
# Create response
|
|
445
|
+
response = {
|
|
446
|
+
"printers": discovered_printers
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return response
|
|
450
|
+
|
|
451
|
+
@mcp_server.tool
|
|
452
|
+
def connect_to_printer(printer_id: str) -> Dict[str, Any]:
|
|
453
|
+
"""
|
|
454
|
+
Connect to a 3D printer.
|
|
455
|
+
|
|
456
|
+
Args:
|
|
457
|
+
printer_id: ID of the printer to connect to
|
|
458
|
+
|
|
459
|
+
Returns:
|
|
460
|
+
Dictionary with connection information
|
|
461
|
+
"""
|
|
462
|
+
# Check if printer exists
|
|
463
|
+
if printer_id not in printers:
|
|
464
|
+
raise ValueError(f"Printer with ID {printer_id} not found")
|
|
465
|
+
|
|
466
|
+
# Get printer information
|
|
467
|
+
printer_info = printers[printer_id]
|
|
468
|
+
|
|
469
|
+
# Connect to printer
|
|
470
|
+
printer_interface = PrinterInterface(printer_info)
|
|
471
|
+
success, error = printer_interface.connect()
|
|
472
|
+
|
|
473
|
+
if not success:
|
|
474
|
+
raise ValueError(f"Failed to connect to printer: {error}")
|
|
475
|
+
|
|
476
|
+
# Update printer information
|
|
477
|
+
printers[printer_id]["connected"] = True
|
|
478
|
+
printers[printer_id]["interface"] = printer_interface
|
|
479
|
+
|
|
480
|
+
# Create response
|
|
481
|
+
response = {
|
|
482
|
+
"printer_id": printer_id,
|
|
483
|
+
"connected": True,
|
|
484
|
+
"printer_info": printer_info
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return response
|
|
488
|
+
|
|
489
|
+
@mcp_server.tool
|
|
490
|
+
def print_model(model_id: str, printer_id: str) -> Dict[str, Any]:
|
|
491
|
+
"""
|
|
492
|
+
Print a 3D model on a connected printer.
|
|
493
|
+
|
|
494
|
+
Args:
|
|
495
|
+
model_id: ID of the model to print
|
|
496
|
+
printer_id: ID of the printer to print on
|
|
497
|
+
|
|
498
|
+
Returns:
|
|
499
|
+
Dictionary with print job information
|
|
500
|
+
"""
|
|
501
|
+
# Check if model exists
|
|
502
|
+
if model_id not in models:
|
|
503
|
+
raise ValueError(f"Model with ID {model_id} not found")
|
|
504
|
+
|
|
505
|
+
# Check if printer exists
|
|
506
|
+
if printer_id not in printers:
|
|
507
|
+
raise ValueError(f"Printer with ID {printer_id} not found")
|
|
508
|
+
|
|
509
|
+
# Check if printer is connected
|
|
510
|
+
if not printers[printer_id].get("connected", False):
|
|
511
|
+
raise ValueError(f"Printer with ID {printer_id} is not connected")
|
|
512
|
+
|
|
513
|
+
# Get model and printer information
|
|
514
|
+
model_info = models[model_id]
|
|
515
|
+
printer_info = printers[printer_id]
|
|
516
|
+
|
|
517
|
+
# Check if model has been exported to a printable format
|
|
518
|
+
if not model_info.get("model_file"):
|
|
519
|
+
raise ValueError(f"Model with ID {model_id} has not been exported")
|
|
520
|
+
|
|
521
|
+
# Print model
|
|
522
|
+
printer_interface = printer_info["interface"]
|
|
523
|
+
job_id, error = printer_interface.print_model(model_info["model_file"])
|
|
524
|
+
|
|
525
|
+
if not job_id:
|
|
526
|
+
raise ValueError(f"Failed to print model: {error}")
|
|
527
|
+
|
|
528
|
+
# Create response
|
|
529
|
+
response = {
|
|
530
|
+
"model_id": model_id,
|
|
531
|
+
"printer_id": printer_id,
|
|
532
|
+
"job_id": job_id,
|
|
533
|
+
"status": "printing"
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
return response
|
|
537
|
+
|
|
538
|
+
@mcp_server.tool
|
|
539
|
+
def get_printer_status(printer_id: str) -> Dict[str, Any]:
|
|
540
|
+
"""
|
|
541
|
+
Get the status of a printer.
|
|
542
|
+
|
|
543
|
+
Args:
|
|
544
|
+
printer_id: ID of the printer to get status for
|
|
545
|
+
|
|
546
|
+
Returns:
|
|
547
|
+
Dictionary with printer status
|
|
548
|
+
"""
|
|
549
|
+
# Check if printer exists
|
|
550
|
+
if printer_id not in printers:
|
|
551
|
+
raise ValueError(f"Printer with ID {printer_id} not found")
|
|
552
|
+
|
|
553
|
+
# Check if printer is connected
|
|
554
|
+
if not printers[printer_id].get("connected", False):
|
|
555
|
+
raise ValueError(f"Printer with ID {printer_id} is not connected")
|
|
556
|
+
|
|
557
|
+
# Get printer information
|
|
558
|
+
printer_info = printers[printer_id]
|
|
559
|
+
|
|
560
|
+
# Get printer status
|
|
561
|
+
printer_interface = printer_info["interface"]
|
|
562
|
+
status = printer_interface.get_status()
|
|
563
|
+
|
|
564
|
+
# Create response
|
|
565
|
+
response = {
|
|
566
|
+
"printer_id": printer_id,
|
|
567
|
+
"status": status
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
return response
|
|
571
|
+
|
|
572
|
+
@mcp_server.tool
|
|
573
|
+
def cancel_print_job(printer_id: str, job_id: str) -> Dict[str, Any]:
|
|
574
|
+
"""
|
|
575
|
+
Cancel a print job.
|
|
576
|
+
|
|
577
|
+
Args:
|
|
578
|
+
printer_id: ID of the printer
|
|
579
|
+
job_id: ID of the print job to cancel
|
|
580
|
+
|
|
581
|
+
Returns:
|
|
582
|
+
Dictionary with cancellation information
|
|
583
|
+
"""
|
|
584
|
+
# Check if printer exists
|
|
585
|
+
if printer_id not in printers:
|
|
586
|
+
raise ValueError(f"Printer with ID {printer_id} not found")
|
|
587
|
+
|
|
588
|
+
# Check if printer is connected
|
|
589
|
+
if not printers[printer_id].get("connected", False):
|
|
590
|
+
raise ValueError(f"Printer with ID {printer_id} is not connected")
|
|
591
|
+
|
|
592
|
+
# Get printer information
|
|
593
|
+
printer_info = printers[printer_id]
|
|
594
|
+
|
|
595
|
+
# Cancel print job
|
|
596
|
+
printer_interface = printer_info["interface"]
|
|
597
|
+
success, error = printer_interface.cancel_job(job_id)
|
|
598
|
+
|
|
599
|
+
if not success:
|
|
600
|
+
raise ValueError(f"Failed to cancel print job: {error}")
|
|
601
|
+
|
|
602
|
+
# Create response
|
|
603
|
+
response = {
|
|
604
|
+
"printer_id": printer_id,
|
|
605
|
+
"job_id": job_id,
|
|
606
|
+
"status": "cancelled"
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
return response
|
|
610
|
+
|
|
611
|
+
# Add Venice.ai image generation tool
|
|
612
|
+
@mcp_server.tool
|
|
613
|
+
def generate_image(prompt: str, model: str = "fluently-xl") -> Dict[str, Any]:
|
|
614
|
+
"""
|
|
615
|
+
Generate an image using Venice.ai's image generation models.
|
|
616
|
+
|
|
617
|
+
Args:
|
|
618
|
+
prompt: Text description for image generation
|
|
619
|
+
model: Model to use (default: fluently-xl). Options include:
|
|
620
|
+
- "fluently-xl" (fastest, 2.30s): Quick generation with good quality
|
|
621
|
+
- "flux-dev" (high quality): Detailed, premium image quality
|
|
622
|
+
- "flux-dev-uncensored": Uncensored version of flux-dev model
|
|
623
|
+
- "stable-diffusion-3.5": Standard stable diffusion model
|
|
624
|
+
- "pony-realism": Specialized for realistic outputs
|
|
625
|
+
- "lustify-sdxl": Artistic stylization model
|
|
626
|
+
|
|
627
|
+
You can also use natural language like:
|
|
628
|
+
- "fastest model", "quick generation", "efficient"
|
|
629
|
+
- "high quality", "detailed", "premium quality"
|
|
630
|
+
- "realistic", "photorealistic"
|
|
631
|
+
- "artistic", "stylized", "creative"
|
|
632
|
+
|
|
633
|
+
Returns:
|
|
634
|
+
Dictionary with image information
|
|
635
|
+
"""
|
|
636
|
+
# Generate a unique image ID
|
|
637
|
+
image_id = str(uuid.uuid4())
|
|
638
|
+
|
|
639
|
+
# Generate image
|
|
640
|
+
result = venice_generator.generate_image(prompt, model)
|
|
641
|
+
|
|
642
|
+
# Create response
|
|
643
|
+
response = {
|
|
644
|
+
"image_id": image_id,
|
|
645
|
+
"prompt": prompt,
|
|
646
|
+
"model": model,
|
|
647
|
+
"image_path": result.get("local_path"),
|
|
648
|
+
"image_url": result.get("image_url")
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
return response
|
|
652
|
+
|
|
653
|
+
# Add SAM2 segmentation tool
|
|
654
|
+
@mcp_server.tool
|
|
655
|
+
def segment_image(image_path: str, points: Optional[List[Tuple[int, int]]] = None) -> Dict[str, Any]:
|
|
656
|
+
"""
|
|
657
|
+
Segment objects in an image using SAM2 (Segment Anything Model 2).
|
|
658
|
+
|
|
659
|
+
Args:
|
|
660
|
+
image_path: Path to the input image
|
|
661
|
+
points: Optional list of (x, y) points to guide segmentation
|
|
662
|
+
If not provided, automatic segmentation will be used
|
|
663
|
+
|
|
664
|
+
Returns:
|
|
665
|
+
Dictionary with segmentation masks and metadata
|
|
666
|
+
"""
|
|
667
|
+
# Get or initialize SAM2 segmenter
|
|
668
|
+
sam_segmenter = get_sam_segmenter()
|
|
669
|
+
|
|
670
|
+
# Generate a unique segmentation ID
|
|
671
|
+
segmentation_id = str(uuid.uuid4())
|
|
672
|
+
|
|
673
|
+
try:
|
|
674
|
+
# Perform segmentation
|
|
675
|
+
if points:
|
|
676
|
+
result = sam_segmenter.segment_image(image_path, points)
|
|
677
|
+
else:
|
|
678
|
+
result = sam_segmenter.segment_with_auto_points(image_path)
|
|
679
|
+
|
|
680
|
+
# Create response
|
|
681
|
+
response = {
|
|
682
|
+
"segmentation_id": segmentation_id,
|
|
683
|
+
"image_path": image_path,
|
|
684
|
+
"mask_paths": result.get("mask_paths", []),
|
|
685
|
+
"num_masks": result.get("num_masks", 0),
|
|
686
|
+
"points_used": points if points else result.get("points", [])
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
return response
|
|
690
|
+
except Exception as e:
|
|
691
|
+
logger.error(f"Error segmenting image: {str(e)}")
|
|
692
|
+
raise HTTPException(status_code=500, detail=f"Error segmenting image: {str(e)}")
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
# Add Google Gemini image generation tool
|
|
696
|
+
@mcp_server.tool
|
|
697
|
+
def generate_image_gemini(prompt: str, model: str = GEMINI_MODEL) -> Dict[str, Any]:
|
|
698
|
+
"""
|
|
699
|
+
Generate an image using Google Gemini's image generation models.
|
|
700
|
+
|
|
701
|
+
Args:
|
|
702
|
+
prompt: Text description for image generation
|
|
703
|
+
model: Model to use (default: gemini-2.0-flash-exp-image-generation)
|
|
704
|
+
|
|
705
|
+
Returns:
|
|
706
|
+
Dictionary with image information
|
|
707
|
+
"""
|
|
708
|
+
# Generate a unique image ID
|
|
709
|
+
image_id = str(uuid.uuid4())
|
|
710
|
+
|
|
711
|
+
# Generate image
|
|
712
|
+
result = gemini_generator.generate_image(prompt, model)
|
|
713
|
+
|
|
714
|
+
# Create response
|
|
715
|
+
response = {
|
|
716
|
+
"image_id": image_id,
|
|
717
|
+
"prompt": prompt,
|
|
718
|
+
"model": model,
|
|
719
|
+
"image_path": result.get("local_path"),
|
|
720
|
+
"image_url": f"/images/{os.path.basename(result.get('local_path', ''))}"
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
return response
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
# Add multi-view image generation tool
|
|
727
|
+
@mcp_server.tool
|
|
728
|
+
def generate_multi_view_images(prompt: str, num_views: int = 4) -> Dict[str, Any]:
|
|
729
|
+
"""
|
|
730
|
+
Generate multiple views of the same 3D object using Google Gemini.
|
|
731
|
+
|
|
732
|
+
Args:
|
|
733
|
+
prompt: Text description of the 3D object
|
|
734
|
+
num_views: Number of views to generate (default: 4)
|
|
735
|
+
|
|
736
|
+
Returns:
|
|
737
|
+
Dictionary with multi-view image information
|
|
738
|
+
"""
|
|
739
|
+
# Validate number of views
|
|
740
|
+
if num_views < MULTI_VIEW_PIPELINE["MIN_NUM_VIEWS"]:
|
|
741
|
+
raise ValueError(f"Number of views must be at least {MULTI_VIEW_PIPELINE['MIN_NUM_VIEWS']}")
|
|
742
|
+
|
|
743
|
+
if num_views > MULTI_VIEW_PIPELINE["MAX_NUM_VIEWS"]:
|
|
744
|
+
raise ValueError(f"Number of views cannot exceed {MULTI_VIEW_PIPELINE['MAX_NUM_VIEWS']}")
|
|
745
|
+
|
|
746
|
+
# Generate a unique multi-view ID
|
|
747
|
+
multi_view_id = str(uuid.uuid4())
|
|
748
|
+
|
|
749
|
+
# Generate multi-view images
|
|
750
|
+
results = gemini_generator.generate_multiple_views(prompt, num_views)
|
|
751
|
+
|
|
752
|
+
# Create response
|
|
753
|
+
response = {
|
|
754
|
+
"multi_view_id": multi_view_id,
|
|
755
|
+
"prompt": prompt,
|
|
756
|
+
"num_views": num_views,
|
|
757
|
+
"views": [
|
|
758
|
+
{
|
|
759
|
+
"view_id": result.get("view_id", f"view_{i+1}"),
|
|
760
|
+
"view_index": result.get("view_index", i+1),
|
|
761
|
+
"view_direction": result.get("view_direction", ""),
|
|
762
|
+
"image_path": result.get("local_path"),
|
|
763
|
+
"image_url": f"/images/{os.path.basename(result.get('local_path', ''))}"
|
|
764
|
+
}
|
|
765
|
+
for i, result in enumerate(results)
|
|
766
|
+
],
|
|
767
|
+
"approval_required": IMAGE_APPROVAL["ENABLED"] and not IMAGE_APPROVAL["AUTO_APPROVE"]
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
# Store multi-view information for approval
|
|
771
|
+
if IMAGE_APPROVAL["ENABLED"]:
|
|
772
|
+
approved_images[multi_view_id] = {
|
|
773
|
+
"multi_view_id": multi_view_id,
|
|
774
|
+
"prompt": prompt,
|
|
775
|
+
"num_views": num_views,
|
|
776
|
+
"views": response["views"],
|
|
777
|
+
"approved_views": [] if not IMAGE_APPROVAL["AUTO_APPROVE"] else [view["view_id"] for view in response["views"]],
|
|
778
|
+
"rejected_views": [],
|
|
779
|
+
"approval_complete": IMAGE_APPROVAL["AUTO_APPROVE"]
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
return response
|
|
783
|
+
|
|
784
|
+
|
|
785
|
+
# Add image approval tool
|
|
786
|
+
@mcp_server.tool
|
|
787
|
+
def approve_image(multi_view_id: str, view_id: str) -> Dict[str, Any]:
|
|
788
|
+
"""
|
|
789
|
+
Approve an image for 3D model generation.
|
|
790
|
+
|
|
791
|
+
Args:
|
|
792
|
+
multi_view_id: ID of the multi-view set
|
|
793
|
+
view_id: ID of the view to approve
|
|
794
|
+
|
|
795
|
+
Returns:
|
|
796
|
+
Dictionary with approval information
|
|
797
|
+
"""
|
|
798
|
+
# Check if multi-view ID exists
|
|
799
|
+
if multi_view_id not in approved_images:
|
|
800
|
+
raise ValueError(f"Multi-view set with ID {multi_view_id} not found")
|
|
801
|
+
|
|
802
|
+
# Get multi-view information
|
|
803
|
+
multi_view_info = approved_images[multi_view_id]
|
|
804
|
+
|
|
805
|
+
# Check if view ID exists
|
|
806
|
+
view_exists = False
|
|
807
|
+
for view in multi_view_info["views"]:
|
|
808
|
+
if view["view_id"] == view_id:
|
|
809
|
+
view_exists = True
|
|
810
|
+
break
|
|
811
|
+
|
|
812
|
+
if not view_exists:
|
|
813
|
+
raise ValueError(f"View with ID {view_id} not found in multi-view set {multi_view_id}")
|
|
814
|
+
|
|
815
|
+
# Check if view is already approved
|
|
816
|
+
if view_id in multi_view_info["approved_views"]:
|
|
817
|
+
return {
|
|
818
|
+
"multi_view_id": multi_view_id,
|
|
819
|
+
"view_id": view_id,
|
|
820
|
+
"status": "already_approved",
|
|
821
|
+
"approved_views": multi_view_info["approved_views"],
|
|
822
|
+
"rejected_views": multi_view_info["rejected_views"],
|
|
823
|
+
"approval_complete": multi_view_info["approval_complete"]
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
# Remove from rejected views if present
|
|
827
|
+
if view_id in multi_view_info["rejected_views"]:
|
|
828
|
+
multi_view_info["rejected_views"].remove(view_id)
|
|
829
|
+
|
|
830
|
+
# Add to approved views
|
|
831
|
+
multi_view_info["approved_views"].append(view_id)
|
|
832
|
+
|
|
833
|
+
# Check if approval is complete
|
|
834
|
+
if len(multi_view_info["approved_views"]) >= IMAGE_APPROVAL["MIN_APPROVED_IMAGES"]:
|
|
835
|
+
multi_view_info["approval_complete"] = True
|
|
836
|
+
|
|
837
|
+
# Create response
|
|
838
|
+
response = {
|
|
839
|
+
"multi_view_id": multi_view_id,
|
|
840
|
+
"view_id": view_id,
|
|
841
|
+
"status": "approved",
|
|
842
|
+
"approved_views": multi_view_info["approved_views"],
|
|
843
|
+
"rejected_views": multi_view_info["rejected_views"],
|
|
844
|
+
"approval_complete": multi_view_info["approval_complete"]
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
return response
|
|
848
|
+
|
|
849
|
+
|
|
850
|
+
# Add image rejection tool
|
|
851
|
+
@mcp_server.tool
|
|
852
|
+
def reject_image(multi_view_id: str, view_id: str) -> Dict[str, Any]:
|
|
853
|
+
"""
|
|
854
|
+
Reject an image for 3D model generation.
|
|
855
|
+
|
|
856
|
+
Args:
|
|
857
|
+
multi_view_id: ID of the multi-view set
|
|
858
|
+
view_id: ID of the view to reject
|
|
859
|
+
|
|
860
|
+
Returns:
|
|
861
|
+
Dictionary with rejection information
|
|
862
|
+
"""
|
|
863
|
+
# Check if multi-view ID exists
|
|
864
|
+
if multi_view_id not in approved_images:
|
|
865
|
+
raise ValueError(f"Multi-view set with ID {multi_view_id} not found")
|
|
866
|
+
|
|
867
|
+
# Get multi-view information
|
|
868
|
+
multi_view_info = approved_images[multi_view_id]
|
|
869
|
+
|
|
870
|
+
# Check if view ID exists
|
|
871
|
+
view_exists = False
|
|
872
|
+
for view in multi_view_info["views"]:
|
|
873
|
+
if view["view_id"] == view_id:
|
|
874
|
+
view_exists = True
|
|
875
|
+
break
|
|
876
|
+
|
|
877
|
+
if not view_exists:
|
|
878
|
+
raise ValueError(f"View with ID {view_id} not found in multi-view set {multi_view_id}")
|
|
879
|
+
|
|
880
|
+
# Check if view is already rejected
|
|
881
|
+
if view_id in multi_view_info["rejected_views"]:
|
|
882
|
+
return {
|
|
883
|
+
"multi_view_id": multi_view_id,
|
|
884
|
+
"view_id": view_id,
|
|
885
|
+
"status": "already_rejected",
|
|
886
|
+
"approved_views": multi_view_info["approved_views"],
|
|
887
|
+
"rejected_views": multi_view_info["rejected_views"],
|
|
888
|
+
"approval_complete": multi_view_info["approval_complete"]
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
# Remove from approved views if present
|
|
892
|
+
if view_id in multi_view_info["approved_views"]:
|
|
893
|
+
multi_view_info["approved_views"].remove(view_id)
|
|
894
|
+
|
|
895
|
+
# Add to rejected views
|
|
896
|
+
multi_view_info["rejected_views"].append(view_id)
|
|
897
|
+
|
|
898
|
+
# Check if approval is complete
|
|
899
|
+
if len(multi_view_info["approved_views"]) >= IMAGE_APPROVAL["MIN_APPROVED_IMAGES"]:
|
|
900
|
+
multi_view_info["approval_complete"] = True
|
|
901
|
+
else:
|
|
902
|
+
multi_view_info["approval_complete"] = False
|
|
903
|
+
|
|
904
|
+
# Create response
|
|
905
|
+
response = {
|
|
906
|
+
"multi_view_id": multi_view_id,
|
|
907
|
+
"view_id": view_id,
|
|
908
|
+
"status": "rejected",
|
|
909
|
+
"approved_views": multi_view_info["approved_views"],
|
|
910
|
+
"rejected_views": multi_view_info["rejected_views"],
|
|
911
|
+
"approval_complete": multi_view_info["approval_complete"]
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
return response
|
|
915
|
+
|
|
916
|
+
|
|
917
|
+
# Add 3D model generation from approved images tool
|
|
918
|
+
@mcp_server.tool
|
|
919
|
+
def create_3d_model_from_images(multi_view_id: str, output_name: Optional[str] = None) -> Dict[str, Any]:
|
|
920
|
+
"""
|
|
921
|
+
Create a 3D model from approved multi-view images.
|
|
922
|
+
|
|
923
|
+
Args:
|
|
924
|
+
multi_view_id: ID of the multi-view set
|
|
925
|
+
output_name: Optional name for the output model
|
|
926
|
+
|
|
927
|
+
Returns:
|
|
928
|
+
Dictionary with model information
|
|
929
|
+
"""
|
|
930
|
+
# Check if multi-view ID exists
|
|
931
|
+
if multi_view_id not in approved_images:
|
|
932
|
+
raise ValueError(f"Multi-view set with ID {multi_view_id} not found")
|
|
933
|
+
|
|
934
|
+
# Get multi-view information
|
|
935
|
+
multi_view_info = approved_images[multi_view_id]
|
|
936
|
+
|
|
937
|
+
# Check if approval is complete
|
|
938
|
+
if not multi_view_info["approval_complete"]:
|
|
939
|
+
raise ValueError(f"Approval for multi-view set {multi_view_id} is not complete")
|
|
940
|
+
|
|
941
|
+
# Check if there are enough approved images
|
|
942
|
+
if len(multi_view_info["approved_views"]) < IMAGE_APPROVAL["MIN_APPROVED_IMAGES"]:
|
|
943
|
+
raise ValueError(f"Not enough approved images. Need at least {IMAGE_APPROVAL['MIN_APPROVED_IMAGES']}, but only have {len(multi_view_info['approved_views'])}")
|
|
944
|
+
|
|
945
|
+
# Get approved image paths
|
|
946
|
+
approved_image_paths = []
|
|
947
|
+
for view in multi_view_info["views"]:
|
|
948
|
+
if view["view_id"] in multi_view_info["approved_views"]:
|
|
949
|
+
approved_image_paths.append(view["image_path"])
|
|
950
|
+
|
|
951
|
+
# Generate a unique model ID
|
|
952
|
+
model_id = str(uuid.uuid4())
|
|
953
|
+
|
|
954
|
+
# Set output name if not provided
|
|
955
|
+
if not output_name:
|
|
956
|
+
output_name = f"model_{model_id[:8]}"
|
|
957
|
+
|
|
958
|
+
# Create 3D model
|
|
959
|
+
if REMOTE_CUDA_MVS["ENABLED"] and remote_connection_manager:
|
|
960
|
+
# Use remote CUDA MVS processing
|
|
961
|
+
servers = discover_remote_servers()
|
|
962
|
+
|
|
963
|
+
if not servers:
|
|
964
|
+
raise ValueError("No remote CUDA MVS servers found")
|
|
965
|
+
|
|
966
|
+
# Use the first available server
|
|
967
|
+
server_id = servers[0]["id"]
|
|
968
|
+
|
|
969
|
+
# Upload images
|
|
970
|
+
upload_result = upload_images_to_server(server_id, approved_image_paths)
|
|
971
|
+
|
|
972
|
+
if not upload_result or "job_id" not in upload_result:
|
|
973
|
+
raise ValueError("Failed to upload images to remote server")
|
|
974
|
+
|
|
975
|
+
job_id = upload_result["job_id"]
|
|
976
|
+
|
|
977
|
+
# Process images
|
|
978
|
+
process_result = process_images_remotely(
|
|
979
|
+
server_id,
|
|
980
|
+
job_id,
|
|
981
|
+
{
|
|
982
|
+
"quality": REMOTE_CUDA_MVS["DEFAULT_RECONSTRUCTION_QUALITY"],
|
|
983
|
+
"output_format": REMOTE_CUDA_MVS["DEFAULT_OUTPUT_FORMAT"]
|
|
984
|
+
}
|
|
985
|
+
)
|
|
986
|
+
|
|
987
|
+
if not process_result:
|
|
988
|
+
raise ValueError(f"Failed to process images for job {job_id}")
|
|
989
|
+
|
|
990
|
+
# Wait for completion if requested
|
|
991
|
+
if REMOTE_CUDA_MVS["WAIT_FOR_COMPLETION"]:
|
|
992
|
+
import time
|
|
993
|
+
|
|
994
|
+
while True:
|
|
995
|
+
status = get_job_status(job_id)
|
|
996
|
+
|
|
997
|
+
if not status:
|
|
998
|
+
raise ValueError(f"Failed to get status for job {job_id}")
|
|
999
|
+
|
|
1000
|
+
if status["status"] in ["completed", "failed", "cancelled"]:
|
|
1001
|
+
break
|
|
1002
|
+
|
|
1003
|
+
time.sleep(REMOTE_CUDA_MVS["POLL_INTERVAL"])
|
|
1004
|
+
|
|
1005
|
+
if status["status"] == "completed":
|
|
1006
|
+
# Download model
|
|
1007
|
+
download_result = download_remote_model(job_id)
|
|
1008
|
+
|
|
1009
|
+
if not download_result:
|
|
1010
|
+
raise ValueError(f"Failed to download model for job {job_id}")
|
|
1011
|
+
|
|
1012
|
+
# Store model information
|
|
1013
|
+
models[model_id] = {
|
|
1014
|
+
"id": model_id,
|
|
1015
|
+
"type": "cuda_mvs_remote",
|
|
1016
|
+
"parameters": {
|
|
1017
|
+
"multi_view_id": multi_view_id,
|
|
1018
|
+
"prompt": multi_view_info["prompt"],
|
|
1019
|
+
"num_views": len(approved_image_paths),
|
|
1020
|
+
"quality": REMOTE_CUDA_MVS["DEFAULT_RECONSTRUCTION_QUALITY"],
|
|
1021
|
+
"output_format": REMOTE_CUDA_MVS["DEFAULT_OUTPUT_FORMAT"]
|
|
1022
|
+
},
|
|
1023
|
+
"description": f"3D model generated from {len(approved_image_paths)} views of '{multi_view_info['prompt']}'",
|
|
1024
|
+
"model_file": download_result.get("model_path"),
|
|
1025
|
+
"point_cloud_file": download_result.get("point_cloud_path"),
|
|
1026
|
+
"previews": {}, # Will be generated later
|
|
1027
|
+
"format": REMOTE_CUDA_MVS["DEFAULT_OUTPUT_FORMAT"],
|
|
1028
|
+
"remote_job_id": job_id
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
# Create response
|
|
1032
|
+
response = {
|
|
1033
|
+
"model_id": model_id,
|
|
1034
|
+
"multi_view_id": multi_view_id,
|
|
1035
|
+
"status": "completed",
|
|
1036
|
+
"model_path": download_result.get("model_path"),
|
|
1037
|
+
"point_cloud_path": download_result.get("point_cloud_path"),
|
|
1038
|
+
"format": REMOTE_CUDA_MVS["DEFAULT_OUTPUT_FORMAT"]
|
|
1039
|
+
}
|
|
1040
|
+
else:
|
|
1041
|
+
# Store job information
|
|
1042
|
+
remote_jobs[job_id] = {
|
|
1043
|
+
"model_id": model_id,
|
|
1044
|
+
"multi_view_id": multi_view_id,
|
|
1045
|
+
"server_id": server_id,
|
|
1046
|
+
"job_id": job_id,
|
|
1047
|
+
"status": status["status"],
|
|
1048
|
+
"message": status.get("message", "")
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
# Create response
|
|
1052
|
+
response = {
|
|
1053
|
+
"model_id": model_id,
|
|
1054
|
+
"multi_view_id": multi_view_id,
|
|
1055
|
+
"status": status["status"],
|
|
1056
|
+
"message": status.get("message", ""),
|
|
1057
|
+
"job_id": job_id
|
|
1058
|
+
}
|
|
1059
|
+
else:
|
|
1060
|
+
# Store job information
|
|
1061
|
+
remote_jobs[job_id] = {
|
|
1062
|
+
"model_id": model_id,
|
|
1063
|
+
"multi_view_id": multi_view_id,
|
|
1064
|
+
"server_id": server_id,
|
|
1065
|
+
"job_id": job_id,
|
|
1066
|
+
"status": "processing"
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
# Create response
|
|
1070
|
+
response = {
|
|
1071
|
+
"model_id": model_id,
|
|
1072
|
+
"multi_view_id": multi_view_id,
|
|
1073
|
+
"status": "processing",
|
|
1074
|
+
"job_id": job_id,
|
|
1075
|
+
"server_id": server_id
|
|
1076
|
+
}
|
|
1077
|
+
else:
|
|
1078
|
+
# Use local CUDA MVS processing
|
|
1079
|
+
result = cuda_mvs.process_images(
|
|
1080
|
+
approved_image_paths,
|
|
1081
|
+
output_name=output_name,
|
|
1082
|
+
quality=REMOTE_CUDA_MVS["DEFAULT_RECONSTRUCTION_QUALITY"],
|
|
1083
|
+
output_format=REMOTE_CUDA_MVS["DEFAULT_OUTPUT_FORMAT"]
|
|
1084
|
+
)
|
|
1085
|
+
|
|
1086
|
+
# Store model information
|
|
1087
|
+
models[model_id] = {
|
|
1088
|
+
"id": model_id,
|
|
1089
|
+
"type": "cuda_mvs_local",
|
|
1090
|
+
"parameters": {
|
|
1091
|
+
"multi_view_id": multi_view_id,
|
|
1092
|
+
"prompt": multi_view_info["prompt"],
|
|
1093
|
+
"num_views": len(approved_image_paths),
|
|
1094
|
+
"quality": REMOTE_CUDA_MVS["DEFAULT_RECONSTRUCTION_QUALITY"],
|
|
1095
|
+
"output_format": REMOTE_CUDA_MVS["DEFAULT_OUTPUT_FORMAT"]
|
|
1096
|
+
},
|
|
1097
|
+
"description": f"3D model generated from {len(approved_image_paths)} views of '{multi_view_info['prompt']}'",
|
|
1098
|
+
"model_file": result.get("model_path"),
|
|
1099
|
+
"point_cloud_file": result.get("point_cloud_path"),
|
|
1100
|
+
"previews": {}, # Will be generated later
|
|
1101
|
+
"format": REMOTE_CUDA_MVS["DEFAULT_OUTPUT_FORMAT"]
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
# Create response
|
|
1105
|
+
response = {
|
|
1106
|
+
"model_id": model_id,
|
|
1107
|
+
"multi_view_id": multi_view_id,
|
|
1108
|
+
"status": "completed",
|
|
1109
|
+
"model_path": result.get("model_path"),
|
|
1110
|
+
"point_cloud_path": result.get("point_cloud_path"),
|
|
1111
|
+
"format": REMOTE_CUDA_MVS["DEFAULT_OUTPUT_FORMAT"]
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
return response
|
|
1115
|
+
|
|
1116
|
+
|
|
1117
|
+
# Add complete pipeline tool (text to 3D model)
|
|
1118
|
+
@mcp_server.tool
|
|
1119
|
+
def create_3d_model_from_text(prompt: str, num_views: int = 4, wait_for_completion: bool = True) -> Dict[str, Any]:
|
|
1120
|
+
"""
|
|
1121
|
+
Create a 3D model from a text description using the complete pipeline.
|
|
1122
|
+
|
|
1123
|
+
Args:
|
|
1124
|
+
prompt: Text description of the 3D object
|
|
1125
|
+
num_views: Number of views to generate (default: 4)
|
|
1126
|
+
wait_for_completion: Whether to wait for remote processing to complete
|
|
1127
|
+
|
|
1128
|
+
Returns:
|
|
1129
|
+
Dictionary with model information
|
|
1130
|
+
"""
|
|
1131
|
+
# Generate multi-view images
|
|
1132
|
+
multi_view_result = generate_multi_view_images(prompt, num_views)
|
|
1133
|
+
|
|
1134
|
+
multi_view_id = multi_view_result["multi_view_id"]
|
|
1135
|
+
|
|
1136
|
+
# Auto-approve all images if enabled
|
|
1137
|
+
if IMAGE_APPROVAL["AUTO_APPROVE"]:
|
|
1138
|
+
for view in multi_view_result["views"]:
|
|
1139
|
+
approve_image(multi_view_id, view["view_id"])
|
|
1140
|
+
else:
|
|
1141
|
+
# Return multi-view result for manual approval
|
|
1142
|
+
return {
|
|
1143
|
+
"status": "awaiting_approval",
|
|
1144
|
+
"message": "Please approve or reject each image before proceeding",
|
|
1145
|
+
"multi_view_id": multi_view_id,
|
|
1146
|
+
"views": multi_view_result["views"]
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
# Create 3D model from approved images
|
|
1150
|
+
model_result = create_3d_model_from_images(multi_view_id)
|
|
1151
|
+
|
|
1152
|
+
# If remote processing is not waiting for completion, return job information
|
|
1153
|
+
if not wait_for_completion and model_result.get("status") == "processing":
|
|
1154
|
+
return model_result
|
|
1155
|
+
|
|
1156
|
+
# Return model information
|
|
1157
|
+
return model_result
|
|
1158
|
+
|
|
1159
|
+
|
|
1160
|
+
# Add remote CUDA MVS server discovery tool
|
|
1161
|
+
@mcp_server.tool
|
|
1162
|
+
def discover_remote_cuda_mvs_servers() -> Dict[str, Any]:
|
|
1163
|
+
"""
|
|
1164
|
+
Discover remote CUDA MVS servers on the network.
|
|
1165
|
+
|
|
1166
|
+
Returns:
|
|
1167
|
+
Dictionary with discovered servers
|
|
1168
|
+
"""
|
|
1169
|
+
if not REMOTE_CUDA_MVS["ENABLED"]:
|
|
1170
|
+
raise ValueError("Remote CUDA MVS processing is not enabled")
|
|
1171
|
+
|
|
1172
|
+
if not remote_connection_manager:
|
|
1173
|
+
raise ValueError("Remote CUDA MVS connection manager is not initialized")
|
|
1174
|
+
|
|
1175
|
+
servers = discover_remote_servers()
|
|
1176
|
+
|
|
1177
|
+
return {
|
|
1178
|
+
"servers": servers,
|
|
1179
|
+
"count": len(servers)
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
|
|
1183
|
+
# Add remote job status tool
|
|
1184
|
+
@mcp_server.tool
|
|
1185
|
+
def get_remote_job_status(job_id: str) -> Dict[str, Any]:
|
|
1186
|
+
"""
|
|
1187
|
+
Get the status of a remote CUDA MVS processing job.
|
|
1188
|
+
|
|
1189
|
+
Args:
|
|
1190
|
+
job_id: ID of the job to get status for
|
|
1191
|
+
|
|
1192
|
+
Returns:
|
|
1193
|
+
Dictionary with job status
|
|
1194
|
+
"""
|
|
1195
|
+
if not REMOTE_CUDA_MVS["ENABLED"]:
|
|
1196
|
+
raise ValueError("Remote CUDA MVS processing is not enabled")
|
|
1197
|
+
|
|
1198
|
+
if not remote_connection_manager:
|
|
1199
|
+
raise ValueError("Remote CUDA MVS connection manager is not initialized")
|
|
1200
|
+
|
|
1201
|
+
# Check if job exists
|
|
1202
|
+
if job_id not in remote_jobs:
|
|
1203
|
+
raise ValueError(f"Job with ID {job_id} not found")
|
|
1204
|
+
|
|
1205
|
+
# Get job information
|
|
1206
|
+
job_info = remote_jobs[job_id]
|
|
1207
|
+
|
|
1208
|
+
# Get status from server
|
|
1209
|
+
status = get_job_status(job_id)
|
|
1210
|
+
|
|
1211
|
+
if not status:
|
|
1212
|
+
raise ValueError(f"Failed to get status for job with ID {job_id}")
|
|
1213
|
+
|
|
1214
|
+
# Update job information
|
|
1215
|
+
job_info["status"] = status.get("status", job_info["status"])
|
|
1216
|
+
job_info["progress"] = status.get("progress", 0)
|
|
1217
|
+
job_info["message"] = status.get("message", "")
|
|
1218
|
+
|
|
1219
|
+
return job_info
|
|
1220
|
+
|
|
1221
|
+
|
|
1222
|
+
# Add remote model download tool
|
|
1223
|
+
@mcp_server.tool
|
|
1224
|
+
def download_remote_model_result(job_id: str) -> Dict[str, Any]:
|
|
1225
|
+
"""
|
|
1226
|
+
Download a processed model from a remote CUDA MVS server.
|
|
1227
|
+
|
|
1228
|
+
Args:
|
|
1229
|
+
job_id: ID of the job to download model for
|
|
1230
|
+
|
|
1231
|
+
Returns:
|
|
1232
|
+
Dictionary with model information
|
|
1233
|
+
"""
|
|
1234
|
+
if not REMOTE_CUDA_MVS["ENABLED"]:
|
|
1235
|
+
raise ValueError("Remote CUDA MVS processing is not enabled")
|
|
1236
|
+
|
|
1237
|
+
if not remote_connection_manager:
|
|
1238
|
+
raise ValueError("Remote CUDA MVS connection manager is not initialized")
|
|
1239
|
+
|
|
1240
|
+
# Check if job exists
|
|
1241
|
+
if job_id not in remote_jobs:
|
|
1242
|
+
raise ValueError(f"Job with ID {job_id} not found")
|
|
1243
|
+
|
|
1244
|
+
# Get job information
|
|
1245
|
+
job_info = remote_jobs[job_id]
|
|
1246
|
+
|
|
1247
|
+
# Check if job is completed
|
|
1248
|
+
if job_info["status"] != "completed":
|
|
1249
|
+
raise ValueError(f"Job with ID {job_id} is not completed (status: {job_info['status']})")
|
|
1250
|
+
|
|
1251
|
+
# Download model
|
|
1252
|
+
result = download_remote_model(job_id)
|
|
1253
|
+
|
|
1254
|
+
if not result:
|
|
1255
|
+
raise ValueError(f"Failed to download model for job with ID {job_id}")
|
|
1256
|
+
|
|
1257
|
+
# Update job information
|
|
1258
|
+
job_info["model_path"] = result.get("model_path")
|
|
1259
|
+
job_info["point_cloud_path"] = result.get("point_cloud_path")
|
|
1260
|
+
job_info["downloaded"] = True
|
|
1261
|
+
|
|
1262
|
+
# Update model information if available
|
|
1263
|
+
if "model_id" in job_info and job_info["model_id"] in models:
|
|
1264
|
+
model_id = job_info["model_id"]
|
|
1265
|
+
models[model_id]["model_file"] = result.get("model_path")
|
|
1266
|
+
models[model_id]["point_cloud_file"] = result.get("point_cloud_path")
|
|
1267
|
+
|
|
1268
|
+
return {
|
|
1269
|
+
"job_id": job_id,
|
|
1270
|
+
"model_path": result.get("model_path"),
|
|
1271
|
+
"point_cloud_path": result.get("point_cloud_path"),
|
|
1272
|
+
"format": REMOTE_CUDA_MVS["DEFAULT_OUTPUT_FORMAT"]
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
|
|
1276
|
+
# Add remote job cancellation tool
|
|
1277
|
+
@mcp_server.tool
|
|
1278
|
+
def cancel_remote_job(job_id: str) -> Dict[str, Any]:
|
|
1279
|
+
"""
|
|
1280
|
+
Cancel a remote CUDA MVS processing job.
|
|
1281
|
+
|
|
1282
|
+
Args:
|
|
1283
|
+
job_id: ID of the job to cancel
|
|
1284
|
+
|
|
1285
|
+
Returns:
|
|
1286
|
+
Dictionary with cancellation result
|
|
1287
|
+
"""
|
|
1288
|
+
if not REMOTE_CUDA_MVS["ENABLED"]:
|
|
1289
|
+
raise ValueError("Remote CUDA MVS processing is not enabled")
|
|
1290
|
+
|
|
1291
|
+
if not remote_connection_manager:
|
|
1292
|
+
raise ValueError("Remote CUDA MVS connection manager is not initialized")
|
|
1293
|
+
|
|
1294
|
+
# Check if job exists
|
|
1295
|
+
if job_id not in remote_jobs:
|
|
1296
|
+
raise ValueError(f"Job with ID {job_id} not found")
|
|
1297
|
+
|
|
1298
|
+
# Get job information
|
|
1299
|
+
job_info = remote_jobs[job_id]
|
|
1300
|
+
|
|
1301
|
+
# Cancel job
|
|
1302
|
+
result = cancel_job(job_id)
|
|
1303
|
+
|
|
1304
|
+
if not result:
|
|
1305
|
+
raise ValueError(f"Failed to cancel job with ID {job_id}")
|
|
1306
|
+
|
|
1307
|
+
# Update job information
|
|
1308
|
+
if result.get("cancelled", False):
|
|
1309
|
+
job_info["status"] = "cancelled"
|
|
1310
|
+
job_info["message"] = "Job cancelled by user"
|
|
1311
|
+
|
|
1312
|
+
return {
|
|
1313
|
+
"job_id": job_id,
|
|
1314
|
+
"cancelled": result.get("cancelled", False),
|
|
1315
|
+
"status": job_info["status"],
|
|
1316
|
+
"message": job_info.get("message", "")
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
|
|
1320
|
+
# FastAPI routes
|
|
1321
|
+
@app.post("/tool_call")
|
|
1322
|
+
async def handle_tool_call(request: Request) -> JSONResponse:
|
|
1323
|
+
"""
|
|
1324
|
+
Handle a tool call from a client.
|
|
1325
|
+
|
|
1326
|
+
Args:
|
|
1327
|
+
request: FastAPI request object
|
|
1328
|
+
|
|
1329
|
+
Returns:
|
|
1330
|
+
JSON response with tool call result
|
|
1331
|
+
"""
|
|
1332
|
+
# Parse request
|
|
1333
|
+
data = await request.json()
|
|
1334
|
+
|
|
1335
|
+
# Check if tool name is provided
|
|
1336
|
+
if "tool_name" not in data:
|
|
1337
|
+
raise HTTPException(status_code=400, detail="Tool name is required")
|
|
1338
|
+
|
|
1339
|
+
# Check if tool exists
|
|
1340
|
+
tool_name = data["tool_name"]
|
|
1341
|
+
if tool_name not in mcp_server.tools:
|
|
1342
|
+
raise HTTPException(status_code=404, detail=f"Tool {tool_name} not found")
|
|
1343
|
+
|
|
1344
|
+
# Get tool parameters
|
|
1345
|
+
tool_params = data.get("tool_params", {})
|
|
1346
|
+
|
|
1347
|
+
# Call tool
|
|
1348
|
+
try:
|
|
1349
|
+
result = mcp_server.tools[tool_name](**tool_params)
|
|
1350
|
+
return JSONResponse(content=result)
|
|
1351
|
+
except Exception as e:
|
|
1352
|
+
logger.error(f"Error calling tool {tool_name}: {str(e)}")
|
|
1353
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
1354
|
+
|
|
1355
|
+
@app.get("/ui/preview/{model_id}")
|
|
1356
|
+
async def preview_model(request: Request, model_id: str) -> Response:
|
|
1357
|
+
"""
|
|
1358
|
+
Render a preview page for a model.
|
|
1359
|
+
|
|
1360
|
+
Args:
|
|
1361
|
+
request: FastAPI request object
|
|
1362
|
+
model_id: ID of the model to preview
|
|
1363
|
+
|
|
1364
|
+
Returns:
|
|
1365
|
+
HTML response with model preview
|
|
1366
|
+
"""
|
|
1367
|
+
# Check if model exists
|
|
1368
|
+
if model_id not in models:
|
|
1369
|
+
raise HTTPException(status_code=404, detail=f"Model with ID {model_id} not found")
|
|
1370
|
+
|
|
1371
|
+
# Get model information
|
|
1372
|
+
model_info = models[model_id]
|
|
1373
|
+
|
|
1374
|
+
# Render template
|
|
1375
|
+
return templates.TemplateResponse(
|
|
1376
|
+
"preview.html",
|
|
1377
|
+
{
|
|
1378
|
+
"request": request,
|
|
1379
|
+
"model_id": model_id,
|
|
1380
|
+
"parameters": model_info["parameters"],
|
|
1381
|
+
"previews": model_info["previews"]
|
|
1382
|
+
}
|
|
1383
|
+
)
|
|
1384
|
+
|
|
1385
|
+
@app.get("/preview/{view}/{model_id}")
|
|
1386
|
+
async def get_preview(view: str, model_id: str) -> FileResponse:
|
|
1387
|
+
"""
|
|
1388
|
+
Get a preview image for a model.
|
|
1389
|
+
|
|
1390
|
+
Args:
|
|
1391
|
+
view: View to get preview for
|
|
1392
|
+
model_id: ID of the model
|
|
1393
|
+
|
|
1394
|
+
Returns:
|
|
1395
|
+
Image file response
|
|
1396
|
+
"""
|
|
1397
|
+
# Check if model exists
|
|
1398
|
+
if model_id not in models:
|
|
1399
|
+
raise HTTPException(status_code=404, detail=f"Model with ID {model_id} not found")
|
|
1400
|
+
|
|
1401
|
+
# Get model information
|
|
1402
|
+
model_info = models[model_id]
|
|
1403
|
+
|
|
1404
|
+
# Check if preview exists
|
|
1405
|
+
if view not in model_info["previews"]:
|
|
1406
|
+
raise HTTPException(status_code=404, detail=f"Preview for view {view} not found")
|
|
1407
|
+
|
|
1408
|
+
# Return preview image
|
|
1409
|
+
return FileResponse(model_info["previews"][view])
|
|
1410
|
+
|
|
1411
|
+
@app.get("/download/{model_id}")
|
|
1412
|
+
async def download_model(model_id: str) -> FileResponse:
|
|
1413
|
+
"""
|
|
1414
|
+
Download a model file.
|
|
1415
|
+
|
|
1416
|
+
Args:
|
|
1417
|
+
model_id: ID of the model to download
|
|
1418
|
+
|
|
1419
|
+
Returns:
|
|
1420
|
+
Model file response
|
|
1421
|
+
"""
|
|
1422
|
+
# Check if model exists
|
|
1423
|
+
if model_id not in models:
|
|
1424
|
+
raise HTTPException(status_code=404, detail=f"Model with ID {model_id} not found")
|
|
1425
|
+
|
|
1426
|
+
# Get model information
|
|
1427
|
+
model_info = models[model_id]
|
|
1428
|
+
|
|
1429
|
+
# Check if model file exists
|
|
1430
|
+
if not model_info.get("model_file"):
|
|
1431
|
+
raise HTTPException(status_code=404, detail=f"Model file for model with ID {model_id} not found")
|
|
1432
|
+
|
|
1433
|
+
# Return model file
|
|
1434
|
+
return FileResponse(
|
|
1435
|
+
model_info["model_file"],
|
|
1436
|
+
filename=f"{model_id}.{model_info['format']}"
|
|
1437
|
+
)
|
|
1438
|
+
|
|
1439
|
+
@app.get("/")
|
|
1440
|
+
async def root() -> Dict[str, Any]:
|
|
1441
|
+
"""
|
|
1442
|
+
Root endpoint.
|
|
1443
|
+
|
|
1444
|
+
Returns:
|
|
1445
|
+
Dictionary with server information
|
|
1446
|
+
"""
|
|
1447
|
+
return {
|
|
1448
|
+
"name": "OpenSCAD MCP Server",
|
|
1449
|
+
"version": "1.0.0",
|
|
1450
|
+
"description": "MCP server for OpenSCAD",
|
|
1451
|
+
"tools": list(mcp_server.tools.keys())
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
# Run server
|
|
1455
|
+
if __name__ == "__main__":
|
|
1456
|
+
uvicorn.run("src.main:app", host="0.0.0.0", port=8000, reload=True)
|