i0switch commited on
Commit
478bf4d
·
verified ·
1 Parent(s): 6fe182c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +80 -112
app.py CHANGED
@@ -1,5 +1,5 @@
1
- # app.py — InstantID × Beautiful Realistic Asians v7(ZeroGPU 対応・CrucibleAI ControlNet
2
- # 2025-06-22
3
 
4
  ##############################################################################
5
  # 0. 旧 API → 新 API 互換パッチ(必ず diffusers import の前に置く)
@@ -7,9 +7,9 @@
7
  from huggingface_hub import hf_hub_download
8
  import huggingface_hub as _hf_hub
9
 
10
- # diffusers-0.27 は cached_download() を呼び出すため、v0.28+ でも使えるよう注入
11
  if not hasattr(_hf_hub, "cached_download"):
12
- _hf_hub.cached_download = hf_hub_download
13
 
14
  ##############################################################################
15
  # 1. 標準 & 外部ライブラリ
@@ -37,7 +37,7 @@ from basicsr.utils.download_util import load_file_from_url
37
  from realesrgan import RealESRGANer
38
 
39
  ##############################################################################
40
- # 2. キャッシュ & パス
41
  ##############################################################################
42
  PERSIST_BASE = Path("/data")
43
  CACHE_ROOT = (
@@ -46,43 +46,54 @@ CACHE_ROOT = (
46
  else Path.home() / ".cache" / "instantid_cache"
47
  )
48
  MODELS_DIR = CACHE_ROOT / "models"
49
- LORA_DIR = CACHE_ROOT / "lora"
50
  UPSCALE_DIR = CACHE_ROOT / "realesrgan"
51
- for _p in (MODELS_DIR, LORA_DIR, UPSCALE_DIR):
52
- _p.mkdir(parents=True, exist_ok=True)
53
 
54
  ##############################################################################
55
- # 3. モデル URL 一覧
56
  ##############################################################################
57
- BRA_V7_URL = (
58
- "https://huggingface.co/i0switch-assets/Beautiful_Realistic_Asians_v7/"
59
- "resolve/main/beautiful_realistic_asians_v7_fp16.safetensors"
60
- )
61
- IP_ADAPTER_BIN_URL = (
62
- "https://huggingface.co/h94/IP-Adapter/"
63
- "resolve/main/ip-adapter-plus-face_sd15.bin"
64
- )
65
- IP_ADAPTER_LORA_URL = (
66
- "https://huggingface.co/h94/IP-Adapter-FaceID/"
67
- "resolve/main/ip-adapter-faceid-plusv2_sd15_lora.safetensors"
68
- )
69
- REALESRGAN_URL = (
70
- "https://huggingface.co/aimagelab/realesrgan/"
71
- "resolve/main/RealESRGAN_x4plus.pth"
72
- )
73
 
74
  ##############################################################################
75
- # 4. ダウンローダ
76
  ##############################################################################
77
- def download(url: str, dst: Path, attempts: int = 2):
 
 
 
 
 
 
 
 
 
 
 
 
78
  if dst.exists():
79
  return dst
80
- for i in range(1, attempts + 1):
81
  try:
82
  subprocess.check_call(["curl", "-L", "-o", str(dst), url])
83
  return dst
84
  except subprocess.CalledProcessError:
85
- print(f"[DL] Retry {i}/{attempts} failed: {url}")
86
  load_file_from_url(url=url, model_dir=str(dst.parent), file_name=dst.name)
87
  return dst
88
 
@@ -103,23 +114,18 @@ def initialize_pipelines():
103
 
104
  print("[INIT] Downloading model assets …")
105
 
106
- # 6-1 ベースモデル & IP-Adapter
107
- bra_ckpt = download(BRA_V7_URL, MODELS_DIR / "bra_v7.safetensors")
108
- ip_bin = download(IP_ADAPTER_BIN_URL, MODELS_DIR / "ip_adapter.bin")
109
- ip_lora = download(IP_ADAPTER_LORA_URL, LORA_DIR / "ip_adapter_faceid.lora")
110
-
111
- # 6-2 ControlNet(CrucibleAI / diffusion_sd15)
112
- controlnet = ControlNetModel.from_pretrained(
113
- "CrucibleAI/ControlNetMediaPipeFace", # 公開リポジトリ :contentReference[oaicite:0]{index=0}
114
- subfolder="diffusion_sd15", # SD-1.5 用フォルダ :contentReference[oaicite:1]{index=1}
115
- torch_dtype=torch.float16,
116
- cache_dir=str(MODELS_DIR),
117
  )
118
 
119
- # 6-3 Diffusers パイプライン
120
  pipe_tmp = StableDiffusionControlNetPipeline.from_pretrained(
121
  "runwayml/stable-diffusion-v1-5",
122
- controlnet=controlnet,
123
  vae=AutoencoderKL.from_pretrained(
124
  "stabilityai/sd-vae-ft-mse", torch_dtype=torch.float16
125
  ),
@@ -133,16 +139,12 @@ def initialize_pipelines():
133
  cache_dir=str(MODELS_DIR),
134
  )
135
 
136
- # 6-4 **IP-Adapter 読み込み(API 仕様は positional 三つ)**
137
- # diffusers-0.27.2 では subfolder / weight_name が必須 :contentReference[oaicite:2]{index=2}
138
- ip_dir = ip_bin.parent
139
  pipe_tmp.load_ip_adapter(
140
- str(ip_dir), # path or repo id
141
- "", # subfolder(ファイル直下なので空文字列)
142
  ip_bin.name # weight_name
143
  )
144
-
145
- # IP-Adapter の追加 LoRA を合流
146
  AttnProcsLayers(pipe_tmp.unet.attn_processors).load_lora_weights(
147
  ip_lora, adapter_name="ip_faceid", safe_load=True
148
  )
@@ -151,22 +153,19 @@ def initialize_pipelines():
151
 
152
  pipe = pipe_tmp
153
 
154
- # 6-5 InsightFace
155
  face_analyser = FaceAnalysis(
156
  name="buffalo_l", root=str(MODELS_DIR), providers=["CUDAExecutionProvider"]
157
  )
158
  face_analyser.prepare(ctx_id=0, det_size=(640, 640))
159
 
160
- # 6-6 Real-ESRGAN
161
- esrgan_ckpt = download(REALESRGAN_URL, UPSCALE_DIR / "realesrgan_x4plus.pth")
162
  upsampler = RealESRGANer(
163
  scale=4,
164
- model_path=str(esrgan_ckpt),
165
  half=True,
166
- tile=512,
167
- tile_pad=10,
168
- pre_pad=0,
169
- gpu_id=0,
170
  )
171
 
172
  print("[INIT] Pipelines ready.")
@@ -185,9 +184,9 @@ NEG_PROMPT = (
185
  )
186
 
187
  ##############################################################################
188
- # 8. 生成関数(GPU アタッチ)
189
  ##############################################################################
190
- @spaces.GPU(duration=60)
191
  def generate_core(
192
  face_img: Image.Image,
193
  subject: str,
@@ -206,14 +205,13 @@ def generate_core(
206
  if pipe is None:
207
  initialize_pipelines()
208
 
209
- faces = face_analyser.get(np.array(face_img))
210
- if len(faces) == 0:
211
- raise ValueError("顔が検出できませんでした。別の画像をお試しください。")
212
 
213
  pipe.set_adapters(["ip_faceid"], adapter_weights=[ip_scale])
214
 
215
- prompt = BASE_PROMPT + subject + ", " + add_prompt
216
- negative = NEG_PROMPT + ", " + add_neg
217
 
218
  result = pipe(
219
  prompt=prompt,
@@ -222,11 +220,10 @@ def generate_core(
222
  guidance_scale=float(cfg),
223
  image=face_img,
224
  control_image=None,
225
- width=int(w),
226
- height=int(h),
227
  ).images[0]
228
 
229
- if upscale and upsampler is not None:
230
  upsampler.scale = 4 if up_factor == 4 else 8
231
  result, _ = upsampler.enhance(np.array(result))
232
  result = Image.fromarray(result)
@@ -244,41 +241,27 @@ with gr.Blocks(title="InstantID × BRA v7 (ZeroGPU)") as demo:
244
  gr.Markdown("## InstantID × Beautiful Realistic Asians v7")
245
  with gr.Row():
246
  face_img = gr.Image(type="pil", label="Face ID", sources=["upload"])
247
- subject = gr.Textbox(
248
- label="被写体説明(例: 30代日本人女性、黒髪セミロング)", interactive=True
249
- )
250
  add_prompt = gr.Textbox(label="追加プロンプト", interactive=True)
251
- add_neg = gr.Textbox(label="追加ネガティブ", interactive=True)
252
  with gr.Row():
253
- cfg = gr.Slider(1, 20, value=7.5, step=0.5, label="CFG Scale")
254
  ip_scale = gr.Slider(0.1, 1.0, value=0.6, step=0.05, label="IP-Adapter Weight")
255
  with gr.Row():
256
  steps = gr.Slider(10, 50, value=30, step=1, label="Steps")
257
- w = gr.Slider(512, 1024, value=768, step=64, label="Width")
258
- h = gr.Slider(512, 1024, value=768, step=64, label="Height")
259
  with gr.Row():
260
- upscale = gr.Checkbox(label="Real-ESRGAN Upscale", value=False)
261
  up_factor = gr.Radio([4, 8], value=4, label="Upscale Factor")
262
- run_btn = gr.Button("Generate")
263
- output_img = gr.Image(type="pil", label="Result")
264
 
265
  run_btn.click(
266
  fn=generate_core,
267
- inputs=[
268
- face_img,
269
- subject,
270
- add_prompt,
271
- add_neg,
272
- cfg,
273
- ip_scale,
274
- steps,
275
- w,
276
- h,
277
- upscale,
278
- up_factor,
279
- ],
280
- outputs=output_img,
281
- show_progress=True,
282
  )
283
 
284
  ##############################################################################
@@ -297,25 +280,10 @@ async def api_generate(
297
  file: UploadFile = File(...),
298
  ):
299
  try:
300
- img_bytes = await file.read()
301
- pil = Image.open(io.BytesIO(img_bytes)).convert("RGB")
302
- res = generate_core(
303
- face_img=pil,
304
- subject=subject,
305
- add_prompt="",
306
- add_neg="",
307
- cfg=cfg,
308
- ip_scale=ip_scale,
309
- steps=steps,
310
- w=w,
311
- h=h,
312
- upscale=False,
313
- up_factor=4,
314
- )
315
- buf = io.BytesIO()
316
- res.save(buf, format="PNG")
317
- b64 = base64.b64encode(buf.getvalue()).decode()
318
- return {"image": f"data:image/png;base64,{b64}"}
319
  except Exception as e:
320
  traceback.print_exc()
321
  raise HTTPException(status_code=500, detail=str(e))
@@ -323,4 +291,4 @@ async def api_generate(
323
  ##############################################################################
324
  # 11. Launch(Gradio が自動で Uvicorn を起動)
325
  ##############################################################################
326
- demo.queue(default_concurrency_limit=2).launch(share=False)
 
1
+ # app.py — InstantID × Beautiful Realistic Asians v7(ZeroGPU / ControlNetMediaPipeFace
2
+ # 2025-06-22
3
 
4
  ##############################################################################
5
  # 0. 旧 API → 新 API 互換パッチ(必ず diffusers import の前に置く)
 
7
  from huggingface_hub import hf_hub_download
8
  import huggingface_hub as _hf_hub
9
 
10
+ # diffusers-0.27 は cached_download() を呼び出すため、HF-Hub ≥0.28 でも使えるように注入
11
  if not hasattr(_hf_hub, "cached_download"):
12
+ _hf_hub.cached_download = hf_hub_download # :contentReference[oaicite:1]{index=1}
13
 
14
  ##############################################################################
15
  # 1. 標準 & 外部ライブラリ
 
37
  from realesrgan import RealESRGANer
38
 
39
  ##############################################################################
40
+ # 2. キャッシュ & 永続パス
41
  ##############################################################################
42
  PERSIST_BASE = Path("/data")
43
  CACHE_ROOT = (
 
46
  else Path.home() / ".cache" / "instantid_cache"
47
  )
48
  MODELS_DIR = CACHE_ROOT / "models"
49
+ LORA_DIR = CACHE_ROOT / "lora"
50
  UPSCALE_DIR = CACHE_ROOT / "realesrgan"
51
+ for p in (MODELS_DIR, LORA_DIR, UPSCALE_DIR):
52
+ p.mkdir(parents=True, exist_ok=True)
53
 
54
  ##############################################################################
55
+ # 3. モデル識別子 & ファイル名
56
  ##############################################################################
57
+ # すべて HF Hub 側にバイナリがあるため、curl ではなく hf_hub_download() を推奨
58
+ BRA_REPO = "i0switch-assets/Beautiful_Realistic_Asians_v7"
59
+ BRA_FILE = "beautiful_realistic_asians_v7_fp16.safetensors"
60
+
61
+ IP_REPO = "h94/IP-Adapter"
62
+ IP_FILE_BIN = "ip-adapter-plus-face_sd15.bin" # Git LFS バイナリ :contentReference[oaicite:2]{index=2}
63
+
64
+ IP_LORA_REPO = "h94/IP-Adapter-FaceID"
65
+ IP_FILE_LORA = "ip-adapter-faceid-plusv2_sd15_lora.safetensors" # Git LFS バイナリ
66
+
67
+ CN_REPO = "CrucibleAI/ControlNetMediaPipeFace" # 公開・無認証で DL 可 :contentReference[oaicite:3]{index=3}
68
+ CN_FOLDER = "diffusion_sd15" # SD-1.5 用フォルダ :contentReference[oaicite:4]{index=4}
69
+
70
+ REALESRGAN_REPO = "aimagelab/realesrgan"
71
+ REALESRGAN_FILE = "RealESRGAN_x4plus.pth"
 
72
 
73
  ##############################################################################
74
+ # 4. ダウンローダ(HF Hub 優先)
75
  ##############################################################################
76
+ def dl_hf(repo: str, filename: str, subfolder: Optional[str] = None) -> Path:
77
+ """HF Hub から大容量バイナリを安全に取得(Git LFS ポインタ問題を回避)"""
78
+ return Path(
79
+ hf_hub_download(
80
+ repo_id=repo,
81
+ filename=filename,
82
+ subfolder=subfolder,
83
+ cache_dir=str(MODELS_DIR),
84
+ )
85
+ )
86
+
87
+ def dl_http(url: str, dst: Path):
88
+ """小さなファイルのみ curl で取得(retry 付)"""
89
  if dst.exists():
90
  return dst
91
+ for _ in range(2):
92
  try:
93
  subprocess.check_call(["curl", "-L", "-o", str(dst), url])
94
  return dst
95
  except subprocess.CalledProcessError:
96
+ pass
97
  load_file_from_url(url=url, model_dir=str(dst.parent), file_name=dst.name)
98
  return dst
99
 
 
114
 
115
  print("[INIT] Downloading model assets …")
116
 
117
+ # 6-1 主要モデル
118
+ bra_ckpt = dl_hf(BRA_REPO, BRA_FILE)
119
+ ip_bin = dl_hf(IP_REPO, IP_FILE_BIN)
120
+ ip_lora = dl_hf(IP_LORA_REPO, IP_FILE_LORA)
121
+ cn_model = ControlNetModel.from_pretrained(
122
+ CN_REPO, subfolder=CN_FOLDER, torch_dtype=torch.float16, cache_dir=str(MODELS_DIR)
 
 
 
 
 
123
  )
124
 
125
+ # 6-2 Diffusers パイプライン
126
  pipe_tmp = StableDiffusionControlNetPipeline.from_pretrained(
127
  "runwayml/stable-diffusion-v1-5",
128
+ controlnet=cn_model,
129
  vae=AutoencoderKL.from_pretrained(
130
  "stabilityai/sd-vae-ft-mse", torch_dtype=torch.float16
131
  ),
 
139
  cache_dir=str(MODELS_DIR),
140
  )
141
 
142
+ # 6-3 IP-Adapter ロード(必須 3 引数) :contentReference[oaicite:5]{index=5}
 
 
143
  pipe_tmp.load_ip_adapter(
144
+ str(ip_bin.parent), # repo_or_path
145
+ "", # subfolder(直下なので空文字)
146
  ip_bin.name # weight_name
147
  )
 
 
148
  AttnProcsLayers(pipe_tmp.unet.attn_processors).load_lora_weights(
149
  ip_lora, adapter_name="ip_faceid", safe_load=True
150
  )
 
153
 
154
  pipe = pipe_tmp
155
 
156
+ # 6-4 InsightFace
157
  face_analyser = FaceAnalysis(
158
  name="buffalo_l", root=str(MODELS_DIR), providers=["CUDAExecutionProvider"]
159
  )
160
  face_analyser.prepare(ctx_id=0, det_size=(640, 640))
161
 
162
+ # 6-5 Real-ESRGAN
163
+ re_ckpt = dl_hf(REALESRGAN_REPO, REALESRGAN_FILE)
164
  upsampler = RealESRGANer(
165
  scale=4,
166
+ model_path=str(re_ckpt),
167
  half=True,
168
+ tile=512, tile_pad=10, pre_pad=0, gpu_id=0
 
 
 
169
  )
170
 
171
  print("[INIT] Pipelines ready.")
 
184
  )
185
 
186
  ##############################################################################
187
+ # 8. 生成コア(GPU アタッチ)
188
  ##############################################################################
189
+ @spaces.GPU(duration=60) # ZeroGPU で 60 s まで実行可 :contentReference[oaicite:6]{index=6}
190
  def generate_core(
191
  face_img: Image.Image,
192
  subject: str,
 
205
  if pipe is None:
206
  initialize_pipelines()
207
 
208
+ if len(face_analyser.get(np.array(face_img))) == 0:
209
+ raise ValueError("顔が検出できません。別の画像でお試しください。")
 
210
 
211
  pipe.set_adapters(["ip_faceid"], adapter_weights=[ip_scale])
212
 
213
+ prompt = BASE_PROMPT + subject + ", " + add_prompt
214
+ negative = NEG_PROMPT + ", " + add_neg
215
 
216
  result = pipe(
217
  prompt=prompt,
 
220
  guidance_scale=float(cfg),
221
  image=face_img,
222
  control_image=None,
223
+ width=int(w), height=int(h),
 
224
  ).images[0]
225
 
226
+ if upscale:
227
  upsampler.scale = 4 if up_factor == 4 else 8
228
  result, _ = upsampler.enhance(np.array(result))
229
  result = Image.fromarray(result)
 
241
  gr.Markdown("## InstantID × Beautiful Realistic Asians v7")
242
  with gr.Row():
243
  face_img = gr.Image(type="pil", label="Face ID", sources=["upload"])
244
+ subject = gr.Textbox(label="被写体説明(例: 30代日本人女性、黒髪セミロング)", interactive=True)
 
 
245
  add_prompt = gr.Textbox(label="追加プロンプト", interactive=True)
246
+ add_neg = gr.Textbox(label="追加ネガティブ", interactive=True)
247
  with gr.Row():
248
+ cfg = gr.Slider(1, 20, value=7.5, step=0.5, label="CFG Scale")
249
  ip_scale = gr.Slider(0.1, 1.0, value=0.6, step=0.05, label="IP-Adapter Weight")
250
  with gr.Row():
251
  steps = gr.Slider(10, 50, value=30, step=1, label="Steps")
252
+ w = gr.Slider(512, 1024, value=768, step=64, label="Width")
253
+ h = gr.Slider(512, 1024, value=768, step=64, label="Height")
254
  with gr.Row():
255
+ upscale = gr.Checkbox(label="Real-ESRGAN Upscale", value=False)
256
  up_factor = gr.Radio([4, 8], value=4, label="Upscale Factor")
257
+ run_btn = gr.Button("Generate")
258
+ output_im = gr.Image(type="pil", label="Result")
259
 
260
  run_btn.click(
261
  fn=generate_core,
262
+ inputs=[face_img, subject, add_prompt, add_neg,
263
+ cfg, ip_scale, steps, w, h, upscale, up_factor],
264
+ outputs=output_im, show_progress=True
 
 
 
 
 
 
 
 
 
 
 
 
265
  )
266
 
267
  ##############################################################################
 
280
  file: UploadFile = File(...),
281
  ):
282
  try:
283
+ img = Image.open(io.BytesIO(await file.read())).convert("RGB") # noqa
284
+ res = generate_core(img, subject, "", "", cfg, ip_scale, steps, w, h, False, 4)
285
+ buf = io.BytesIO(); res.save(buf, format="PNG")
286
+ return {"image": "data:image/png;base64," + base64.b64encode(buf.getvalue()).decode()}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
287
  except Exception as e:
288
  traceback.print_exc()
289
  raise HTTPException(status_code=500, detail=str(e))
 
291
  ##############################################################################
292
  # 11. Launch(Gradio が自動で Uvicorn を起動)
293
  ##############################################################################
294
+ demo.queue(default_concurrency_limit=2).launch(share=False) # :contentReference[oaicite:7]{index=7}