| import os |
| import io |
| import json |
| import zipfile |
| import traceback |
| from datetime import datetime |
| from typing import Optional, Tuple, List |
|
|
| import gradio as gr |
| from PIL import Image, ImageDraw, ImageFont |
|
|
| try: |
| from huggingface_hub import InferenceClient |
| except Exception: |
| InferenceClient = None |
|
|
| APP_TITLE = "画像生成ハブ" |
| APP_DESC = "ジャンルフリー版。基本は選択式、必要時だけ自由入力。診断ログ付き。" |
|
|
| HF_TOKEN = os.getenv("HF_TOKEN", "") |
| TEXT2IMG_MODEL = os.getenv("TEXT2IMG_MODEL", "black-forest-labs/FLUX.1-schnell") |
|
|
| DATA_DIR = "data" |
| CHAR_DIR = os.path.join(DATA_DIR, "characters") |
| EXPORT_DIR = os.path.join(DATA_DIR, "exports") |
| os.makedirs(CHAR_DIR, exist_ok=True) |
| os.makedirs(EXPORT_DIR, exist_ok=True) |
|
|
| CATEGORY_MAP = { |
| "動物(陸)": ["犬", "猫", "うさぎ", "くま", "きつね", "たぬき", "ハムスター", "その他"], |
| "海の生き物": ["イルカ", "シャチ", "シロイルカ", "クラゲ", "ペンギン", "アザラシ", "ラッコ", "フグ", "その他"], |
| "鳥": ["すずめ", "ひよこ", "ふくろう", "ペンギン", "カモメ", "その他"], |
| "ファンタジー": ["ドラゴン", "精霊", "ユニコーン", "スライム", "魔法生物", "その他"], |
| "人型": ["男の子", "女の子", "中性的", "ちびキャラ", "マスコット人型", "その他"], |
| "食べ物・物": ["たい焼き", "おにぎり", "パン", "コーヒーカップ", "星", "雲", "その他"], |
| "完全自由": ["自由入力"], |
| } |
|
|
| PURPOSE_PRESETS = { |
| "ステッカー向け": { |
| "size": "1024x1024", |
| "detail": "plain background, centered character, no text, sticker-friendly, clean silhouette", |
| "style": "ステッカー向け", |
| }, |
| "MV素材向け": { |
| "size": "1344x768", |
| "detail": "cinematic composition, atmospheric, no text, suitable for MV visual", |
| "style": "アニメ寄り", |
| }, |
| "SNSアイコン向け": { |
| "size": "1024x1024", |
| "detail": "simple composition, centered subject, clean thumbnail readability, no text", |
| "style": "シンプル", |
| }, |
| "商品画像向け": { |
| "size": "1536x1024", |
| "detail": "clean product-style presentation, bright background, no text", |
| "style": "シンプル", |
| }, |
| "背景向け": { |
| "size": "1344x768", |
| "detail": "wide background scene, no text, rich atmosphere, suitable as background", |
| "style": "水彩寄り", |
| }, |
| } |
|
|
| SIZE_PRESETS = { |
| "1024x1024": (1024, 1024), |
| "1344x768": (1344, 768), |
| "768x1344": (768, 1344), |
| "1536x1024": (1536, 1024), |
| "1024x1536": (1024, 1536), |
| } |
|
|
| PERSONALITY_CHOICES = ["無気力", "眠そう", "ぼんやり", "やさしい", "元気", "ちょい不機嫌", "自由入力"] |
| PALETTE_CHOICES = ["白系", "水色系", "黒白系", "グレー系", "ベージュ系", "パステル系", "自由入力"] |
| STYLE_CHOICES = ["ゆるい", "ステッカー向け", "シンプル", "水彩寄り", "アニメ寄り", "ミニマル", "自由入力"] |
| EXPRESSION_CHOICES = ["半目", "眠い", "無表情", "ぼーっと", "うとうと", "少し笑う", "自由入力"] |
| POSE_CHOICES = ["座る", "伏せる", "漂う", "正面", "横向き", "ちょこんと立つ", "自由入力"] |
|
|
|
|
| MODEL_CHOICES = [ |
| "black-forest-labs/FLUX.1-schnell", |
| "black-forest-labs/FLUX.1-dev", |
| "stabilityai/stable-diffusion-xl-base-1.0", |
| ] |
|
|
| NEGATIVE_PROMPT_DEFAULT = ( |
| "text, letters, japanese text, english text, typography, caption, subtitle, watermark, " |
| "logo, signature, blurry, low quality, deformed, extra limbs, extra fingers, multiple characters, " |
| "cropped, cluttered background" |
| ) |
|
|
| TERM_MAP = { |
| "犬": "dog", |
| "猫": "cat", |
| "うさぎ": "rabbit", |
| "くま": "bear", |
| "きつね": "fox", |
| "たぬき": "tanuki", |
| "ハムスター": "hamster", |
| "イルカ": "dolphin", |
| "シャチ": "orca", |
| "シロイルカ": "beluga whale", |
| "クラゲ": "jellyfish", |
| "ペンギン": "penguin", |
| "アザラシ": "seal", |
| "ラッコ": "otter", |
| "フグ": "pufferfish", |
| "すずめ": "sparrow", |
| "ひよこ": "chick", |
| "ふくろう": "owl", |
| "カモメ": "seagull", |
| "ドラゴン": "dragon", |
| "精霊": "spirit", |
| "ユニコーン": "unicorn", |
| "スライム": "slime creature", |
| "魔法生物": "magical creature", |
| "男の子": "boy character", |
| "女の子": "girl character", |
| "中性的": "androgynous character", |
| "ちびキャラ": "chibi character", |
| "マスコット人型": "mascot-style human character", |
| "たい焼き": "taiyaki mascot", |
| "おにぎり": "onigiri mascot", |
| "パン": "bread mascot", |
| "コーヒーカップ": "coffee cup mascot", |
| "星": "star mascot", |
| "雲": "cloud mascot", |
| "無気力": "low-energy", |
| "眠そう": "sleepy", |
| "ぼんやり": "vacant gentle", |
| "やさしい": "gentle", |
| "元気": "energetic", |
| "ちょい不機嫌": "slightly grumpy", |
| "白系": "white and cream palette", |
| "水色系": "pale aqua palette", |
| "黒白系": "black and white palette", |
| "グレー系": "soft gray palette", |
| "ベージュ系": "beige and cream palette", |
| "パステル系": "pastel palette", |
| "ゆるい": "cute relaxed mascot illustration", |
| "ステッカー向け": "clean sticker-style illustration", |
| "シンプル": "simple minimal illustration", |
| "水彩寄り": "soft watercolor illustration", |
| "アニメ寄り": "anime-inspired illustration", |
| "ミニマル": "minimal flat illustration", |
| "半目": "half-closed eyes", |
| "眠い": "sleepy eyes", |
| "無表情": "blank expression", |
| "ぼーっと": "vacant calm expression", |
| "うとうと": "drowsy expression", |
| "少し笑う": "slight smile", |
| "座る": "sitting pose", |
| "伏せる": "lying down pose", |
| "漂う": "floating pose", |
| "正面": "front view", |
| "横向き": "side view", |
| "ちょこんと立つ": "small standing pose", |
| } |
|
|
| def to_english_term(value: str) -> str: |
| value = (value or "").strip() |
| return TERM_MAP.get(value, value) |
|
|
| def build_clean_detail_text(detail_text: str) -> str: |
| return " ".join((detail_text or "").strip().split()) |
|
|
|
|
| def safe_name(text: str) -> str: |
| text = (text or "untitled").strip() |
| allowed = "-_" |
| return "".join(ch for ch in text if ch.isalnum() or ch in allowed).strip("_") or "untitled" |
|
|
|
|
|
|
| def mask_token(token: str) -> str: |
| if not token: |
| return "未設定" |
| if len(token) <= 8: |
| return "設定あり" |
| return f"{token[:4]}...{token[-4:]}" |
|
|
|
|
|
|
| def runtime_status_text(model_name: Optional[str] = None) -> str: |
| selected_model = model_name or TEXT2IMG_MODEL |
| lines = [ |
| f"HF_TOKEN: {'あり' if HF_TOKEN else 'なし'} ({mask_token(HF_TOKEN)})", |
| f"InferenceClient import: {'OK' if InferenceClient is not None else 'NG'}", |
| f"TEXT2IMG_MODEL(default): {TEXT2IMG_MODEL}", |
| f"TEXT2IMG_MODEL(selected): {selected_model}", |
| ] |
| return "\n".join(lines) |
|
|
|
|
|
|
| def get_client() -> Tuple[Optional["InferenceClient"], str]: |
| if not HF_TOKEN: |
| return None, "HF_TOKEN が未設定です。Space の Secrets に HF_TOKEN を追加してください。" |
| if InferenceClient is None: |
| return None, "huggingface_hub / InferenceClient の import に失敗しています。" |
| try: |
| client = InferenceClient(api_key=HF_TOKEN) |
| return client, "InferenceClient の初期化に成功しました。" |
| except Exception as e: |
| return None, f"InferenceClient 初期化失敗: {type(e).__name__}: {e}" |
|
|
|
|
|
|
| def visible_if_custom(value: str): |
| return gr.update(visible=value in {"自由入力", "その他"}) |
|
|
|
|
|
|
| def motif_choices(category: str): |
| return CATEGORY_MAP.get(category, ["その他"]) |
|
|
|
|
|
|
| def size_from_preset(size_name: str): |
| return SIZE_PRESETS.get(size_name, (1024, 1024)) |
|
|
|
|
|
|
| def apply_purpose_preset(purpose: str): |
| preset = PURPOSE_PRESETS.get(purpose, PURPOSE_PRESETS["ステッカー向け"]) |
| return preset["size"], preset["style"], preset["detail"] |
|
|
|
|
|
|
| def draw_placeholder(prompt: str, width: int, height: int, title: str = "PREVIEW") -> Image.Image: |
| img = Image.new("RGB", (width, height), (236, 238, 242)) |
| draw = ImageDraw.Draw(img) |
| font = ImageFont.load_default() |
| box_w = width - 80 |
| x0, y0 = 40, 40 |
| draw.rectangle([x0, y0, x0 + box_w, min(height - 40, y0 + 240)], outline=(255, 145, 0), width=4) |
| text = f"{title}\n\nPrompt preview:\n{prompt[:800]}" |
| draw.multiline_text((x0 + 20, y0 + 20), text, fill=(60, 60, 60), font=font, spacing=6) |
| return img |
|
|
|
|
|
|
| def build_character_prompt( |
| char_name: str, |
| category: str, |
| motif: str, |
| motif_custom: str, |
| personality: str, |
| personality_custom: str, |
| palette: str, |
| palette_custom: str, |
| style: str, |
| style_custom: str, |
| expression: str, |
| expression_custom: str, |
| pose: str, |
| pose_custom: str, |
| detail_text: str, |
| ) -> Tuple[str, dict]: |
| motif_final = motif_custom.strip() if motif in {"その他", "自由入力"} else motif |
| personality_final = personality_custom.strip() if personality == "自由入力" else personality |
| palette_final = palette_custom.strip() if palette == "自由入力" else palette |
| style_final = style_custom.strip() if style == "自由入力" else style |
| expression_final = expression_custom.strip() if expression == "自由入力" else expression |
| pose_final = pose_custom.strip() if pose == "自由入力" else pose |
|
|
| motif_en = to_english_term(motif_final) |
| personality_en = to_english_term(personality_final) |
| palette_en = to_english_term(palette_final) |
| style_en = to_english_term(style_final) |
| expression_en = to_english_term(expression_final) |
| pose_en = to_english_term(pose_final) |
| detail_clean = build_clean_detail_text(detail_text) |
|
|
| char_name_clean = char_name.strip() |
| subject = f"{motif_en} mascot character" if motif_en else "cute mascot character" |
|
|
| prompt_parts = [ |
| subject, |
| f"named {char_name_clean}" if char_name_clean else "", |
| personality_en, |
| expression_en, |
| pose_en, |
| palette_en, |
| style_en, |
| "single character", |
| "centered composition", |
| "plain clean background", |
| "clean silhouette", |
| "no text", |
| "no letters", |
| "no watermark", |
| detail_clean, |
| ] |
| prompt = ", ".join([p for p in prompt_parts if p]) |
| meta = { |
| "char_name": char_name_clean, |
| "category": category, |
| "motif": motif_final, |
| "personality": personality_final, |
| "palette": palette_final, |
| "style": style_final, |
| "expression": expression_final, |
| "pose": pose_final, |
| "detail_text": detail_clean, |
| "prompt": prompt, |
| "negative_prompt": NEGATIVE_PROMPT_DEFAULT, |
| } |
| return prompt, meta |
|
|
|
|
|
|
| def generate_images( |
| prompt: str, |
| width: int, |
| height: int, |
| num_images: int, |
| model_name: Optional[str] = None, |
| negative_prompt: str = NEGATIVE_PROMPT_DEFAULT, |
| ) -> Tuple[List[Image.Image], str]: |
| selected_model = (model_name or TEXT2IMG_MODEL).strip() |
| logs = [runtime_status_text(selected_model)] |
| client, init_message = get_client() |
| logs.append(init_message) |
| logs.append(f"prompt: {prompt}") |
| logs.append(f"negative_prompt: {negative_prompt}") |
|
|
| if client is None: |
| logs.append("実APIは使えないため、プレースホルダー画像を返します。") |
| images = [draw_placeholder(prompt, width, height, f"PREVIEW {i+1}") for i in range(num_images)] |
| return images, "\n".join(logs) |
|
|
| images: List[Image.Image] = [] |
| try: |
| logs.append(f"モデル呼び出し開始: {selected_model}") |
| for i in range(num_images): |
| result = client.text_to_image( |
| prompt, |
| model=selected_model, |
| width=width, |
| height=height, |
| negative_prompt=negative_prompt, |
| ) |
| if isinstance(result, Image.Image): |
| images.append(result) |
| else: |
| images.append(Image.open(io.BytesIO(result)).convert("RGB")) |
| logs.append(f"{i+1}/{num_images} 枚目生成成功") |
| return images, "\n".join(logs) |
| except Exception as e: |
| logs.append(f"生成失敗: {type(e).__name__}: {e}") |
| logs.append(traceback.format_exc()) |
| fallback = [draw_placeholder(prompt, width, height, f"ERROR PREVIEW {i+1}") for i in range(num_images)] |
| return fallback, "\n".join(logs) |
|
|
|
|
|
|
| def make_gallery(images: List[Image.Image]): |
| return [(img, f"candidate_{i+1}.png") for i, img in enumerate(images)] |
|
|
|
|
|
|
| def new_character_generate( |
| char_name, |
| purpose, |
| model_name, |
| size_name, |
| category, |
| motif, |
| motif_custom, |
| personality, |
| personality_custom, |
| palette, |
| palette_custom, |
| style, |
| style_custom, |
| expression, |
| expression_custom, |
| pose, |
| pose_custom, |
| count, |
| detail_text, |
| ): |
| width, height = size_from_preset(size_name) |
| prompt, meta = build_character_prompt( |
| char_name, |
| category, |
| motif, |
| motif_custom, |
| personality, |
| personality_custom, |
| palette, |
| palette_custom, |
| style, |
| style_custom, |
| expression, |
| expression_custom, |
| pose, |
| pose_custom, |
| detail_text, |
| ) |
| meta["purpose"] = purpose |
| images, diag_text = generate_images(prompt, width, height, int(count), model_name=model_name) |
| gallery = make_gallery(images) |
| meta_text = json.dumps(meta, ensure_ascii=False, indent=2) |
| base_img = images[0] if images else None |
| return gallery, meta_text, base_img, diag_text |
|
|
|
|
|
|
| def variant_generate(base_image, variation_request, count, model_name): |
| if base_image is None: |
| msg = "ベース画像がありません。先に画像を保存するか、ベース画像をアップロードしてください。" |
| return [], msg |
| if not isinstance(base_image, Image.Image): |
| try: |
| base_image = Image.open(base_image).convert("RGB") |
| except Exception: |
| return [], "ベース画像の読み込みに失敗しました。" |
|
|
| width, height = base_image.size |
| prompt = f"same mascot character, same identity, same colors, same silhouette, variation only, {variation_request}" |
| images, diag_text = generate_images(prompt, width, height, int(count), model_name=model_name) |
| return make_gallery(images), diag_text |
|
|
|
|
|
|
| def background_generate(world, time_of_day, color_tone, ratio, detail_text, count, model_name): |
| width, height = size_from_preset(ratio) |
| prompt = f"{world}, {time_of_day}, {color_tone}, cinematic background scene, no text, no logo, {build_clean_detail_text(detail_text)}" |
| images, diag_text = generate_images(prompt, width, height, int(count), model_name=model_name) |
| return make_gallery(images), diag_text |
|
|
|
|
|
|
| def product_generate(subject, product_type, mood, ratio, detail_text, count, model_name): |
| width, height = size_from_preset(ratio) |
| prompt = f"{subject}, {product_type}, {mood}, clean product presentation, no text, no logo, {build_clean_detail_text(detail_text)}" |
| images, diag_text = generate_images(prompt, width, height, int(count), model_name=model_name) |
| return make_gallery(images), diag_text |
|
|
|
|
|
|
| def add_sticker_border(image, border_px=24): |
| if image is None: |
| return None, "画像がありません。" |
| if not isinstance(image, Image.Image): |
| try: |
| image = Image.open(image).convert("RGBA") |
| except Exception: |
| return None, "画像の読み込みに失敗しました。" |
| rgba = image.convert("RGBA") |
| alpha = rgba.getchannel("A") |
| padded = Image.new("RGBA", (rgba.width + border_px * 2, rgba.height + border_px * 2), (0, 0, 0, 0)) |
| padded.paste(rgba, (border_px, border_px), rgba) |
| mask = Image.new("L", padded.size, 0) |
| mask.paste(alpha, (border_px, border_px)) |
| expanded = mask |
| for _ in range(border_px): |
| expanded = expanded.filter(ImageFilter.MaxFilter(3)) if False else expanded |
| border = Image.new("RGBA", padded.size, (255, 255, 255, 255)) |
| border.putalpha(expanded) |
| out = Image.alpha_composite(border, padded) |
| return out, "白フチを追加しました。" |
|
|
|
|
|
|
| def save_base_character(image, profile_json): |
| if image is None: |
| return "画像がありません。", gr.update(choices=list_saved_characters()) |
| if not isinstance(image, Image.Image): |
| try: |
| image = Image.open(image).convert("RGB") |
| except Exception: |
| return "画像の読み込みに失敗しました。", gr.update(choices=list_saved_characters()) |
|
|
| try: |
| meta = json.loads(profile_json) if profile_json.strip() else {} |
| except Exception: |
| meta = {"char_name": "untitled"} |
| name = safe_name(meta.get("char_name", "untitled")) |
| stamp = datetime.now().strftime("%Y%m%d_%H%M%S") |
| folder = os.path.join(CHAR_DIR, f"{name}_{stamp}") |
| os.makedirs(folder, exist_ok=True) |
| image.save(os.path.join(folder, "base.png")) |
| with open(os.path.join(folder, "profile.json"), "w", encoding="utf-8") as f: |
| json.dump(meta, f, ensure_ascii=False, indent=2) |
| return f"保存しました: {folder}", gr.update(choices=list_saved_characters(), value=os.path.basename(folder)) |
|
|
|
|
|
|
| def list_saved_characters(): |
| if not os.path.exists(CHAR_DIR): |
| return [] |
| items = [d for d in os.listdir(CHAR_DIR) if os.path.isdir(os.path.join(CHAR_DIR, d))] |
| items.sort(reverse=True) |
| return items |
|
|
|
|
|
|
| def load_saved_character(name): |
| if not name: |
| return None, "", "選択されていません。" |
| folder = os.path.join(CHAR_DIR, name) |
| img_path = os.path.join(folder, "base.png") |
| profile_path = os.path.join(folder, "profile.json") |
| img = Image.open(img_path).convert("RGB") if os.path.exists(img_path) else None |
| text = "" |
| if os.path.exists(profile_path): |
| with open(profile_path, "r", encoding="utf-8") as f: |
| text = f.read() |
| return img, text, f"読み込みました: {name}" |
|
|
|
|
|
|
| def export_zip(): |
| stamp = datetime.now().strftime("%Y%m%d_%H%M%S") |
| zip_path = os.path.join(EXPORT_DIR, f"image_hub_export_{stamp}.zip") |
| with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf: |
| for root, _, files in os.walk(DATA_DIR): |
| for fn in files: |
| full = os.path.join(root, fn) |
| if full == zip_path: |
| continue |
| zf.write(full, arcname=os.path.relpath(full, DATA_DIR)) |
| return zip_path, f"ZIPを書き出しました: {zip_path}" |
|
|
|
|
| with gr.Blocks(title=APP_TITLE) as demo: |
| gr.Markdown(f"# {APP_TITLE}\n\n{APP_DESC}") |
| with gr.Accordion("診断パネル", open=False): |
| runtime_box = gr.Textbox(label="ランタイム状態", lines=4, value=runtime_status_text()) |
|
|
| with gr.Tabs(): |
| with gr.Tab("1. 新規キャラ生成"): |
| with gr.Row(): |
| with gr.Column(scale=5): |
| char_name = gr.Textbox(label="キャラ名", value="Beluga Mochi") |
| purpose = gr.Dropdown(label="用途プリセット", choices=list(PURPOSE_PRESETS.keys()), value="ステッカー向け") |
| model_name = gr.Dropdown(label="モデル", choices=MODEL_CHOICES, value=TEXT2IMG_MODEL if TEXT2IMG_MODEL in MODEL_CHOICES else MODEL_CHOICES[0], allow_custom_value=True) |
| size_name = gr.Dropdown(label="サイズ", choices=list(SIZE_PRESETS.keys()), value="1024x1024") |
| category = gr.Dropdown(label="ジャンル", choices=list(CATEGORY_MAP.keys()), value="海の生き物") |
| motif = gr.Dropdown(label="モチーフ", choices=motif_choices("海の生き物"), value="シロイルカ") |
| motif_custom = gr.Textbox(label="モチーフ自由入力", visible=False) |
| personality = gr.Dropdown(label="性格", choices=PERSONALITY_CHOICES, value="無気力") |
| personality_custom = gr.Textbox(label="性格自由入力", visible=False) |
| palette = gr.Dropdown(label="配色", choices=PALETTE_CHOICES, value="白系") |
| palette_custom = gr.Textbox(label="配色自由入力", visible=False) |
| style = gr.Dropdown(label="絵柄", choices=STYLE_CHOICES, value="ステッカー向け") |
| style_custom = gr.Textbox(label="絵柄自由入力", visible=False) |
| expression = gr.Dropdown(label="表情", choices=EXPRESSION_CHOICES, value="半目") |
| expression_custom = gr.Textbox(label="表情自由入力", visible=False) |
| pose = gr.Dropdown(label="ポーズ", choices=POSE_CHOICES, value="座る") |
| pose_custom = gr.Textbox(label="ポーズ自由入力", visible=False) |
| count = gr.Slider(label="候補数", minimum=1, maximum=8, step=1, value=4) |
| detail_text = gr.Textbox(label="詳細こだわり(任意)", lines=3, value=PURPOSE_PRESETS["ステッカー向け"]["detail"]) |
| btn_generate = gr.Button("候補を生成", variant="primary") |
| with gr.Column(scale=5): |
| gallery = gr.Gallery(label="生成結果", columns=2, height=520) |
| profile_json = gr.Code(label="profile.json", language="json") |
| base_image = gr.Image(label="ベースとして保存する画像", type="pil") |
| btn_save = gr.Button("この画像をベース保存") |
| save_result = gr.Textbox(label="保存結果") |
| diag_generate = gr.Textbox(label="診断ログ", lines=12) |
|
|
| category.change(fn=lambda c: gr.update(choices=motif_choices(c), value=motif_choices(c)[0]), inputs=category, outputs=motif) |
| purpose.change(fn=apply_purpose_preset, inputs=purpose, outputs=[size_name, style, detail_text]) |
| motif.change(fn=visible_if_custom, inputs=motif, outputs=motif_custom) |
| personality.change(fn=visible_if_custom, inputs=personality, outputs=personality_custom) |
| palette.change(fn=visible_if_custom, inputs=palette, outputs=palette_custom) |
| style.change(fn=visible_if_custom, inputs=style, outputs=style_custom) |
| expression.change(fn=visible_if_custom, inputs=expression, outputs=expression_custom) |
| pose.change(fn=visible_if_custom, inputs=pose, outputs=pose_custom) |
|
|
| saved_characters = gr.Dropdown(label="保存済みキャラ", choices=list_saved_characters()) |
| btn_load = gr.Button("選択したキャラを読み込む") |
| load_status = gr.Textbox(label="読み込み状態") |
|
|
| btn_generate.click( |
| fn=new_character_generate, |
| inputs=[char_name, purpose, model_name, size_name, category, motif, motif_custom, personality, personality_custom, |
| palette, palette_custom, style, style_custom, expression, expression_custom, |
| pose, pose_custom, count, detail_text], |
| outputs=[gallery, profile_json, base_image, diag_generate], |
| ) |
| btn_save.click(fn=save_base_character, inputs=[base_image, profile_json], outputs=[save_result, saved_characters]) |
| btn_load.click(fn=load_saved_character, inputs=saved_characters, outputs=[base_image, profile_json, load_status]) |
|
|
| with gr.Tab("2. 同一キャラ派生"): |
| variant_base = gr.Image(label="ベース画像", type="pil") |
| variant_request = gr.Textbox(label="変えたい内容", lines=3, value="sleepy expression, slightly different pose, no text") |
| variant_model = gr.Dropdown(label="モデル", choices=MODEL_CHOICES, value=TEXT2IMG_MODEL if TEXT2IMG_MODEL in MODEL_CHOICES else MODEL_CHOICES[0], allow_custom_value=True) |
| variant_count = gr.Slider(label="枚数", minimum=1, maximum=8, step=1, value=4) |
| variant_btn = gr.Button("派生を生成", variant="primary") |
| variant_gallery = gr.Gallery(label="派生結果", columns=2, height=520) |
| variant_diag = gr.Textbox(label="診断ログ", lines=12) |
| variant_btn.click(fn=variant_generate, inputs=[variant_base, variant_request, variant_count, variant_model], outputs=[variant_gallery, variant_diag]) |
|
|
| with gr.Tab("3. 背景 / MV素材"): |
| bg_world = gr.Textbox(label="世界観", value="幻想的な月夜の海") |
| bg_time = gr.Dropdown(label="時間帯", choices=["朝", "昼", "夕方", "夜"], value="夜") |
| bg_tone = gr.Dropdown(label="色調", choices=["青系", "エメラルド系", "暖色", "モノクロ", "紫系"], value="青系") |
| bg_ratio = gr.Dropdown(label="比率", choices=list(SIZE_PRESETS.keys()), value="1344x768") |
| bg_model = gr.Dropdown(label="モデル", choices=MODEL_CHOICES, value=TEXT2IMG_MODEL if TEXT2IMG_MODEL in MODEL_CHOICES else MODEL_CHOICES[0], allow_custom_value=True) |
| bg_detail = gr.Textbox(label="詳細", lines=3, value="cinematic, atmospheric, no text, no logo") |
| bg_count = gr.Slider(label="候補数", minimum=1, maximum=8, step=1, value=4) |
| bg_btn = gr.Button("背景を生成", variant="primary") |
| bg_gallery = gr.Gallery(label="背景結果", columns=2, height=520) |
| bg_diag = gr.Textbox(label="診断ログ", lines=12) |
| bg_btn.click(fn=background_generate, inputs=[bg_world, bg_time, bg_tone, bg_ratio, bg_detail, bg_count, bg_model], outputs=[bg_gallery, bg_diag]) |
|
|
| with gr.Tab("4. 商品画像"): |
| prod_subject = gr.Textbox(label="主題", value="ゆるいキャラクターステッカー") |
| prod_type = gr.Dropdown(label="商品タイプ", choices=["ステッカー", "Tシャツ", "マグカップ", "アクリルキーホルダー"], value="ステッカー") |
| prod_mood = gr.Dropdown(label="見せ方", choices=["シンプル", "おしゃれ", "物撮り風", "告知風"], value="シンプル") |
| prod_ratio = gr.Dropdown(label="比率", choices=list(SIZE_PRESETS.keys()), value="1536x1024") |
| prod_model = gr.Dropdown(label="モデル", choices=MODEL_CHOICES, value=TEXT2IMG_MODEL if TEXT2IMG_MODEL in MODEL_CHOICES else MODEL_CHOICES[0], allow_custom_value=True) |
| prod_detail = gr.Textbox(label="詳細", lines=3, value="clean product style, bright background, no text, no logo") |
| prod_count = gr.Slider(label="候補数", minimum=1, maximum=8, step=1, value=4) |
| prod_btn = gr.Button("商品画像を生成", variant="primary") |
| prod_gallery = gr.Gallery(label="商品画像結果", columns=2, height=520) |
| prod_diag = gr.Textbox(label="診断ログ", lines=12) |
| prod_btn.click(fn=product_generate, inputs=[prod_subject, prod_type, prod_mood, prod_ratio, prod_detail, prod_count, prod_model], outputs=[prod_gallery, prod_diag]) |
|
|
| with gr.Tab("5. ステッカー化補助"): |
| gr.Markdown("白フチ追加は簡易版です。") |
| sticker_input = gr.Image(label="入力画像", type="pil") |
| sticker_btn = gr.Button("プレビュー用にそのまま表示") |
| sticker_output = gr.Image(label="出力画像", type="pil") |
| sticker_status = gr.Textbox(label="状態") |
| sticker_btn.click(fn=lambda img: (img, "画像をそのまま表示しました。必要なら後で白フチ処理を追加します。"), inputs=sticker_input, outputs=[sticker_output, sticker_status]) |
|
|
| with gr.Tab("6. 書き出し"): |
| export_btn = gr.Button("データをZIP書き出し", variant="primary") |
| export_file = gr.File(label="ZIPファイル") |
| export_status = gr.Textbox(label="状態") |
| export_btn.click(fn=export_zip, inputs=None, outputs=[export_file, export_status]) |
|
|
|
|
| demo.launch() |
|
|