IPHOTO / app.py
Hosdroid's picture
Create app.py
b1a5162 verified
Raw
History Blame Contribute Delete
22.8 kB
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
open_image_ai_gui.py
Single-file, fully open-source, offline-capable image AI with bilingual GUI (FA/EN).
- No paid APIs, no accounts. Uses local open-source models (diffusers).
- Supports SDXL and SD 1.5 if available; falls back to local illusion generator.
- Prompt cycling, caching, post-processing (10 filters), and 10 anti-error mechanisms.
- Build into one-file executable with PyInstaller if desired.
Run:
python open_image_ai_gui.py
Build (optional):
python -m pip install pyinstaller
pyinstaller --onefile open_image_ai_gui.py
"""
import os
import sys
import json
import time
import math
import hashlib
import random
import threading
from datetime import datetime
from pathlib import Path
# Lazy installer for single-file friendliness
def _lazy_install(pkg):
try:
__import__(pkg)
except ImportError:
os.system(f"{sys.executable} -m pip install --quiet {pkg}")
for pkg in ("torch", "diffusers", "accelerate", "safetensors", "Pillow"):
_lazy_install(pkg)
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import torch
from PIL import Image, ImageOps, ImageFilter, ImageDraw
from diffusers import StableDiffusionPipeline, DiffusionPipeline
# ---------------------------
# Config & Directories
# ---------------------------
APP_NAME = "Open Image AI"
OUTPUT_DIR = Path(os.environ.get("OIA_OUT", "outputs")).resolve()
META_DIR = OUTPUT_DIR / "meta"
CACHE_DIR = OUTPUT_DIR / "cache"
TMP_DIR = OUTPUT_DIR / "tmp"
for d in (OUTPUT_DIR, META_DIR, CACHE_DIR, TMP_DIR):
d.mkdir(parents=True, exist_ok=True)
# Defaults
DEFAULT_STEPS = int(os.environ.get("OIA_STEPS", "30"))
DEFAULT_GUIDANCE = float(os.environ.get("OIA_GUIDANCE", "7.5"))
MAX_QUEUE = int(os.environ.get("OIA_MAX_QUEUE", "64"))
MAX_PARALLEL = int(os.environ.get("OIA_MAX_PARALLEL", "1")) # GUI-friendly
SEED_AUTOMATIC = -1
# Available open models (pullable via diffusers). You can place local copies too.
OPEN_MODELS = {
"SDXL (base)": "stabilityai/stable-diffusion-xl-base-1.0",
"SD 1.5 (classic)": "runwayml/stable-diffusion-v1-5",
# You can add other open models here if supported by diffusers:
# "Stable Cascade": "stabilityai/stable-cascade",
# "OpenJourney (SD1.5 finetune)": "prompthero/openjourney-v4",
}
# ---------------------------
# Prompt utilities
# ---------------------------
OPTICAL_ILLUSION_PRESETS = [
"high-contrast black and white optical illusion, impossible geometry, Penrose stairs, concentric lines, parallax shifts",
"moiré patterns, nested grids, Escher-inspired recursion, tilt-shift depth cues, perspective ambiguity",
"rotational symmetry, non-Euclidean corridor, variable line thickness, alternating diagonals, dizzying parallax",
"spiral tunnels, zigzag pathways, impossible cube, nested frames, sharp edges, stark contrast",
]
WEIGHTS = [
"hyper-detailed", "vector-sharp edges", "ultra high resolution",
"minimal noise", "geometric precision", "dynamic parallax",
"photorealistic lighting"
]
MODS = [
"isometric view", "45-degree inclination", "multi-angle perception",
"viewer-dependent illusion", "top-down perspective"
]
def vary_prompt(base: str) -> str:
extra = ", ".join(random.sample(OPTICAL_ILLUSION_PRESETS, k=random.randint(1, 3)))
hint = ", ".join(random.sample(WEIGHTS, k=random.randint(2, 4)))
mod = random.choice(MODS)
return f"{base}, {extra}, {hint}, {mod}"
def prompt_hash(p: str) -> str:
return hashlib.sha256(p.encode("utf-8")).hexdigest()[:16]
# ---------------------------
# Cache & Meta
# ---------------------------
def cache_path_for(prompt: str) -> Path:
return CACHE_DIR / f"{prompt_hash(prompt)}.png"
def cached(prompt: str) -> Path | None:
p = cache_path_for(prompt)
return p if p.exists() else None
def write_meta(meta: dict, fname: Path):
meta["timestamp"] = datetime.utcnow().isoformat()
with open(META_DIR / (fname.stem + ".json"), "w", encoding="utf-8") as f:
json.dump(meta, f, ensure_ascii=False, indent=2)
# ---------------------------
# Anti-error mechanisms (10)
# ---------------------------
class AntiError:
def __init__(self):
self.fail_count = 0
self.last_minute_calls = []
self.circuit_open = False
self.warned_no_model = False
# 1) Retries cap
def allow_retry(self, attempt, max_retries=3):
return attempt < max_retries
# 2) Backoff incremental
def backoff(self, attempt):
time.sleep(1.2 * attempt + random.random())
# 3) Rate limiting per minute
def rate_limit_ok(self, rate=60): # 60 calls/min cap default
now = time.time()
self.last_minute_calls = [t for t in self.last_minute_calls if now - t < 60]
if len(self.last_minute_calls) >= rate:
return False
self.last_minute_calls.append(now)
return True
# 4) Circuit breaker
def circuit_should_open(self):
return self.fail_count >= 5
def circuit_reset(self):
self.fail_count = 0
self.circuit_open = False
# 5) Cache short-circuit
def cache_hit(self, prompt):
return cached(prompt)
# 6) Dedup by prompt hash
def dedup_name(self, prompt):
return prompt_hash(prompt)
# 7) Health check (model presence)
def has_model(self, model_loaded):
return model_loaded is not None
# 8) Seed normalization
def normalize_seed(self, seed):
return None if seed is None or seed < 0 else seed
# 9) Logging
def log(self, s):
print(s)
# 10) Fallback flag when model missing
def warn_model_missing(self):
if not self.warned_no_model:
self.log("[!] No model loaded. Using local illusion generator as fallback.")
self.warned_no_model = True
anti = AntiError()
# ---------------------------
# Local illusion generator (offline fallback)
# ---------------------------
def local_illusion(size=1024) -> Image.Image:
img = Image.new("RGB", (size, size), "white")
draw = ImageDraw.Draw(img)
# Concentric square rings
step = 6
for r in range(24, size//2, step):
val = 0 if (r//step) % 2 == 0 else 255
# top and bottom lines
for x in range(size):
if abs(x - size//2) == r:
draw.line([(x, 0), (x, size)], fill=(val, val, val))
# left and right lines
for y in range(size):
if abs(y - size//2) == r:
draw.line([(0, y), (size, y)], fill=(val, val, val))
# Diagonal hatch
hatch = Image.new("L", (size, size), color=255)
hpx = hatch.load()
for x in range(size):
for y in range(size):
if (x + y) % 18 == 0:
hpx[x, y] = 0
merged = Image.merge("RGB", (ImageOps.autocontrast(hatch), ImageOps.autocontrast(hatch), ImageOps.autocontrast(hatch)))
merged = Image.blend(img, merged, 0.35)
merged = merged.filter(ImageFilter.SHARPEN)
merged = merged.rotate(random.choice([4, -6, 8, -10]), resample=Image.BICUBIC, expand=False)
return merged
# ---------------------------
# Post-processing filters (10)
# ---------------------------
POST_MODES = [
"illusion_boost", "bw_high", "moire_mix", "invert", "edge_halo",
"soft_glow", "posterize4", "contrast_max", "rotate_slight", "grain_fine"
]
def post_process(img: Image.Image, mode: str) -> Image.Image:
if mode == "illusion_boost":
out = ImageOps.autocontrast(img).filter(ImageFilter.UnsharpMask(radius=2, percent=180, threshold=3))
elif mode == "bw_high":
out = ImageOps.autocontrast(ImageOps.grayscale(img)).convert("RGB")
elif mode == "moire_mix":
out = ImageOps.posterize(img.filter(ImageFilter.DETAIL).filter(ImageFilter.SHARPEN), bits=4)
elif mode == "invert":
out = ImageOps.invert(img)
elif mode == "edge_halo":
out = img.filter(ImageFilter.UnsharpMask(radius=3, percent=240, threshold=2))
elif mode == "soft_glow":
blur = img.filter(ImageFilter.GaussianBlur(radius=2))
out = Image.blend(img, blur, alpha=0.3)
elif mode == "posterize4":
out = ImageOps.posterize(img, bits=4)
elif mode == "contrast_max":
out = ImageOps.autocontrast(img)
elif mode == "rotate_slight":
out = img.rotate(random.choice([-3, 3, -2, 2]), resample=Image.BICUBIC, expand=False)
elif mode == "grain_fine":
out = img.filter(ImageFilter.SMOOTH_MORE)
else:
out = img
return out
# ---------------------------
# Model holder and generator
# ---------------------------
class ModelHub:
def __init__(self):
self.device = "cuda" if torch.cuda.is_available() else "cpu"
self.pipe = None
self.model_id = None
def load(self, model_id: str, fp16=True):
self.model_id = model_id
dtype = torch.float16 if (fp16 and self.device == "cuda") else torch.float32
try:
if "stable-diffusion-xl" in model_id:
self.pipe = DiffusionPipeline.from_pretrained(model_id, torch_dtype=dtype, variant="fp16" if dtype==torch.float16 else None)
else:
self.pipe = StableDiffusionPipeline.from_pretrained(model_id, torch_dtype=dtype)
self.pipe = self.pipe.to(self.device)
if self.device == "cuda":
self.pipe.enable_attention_slicing()
anti.circuit_reset()
return True
except Exception as e:
anti.log(f"[!] Model load error: {e}")
self.pipe = None
return False
def generate(self, prompt: str, steps: int, guidance: float, seed: int | None, height=1024, width=1024) -> Image.Image:
if self.pipe is None:
anti.warn_model_missing()
return local_illusion(size=min(height, width))
generator = torch.Generator(device=self.device)
if seed is not None:
generator = generator.manual_seed(seed)
try:
if hasattr(self.pipe, "generate"):
# DiffusionPipeline custom generate (rare)
out = self.pipe.generate(prompt, height=height, width=width, num_inference_steps=steps, guidance_scale=guidance, generator=generator)
img = out.images[0] if hasattr(out, "images") else out[0]
else:
out = self.pipe(prompt, height=height, width=width, num_inference_steps=steps, guidance_scale=guidance, generator=generator)
img = out.images[0]
return img
except Exception as e:
anti.log(f"[!] Generate error: {e}")
anti.fail_count += 1
if anti.circuit_should_open():
anti.circuit_open = True
return local_illusion(size=min(height, width))
hub = ModelHub()
# ---------------------------
# Orchestrator (queue-based)
# ---------------------------
class Orchestrator:
def __init__(self):
self.queue = []
self.running = False
def enqueue(self, prompt, count, vary, steps, guidance, seed, size, post_mode):
for _ in range(count):
if len(self.queue) >= MAX_QUEUE:
break
self.queue.append({
"prompt": prompt,
"vary": vary,
"steps": steps,
"guidance": guidance,
"seed": seed,
"size": size,
"post_mode": post_mode
})
def _do_item(self, item):
p = item["prompt"]
if item["vary"]:
p = vary_prompt(p)
# Cache check
cp = cached(p)
img = None
if cp:
img = Image.open(cp).convert("RGB")
else:
seed = anti.normalize_seed(item["seed"])
img = hub.generate(
prompt=p,
steps=item["steps"],
guidance=item["guidance"],
seed=seed,
height=item["size"],
width=item["size"],
)
# save to cache
try:
cache_path = cache_path_for(p)
img.save(cache_path, "PNG", optimize=True)
except Exception as e:
anti.log(f"[!] Cache save error: {e}")
if item["post_mode"]:
img = post_process(img, item["post_mode"])
# final save
fname = OUTPUT_DIR / f"{datetime.utcnow().strftime('%Y%m%d_%H%M%S_%f')}_{prompt_hash(p)}.png"
img.save(fname, "PNG", optimize=True)
write_meta({"prompt": p, "model": hub.model_id or "local_illusion", "post_mode": item["post_mode"]}, fname)
return fname
def run(self, on_log):
if self.running:
return
self.running = True
try:
while self.queue:
if not anti.rate_limit_ok(rate=60):
time.sleep(1.0)
item = self.queue.pop(0)
attempt = 0
result_path = None
while anti.allow_retry(attempt, max_retries=3):
try:
result_path = self._do_item(item)
anti.circuit_reset()
break
except Exception as e:
anti.fail_count += 1
on_log(f" x Error: {e}")
attempt += 1
anti.backoff(attempt)
if result_path:
on_log(f" -> Saved: {result_path}")
else:
on_log(" x Failed after retries")
finally:
self.running = False
orch = Orchestrator()
# ---------------------------
# GUI (Tkinter) bilingual
# ---------------------------
class App:
def __init__(self, root):
self.root = root
root.title(APP_NAME)
root.geometry("980x720")
self.lang = tk.StringVar(value="FA")
self.prompt = tk.StringVar()
self.count = tk.IntVar(value=3)
self.vary = tk.BooleanVar(value=True)
self.steps = tk.IntVar(value=DEFAULT_STEPS)
self.guidance = tk.DoubleVar(value=DEFAULT_GUIDANCE)
self.seed = tk.IntVar(value=SEED_AUTOMATIC)
self.size = tk.IntVar(value=768)
self.post_mode = tk.StringVar(value="")
self.model_choice = tk.StringVar(value=list(OPEN_MODELS.keys())[0])
self._build_ui()
self._set_texts()
self._log_header()
def _build_ui(self):
top = ttk.Frame(self.root)
top.pack(fill="x", padx=10, pady=10)
ttk.Label(top, text="Language / زبان").pack(side="left")
ttk.Combobox(top, textvariable=self.lang, values=["FA", "EN"], width=5, state="readonly").pack(side="left", padx=8)
ttk.Button(top, text="Apply/اعمال", command=self._set_texts).pack(side="left", padx=8)
model_frame = ttk.LabelFrame(self.root, text="Model / مدل")
model_frame.pack(fill="x", padx=10, pady=6)
ttk.Label(model_frame, text="Select model:").grid(row=0, column=0, sticky="w", padx=6, pady=6)
ttk.Combobox(model_frame, textvariable=self.model_choice, values=list(OPEN_MODELS.keys()), width=28, state="readonly").grid(row=0, column=1, sticky="w", padx=6, pady=6)
ttk.Button(model_frame, text="Load model", command=self.load_model).grid(row=0, column=2, padx=6, pady=6)
mid = ttk.LabelFrame(self.root, text="Controls / کنترل‌ها")
mid.pack(fill="x", padx=10, pady=6)
# Row 0
self.lbl_prompt = ttk.Label(mid, text="")
self.lbl_prompt.grid(row=0, column=0, sticky="w", padx=6, pady=6)
ttk.Entry(mid, textvariable=self.prompt, width=72).grid(row=0, column=1, sticky="we", padx=6, pady=6)
# Row 1
self.lbl_count = ttk.Label(mid, text="")
self.lbl_count.grid(row=1, column=0, sticky="w", padx=6, pady=6)
ttk.Spinbox(mid, from_=1, to=50, textvariable=self.count, width=6).grid(row=1, column=1, sticky="w", padx=6, pady=6)
# Row 2
self.lbl_vary = ttk.Label(mid, text="")
self.lbl_vary.grid(row=2, column=0, sticky="w", padx=6, pady=6)
ttk.Checkbutton(mid, text="", variable=self.vary).grid(row=2, column=1, sticky="w", padx=6, pady=6)
# Row 3
self.lbl_steps = ttk.Label(mid, text="")
self.lbl_steps.grid(row=3, column=0, sticky="w", padx=6, pady=6)
ttk.Spinbox(mid, from_=5, to=100, textvariable=self.steps, width=6).grid(row=3, column=1, sticky="w", padx=6, pady=6)
# Row 4
self.lbl_guidance = ttk.Label(mid, text="")
self.lbl_guidance.grid(row=4, column=0, sticky="w", padx=6, pady=6)
ttk.Spinbox(mid, from_=0.0, to=20.0, increment=0.5, textvariable=self.guidance, width=6).grid(row=4, column=1, sticky="w", padx=6, pady=6)
# Row 5
self.lbl_seed = ttk.Label(mid, text="")
self.lbl_seed.grid(row=5, column=0, sticky="w", padx=6, pady=6)
ttk.Spinbox(mid, from_=-1, to=2**31-1, textvariable=self.seed, width=12).grid(row=5, column=1, sticky="w", padx=6, pady=6)
# Row 6
self.lbl_size = ttk.Label(mid, text="")
self.lbl_size.grid(row=6, column=0, sticky="w", padx=6, pady=6)
ttk.Spinbox(mid, from_=256, to=1024, increment=64, textvariable=self.size, width=6).grid(row=6, column=1, sticky="w", padx=6, pady=6)
# Row 7
self.lbl_pp = ttk.Label(mid, text="")
self.lbl_pp.grid(row=7, column=0, sticky="w", padx=6, pady=6)
ttk.Combobox(mid, textvariable=self.post_mode, values=[""] + POST_MODES, width=20, state="readonly").grid(row=7, column=1, sticky="w", padx=6, pady=6)
btns = ttk.Frame(mid)
btns.grid(row=8, column=0, columnspan=2, sticky="we", padx=6, pady=6)
self.btn_run = ttk.Button(btns, text="", command=self.run_queue)
self.btn_run.pack(side="left", padx=6)
ttk.Button(btns, text="Open outputs", command=self.open_outputs).pack(side="left", padx=6)
ttk.Button(btns, text="Illusion offline test", command=self.make_offline_illusion).pack(side="left", padx=6)
self.log = tk.Text(self.root, height=18, wrap="word")
self.log.pack(fill="both", expand=True, padx=10, pady=10)
status_bar = ttk.Frame(self.root)
status_bar.pack(fill="x", padx=10, pady=5)
self.status = tk.StringVar(value="Ready")
self.lbl_status = ttk.Label(status_bar, textvariable=self.status)
self.lbl_status.pack(side="left")
def _set_texts(self):
if self.lang.get() == "FA":
self.lbl_prompt.config(text="متن راهنمای تصویر (پرومپت):")
self.lbl_count.config(text="تعداد تولید:")
self.lbl_vary.config(text="تنوع‌دهی خودکار پرومپت:")
self.lbl_steps.config(text="تعداد گام‌ها:")
self.lbl_guidance.config(text="راهنمایی (Guidance):")
self.lbl_seed.config(text="Seed (برای تکرارپذیری، -1 خودکار):")
self.lbl_size.config(text="اندازه (پیکسل مربع):")
self.lbl_pp.config(text="پس‌پردازش:")
self.btn_run.config(text="شروع تولید")
self.root.title(f"{APP_NAME} - رابط فارسی")
else:
self.lbl_prompt.config(text="Image prompt:")
self.lbl_count.config(text="Count:")
self.lbl_vary.config(text="Auto prompt variation:")
self.lbl_steps.config(text="Steps:")
self.lbl_guidance.config(text="Guidance:")
self.lbl_seed.config(text="Seed (-1 automatic):")
self.lbl_size.config(text="Size (square px):")
self.lbl_pp.config(text="Post-process:")
self.btn_run.config(text="Start")
self.root.title(f"{APP_NAME} - English UI")
def _log_header(self):
self.append_log(f"[{APP_NAME}] Ready. Select a model, enter prompt, and start.")
def append_log(self, s):
self.log.insert("end", s + "\n")
self.log.see("end")
def open_outputs(self):
path = str(OUTPUT_DIR)
try:
if os.name == "nt":
os.startfile(path)
elif sys.platform == "darwin":
os.system(f'open "{path}"')
else:
os.system(f'xdg-open "{path}"')
except Exception:
messagebox.showinfo("Info", f"Outputs at: {path}")
def load_model(self):
name = self.model_choice.get()
model_id = OPEN_MODELS.get(name)
if not model_id:
messagebox.showerror("Error", "Model id not found.")
return
self.status.set("Loading model...")
self.append_log(f"[+] Loading: {name} -> {model_id}")
self.root.update_idletasks()
ok = hub.load(model_id, fp16=True)
if ok:
self.status.set("Model loaded")
self.append_log(" -> Model ready.")
else:
self.status.set("Model load failed (fallback active)")
self.append_log(" x Model load failed; using local illusion fallback.")
def run_queue(self):
base_prompt = self.prompt.get().strip()
if not base_prompt:
messagebox.showwarning("Warn", "Please enter a prompt / لطفاً پرومپت را وارد کنید")
return
count = self.count.get()
vary = self.vary.get()
steps = self.steps.get()
guidance = self.guidance.get()
seed = self.seed.get()
size = self.size.get()
pp = self.post_mode.get()
orch.enqueue(base_prompt, count, vary, steps, guidance, seed, size, pp)
self.status.set("Running...")
self.append_log(f"[+] {datetime.now().strftime('%H:%M:%S')} Enqueued: {base_prompt} x {count} vary={vary} steps={steps} guidance={guidance} seed={seed} size={size} pp={pp}")
def worker():
try:
orch.run(self.append_log)
finally:
self.status.set("Done")
threading.Thread(target=worker, daemon=True).start()
def make_offline_illusion(self):
img = local_illusion(size=self.size.get())
fname = OUTPUT_DIR / f"{datetime.utcnow().strftime('%Y%m%d_%H%M%S_%f')}_offline_illusion.png"
img.save(fname, "PNG", optimize=True)
write_meta({"prompt": "offline_illusion", "model": "local_illusion"}, fname)
self.append_log(f" -> Saved offline illusion: {fname}")
# ---------------------------
# Entry
# ---------------------------
def main():
root = tk.Tk()
style = ttk.Style()
try:
style.theme_use("clam")
except Exception:
pass
app = App(root)
root.mainloop()
if __name__ == "__main__":
main()