@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.
Files changed (192) hide show
  1. package/README.md +116 -0
  2. package/config/mcp/antigravity.json +22 -0
  3. package/config/mcp/claude.json +22 -0
  4. package/config/mcp/codex.toml +25 -0
  5. package/docs/adr/ADR-001-KICAD-OPENSCAD-MCP-STRATEGY.md +334 -0
  6. package/docs/review_documentation.md +121 -0
  7. package/installer.js +358 -0
  8. package/mcp_servers/kicad-mcp-server/.env.example +22 -0
  9. package/mcp_servers/kicad-mcp-server/.github/workflows/ci.yml +36 -0
  10. package/mcp_servers/kicad-mcp-server/CLAUDE.md +487 -0
  11. package/mcp_servers/kicad-mcp-server/README.md +316 -0
  12. package/mcp_servers/kicad-mcp-server/docs/DEVICE_TREE.md +416 -0
  13. package/mcp_servers/kicad-mcp-server/docs/INSTALLATION.md +332 -0
  14. package/mcp_servers/kicad-mcp-server/docs/PIN_ANALYSIS.md +332 -0
  15. package/mcp_servers/kicad-mcp-server/docs/README.md +240 -0
  16. package/mcp_servers/kicad-mcp-server/docs/TESTING.md +613 -0
  17. package/mcp_servers/kicad-mcp-server/docs/VALIDATION.md +268 -0
  18. package/mcp_servers/kicad-mcp-server/pyproject.toml +96 -0
  19. package/mcp_servers/kicad-mcp-server/requirements-dev.txt +16 -0
  20. package/mcp_servers/kicad-mcp-server/requirements-test.txt +24 -0
  21. package/mcp_servers/kicad-mcp-server/requirements.txt +13 -0
  22. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/__init__.py +3 -0
  23. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/__main__.py +17 -0
  24. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/config.py +46 -0
  25. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/models/__init__.py +1 -0
  26. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/models/types.py +87 -0
  27. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/parsers/__init__.py +1 -0
  28. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/parsers/netlist_parser.py +234 -0
  29. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/parsers/pcb_parser.py +375 -0
  30. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/parsers/pcb_parser_kicad.py +327 -0
  31. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/parsers/schematic_parser.py +902 -0
  32. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/server.py +71 -0
  33. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/__init__.py +1 -0
  34. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/arduino/connectivity_test.cpp.j2 +189 -0
  35. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/device_tree/atmega.dts.j2 +77 -0
  36. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/device_tree/esp32.dts.j2 +77 -0
  37. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/device_tree/nrf52.dts.j2 +77 -0
  38. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/device_tree/stm32f4.dts.j2 +89 -0
  39. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/esp_idf/test_suite.c.j2 +340 -0
  40. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/pytest/test_connectivity.py.j2 +147 -0
  41. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/st_hal/hal_test.c.j2 +313 -0
  42. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/pytest_gpio_test.py.j2 +99 -0
  43. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/pytest_i2c_test.py.j2 +117 -0
  44. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/pytest_pinmux_test.py.j2 +43 -0
  45. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/pytest_spi_test.py.j2 +94 -0
  46. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/unity_gpio_test.c.j2 +113 -0
  47. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/unity_i2c_test.c.j2 +101 -0
  48. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/unity_spi_test.c.j2 +94 -0
  49. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/unittest/test_schematic.py.j2 +172 -0
  50. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/__init__.py +36 -0
  51. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/device_tree.py +1187 -0
  52. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/hierarchical_analysis.py +211 -0
  53. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/netlist.py +320 -0
  54. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/parts_registry.py +142 -0
  55. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/pcb.py +955 -0
  56. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/pcb_layout.py +308 -0
  57. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/pin_analysis.py +765 -0
  58. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/project.py +196 -0
  59. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/schematic.py +319 -0
  60. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/schematic_editor.py +674 -0
  61. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/schematic_search.py +158 -0
  62. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/validation.py +866 -0
  63. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/visualization.py +225 -0
  64. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/utils/__init__.py +1 -0
  65. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/utils/file_handlers.py +65 -0
  66. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/utils/kicad_cli.py +103 -0
  67. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/utils/kicad_version.py +103 -0
  68. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/utils/parts_registry.py +197 -0
  69. package/mcp_servers/kicad-mcp-server/tests/__init__.py +1 -0
  70. package/mcp_servers/kicad-mcp-server/tests/examples/ESP32S3_TEST.md +219 -0
  71. package/mcp_servers/kicad-mcp-server/tests/fixtures/README.md +65 -0
  72. package/mcp_servers/kicad-mcp-server/tests/fixtures/__init__.py +1 -0
  73. package/mcp_servers/kicad-mcp-server/tests/fixtures/example_pcb.kicad_pcb +177 -0
  74. package/mcp_servers/kicad-mcp-server/tests/fixtures/example_schematic.kicad_sch +145 -0
  75. package/mcp_servers/kicad-mcp-server/tests/fixtures/hier/child.kicad_sch +24 -0
  76. package/mcp_servers/kicad-mcp-server/tests/fixtures/hier/root.kicad_sch +38 -0
  77. package/mcp_servers/kicad-mcp-server/tests/test_tools/__init__.py +1 -0
  78. package/mcp_servers/kicad-mcp-server/tests/test_tools/test_hierarchical_labels.py +195 -0
  79. package/mcp_servers/kicad-mcp-server/tests/test_tools/test_kicad_cli.py +116 -0
  80. package/mcp_servers/kicad-mcp-server/tests/test_tools/test_netlist_cache_path.py +29 -0
  81. package/mcp_servers/kicad-mcp-server/tests/test_tools/test_schematic.py +189 -0
  82. package/mcp_servers/kicad-mcp-server/tests/test_tools/test_schematic_hierarchy.py +69 -0
  83. package/mcp_servers/kicad-mcp-server/tests/test_tools/test_visualization.py +136 -0
  84. package/mcp_servers/kicad-mcp-server/uv.lock +2873 -0
  85. package/mcp_servers/openscad-mcp-server/.dockerignore +9 -0
  86. package/mcp_servers/openscad-mcp-server/.github/workflows/test.yml +40 -0
  87. package/mcp_servers/openscad-mcp-server/Dockerfile +29 -0
  88. package/mcp_servers/openscad-mcp-server/LICENSE +21 -0
  89. package/mcp_servers/openscad-mcp-server/README.md +154 -0
  90. package/mcp_servers/openscad-mcp-server/docs/audit.md +56 -0
  91. package/mcp_servers/openscad-mcp-server/docs/docker.md +66 -0
  92. package/mcp_servers/openscad-mcp-server/docs/issue-followup.md +19 -0
  93. package/mcp_servers/openscad-mcp-server/docs/jetson.md +17 -0
  94. package/mcp_servers/openscad-mcp-server/glama.json +4 -0
  95. package/mcp_servers/openscad-mcp-server/legacy/README.md +15 -0
  96. package/mcp_servers/openscad-mcp-server/legacy/README.original.md +294 -0
  97. package/mcp_servers/openscad-mcp-server/legacy/implementation_plan.md +100 -0
  98. package/mcp_servers/openscad-mcp-server/legacy/old/download_sam2_checkpoint.py +115 -0
  99. package/mcp_servers/openscad-mcp-server/legacy/old/src/ai/sam_segmentation.py +209 -0
  100. package/mcp_servers/openscad-mcp-server/legacy/old/src/models/threestudio_generator.py +231 -0
  101. package/mcp_servers/openscad-mcp-server/legacy/old/src/workflow/image_to_model_pipeline.py +260 -0
  102. package/mcp_servers/openscad-mcp-server/legacy/old/test_sam2_segmentation.py +96 -0
  103. package/mcp_servers/openscad-mcp-server/legacy/requirements.txt +57 -0
  104. package/mcp_servers/openscad-mcp-server/legacy/rtfmd/README.md +39 -0
  105. package/mcp_servers/openscad-mcp-server/legacy/rtfmd/decisions/ai-driven-code-generation.md +122 -0
  106. package/mcp_servers/openscad-mcp-server/legacy/rtfmd/decisions/export-formats.md +76 -0
  107. package/mcp_servers/openscad-mcp-server/legacy/rtfmd/files/src/ai/ai_service.py.md +51 -0
  108. package/mcp_servers/openscad-mcp-server/legacy/rtfmd/files/src/main.py.md +63 -0
  109. package/mcp_servers/openscad-mcp-server/legacy/rtfmd/files/src/models/code_generator.py.md +63 -0
  110. package/mcp_servers/openscad-mcp-server/legacy/rtfmd/files/src/nlp/parameter_extractor.py.md +63 -0
  111. package/mcp_servers/openscad-mcp-server/legacy/rtfmd/knowledge/ai/natural-language-processing.md +78 -0
  112. package/mcp_servers/openscad-mcp-server/legacy/rtfmd/knowledge/nlp/parameter-extraction.md +173 -0
  113. package/mcp_servers/openscad-mcp-server/legacy/rtfmd/knowledge/openscad/export-formats.md +91 -0
  114. package/mcp_servers/openscad-mcp-server/legacy/rtfmd/knowledge/openscad/openscad-basics.md +66 -0
  115. package/mcp_servers/openscad-mcp-server/legacy/rtfmd/knowledge/openscad/primitive-testing.md +79 -0
  116. package/mcp_servers/openscad-mcp-server/legacy/src/__init__.py +0 -0
  117. package/mcp_servers/openscad-mcp-server/legacy/src/ai/ai_service.py +257 -0
  118. package/mcp_servers/openscad-mcp-server/legacy/src/ai/gemini_api.py +161 -0
  119. package/mcp_servers/openscad-mcp-server/legacy/src/ai/venice_api.py +203 -0
  120. package/mcp_servers/openscad-mcp-server/legacy/src/config.py +121 -0
  121. package/mcp_servers/openscad-mcp-server/legacy/src/main.py +1456 -0
  122. package/mcp_servers/openscad-mcp-server/legacy/src/main.py.new +404 -0
  123. package/mcp_servers/openscad-mcp-server/legacy/src/main_remote.py +401 -0
  124. package/mcp_servers/openscad-mcp-server/legacy/src/models/__init__.py +0 -0
  125. package/mcp_servers/openscad-mcp-server/legacy/src/models/code_generator.py +321 -0
  126. package/mcp_servers/openscad-mcp-server/legacy/src/models/cuda_mvs.py +209 -0
  127. package/mcp_servers/openscad-mcp-server/legacy/src/models/scad_templates/basic_shapes.scad +144 -0
  128. package/mcp_servers/openscad-mcp-server/legacy/src/nlp/__init__.py +0 -0
  129. package/mcp_servers/openscad-mcp-server/legacy/src/nlp/parameter_extractor.py +388 -0
  130. package/mcp_servers/openscad-mcp-server/legacy/src/openscad_wrapper/__init__.py +0 -0
  131. package/mcp_servers/openscad-mcp-server/legacy/src/openscad_wrapper/wrapper.py +418 -0
  132. package/mcp_servers/openscad-mcp-server/legacy/src/printer_discovery/__init__.py +1 -0
  133. package/mcp_servers/openscad-mcp-server/legacy/src/printer_discovery/printer_discovery.py +471 -0
  134. package/mcp_servers/openscad-mcp-server/legacy/src/remote/connection_manager.py +537 -0
  135. package/mcp_servers/openscad-mcp-server/legacy/src/remote/cuda_mvs_client.py +435 -0
  136. package/mcp_servers/openscad-mcp-server/legacy/src/remote/cuda_mvs_server.py +787 -0
  137. package/mcp_servers/openscad-mcp-server/legacy/src/remote/error_handling.py +415 -0
  138. package/mcp_servers/openscad-mcp-server/legacy/src/testing/__init__.py +0 -0
  139. package/mcp_servers/openscad-mcp-server/legacy/src/testing/primitive_tester.py +203 -0
  140. package/mcp_servers/openscad-mcp-server/legacy/src/testing/test_primitives.py +98 -0
  141. package/mcp_servers/openscad-mcp-server/legacy/src/utils/__init__.py +1 -0
  142. package/mcp_servers/openscad-mcp-server/legacy/src/utils/cad_exporter.py +241 -0
  143. package/mcp_servers/openscad-mcp-server/legacy/src/utils/format_validator.py +206 -0
  144. package/mcp_servers/openscad-mcp-server/legacy/src/utils/stl_exporter.py +140 -0
  145. package/mcp_servers/openscad-mcp-server/legacy/src/utils/stl_repair.py +91 -0
  146. package/mcp_servers/openscad-mcp-server/legacy/src/utils/stl_validator.py +123 -0
  147. package/mcp_servers/openscad-mcp-server/legacy/src/visualization/__init__.py +0 -0
  148. package/mcp_servers/openscad-mcp-server/legacy/src/visualization/headless_renderer.py +52 -0
  149. package/mcp_servers/openscad-mcp-server/legacy/src/visualization/renderer.py +177 -0
  150. package/mcp_servers/openscad-mcp-server/legacy/src/visualization/web_interface.py +639 -0
  151. package/mcp_servers/openscad-mcp-server/legacy/src/workflow/image_approval.py +148 -0
  152. package/mcp_servers/openscad-mcp-server/legacy/src/workflow/multi_view_to_model_pipeline.py +338 -0
  153. package/mcp_servers/openscad-mcp-server/legacy/test_complete_workflow.py +374 -0
  154. package/mcp_servers/openscad-mcp-server/legacy/test_cuda_mvs.py +191 -0
  155. package/mcp_servers/openscad-mcp-server/legacy/test_gemini_api.py +168 -0
  156. package/mcp_servers/openscad-mcp-server/legacy/test_image_approval.py +192 -0
  157. package/mcp_servers/openscad-mcp-server/legacy/test_image_approval_workflow.py +251 -0
  158. package/mcp_servers/openscad-mcp-server/legacy/test_image_to_model_pipeline.py +145 -0
  159. package/mcp_servers/openscad-mcp-server/legacy/test_model_selection.py +41 -0
  160. package/mcp_servers/openscad-mcp-server/legacy/test_multi_view_pipeline.py +290 -0
  161. package/mcp_servers/openscad-mcp-server/legacy/test_primitives.sh +13 -0
  162. package/mcp_servers/openscad-mcp-server/legacy/test_rabbit_direct.py +71 -0
  163. package/mcp_servers/openscad-mcp-server/legacy/test_remote_cuda_mvs.py +283 -0
  164. package/mcp_servers/openscad-mcp-server/legacy/test_venice_example.py +69 -0
  165. package/mcp_servers/openscad-mcp-server/pyproject.toml +33 -0
  166. package/mcp_servers/openscad-mcp-server/requirements.txt +2 -0
  167. package/mcp_servers/openscad-mcp-server/scad/simple_cube.scad +2 -0
  168. package/mcp_servers/openscad-mcp-server/scripts/test_docker.py +220 -0
  169. package/mcp_servers/openscad-mcp-server/src/openscad_mcp/__init__.py +3 -0
  170. package/mcp_servers/openscad-mcp-server/src/openscad_mcp/__main__.py +3 -0
  171. package/mcp_servers/openscad-mcp-server/src/openscad_mcp/engine.py +94 -0
  172. package/mcp_servers/openscad-mcp-server/src/openscad_mcp/geometry.py +242 -0
  173. package/mcp_servers/openscad-mcp-server/src/openscad_mcp/server.py +245 -0
  174. package/mcp_servers/openscad-mcp-server/src/openscad_mcp/service.py +190 -0
  175. package/mcp_servers/openscad-mcp-server/tests/conftest.py +17 -0
  176. package/mcp_servers/openscad-mcp-server/tests/test_engine.py +37 -0
  177. package/mcp_servers/openscad-mcp-server/tests/test_geometry.py +106 -0
  178. package/mcp_servers/openscad-mcp-server/tests/test_integration.py +172 -0
  179. package/mcp_servers/openscad-mcp-server/tests/test_transports.py +314 -0
  180. package/mcp_servers/openscad-mcp-server/uv.lock +1123 -0
  181. package/package.json +44 -0
  182. package/scripts/install_mcps.sh +86 -0
  183. package/scripts/openscad_wrapper.sh +62 -0
  184. package/scripts/run_kicad_mcp.sh +25 -0
  185. package/scripts/run_openscad_mcp.sh +41 -0
  186. package/scripts/test_mcp_connection.py +626 -0
  187. package/scripts/test_mcp_connection.sh +168 -0
  188. package/skills/code-first-hardware-design/SKILL.md +260 -0
  189. package/skills/pcb-constraint-definition/SKILL.md +236 -0
  190. package/skills/pcb-layout-routing-automation/SKILL.md +153 -0
  191. package/skills/pcb-validation-dfm-signoff/SKILL.md +194 -0
  192. package/skills/schematic-datasheet-analysis/SKILL.md +202 -0
@@ -0,0 +1,787 @@
1
+ """
2
+ Server for remote CUDA Multi-View Stereo processing.
3
+
4
+ This module provides a server implementation that can be deployed on a machine
5
+ with CUDA capabilities to process multi-view images into 3D models remotely.
6
+ """
7
+
8
+ import os
9
+ import sys
10
+ import json
11
+ import uuid
12
+ import logging
13
+ import argparse
14
+ import tempfile
15
+ import subprocess
16
+ from typing import Dict, List, Optional, Any, Union
17
+ from pathlib import Path
18
+ import shutil
19
+ import time
20
+
21
+ from fastapi import FastAPI, File, UploadFile, Form, HTTPException, BackgroundTasks, Depends
22
+ from fastapi.responses import FileResponse, JSONResponse
23
+ from fastapi.middleware.cors import CORSMiddleware
24
+ from fastapi.security import APIKeyHeader
25
+ import uvicorn
26
+ from pydantic import BaseModel, Field
27
+ from zeroconf import ServiceInfo, Zeroconf
28
+
29
+ # Configure logging
30
+ logging.basicConfig(level=logging.INFO)
31
+ logger = logging.getLogger(__name__)
32
+
33
+ # Constants
34
+ DEFAULT_PORT = 8765
35
+ DEFAULT_HOST = "0.0.0.0"
36
+ DEFAULT_CUDA_MVS_PATH = "/opt/cuda-multi-view-stereo"
37
+ DEFAULT_OUTPUT_DIR = "output"
38
+ DEFAULT_MAX_JOBS = 5
39
+ DEFAULT_MAX_IMAGES_PER_JOB = 50
40
+ DEFAULT_JOB_TIMEOUT = 3600 # 1 hour
41
+
42
+ # Models
43
+ class JobStatus(BaseModel):
44
+ """Job status model."""
45
+ job_id: str
46
+ status: str = "created" # created, uploading, processing, completed, failed
47
+ created_at: float = Field(default_factory=time.time)
48
+ updated_at: float = Field(default_factory=time.time)
49
+ num_images: int = 0
50
+ images_uploaded: int = 0
51
+ progress: float = 0.0
52
+ error_message: Optional[str] = None
53
+ model_id: Optional[str] = None
54
+ output_dir: Optional[str] = None
55
+ point_cloud_file: Optional[str] = None
56
+ obj_file: Optional[str] = None
57
+ processing_time: Optional[float] = None
58
+
59
+ class ServerConfig(BaseModel):
60
+ """Server configuration model."""
61
+ cuda_mvs_path: str = DEFAULT_CUDA_MVS_PATH
62
+ output_dir: str = DEFAULT_OUTPUT_DIR
63
+ max_jobs: int = DEFAULT_MAX_JOBS
64
+ max_images_per_job: int = DEFAULT_MAX_IMAGES_PER_JOB
65
+ job_timeout: int = DEFAULT_JOB_TIMEOUT
66
+ api_key: Optional[str] = None
67
+ server_name: str = "CUDA MVS Server"
68
+ advertise_service: bool = True
69
+ gpu_info: Optional[str] = None
70
+
71
+ class JobRequest(BaseModel):
72
+ """Job creation request model."""
73
+ num_images: int
74
+
75
+ class ProcessRequest(BaseModel):
76
+ """Process job request model."""
77
+ reconstruction_quality: str = "normal" # low, normal, high
78
+ output_formats: List[str] = ["obj", "ply"]
79
+
80
+ class ServerInfo(BaseModel):
81
+ """Server information model."""
82
+ server_id: str
83
+ name: str
84
+ version: str = "1.0.0"
85
+ status: str = "running"
86
+ capabilities: Dict[str, Any]
87
+ jobs: Dict[str, Any]
88
+ uptime: float
89
+
90
+ # Server implementation
91
+ class CUDAMVSServer:
92
+ """
93
+ Server for remote CUDA Multi-View Stereo processing.
94
+
95
+ This server:
96
+ 1. Accepts image uploads
97
+ 2. Processes images using CUDA MVS
98
+ 3. Provides 3D model downloads
99
+ 4. Advertises itself on the local network
100
+ """
101
+
102
+ def __init__(self, config: ServerConfig):
103
+ """
104
+ Initialize the CUDA MVS server.
105
+
106
+ Args:
107
+ config: Server configuration
108
+ """
109
+ self.config = config
110
+ self.app = FastAPI(title="CUDA MVS Server", description="Remote CUDA Multi-View Stereo processing server")
111
+ self.jobs: Dict[str, JobStatus] = {}
112
+ self.server_id = str(uuid.uuid4())
113
+ self.start_time = time.time()
114
+ self.zeroconf = None
115
+
116
+ # Create output directory
117
+ os.makedirs(config.output_dir, exist_ok=True)
118
+
119
+ # Configure CORS
120
+ self.app.add_middleware(
121
+ CORSMiddleware,
122
+ allow_origins=["*"],
123
+ allow_credentials=True,
124
+ allow_methods=["*"],
125
+ allow_headers=["*"],
126
+ )
127
+
128
+ # Detect GPU info
129
+ self.detect_gpu_info()
130
+
131
+ # Register routes
132
+ self.register_routes()
133
+
134
+ # Advertise service if enabled
135
+ if config.advertise_service:
136
+ self.advertise_service()
137
+
138
+ def detect_gpu_info(self):
139
+ """Detect GPU information."""
140
+ if not self.config.gpu_info:
141
+ try:
142
+ # Try to get GPU info using nvidia-smi
143
+ result = subprocess.run(
144
+ ["nvidia-smi", "--query-gpu=name,memory.total", "--format=csv,noheader"],
145
+ capture_output=True,
146
+ text=True,
147
+ check=True
148
+ )
149
+ self.config.gpu_info = result.stdout.strip()
150
+ except (subprocess.SubprocessError, FileNotFoundError):
151
+ # If nvidia-smi fails, try lspci
152
+ try:
153
+ result = subprocess.run(
154
+ ["lspci", "-v", "|", "grep", "-i", "vga"],
155
+ capture_output=True,
156
+ text=True,
157
+ shell=True
158
+ )
159
+ self.config.gpu_info = result.stdout.strip()
160
+ except subprocess.SubprocessError:
161
+ self.config.gpu_info = "Unknown GPU"
162
+
163
+ def register_routes(self):
164
+ """Register API routes."""
165
+
166
+ # Authentication dependency
167
+ api_key_header = APIKeyHeader(name="Authorization", auto_error=False)
168
+
169
+ async def verify_api_key(api_key: str = Depends(api_key_header)):
170
+ if self.config.api_key:
171
+ if not api_key:
172
+ raise HTTPException(status_code=401, detail="API key required")
173
+
174
+ # Check if the API key is in the format "Bearer <key>"
175
+ if api_key.startswith("Bearer "):
176
+ api_key = api_key[7:]
177
+
178
+ if api_key != self.config.api_key:
179
+ raise HTTPException(status_code=401, detail="Invalid API key")
180
+ return True
181
+
182
+ # Status endpoint
183
+ @self.app.get("/api/status")
184
+ async def get_status():
185
+ return self.get_server_info()
186
+
187
+ # Job management endpoints
188
+ @self.app.post("/api/jobs", status_code=201, dependencies=[Depends(verify_api_key)])
189
+ async def create_job(job_request: JobRequest):
190
+ return self.create_job(job_request.num_images)
191
+
192
+ @self.app.get("/api/jobs", dependencies=[Depends(verify_api_key)])
193
+ async def list_jobs():
194
+ return {"jobs": self.jobs}
195
+
196
+ @self.app.get("/api/jobs/{job_id}", dependencies=[Depends(verify_api_key)])
197
+ async def get_job(job_id: str):
198
+ if job_id not in self.jobs:
199
+ raise HTTPException(status_code=404, detail="Job not found")
200
+ return self.jobs[job_id]
201
+
202
+ @self.app.delete("/api/jobs/{job_id}", dependencies=[Depends(verify_api_key)])
203
+ async def cancel_job(job_id: str):
204
+ if job_id not in self.jobs:
205
+ raise HTTPException(status_code=404, detail="Job not found")
206
+
207
+ # Cancel the job
208
+ job = self.jobs[job_id]
209
+ if job.status in ["created", "uploading", "processing"]:
210
+ job.status = "cancelled"
211
+ job.updated_at = time.time()
212
+
213
+ # Clean up job directory
214
+ job_dir = os.path.join(self.config.output_dir, job_id)
215
+ if os.path.exists(job_dir):
216
+ shutil.rmtree(job_dir)
217
+
218
+ return {"status": "success", "message": "Job cancelled"}
219
+ else:
220
+ return {"status": "error", "message": f"Cannot cancel job in {job.status} state"}
221
+
222
+ # Image upload endpoint
223
+ @self.app.post("/api/jobs/{job_id}/images", dependencies=[Depends(verify_api_key)])
224
+ async def upload_image(
225
+ job_id: str,
226
+ file: UploadFile = File(...),
227
+ metadata: str = Form(None)
228
+ ):
229
+ if job_id not in self.jobs:
230
+ raise HTTPException(status_code=404, detail="Job not found")
231
+
232
+ job = self.jobs[job_id]
233
+
234
+ # Check if job is in a valid state for uploads
235
+ if job.status not in ["created", "uploading"]:
236
+ raise HTTPException(
237
+ status_code=400,
238
+ detail=f"Cannot upload images to job in {job.status} state"
239
+ )
240
+
241
+ # Check if we've reached the maximum number of images
242
+ if job.images_uploaded >= job.num_images:
243
+ raise HTTPException(
244
+ status_code=400,
245
+ detail=f"Maximum number of images ({job.num_images}) already uploaded"
246
+ )
247
+
248
+ # Update job status
249
+ job.status = "uploading"
250
+ job.updated_at = time.time()
251
+
252
+ # Create job directory if it doesn't exist
253
+ job_dir = os.path.join(self.config.output_dir, job_id, "images")
254
+ os.makedirs(job_dir, exist_ok=True)
255
+
256
+ # Parse metadata
257
+ image_metadata = {}
258
+ if metadata:
259
+ try:
260
+ image_metadata = json.loads(metadata)
261
+ except json.JSONDecodeError:
262
+ logger.warning(f"Invalid metadata format: {metadata}")
263
+
264
+ # Save the file
265
+ image_index = job.images_uploaded
266
+ file_extension = os.path.splitext(file.filename)[1]
267
+ image_path = os.path.join(job_dir, f"image_{image_index:04d}{file_extension}")
268
+
269
+ with open(image_path, "wb") as f:
270
+ shutil.copyfileobj(file.file, f)
271
+
272
+ # Update job status
273
+ job.images_uploaded += 1
274
+ job.updated_at = time.time()
275
+
276
+ return {
277
+ "status": "success",
278
+ "job_id": job_id,
279
+ "image_index": image_index,
280
+ "image_path": image_path,
281
+ "images_uploaded": job.images_uploaded,
282
+ "total_images": job.num_images
283
+ }
284
+
285
+ # Process job endpoint
286
+ @self.app.post("/api/jobs/{job_id}/process", status_code=202, dependencies=[Depends(verify_api_key)])
287
+ async def process_job(
288
+ job_id: str,
289
+ process_request: ProcessRequest,
290
+ background_tasks: BackgroundTasks
291
+ ):
292
+ if job_id not in self.jobs:
293
+ raise HTTPException(status_code=404, detail="Job not found")
294
+
295
+ job = self.jobs[job_id]
296
+
297
+ # Check if job is in a valid state for processing
298
+ if job.status != "uploading":
299
+ raise HTTPException(
300
+ status_code=400,
301
+ detail=f"Cannot process job in {job.status} state"
302
+ )
303
+
304
+ # Check if all images have been uploaded
305
+ if job.images_uploaded < job.num_images:
306
+ raise HTTPException(
307
+ status_code=400,
308
+ detail=f"Not all images uploaded ({job.images_uploaded}/{job.num_images})"
309
+ )
310
+
311
+ # Update job status
312
+ job.status = "processing"
313
+ job.updated_at = time.time()
314
+ job.progress = 0.0
315
+
316
+ # Start processing in the background
317
+ background_tasks.add_task(
318
+ self.process_job_task,
319
+ job_id,
320
+ process_request.reconstruction_quality,
321
+ process_request.output_formats
322
+ )
323
+
324
+ return {
325
+ "status": "success",
326
+ "job_id": job_id,
327
+ "message": "Processing started"
328
+ }
329
+
330
+ # Model download endpoint
331
+ @self.app.get("/api/jobs/{job_id}/model", dependencies=[Depends(verify_api_key)])
332
+ async def download_model(job_id: str, format: str = "obj"):
333
+ if job_id not in self.jobs:
334
+ raise HTTPException(status_code=404, detail="Job not found")
335
+
336
+ job = self.jobs[job_id]
337
+
338
+ # Check if job is completed
339
+ if job.status != "completed":
340
+ raise HTTPException(
341
+ status_code=400,
342
+ detail=f"Job is not completed. Current status: {job.status}"
343
+ )
344
+
345
+ # Check if the requested format is available
346
+ if format == "obj" and job.obj_file:
347
+ return FileResponse(job.obj_file, filename=f"{job.model_id}.obj")
348
+ elif format == "ply" and job.point_cloud_file:
349
+ return FileResponse(job.point_cloud_file, filename=f"{job.model_id}.ply")
350
+ else:
351
+ raise HTTPException(
352
+ status_code=404,
353
+ detail=f"Model in {format} format not available"
354
+ )
355
+
356
+ def create_job(self, num_images: int) -> Dict[str, Any]:
357
+ """
358
+ Create a new job.
359
+
360
+ Args:
361
+ num_images: Number of images to be uploaded
362
+
363
+ Returns:
364
+ Dictionary with job information
365
+ """
366
+ # Check if we've reached the maximum number of jobs
367
+ active_jobs = sum(1 for job in self.jobs.values() if job.status in ["created", "uploading", "processing"])
368
+ if active_jobs >= self.config.max_jobs:
369
+ raise HTTPException(
370
+ status_code=429,
371
+ detail=f"Maximum number of active jobs ({self.config.max_jobs}) reached"
372
+ )
373
+
374
+ # Check if the number of images is valid
375
+ if num_images <= 0 or num_images > self.config.max_images_per_job:
376
+ raise HTTPException(
377
+ status_code=400,
378
+ detail=f"Number of images must be between 1 and {self.config.max_images_per_job}"
379
+ )
380
+
381
+ # Create a new job
382
+ job_id = str(uuid.uuid4())
383
+ job = JobStatus(
384
+ job_id=job_id,
385
+ num_images=num_images
386
+ )
387
+
388
+ # Add job to the list
389
+ self.jobs[job_id] = job
390
+
391
+ # Create job directory
392
+ job_dir = os.path.join(self.config.output_dir, job_id)
393
+ os.makedirs(job_dir, exist_ok=True)
394
+
395
+ return job.dict()
396
+
397
+ async def process_job_task(
398
+ self,
399
+ job_id: str,
400
+ reconstruction_quality: str,
401
+ output_formats: List[str]
402
+ ):
403
+ """
404
+ Process a job in the background.
405
+
406
+ Args:
407
+ job_id: ID of the job to process
408
+ reconstruction_quality: Quality of the reconstruction (low, normal, high)
409
+ output_formats: List of output formats to generate
410
+ """
411
+ if job_id not in self.jobs:
412
+ logger.error(f"Job {job_id} not found")
413
+ return
414
+
415
+ job = self.jobs[job_id]
416
+ job_dir = os.path.join(self.config.output_dir, job_id)
417
+ images_dir = os.path.join(job_dir, "images")
418
+ output_dir = os.path.join(job_dir, "output")
419
+
420
+ # Create output directory
421
+ os.makedirs(output_dir, exist_ok=True)
422
+
423
+ # Generate a model ID
424
+ model_id = f"model_{job_id[:8]}"
425
+ job.model_id = model_id
426
+
427
+ # Update job status
428
+ job.status = "processing"
429
+ job.progress = 0.0
430
+ job.updated_at = time.time()
431
+
432
+ try:
433
+ # Start timing
434
+ start_time = time.time()
435
+
436
+ # Run CUDA MVS
437
+ await self.run_cuda_mvs(
438
+ job,
439
+ images_dir,
440
+ output_dir,
441
+ model_id,
442
+ reconstruction_quality
443
+ )
444
+
445
+ # Convert output formats if needed
446
+ if "obj" in output_formats and not job.obj_file:
447
+ # Convert PLY to OBJ if needed
448
+ ply_file = job.point_cloud_file
449
+ if ply_file and os.path.exists(ply_file):
450
+ obj_file = os.path.join(output_dir, f"{model_id}.obj")
451
+ await self.convert_ply_to_obj(ply_file, obj_file)
452
+ job.obj_file = obj_file
453
+
454
+ # Calculate processing time
455
+ job.processing_time = time.time() - start_time
456
+
457
+ # Update job status
458
+ job.status = "completed"
459
+ job.progress = 100.0
460
+ job.updated_at = time.time()
461
+ job.output_dir = output_dir
462
+
463
+ logger.info(f"Job {job_id} completed successfully in {job.processing_time:.2f} seconds")
464
+
465
+ except Exception as e:
466
+ # Update job status
467
+ job.status = "failed"
468
+ job.error_message = str(e)
469
+ job.updated_at = time.time()
470
+
471
+ logger.error(f"Job {job_id} failed: {e}")
472
+
473
+ async def run_cuda_mvs(
474
+ self,
475
+ job: JobStatus,
476
+ images_dir: str,
477
+ output_dir: str,
478
+ model_id: str,
479
+ reconstruction_quality: str
480
+ ):
481
+ """
482
+ Run CUDA MVS on the uploaded images.
483
+
484
+ Args:
485
+ job: Job status object
486
+ images_dir: Directory containing the images
487
+ output_dir: Directory to save the output
488
+ model_id: ID of the model
489
+ reconstruction_quality: Quality of the reconstruction
490
+ """
491
+ # Check if CUDA MVS is installed
492
+ cuda_mvs_executable = os.path.join(self.config.cuda_mvs_path, "build", "app_patch_match_mvs")
493
+ if not os.path.exists(cuda_mvs_executable):
494
+ raise FileNotFoundError(f"CUDA MVS executable not found at {cuda_mvs_executable}")
495
+
496
+ # Create a list of image paths
497
+ image_files = []
498
+ for file in os.listdir(images_dir):
499
+ if file.lower().endswith((".jpg", ".jpeg", ".png")):
500
+ image_files.append(os.path.join(images_dir, file))
501
+
502
+ if not image_files:
503
+ raise ValueError("No valid image files found")
504
+
505
+ # Sort image files to ensure consistent order
506
+ image_files.sort()
507
+
508
+ # Create a camera parameter file
509
+ camera_params_file = os.path.join(output_dir, "cameras.txt")
510
+ await self.generate_camera_params(image_files, camera_params_file)
511
+
512
+ # Set quality parameters
513
+ if reconstruction_quality == "low":
514
+ num_iterations = 3
515
+ max_resolution = 1024
516
+ elif reconstruction_quality == "normal":
517
+ num_iterations = 5
518
+ max_resolution = 2048
519
+ elif reconstruction_quality == "high":
520
+ num_iterations = 7
521
+ max_resolution = 4096
522
+ else:
523
+ num_iterations = 5
524
+ max_resolution = 2048
525
+
526
+ # Prepare output files
527
+ point_cloud_file = os.path.join(output_dir, f"{model_id}.ply")
528
+
529
+ # Build the command
530
+ cmd = [
531
+ cuda_mvs_executable,
532
+ "--input_folder", images_dir,
533
+ "--camera_file", camera_params_file,
534
+ "--output_folder", output_dir,
535
+ "--output_file", point_cloud_file,
536
+ "--num_iterations", str(num_iterations),
537
+ "--max_resolution", str(max_resolution)
538
+ ]
539
+
540
+ # Run the command
541
+ logger.info(f"Running CUDA MVS: {' '.join(cmd)}")
542
+
543
+ process = subprocess.Popen(
544
+ cmd,
545
+ stdout=subprocess.PIPE,
546
+ stderr=subprocess.PIPE,
547
+ text=True
548
+ )
549
+
550
+ # Monitor progress
551
+ while True:
552
+ if process.poll() is not None:
553
+ break
554
+
555
+ # Read output line by line
556
+ output = process.stdout.readline()
557
+ if output:
558
+ # Try to parse progress information
559
+ if "Progress:" in output:
560
+ try:
561
+ progress_str = output.split("Progress:")[1].strip().rstrip("%")
562
+ progress = float(progress_str)
563
+ job.progress = progress
564
+ job.updated_at = time.time()
565
+ except (ValueError, IndexError):
566
+ pass
567
+
568
+ # Sleep briefly to avoid CPU spinning
569
+ await asyncio.sleep(0.1)
570
+
571
+ # Get the final output
572
+ stdout, stderr = process.communicate()
573
+
574
+ # Check if the process was successful
575
+ if process.returncode != 0:
576
+ raise RuntimeError(f"CUDA MVS failed with error: {stderr}")
577
+
578
+ # Check if the output file was created
579
+ if not os.path.exists(point_cloud_file):
580
+ raise FileNotFoundError(f"Output file not created: {point_cloud_file}")
581
+
582
+ # Update job with the output file
583
+ job.point_cloud_file = point_cloud_file
584
+
585
+ async def generate_camera_params(self, image_files: List[str], output_file: str):
586
+ """
587
+ Generate camera parameters for CUDA MVS.
588
+
589
+ Args:
590
+ image_files: List of image files
591
+ output_file: Output file for camera parameters
592
+ """
593
+ # For now, use a simple camera model with default parameters
594
+ # In a real implementation, this would use structure from motion
595
+ # to estimate camera parameters from the images
596
+
597
+ with open(output_file, "w") as f:
598
+ f.write(f"# Camera parameters for {len(image_files)} images\n")
599
+ f.write("# Format: image_name width height fx fy cx cy\n")
600
+
601
+ for i, image_file in enumerate(image_files):
602
+ # Get image dimensions
603
+ from PIL import Image
604
+ with Image.open(image_file) as img:
605
+ width, height = img.size
606
+
607
+ # Use default camera parameters
608
+ fx = width * 1.2 # Focal length x
609
+ fy = height * 1.2 # Focal length y
610
+ cx = width / 2 # Principal point x
611
+ cy = height / 2 # Principal point y
612
+
613
+ # Write camera parameters
614
+ f.write(f"{os.path.basename(image_file)} {width} {height} {fx} {fy} {cx} {cy}\n")
615
+
616
+ async def convert_ply_to_obj(self, ply_file: str, obj_file: str):
617
+ """
618
+ Convert PLY file to OBJ format.
619
+
620
+ Args:
621
+ ply_file: Input PLY file
622
+ obj_file: Output OBJ file
623
+ """
624
+ try:
625
+ import open3d as o3d
626
+
627
+ # Load the PLY file
628
+ mesh = o3d.io.read_triangle_mesh(ply_file)
629
+
630
+ # Save as OBJ
631
+ o3d.io.write_triangle_mesh(obj_file, mesh)
632
+
633
+ logger.info(f"Converted {ply_file} to {obj_file}")
634
+
635
+ except ImportError:
636
+ # If open3d is not available, use a subprocess
637
+ try:
638
+ # Try using meshlab
639
+ subprocess.run(
640
+ ["meshlabserver", "-i", ply_file, "-o", obj_file],
641
+ check=True,
642
+ capture_output=True
643
+ )
644
+ except (subprocess.SubprocessError, FileNotFoundError):
645
+ # If meshlab is not available, try using assimp
646
+ try:
647
+ subprocess.run(
648
+ ["assimp", "export", ply_file, obj_file],
649
+ check=True,
650
+ capture_output=True
651
+ )
652
+ except (subprocess.SubprocessError, FileNotFoundError):
653
+ raise RuntimeError("No suitable tool found to convert PLY to OBJ")
654
+
655
+ def get_server_info(self) -> Dict[str, Any]:
656
+ """
657
+ Get server information.
658
+
659
+ Returns:
660
+ Dictionary with server information
661
+ """
662
+ # Count active jobs
663
+ active_jobs = sum(1 for job in self.jobs.values() if job.status in ["created", "uploading", "processing"])
664
+
665
+ # Get server capabilities
666
+ capabilities = {
667
+ "max_jobs": self.config.max_jobs,
668
+ "max_images_per_job": self.config.max_images_per_job,
669
+ "job_timeout": self.config.job_timeout,
670
+ "supported_formats": ["obj", "ply"],
671
+ "gpu_info": self.config.gpu_info
672
+ }
673
+
674
+ # Get job summary
675
+ job_summary = {
676
+ "total": len(self.jobs),
677
+ "active": active_jobs,
678
+ "completed": sum(1 for job in self.jobs.values() if job.status == "completed"),
679
+ "failed": sum(1 for job in self.jobs.values() if job.status == "failed")
680
+ }
681
+
682
+ return {
683
+ "server_id": self.server_id,
684
+ "name": self.config.server_name,
685
+ "version": "1.0.0",
686
+ "status": "running",
687
+ "capabilities": capabilities,
688
+ "jobs": job_summary,
689
+ "uptime": time.time() - self.start_time
690
+ }
691
+
692
+ def advertise_service(self):
693
+ """Advertise the server on the local network using Zeroconf."""
694
+ try:
695
+ import socket
696
+ from zeroconf import ServiceInfo, Zeroconf
697
+
698
+ # Get local IP address
699
+ s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
700
+ s.connect(("8.8.8.8", 80))
701
+ local_ip = s.getsockname()[0]
702
+ s.close()
703
+
704
+ # Create service info
705
+ capabilities = {
706
+ "max_jobs": self.config.max_jobs,
707
+ "max_images_per_job": self.config.max_images_per_job,
708
+ "supported_formats": ["obj", "ply"],
709
+ "gpu_info": self.config.gpu_info
710
+ }
711
+
712
+ service_info = ServiceInfo(
713
+ "_cudamvs._tcp.local.",
714
+ f"{self.server_id}._cudamvs._tcp.local.",
715
+ addresses=[socket.inet_aton(local_ip)],
716
+ port=DEFAULT_PORT,
717
+ properties={
718
+ b"name": self.config.server_name.encode("utf-8"),
719
+ b"capabilities": json.dumps(capabilities).encode("utf-8")
720
+ }
721
+ )
722
+
723
+ # Register service
724
+ self.zeroconf = Zeroconf()
725
+ self.zeroconf.register_service(service_info)
726
+
727
+ logger.info(f"Advertising CUDA MVS service on {local_ip}:{DEFAULT_PORT}")
728
+
729
+ except Exception as e:
730
+ logger.error(f"Failed to advertise service: {e}")
731
+
732
+ def run(self, host: str = DEFAULT_HOST, port: int = DEFAULT_PORT):
733
+ """
734
+ Run the server.
735
+
736
+ Args:
737
+ host: Host to bind to
738
+ port: Port to bind to
739
+ """
740
+ uvicorn.run(self.app, host=host, port=port)
741
+
742
+ def cleanup(self):
743
+ """Clean up resources."""
744
+ if self.zeroconf:
745
+ self.zeroconf.close()
746
+
747
+ # Main entry point
748
+ def main():
749
+ """Main entry point."""
750
+ parser = argparse.ArgumentParser(description="CUDA MVS Server")
751
+ parser.add_argument("--host", default=DEFAULT_HOST, help="Host to bind to")
752
+ parser.add_argument("--port", type=int, default=DEFAULT_PORT, help="Port to bind to")
753
+ parser.add_argument("--cuda-mvs-path", default=DEFAULT_CUDA_MVS_PATH, help="Path to CUDA MVS installation")
754
+ parser.add_argument("--output-dir", default=DEFAULT_OUTPUT_DIR, help="Output directory")
755
+ parser.add_argument("--max-jobs", type=int, default=DEFAULT_MAX_JOBS, help="Maximum number of concurrent jobs")
756
+ parser.add_argument("--max-images", type=int, default=DEFAULT_MAX_IMAGES_PER_JOB, help="Maximum images per job")
757
+ parser.add_argument("--job-timeout", type=int, default=DEFAULT_JOB_TIMEOUT, help="Job timeout in seconds")
758
+ parser.add_argument("--api-key", help="API key for authentication")
759
+ parser.add_argument("--server-name", default="CUDA MVS Server", help="Server name")
760
+ parser.add_argument("--no-advertise", action="store_true", help="Don't advertise service on the network")
761
+
762
+ args = parser.parse_args()
763
+
764
+ # Create server configuration
765
+ config = ServerConfig(
766
+ cuda_mvs_path=args.cuda_mvs_path,
767
+ output_dir=args.output_dir,
768
+ max_jobs=args.max_jobs,
769
+ max_images_per_job=args.max_images,
770
+ job_timeout=args.job_timeout,
771
+ api_key=args.api_key,
772
+ server_name=args.server_name,
773
+ advertise_service=not args.no_advertise
774
+ )
775
+
776
+ # Create and run server
777
+ server = CUDAMVSServer(config)
778
+
779
+ try:
780
+ server.run(host=args.host, port=args.port)
781
+ finally:
782
+ server.cleanup()
783
+
784
+ if __name__ == "__main__":
785
+ # Add asyncio import for async/await support
786
+ import asyncio
787
+ main()