Nad54 commited on
Commit
22a4eb5
·
verified ·
1 Parent(s): 089483f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +112 -57
app.py CHANGED
@@ -1,4 +1,5 @@
1
- # app.py — InstantID SDXL (stable) : ControlNet objet unique, téléchargements sûrs, InsightFace fallback
 
2
 
3
  # 0) Environnement AVANT TOUT IMPORT
4
  import os, sys
@@ -19,15 +20,21 @@ DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
19
  DTYPE = torch.float16 if DEVICE == "cuda" else torch.float32
20
 
21
  # 2) Chemins & Hub
22
- ASSETS_REPO = "InstantX/InstantID" # repo Model public qui contient les poids
23
  CHECKPOINTS_DIR = "./checkpoints"
24
  CN_LOCAL_DIR = os.path.join(CHECKPOINTS_DIR, "ControlNetModel")
25
  IP_ADAPTER_LOCAL = os.path.join(CHECKPOINTS_DIR, "ip-adapter.bin")
26
 
 
 
 
 
 
 
27
  # 3) Téléchargements sûrs (détecte fichiers vides)
28
- def safe_download(repo, filename, local_dir, min_bytes, label):
29
  os.makedirs(local_dir, exist_ok=True)
30
- local_path = os.path.join(local_dir, filename)
31
  if os.path.exists(local_path) and os.path.getsize(local_path) < min_bytes:
32
  print(f"⚠️ {label} corrompu ({os.path.getsize(local_path)} bytes) → suppression")
33
  try: os.remove(local_path)
@@ -36,8 +43,10 @@ def safe_download(repo, filename, local_dir, min_bytes, label):
36
  repo_id=repo,
37
  filename=filename,
38
  local_dir=local_dir,
 
39
  resume_download=True,
40
  force_download=not os.path.exists(local_path),
 
41
  )
42
  size = os.path.getsize(path)
43
  print(f"✅ {label} téléchargé ({size/1e6:.1f} MB)")
@@ -48,9 +57,18 @@ def safe_download(repo, filename, local_dir, min_bytes, label):
48
  def ensure_assets_or_download():
49
  os.makedirs(CHECKPOINTS_DIR, exist_ok=True)
50
  os.makedirs(CN_LOCAL_DIR, exist_ok=True)
 
51
  safe_download(ASSETS_REPO, "ControlNetModel/config.json", CHECKPOINTS_DIR, 1_000, "IdentityNet config")
52
  safe_download(ASSETS_REPO, "ControlNetModel/diffusion_pytorch_model.safetensors", CHECKPOINTS_DIR, 100_000_000, "IdentityNet weights")
53
- safe_download(ASSETS_REPO, "ip-adapter.bin", CHECKPOINTS_DIR, 100_000_000, "ip-adapter")
 
 
 
 
 
 
 
 
54
 
55
  # 4) Import dynamique de la pipeline InstantID (fichier texte local)
56
  def import_pipeline_or_fail():
@@ -96,16 +114,34 @@ try:
96
 
97
  controlnet_identitynet = ControlNetModel.from_pretrained(CN_LOCAL_DIR, torch_dtype=DTYPE)
98
 
99
- # 🔧 Ici: controlnet=controlnet_identitynet (objet unique)
100
  pipe = SDXLInstantID.from_pretrained(
101
  BASE_MODEL,
102
- controlnet=controlnet_identitynet,
103
  torch_dtype=DTYPE,
104
  safety_checker=None,
105
  feature_extractor=None,
106
  ).to(DEVICE)
107
 
 
 
108
  pipe.load_ip_adapter_instantid(IP_ADAPTER_LOCAL)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
  if DEVICE == "cuda":
110
  if hasattr(pipe, "image_proj_model"): pipe.image_proj_model.to("cuda")
111
  if hasattr(pipe, "unet"): pipe.unet.to("cuda")
@@ -114,6 +150,7 @@ try:
114
  except Exception:
115
  load_logs += ["❌ ERREUR au chargement:", traceback.format_exc()]
116
  pipe = None
 
117
 
118
  if pipe is None:
119
  raise RuntimeError("Échec de chargement du pipeline.\n" + "\n".join(load_logs))
@@ -124,7 +161,6 @@ def load_face_analyser():
124
  errors = []
125
  for name in ("antelopev2", "buffalo_l"):
126
  try:
127
- # cache sous ./models pour éviter les permissions
128
  fa = FaceAnalysis(name=name, root="./models", providers=["CPUExecutionProvider"])
129
  fa.prepare(ctx_id=0, det_size=(640, 640))
130
  print(f"✅ InsightFace chargé: {name}")
@@ -143,106 +179,115 @@ def extract_kps_image(pil_img):
143
  if not faces:
144
  raise ValueError("Aucun visage détecté dans la photo.")
145
  face = faces[-1]
146
- # faces[-1]['kps'] → 5 points (yeux G/D, nez, bouche G/D)
147
  return draw_kps_local(pil_img, face["kps"])
148
 
149
- # --- util: normaliser image_embeds quelle que soit la forme renvoyée (tensor / tuple / dict / list)
150
  def _normalize_image_embeds(image_embeds):
151
  import numpy as np
152
- # dict
153
  if isinstance(image_embeds, dict):
154
  for k in ("image_embeds", "prompt_image_embeds", "pooled_prompt_embeds"):
155
  if k in image_embeds and image_embeds[k] is not None:
156
- image_embeds = image_embeds[k]
157
- break
158
- # tuple/list
159
  if isinstance(image_embeds, (tuple, list)):
160
- # heuristique: certains renvoient (image_embeds, pooled) ou l’inverse
161
- if len(image_embeds) == 0:
162
- return None
163
  image_embeds = image_embeds[0] if image_embeds[0] is not None else image_embeds[-1]
164
- # numpy -> torch
165
  if isinstance(image_embeds, np.ndarray):
166
  image_embeds = torch.from_numpy(image_embeds)
167
- # vers bon device/dtype si tensor
168
  if isinstance(image_embeds, torch.Tensor):
169
  image_embeds = image_embeds.to(device=DEVICE, dtype=DTYPE if DEVICE == "cuda" else torch.float32)
170
  return image_embeds
171
 
172
  # 8) Génération
173
- def generate(face_image, prompt, negative_prompt, identity_strength, adapter_strength, steps, cfg, width, height, seed):
 
 
174
  try:
175
- # 0) garde-fous basiques
176
  if face_image is None:
177
  return None, "Merci d'ajouter une photo visage.", "\n".join(load_logs)
178
 
179
- # 1) seed
180
  gen = None if seed is None or int(seed) < 0 else torch.Generator(device=DEVICE).manual_seed(int(seed))
181
 
182
- # 2) préparer l'image visage (carré 512) + landmarks (kps)
183
  face = ImageOps.exif_transpose(face_image).convert("RGB")
184
- ms = min(face.size)
185
- x = (face.width - ms) // 2
186
- y = (face.height - ms) // 2
187
  face_sq = face.crop((x, y, x + ms, y + ms)).resize((512, 512), Image.Resampling.LANCZOS)
188
 
189
  kps_img = extract_kps_image(face_sq) # -> image landmarks (noir sur blanc)
190
 
191
- # 3) régler la force de l’IP-Adapter (style/détails)
192
- if hasattr(pipe, "set_ip_adapter_scale"):
193
- pipe.set_ip_adapter_scale(float(adapter_strength))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
 
195
- # 4) calculer les embeddings d'image pour InstantID (différentes API selon versions)
196
  image_embeds = None
197
  for m in ("get_image_embeds", "prepare_ip_adapter_image_embeds", "encode_image", "encode_ip_image"):
198
  fn = getattr(pipe, m, None)
199
  if callable(fn):
200
  try:
201
- image_embeds = fn(face_sq)
202
- break
203
  except Exception as e:
204
  print(f"⚠️ {m} a échoué: {e}")
205
-
206
  image_embeds = _normalize_image_embeds(image_embeds)
207
  if image_embeds is None:
208
  return None, (
209
  "La pipeline InstantID SDXL requiert des `image_embeds`, mais aucune méthode compatible n'a été trouvée "
210
  "(get_image_embeds / prepare_ip_adapter_image_embeds / encode_image / encode_ip_image). "
211
- "Vérifie que tu as bien collé la version SDXL officielle du fichier "
212
- "`pipeline_stable_diffusion_xl_instantid.py`."
213
  ), "\n".join(load_logs)
214
 
215
- # 5) compat multi-ControlNet : si la pipeline expose controlnet en liste, il faut des listes
216
  cn = getattr(pipe, "controlnet", None)
217
  if isinstance(cn, (list, tuple)):
218
  n_cn = len(cn)
219
  else:
220
- try:
221
- n_cn = len(cn)
222
- except Exception:
223
- n_cn = 1
224
 
225
  image_arg = [kps_img] * n_cn if n_cn > 1 else ([kps_img] if isinstance(cn, (list, tuple)) else kps_img)
226
  scale_val = float(identity_strength)
227
  scale_arg = [scale_val] * n_cn if n_cn > 1 else ([scale_val] if isinstance(cn, (list, tuple)) else scale_val)
228
 
229
- # 6) appel pipeline
230
- images = pipe(
231
  prompt=prompt.strip(),
232
  negative_prompt=(negative_prompt or "").strip(),
233
- image=image_arg, # liste si multi-controlnet
234
- image_embeds=image_embeds, # embeddings visage (obligatoire pour InstantID SDXL)
235
- controlnet_conditioning_scale=scale_arg, # liste si multi-controlnet
236
  num_inference_steps=int(steps),
237
  guidance_scale=float(cfg),
238
  width=int(width),
239
  height=int(height),
240
  generator=gen,
241
- ).images
 
 
 
 
 
 
242
 
 
 
243
  return images[0], "", "\n".join(load_logs)
244
 
245
- except torch.cuda.OutOfMemoryError as oom:
246
  return None, "CUDA OOM: baisse la résolution ou les steps.", "\n".join(load_logs)
247
  except Exception:
248
  return None, "Erreur:\n" + traceback.format_exc(), "\n".join(load_logs)
@@ -251,25 +296,33 @@ def generate(face_image, prompt, negative_prompt, identity_strength, adapter_str
251
  EX_PROMPT = (
252
  "one piece style, Eiichiro Oda style, anime portrait, upper body, pirate outfit, straw hat, "
253
  "clean lineart, cel shading, vibrant colors, expressive eyes, symmetrical face, looking at camera, "
254
- "dynamic lighting, simple background, high detail"
255
  )
256
  EX_NEG = (
 
257
  "low quality, worst quality, lowres, blurry, noisy, watermark, text, logo, jpeg artifacts, "
258
  "bad anatomy, distorted eyes, deformed, multiple faces, nsfw"
259
  )
260
 
261
  with gr.Blocks(css="footer{display:none !important}") as demo:
262
- gr.Markdown("# 🏴‍☠️ One Piece – InstantID SDXL (stable)")
263
 
264
  with gr.Row():
265
  with gr.Column():
266
- face_image = gr.Image(type="pil", label="Photo visage", height=360)
267
- prompt = gr.Textbox(label="Prompt", value=EX_PROMPT, lines=3)
268
- negative = gr.Textbox(label="Negative Prompt", value=EX_NEG, lines=3)
269
- identity_strength = gr.Slider(0.2, 1.5, 0.95, 0.05, label="Fidélité visage (IdentityNet)")
270
- adapter_strength = gr.Slider(0.2, 1.5, 0.85, 0.05, label="Détails anime (Adapter)")
 
 
 
 
 
 
 
271
  steps = gr.Slider(10, 60, 30, 1, label="Steps")
272
- cfg = gr.Slider(0.1, 12.0, 5.5, 0.1, label="CFG")
273
  width = gr.Dropdown(choices=[576, 640, 704, 768, 896], value=704, label="Largeur")
274
  height = gr.Dropdown(choices=[704, 768, 896, 1024], value=896, label="Hauteur")
275
  seed = gr.Number(value=-1, label="Seed (-1 aléatoire)")
@@ -278,7 +331,7 @@ with gr.Blocks(css="footer{display:none !important}") as demo:
278
  with gr.Column():
279
  out_image = gr.Image(label="Résultat", interactive=False)
280
  err_box = gr.Textbox(label="Erreurs", visible=False)
281
- log_box = gr.Textbox(label="Logs", value="\n".join(load_logs), lines=10)
282
 
283
  def wrap(*args):
284
  img, err, logs = generate(*args)
@@ -286,7 +339,9 @@ with gr.Blocks(css="footer{display:none !important}") as demo:
286
 
287
  btn.click(
288
  wrap,
289
- inputs=[face_image, prompt, negative, identity_strength, adapter_strength, steps, cfg, width, height, seed],
 
 
290
  outputs=[out_image, err_box, log_box],
291
  )
292
 
 
1
+ # app.py — InstantID SDXL + (optionnel) IP-Adapter Style pour rendu 2D
2
+ # HF Space ready
3
 
4
  # 0) Environnement AVANT TOUT IMPORT
5
  import os, sys
 
20
  DTYPE = torch.float16 if DEVICE == "cuda" else torch.float32
21
 
22
  # 2) Chemins & Hub
23
+ ASSETS_REPO = "InstantX/InstantID" # poids InstantID (ControlNet + ip-adapter instantid)
24
  CHECKPOINTS_DIR = "./checkpoints"
25
  CN_LOCAL_DIR = os.path.join(CHECKPOINTS_DIR, "ControlNetModel")
26
  IP_ADAPTER_LOCAL = os.path.join(CHECKPOINTS_DIR, "ip-adapter.bin")
27
 
28
+ # --- IP-Adapter Style (SDXL) : h94/IP-Adapter
29
+ IP_STYLE_REPO = "h94/IP-Adapter"
30
+ IP_STYLE_SUBFOLDER= "sdxl_models"
31
+ IP_STYLE_WEIGHT = "ip-adapter_sdxl.bin"
32
+ IP_STYLE_LOCAL = os.path.join(CHECKPOINTS_DIR, "ip-adapter_sdxl.bin") # copie locale (utile hors cache HF)
33
+
34
  # 3) Téléchargements sûrs (détecte fichiers vides)
35
+ def safe_download(repo, filename, local_dir, min_bytes, label, subfolder=None):
36
  os.makedirs(local_dir, exist_ok=True)
37
+ local_path = os.path.join(local_dir, filename if subfolder is None else os.path.basename(filename))
38
  if os.path.exists(local_path) and os.path.getsize(local_path) < min_bytes:
39
  print(f"⚠️ {label} corrompu ({os.path.getsize(local_path)} bytes) → suppression")
40
  try: os.remove(local_path)
 
43
  repo_id=repo,
44
  filename=filename,
45
  local_dir=local_dir,
46
+ local_dir_use_symlinks=False,
47
  resume_download=True,
48
  force_download=not os.path.exists(local_path),
49
+ subfolder=subfolder,
50
  )
51
  size = os.path.getsize(path)
52
  print(f"✅ {label} téléchargé ({size/1e6:.1f} MB)")
 
57
  def ensure_assets_or_download():
58
  os.makedirs(CHECKPOINTS_DIR, exist_ok=True)
59
  os.makedirs(CN_LOCAL_DIR, exist_ok=True)
60
+ # InstantID (IdentityNet + ip-adapter instantid)
61
  safe_download(ASSETS_REPO, "ControlNetModel/config.json", CHECKPOINTS_DIR, 1_000, "IdentityNet config")
62
  safe_download(ASSETS_REPO, "ControlNetModel/diffusion_pytorch_model.safetensors", CHECKPOINTS_DIR, 100_000_000, "IdentityNet weights")
63
+ safe_download(ASSETS_REPO, "ip-adapter.bin", CHECKPOINTS_DIR, 100_000_000, "ip-adapter (InstantID)")
64
+ # IP-Adapter Style (SDXL)
65
+ p = safe_download(IP_STYLE_REPO, IP_STYLE_WEIGHT, CHECKPOINTS_DIR, 20_000_000, "IP-Adapter Style (SDXL)", subfolder=IP_STYLE_SUBFOLDER)
66
+ # copieur simple pour avoir une voie locale directe si besoin
67
+ try:
68
+ if not os.path.exists(IP_STYLE_LOCAL):
69
+ import shutil; shutil.copy2(p, IP_STYLE_LOCAL)
70
+ except Exception as e:
71
+ print(f"ℹ️ Copie locale IP-Adapter Style ignorée: {e}")
72
 
73
  # 4) Import dynamique de la pipeline InstantID (fichier texte local)
74
  def import_pipeline_or_fail():
 
114
 
115
  controlnet_identitynet = ControlNetModel.from_pretrained(CN_LOCAL_DIR, torch_dtype=DTYPE)
116
 
 
117
  pipe = SDXLInstantID.from_pretrained(
118
  BASE_MODEL,
119
+ controlnet=controlnet_identitynet, # objet unique
120
  torch_dtype=DTYPE,
121
  safety_checker=None,
122
  feature_extractor=None,
123
  ).to(DEVICE)
124
 
125
+ # 6.1) Charger l'IP-Adapter InstantID (pour l'identité)
126
+ # (la pipeline InstantID fournit cette méthode utilitaire)
127
  pipe.load_ip_adapter_instantid(IP_ADAPTER_LOCAL)
128
+
129
+ # 6.2) (Optionnel) Charger un IP-Adapter Style SDXL (h94/IP-Adapter)
130
+ # On le nomme "style" pour l'adresser séparément dans set_ip_adapter_scale().
131
+ # Suivant diffusers, adapter_name peut ne pas exister: gérer fallback.
132
+ try:
133
+ pipe.load_ip_adapter(
134
+ IP_STYLE_REPO,
135
+ subfolder=IP_STYLE_SUBFOLDER,
136
+ weight_name=IP_STYLE_WEIGHT,
137
+ adapter_name="style",
138
+ )
139
+ load_logs.append("✅ IP-Adapter Style (SDXL) chargé (adapter_name='style').")
140
+ HAS_STYLE_ADAPTER = True
141
+ except Exception as e:
142
+ load_logs.append(f"ℹ️ Impossible de charger IP-Adapter Style via load_ip_adapter: {e}")
143
+ HAS_STYLE_ADAPTER = False
144
+
145
  if DEVICE == "cuda":
146
  if hasattr(pipe, "image_proj_model"): pipe.image_proj_model.to("cuda")
147
  if hasattr(pipe, "unet"): pipe.unet.to("cuda")
 
150
  except Exception:
151
  load_logs += ["❌ ERREUR au chargement:", traceback.format_exc()]
152
  pipe = None
153
+ HAS_STYLE_ADAPTER = False
154
 
155
  if pipe is None:
156
  raise RuntimeError("Échec de chargement du pipeline.\n" + "\n".join(load_logs))
 
161
  errors = []
162
  for name in ("antelopev2", "buffalo_l"):
163
  try:
 
164
  fa = FaceAnalysis(name=name, root="./models", providers=["CPUExecutionProvider"])
165
  fa.prepare(ctx_id=0, det_size=(640, 640))
166
  print(f"✅ InsightFace chargé: {name}")
 
179
  if not faces:
180
  raise ValueError("Aucun visage détecté dans la photo.")
181
  face = faces[-1]
 
182
  return draw_kps_local(pil_img, face["kps"])
183
 
184
+ # --- util: normaliser image_embeds quelle que soit la forme renvoyée
185
  def _normalize_image_embeds(image_embeds):
186
  import numpy as np
 
187
  if isinstance(image_embeds, dict):
188
  for k in ("image_embeds", "prompt_image_embeds", "pooled_prompt_embeds"):
189
  if k in image_embeds and image_embeds[k] is not None:
190
+ image_embeds = image_embeds[k]; break
 
 
191
  if isinstance(image_embeds, (tuple, list)):
192
+ if len(image_embeds) == 0: return None
 
 
193
  image_embeds = image_embeds[0] if image_embeds[0] is not None else image_embeds[-1]
 
194
  if isinstance(image_embeds, np.ndarray):
195
  image_embeds = torch.from_numpy(image_embeds)
 
196
  if isinstance(image_embeds, torch.Tensor):
197
  image_embeds = image_embeds.to(device=DEVICE, dtype=DTYPE if DEVICE == "cuda" else torch.float32)
198
  return image_embeds
199
 
200
  # 8) Génération
201
+ def generate(face_image, style_image, prompt, negative_prompt,
202
+ identity_strength, adapter_strength, style_strength,
203
+ steps, cfg, width, height, seed):
204
  try:
 
205
  if face_image is None:
206
  return None, "Merci d'ajouter une photo visage.", "\n".join(load_logs)
207
 
 
208
  gen = None if seed is None or int(seed) < 0 else torch.Generator(device=DEVICE).manual_seed(int(seed))
209
 
210
+ # 2) préparer visage (carré 512) + landmarks
211
  face = ImageOps.exif_transpose(face_image).convert("RGB")
212
+ ms = min(face.size); x = (face.width - ms) // 2; y = (face.height - ms) // 2
 
 
213
  face_sq = face.crop((x, y, x + ms, y + ms)).resize((512, 512), Image.Resampling.LANCZOS)
214
 
215
  kps_img = extract_kps_image(face_sq) # -> image landmarks (noir sur blanc)
216
 
217
+ # 3) règles IP-Adapter scales
218
+ # - adapter_strength = détails anime d’InstantID (son IP-adapter interne)
219
+ # - style_strength = force de l’IP-Adapter Style (si présent + image fournie)
220
+ # On supporte dict (multi-adapters nommés) et fallback simple.
221
+ set_scale_ok = False
222
+ try:
223
+ if HAS_STYLE_ADAPTER and style_image is not None:
224
+ pipe.set_ip_adapter_scale({"instantid": float(adapter_strength), "style": float(style_strength)})
225
+ else:
226
+ # pas de style → ne régler que l’adapter InstantID
227
+ pipe.set_ip_adapter_scale(float(adapter_strength))
228
+ set_scale_ok = True
229
+ except Exception as e:
230
+ print(f"ℹ️ set_ip_adapter_scale fallback: {e}")
231
+ try:
232
+ pipe.set_ip_adapter_scale(float(adapter_strength))
233
+ set_scale_ok = True
234
+ except Exception as e2:
235
+ print(f"⚠️ set_ip_adapter_scale impossible: {e2}")
236
 
237
+ # 4) embeddings InstantID visage
238
  image_embeds = None
239
  for m in ("get_image_embeds", "prepare_ip_adapter_image_embeds", "encode_image", "encode_ip_image"):
240
  fn = getattr(pipe, m, None)
241
  if callable(fn):
242
  try:
243
+ image_embeds = fn(face_sq); break
 
244
  except Exception as e:
245
  print(f"⚠️ {m} a échoué: {e}")
 
246
  image_embeds = _normalize_image_embeds(image_embeds)
247
  if image_embeds is None:
248
  return None, (
249
  "La pipeline InstantID SDXL requiert des `image_embeds`, mais aucune méthode compatible n'a été trouvée "
250
  "(get_image_embeds / prepare_ip_adapter_image_embeds / encode_image / encode_ip_image). "
251
+ "Vérifie que le fichier `pipeline_stable_diffusion_xl_instantid.py` est bien la version SDXL officielle."
 
252
  ), "\n".join(load_logs)
253
 
254
+ # 5) multi-ControlNet compat
255
  cn = getattr(pipe, "controlnet", None)
256
  if isinstance(cn, (list, tuple)):
257
  n_cn = len(cn)
258
  else:
259
+ try: n_cn = len(cn)
260
+ except Exception: n_cn = 1
 
 
261
 
262
  image_arg = [kps_img] * n_cn if n_cn > 1 else ([kps_img] if isinstance(cn, (list, tuple)) else kps_img)
263
  scale_val = float(identity_strength)
264
  scale_arg = [scale_val] * n_cn if n_cn > 1 else ([scale_val] if isinstance(cn, (list, tuple)) else scale_val)
265
 
266
+ # 6) préparer kwargs IP-Adapter Style si utilisé
267
+ gen_kwargs = dict(
268
  prompt=prompt.strip(),
269
  negative_prompt=(negative_prompt or "").strip(),
270
+ image=image_arg,
271
+ image_embeds=image_embeds,
272
+ controlnet_conditioning_scale=scale_arg,
273
  num_inference_steps=int(steps),
274
  guidance_scale=float(cfg),
275
  width=int(width),
276
  height=int(height),
277
  generator=gen,
278
+ )
279
+ # diffusers modernes: ip_adapter_image=style_image
280
+ if HAS_STYLE_ADAPTER and style_image is not None:
281
+ try:
282
+ gen_kwargs["ip_adapter_image"] = ImageOps.exif_transpose(style_image).convert("RGB")
283
+ except Exception as e:
284
+ print(f"ℹ️ ip_adapter_image conversion ignorée: {e}")
285
 
286
+ # 7) appel pipeline
287
+ images = pipe(**gen_kwargs).images
288
  return images[0], "", "\n".join(load_logs)
289
 
290
+ except torch.cuda.OutOfMemoryError:
291
  return None, "CUDA OOM: baisse la résolution ou les steps.", "\n".join(load_logs)
292
  except Exception:
293
  return None, "Erreur:\n" + traceback.format_exc(), "\n".join(load_logs)
 
296
  EX_PROMPT = (
297
  "one piece style, Eiichiro Oda style, anime portrait, upper body, pirate outfit, straw hat, "
298
  "clean lineart, cel shading, vibrant colors, expressive eyes, symmetrical face, looking at camera, "
299
+ "dynamic composition, simple background"
300
  )
301
  EX_NEG = (
302
+ "realistic, photo, photorealistic, skin pores, complex lighting, "
303
  "low quality, worst quality, lowres, blurry, noisy, watermark, text, logo, jpeg artifacts, "
304
  "bad anatomy, distorted eyes, deformed, multiple faces, nsfw"
305
  )
306
 
307
  with gr.Blocks(css="footer{display:none !important}") as demo:
308
+ gr.Markdown("# 🏴‍☠️ One Piece – InstantID SDXL + IP-Adapter Style (2D total)")
309
 
310
  with gr.Row():
311
  with gr.Column():
312
+ face_image = gr.Image(type="pil", label="Photo visage (obligatoire)", height=260)
313
+ style_image = gr.Image(type="pil", label="Image de style (IP-Adapter) — optionnel", height=260)
314
+ prompt = gr.Textbox(label="Prompt", value=EX_PROMPT, lines=3)
315
+ negative= gr.Textbox(label="Negative Prompt", value=EX_NEG, lines=3)
316
+
317
+ with gr.Row():
318
+ identity_strength = gr.Slider(0.2, 1.5, 0.95, 0.05, label="Fidélité visage (IdentityNet)")
319
+ adapter_strength = gr.Slider(0.1, 1.5, 0.85, 0.05, label="Détails anime (InstantID IP-Adapter)")
320
+
321
+ # Nouveau : force style (IP-Adapter Style)
322
+ style_strength = gr.Slider(0.1, 1.5, 0.95, 0.05, label="Force style (IP-Adapter Style)")
323
+
324
  steps = gr.Slider(10, 60, 30, 1, label="Steps")
325
+ cfg = gr.Slider(0.1, 12.0, 6.5, 0.1, label="CFG")
326
  width = gr.Dropdown(choices=[576, 640, 704, 768, 896], value=704, label="Largeur")
327
  height = gr.Dropdown(choices=[704, 768, 896, 1024], value=896, label="Hauteur")
328
  seed = gr.Number(value=-1, label="Seed (-1 aléatoire)")
 
331
  with gr.Column():
332
  out_image = gr.Image(label="Résultat", interactive=False)
333
  err_box = gr.Textbox(label="Erreurs", visible=False)
334
+ log_box = gr.Textbox(label="Logs", value="\n".join(load_logs), lines=12)
335
 
336
  def wrap(*args):
337
  img, err, logs = generate(*args)
 
339
 
340
  btn.click(
341
  wrap,
342
+ inputs=[face_image, style_image, prompt, negative,
343
+ identity_strength, adapter_strength, style_strength,
344
+ steps, cfg, width, height, seed],
345
  outputs=[out_image, err_box, log_box],
346
  )
347