Files changed (5) hide show
  1. .gitattributes +35 -0
  2. README.md +6 -11
  3. app.py +0 -541
  4. packages.txt +0 -5
  5. requirements.txt +0 -5
.gitattributes ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -1,18 +1,13 @@
1
  ---
2
- title: AI Photo Studio
3
- emoji:
4
  colorFrom: purple
5
- colorTo: pink
6
  sdk: gradio
7
- sdk_version: 5.41.0
 
8
  app_file: app.py
9
  pinned: false
10
- license: mit
11
- short_description: AI face enhancement with CodeFormer
12
  ---
13
 
14
- # AI Photo Studio
15
-
16
- Upload any photo → AI enhances faces → Download PNG
17
-
18
- Powered by CodeFormer AI + Professional Post-Processing
 
1
  ---
2
+ title: Face Enhancer Api
3
+ emoji: 📉
4
  colorFrom: purple
5
+ colorTo: purple
6
  sdk: gradio
7
+ sdk_version: 6.24.0
8
+ python_version: '3.12'
9
  app_file: app.py
10
  pinned: false
 
 
11
  ---
12
 
13
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
app.py DELETED
@@ -1,541 +0,0 @@
1
- """
2
- ✨ AI Photo Studio — Works Great With or Without CodeFormer
3
- Full enhancement pipeline: AI or advanced OpenCV
4
- """
5
-
6
- import gradio as gr
7
- import cv2
8
- import numpy as np
9
- import time
10
- import logging
11
- import tempfile
12
- import os
13
- from PIL import Image, ImageEnhance, ImageFilter
14
-
15
- logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s')
16
- logger = logging.getLogger(__name__)
17
-
18
- try:
19
- import spaces
20
- except ImportError:
21
- class spaces:
22
- @staticmethod
23
- def GPU(fn=None, **kwargs):
24
- if fn is None: return lambda f: f
25
- return fn
26
-
27
- try:
28
- from gradio_client import Client as HFClient
29
- # Try to import handle_file, fall back to string path
30
- try:
31
- from gradio_client import handle_file as _handle_file
32
- def make_file_handle(path):
33
- return _handle_file(path)
34
- logger.info("✅ gradio_client + handle_file available")
35
- except ImportError:
36
- def make_file_handle(path):
37
- return path # Older gradio_client accepts string paths
38
- logger.info("✅ gradio_client available (no handle_file, using string paths)")
39
- HAS_CLIENT = True
40
- except ImportError:
41
- HAS_CLIENT = False
42
- def make_file_handle(path): return path
43
- logger.error("❌ gradio_client not available")
44
-
45
- STATE = {'client': None, 'connected': False}
46
- HF_TOKEN = os.environ.get("HF_TOKEN", "")
47
-
48
-
49
- # ═══════════════════════════════════════════════════════════════
50
- # CODEFORMER
51
- # ═══════════════════════════════════════════════════════════════
52
-
53
- def connect():
54
- if not HAS_CLIENT:
55
- logger.error("❌ gradio_client not available")
56
- return False
57
- try:
58
- if HF_TOKEN:
59
- logger.info("🔌 Connecting to CodeFormer with HF_TOKEN...")
60
- STATE['client'] = HFClient("sczhou/CodeFormer", hf_token=HF_TOKEN)
61
- else:
62
- logger.info("🔌 Connecting to CodeFormer (no token)...")
63
- STATE['client'] = HFClient("sczhou/CodeFormer")
64
- STATE['connected'] = True
65
- logger.info("✅ Connected to CodeFormer!")
66
- return True
67
- except Exception as e:
68
- logger.error(f"❌ CodeFormer connection failed: {e}")
69
- return False
70
-
71
- def call_codeformer(pil_img):
72
- """Try CodeFormer with multiple parameter combinations"""
73
- c = STATE.get('client')
74
- if not c: return None
75
-
76
- configs = [
77
- {'upscale': 2, 'fidelity': 0.1},
78
- {'upscale': 2, 'fidelity': 0.5},
79
- {'upscale': 4, 'fidelity': 0.1},
80
- ]
81
-
82
- for cfg in configs:
83
- t = tempfile.NamedTemporaryFile(suffix='.png', delete=False)
84
- pil_img.save(t.name, 'PNG')
85
- t.close()
86
- try:
87
- file_arg = make_file_handle(t.name)
88
- logger.info(f"Calling CodeFormer: upscale={cfg['upscale']}, fidelity={cfg['fidelity']}, file_type={type(file_arg)}")
89
- r = c.predict(
90
- image=file_arg,
91
- face_align=True,
92
- background_enhance=True,
93
- face_upsample=True,
94
- upscale=cfg['upscale'],
95
- codeformer_fidelity=cfg['fidelity'],
96
- api_name="/inference"
97
- )
98
- d = r[0] if isinstance(r, (list, tuple)) else r
99
- if isinstance(d, dict): d = d.get('path') or d.get('url')
100
- if isinstance(d, str) and os.path.exists(d):
101
- logger.info(f"✅ CodeFormer success! Output: {d}")
102
- return Image.open(d)
103
- elif isinstance(d, str):
104
- # Try downloading from URL
105
- logger.info(f"CodeFormer returned URL: {d[:100]}")
106
- try:
107
- import urllib.request
108
- dl = tempfile.NamedTemporaryFile(suffix='.png', delete=False)
109
- urllib.request.urlretrieve(d, dl.name)
110
- dl.close()
111
- return Image.open(dl.name)
112
- except Exception as e2:
113
- logger.warning(f"Download failed: {e2}")
114
- except Exception as e:
115
- logger.warning(f"CodeFormer failed (upscale={cfg['upscale']}): {e}")
116
- finally:
117
- try: os.unlink(t.name)
118
- except: pass
119
-
120
- return None
121
-
122
-
123
- # ═══════════════════════════════════════════════════════════════
124
- # ADVANCED OPENCV PIPELINE (when CodeFormer unavailable)
125
- # ═══════════════════════════════════════════════════════════════
126
-
127
- def detect_faces(img):
128
- h, w = img.shape[:2]
129
- ycrcb = cv2.cvtColor(img, cv2.COLOR_BGR2YCrCb)
130
- hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
131
- m1 = cv2.inRange(ycrcb, np.array([0,133,77]), np.array([255,173,127]))
132
- m2 = cv2.inRange(hsv, np.array([0,15,60]), np.array([30,255,255]))
133
- skin = cv2.bitwise_and(m1, m2)
134
- k = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (7,7))
135
- skin = cv2.morphologyEx(skin, cv2.MORPH_CLOSE, k, iterations=3)
136
- skin = cv2.morphologyEx(skin, cv2.MORPH_OPEN, k, iterations=2)
137
- n,_,stats,_ = cv2.connectedComponentsWithStats(skin, 8)
138
- faces = []
139
- for i in range(1, n):
140
- a = stats[i, cv2.CC_STAT_AREA]
141
- if a > (h*w)*0.005:
142
- x,y = stats[i,cv2.CC_STAT_LEFT], stats[i,cv2.CC_STAT_TOP]
143
- bw,bh = stats[i,cv2.CC_STAT_WIDTH], stats[i,cv2.CC_STAT_HEIGHT]
144
- if 0.4 < bw/max(bh,1) < 2.5:
145
- p = int(max(bw,bh)*0.15)
146
- faces.append([max(0,x-p), max(0,y-p), min(w,x+bw+p), min(h,y+bh+p)])
147
- return faces
148
-
149
- def get_skin_mask(img):
150
- ycrcb = cv2.cvtColor(img, cv2.COLOR_BGR2YCrCb)
151
- hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
152
- m1 = cv2.inRange(ycrcb, np.array([0,133,77]), np.array([255,173,127]))
153
- m2 = cv2.inRange(hsv, np.array([0,15,60]), np.array([30,255,255]))
154
- kn = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3,3))
155
- sk = cv2.morphologyEx(cv2.bitwise_and(m1,m2), cv2.MORPH_CLOSE, kn, iterations=2)
156
- sk = cv2.morphologyEx(sk, cv2.MORPH_OPEN, kn, iterations=1)
157
- return cv2.GaussianBlur(sk, (15,15), 0).astype(np.float32)/255.0
158
-
159
- def opencv_full_enhance(img_cv):
160
- """Complete OpenCV enhancement pipeline — no AI needed"""
161
- h, w = img_cv.shape[:2]
162
- r = img_cv.copy()
163
-
164
- # ── 1. Strong denoise ──
165
- r = cv2.fastNlMeansDenoisingColored(r, None, 8, 8, 7, 21)
166
-
167
- # ── 2. HDR-like tone mapping ──
168
- lab = cv2.cvtColor(r, cv2.COLOR_BGR2LAB)
169
- l, a, b = cv2.split(lab)
170
- lf = l.astype(np.float32)
171
- base = cv2.bilateralFilter(lf, -1, 50, 50)
172
- detail = lf - base
173
- l_new = np.clip(base * 0.7 + 128 * 0.3 + detail * 1.4, 0, 255).astype(np.uint8)
174
- r = cv2.cvtColor(cv2.merge([l_new, a, b]), cv2.COLOR_LAB2BGR)
175
-
176
- # ── 3. CLAHE contrast ──
177
- lab = cv2.cvtColor(r, cv2.COLOR_BGR2LAB)
178
- l, a, b = cv2.split(lab)
179
- l = cv2.createCLAHE(clipLimit=2.5, tileGridSize=(8,8)).apply(l)
180
- r = cv2.cvtColor(cv2.merge([l, a, b]), cv2.COLOR_LAB2BGR)
181
-
182
- # ── 4. Gamma correction ──
183
- gray = cv2.cvtColor(r, cv2.COLOR_BGR2GRAY)
184
- mean_b = gray.mean()
185
- if mean_b < 115:
186
- gamma = 1.0 + (115 - mean_b) / 115 * 0.4
187
- elif mean_b > 180:
188
- gamma = 1.0 - (mean_b - 180) / 180 * 0.2
189
- else:
190
- gamma = 1.0
191
- if gamma != 1.0:
192
- table = np.array([((i/255.0)**(1.0/gamma))*255 for i in range(256)]).astype(np.uint8)
193
- r = cv2.LUT(r, table)
194
-
195
- # ── 5. White balance (percentile) ──
196
- f = r.astype(np.float32)
197
- for c in range(3):
198
- lo, hi = np.percentile(f[:,:,c], 1), np.percentile(f[:,:,c], 99)
199
- if hi > lo: f[:,:,c] = np.clip((f[:,:,c]-lo)/(hi-lo)*255, 0, 255)
200
- r = f.astype(np.uint8)
201
-
202
- # ── 6. Skin smoothing (light) ──
203
- sk = get_skin_mask(r)
204
- smoothed = cv2.bilateralFilter(r, 7, 25, 25)
205
- alpha = np.expand_dims(sk * 0.2, 2)
206
- r = np.clip(r.astype(np.float32)*(1-alpha) + smoothed.astype(np.float32)*alpha, 0, 255).astype(np.uint8)
207
- # Texture restore
208
- detail = r.astype(np.float32) - cv2.GaussianBlur(r, (0,0), 1.5).astype(np.float32)
209
- r = np.clip(r.astype(np.float32) + detail * 0.5 * np.expand_dims(sk, 2), 0, 255).astype(np.uint8)
210
-
211
- # ── 7. Face-specific sharpening ──
212
- faces = detect_faces(r)
213
- if faces:
214
- for x1,y1,x2,y2 in faces:
215
- face = r[y1:y2, x1:x2].copy()
216
- if face.size == 0: continue
217
- # Strong unsharp on face
218
- g = cv2.GaussianBlur(face, (0,0), 2.0)
219
- sharpened = cv2.addWeighted(face, 1.7, g, -0.7, 0)
220
- # Detail kernel
221
- kernel = np.array([[0,-0.5,0],[-0.5,3.0,-0.5],[0,-0.5,0]])
222
- sharpened = cv2.filter2D(sharpened, -1, kernel)
223
- # Blend back
224
- fh, fw = sharpened.shape[:2]
225
- mask = np.ones((fh,fw), dtype=np.float32)
226
- border = int(min(fh,fw)*0.15)
227
- for i in range(border):
228
- al = i/border
229
- mask[i,:]*=al; mask[-(i+1),:]*=al; mask[:,i]*=al; mask[:,-(i+1)]*=al
230
- mask = cv2.GaussianBlur(mask, (11,11), 0)
231
- m3 = np.expand_dims(mask, 2)
232
- region = r[y1:y2, x1:x2].astype(np.float32)
233
- r[y1:y2, x1:x2] = np.clip(region*(1-m3) + sharpened.astype(np.float32)*m3, 0, 255).astype(np.uint8)
234
- else:
235
- # No faces — sharpen entire image
236
- g = cv2.GaussianBlur(r, (0,0), 2.0)
237
- r = cv2.addWeighted(r, 1.5, g, -0.5, 0)
238
- kernel = np.array([[0,-0.4,0],[-0.4,2.6,-0.4],[0,-0.4,0]])
239
- r = cv2.filter2D(r, -1, kernel)
240
-
241
- # ── 8. Skin tone fix (prevent blue) ──
242
- if faces:
243
- for x1,y1,x2,y2 in faces:
244
- face = r[y1:y2, x1:x2].copy()
245
- if face.size == 0: continue
246
- sk_face = get_skin_mask(face)
247
- sk_bool = sk_face > 0.5
248
- if np.sum(sk_bool) < 100: continue
249
- avg_b = np.mean(face[:,:,0][sk_bool])
250
- avg_r = np.mean(face[:,:,2][sk_bool])
251
- if avg_b > avg_r * 0.85:
252
- correction = np.ones_like(face, dtype=np.float32)
253
- correction[:,:,0] = 0.92
254
- correction[:,:,2] = 1.05
255
- sk3 = np.expand_dims(sk_face, 2)
256
- corrected = face.astype(np.float32)*(1-sk3*0.5) + (face.astype(np.float32)*correction)*sk3*0.5
257
- face_fixed = np.clip(corrected, 0, 255).astype(np.uint8)
258
- fh, fw = face_fixed.shape[:2]
259
- mask = np.ones((fh,fw), dtype=np.float32)
260
- border = int(min(fh,fw)*0.12)
261
- for i in range(border):
262
- al = i/border
263
- mask[i,:]*=al; mask[-(i+1),:]*=al; mask[:,i]*=al; mask[:,-(i+1)]*=al
264
- mask = cv2.GaussianBlur(mask, (9,9), 0)
265
- m3 = np.expand_dims(mask, 2)
266
- region = r[y1:y2, x1:x2].astype(np.float32)
267
- r[y1:y2, x1:x2] = np.clip(region*(1-m3) + face_fixed.astype(np.float32)*m3, 0, 255).astype(np.uint8)
268
-
269
- # ── 9. Warm color grading ──
270
- lab = cv2.cvtColor(r, cv2.COLOR_BGR2LAB).astype(np.float32)
271
- lab[:,:,1] = np.clip(lab[:,:,1] + 0.5, 0, 255)
272
- lab[:,:,2] = np.clip(lab[:,:,2] + 0.3, 0, 255)
273
- r = cv2.cvtColor(lab.astype(np.uint8), cv2.COLOR_LAB2BGR)
274
-
275
- # ── 10. Saturation ──
276
- hsv = cv2.cvtColor(r, cv2.COLOR_BGR2HSV).astype(np.float32)
277
- hsv[:,:,1] = np.clip(hsv[:,:,1] * 1.08, 0, 255)
278
- r = cv2.cvtColor(hsv.astype(np.uint8), cv2.COLOR_HSV2BGR)
279
-
280
- # ── 11. Vignette ──
281
- Y, X = np.ogrid[:h,:w]
282
- dist = np.sqrt(((X-w/2)/(w/2))**2 + ((Y-h/2)/(h/2))**2)
283
- vig = np.clip(np.expand_dims(1 - 0.04*(dist**2), 2), 0, 1)
284
- r = np.clip(r.astype(np.float32) * vig, 0, 255).astype(np.uint8)
285
-
286
- return r
287
-
288
-
289
- def upscale_smart(img_cv, min_size=1024):
290
- """Smart multi-step upscaling"""
291
- h, w = img_cv.shape[:2]
292
- if max(h, w) >= min_size:
293
- return img_cv
294
- scale = min_size / max(h, w)
295
- # Multi-step for better quality
296
- if scale > 2.5:
297
- # Step 1: 2x
298
- img_cv = cv2.resize(img_cv, (w*2, h*2), interpolation=cv2.INTER_LANCZOS4)
299
- remaining = scale / 2.0
300
- h, w = img_cv.shape[:2]
301
- img_cv = cv2.resize(img_cv, (int(w*remaining), int(h*remaining)), interpolation=cv2.INTER_LANCZOS4)
302
- else:
303
- img_cv = cv2.resize(img_cv, (int(w*scale), int(h*scale)), interpolation=cv2.INTER_LANCZOS4)
304
- # Unsharp mask
305
- g = cv2.GaussianBlur(img_cv, (0,0), 2.0)
306
- img_cv = cv2.addWeighted(img_cv, 1.5, g, -0.5, 0)
307
- img_cv = cv2.fastNlMeansDenoisingColored(img_cv, None, 3, 3, 7, 21)
308
- return img_cv
309
-
310
-
311
- def skin_smooth(img):
312
- """Light skin smoothing with texture preservation"""
313
- h, w = img.shape[:2]
314
- if h < 50 or w < 50: return img
315
- sk = get_skin_mask(img)
316
- smoothed = cv2.bilateralFilter(img, 7, 22, 22)
317
- alpha = np.expand_dims(sk * 0.2, 2)
318
- result = np.clip(img.astype(np.float32)*(1-alpha) + smoothed.astype(np.float32)*alpha, 0, 255).astype(np.uint8)
319
- detail = result.astype(np.float32) - cv2.GaussianBlur(result, (0,0), 1.5).astype(np.float32)
320
- result = np.clip(result.astype(np.float32) + detail * 0.5 * np.expand_dims(sk, 2), 0, 255).astype(np.uint8)
321
- return result
322
-
323
- def face_sharpen(img):
324
- """Sharpen face regions"""
325
- faces = detect_faces(img)
326
- if not faces:
327
- g = cv2.GaussianBlur(img, (0,0), 2.0)
328
- img = cv2.addWeighted(img, 1.5, g, -0.5, 0)
329
- kernel = np.array([[0,-0.4,0],[-0.4,2.6,-0.4],[0,-0.4,0]])
330
- return cv2.filter2D(img, -1, kernel)
331
- for x1,y1,x2,y2 in faces:
332
- face = img[y1:y2, x1:x2].copy()
333
- if face.size == 0: continue
334
- g = cv2.GaussianBlur(face, (0,0), 2.0)
335
- sharpened = cv2.addWeighted(face, 1.6, g, -0.6, 0)
336
- kernel = np.array([[0,-0.5,0],[-0.5,3.0,-0.5],[0,-0.5,0]])
337
- sharpened = cv2.filter2D(sharpened, -1, kernel)
338
- fh, fw = sharpened.shape[:2]
339
- mask = np.ones((fh,fw), dtype=np.float32)
340
- border = int(min(fh,fw)*0.15)
341
- for i in range(border):
342
- al = i/border
343
- mask[i,:]*=al; mask[-(i+1),:]*=al; mask[:,i]*=al; mask[:,-(i+1)]*=al
344
- mask = cv2.GaussianBlur(mask, (11,11), 0)
345
- m3 = np.expand_dims(mask, 2)
346
- region = img[y1:y2, x1:x2].astype(np.float32)
347
- img[y1:y2, x1:x2] = np.clip(region*(1-m3) + sharpened.astype(np.float32)*m3, 0, 255).astype(np.uint8)
348
- return img
349
-
350
- def studio_grade(img):
351
- """Studio color grading"""
352
- r = img.copy()
353
- h, w = r.shape[:2]
354
- f = r.astype(np.float32)
355
- for c in range(3):
356
- lo, hi = np.percentile(f[:,:,c], 1), np.percentile(f[:,:,c], 99)
357
- if hi > lo: f[:,:,c] = np.clip((f[:,:,c]-lo)/(hi-lo)*255, 0, 255)
358
- r = f.astype(np.uint8)
359
- lab = cv2.cvtColor(r, cv2.COLOR_BGR2LAB)
360
- l, a, b = cv2.split(lab)
361
- l = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8)).apply(l)
362
- r = cv2.cvtColor(cv2.merge([l, a, b]), cv2.COLOR_LAB2BGR)
363
- lab = cv2.cvtColor(r, cv2.COLOR_BGR2LAB).astype(np.float32)
364
- lab[:,:,1] = np.clip(lab[:,:,1] + 0.5, 0, 255)
365
- lab[:,:,2] = np.clip(lab[:,:,2] + 0.3, 0, 255)
366
- r = cv2.cvtColor(lab.astype(np.uint8), cv2.COLOR_LAB2BGR)
367
- hsv = cv2.cvtColor(r, cv2.COLOR_BGR2HSV).astype(np.float32)
368
- hsv[:,:,1] = np.clip(hsv[:,:,1] * 1.06, 0, 255)
369
- r = cv2.cvtColor(hsv.astype(np.uint8), cv2.COLOR_HSV2BGR)
370
- Y, X = np.ogrid[:h,:w]
371
- dist = np.sqrt(((X-w/2)/(w/2))**2 + ((Y-h/2)/(h/2))**2)
372
- vig = np.clip(np.expand_dims(1 - 0.04*(dist**2), 2), 0, 1)
373
- r = np.clip(r.astype(np.float32) * vig, 0, 255).astype(np.uint8)
374
- return r
375
-
376
- def pil_enhance(pil_img):
377
- img = pil_img.copy()
378
- img = ImageEnhance.Contrast(img).enhance(1.08)
379
- img = ImageEnhance.Color(img).enhance(1.06)
380
- img = ImageEnhance.Brightness(img).enhance(1.03)
381
- img = ImageEnhance.Sharpness(img).enhance(1.15)
382
- img = img.filter(ImageFilter.DETAIL)
383
- img = img.filter(ImageFilter.UnsharpMask(radius=1.5, percent=40, threshold=3))
384
- return img
385
-
386
- def fix_skin_tone(img):
387
- faces = detect_faces(img)
388
- if not faces: return img
389
- for x1,y1,x2,y2 in faces:
390
- face = img[y1:y2, x1:x2].copy()
391
- if face.size == 0: continue
392
- sk = get_skin_mask(face)
393
- sk_bool = sk > 0.5
394
- if np.sum(sk_bool) < 100: continue
395
- avg_b = np.mean(face[:,:,0][sk_bool])
396
- avg_r = np.mean(face[:,:,2][sk_bool])
397
- if avg_b > avg_r * 0.85:
398
- correction = np.ones_like(face, dtype=np.float32)
399
- correction[:,:,0] = 0.92; correction[:,:,2] = 1.05
400
- sk3 = np.expand_dims(sk, 2)
401
- corrected = face.astype(np.float32)*(1-sk3*0.5) + (face.astype(np.float32)*correction)*sk3*0.5
402
- face_fixed = np.clip(corrected, 0, 255).astype(np.uint8)
403
- fh, fw = face_fixed.shape[:2]
404
- mask = np.ones((fh,fw), dtype=np.float32)
405
- border = int(min(fh,fw)*0.12)
406
- for i in range(border):
407
- al = i/border
408
- mask[i,:]*=al; mask[-(i+1),:]*=al; mask[:,i]*=al; mask[:,-(i+1)]*=al
409
- mask = cv2.GaussianBlur(mask, (9,9), 0)
410
- m3 = np.expand_dims(mask, 2)
411
- region = img[y1:y2, x1:x2].astype(np.float32)
412
- img[y1:y2, x1:x2] = np.clip(region*(1-m3) + face_fixed.astype(np.float32)*m3, 0, 255).astype(np.uint8)
413
- return img
414
-
415
-
416
- # ═══════════════════════════════════════════════════════════════
417
- # MAIN PIPELINE
418
- # ═══════════════════════════════════════════════════════════════
419
-
420
- # Dummy GPU function to satisfy ZeroGPU requirement (if hardware is ZeroGPU)
421
- # The actual enhance function runs on CPU - CodeFormer uses REMOTE GPU
422
- @spaces.GPU(duration=5)
423
- def _gpu_placeholder():
424
- """Dummy function for ZeroGPU compatibility. Does nothing."""
425
- return True
426
-
427
- # NOTE: The actual enhance function runs on CPU.
428
- # CodeFormer AI runs on the REMOTE Space's GPU (sczhou/CodeFormer).
429
- # Set Space hardware to "CPU basic" for unlimited free usage.
430
- def enhance(image_pil, progress=gr.Progress()):
431
- start = time.time()
432
- steps = []
433
- if image_pil.mode != 'RGB': image_pil = image_pil.convert('RGB')
434
- oh, ow = image_pil.size[1], image_pil.size[0]
435
-
436
- try:
437
- # Try CodeFormer
438
- progress(0.05, desc="🔌 Connecting to AI...")
439
- if not STATE.get('connected'): connect()
440
-
441
- progress(0.1, desc="🤖 AI face restoration...")
442
- cf_result = None
443
- debug_info = f"connected={STATE.get('connected')}, has_client={STATE.get('client') is not None}, has_gradio={HAS_CLIENT}"
444
- if STATE.get('connected'):
445
- cf_result = call_codeformer(image_pil)
446
- if cf_result:
447
- debug_info += ", cf=SUCCESS"
448
- else:
449
- debug_info += ", cf=FAILED"
450
- else:
451
- debug_info += ", NOT_CONNECTED"
452
-
453
- if cf_result:
454
- steps.append("🤖 CodeFormer AI (fidelity=0.1, 4x)")
455
- img_cv = cv2.cvtColor(np.array(cf_result), cv2.COLOR_RGB2BGR)
456
- else:
457
- # ═══ FULL OPENCV PIPELINE ═══
458
- steps.append("🔧 Advanced OpenCV pipeline (11 stages)")
459
- img_cv = cv2.cvtColor(np.array(image_pil), cv2.COLOR_RGB2BGR)
460
-
461
- progress(0.2, desc="🔧 Full enhancement...")
462
- img_cv = opencv_full_enhance(img_cv)
463
- steps.append(" ✓ Denoise + HDR + CLAHE + Gamma + WB")
464
- steps.append(" ✓ Skin smooth + Face sharpen + Tone fix")
465
- steps.append(" ✓ Color grade + Saturation + Vignette")
466
-
467
- # Upscale if needed
468
- progress(0.5, desc="⬆️ Resolution...")
469
- img_cv = upscale_smart(img_cv, 1024)
470
- rh, rw = img_cv.shape[:2]
471
- steps.append(f"⬆️ {rw}×{rh}")
472
-
473
- # Skin smooth (if CodeFormer was used)
474
- if cf_result:
475
- progress(0.6, desc="✨ Skin...")
476
- img_cv = skin_smooth(img_cv)
477
- steps.append("✨ Skin smoothing")
478
- progress(0.65, desc="🔍 Sharpen...")
479
- img_cv = face_sharpen(img_cv)
480
- steps.append("🔍 Face sharpen")
481
- progress(0.7, desc="🎨 Color...")
482
- img_cv = studio_grade(img_cv)
483
- steps.append("🎨 Studio color grading")
484
- progress(0.75, desc="⚖️ Tone...")
485
- img_cv = fix_skin_tone(img_cv)
486
- steps.append("⚖️ Skin tone fix")
487
-
488
- # PIL polish
489
- progress(0.85, desc="🖼️ Final polish...")
490
- result_pil = Image.fromarray(cv2.cvtColor(img_cv, cv2.COLOR_BGR2RGB))
491
- result_pil = pil_enhance(result_pil)
492
- steps.append("🖼️ PIL polish")
493
-
494
- # Save PNG
495
- progress(0.95, desc="💾 Saving...")
496
- tmp = tempfile.NamedTemporaryFile(suffix='.png', delete=False)
497
- result_pil.save(tmp.name, format='PNG')
498
- final = Image.open(tmp.name)
499
-
500
- except Exception as e:
501
- logger.error(f"Error: {e}")
502
- steps.append(f"⚠️ Error: {str(e)[:60]}")
503
- final = image_pil.copy()
504
-
505
- elapsed = (time.time()-start)*1000
506
- rw, rh = final.size
507
- progress(1.0, desc=f"✅ {elapsed:.0f}ms")
508
-
509
- lines = [f"## ✨ Enhanced in {elapsed:.0f}ms!\n",
510
- f"| Before | After |\n|---|---|\n| {ow}×{oh} | **{rw}×{rh}** |\n",
511
- f"*Debug: {debug_info}*",
512
- "### Pipeline:"]
513
- for s in steps: lines.append(f"- {s}")
514
- if not cf_result:
515
- lines.append("\n> 💡 **Tip:** Add `HF_TOKEN` in Space Settings → Secrets for AI-powered face restoration (even better results)")
516
- return final, "\n".join(lines)
517
-
518
-
519
- # ═══════════════════════════════════════════════════════════════
520
- # UI
521
- # ═══════════════════════════════════════════════════════════════
522
-
523
- _T = gr.themes.Soft(primary_hue="purple", secondary_hue="pink")
524
- _CSS = ".hdr{text-align:center;margin-bottom:12px}.hdr h1{background:linear-gradient(135deg,#7c5cfc,#ec4899);-webkit-background-clip:text;-webkit-text-fill-color:transparent;font-size:2.2em;font-weight:800}.hdr p{color:#888}footer{display:none!important}.gradio-container{max-width:900px!important;margin:0 auto!important}"
525
-
526
- def build_app():
527
- with gr.Blocks(title="✨ AI Photo Studio", theme=_T, css=_CSS) as app:
528
- gr.HTML('<div class="hdr"><h1>✨ AI Photo Studio</h1><p>Upload any photo → Get enhanced result → Download PNG</p></div>')
529
- with gr.Row():
530
- with gr.Column():
531
- inp = gr.Image(label="📸 Upload your photo", type="pil", height=420, sources=["upload","clipboard"])
532
- btn = gr.Button("✨ Enhance My Photo", variant="primary", size="lg")
533
- with gr.Column():
534
- out = gr.Image(label="✨ Enhanced Result (PNG)", type="pil", height=420, format="png")
535
- st = gr.Markdown("*Upload a photo and click Enhance*")
536
- btn.click(fn=enhance, inputs=[inp], outputs=[out, st])
537
- return app
538
-
539
- if __name__ == "__main__":
540
- app = build_app()
541
- app.launch(server_name="0.0.0.0", share=False, show_error=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
packages.txt DELETED
@@ -1,5 +0,0 @@
1
- libgl1
2
- libglib2.0-0
3
- libsm6
4
- libxext6
5
- libxrender1
 
 
 
 
 
 
requirements.txt DELETED
@@ -1,5 +0,0 @@
1
- rembg>=2.0.50
2
- onnxruntime>=1.16.0
3
- opencv-python-headless>=4.8.0
4
- numpy>=1.24.0
5
- Pillow>=10.0.0