| """ |
| ImageForge — Custom AI image generation wrapper |
| Tab 1: Text-to-Image with character presets (fully working) |
| Tabs 2-6: stubbed placeholders, built out in later passes |
| |
| Deploy target: Hugging Face Space, Gradio SDK, ZeroGPU hardware |
| """ |
|
|
| import gradio as gr |
| import spaces |
| import torch |
| import json |
| import os |
| import random |
| from datetime import datetime |
| from pathlib import Path |
|
|
| |
| |
| |
|
|
| PRESETS_FILE = "presets.json" |
| MODELS_FILE = "models.json" |
| OUTPUT_DIR = Path("generations") |
| OUTPUT_DIR.mkdir(exist_ok=True) |
|
|
| |
| |
| |
| |
| DEFAULT_MODEL_OPTIONS = { |
| "Z-Image Turbo (fast, photorealistic)": "Tongyi-MAI/Z-Image-Turbo", |
| "Qwen-Image (best prompt following + text)": "Qwen/Qwen-Image", |
| "Flux Schnell (fast, Apache 2.0)": "black-forest-labs/FLUX.1-schnell", |
| "Flux Krea (stronger realism)": "black-forest-labs/FLUX.1-Krea-dev", |
| } |
|
|
|
|
| def load_models(): |
| """Returns the combined model list: built-in defaults + anything |
| the user has added via the UI, persisted in models.json.""" |
| if os.path.exists(MODELS_FILE): |
| with open(MODELS_FILE, "r") as f: |
| custom = json.load(f) |
| else: |
| custom = {} |
| combined = dict(DEFAULT_MODEL_OPTIONS) |
| combined.update(custom) |
| return combined |
|
|
|
|
| def save_custom_models(custom): |
| with open(MODELS_FILE, "w") as f: |
| json.dump(custom, f, indent=2) |
|
|
|
|
| def add_model(display_name, model_id): |
| """Add a new model to the dropdown by pasting any Hugging Face model id, |
| e.g. 'THUDM/CogView4-6B' or a brand new release. No code edit needed. |
| NOTE: only diffusers-compatible text-to-image pipelines will work here — |
| if a new model needs custom loading code, it may need a small code update.""" |
| if not display_name.strip() or not model_id.strip(): |
| return gr.update(), "Both a display name and a model ID are required." |
| if os.path.exists(MODELS_FILE): |
| with open(MODELS_FILE, "r") as f: |
| custom = json.load(f) |
| else: |
| custom = {} |
| custom[display_name.strip()] = model_id.strip() |
| save_custom_models(custom) |
| return gr.update(choices=list(load_models().keys())), f"Added model '{display_name.strip()}'." |
|
|
|
|
| MODEL_OPTIONS = load_models() |
|
|
| |
| DEFAULT_NEGATIVE_PROMPT = ( |
| "blurry, distorted, deformed hands, extra limbs, extra fingers, " |
| "watermark, text artifacts, low quality, off-model, inconsistent style" |
| ) |
|
|
| |
| QUALITY_PRESETS = { |
| "Draft (fast)": {"steps": 8, "guidance": 2.5}, |
| "Final Quality (slower, better)": {"steps": 30, "guidance": 4.5}, |
| } |
|
|
| _loaded_pipelines = {} |
|
|
|
|
| |
| |
| |
|
|
| def load_presets(): |
| if os.path.exists(PRESETS_FILE): |
| with open(PRESETS_FILE, "r") as f: |
| return json.load(f) |
| return {} |
|
|
|
|
| def save_presets(presets): |
| with open(PRESETS_FILE, "w") as f: |
| json.dump(presets, f, indent=2) |
|
|
|
|
| def preset_names(): |
| return list(load_presets().keys()) |
|
|
|
|
| def add_or_update_preset(name, description, style): |
| if not name or not name.strip(): |
| return gr.update(), "Preset name can't be empty." |
| presets = load_presets() |
| presets[name.strip()] = { |
| "description": description.strip(), |
| "style": style.strip(), |
| } |
| save_presets(presets) |
| return gr.update(choices=preset_names(), value=name.strip()), f"Saved preset '{name.strip()}'." |
|
|
|
|
| def delete_preset(name): |
| presets = load_presets() |
| if name in presets: |
| del presets[name] |
| save_presets(presets) |
| return gr.update(choices=preset_names(), value=None), f"Deleted preset '{name}'." |
| return gr.update(), "Nothing to delete." |
|
|
|
|
| def load_preset_into_fields(name): |
| presets = load_presets() |
| if name and name in presets: |
| p = presets[name] |
| return p["description"], p["style"] |
| return "", "" |
|
|
|
|
| |
| |
| |
|
|
| def build_final_prompt(user_prompt, preset_name, style_override): |
| """Silently combine: user prompt + character preset + style, so the |
| person typing never has to re-type the character description every time.""" |
| parts = [user_prompt.strip()] |
|
|
| presets = load_presets() |
| if preset_name and preset_name in presets: |
| p = presets[preset_name] |
| if p.get("description"): |
| parts.append(p["description"]) |
| if p.get("style") and not style_override: |
| parts.append(p["style"]) |
|
|
| if style_override and style_override.strip(): |
| parts.append(style_override.strip()) |
|
|
| return ", ".join([p for p in parts if p]) |
|
|
|
|
| |
| |
| |
|
|
| def get_pipeline(model_key): |
| model_id = load_models()[model_key] |
| if model_id not in _loaded_pipelines: |
| from diffusers import DiffusionPipeline |
| pipe = DiffusionPipeline.from_pretrained( |
| model_id, |
| torch_dtype=torch.bfloat16, |
| ) |
| pipe = pipe.to("cuda") |
| _loaded_pipelines[model_id] = pipe |
| return _loaded_pipelines[model_id] |
|
|
|
|
| @spaces.GPU(duration=90) |
| def generate_images( |
| prompt, |
| negative_prompt, |
| model_key, |
| quality_key, |
| preset_name, |
| style_override, |
| seed, |
| num_variants, |
| ): |
| settings = QUALITY_PRESETS[quality_key] |
| final_prompt = build_final_prompt(prompt, preset_name, style_override) |
| neg_prompt = negative_prompt.strip() if negative_prompt.strip() else DEFAULT_NEGATIVE_PROMPT |
|
|
| pipe = get_pipeline(model_key) |
|
|
| |
| base_seed = random.randint(0, 2**31 - 1) if seed is None or seed < 0 else int(seed) |
|
|
| images = [] |
| used_seeds = [] |
| for i in range(int(num_variants)): |
| this_seed = base_seed + i |
| generator = torch.Generator(device="cuda").manual_seed(this_seed) |
| result = pipe( |
| prompt=final_prompt, |
| negative_prompt=neg_prompt, |
| num_inference_steps=settings["steps"], |
| guidance_scale=settings["guidance"], |
| generator=generator, |
| ) |
| img = result.images[0] |
| images.append(img) |
| used_seeds.append(this_seed) |
|
|
| |
| |
| char_tag = preset_name.replace(" ", "") if preset_name else "NoPreset" |
| timestamp = datetime.now().strftime("%Y%m%d-%H%M%S") |
| filename = f"{char_tag}_seed{this_seed}_{timestamp}.png" |
| img.save(OUTPUT_DIR / filename) |
|
|
| seed_report = ", ".join(str(s) for s in used_seeds) |
| status = f"Generated {len(images)} variant(s). Seeds used: {seed_report}\nPrompt sent: {final_prompt}" |
| return images, status |
|
|
|
|
| |
| |
| |
|
|
| with gr.Blocks(title="ImageForge") as demo: |
| gr.Markdown("# 🎨 ImageForge\nCustom AI image generation — built for the Bojo project, usable for any character/scene/asset work.") |
|
|
| with gr.Tabs(): |
| |
| |
| |
| with gr.Tab("1. Text to Image"): |
| with gr.Row(): |
| with gr.Column(scale=1): |
| prompt_box = gr.Textbox( |
| label="Describe the scene", |
| placeholder="Bojo standing in a sunlit meadow, looking surprised", |
| lines=3, |
| ) |
| model_dropdown = gr.Dropdown( |
| label="Model", |
| choices=list(MODEL_OPTIONS.keys()), |
| value=list(MODEL_OPTIONS.keys())[0], |
| ) |
| with gr.Accordion("Add a new model (any Hugging Face model ID)", open=False): |
| gr.Markdown( |
| "New open-source image models come out all the time — this list " |
| "isn't fixed. Paste any diffusers-compatible model ID from " |
| "huggingface.co to add it here permanently." |
| ) |
| new_model_name_box = gr.Textbox(label="Display name", placeholder="e.g. CogView4") |
| new_model_id_box = gr.Textbox( |
| label="Hugging Face model ID", placeholder="e.g. THUDM/CogView4-6B" |
| ) |
| add_model_btn = gr.Button("Add model") |
| add_model_status = gr.Textbox(label="Status", interactive=False) |
| quality_dropdown = gr.Dropdown( |
| label="Quality", |
| choices=list(QUALITY_PRESETS.keys()), |
| value="Draft (fast)", |
| ) |
|
|
| gr.Markdown("### Character preset") |
| preset_dropdown = gr.Dropdown( |
| label="Load a saved character", |
| choices=preset_names(), |
| value=None, |
| ) |
| style_override_box = gr.Textbox( |
| label="Style override (optional, replaces preset style just for this run)", |
| placeholder="e.g. watercolor, painterly, storybook illustration", |
| ) |
|
|
| with gr.Accordion("Advanced (seed, negative prompt, variants)", open=False): |
| seed_box = gr.Number(label="Seed (-1 = random)", value=-1, precision=0) |
| num_variants_box = gr.Slider( |
| label="Number of variants", minimum=1, maximum=4, step=1, value=2 |
| ) |
| negative_prompt_box = gr.Textbox( |
| label="Negative prompt (leave blank to use the default)", |
| placeholder=DEFAULT_NEGATIVE_PROMPT, |
| lines=2, |
| ) |
|
|
| generate_btn = gr.Button("Generate", variant="primary") |
|
|
| with gr.Column(scale=1): |
| gallery = gr.Gallery(label="Results", columns=2, height=500) |
| status_box = gr.Textbox(label="Status / prompt actually sent", lines=4, interactive=False) |
| gr.Markdown( |
| "All generations auto-save to this Space's storage as a backup. " |
| "Click any image in the gallery, then use the download icon to save it to your PC." |
| ) |
|
|
| generate_btn.click( |
| fn=generate_images, |
| inputs=[ |
| prompt_box, |
| negative_prompt_box, |
| model_dropdown, |
| quality_dropdown, |
| preset_dropdown, |
| style_override_box, |
| seed_box, |
| num_variants_box, |
| ], |
| outputs=[gallery, status_box], |
| ) |
|
|
| add_model_btn.click( |
| fn=add_model, |
| inputs=[new_model_name_box, new_model_id_box], |
| outputs=[model_dropdown, add_model_status], |
| ) |
|
|
| gr.Markdown("---") |
| gr.Markdown("### Manage character presets") |
| with gr.Row(): |
| with gr.Column(): |
| preset_name_box = gr.Textbox(label="Preset name", placeholder="Bojo") |
| preset_desc_box = gr.Textbox( |
| label="Character description", |
| placeholder="a grey donkey with a patched blue vest, big expressive eyes, floppy ears", |
| lines=3, |
| ) |
| preset_style_box = gr.Textbox( |
| label="Default style for this character", |
| placeholder="flat cartoon, thick outlines, warm color palette", |
| lines=2, |
| ) |
| with gr.Row(): |
| save_preset_btn = gr.Button("Save / Update preset") |
| delete_preset_btn = gr.Button("Delete selected preset") |
| preset_status_box = gr.Textbox(label="Preset status", interactive=False) |
|
|
| |
| preset_dropdown.change( |
| fn=load_preset_into_fields, |
| inputs=[preset_dropdown], |
| outputs=[preset_desc_box, preset_style_box], |
| ) |
| save_preset_btn.click( |
| fn=add_or_update_preset, |
| inputs=[preset_name_box, preset_desc_box, preset_style_box], |
| outputs=[preset_dropdown, preset_status_box], |
| ) |
| delete_preset_btn.click( |
| fn=delete_preset, |
| inputs=[preset_dropdown], |
| outputs=[preset_dropdown, preset_status_box], |
| ) |
|
|
| |
| |
| |
| with gr.Tab("2. Image to Image (coming next)"): |
| gr.Markdown("Reference-based editing with Qwen-Image-Edit / FLUX Kontext. Built after Tab 1 is confirmed working.") |
|
|
| with gr.Tab("3. Reference & Consistency (coming next)"): |
| gr.Markdown("Reference image lock + seed reuse + style presets, layered on top of Tab 1/2.") |
|
|
| with gr.Tab("4. Character Sheet Builder (coming next)"): |
| gr.Markdown("One reference run through multiple angle/pose presets in a batch.") |
|
|
| with gr.Tab("5. Storyboard (coming next)"): |
| gr.Markdown("Grid view of all scenes, click-to-regenerate, export as one board.") |
|
|
| with gr.Tab("6. Image Editor (coming next)"): |
| gr.Markdown("Crop, resize, mask-based inpainting on any saved generation.") |
|
|
| if __name__ == "__main__": |
| demo.queue() |
| demo.launch() |