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()