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
This commit is contained in:
BIN
scripts/.DS_Store
vendored
Normal file
BIN
scripts/.DS_Store
vendored
Normal file
Binary file not shown.
9
scripts/README.md
Normal file
9
scripts/README.md
Normal file
@@ -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.
|
||||
61
scripts/extract_texture_filename_from_3ds.py
Normal file
61
scripts/extract_texture_filename_from_3ds.py
Normal file
@@ -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('<HI', header)
|
||||
data_len = chunk_len - 6
|
||||
if chunk_id == 0xA300: # Mapping Filename
|
||||
name_bytes = b''
|
||||
# Read until null terminator
|
||||
while True:
|
||||
c = f.read(1)
|
||||
if not c or c == b'\x00':
|
||||
break
|
||||
name_bytes += c
|
||||
try:
|
||||
name = name_bytes.decode('ascii')
|
||||
except UnicodeDecodeError:
|
||||
name = name_bytes.decode('latin-1')
|
||||
paths.append(name)
|
||||
# Skip any leftover bytes in this chunk
|
||||
f.seek(data_len - len(name_bytes) - 1, 1)
|
||||
else:
|
||||
# Skip this chunk's data
|
||||
f.seek(data_len, 1)
|
||||
return paths
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 2:
|
||||
print(f"Usage: python {sys.argv[0]} <path/to/model.3ds>")
|
||||
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()
|
||||
71
scripts/generate_3d_glb.py
Normal file
71
scripts/generate_3d_glb.py
Normal file
@@ -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()
|
||||
100
scripts/generate_json.py
Normal file
100
scripts/generate_json.py
Normal file
@@ -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()
|
||||
188
scripts/image_from_json.py
Normal file
188
scripts/image_from_json.py
Normal file
@@ -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()
|
||||
66
scripts/naming.py
Normal file
66
scripts/naming.py
Normal file
@@ -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", "???"))
|
||||
112
scripts/openai_image_gen.py
Normal file
112
scripts/openai_image_gen.py
Normal file
@@ -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()
|
||||
150
scripts/remesh_bake_batch.py
Normal file
150
scripts/remesh_bake_batch.py
Normal file
@@ -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.")
|
||||
BIN
server/public/assets/.DS_Store
vendored
BIN
server/public/assets/.DS_Store
vendored
Binary file not shown.
BIN
server/public/assets/images/.DS_Store
vendored
Normal file
BIN
server/public/assets/images/.DS_Store
vendored
Normal file
Binary file not shown.
BIN
server/public/assets/models/.DS_Store
vendored
BIN
server/public/assets/models/.DS_Store
vendored
Binary file not shown.
BIN
server/public/assets/models/spirits/.DS_Store
vendored
Normal file
BIN
server/public/assets/models/spirits/.DS_Store
vendored
Normal file
Binary file not shown.
Reference in New Issue
Block a user