Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
@@ -8,23 +8,20 @@ from flask import Flask, request, jsonify, send_file, Response, stream_with_cont
|
|
8 |
from werkzeug.utils import secure_filename
|
9 |
from PIL import Image
|
10 |
import io
|
11 |
-
import zipfile
|
12 |
import uuid
|
13 |
import traceback
|
14 |
from huggingface_hub import snapshot_download
|
15 |
from flask_cors import CORS
|
16 |
import numpy as np
|
17 |
import trimesh
|
18 |
-
import
|
19 |
-
import pymeshlab
|
20 |
-
from hy3dgen.shapegen import Hunyuan3DDiTFlowMatchingPipeline
|
21 |
|
22 |
# Force CPU usage
|
23 |
-
os.environ["CUDA_VISIBLE_DEVICES"] = ""
|
24 |
-
torch.set_default_device("cpu")
|
25 |
|
26 |
app = Flask(__name__)
|
27 |
-
CORS(app)
|
28 |
|
29 |
# Configure directories
|
30 |
UPLOAD_FOLDER = '/tmp/uploads'
|
@@ -43,19 +40,19 @@ os.environ['TRANSFORMERS_CACHE'] = os.path.join(CACHE_DIR, 'transformers')
|
|
43 |
os.environ['HF_DATASETS_CACHE'] = os.path.join(CACHE_DIR, 'datasets')
|
44 |
|
45 |
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
|
46 |
-
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024
|
47 |
|
48 |
# Job tracking dictionary
|
49 |
processing_jobs = {}
|
50 |
|
51 |
# Global model variables
|
52 |
-
|
53 |
model_loaded = False
|
54 |
model_loading = False
|
55 |
|
56 |
# Configuration for processing
|
57 |
-
TIMEOUT_SECONDS =
|
58 |
-
MAX_DIMENSION = 256
|
59 |
|
60 |
# TimeoutError for handling timeouts
|
61 |
class TimeoutError(Exception):
|
@@ -94,12 +91,10 @@ def process_with_timeout(function, args, timeout):
|
|
94 |
def allowed_file(filename):
|
95 |
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
|
96 |
|
97 |
-
# Simplified image preprocessing
|
98 |
def preprocess_image(image_path):
|
99 |
with Image.open(image_path) as img:
|
100 |
img = img.convert("RGB")
|
101 |
-
|
102 |
-
# Resize to smaller dimensions for CPU
|
103 |
if img.width > MAX_DIMENSION or img.height > MAX_DIMENSION:
|
104 |
if img.width > img.height:
|
105 |
new_width = MAX_DIMENSION
|
@@ -108,25 +103,24 @@ def preprocess_image(image_path):
|
|
108 |
new_height = MAX_DIMENSION
|
109 |
new_width = int(img.width * (MAX_DIMENSION / img.height))
|
110 |
img = img.resize((new_width, new_height), Image.LANCZOS)
|
111 |
-
|
112 |
return img
|
113 |
|
114 |
def load_model():
|
115 |
-
global
|
116 |
|
117 |
if model_loaded:
|
118 |
-
return
|
119 |
|
120 |
if model_loading:
|
121 |
while model_loading and not model_loaded:
|
122 |
time.sleep(0.5)
|
123 |
-
return
|
124 |
|
125 |
try:
|
126 |
model_loading = True
|
127 |
print("Starting model loading...")
|
128 |
|
129 |
-
model_name = "
|
130 |
|
131 |
# Download model with retry mechanism
|
132 |
max_retries = 3
|
@@ -147,19 +141,18 @@ def load_model():
|
|
147 |
else:
|
148 |
raise
|
149 |
|
150 |
-
# Load
|
151 |
-
|
152 |
model_name,
|
153 |
-
subfolder="hunyuan3d-dit-v2-mini",
|
154 |
use_safetensors=True,
|
155 |
torch_dtype=torch.float16,
|
156 |
cache_dir=CACHE_DIR,
|
157 |
-
|
158 |
)
|
159 |
|
160 |
model_loaded = True
|
161 |
print("Model loaded successfully on CPU")
|
162 |
-
return
|
163 |
|
164 |
except Exception as e:
|
165 |
print(f"Error loading model: {str(e)}")
|
@@ -168,20 +161,11 @@ def load_model():
|
|
168 |
finally:
|
169 |
model_loading = False
|
170 |
|
171 |
-
# Optimize mesh for Unity
|
172 |
-
def optimize_mesh(mesh_path, target_faces=10000):
|
173 |
-
ms = pymeshlab.MeshSet()
|
174 |
-
ms.load_new_mesh(mesh_path)
|
175 |
-
ms.meshing_decimation_quadric_edge_collapse(targetfacenum=target_faces)
|
176 |
-
optimized_path = mesh_path.replace(".glb", "_optimized.glb")
|
177 |
-
ms.save_current_mesh(optimized_path)
|
178 |
-
return optimized_path
|
179 |
-
|
180 |
@app.route('/health', methods=['GET'])
|
181 |
def health_check():
|
182 |
return jsonify({
|
183 |
"status": "healthy",
|
184 |
-
"model": "
|
185 |
"device": "cpu"
|
186 |
}), 200
|
187 |
|
@@ -239,7 +223,7 @@ def convert_image_to_3d():
|
|
239 |
return jsonify({"error": "Invalid parameter values"}), 400
|
240 |
|
241 |
if output_format not in ['glb']:
|
242 |
-
return jsonify({"error": "Only GLB format is supported
|
243 |
|
244 |
job_id = str(uuid.uuid4())
|
245 |
output_dir = os.path.join(RESULTS_FOLDER, job_id)
|
@@ -279,17 +263,19 @@ def convert_image_to_3d():
|
|
279 |
try:
|
280 |
def generate_3d():
|
281 |
# Adjust settings based on detail level
|
282 |
-
steps = {'low':
|
283 |
-
resolution = {'low':
|
284 |
|
|
|
|
|
|
|
|
|
285 |
mesh = model(
|
286 |
-
image=
|
287 |
-
|
288 |
-
|
289 |
-
|
290 |
-
|
291 |
-
output_type="trimesh"
|
292 |
-
)[0]
|
293 |
return mesh
|
294 |
|
295 |
mesh, error = process_with_timeout(generate_3d, [], TIMEOUT_SECONDS)
|
@@ -304,13 +290,10 @@ def convert_image_to_3d():
|
|
304 |
|
305 |
processing_jobs[job_id]['progress'] = 80
|
306 |
|
307 |
-
# Export
|
308 |
glb_path = os.path.join(output_dir, "model.glb")
|
309 |
mesh.export(glb_path, file_type='glb')
|
310 |
|
311 |
-
# Optimize for Unity
|
312 |
-
optimized_path = optimize_mesh(glb_path)
|
313 |
-
|
314 |
processing_jobs[job_id]['result_url'] = f"/download/{job_id}"
|
315 |
processing_jobs[job_id]['preview_url'] = f"/preview/{job_id}"
|
316 |
|
@@ -353,7 +336,7 @@ def download_model(job_id):
|
|
353 |
return jsonify({"error": "Model not found or processing not complete"}), 404
|
354 |
|
355 |
output_dir = os.path.join(RESULTS_FOLDER, job_id)
|
356 |
-
glb_path = os.path.join(output_dir, "
|
357 |
|
358 |
if os.path.exists(glb_path):
|
359 |
return send_file(glb_path, as_attachment=True, download_name="model.glb")
|
@@ -366,7 +349,7 @@ def preview_model(job_id):
|
|
366 |
return jsonify({"error": "Model not found or processing not complete"}), 404
|
367 |
|
368 |
output_dir = os.path.join(RESULTS_FOLDER, job_id)
|
369 |
-
glb_path = os.path.join(output_dir, "
|
370 |
|
371 |
if os.path.exists(glb_path):
|
372 |
return send_file(glb_path, mimetype='model/gltf-binary')
|
@@ -414,7 +397,7 @@ def model_info(job_id):
|
|
414 |
output_dir = os.path.join(RESULTS_FOLDER, job_id)
|
415 |
model_stats = {}
|
416 |
|
417 |
-
glb_path = os.path.join(output_dir, "
|
418 |
if os.path.exists(glb_path):
|
419 |
model_stats['model_size'] = os.path.getsize(glb_path)
|
420 |
|
@@ -431,7 +414,7 @@ def model_info(job_id):
|
|
431 |
@app.route('/', methods=['GET'])
|
432 |
def index():
|
433 |
return jsonify({
|
434 |
-
"message": "Image to 3D API (
|
435 |
"endpoints": [
|
436 |
"/convert",
|
437 |
"/progress/<job_id>",
|
@@ -443,7 +426,7 @@ def index():
|
|
443 |
"output_format": "glb",
|
444 |
"detail_level": "low, medium, or high - controls mesh detail"
|
445 |
},
|
446 |
-
"description": "This API creates full 3D models from 2D images using
|
447 |
}), 200
|
448 |
|
449 |
if __name__ == '__main__':
|
|
|
8 |
from werkzeug.utils import secure_filename
|
9 |
from PIL import Image
|
10 |
import io
|
|
|
11 |
import uuid
|
12 |
import traceback
|
13 |
from huggingface_hub import snapshot_download
|
14 |
from flask_cors import CORS
|
15 |
import numpy as np
|
16 |
import trimesh
|
17 |
+
from instantmesh import InstantMesh
|
|
|
|
|
18 |
|
19 |
# Force CPU usage
|
20 |
+
os.environ["CUDA_VISIBLE_DEVICES"] = ""
|
21 |
+
torch.set_default_device("cpu")
|
22 |
|
23 |
app = Flask(__name__)
|
24 |
+
CORS(app)
|
25 |
|
26 |
# Configure directories
|
27 |
UPLOAD_FOLDER = '/tmp/uploads'
|
|
|
40 |
os.environ['HF_DATASETS_CACHE'] = os.path.join(CACHE_DIR, 'datasets')
|
41 |
|
42 |
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
|
43 |
+
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024
|
44 |
|
45 |
# Job tracking dictionary
|
46 |
processing_jobs = {}
|
47 |
|
48 |
# Global model variables
|
49 |
+
instant_mesh = None
|
50 |
model_loaded = False
|
51 |
model_loading = False
|
52 |
|
53 |
# Configuration for processing
|
54 |
+
TIMEOUT_SECONDS = 300 # 5 minutes max for InstantMesh on CPU
|
55 |
+
MAX_DIMENSION = 256
|
56 |
|
57 |
# TimeoutError for handling timeouts
|
58 |
class TimeoutError(Exception):
|
|
|
91 |
def allowed_file(filename):
|
92 |
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
|
93 |
|
94 |
+
# Simplified image preprocessing
|
95 |
def preprocess_image(image_path):
|
96 |
with Image.open(image_path) as img:
|
97 |
img = img.convert("RGB")
|
|
|
|
|
98 |
if img.width > MAX_DIMENSION or img.height > MAX_DIMENSION:
|
99 |
if img.width > img.height:
|
100 |
new_width = MAX_DIMENSION
|
|
|
103 |
new_height = MAX_DIMENSION
|
104 |
new_width = int(img.width * (MAX_DIMENSION / img.height))
|
105 |
img = img.resize((new_width, new_height), Image.LANCZOS)
|
|
|
106 |
return img
|
107 |
|
108 |
def load_model():
|
109 |
+
global instant_mesh, model_loaded, model_loading
|
110 |
|
111 |
if model_loaded:
|
112 |
+
return instant_mesh
|
113 |
|
114 |
if model_loading:
|
115 |
while model_loading and not model_loaded:
|
116 |
time.sleep(0.5)
|
117 |
+
return instant_mesh
|
118 |
|
119 |
try:
|
120 |
model_loading = True
|
121 |
print("Starting model loading...")
|
122 |
|
123 |
+
model_name = "BAAI/InstantMesh"
|
124 |
|
125 |
# Download model with retry mechanism
|
126 |
max_retries = 3
|
|
|
141 |
else:
|
142 |
raise
|
143 |
|
144 |
+
# Load InstantMesh
|
145 |
+
instant_mesh = InstantMesh.from_pretrained(
|
146 |
model_name,
|
|
|
147 |
use_safetensors=True,
|
148 |
torch_dtype=torch.float16,
|
149 |
cache_dir=CACHE_DIR,
|
150 |
+
device="cpu"
|
151 |
)
|
152 |
|
153 |
model_loaded = True
|
154 |
print("Model loaded successfully on CPU")
|
155 |
+
return instant_mesh
|
156 |
|
157 |
except Exception as e:
|
158 |
print(f"Error loading model: {str(e)}")
|
|
|
161 |
finally:
|
162 |
model_loading = False
|
163 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
164 |
@app.route('/health', methods=['GET'])
|
165 |
def health_check():
|
166 |
return jsonify({
|
167 |
"status": "healthy",
|
168 |
+
"model": "InstantMesh 3D Generator",
|
169 |
"device": "cpu"
|
170 |
}), 200
|
171 |
|
|
|
223 |
return jsonify({"error": "Invalid parameter values"}), 400
|
224 |
|
225 |
if output_format not in ['glb']:
|
226 |
+
return jsonify({"error": "Only GLB format is supported"}), 400
|
227 |
|
228 |
job_id = str(uuid.uuid4())
|
229 |
output_dir = os.path.join(RESULTS_FOLDER, job_id)
|
|
|
263 |
try:
|
264 |
def generate_3d():
|
265 |
# Adjust settings based on detail level
|
266 |
+
steps = {'low': 50, 'medium': 75, 'high': 100}
|
267 |
+
resolution = {'low': 128, 'medium': 256, 'high': 512}
|
268 |
|
269 |
+
# Convert PIL image to numpy for InstantMesh
|
270 |
+
img_array = np.array(image)
|
271 |
+
|
272 |
+
# Generate mesh
|
273 |
mesh = model(
|
274 |
+
image=img_array,
|
275 |
+
num_steps=steps[detail_level],
|
276 |
+
resolution=resolution[detail_level],
|
277 |
+
seed=12345
|
278 |
+
)
|
|
|
|
|
279 |
return mesh
|
280 |
|
281 |
mesh, error = process_with_timeout(generate_3d, [], TIMEOUT_SECONDS)
|
|
|
290 |
|
291 |
processing_jobs[job_id]['progress'] = 80
|
292 |
|
293 |
+
# Export as GLB
|
294 |
glb_path = os.path.join(output_dir, "model.glb")
|
295 |
mesh.export(glb_path, file_type='glb')
|
296 |
|
|
|
|
|
|
|
297 |
processing_jobs[job_id]['result_url'] = f"/download/{job_id}"
|
298 |
processing_jobs[job_id]['preview_url'] = f"/preview/{job_id}"
|
299 |
|
|
|
336 |
return jsonify({"error": "Model not found or processing not complete"}), 404
|
337 |
|
338 |
output_dir = os.path.join(RESULTS_FOLDER, job_id)
|
339 |
+
glb_path = os.path.join(output_dir, "model.glb")
|
340 |
|
341 |
if os.path.exists(glb_path):
|
342 |
return send_file(glb_path, as_attachment=True, download_name="model.glb")
|
|
|
349 |
return jsonify({"error": "Model not found or processing not complete"}), 404
|
350 |
|
351 |
output_dir = os.path.join(RESULTS_FOLDER, job_id)
|
352 |
+
glb_path = os.path.join(output_dir, "model.glb")
|
353 |
|
354 |
if os.path.exists(glb_path):
|
355 |
return send_file(glb_path, mimetype='model/gltf-binary')
|
|
|
397 |
output_dir = os.path.join(RESULTS_FOLDER, job_id)
|
398 |
model_stats = {}
|
399 |
|
400 |
+
glb_path = os.path.join(output_dir, "model.glb")
|
401 |
if os.path.exists(glb_path):
|
402 |
model_stats['model_size'] = os.path.getsize(glb_path)
|
403 |
|
|
|
414 |
@app.route('/', methods=['GET'])
|
415 |
def index():
|
416 |
return jsonify({
|
417 |
+
"message": "Image to 3D API (InstantMesh)",
|
418 |
"endpoints": [
|
419 |
"/convert",
|
420 |
"/progress/<job_id>",
|
|
|
426 |
"output_format": "glb",
|
427 |
"detail_level": "low, medium, or high - controls mesh detail"
|
428 |
},
|
429 |
+
"description": "This API creates full 3D models from 2D images using InstantMesh"
|
430 |
}), 200
|
431 |
|
432 |
if __name__ == '__main__':
|