Create app.py

This commit is contained in:
sigma
2025-12-14 19:27:06 +00:00
committed by system
parent 1ff7948389
commit b717e92ee3

340
app.py Normal file
View File

@@ -0,0 +1,340 @@
import gradio as gr
import os
import subprocess
import shutil
import json
import time
from pathlib import Path
import torch
import spaces
from diffusers import DiffusionPipeline
# ==========================================
# 1. SETUP & GLOBAL VARS
# ==========================================
DATASET_DIR = Path("./datasets")
OUTPUT_DIR = Path("./output")
DATASET_DIR.mkdir(exist_ok=True)
OUTPUT_DIR.mkdir(exist_ok=True)
# global tracking for loras
# key: friendly name, value: path
AVAILABLE_LORAS = {}
print("loading z-image-turbo pipeline...")
pipe = DiffusionPipeline.from_pretrained(
"Tongyi-MAI/Z-Image-Turbo",
torch_dtype=torch.bfloat16,
low_cpu_mem_usage=False,
)
pipe.to("cuda")
print("pipeline loaded!")
# ==========================================
# 2. TRAINING LOGIC
# ==========================================
def check_gpu():
if torch.cuda.is_available():
return f"✅ gpu available: {torch.cuda.get_device_name(0)}"
return "⚠️ no gpu detected"
def upload_and_prepare_dataset(files, dataset_name, trigger_word):
if not files:
return "❌ upload images first", None, ""
if not dataset_name:
dataset_name = f"dataset_{int(time.time())}"
dataset_path = DATASET_DIR / dataset_name
dataset_path.mkdir(exist_ok=True, parents=True)
image_count = 0
for file in files:
if file.name.lower().endswith(('.png', '.jpg', '.jpeg', '.webp', '.bmp')):
filename = Path(file.name).name
dest = dataset_path / filename
shutil.copy(file.name, dest)
caption_file = dest.with_suffix('.txt')
caption_text = trigger_word if trigger_word else "a photo"
with open(caption_file, 'w') as f:
f.write(caption_text)
image_count += 1
if image_count == 0:
return "❌ no valid images found", None, ""
return f"✅ ready: {image_count} images in {dataset_name}", str(dataset_path), dataset_name
# request 1 hour gpu for training
@spaces.GPU(duration=3600)
def train_lora(
dataset_path,
project_name,
trigger_word,
steps,
learning_rate,
lora_rank,
resolution,
progress=gr.Progress()
):
if not dataset_path:
return "❌ no dataset", None
if not project_name:
project_name = f"lora_{int(time.time())}"
output_path = OUTPUT_DIR / project_name
output_path.mkdir(exist_ok=True, parents=True)
# config generation
config = {
"job": "extension",
"config": {
"name": project_name,
"process": [{
"type": "sd_trainer",
"training_folder": str(output_path),
"device": "cuda:0",
"trigger_word": trigger_word or "",
"network": {
"type": "lora",
"linear": int(lora_rank),
"linear_alpha": int(lora_rank),
},
"save": {
"dtype": "float16",
"save_every": int(steps), # save only at end to save space
"max_step_saves_to_keep": 1,
},
"datasets": [{
"folder_path": dataset_path,
"caption_ext": "txt",
"caption_dropout_rate": 0.05,
"resolution": [int(resolution), int(resolution)],
}],
"train": {
"batch_size": 1,
"steps": int(steps),
"gradient_accumulation_steps": 1,
"train_unet": True,
"train_text_encoder": False,
"gradient_checkpointing": True,
"noise_scheduler": "flowmatch",
"optimizer": "adamw8bit",
"lr": float(learning_rate),
"ema_config": {"use_ema": True, "ema_decay": 0.99},
"dtype": "bf16",
},
"model": {
"name_or_path": "Tongyi-MAI/Z-Image-Base",
"is_v_pred": False,
"quantize": True,
},
}]
}
}
config_path = output_path / "config.json"
with open(config_path, 'w') as f:
json.dump(config, f, indent=2)
# install ai-toolkit
progress(0.1, desc="setting up environment...")
if not Path("./ai-toolkit").exists():
try:
subprocess.run(["git", "clone", "https://github.com/ostris/ai-toolkit.git"], check=True)
subprocess.run(["pip", "install", "-q", "-r", "ai-toolkit/requirements.txt"], check=True)
except Exception as e:
return f"❌ setup failed: {e}", None
progress(0.2, desc="training (this takes time)...")
try:
# run training script
# explicitly passing environment to ensure cuda visibility in subprocess
env = os.environ.copy()
proc = subprocess.run(
["python", "ai-toolkit/run.py", str(config_path)],
capture_output=True,
text=True,
env=env,
timeout=3500
)
if proc.returncode != 0:
return f"❌ training crashed:\n{proc.stderr}", None
# find result
lora_files = list(output_path.glob("*.safetensors"))
if lora_files:
lora_file = lora_files[-1]
AVAILABLE_LORAS[project_name] = str(lora_file)
# update the dropdown choices dynamically
choices = [("None", None)] + [(k, v) for k, v in AVAILABLE_LORAS.items()]
return f"✅ trained: {project_name}", str(lora_file)
return "⚠️ finished but no safetensors found", None
except Exception as e:
return f"❌ fatal error: {e}", None
# ==========================================
# 3. INFERENCE LOGIC
# ==========================================
@spaces.GPU
def generate_image(
prompt,
height,
width,
steps,
seed,
randomize_seed,
lora_path,
lora_scale
):
# handle lora loading/unloading
pipe.unload_lora_weights() # clean slate
if lora_path and os.path.exists(lora_path):
print(f"loading lora: {lora_path}")
try:
pipe.load_lora_weights(lora_path)
# manual scaling not always supported directly without fuse,
# but usually applied by default.
# for simplicitly we just load it.
except Exception as e:
print(f"lora load failed: {e}")
if randomize_seed:
seed = torch.randint(0, 2**32 - 1, (1,)).item()
generator = torch.Generator("cuda").manual_seed(int(seed))
image = pipe(
prompt=prompt,
height=int(height),
width=int(width),
num_inference_steps=int(steps),
guidance_scale=0.0,
generator=generator,
).images[0]
return image, seed
def update_lora_list():
"""helper to refresh dropdown"""
choices = [("None", None)] + [(k, v) for k, v in AVAILABLE_LORAS.items()]
return gr.Dropdown(choices=choices)
# ==========================================
# 4. UI CONSTRUCTION
# ==========================================
custom_theme = gr.themes.Soft(primary_hue="yellow", secondary_hue="slate")
with gr.Blocks(theme=custom_theme, title="Z-Image ZeroGPU Trainer") as demo:
gr.Markdown("# ⚡ Z-Image-Turbo: Train & Test")
with gr.Tabs():
# TAB 1: INFERENCE
with gr.Tab("🎨 Generate"):
with gr.Row():
with gr.Column():
prompt_input = gr.Textbox(label="Prompt", lines=3)
with gr.Row():
lora_selector = gr.Dropdown(
label="Select LoRA",
choices=[("None", None)],
value=None,
interactive=True
)
refresh_btn = gr.Button("🔄", size="sm", scale=0)
with gr.Accordion("Settings", open=False):
h_slider = gr.Slider(512, 2048, 1024, step=64, label="Height")
w_slider = gr.Slider(512, 2048, 1024, step=64, label="Width")
steps_slider = gr.Slider(1, 50, 9, step=1, label="Steps")
seed_num = gr.Number(42, label="Seed")
rand_seed = gr.Checkbox(True, label="Randomize Seed")
gen_btn = gr.Button("Generate", variant="primary")
with gr.Column():
out_img = gr.Image(label="Result")
out_seed = gr.Number(label="Seed Used")
# TAB 2: TRAINING
with gr.Tab("🏋️ Train LoRA"):
gr.Markdown("⚠️ **Note:** Requires paid GPU space for long timeouts.")
with gr.Row():
with gr.Column():
train_files = gr.Files(label="Images", file_types=["image"])
train_name = gr.Textbox(label="Project Name", value="my_lora")
train_trigger = gr.Textbox(label="Trigger Word", value="ohwx")
# hidden state for dataset path
dataset_path_state = gr.State()
upload_btn = gr.Button("1. Process Dataset")
upload_status = gr.Textbox(label="Dataset Status")
gr.Markdown("---")
train_steps = gr.Slider(100, 2000, 500, step=100, label="Steps")
train_lr = gr.Slider(1e-5, 1e-3, 1e-4, step=1e-5, label="Learning Rate")
train_rank = gr.Slider(4, 128, 16, step=4, label="Rank")
start_train_btn = gr.Button("2. Start Training", variant="stop")
with gr.Column():
train_log = gr.Textbox(label="Training Log", lines=10)
lora_file_download = gr.File(label="Download LoRA")
# WIRING
# Refresh LoRA list
refresh_btn.click(update_lora_list, outputs=lora_selector)
# Upload
upload_btn.click(
upload_and_prepare_dataset,
[train_files, train_name, train_trigger],
[upload_status, dataset_path_state, train_name]
)
# Train
def on_train_complete(status, file_path):
# Update available loras list immediately after training
new_choices = [("None", None)] + [(k, v) for k, v in AVAILABLE_LORAS.items()]
return status, file_path, gr.Dropdown(choices=new_choices)
start_train_btn.click(
train_lora,
[dataset_path_state, train_name, train_trigger, train_steps, train_lr, train_rank, h_slider], # reusing h_slider for res
[train_log, lora_file_download]
).then(
update_lora_list,
outputs=[lora_selector]
)
# Generate
gen_btn.click(
generate_image,
[prompt_input, h_slider, w_slider, steps_slider, seed_num, rand_seed, lora_selector, train_lr], # train_lr dummy
[out_img, out_seed]
)
if __name__ == "__main__":
demo.launch()