193 lines
6.3 KiB
Python
193 lines
6.3 KiB
Python
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 <image1> [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() |