import sys import os os.environ["SYNEXA_API_KEY"] = "YOUR_SYNEXA_API_KEY" import tempfile import requests import synexa from PIL import Image UPLOAD_URL = "https://yourwebsite.com/yourtmpimagefolder/secure_upload.php" CLEANUP_URL = "https://yourwebsite.com/yourtmpimagefolder/cleanup.php" UPLOAD_PASSWORD = "YOUR_PASSWORD_AS_DEFINED_IN_PHP" BASE_URL = "https://yourwebsite.com/yourtmpimagefolder" MODEL_NAME = "tencent/hunyuan3d-2" TIMEOUT = 180 # seconds ALLOWED_EXTENSIONS = (".jpg", ".jpeg", ".png") def is_native_image(filename): ext = os.path.splitext(filename)[1].lower() return ext in ALLOWED_EXTENSIONS def convert_to_png(src_path): img = Image.open(src_path) fd, tmp_path = tempfile.mkstemp(suffix=".png") os.close(fd) img.save(tmp_path, "PNG") return tmp_path def upload_image(image_path): with open(image_path, 'rb') as f: files = {'file': (os.path.basename(image_path), f)} data = {'password': UPLOAD_PASSWORD} r = requests.post(UPLOAD_URL, files=files, data=data) try: resp = r.json() except Exception as e: print(f"[UPLOAD] Fehler: {e}") return None if "url" in resp: return resp["url"] else: print(f"[UPLOAD] Fehler: {resp}") return None def cleanup_remote_file(filename): data = {'password': UPLOAD_PASSWORD, 'filename': filename} try: r = requests.post(CLEANUP_URL, data=data) if r.status_code == 200 and r.json().get("success"): print(f"[CLEANUP] Remote Datei gelöscht: {filename}") return True else: print(f"[CLEANUP] Fehler beim Löschen: {r.text}") return False except Exception as e: print(f"[CLEANUP] Fehler: {e}") return False def notify(title, message): # macOS Notification über osascript os.system(f'''osascript -e 'display notification "{message}" with title "{title}"' ''') def process_image(img_path): orig_name = os.path.basename(img_path) orig_dir = os.path.dirname(os.path.abspath(img_path)) temp_files = [] try: # 1. Konvertieren falls nötig if is_native_image(img_path): upload_path = img_path upload_name = orig_name else: upload_path = convert_to_png(img_path) temp_files.append(upload_path) upload_name = os.path.basename(upload_path) print(f"Konvertiert zu PNG: {upload_path}") # 2. Upload url = upload_image(upload_path) if not url: msg = f"Upload von {upload_name} fehlgeschlagen." print(msg) notify("3D-Konvertierung Fehler", msg) return None print(f"Upload erfolgreich: {url}") # 3. Synexa: Model run base_name, ext = os.path.splitext(upload_name) if not upload_name.lower().endswith(ALLOWED_EXTENSIONS): msg = f"Bild {upload_name} ist kein PNG/JPG/JPEG auf dem Server, überspringe Synexa." print(msg) cleanup_remote_file(upload_name) notify("3D-Konvertierung Fehler", msg) return None image_url = f"{BASE_URL}/{upload_name}" output_filename = os.path.join(orig_dir, f"{base_name}.glb") print(f"→ Sende an Synexa: {image_url}") try: response_list = synexa.run( MODEL_NAME, input={ "seed": 1234, "image": image_url, "steps": 5, "caption": "", "shape_only": False, "guidance_scale": 5.5, "multiple_views": [], "check_box_rembg": True, "octree_resolution": "256" }, wait=TIMEOUT ) except Exception as e: msg = f"⚠️ Model run failed for {upload_name}: {e}" print(msg) cleanup_remote_file(upload_name) notify("3D-Konvertierung Fehler", msg) return None # 4. Find .glb textured_url = None for fo in response_list: url = getattr(fo, "url", "") if url.endswith("textured_mesh.glb"): textured_url = url break if not textured_url: msg = f"⚠️ No textured_mesh.glb found in response for {upload_name}" print(msg) cleanup_remote_file(upload_name) notify("3D-Konvertierung Fehler", msg) return None # 5. Download print(f"↓ Downloading textured mesh → {output_filename}") try: dl = requests.get(textured_url, timeout=TIMEOUT) dl.raise_for_status() with open(output_filename, "wb") as out_file: out_file.write(dl.content) msg = f"3D-Modell gespeichert: {output_filename}" print(f"✅ {msg}") notify("3D-Konvertierung Erfolgreich", msg) except Exception as e: msg = f"⚠️ Download failed for {upload_name}: {e}" print(msg) notify("3D-Konvertierung Fehler", msg) # 6. Remote Cleanup cleanup_remote_file(upload_name) # 7. Lokale temp-Dateien löschen for tfile in temp_files: try: os.remove(tfile) print(f"[CLEANUP] Lokale temporäre Datei gelöscht: {tfile}") except Exception as e: print(f"[CLEANUP] Fehler beim lokalen Löschen: {e}") return output_filename except Exception as e: msg = f"Allgemeiner Fehler bei {img_path}: {e}" print(msg) notify("3D-Konvertierung Fehler", msg) return None def main(): if len(sys.argv) < 2: notify("3D-Konvertierung Fehler", "Keine Bilddateien angegeben!") print("Verwendung: python image_to_3d.py [image2 ...]") sys.exit(1) all_outputs = [] for img_path in sys.argv[1:]: print(f"\nBearbeite: {img_path}") result = process_image(img_path) if result: all_outputs.append(result) # Sammel-Notification am Ende if all_outputs: notify("3D-Konvertierung", f"Fertig! {len(all_outputs)} Modell(e) gespeichert.") else: notify("3D-Konvertierung", "Keine 3D-Modelle erzeugt.") if __name__ == "__main__": main()