File size: 7,583 Bytes
7a3c654
2e5d29d
8310116
e56bc06
f395223
 
e56bc06
5addfff
8310116
 
 
 
7a3c654
2e5d29d
8310116
a796ca0
8310116
 
 
2e5d29d
 
 
8310116
7a3c654
 
 
2e5d29d
 
 
 
 
7a3c654
2e5d29d
 
0afc625
2e5d29d
 
7a3c654
0afc625
e56bc06
 
 
 
 
 
 
 
 
 
 
 
2e5d29d
7a3c654
2e5d29d
 
d9738c1
0afc625
d638c30
0afc625
2e5d29d
8310116
a32b4dd
2e5d29d
e56bc06
2e5d29d
 
e56bc06
7a3c654
2e5d29d
8310116
 
2e5d29d
8310116
 
 
 
e56bc06
8310116
2e5d29d
8310116
2e5d29d
 
8310116
5080fa8
2e5d29d
5080fa8
 
 
 
 
 
2e5d29d
 
 
 
 
 
5080fa8
e56bc06
2e5d29d
 
 
 
 
 
8310116
5addfff
41a18bb
8310116
d9738c1
2e5d29d
 
 
 
 
 
7a3c654
2e5d29d
d9738c1
2e5d29d
 
 
3aded6c
2e5d29d
 
e56bc06
2e5d29d
 
 
8310116
d638c30
2e5d29d
41a18bb
2e5d29d
 
 
 
 
5080fa8
2e5d29d
a796ca0
2e5d29d
41a18bb
2e5d29d
 
 
e56bc06
 
2e5d29d
 
e56bc06
2e5d29d
 
 
 
77d07ec
2e5d29d
 
 
 
 
 
a762e0c
a796ca0
2e5d29d
 
 
 
 
e56bc06
 
77d07ec
7a3c654
 
e56bc06
77d07ec
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
import sys, os
sys.path.append("../")

# ↓↓↓ ajoute ceci tout de suite après
os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")


import spaces
import torch
import random
import numpy as np
from PIL import Image
import gradio as gr

from huggingface_hub import hf_hub_download
from transformers import AutoModelForImageSegmentation
from torchvision import transforms
from pipeline import InstantCharacterFluxPipeline

# --------------------------------------------
# Global & Model paths
# --------------------------------------------
MAX_SEED = np.iinfo(np.int32).max
device = "cuda" if torch.cuda.is_available() else torch.device("cpu")
dtype = torch.float16 if "cuda" in str(device) else torch.float32

ip_adapter_path = hf_hub_download("tencent/InstantCharacter", "instantcharacter_ip-adapter.bin")
base_model = "black-forest-labs/FLUX.1-dev"
image_encoder_path = "google/siglip-so400m-patch14-384"
image_encoder_2_path = "facebook/dinov2-giant"
birefnet_path = "ZhengPeng7/BiRefNet"

makoto_style_lora_path = hf_hub_download("InstantX/FLUX.1-dev-LoRA-Makoto-Shinkai", "Makoto_Shinkai_style.safetensors")
ghibli_style_lora_path = hf_hub_download("InstantX/FLUX.1-dev-LoRA-Ghibli", "ghibli_style.safetensors")

# local One Piece LoRA
onepiece_style_lora_path = os.path.join(os.path.dirname(__file__), "onepiece_flux_v2.safetensors")
ONEPIECE_TRIGGER = "onepiece style"

# ---- Universal prompt (homme ou femme)
UNIVERSAL_PROMPT = (
    "Upper-body anime portrait of a pirate character inspired by One Piece, confident and charismatic expression, "
    "original and dynamic pose, expressive eyes with anime-style lighting, slightly windswept hair, preserving the subject’s "
    "distinctive facial features and hairstyle (and facial hair if present), detailed anime rendering of the face, natural matte skin tone, "
    "lips matching the skin color (no pink or gloss), wearing stylish pirate clothing appropriate to the subject (open shirt, coat, vest, "
    "belts, scarves, cape, etc...), with optional pirate accessories (earrings, necklace, bandana or hat) only if they fit the subject’s style, "
    "well-framed head and shoulders, centered and balanced, cinematic warm lighting, high-quality cel-shaded coloring and clean linework, "
    "One Piece-style background (ship deck or ocean sky), designed to look cool, original and iconic like a real One Piece portrait character, "
    "no frame, no text."
)

# --------------------------------------------
# Init pipeline
# --------------------------------------------
pipe = InstantCharacterFluxPipeline.from_pretrained(base_model, torch_dtype=torch.bfloat16)
pipe.to(device)
pipe.init_adapter(
    image_encoder_path=image_encoder_path,
    image_encoder_2_path=image_encoder_2_path,
    subject_ipadapter_cfg=dict(subject_ip_adapter_path=ip_adapter_path, nb_token=1024),
)

# --------------------------------------------
# Background remover
# --------------------------------------------
birefnet = AutoModelForImageSegmentation.from_pretrained(birefnet_path, trust_remote_code=True)
birefnet.to(device)
birefnet.eval()
birefnet_transform = transforms.Compose([
    transforms.Resize((1024, 1024)),
    transforms.ToTensor(),
    transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),
])

def remove_bkg(subject_image):
    def infer_matting(img_pil):
        inp = birefnet_transform(img_pil).unsqueeze(0).to(device)
        with torch.no_grad():
            preds = birefnet(inp)[-1].sigmoid().cpu()
        pred = preds[0].squeeze()
        mask = transforms.ToPILImage()(pred).resize(img_pil.size)
        return np.array(mask)[..., None]

    def pad_to_square(image, pad_value=255):
        H, W = image.shape[:2]
        if H == W:
            return image
        pad = abs(H - W)
        pad1, pad2 = pad // 2, pad - pad // 2
        pad_param = ((0, 0), (pad1, pad2), (0, 0)) if H > W else ((pad1, pad2), (0, 0), (0, 0))
        return np.pad(image, pad_param, "constant", constant_values=pad_value)

    mask = infer_matting(subject_image)[..., 0]
    subject_np = np.array(subject_image)
    mask = (mask > 128).astype(np.uint8) * 255
    sample_mask = np.stack([mask] * 3, axis=-1)
    obj = sample_mask / 255 * subject_np + (1 - sample_mask / 255) * 255
    cropped = pad_to_square(obj, 255)
    return Image.fromarray(cropped.astype(np.uint8))

# --------------------------------------------
# Generation logic
# --------------------------------------------
def randomize_seed(seed, randomize):
    return random.randint(0, MAX_SEED) if randomize else seed

@spaces.GPU
def create_image(input_image, prompt, scale, guidance_scale, num_inference_steps, seed, style_mode, negative_prompt=""):
    input_image = remove_bkg(input_image)

    if style_mode == "Makoto Shinkai style":
        lora_path, trigger = makoto_style_lora_path, "Makoto Shinkai style"
    elif style_mode == "Ghibli style":
        lora_path, trigger = ghibli_style_lora_path, "ghibli style"
    elif style_mode == "One Piece style":
        lora_path, trigger = onepiece_style_lora_path, ONEPIECE_TRIGGER
    else:
        lora_path, trigger = None, ""

    generator = torch.manual_seed(seed)
    common_args = dict(
        prompt=prompt,
        negative_prompt=negative_prompt,
        num_inference_steps=num_inference_steps,
        guidance_scale=guidance_scale,
        width=1024, height=768,   # <<< sortie fixe 1024 x 768
        subject_image=input_image,
        subject_scale=scale,
        generator=generator,
    )

    if lora_path:
        result = pipe.with_style_lora(lora_file_path=lora_path, trigger=trigger, **common_args)
    else:
        result = pipe(**common_args)
    return result.images

# --------------------------------------------
# UI definition (Gradio 5)
# --------------------------------------------
def generate_fn(image, prompt, scale, style, guidance, steps, seed, randomize, negative_prompt):
    seed = randomize_seed(seed, randomize)
    return create_image(image, prompt, scale, guidance, steps, seed, style, negative_prompt)

title = "🎨 InstantCharacter + One Piece LoRA"
description = (
    "Upload your photo, use the universal One Piece prompt, choose **One Piece style**. "
    "Output is fixed to **1024×780**. API is enabled for Make.com."
)

# (ne PAS mettre api_open ici)
demo = gr.Interface(
    fn=generate_fn,
    inputs=[
        gr.Image(label="Source Image", type="pil"),
        gr.Textbox(label="Prompt", value=f"a character is riding a bike in snow, {ONEPIECE_TRIGGER}"),
        gr.Slider(0, 1.5, value=1.0, step=0.01, label="Scale"),
        gr.Dropdown(choices=[None, "Makoto Shinkai style", "Ghibli style", "One Piece style"],
                    value="One Piece style", label="Style"),
        gr.Slider(1, 7.0, value=3.5, step=0.01, label="Guidance Scale"),
        gr.Slider(5, 50, value=28, step=1, label="Inference Steps"),
        gr.Slider(-1000000, 1000000, value=123456, step=1, label="Seed"),
        gr.Checkbox(value=True, label="Randomize Seed"),
        gr.Textbox(label="Negative Prompt", placeholder="e.g. photorealistic, realistic skin, pores, hdr")
    ],
    outputs=gr.Gallery(label="Generated Image"),
    title=title,
    description=description,
    examples=[
        ["./assets/girl.jpg", f"A girl playing guitar, {ONEPIECE_TRIGGER}", 0.9, "One Piece style", 3.5, 28, 123, False, ""],
        ["./assets/boy.jpg",  f"A boy riding a bike, {ONEPIECE_TRIGGER}", 0.9, "One Piece style", 3.5, 28, 123, False, ""]
    ]
)

# ⇩⇩⇩ utiliser show_api=True ici
demo.launch(show_api=True)