From 0961bbccbf68c2fc61760d8facee58d03cb23ba4 Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Thu, 4 Dec 2025 08:39:04 +0100 Subject: [PATCH] auto-git: [add] scripts/.DS_Store [add] scripts/README.md [add] scripts/extract_texture_filename_from_3ds.py [add] scripts/generate_3d_glb.py [add] scripts/generate_json.py [add] scripts/image_from_json.py [add] scripts/naming.py [add] scripts/openai_image_gen.py [add] scripts/remesh_bake_batch.py [add] server/public/assets/images/.DS_Store [add] server/public/assets/models/spirits/.DS_Store [change] server/public/assets/.DS_Store [change] server/public/assets/models/.DS_Store --- scripts/.DS_Store | Bin 0 -> 10244 bytes scripts/README.md | 9 + scripts/extract_texture_filename_from_3ds.py | 61 ++++++ scripts/generate_3d_glb.py | 71 +++++++ scripts/generate_json.py | 100 ++++++++++ scripts/image_from_json.py | 188 ++++++++++++++++++ scripts/naming.py | 66 ++++++ scripts/openai_image_gen.py | 112 +++++++++++ scripts/remesh_bake_batch.py | 150 ++++++++++++++ server/public/assets/.DS_Store | Bin 6148 -> 6148 bytes server/public/assets/images/.DS_Store | Bin 0 -> 6148 bytes server/public/assets/models/.DS_Store | Bin 8196 -> 10244 bytes server/public/assets/models/spirits/.DS_Store | Bin 0 -> 6148 bytes 13 files changed, 757 insertions(+) create mode 100644 scripts/.DS_Store create mode 100644 scripts/README.md create mode 100644 scripts/extract_texture_filename_from_3ds.py create mode 100644 scripts/generate_3d_glb.py create mode 100644 scripts/generate_json.py create mode 100644 scripts/image_from_json.py create mode 100644 scripts/naming.py create mode 100644 scripts/openai_image_gen.py create mode 100644 scripts/remesh_bake_batch.py create mode 100644 server/public/assets/images/.DS_Store create mode 100644 server/public/assets/models/spirits/.DS_Store diff --git a/scripts/.DS_Store b/scripts/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..cae8c5f7cffabac8950204b281be0c23fa98ccd2 GIT binary patch literal 10244 zcmeHMdu$X%7@u#;W0x*;3oWlh?&Nq>Dbga8M^UcrK`9SQxI(YAQ1@=P*M+^^y1RGf z5wyf;R7`vz7~>yALH|TzG-3<{O*AG-jM|V$0`c*O7!%as9}^_DVBuiA18Fqd@mFh7K*Q=^J4?@wX(PPG9DIU8$(QWr8 zJk`s&^{UfOdo8wYrV4E}wbjsjJC(8q+wxSy(j8`CXgaENv>KLX_tx7vOLLiRf(s-p zoyydG`>Ge!R7Pr+?yrpOt6ox7T^U(kvuOW*Ni2^ptliwX%gkE#Q+yfVZv|xhuw@+_ zz9!90n<;!pYA|H6Mmq0bR=~@J#a{9kD@x;v$ z)ynAgdpea8W}CLHO^#0Ok_NWvdq!7XIiy7pndqvYv8Ih!h2KEY*|XSIPl1C1b7s)Yll6*+MBp1m~l@ycmsD(WMb0otQ8sJiB7SqNOWitD7F!+|Qy> zE|<4q)>xj=W#~?u>ZWx|!_eE(YiM3~Yu<2;l&N#yltQJ{KXuyllF*FM%z}t34jPI| zR7~bl#zQ&7+tWW$DJ_vhv!aryyxpPUOK!9^I+MmEq0p>(qS#LP4UUdAXUn1am1v;0 zxCjs*LawToB*ksng(=TXwa+JJ9PLng_963+EAwQB|$TjjO`3nRn0vXC+I?RSSm<<(}4J)7)R>J+T z9$H`vY=so;0u!=eg9rQJVR!@%z~k@?JPXgkA$SR1g=26WUWXI#4xEB_;RE;tK817e z8C-(z;WGRHSK$v~tRM;Fgkqsgm@CW^DuikwF02;T2#vxfp-o5$8KIl6WFY^x7gpx; zcOo5{e{Nyh6b81#Ekx$^5zjn6 zBsEx&vy8hzfQDwsGs9aj3-sNCIX5&}m9Y|Lsz}2*bf+8*E2A+_cz3Q`5mv@vj_~gM z@Pe=+U~2MiRd|uC6k+1AEDtRSFOU@wd1tv?D=VWg1*QHFW`r!yjA23&Hc5Rr9#&{V z{fE&0$H4xaTp(YQ%j8#b4aPt*C=kYAz6X|JAg_V7&;%P`BL;I4+MpdeK!*%;!%hrs z5Ax6pdtfg-3Xfr6KLJm|^Y8*3h9htkUcvA_4zFQwzX2!VO?VGZ!x=aW@3S!Phl}v- zu&~`QG;HC!;bEJ#<8lA539-g*PPSkDsBWZ=_OCiU-Pis6+!dqK4oQ!E!m&)-${@e+ zlSXxd5+D#D5FijB5Fl_{AW+2LB0}%~SKs{q|J&kSgO>;p2oSh62*9X#B3_SU-|TCZ z-nB>Yc@Q63s9qnZ%mgc{Liy}E9y|0pp4}oL&%exI@|dsJ$0;*MJ7$iQ-{#-)KLhA( PSaALi&i~T?_x%4ijS)=> literal 0 HcmV?d00001 diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..c35da3f --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,9 @@ +# Scripts Overview + +- `extract_texture_filename_from_3ds.py`: Parses a `.3ds` binary and lists referenced texture filenames. +- `generate_3d_glb.py`: For each PNG in `images/`, calls the `tencent/hunyuan3d-2` model via `synexa`, downloads `textured_mesh.glb`, and saves it locally. +- `generate_json.py`: Loads `wesen.json`, fuzzy-maps names to a hardcoded model list, and writes `spirit_list_out.json` with `Model URL` fields (German console messages). +- `image_from_json.py`: For each entry in a JSON list, asks an OpenAI chat model for an image prompt, then calls the Image API to generate/download images (configurable CLI). +- `naming.py`: Matches `.webp` images in `webp/` to entries in `spirit_list.json` (or their model filenames), adds `Image URL` fields, and writes `spirit_list_with_images.json`. +- `openai_image_gen.py`: Simple CLI wrapper around the OpenAI Image API to generate and download images from a prompt. +- `remesh_bake_batch.py`: Blender automation: imports a GLB, QuadRemesher remeshes it, auto-UVs, bakes diffuse/normal maps, exports a remeshed GLB plus PNG bake outputs. diff --git a/scripts/extract_texture_filename_from_3ds.py b/scripts/extract_texture_filename_from_3ds.py new file mode 100644 index 0000000..f0f5bf0 --- /dev/null +++ b/scripts/extract_texture_filename_from_3ds.py @@ -0,0 +1,61 @@ +import struct +import sys + + +def extract_3ds_texture_paths(three_ds_path): + """ + Reads a .3ds file and returns a list of referenced texture filenames. + + Args: + three_ds_path (str): Path to the .3ds file. + + Returns: + List[str]: Texture filenames referenced in the .3ds file. + """ + paths = [] + with open(three_ds_path, 'rb') as f: + while True: + header = f.read(6) + if len(header) < 6: + break + chunk_id, chunk_len = struct.unpack('") + sys.exit(1) + + input_path = sys.argv[1] + textures = extract_3ds_texture_paths(input_path) + + if textures: + print("Referenced textures:") + for tex in textures: + print(f"- {tex}") + else: + print("No texture filenames found in the .3ds file.") + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/scripts/generate_3d_glb.py b/scripts/generate_3d_glb.py new file mode 100644 index 0000000..d65e697 --- /dev/null +++ b/scripts/generate_3d_glb.py @@ -0,0 +1,71 @@ +import os +import requests +import synexa + +# Configuration +INPUT_DIR = "images" # local folder with your .png files +BASE_URL = "https://www.victorgiers.com/shinto" # where the images live +MODEL_NAME = "tencent/hunyuan3d-2" +TIMEOUT = 180 # seconds + +def process_image_file(filename: str): + # 1. Build the full URL for the input image + base_name, ext = os.path.splitext(filename) + if ext.lower() != ".png": + return # skip non-png files + + image_url = f"{BASE_URL}/{filename}" + output_filename = f"{base_name}.glb" + + print(f"\n→ Processing {filename}…") + # 2. Run the model with extended timeout + 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: + print(f" ⚠️ Model run failed for {filename}: {e}") + return + + # 3. Find the textured_mesh.glb URL + 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: + print(f" ⚠️ No textured_mesh.glb found in response for {filename}") + return + + # 4. Download and save + 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) + print(f" ✅ Saved {output_filename}") + except Exception as e: + print(f" ⚠️ Download failed for {filename}: {e}") + +def main(): + # Ensure we're in the right directory (or adjust INPUT_DIR to full path) + for fname in os.listdir(INPUT_DIR): + process_image_file(fname) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/scripts/generate_json.py b/scripts/generate_json.py new file mode 100644 index 0000000..43c92c1 --- /dev/null +++ b/scripts/generate_json.py @@ -0,0 +1,100 @@ +import json +import re +from difflib import get_close_matches + +MODEL_FILES = [ + "Ebisu.glb","Enenra.glb","Enenra2.glb","Oboroguruma.glb","Oiwa.glb","Okiku.glb","Okomeki.001.glb", + "Okuninushi.glb","Oni.glb","Onryo.glb","Oyamatsumi.001.glb","Raijin.glb","Rokurokubi.glb","Ryujin.glb", + "Sarutahiko_Okami.glb","Shinigami.001.glb","Shuten_Doji.glb","Sojobo.glb","Sojobo2.glb","Susanoo.glb", + "Takeminakata.glb","Takeminakata2.001.glb","Tanuki.glb","Tengu.glb","Tenjin.glb","Tsukumogami.glb", + "Tsukuyomi_No_Mikoto.glb","Tsurube_Otoshi.glb","Tsurube_Otoshi2.glb","Tsurube_Otoshi3.glb","Tsurube_Otoshi4.glb", + "Ubume.glb","Yama_Uba.glb","Yama_Uba2.glb","Yamata_No_Orichi.glb","Yamawaro.glb","Yatagarasu2.glb", + "Yuki_Onna.glb","Yurei.glb","Abe_No_Seimei.glb","Abura_Akago.glb","Abura_Sumashi.glb","Abura_Sumashi2.glb", + "Aka_Manto.glb","Akaname.glb","Akateko2.glb","Akkorokamui.glb","Akuchu.glb","Amabie2.glb","Amanojaku.glb", + "Amaterasu.glb","Ame_No_Uzume.001.glb","Amenominakanushi.glb","Aoandon.001.glb","Aoandon2.001.glb", + "Ashiari_Yashiki.glb","Ashinaga_Tenaga2.glb","Azukiarai.glb","Azukibabaa.glb","Azukihakari.glb", + "Bake_Kujira.glb","Bake_Kujira2.glb","Bake_Kujira3.glb","Bakezori.glb","Baku.glb","Basan.glb", + "Benzaiten.glb","Betobeto_San.glb","Bishamonten.glb","Biwa_Bokuboku.glb","Chochin_Obake.glb","Daidarabotchi.glb", + "Daikokuten+Text.glb","Daikokuten.glb","Fujin.glb","Funayurei.glb","Furaribi.glb","Futakuchi_Onna.glb", + "Gaki.glb","Gashadokuro.glb","Hachiman.glb","Hiderigami.001.glb","Hitotsume_Kozo.glb","Hoko.glb", + "Inari_Okami.glb","Ittan_Momen2.glb","Izanagi_No_Mikoto.glb","Izanami_No_Mikoto.glb","Jikininki.glb", + "Jorogumo3.glb","Kamaitachi.glb","Kamikiri.glb","Kappa.glb","Karakasa_Obake.glb","Karakasa_Obake2.glb", + "Kitsune.glb","Kodama.glb","Kudan.glb","Mizushi.glb","Mokumokuren.glb","Mujina.glb","Nekomata.glb", + "Noppera_Bo.glb","Nue.glb","Nuppeppo2.glb","Nurarihyon.glb","Nure_Onna.glb","Nurikabe.glb","Nurikabe2.glb" +] + +def normalize(s): + s = s.lower() + s = re.sub(r"[^a-z0-9]", "", s) + s = s.replace("ou", "o") # für "YamatanoOrOchi" vs "Yamata_No_Orichi" + return s + +def generate_candidates(spirit_name): + base = spirit_name.split()[0] + latin = re.split(r"\s|\(|(", spirit_name)[0] + candidates = [latin] + candidates += [latin.replace("-", "_"), latin.replace("-", ""), latin.replace("_", ""), latin.title(), latin.upper()] + if not latin.endswith("NoMikoto"): + candidates.append(latin + "NoMikoto") + candidates.append(latin + "_No_Mikoto") + return list(set([normalize(c) for c in candidates])) + +def find_best_model(spirit_name): + candidates = generate_candidates(spirit_name) + model_names = [f[:-4] for f in MODEL_FILES] + normalized_models = [normalize(n) for n in model_names] + results = [] + for c in candidates: + for i, n in enumerate(normalized_models): + dist = levenshtein(c, n) + if dist <= 2 or c in n or n in c: + results.append(MODEL_FILES[i]) + results = sorted(list(set(results))) + if not results: + matches = get_close_matches(candidates[0], normalized_models, n=3, cutoff=0.6) + models = [MODEL_FILES[normalized_models.index(m)] for m in matches] + return models + return results + +def levenshtein(a, b): + if a == b: return 0 + if not a: return len(b) + if not b: return len(a) + v0 = list(range(len(b) + 1)) + v1 = [0] * (len(b) + 1) + for i in range(len(a)): + v1[0] = i + 1 + for j in range(len(b)): + cost = 0 if a[i] == b[j] else 1 + v1[j + 1] = min(v1[j] + 1, v0[j + 1] + 1, v0[j] + cost) + v0, v1 = v1, v0 + return v0[len(b)] + +def main(): + with open("wesen.json", encoding="utf-8") as f: + spirits = json.load(f) + output = [] + for spirit in spirits: + name = spirit.get("Name", "") + matches = find_best_model(name) + if not matches: + print(f"[!] Kein Modell gefunden für '{name}'!") + new_spirit = spirit.copy() + new_spirit["Model URL"] = "" + output.append(new_spirit) + elif len(matches) == 1: + new_spirit = spirit.copy() + new_spirit["Model URL"] = "/assets/models/spirits/" + matches[0] + output.append(new_spirit) + else: + print(f"\n[?] Mehrere mögliche Modelle für '{name}': {matches}") + for m in matches: + new_spirit = spirit.copy() + new_spirit["Model URL"] = "/assets/models/spirits/" + m + output.append(new_spirit) + with open("spirit_list_out.json", "w", encoding="utf-8") as f: + json.dump(output, f, ensure_ascii=False, indent=2) + print("\nFERTIG. Ergebnis: spirit_list_out.json") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/scripts/image_from_json.py b/scripts/image_from_json.py new file mode 100644 index 0000000..e3aa2b1 --- /dev/null +++ b/scripts/image_from_json.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python3 +import os +import sys +import argparse +import json +import requests +import urllib.parse + +def generate_image_prompt(entity_json: dict, chat_model: str, api_key: str) -> str: + """ + Generiert einen Bild-Prompt aus der JSON-Beschreibung mit Hilfe eines OpenAI-Chat-Modells. + """ + url = "https://api.openai.com/v1/chat/completions" + headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {api_key}", + } + pretext = ( + "Ich schicke dir nun einen JSON-Abschnitt, der ein japanisches spirituelles Wesen beschreibt." + " Du wirst das Internet bemühen, um nach Darstellungen und weitere Beschreibung der äußeren Erscheinung dieses Wesens zu finden." + " Mit all diesen Informationen generierst du dann ein Bild von dem Wesen im Stil von moderner Low-Poly 3D-Grafik," + " ohne Hintergrund, nur das Wesen selbst. Das Wesen soll vollständig auf dem Bild dargestellt sein, nicht abgeschnitten." + " Es soll dafür geeignet sein, ein 3D Objekt daraus zu bauen." + " Hier der JSON-Abschnitt:" + ) + content = f"{pretext}\n{json.dumps(entity_json, ensure_ascii=False)}" + payload = { + "model": chat_model, + "messages": [ + {"role": "user", "content": content} + ], + "temperature": 0.7, + } + resp = requests.post(url, headers=headers, json=payload) + resp.raise_for_status() + data = resp.json() + # Annahme: Der Prompt steht im ersten Choice unter message.content + prompt = data["choices"][0]["message"]["content"].strip() + return prompt + + +def generate_and_download_image(prompt: str, + image_model: str, + api_key: str, + count: int, + size: str, + fmt: str, + base_output: str): + """ + Generiert Bilder mit der OpenAI Image API und lädt sie herunter. + """ + url = "https://api.openai.com/v1/images/generations" + headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {api_key}", + } + payload = { + "model": image_model, + "prompt": prompt, + "n": count, + "size": size, + "response_format": fmt, + } + resp = requests.post(url, headers=headers, json=payload) + resp.raise_for_status() + data = resp.json().get("data", []) + + for idx, item in enumerate(data, start=1): + if fmt == "url": + img_url = item.get("url") + try: + img_resp = requests.get(img_url) + img_resp.raise_for_status() + except requests.RequestException as e: + print(f"Fehler beim Herunterladen des Bildes #{idx}: {e}", file=sys.stderr) + continue + # Dateinamen bestimmen + if base_output: + base, ext = os.path.splitext(base_output) + ext = ext or os.path.splitext(urllib.parse.urlparse(img_url).path)[1] + filename = f"{base}_{idx}{ext}" if count > 1 else base_output + else: + path = urllib.parse.urlparse(img_url).path + filename = os.path.basename(path) + with open(filename, "wb") as f: + f.write(img_resp.content) + print(f"Bild gespeichert: {filename}") + else: + # Base64 JSON direkt ausgeben + b64 = item.get("b64_json") + out_name = f"{base_output or 'image'}_{idx}.b64.txt" + with open(out_name, "w") as f: + f.write(b64) + print(f"Base64 in Datei geschrieben: {out_name}") + + +def main(): + parser = argparse.ArgumentParser( + description="Generiere Bild-Prompts aus JSON und erstelle Bilder via OpenAI API" + ) + parser.add_argument( + "--api_key", "-k", + help="OpenAI API-Schlüssel (alternativ ENV OPENAI_API_KEY)", + ) + parser.add_argument( + "--chat_model", "-c", + default="o4-mini-high", + help="Modell für die Prompt-Generierung (z.B. 'o4-mini-high')", + ) + parser.add_argument( + "--image_model", "-m", + default="dall-e-3", + help="Modell für die Bild-Generierung (z.B. 'dall-e-3')", + ) + parser.add_argument( + "--input", "-i", + required=True, + help="Pfad zu JSON-Datei mit einer Liste von Entity-Objekten", + ) + parser.add_argument( + "--count", "-n", + type=int, + default=1, + help="Anzahl Bilder pro Entity", + ) + parser.add_argument( + "--size", "-s", + choices=["256x256", "512x512", "1024x1024"], + default="1024x1024", + help="Bildgröße", + ) + parser.add_argument( + "--format", "-f", + choices=["url", "b64_json"], + default="url", + help="Antwort-Format", + ) + parser.add_argument( + "--output", "-o", + help="Basis-Ausgabe-Dateiname oder Verzeichnis (Suffixe _1,_2 werden ergänzt)", + ) + args = parser.parse_args() + + api_key = args.api_key or os.getenv("OPENAI_API_KEY") + if not api_key: + print("Error: API-Schlüssel fehlt. Nutze --api_key oder setze OPENAI_API_KEY.", file=sys.stderr) + sys.exit(1) + + # JSON-Datei einlesen + try: + with open(args.input, "r", encoding="utf-8") as f: + entities = json.load(f) + except Exception as e: + print(f"Fehler beim Laden der JSON-Datei: {e}", file=sys.stderr) + sys.exit(1) + + if not isinstance(entities, list): + print("Error: Die JSON-Datei muss eine Liste von Objekten enthalten.", file=sys.stderr) + sys.exit(1) + + # Für jede Entity Prompt generieren und Bild erstellen + for idx, entity in enumerate(entities, start=1): + name_safe = entity.get("Name", f"entity_{idx}").replace(" ", "_") + print(f"Verarbeite: {entity.get('Name', name_safe)}") + + prompt = generate_image_prompt(entity, args.chat_model, api_key) + print(f"Generierter Prompt: {prompt}\n") + + base_out = None + if args.output: + # Wenn Ausgabe ein Verzeichnis ist, dort ablegen + if os.path.isdir(args.output): + base_out = os.path.join(args.output, f"{name_safe}.png") + else: + base_out = f"{os.path.splitext(args.output)[0]}_{name_safe}.png" + + generate_and_download_image( + prompt=prompt, + image_model=args.image_model, + api_key=api_key, + count=args.count, + size=args.size, + fmt=args.format, + base_output=base_out, + ) + +if __name__ == "__main__": + main() diff --git a/scripts/naming.py b/scripts/naming.py new file mode 100644 index 0000000..ced50de --- /dev/null +++ b/scripts/naming.py @@ -0,0 +1,66 @@ +import os +import json +import re +from difflib import get_close_matches + +# ---- Konfiguration ---- +image_dir = "webp" +json_path = "spirit_list.json" +output_path = "spirit_list_with_images.json" +image_url_prefix = "/assets/images/spirits/" # Deine URL + +# --- Hilfsfunktion: Normalisiere Namen (um sie vergleichbar zu machen) --- +def norm(s): + s = s.lower() + s = re.sub(r'[^a-z0-9]+', '', s) # Alles außer Buchstaben/Zahlen raus + return s + +# ---- Bilddateien einlesen & normalisieren ---- +image_files = [f for f in os.listdir(image_dir) if f.lower().endswith('.webp')] +norm2file = {norm(os.path.splitext(f)[0]): f for f in image_files} + +# ---- JSON einlesen ---- +with open(json_path, "r", encoding="utf-8") as f: + spirits = json.load(f) + +matched = 0 +notfound = [] + +for entry in spirits: + # Nimm zuerst Model URL, ansonsten Name + base = None + if "Model URL" in entry and entry["Model URL"]: + base = os.path.splitext(os.path.basename(entry["Model URL"]))[0] + if not base and "Name" in entry: + base = entry["Name"] + if not base: + notfound.append(entry) + continue + + base_norm = norm(base) + # Direktes Mapping versuchen + if base_norm in norm2file: + entry["Image URL"] = image_url_prefix + norm2file[base_norm] + matched += 1 + continue + + # Fuzzy-Match, falls nicht gefunden + candidates = get_close_matches(base_norm, norm2file.keys(), n=1, cutoff=0.7) + if candidates: + file_name = norm2file[candidates[0]] + entry["Image URL"] = image_url_prefix + file_name + print(f"Fuzzy: {base} → {file_name}") + matched += 1 + else: + print(f"Kein Bild gefunden für: {base}") + notfound.append(entry) + +# --- Neue JSON schreiben --- +with open(output_path, "w", encoding="utf-8") as f: + json.dump(spirits, f, indent=2, ensure_ascii=False) + +print(f"{matched} von {len(spirits)} Einträgen mit Bild gematcht.") +print(f"Nicht gefunden: {len(notfound)}") +if notfound: + for entry in notfound: + print(" -", entry.get("Name", "???")) \ No newline at end of file diff --git a/scripts/openai_image_gen.py b/scripts/openai_image_gen.py new file mode 100644 index 0000000..2778bd1 --- /dev/null +++ b/scripts/openai_image_gen.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +import os +import sys +import argparse +import requests +import urllib.parse + +def main(): + parser = argparse.ArgumentParser( + description="Bilder mit der OpenAI Image API generieren und herunterladen" + ) + parser.add_argument( + "--api_key", "-k", + help="OpenAI API-Schlüssel (alternativ über OPENAI_API_KEY)" + ) + parser.add_argument( + "--model", "-m", + default="dall-e-2", + help="Modellname (z.B. 'gpt-image-1' oder 'dall-e-2')" + ) + parser.add_argument( + "--prompt", "-p", + required=True, + help="Text-Prompt für die Bildgenerierung" + ) + parser.add_argument( + "--count", "-n", + type=int, + default=1, + help="Anzahl der Bilder" + ) + parser.add_argument( + "--size", "-s", + choices=["256x256", "512x512", "1024x1024"], + default="1024x1024", + help="Bildgröße" + ) + parser.add_argument( + "--format", "-f", + choices=["url", "b64_json"], + default="url", + help="Format der Antwort" + ) + parser.add_argument( + "--output", "-o", + help="Zieldatei für das heruntergeladene Bild (bei mehreren: Suffix _1,_2 etc.)" + ) + args = parser.parse_args() + + # API-Key: zuerst aus Argument, sonst aus Umgebungsvariable + api_key = args.api_key or os.getenv("OPENAI_API_KEY") + if not api_key: + print("Error: Bitte gib einen API-Schlüssel via --api_key an oder setze OPENAI_API_KEY.", file=sys.stderr) + sys.exit(1) + + # Request aufsetzen + url = "https://api.openai.com/v1/images/generations" + headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {api_key}", + } + payload = { + "model": args.model, + "prompt": args.prompt, + "n": args.count, + "size": args.size, + "response_format": args.format, + } + + # Anfrage abschicken + try: + response = requests.post(url, headers=headers, json=payload) + response.raise_for_status() + except requests.RequestException as e: + print(f"API-Request fehlgeschlagen: {e}", file=sys.stderr) + sys.exit(1) + + # Antwort auswerten und ggf. herunterladen + data = response.json().get("data", []) + for i, item in enumerate(data, start=1): + if args.format == "url": + img_url = item.get('url') + print(f"[{i}] Bild-URL: {img_url}") + + # Bild herunterladen + try: + img_resp = requests.get(img_url) + img_resp.raise_for_status() + except requests.RequestException as e: + print(f"Fehler beim Herunterladen des Bildes: {e}", file=sys.stderr) + continue + + # Dateinamen bestimmen + if args.output: + base, ext = os.path.splitext(args.output) + filename = f"{base}_{i}{ext}" if args.count > 1 else args.output + else: + path = urllib.parse.urlparse(img_url).path + filename = os.path.basename(path) + + # Datei schreiben + with open(filename, 'wb') as f: + f.write(img_resp.content) + print(f"Bild gespeichert: {filename}") + + else: + # Base64-Ausgabe + b64 = item.get('b64_json') + print(f"[{i}] Bild (Base64):\n{b64}\n") + +if __name__ == "__main__": + main() diff --git a/scripts/remesh_bake_batch.py b/scripts/remesh_bake_batch.py new file mode 100644 index 0000000..3b8a83d --- /dev/null +++ b/scripts/remesh_bake_batch.py @@ -0,0 +1,150 @@ +import bpy +import sys +import os +import math + +# ----------- Argument Handling ----------- +# Get input and output file from command line args +argv = sys.argv +if "--" not in argv: + print("ERROR: No arguments passed. Usage: blender --background --python remesh_bake_batch.py -- /path/to/input.glb [/path/to/output.glb]") + sys.exit(1) +argv = argv[argv.index("--") + 1:] +input_path = os.path.abspath(argv[0]) +output_path = os.path.abspath(argv[1]) if len(argv) > 1 else os.path.splitext(input_path)[0] + "_remesh.glb" + +# ----------- Scene Cleanup ----------- +bpy.ops.object.select_all(action='SELECT') +bpy.ops.object.delete() +for block in bpy.data.meshes: bpy.data.meshes.remove(block) +for block in bpy.data.materials: bpy.data.materials.remove(block) +for block in bpy.data.images: bpy.data.images.remove(block) +for block in bpy.data.textures: bpy.data.textures.remove(block) +for block in bpy.data.lights: bpy.data.lights.remove(block) +for block in bpy.data.cameras: bpy.data.cameras.remove(block) + +# ----------- Import GLB ----------- +print(f"Importing {input_path}...") +bpy.ops.import_scene.gltf(filepath=input_path) +objs = [o for o in bpy.context.scene.objects if o.type == 'MESH'] +if not objs: + print("ERROR: No mesh objects found in the imported file.") + sys.exit(1) +high = objs[0] + +# ----------- Optional: Center object and apply transforms ----------- +bpy.context.view_layer.objects.active = high +bpy.ops.object.select_all(action='DESELECT') +high.select_set(True) +bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) + +# ----------- Remesh via QuadRemesher ----------- +# NOTE: You need QuadRemesher installed & activated in your Blender install! +bpy.ops.object.select_all(action='DESELECT') +high.select_set(True) +bpy.context.view_layer.objects.active = high +bpy.ops.qremesher.remesh() +# Wait for new mesh to appear +import time +max_wait = 60 +before = set(bpy.context.scene.objects) +for t in range(max_wait * 10): + after = set(bpy.context.scene.objects) + new_objs = [o for o in after - before if o.type == 'MESH'] + if new_objs: + low = sorted(new_objs, key=lambda o: len(o.name))[0] + break + time.sleep(0.1) +else: + print("ERROR: Remeshed object not found after 60s.") + sys.exit(1) + +# ----------- UV Mapping & Packing ----------- +bpy.ops.object.select_all(action='DESELECT') +low.select_set(True) +bpy.context.view_layer.objects.active = low + +while low.data.uv_layers: + low.data.uv_layers.remove(low.data.uv_layers[0]) + +bpy.ops.object.mode_set(mode='EDIT') +bpy.ops.mesh.select_all(action='SELECT') +bpy.ops.uv.smart_project(angle_limit=math.radians(66), island_margin=0.03) +bpy.ops.uv.pack_islands(margin=0.003) +bpy.ops.object.mode_set(mode='OBJECT') + +# ----------- Material & Bake Setup ----------- +mat = bpy.data.materials.new(f"{low.name}_BakeMat") +mat.use_nodes = True +low.data.materials.clear() +low.data.materials.append(mat) +nodes = mat.node_tree.nodes +links = mat.node_tree.links +nodes.clear() +out = nodes.new('ShaderNodeOutputMaterial'); out.location = (300, 0) +bsdf = nodes.new('ShaderNodeBsdfPrincipled'); bsdf.location = (0, 0) +links.new(bsdf.outputs['BSDF'], out.inputs['Surface']) + +# --- Diffuse Image --- +diff = nodes.new('ShaderNodeTexImage') +diff.name = diff.label = "Diffuse" +diff.location = (-400, 200) +img_diff = bpy.data.images.new(f"{low.name}_Diffuse", 1024, 1024) +diff.image = img_diff + +scene = bpy.context.scene +scene.render.engine = 'CYCLES' +scene.cycles.use_bake_selected_to_active = False # Only bake from itself! +scene.cycles.bake_margin = 16 +scene.cycles.bake_type = 'DIFFUSE' +scene.cycles.use_bake_direct = False +scene.cycles.use_bake_indirect = False +scene.cycles.use_bake_color = True + +bpy.ops.object.select_all(action='DESELECT') +low.select_set(True) +bpy.context.view_layer.objects.active = low + +for n in nodes: n.select = False +diff.select = True; nodes.active = diff +bpy.ops.object.bake(type='DIFFUSE') +links.new(diff.outputs['Color'], bsdf.inputs['Base Color']) + +# --- Normal Image --- +norm_img = bpy.data.images.new(f"{low.name}_Normal", 1024, 1024) +norm = nodes.new('ShaderNodeTexImage') +norm.name = norm.label = "Normal" +norm.location = (-400, -200) +norm.image = norm_img + +scene.cycles.bake_type = 'NORMAL' +scene.cycles.normal_space = 'TANGENT' + +for n in nodes: n.select = False +norm.select = True; nodes.active = norm +bpy.ops.object.bake(type='NORMAL') + +nm_node = nodes.new('ShaderNodeNormalMap') +nm_node.location = (-150, -200) +nm_node.inputs['Strength'].default_value = 0.5 +links.new(norm.outputs['Color'], nm_node.inputs['Color']) +links.new(nm_node.outputs['Normal'], bsdf.inputs['Normal']) + +bsdf.inputs['Metallic'].default_value = 1.0 +bsdf.inputs['Roughness'].default_value = 0.95 + +# ----------- Export as GLB ----------- +print(f"Exporting {output_path}...") +bpy.ops.export_scene.gltf(filepath=output_path, export_format='GLB', export_selected=False) +print("✅ Done.") + +# ----------- Optional: Save Baked Images Externally ----------- +img_diff.filepath_raw = os.path.splitext(output_path)[0] + "_diffuse.png" +img_diff.file_format = 'PNG' +img_diff.save() + +norm_img.filepath_raw = os.path.splitext(output_path)[0] + "_normal.png" +norm_img.file_format = 'PNG' +norm_img.save() + +print("✅ Images saved.") \ No newline at end of file diff --git a/server/public/assets/.DS_Store b/server/public/assets/.DS_Store index d0da439dffcc3253b60c3efbd08401ed8b1d1bf9..8a3693c928e61256cf7d9ea0196768fca0538c49 100644 GIT binary patch delta 56 zcmZoMXfc@J&&|ZZz`)4BAi%(oIx)&{vVjPTC>uj2LoP!iLpnn$Lvc!Ra!yiye$K}3 LWcJPM9Dn%%QHBl~ delta 33 ncmZoMXfc@J&&|ldzyQPo3=BCFqYO7LNoJpTL2@%Y$6tN`g5?PG diff --git a/server/public/assets/images/.DS_Store b/server/public/assets/images/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..c64bb3db5ea431859543f07c4e1d77af9ac8d54c GIT binary patch literal 6148 zcmeHK%Sr=55Ukb(1G(hrael!+SVH^)e<0`{3n~FG^pD~Ot$LG`OILllPIJd(q2E5~N$-l#h zHyk{S4~&;v%cp5a3P=GdAO)m=6!=X6)@--cI#Ht(kOETRM*;snG`eG7I3>oXgG00c z#E#)G&ZCzgHV+W{!YPpvnkAK(RI3)llFoRmyuNTsOgb!X<~en7MgNcjG)L9HJZ( jqa1VL<@i35GOziZ`+eb*7<9&iPSnqU>mri^f33hb;2Ii6 literal 0 HcmV?d00001 diff --git a/server/public/assets/models/.DS_Store b/server/public/assets/models/.DS_Store index e6e75110b8b50a3ca22f6c3de4514ce1fd50d636..59449dba368f0bf71df8a823700fdeec8678c9a4 100644 GIT binary patch delta 763 zcma)4J#5oJ6n-xuG=C5$4IzXEfx^H7q+1t6nzTagl0b@-(9+=87stZ(IrgPL2q_u5 zfP_S4F?U7$3{emZLh1%917d`^0}|@Mf)Hn?B2`(i^xpgK{N4A?&)-&ItC9u)EOzB- zfb$#%dAL=GZg*1KS1w?9P@`hIwOKy@ zRfwa{e=Kl(A{Yusq9;zqgm_{yc`9`}olc&~oK0u4=Q0gpvMEaU6^nKaxkNQ;6}2u| zRxDMnnxdo{N{x{XR#hL6hLG^6y)pL|kyppZmo`mKN{%-?y&D!Gh9_8VwQRy_hj9Il zxUuwCK4wc6)wBw=Ri;v-zGAoO`T`^Rg3+S9`^)L_a802@yfjQ{`u diff --git a/server/public/assets/models/spirits/.DS_Store b/server/public/assets/models/spirits/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 GIT binary patch literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0