117 lines
5.2 KiB
Python
117 lines
5.2 KiB
Python
"""Isolated Gallery preview. NOT a ComfyUI inference server.
|
||
|
||
python scripts/preview.py --port 8189
|
||
Creates demo images/state in a temporary directory, never in real ComfyUI paths.
|
||
Bind to localhost outside a sandbox; --host 0.0.0.0 is only for controlled preview.
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import json
|
||
import os
|
||
from pathlib import Path
|
||
import random
|
||
import re
|
||
import sys
|
||
import tempfile
|
||
import time
|
||
import types
|
||
|
||
parser = argparse.ArgumentParser(description=__doc__)
|
||
parser.add_argument("--host", default="127.0.0.1")
|
||
parser.add_argument("--port", type=int, default=8189)
|
||
parser.add_argument("--trash-items", type=int, default=0, help="Seed up to 500 disposable trash fixtures for bulk-action QA")
|
||
parser.add_argument("--image-count", type=int, default=18, help="Number of procedural images for scrolling QA (1–500)")
|
||
args = parser.parse_args()
|
||
if not 0 <= args.trash_items <= 500:
|
||
parser.error("--trash-items must be between 0 and 500")
|
||
if not 1 <= args.image_count <= 500:
|
||
parser.error("--image-count must be between 1 and 500")
|
||
root = Path(__file__).resolve().parents[1]
|
||
sys.path.insert(0, str(root))
|
||
temporary = tempfile.TemporaryDirectory(prefix="universal-preview-")
|
||
base = Path(temporary.name)
|
||
for name in ("output", "input", "data"):
|
||
(base / name).mkdir()
|
||
os.environ["UNIVERSAL_EXTRACTOR_DATA_DIR"] = str(base / "data")
|
||
folder_paths = types.ModuleType("folder_paths")
|
||
folder_paths.base_path = str(base)
|
||
folder_paths.get_output_directory = lambda: str(base / "output")
|
||
folder_paths.get_input_directory = lambda: str(base / "input")
|
||
sys.modules["folder_paths"] = folder_paths
|
||
|
||
from PIL import Image, ImageDraw, PngImagePlugin
|
||
from aiohttp import web
|
||
from py.gallery import routes
|
||
|
||
palettes = [
|
||
("#efe9df", "#c3c6b3", "#677c68", "#ddd2b7"),
|
||
("#e9ddd3", "#dba890", "#ad705b", "#f2cfc0"),
|
||
("#dce4e7", "#b0c5c8", "#556f7c", "#d4d6c0"),
|
||
("#efe8d8", "#d7c09b", "#958267", "#e0c697"),
|
||
]
|
||
folders = ["01 · Landscapes", "02 · Color studies", "03 · Architecture"]
|
||
for folder in folders:
|
||
(base / "output" / folder).mkdir()
|
||
for index in range(args.image_count):
|
||
width, height = [(600, 760), (600, 600), (600, 460)][index % 3]
|
||
colors = palettes[index % len(palettes)]
|
||
image = Image.new("RGB", (width, height), colors[0])
|
||
draw = ImageDraw.Draw(image)
|
||
rng = random.Random(index)
|
||
if index % 3 == 0:
|
||
draw.ellipse((360, 100, 460, 200), fill=colors[3])
|
||
for layer in range(3):
|
||
y = height * (.44 + .17 * layer)
|
||
points = [(0, height), (0, y)] + [(x, y + rng.randrange(-65, 65)) for x in range(0, 660, 60)] + [(width, height)]
|
||
draw.polygon(points, fill=colors[(layer + 1) % 4])
|
||
elif index % 3 == 1:
|
||
for ring in range(8, 0, -1):
|
||
radius = ring * 32
|
||
draw.ellipse((300-radius, 300-radius, 300+radius, 300+radius), fill=colors[ring % 4])
|
||
else:
|
||
draw.rectangle((120, 110, 480, 460), fill=colors[1])
|
||
draw.rounded_rectangle((215, 160, 385, 500), radius=85, fill=colors[2])
|
||
draw.rectangle((0, 370, 600, 460), fill=colors[3])
|
||
draw.polygon([(390, 280), (600, 390), (600, 460), (390, 460)], fill=colors[2])
|
||
prompt = f"Demo study {index+1}, minimal composition, soft daylight"
|
||
graph = {"1": {"class_type": "CLIPTextEncode", "inputs": {"text": prompt}},
|
||
"2": {"class_type": "KSampler", "inputs": {"steps": 24, "cfg": 6, "seed": index, "positive": ["1", 0]}}}
|
||
metadata = PngImagePlugin.PngInfo()
|
||
metadata.add_text("prompt", json.dumps(graph))
|
||
image.save(base / "output" / folders[index % 3] / f"Study_{index+1:03}.png", pnginfo=metadata)
|
||
(base / "data" / "Demo prompts.json").write_text(json.dumps([
|
||
{"name": "Soft daylight", "tags": ["natural light", "minimal composition"]},
|
||
{"name": "Quiet landscapes", "tags": ["landscape", "muted colors"]},
|
||
], ensure_ascii=False))
|
||
|
||
if args.trash_items:
|
||
from py.gallery import service
|
||
fixture_names = [f"QA_Disposable_{index:04}.png" for index in range(args.trash_items)]
|
||
for name in fixture_names:
|
||
Image.new("RGB", (16, 16), "#c3c6b3").save(base / "output" / name)
|
||
service.delete_images(fixture_names)
|
||
|
||
async def preview_updates(force=False):
|
||
from py.gallery.update_checker import _build_status
|
||
return {**_build_status([]), "check_status": "preview", "last_success_at": None}
|
||
routes.check_update_status = preview_updates
|
||
app = web.Application()
|
||
routes.register_routes(app)
|
||
# ComfyUI normally provides /view. The isolated fixture supplies it only for its own samples.
|
||
async def preview_image(request):
|
||
kind = request.query.get("type", "output")
|
||
if kind not in ("input", "output"):
|
||
raise web.HTTPNotFound()
|
||
image_root = (base / kind).resolve()
|
||
image_path = (image_root / request.query.get("subfolder", "") / request.query.get("filename", "")).resolve()
|
||
if not image_path.is_relative_to(image_root) or not image_path.is_file():
|
||
raise web.HTTPNotFound()
|
||
return web.FileResponse(image_path)
|
||
app.router.add_get("/view", preview_image)
|
||
async def home(request):
|
||
raise web.HTTPFound("/gallery/")
|
||
app.router.add_get("/", home)
|
||
print("ISOLATED DEMO: procedural sample images, no ComfyUI/GPU inference; state is temporary.")
|
||
web.run_app(app, host=args.host, port=args.port)
|