"""ICTone Hugging Face Spaces demo optimized for ZeroGPU. Space setup: 1. Select ZeroGPU hardware in the Space settings. 2. Add a Space secret named HF_TOKEN. The token owner must have accepted the access conditions for black-forest-labs/FLUX.1-Fill-dev. 3. Keep inference.py in the same directory as this file. """ from __future__ import annotations import os import random import gradio as gr import numpy as np import spaces import torch from diffusers import FluxFillPipeline from PIL import Image from inference import ( DEFAULT_INSTANCE_PROMPT, apply_lut, estimate_lut, run_one, ) MAX_SEED = np.iinfo(np.int32).max FLUX_PATH = os.getenv( "FLUX_PATH", "black-forest-labs/FLUX.1-Fill-dev", ) LORA_PATH = os.getenv( "LORA_PATH", "ToneStyle/ICTone-Fill-LoRA", ) IMAGE_SIZE = int(os.getenv("IMAGE_SIZE", "512")) LUT_SIZE = 33 HF_TOKEN = os.getenv("HF_TOKEN") def load_pipeline() -> FluxFillPipeline: """Load FluxFill + ICTone LoRA once at Space startup. ZeroGPU recommends placing the model on CUDA at module scope. During Space startup this uses ZeroGPU's CUDA emulation; a real GPU is attached only while a @spaces.GPU function is running. """ print(f"[ICTone] Loading base model: {FLUX_PATH}") print(f"[ICTone] Loading LoRA: {LORA_PATH}") load_kwargs = { "torch_dtype": torch.bfloat16, } if HF_TOKEN: load_kwargs["token"] = HF_TOKEN pipe = FluxFillPipeline.from_pretrained( FLUX_PATH, **load_kwargs, ) pipe.load_lora_weights(LORA_PATH) # Required placement pattern for ZeroGPU. Do not lazy-load/move the model # inside infer(). pipe.to("cuda") print("[ICTone] Pipeline ready.") return pipe # Load once at module scope for efficient ZeroGPU model placement. pipe = load_pipeline() @spaces.GPU(duration=60) def infer( content: Image.Image, reference: Image.Image, seed: int, randomize_seed: bool, guidance_scale: float, num_inference_steps: int, progress=gr.Progress(track_tqdm=True), ): """Run ICTone and reconstruct the result at the original content resolution.""" if content is None or reference is None: raise gr.Error("Please upload both a content image and a reference image.") if randomize_seed: seed = random.randint(0, MAX_SEED) seed = int(seed) guidance_scale = float(guidance_scale) num_inference_steps = int(num_inference_steps) content_rgb = content.convert("RGB") reference_rgb = reference.convert("RGB") with torch.inference_mode(): pred, panel, _, _ = run_one( pipe, content_rgb, reference_rgb, size=IMAGE_SIZE, prompt=DEFAULT_INSTANCE_PROMPT, guidance_scale=guidance_scale, num_inference_steps=num_inference_steps, seed=seed, generator_device="cuda", ) # Lift the low-resolution Flux prediction back to the original content # resolution using ICTone's fitted 3D LUT. before = np.asarray(content_rgb) after = np.asarray( pred.resize(content_rgb.size, Image.Resampling.BILINEAR) ) before_flat = before.reshape(-1, 3) after_flat = after.reshape(-1, 3) # Bound LUT fitting cost for very large uploaded images. max_samples = 500_000 if len(before_flat) > max_samples: rng = np.random.default_rng(0) selected = rng.choice( len(before_flat), max_samples, replace=False, ) before_flat = before_flat[selected] after_flat = after_flat[selected] lut = estimate_lut( before_flat, after_flat, size=LUT_SIZE, device="cuda", ) output = Image.fromarray( apply_lut( before, lut, device="cuda", ) ) return output, panel, seed with gr.Blocks(title="ICTone ยท In-Context Tone Style Transfer") as demo: gr.Markdown( """ # ICTone **In-Context Tone Style Transfer** Upload a **content image** and a **reference image**. ICTone transfers the reference color, contrast, and photographic tone while preserving the content of the source image. The demo uses **FLUX.1-Fill-dev** with the **ICTone LoRA** and runs on Hugging Face **ZeroGPU**. A short queue may appear when shared GPUs are busy. """ ) with gr.Row(): content = gr.Image( label="Content image", type="pil", ) reference = gr.Image( label="Reference image", type="pil", ) with gr.Accordion("Generation settings", open=False): with gr.Row(): seed = gr.Number( label="Seed", value=666, precision=0, ) randomize_seed = gr.Checkbox( label="Randomize seed", value=False, ) with gr.Row(): guidance = gr.Slider( label="Guidance scale", minimum=1, maximum=100, value=50, step=1, ) steps = gr.Slider( label="Inference steps", minimum=1, maximum=28, value=4, step=1, ) run = gr.Button( "Transfer tone", variant="primary", ) with gr.Row(): output = gr.Image( label="Result", type="pil", ) preview = gr.Image( label="Content | Reference | Result", type="pil", ) used_seed = gr.Number( label="Used seed", precision=0, ) run.click( fn=infer, inputs=[ content, reference, seed, randomize_seed, guidance, steps, ], outputs=[ output, preview, used_seed, ], show_progress="full", ) gr.Markdown( """ **Models:** `black-forest-labs/FLUX.1-Fill-dev` + `ToneStyle/ICTone-Fill-LoRA` FLUX.1-Fill-dev is subject to the FLUX.1 [dev] license and access conditions. """ ) if __name__ == "__main__": demo.queue().launch( server_name="0.0.0.0", server_port=int(os.getenv("PORT", "7860")), )