| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import os |
| import re |
| import io |
| import base64 |
| import random |
| import datetime |
| from pathlib import Path |
| from typing import Optional, Dict, List, Union, Tuple |
|
|
| from flask import Flask, request, jsonify, send_from_directory |
|
|
| |
| try: |
| import torch |
| from diffusers import DiffusionPipeline |
| from PIL import Image, ImageDraw, ImageFont |
| from PIL.PngImagePlugin import PngInfo |
| HAVE_PIPELINE = True |
| except Exception: |
| torch = None |
| DiffusionPipeline = None |
| Image = None |
| ImageDraw = None |
| ImageFont = None |
| PngInfo = None |
| HAVE_PIPELINE = False |
|
|
| |
| BASE_DIR = Path(__file__).resolve().parent |
| OUTPUT_DIR = BASE_DIR / "output" |
| OUTPUT_DIR.mkdir(exist_ok=True) |
|
|
| |
| RANDOM_WORDS_FILE = BASE_DIR / "random_words.txt" |
|
|
| device = "cuda" if (HAVE_PIPELINE and torch.cuda.is_available()) else "cpu" |
| dtype = torch.float16 if (HAVE_PIPELINE and device == "cuda") else (torch.float32 if HAVE_PIPELINE else None) |
|
|
| if HAVE_PIPELINE: |
| print("Loading Mitsua Likes pipeline...") |
| try: |
| |
| pipe = DiffusionPipeline.from_pretrained( |
| "mitsua-likes/", |
| trust_remote_code=True |
| ).to(device, dtype=dtype) |
| if hasattr(pipe, "safety_checker"): |
| pipe.safety_checker = None |
| except Exception as e: |
| print("Failed to load Mitsua Likes pipeline:", e) |
| pipe = None |
| else: |
| pipe = None |
|
|
| |
| def load_random_words(): |
| if RANDOM_WORDS_FILE.exists(): |
| with open(RANDOM_WORDS_FILE, "r", encoding="utf-8") as f: |
| words = [w.strip() for w in f.readlines() if w.strip()] |
| if words: |
| return words |
| return ["night", "fog", "rain", "ruins", "distant city", "reflection"] |
|
|
|
|
| RANDOM_WORDS = load_random_words() |
|
|
|
|
|
|
| def parse_no_logo(value) -> bool: |
| """ |
| JSON から渡ってくる no_logo の値を安全に bool に変換。 |
| True / "true" / "1" / "yes" / "on" などのみ True とみなし、それ以外は False。 |
| """ |
| if isinstance(value, bool): |
| return value |
| if isinstance(value, (int, float)): |
| return bool(value) |
| if isinstance(value, str): |
| v = value.strip().lower() |
| if v in ("1", "true", "yes", "on"): |
| return True |
| if v in ("0", "false", "no", "off", ""): |
| return False |
| return False |
|
|
|
|
|
|
|
|
|
|
|
|
| def parse_disable_character_detection(value) -> bool: |
| """JSON から渡ってくる disable_character_detection を安全に bool に変換。""" |
| |
| return parse_no_logo(value) |
|
|
|
|
| def _has_detected_public_fictional_characters(ret, idx: int = 0) -> bool: |
| """ |
| Mitsua Likes pipeline の戻り値 ret から、公開キャラクター類似が検知されたか判定する。 |
| ret.detected_public_fictional_characters は [ [str, ...], ... ] を想定。 |
| """ |
| try: |
| if not hasattr(ret, "detected_public_fictional_characters"): |
| return False |
| d = getattr(ret, "detected_public_fictional_characters", None) |
| if d is None: |
| return False |
| |
| if not isinstance(d, (list, tuple)) or len(d) <= idx: |
| return False |
| one = d[idx] |
| if one is None: |
| return False |
| if isinstance(one, (list, tuple)) and len(one) > 0: |
| return True |
| |
| if isinstance(one, str) and one.strip(): |
| return True |
| return False |
| except Exception: |
| return False |
|
|
|
|
| def make_character_detection_placeholder(size: Tuple[int, int], |
| text: str = "detected_public_fictional_characters") -> "Image.Image": |
| """真っ白な画像に指定テキストを描画した代替画像を返す。""" |
| if Image is None or ImageDraw is None: |
| raise RuntimeError("Pillow is not available; cannot create placeholder image.") |
| w, h = int(size[0]), int(size[1]) |
| img = Image.new("RGB", (w, h), (255, 255, 255)) |
| draw = ImageDraw.Draw(img) |
|
|
| font = None |
| if ImageFont is not None: |
| try: |
| |
| font = ImageFont.load_default() |
| except Exception: |
| font = None |
|
|
| |
| try: |
| bbox = draw.textbbox((0, 0), text, font=font) |
| tw, th = bbox[2] - bbox[0], bbox[3] - bbox[1] |
| except Exception: |
| try: |
| tw, th = draw.textsize(text, font=font) |
| except Exception: |
| tw, th = (len(text) * 6, 12) |
|
|
| x = max(0, (w - tw) // 2) |
| y = max(0, (h - th) // 2) |
|
|
| |
| draw.text((x, y), text, font=font, fill=(0, 0, 0)) |
| return img |
| def add_logo_to_image(img: "Image.Image") -> "Image.Image": |
| """ |
| 生成画像の下部に "Generated by Mitsua Likes" のロゴテキストを描画して返す。 |
| - 画像サイズは変更しない(同じキャンバスのまま上書き) |
| - できるだけ例外を出さないように実装し、失敗した場合は元画像をそのまま返す |
| """ |
| if Image is None or ImageDraw is None: |
| return img |
|
|
| try: |
| |
| img_rgba = img.convert("RGBA") |
| width, height = img_rgba.size |
|
|
| overlay = Image.new("RGBA", (width, height), (0, 0, 0, 0)) |
| draw = ImageDraw.Draw(overlay) |
|
|
| text = "Generated by Mitsua Likes" |
|
|
| |
| font = None |
| if ImageFont is not None: |
| try: |
| font = ImageFont.load_default() |
| except Exception: |
| font = None |
|
|
| |
| bar_height = max(height // 20, 24) |
| y0 = height - bar_height |
|
|
| |
| draw.rectangle([0, y0, width, height], fill=(0, 0, 0, 160)) |
|
|
| |
| padding = 8 |
| x_text = width - padding |
| y_text = y0 + bar_height // 2 |
|
|
| |
| try: |
| draw.text((x_text, y_text), text, font=font, fill=(255, 255, 255, 255), anchor="rm") |
| except TypeError: |
| |
| draw.text((width - padding * 2, y0 + (bar_height - 12) // 2), |
| text, font=font, fill=(255, 255, 255, 255)) |
|
|
| combined = Image.alpha_composite(img_rgba, overlay) |
| |
| return combined.convert(img.mode) |
| except Exception: |
| |
| return img |
| try: |
| |
| img_rgba = img.convert("RGBA") |
| overlay = Image.new("RGBA", img_rgba.size, (0, 0, 0, 0)) |
| draw = ImageDraw.Draw(overlay) |
|
|
| |
| font = ImageFont.load_default() |
|
|
| |
| try: |
| text_w, text_h = draw.textsize(text, font=font) |
| except Exception: |
| text_w, text_h = font.getsize(text) |
|
|
| width, height = img_rgba.size |
| padding = 6 |
| bar_height = text_h + padding * 2 |
| y0 = height - bar_height |
|
|
| |
| draw.rectangle([0, y0, width, height], fill=(0, 0, 0, 160)) |
|
|
| |
| x_text = width - text_w - padding |
| y_text = y0 + (bar_height - text_h) // 2 |
| draw.text((x_text, y_text), text, font=font, fill=(255, 255, 255, 255)) |
|
|
| combined = Image.alpha_composite(img_rgba, overlay).convert("RGB") |
| return combined |
| except Exception: |
| |
| return img |
|
|
|
|
| def sanitize_for_filename(text: str, max_len: int = 60) -> str: |
| text = text.strip() |
| text = re.sub(r"\s+", " ", text) |
| text = text[:max_len] |
| text = re.sub(r'[\\/:*?"<>|]', "_", text) |
| if not text: |
| text = "prompt" |
| return text |
|
|
|
|
| def ensure_pipeline_available(): |
| if pipe is None or torch is None: |
| raise RuntimeError( |
| "Diffusion pipeline is not available. " |
| "Install torch, diffusers, pillow and download 'mitsua-likes' model." |
| ) |
|
|
|
|
| def save_image_with_metadata(img, prompt, negative_prompt, |
| steps, guidance_scale, guidance_rescale, |
| seed, filename_override: Optional[str] = None) -> str: |
| if PngInfo is None: |
| raise RuntimeError("Pillow is not available; install pillow to save images.") |
| metadata = PngInfo() |
| param_text = ( |
| f"{prompt}\n" |
| f"Negative prompt: {negative_prompt}\n" |
| f"Steps: {steps}, guidance_scale: {guidance_scale}, " |
| f"guidance_rescale: {guidance_rescale}, seed: {seed}" |
| ) |
| metadata.add_text("parameters", param_text) |
|
|
| if filename_override is not None: |
| filename = filename_override |
| else: |
| prompt_part = sanitize_for_filename(prompt) |
| now = datetime.datetime.now().strftime("%Y%m%d_%H%M%S_%f") |
| filename = f"{prompt_part}_seed{seed}_{now}.png" |
|
|
| fullpath = OUTPUT_DIR / filename |
| img.save(fullpath, pnginfo=metadata) |
| return filename |
|
|
| |
| def save_latent(latent: "torch.Tensor", filename_stem: str) -> str: |
| """ |
| latent を output/<stem>.pt に保存してファイル名を返す。 |
| """ |
| if torch is None: |
| raise RuntimeError("torch is not available; cannot save latent.") |
| path = OUTPUT_DIR / f"{filename_stem}.pt" |
| torch.save(latent.detach().cpu(), path) |
| return path.name |
|
|
|
|
| def parse_parameters_text(param_text: str) -> Optional[Dict]: |
| lines = param_text.splitlines() |
| if not lines: |
| return None |
|
|
| prompt = lines[0].strip() |
| negative_prompt = "" |
| steps = 40 |
| guidance_scale = 6.0 |
| guidance_rescale = 0.7 |
| seed = 1000 |
|
|
| for line in lines[1:]: |
| if line.startswith("Negative prompt:"): |
| negative_prompt = line[len("Negative prompt:"):].strip() |
| elif "Steps:" in line: |
| parts = [p.strip() for p in line.split(",")] |
| for part in parts: |
| if part.startswith("Steps:"): |
| steps = int(part.split(":", 1)[1].strip()) |
| elif part.startswith("guidance_scale:"): |
| guidance_scale = float(part.split(":", 1)[1].strip()) |
| elif part.startswith("guidance_rescale:"): |
| guidance_rescale = float(part.split(":", 1)[1].strip()) |
| elif part.startswith("seed:"): |
| seed = int(part.split(":", 1)[1].strip()) |
|
|
| return { |
| "prompt": prompt, |
| "negative_prompt": negative_prompt, |
| "steps": steps, |
| "guidance_scale": guidance_scale, |
| "guidance_rescale": guidance_rescale, |
| "seed": seed, |
| } |
|
|
|
|
| def generate_single_image(prompt, negative_prompt, |
| width, height, |
| steps, guidance_scale, guidance_rescale, |
| seed, |
| add_logo: bool = True, |
| enable_character_detection: bool = True): |
| """ |
| 通常の 1枚生成。latent はここでは保存しない(inpaint 用に選択されたときだけ再計算して保存する)。 |
| """ |
| ensure_pipeline_available() |
|
|
| generator = torch.Generator().manual_seed(int(seed)) |
|
|
| with torch.no_grad(): |
| ret = pipe( |
| prompt=prompt, |
| negative_prompt=negative_prompt, |
| guidance_scale=float(guidance_scale), |
| guidance_rescale=float(guidance_rescale), |
| generator=generator, |
| width=int(width), |
| height=int(height), |
| num_inference_steps=int(steps), |
| |
| ) |
|
|
| try: |
| if hasattr(ret, "detected_public_fictional_characters"): |
| print("Similarity Restriction:", ret.detected_public_fictional_characters[0]) |
| if hasattr(ret, "detected_public_fictional_characters_info"): |
| print("Similarity Measure:") |
| for k, v in ret.detected_public_fictional_characters_info[0].items(): |
| print(f"{k} : {v:.3%}") |
| except Exception: |
| pass |
|
|
| img = ret.images[0] |
| detected = bool(enable_character_detection and _has_detected_public_fictional_characters(ret, 0)) |
| if detected: |
| img = make_character_detection_placeholder(img.size, "detected_public_fictional_characters") |
| elif add_logo: |
| img = add_logo_to_image(img) |
| filename = save_image_with_metadata( |
| img, prompt, negative_prompt, steps, guidance_scale, guidance_rescale, seed |
| ) |
|
|
| return { |
| "filename": filename, |
| "url": f"/output/{filename}", |
| "prompt": prompt, |
| "negative_prompt": negative_prompt, |
| "width": width, |
| "height": height, |
| "steps": steps, |
| "guidance_scale": guidance_scale, |
| "guidance_rescale": guidance_rescale, |
| "seed": seed, |
| "latent_filename": None, |
| } |
|
|
|
|
| def generate_grid9(prompt, negative_prompt, |
| width, height, |
| steps, guidance_scale, guidance_rescale, |
| seeds, |
| add_logo: bool = True, |
| enable_character_detection: bool = True): |
| """ |
| 3x3 (9枚) をまとめて生成。ここでも latent は保存しない。 |
| """ |
| ensure_pipeline_available() |
|
|
| if len(seeds) != 9: |
| raise ValueError("seeds must have length 9") |
|
|
| prompts = [prompt] * 9 |
| negative_prompts = [negative_prompt] * 9 |
| generators = [torch.Generator().manual_seed(int(s)) for s in seeds] |
|
|
| with torch.no_grad(): |
| ret = pipe( |
| prompt=prompts, |
| negative_prompt=negative_prompts, |
| guidance_scale=float(guidance_scale), |
| guidance_rescale=float(guidance_rescale), |
| generator=generators, |
| width=int(width), |
| height=int(height), |
| num_inference_steps=int(steps), |
| ) |
|
|
| images_info = [] |
| imgs = ret.images |
|
|
| for idx, (seed, img) in enumerate(zip(seeds, imgs)): |
| detected = bool(enable_character_detection and _has_detected_public_fictional_characters(ret, idx)) |
| if detected: |
| img_to_save = make_character_detection_placeholder(img.size, "detected_public_fictional_characters") |
| elif add_logo: |
| img_to_save = add_logo_to_image(img) |
| else: |
| img_to_save = img |
| filename = save_image_with_metadata( |
| img_to_save, prompt, negative_prompt, steps, guidance_scale, guidance_rescale, seed |
| ) |
| images_info.append({ |
| "filename": filename, |
| "url": f"/output/{filename}", |
| "prompt": prompt, |
| "negative_prompt": negative_prompt, |
| "width": width, |
| "height": height, |
| "steps": steps, |
| "guidance_scale": guidance_scale, |
| "guidance_rescale": guidance_rescale, |
| "seed": seed, |
| "latent_filename": None, |
| }) |
|
|
| return images_info |
|
|
|
|
| def get_latent_path_for_image(filename: str) -> Optional[Path]: |
| stem = Path(filename).stem |
| pt = OUTPUT_DIR / f"{stem}.pt" |
| if pt.exists(): |
| return pt |
| return None |
|
|
|
|
| def reconstruct_latent_for_image(filename: str) -> Optional[Path]: |
| """ |
| PNG の parameters から latent を再計算して保存し、その Path を返す。 |
| 失敗したら None を返す。 |
| """ |
| if Image is None or torch is None or pipe is None: |
| return None |
|
|
| png_path = OUTPUT_DIR / filename |
| if not png_path.exists(): |
| return None |
|
|
| img = Image.open(png_path) |
| width, height = img.width, img.height |
| param_text = img.info.get("parameters") |
| if not param_text: |
| return None |
|
|
| meta = parse_parameters_text(param_text) |
| if meta is None: |
| return None |
|
|
| prompt = meta["prompt"] |
| negative_prompt = meta["negative_prompt"] |
| steps = meta["steps"] |
| guidance_scale = meta["guidance_scale"] |
| guidance_rescale = meta["guidance_rescale"] |
| seed = meta["seed"] |
|
|
| try: |
| ensure_pipeline_available() |
| generator = torch.Generator().manual_seed(int(seed)) |
| with torch.no_grad(): |
| ret = pipe( |
| prompt=prompt, |
| negative_prompt=negative_prompt, |
| guidance_scale=float(guidance_scale), |
| guidance_rescale=float(guidance_rescale), |
| generator=generator, |
| width=int(width), |
| height=int(height), |
| num_inference_steps=int(steps), |
| return_latents=True, |
| ) |
| latents = getattr(ret, "latents", None) |
| if isinstance(latents, torch.Tensor): |
| latents_1 = latents[0] |
| elif isinstance(latents, (list, tuple)) and len(latents) > 0: |
| latents_1 = latents[0] |
| else: |
| return None |
|
|
| stem = Path(filename).stem |
| latent_name = save_latent(latents_1, stem) |
| return OUTPUT_DIR / latent_name |
| except Exception: |
| |
| return None |
|
|
|
|
| def decode_mask_from_base64(png_base64: str, target_latent_shape): |
| """ |
| dataURL (image/png;base64,...) を受け取り、 |
| latent の (H, W) に縮小した 0〜1 の torch.Tensor mask を返す。 |
| """ |
| if Image is None or torch is None: |
| raise RuntimeError("Pillow/torch are required for mask processing.") |
|
|
| if png_base64.startswith("data:"): |
| png_base64 = png_base64.split(",", 1)[1] |
|
|
| raw = base64.b64decode(png_base64) |
| img = Image.open(io.BytesIO(raw)).convert("RGBA") |
| |
| alpha = img.split()[-1] |
| |
| alpha = alpha.point(lambda v: 255 if v > 10 else 0) |
|
|
| |
| _, _, h_latent, w_latent = target_latent_shape |
| alpha_small = alpha.resize((w_latent, h_latent), resample=Image.NEAREST) |
|
|
| |
| import numpy as np |
| alpha_np = np.array(alpha_small, dtype="float32") / 255.0 |
| mask = torch.from_numpy(alpha_np) |
| mask = mask.clamp(0.0, 1.0) |
| mask = mask.unsqueeze(0).unsqueeze(0) |
| return mask.to(device=device, dtype=dtype) |
|
|
|
|
| app = Flask(__name__, static_folder="static", static_url_path="/static") |
|
|
|
|
| @app.route("/") |
| def index(): |
| return send_from_directory(app.static_folder, "index.html") |
|
|
|
|
| @app.route("/output/<path:filename>") |
| def serve_output(filename): |
| return send_from_directory(OUTPUT_DIR, filename) |
|
|
|
|
| @app.route("/api/generate-one", methods=["POST"]) |
| def api_generate_one(): |
| data = request.get_json(force=True) |
| prompt = data.get("prompt", "").strip() |
| negative_prompt = data.get("negative_prompt", "").strip() |
| width = int(data.get("width", 672)) |
| height = int(data.get("height", 896)) |
| steps = int(data.get("steps", 40)) |
| guidance_scale = float(data.get("guidance_scale", 6.0)) |
| guidance_rescale = float(data.get("guidance_rescale", 0.7)) |
|
|
| seed_param = data.get("seed", None) |
| if seed_param is None or str(seed_param) == "": |
| seed = random.randint(0, 2**31 - 1) |
| else: |
| seed = int(seed_param) |
|
|
| no_logo = parse_no_logo(data.get("no_logo", False)) |
| disable_character_detection = parse_disable_character_detection(data.get("disable_character_detection", False)) |
|
|
|
|
| try: |
| info = generate_single_image( |
| prompt=prompt, |
| negative_prompt=negative_prompt, |
| width=width, |
| height=height, |
| steps=steps, |
| guidance_scale=guidance_scale, |
| guidance_rescale=guidance_rescale, |
| seed=seed, |
| add_logo=not no_logo, |
| enable_character_detection=not disable_character_detection, |
| ) |
| return jsonify(info) |
| except Exception as e: |
| return jsonify({"error": str(e)}), 500 |
|
|
|
|
| @app.route("/api/generate-grid9", methods=["POST"]) |
| def api_generate_grid9(): |
| data = request.get_json(force=True) |
| prompt = data.get("prompt", "").strip() |
| negative_prompt = data.get("negative_prompt", "").strip() |
| width = int(data.get("width", 672)) |
| height = int(data.get("height", 896)) |
| steps = int(data.get("steps", 40)) |
| guidance_scale = float(data.get("guidance_scale", 6.0)) |
| guidance_rescale = float(data.get("guidance_rescale", 0.7)) |
|
|
| first_seed_param = data.get("first_seed", None) |
|
|
| seeds = [] |
| if first_seed_param is not None and str(first_seed_param) != "": |
| seed0 = int(first_seed_param) |
| else: |
| seed0 = random.randint(0, 2**31 - 1) |
| seeds.append(seed0) |
| for _ in range(8): |
| seeds.append(random.randint(0, 2**31 - 1)) |
|
|
| no_logo = parse_no_logo(data.get("no_logo", False)) |
|
|
| disable_character_detection = parse_disable_character_detection(data.get("disable_character_detection", False)) |
|
|
| try: |
| images_info = generate_grid9( |
| prompt=prompt, |
| negative_prompt=negative_prompt, |
| width=width, |
| height=height, |
| steps=steps, |
| guidance_scale=guidance_scale, |
| guidance_rescale=guidance_rescale, |
| seeds=seeds, |
| add_logo=not no_logo, |
| enable_character_detection=not disable_character_detection, |
| ) |
| return jsonify({"images": images_info}) |
| except Exception as e: |
| return jsonify({"error": str(e)}), 500 |
|
|
|
|
| @app.route("/api/random-word", methods=["GET"]) |
| def api_random_word(): |
| word = random.choice(RANDOM_WORDS) |
| return jsonify({"word": word}) |
|
|
|
|
| @app.route("/api/upload-image", methods=["POST"]) |
| def api_upload_image(): |
| if Image is None: |
| return jsonify({"error": "Pillow is not available; install pillow to use this feature."}), 500 |
|
|
| file = request.files.get("file") |
| if not file: |
| return jsonify({"error": "no file"}), 400 |
|
|
| original_name = sanitize_for_filename(file.filename or "uploaded") |
| now = datetime.datetime.now().strftime("%Y%m%d_%H%M%S_%f") |
| filename = f"uploaded_{now}_{original_name}.png" |
| save_path = OUTPUT_DIR / filename |
| file.save(save_path) |
|
|
| img = Image.open(save_path) |
| param_text = img.info.get("parameters") |
| if not param_text: |
| meta = { |
| "prompt": "", |
| "negative_prompt": "", |
| "steps": 40, |
| "guidance_scale": 6.0, |
| "guidance_rescale": 0.7, |
| "seed": random.randint(0, 2**31 - 1), |
| } |
| else: |
| parsed = parse_parameters_text(param_text) |
| if parsed is None: |
| meta = { |
| "prompt": "", |
| "negative_prompt": "", |
| "steps": 40, |
| "guidance_scale": 6.0, |
| "guidance_rescale": 0.7, |
| "seed": random.randint(0, 2**31 - 1), |
| } |
| else: |
| meta = parsed |
|
|
| meta.update({ |
| "filename": filename, |
| "url": f"/output/{filename}", |
| "width": img.width, |
| "height": img.height, |
| "latent_filename": None, |
| }) |
|
|
| return jsonify(meta) |
|
|
|
|
| @app.route("/api/inpaint-init", methods=["POST"]) |
| def api_inpaint_init(): |
| """ |
| inpaint モード開始時に呼び出す。 |
| - 既に latent .pt があればその名前を返す。 |
| - 無ければ、PNG の parameters から同じ条件で再生成して latent だけ保存する。 |
| """ |
| data = request.get_json(force=True) |
| filename = data.get("filename") |
| if not filename: |
| return jsonify({"error": "filename is required"}), 400 |
|
|
| |
| latent_path = get_latent_path_for_image(filename) |
| if latent_path is not None: |
| return jsonify({"latent_filename": latent_path.name}) |
|
|
| |
| if Image is None or torch is None or pipe is None: |
| return jsonify({"error": "pipeline not available for latent reconstruction"}), 500 |
|
|
| png_path = OUTPUT_DIR / filename |
| if not png_path.exists(): |
| return jsonify({"error": f"image not found: {filename}"}), 400 |
|
|
| img = Image.open(png_path) |
| width, height = img.width, img.height |
| param_text = img.info.get("parameters") |
| if not param_text: |
| return jsonify({"error": "no parameters in PNG; cannot reconstruct latent"}), 400 |
|
|
| meta = parse_parameters_text(param_text) |
| if meta is None: |
| return jsonify({"error": "failed to parse parameters; cannot reconstruct latent"}), 400 |
|
|
| prompt = meta["prompt"] |
| negative_prompt = meta["negative_prompt"] |
| steps = meta["steps"] |
| guidance_scale = meta["guidance_scale"] |
| guidance_rescale = meta["guidance_rescale"] |
| seed = meta["seed"] |
|
|
| try: |
| ensure_pipeline_available() |
| generator = torch.Generator().manual_seed(int(seed)) |
| with torch.no_grad(): |
| ret = pipe( |
| prompt=prompt, |
| negative_prompt=negative_prompt, |
| guidance_scale=float(guidance_scale), |
| guidance_rescale=float(guidance_rescale), |
| generator=generator, |
| width=int(width), |
| height=int(height), |
| num_inference_steps=int(steps), |
| return_latents=True, |
| ) |
| latents = getattr(ret, "latents", None) |
| if isinstance(latents, torch.Tensor): |
| latents_1 = latents[0] |
| elif isinstance(latents, (list, tuple)) and len(latents) > 0: |
| latents_1 = latents[0] |
| else: |
| return jsonify({"error": "pipeline did not return latents"}), 500 |
|
|
| stem = Path(filename).stem |
| latent_filename = save_latent(latents_1, stem) |
| return jsonify({"latent_filename": latent_filename}) |
| except Exception as e: |
| return jsonify({"error": f"latent reconstruction failed: {e}"}), 500 |
|
|
|
|
| def get_unique_inpaint_names(original_filename: str, original_latent_filename: str) -> (str, str): |
| """ |
| もとの画像/latent のファイル名から、上書きしない I<n>_ プレフィックス付きの |
| 画像ファイル名と latent ステムを返す。 |
| 例: original.png -> I1_original.png, I1_original_latentstem |
| """ |
| base_name = Path(original_filename).name |
| latent_stem = Path(original_latent_filename).stem if original_latent_filename else Path(original_filename).stem |
|
|
| |
| n = 1 |
| while True: |
| candidate_img = f"I{n}_" + base_name |
| if not (OUTPUT_DIR / candidate_img).exists(): |
| break |
| n += 1 |
|
|
| new_image_name = f"I{n}_" + base_name |
| new_latent_stem = f"I{n}_" + latent_stem |
| return new_image_name, new_latent_stem |
|
|
|
|
| @app.route("/api/inpaint-repaint", methods=["POST"]) |
| def api_inpaint_repaint(): |
| """ |
| latent + mask + prompt から inpaint して、新しい画像&latent を保存。 |
| Mitsua Likes の pipeline 側に inpaint_internal_latents(latents, mask, ...) を |
| 追加してある前提。 |
| """ |
| if pipe is None or torch is None: |
| return jsonify({"error": "pipeline not available"}), 500 |
| if not hasattr(pipe, "inpaint_internal_latents"): |
| return jsonify({ |
| "error": "pipe.inpaint_internal_latents が実装されていません。" |
| "Mitsua Likes の pipeline に inpaint_internal_latents を追加してください。" |
| }), 500 |
|
|
| data = request.get_json(force=True) |
| filename = data.get("filename") |
| latent_filename = data.get("latent_filename") |
| prompt = data.get("prompt", "").strip() |
| negative_prompt = data.get("negative_prompt", "").strip() |
| width = int(data.get("width", 672)) |
| height = int(data.get("height", 896)) |
| steps = int(data.get("steps", 40)) |
| guidance_scale = float(data.get("guidance_scale", 6.0)) |
| guidance_rescale = float(data.get("guidance_rescale", 0.7)) |
| no_logo = parse_no_logo(data.get("no_logo", False)) |
| disable_character_detection = parse_disable_character_detection(data.get("disable_character_detection", False)) |
| seed = int(data.get("seed", 0)) |
| strength = float(data.get("strength", 0.5)) |
| mask_png = data.get("mask_png") |
|
|
| |
| i2i_mode = bool(data.get("i2i_mode", False)) |
| i2i_mask_pct = data.get("i2i_mask_pct", None) |
|
|
| if not filename: |
| return jsonify({"error": "filename is required"}), 400 |
| if not mask_png and not i2i_mode: |
| return jsonify({"error": "mask_png is required"}), 400 |
|
|
|
|
| |
| latent_path: Optional[Path] = None |
| if latent_filename: |
| candidate = OUTPUT_DIR / latent_filename |
| if candidate.exists(): |
| latent_path = candidate |
|
|
| |
| if latent_path is None: |
| lp = get_latent_path_for_image(filename) |
| if lp is not None and lp.exists(): |
| latent_path = lp |
| latent_filename = lp.name |
|
|
| |
| if latent_path is None: |
| lp = reconstruct_latent_for_image(filename) |
| if lp is None or not lp.exists(): |
| |
| return jsonify({ |
| "error": "latent (.pt) が見つかりません(latents 付きで生成された画像か、PNG+PT をドロップしてください)" |
| }), 400 |
| latent_path = lp |
| latent_filename = lp.name |
|
|
| |
| latents = torch.load(latent_path, map_location=device) |
| if latents.dim() == 3: |
| latents = latents.unsqueeze(0) |
|
|
| |
| if i2i_mode: |
| |
| pct = 100.0 |
| if i2i_mask_pct is not None: |
| try: |
| pct = float(i2i_mask_pct) |
| except (TypeError, ValueError): |
| pct = 100.0 |
| pct = max(0.0, min(100.0, pct)) |
| mask_value = pct / 100.0 |
|
|
| |
| _, _, h, w = latents.shape |
| |
| mask_tensor = torch.full((1, 1, h, w), mask_value, dtype=dtype) |
| else: |
| |
| mask_tensor = decode_mask_from_base64(mask_png, latents.shape) |
|
|
| |
| try: |
| with torch.no_grad(): |
| ret = pipe.inpaint_internal_latents( |
| latents=latents.to(device=device, dtype=dtype), |
| mask=mask_tensor, |
| prompt=prompt, |
| negative_prompt=negative_prompt, |
| num_inference_steps=steps, |
| guidance_scale=guidance_scale, |
| guidance_rescale=guidance_rescale, |
| output_type="pil", |
| ) |
| except Exception as e: |
| return jsonify({"error": f"inpaint_internal_latents failed: {e}"}), 500 |
|
|
| |
| img = ret.images[0] |
| detected = bool((not disable_character_detection) and _has_detected_public_fictional_characters(ret, 0)) |
| if detected: |
| img = make_character_detection_placeholder(img.size, "detected_public_fictional_characters") |
| elif not no_logo: |
| img = add_logo_to_image(img) |
| new_latents = getattr(ret, "latents", None) |
| if isinstance(new_latents, torch.Tensor) and new_latents.dim() == 4: |
| new_latent_1 = new_latents[0] |
| elif isinstance(new_latents, (list, tuple)) and len(new_latents) > 0: |
| new_latent_1 = new_latents[0] |
| else: |
| new_latent_1 = None |
|
|
| |
| new_image_name, new_latent_name_stem = get_unique_inpaint_names(filename, latent_filename) |
|
|
| |
| saved_image_name = save_image_with_metadata( |
| img, prompt, negative_prompt, steps, guidance_scale, guidance_rescale, seed, |
| filename_override=new_image_name |
| ) |
|
|
| |
| if new_latent_1 is not None: |
| latent_saved_name = save_latent(new_latent_1, new_latent_name_stem) |
| else: |
| latent_saved_name = None |
|
|
| info = { |
| "filename": saved_image_name, |
| "url": f"/output/{saved_image_name}", |
| "prompt": prompt, |
| "negative_prompt": negative_prompt, |
| "width": width, |
| "height": height, |
| "steps": steps, |
| "guidance_scale": guidance_scale, |
| "guidance_rescale": guidance_rescale, |
| "seed": seed, |
| "latent_filename": latent_saved_name, |
| } |
| return jsonify(info) |
|
|
| @app.route("/api/inpaint-upload", methods=["POST"]) |
| def api_inpaint_upload(): |
| """ |
| inpaint モード用のアップロード。 |
| PNG と対応する .pt をまとめてドラッグ&ドロップした場合を想定。 |
| - PNG からメタ情報を読み取り |
| - .pt は output/ にコピー |
| """ |
| if Image is None: |
| return jsonify({"error": "Pillow is not available"}), 500 |
|
|
| files = request.files.getlist("files") |
| if not files: |
| return jsonify({"error": "no files"}), 400 |
|
|
| png_file = None |
| pt_file = None |
| for f in files: |
| name = f.filename or "" |
| lower = name.lower() |
| if lower.endswith(".png"): |
| png_file = f |
| elif lower.endswith(".pt"): |
| pt_file = f |
|
|
| if png_file is None: |
| return jsonify({"error": "png file is required"}), 400 |
|
|
| |
| original_name = sanitize_for_filename(png_file.filename or "uploaded") |
| now = datetime.datetime.now().strftime("%Y%m%d_%H%M%S_%f") |
| filename = f"inpaint_uploaded_{now}_{original_name}.png" |
| save_path = OUTPUT_DIR / filename |
| png_file.save(save_path) |
|
|
| img = Image.open(save_path) |
| param_text = img.info.get("parameters") |
| if not param_text: |
| meta = { |
| "prompt": "", |
| "negative_prompt": "", |
| "steps": 40, |
| "guidance_scale": 6.0, |
| "guidance_rescale": 0.7, |
| "seed": random.randint(0, 2**31 - 1), |
| } |
| else: |
| parsed = parse_parameters_text(param_text) |
| if parsed is None: |
| meta = { |
| "prompt": "", |
| "negative_prompt": "", |
| "steps": 40, |
| "guidance_scale": 6.0, |
| "guidance_rescale": 0.7, |
| "seed": random.randint(0, 2**31 - 1), |
| } |
| else: |
| meta = parsed |
|
|
| latent_filename = None |
| if pt_file is not None: |
| latent_name = f"{Path(filename).stem}.pt" |
| latent_path = OUTPUT_DIR / latent_name |
| pt_file.save(latent_path) |
| latent_filename = latent_name |
|
|
| meta.update({ |
| "filename": filename, |
| "url": f"/output/{filename}", |
| "width": img.width, |
| "height": img.height, |
| "latent_filename": latent_filename, |
| }) |
|
|
| return jsonify(meta) |
|
|
|
|
| if __name__ == "__main__": |
| app.run(host="0.0.0.0", port=5000, debug=True) |
|
|