Spaces:
Running on Zero
Running on Zero
| """ | |
| ✨ AI Photo Studio — Works Great With or Without CodeFormer | |
| Full enhancement pipeline: AI or advanced OpenCV | |
| """ | |
| import gradio as gr | |
| import cv2 | |
| import numpy as np | |
| import time | |
| import logging | |
| import tempfile | |
| import os | |
| from PIL import Image, ImageEnhance, ImageFilter | |
| logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s') | |
| logger = logging.getLogger(__name__) | |
| try: | |
| import spaces | |
| except ImportError: | |
| class spaces: | |
| def GPU(fn=None, **kwargs): | |
| if fn is None: return lambda f: f | |
| return fn | |
| try: | |
| from gradio_client import Client as HFClient | |
| # Try to import handle_file, fall back to string path | |
| try: | |
| from gradio_client import handle_file as _handle_file | |
| def make_file_handle(path): | |
| return _handle_file(path) | |
| logger.info("✅ gradio_client + handle_file available") | |
| except ImportError: | |
| def make_file_handle(path): | |
| return path # Older gradio_client accepts string paths | |
| logger.info("✅ gradio_client available (no handle_file, using string paths)") | |
| HAS_CLIENT = True | |
| except ImportError: | |
| HAS_CLIENT = False | |
| def make_file_handle(path): return path | |
| logger.error("❌ gradio_client not available") | |
| STATE = {'client': None, 'connected': False} | |
| HF_TOKEN = os.environ.get("HF_TOKEN", "") | |
| # ═══════════════════════════════════════════════════════════════ | |
| # CODEFORMER | |
| # ═══════════════════════════════════════════════════════════════ | |
| def connect(): | |
| if not HAS_CLIENT: | |
| logger.error("❌ gradio_client not available") | |
| return False | |
| try: | |
| if HF_TOKEN: | |
| logger.info("🔌 Connecting to CodeFormer with HF_TOKEN...") | |
| STATE['client'] = HFClient("sczhou/CodeFormer", hf_token=HF_TOKEN) | |
| else: | |
| logger.info("🔌 Connecting to CodeFormer (no token)...") | |
| STATE['client'] = HFClient("sczhou/CodeFormer") | |
| STATE['connected'] = True | |
| logger.info("✅ Connected to CodeFormer!") | |
| return True | |
| except Exception as e: | |
| logger.error(f"❌ CodeFormer connection failed: {e}") | |
| return False | |
| def call_codeformer(pil_img): | |
| """Try CodeFormer with multiple parameter combinations""" | |
| c = STATE.get('client') | |
| if not c: return None | |
| configs = [ | |
| {'upscale': 2, 'fidelity': 0.1}, | |
| {'upscale': 2, 'fidelity': 0.5}, | |
| {'upscale': 4, 'fidelity': 0.1}, | |
| ] | |
| for cfg in configs: | |
| t = tempfile.NamedTemporaryFile(suffix='.png', delete=False) | |
| pil_img.save(t.name, 'PNG') | |
| t.close() | |
| try: | |
| file_arg = make_file_handle(t.name) | |
| logger.info(f"Calling CodeFormer: upscale={cfg['upscale']}, fidelity={cfg['fidelity']}, file_type={type(file_arg)}") | |
| r = c.predict( | |
| image=file_arg, | |
| face_align=True, | |
| background_enhance=True, | |
| face_upsample=True, | |
| upscale=cfg['upscale'], | |
| codeformer_fidelity=cfg['fidelity'], | |
| api_name="/inference" | |
| ) | |
| d = r[0] if isinstance(r, (list, tuple)) else r | |
| if isinstance(d, dict): d = d.get('path') or d.get('url') | |
| if isinstance(d, str) and os.path.exists(d): | |
| logger.info(f"✅ CodeFormer success! Output: {d}") | |
| return Image.open(d) | |
| elif isinstance(d, str): | |
| # Try downloading from URL | |
| logger.info(f"CodeFormer returned URL: {d[:100]}") | |
| try: | |
| import urllib.request | |
| dl = tempfile.NamedTemporaryFile(suffix='.png', delete=False) | |
| urllib.request.urlretrieve(d, dl.name) | |
| dl.close() | |
| return Image.open(dl.name) | |
| except Exception as e2: | |
| logger.warning(f"Download failed: {e2}") | |
| except Exception as e: | |
| logger.warning(f"CodeFormer failed (upscale={cfg['upscale']}): {e}") | |
| finally: | |
| try: os.unlink(t.name) | |
| except: pass | |
| return None | |
| # ═══════════════════════════════════════════════════════════════ | |
| # ADVANCED OPENCV PIPELINE (when CodeFormer unavailable) | |
| # ═══════════════════════════════════════════════════════════════ | |
| def detect_faces(img): | |
| h, w = img.shape[:2] | |
| ycrcb = cv2.cvtColor(img, cv2.COLOR_BGR2YCrCb) | |
| hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) | |
| m1 = cv2.inRange(ycrcb, np.array([0,133,77]), np.array([255,173,127])) | |
| m2 = cv2.inRange(hsv, np.array([0,15,60]), np.array([30,255,255])) | |
| skin = cv2.bitwise_and(m1, m2) | |
| k = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (7,7)) | |
| skin = cv2.morphologyEx(skin, cv2.MORPH_CLOSE, k, iterations=3) | |
| skin = cv2.morphologyEx(skin, cv2.MORPH_OPEN, k, iterations=2) | |
| n,_,stats,_ = cv2.connectedComponentsWithStats(skin, 8) | |
| faces = [] | |
| for i in range(1, n): | |
| a = stats[i, cv2.CC_STAT_AREA] | |
| if a > (h*w)*0.005: | |
| x,y = stats[i,cv2.CC_STAT_LEFT], stats[i,cv2.CC_STAT_TOP] | |
| bw,bh = stats[i,cv2.CC_STAT_WIDTH], stats[i,cv2.CC_STAT_HEIGHT] | |
| if 0.4 < bw/max(bh,1) < 2.5: | |
| p = int(max(bw,bh)*0.15) | |
| faces.append([max(0,x-p), max(0,y-p), min(w,x+bw+p), min(h,y+bh+p)]) | |
| return faces | |
| def get_skin_mask(img): | |
| ycrcb = cv2.cvtColor(img, cv2.COLOR_BGR2YCrCb) | |
| hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) | |
| m1 = cv2.inRange(ycrcb, np.array([0,133,77]), np.array([255,173,127])) | |
| m2 = cv2.inRange(hsv, np.array([0,15,60]), np.array([30,255,255])) | |
| kn = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3,3)) | |
| sk = cv2.morphologyEx(cv2.bitwise_and(m1,m2), cv2.MORPH_CLOSE, kn, iterations=2) | |
| sk = cv2.morphologyEx(sk, cv2.MORPH_OPEN, kn, iterations=1) | |
| return cv2.GaussianBlur(sk, (15,15), 0).astype(np.float32)/255.0 | |
| def opencv_full_enhance(img_cv): | |
| """Complete OpenCV enhancement pipeline — no AI needed""" | |
| h, w = img_cv.shape[:2] | |
| r = img_cv.copy() | |
| # ── 1. Strong denoise ── | |
| r = cv2.fastNlMeansDenoisingColored(r, None, 8, 8, 7, 21) | |
| # ── 2. HDR-like tone mapping ── | |
| lab = cv2.cvtColor(r, cv2.COLOR_BGR2LAB) | |
| l, a, b = cv2.split(lab) | |
| lf = l.astype(np.float32) | |
| base = cv2.bilateralFilter(lf, -1, 50, 50) | |
| detail = lf - base | |
| l_new = np.clip(base * 0.7 + 128 * 0.3 + detail * 1.4, 0, 255).astype(np.uint8) | |
| r = cv2.cvtColor(cv2.merge([l_new, a, b]), cv2.COLOR_LAB2BGR) | |
| # ── 3. CLAHE contrast ── | |
| lab = cv2.cvtColor(r, cv2.COLOR_BGR2LAB) | |
| l, a, b = cv2.split(lab) | |
| l = cv2.createCLAHE(clipLimit=2.5, tileGridSize=(8,8)).apply(l) | |
| r = cv2.cvtColor(cv2.merge([l, a, b]), cv2.COLOR_LAB2BGR) | |
| # ── 4. Gamma correction ── | |
| gray = cv2.cvtColor(r, cv2.COLOR_BGR2GRAY) | |
| mean_b = gray.mean() | |
| if mean_b < 115: | |
| gamma = 1.0 + (115 - mean_b) / 115 * 0.4 | |
| elif mean_b > 180: | |
| gamma = 1.0 - (mean_b - 180) / 180 * 0.2 | |
| else: | |
| gamma = 1.0 | |
| if gamma != 1.0: | |
| table = np.array([((i/255.0)**(1.0/gamma))*255 for i in range(256)]).astype(np.uint8) | |
| r = cv2.LUT(r, table) | |
| # ── 5. White balance (percentile) ── | |
| f = r.astype(np.float32) | |
| for c in range(3): | |
| lo, hi = np.percentile(f[:,:,c], 1), np.percentile(f[:,:,c], 99) | |
| if hi > lo: f[:,:,c] = np.clip((f[:,:,c]-lo)/(hi-lo)*255, 0, 255) | |
| r = f.astype(np.uint8) | |
| # ── 6. Skin smoothing (light) ── | |
| sk = get_skin_mask(r) | |
| smoothed = cv2.bilateralFilter(r, 7, 25, 25) | |
| alpha = np.expand_dims(sk * 0.2, 2) | |
| r = np.clip(r.astype(np.float32)*(1-alpha) + smoothed.astype(np.float32)*alpha, 0, 255).astype(np.uint8) | |
| # Texture restore | |
| detail = r.astype(np.float32) - cv2.GaussianBlur(r, (0,0), 1.5).astype(np.float32) | |
| r = np.clip(r.astype(np.float32) + detail * 0.5 * np.expand_dims(sk, 2), 0, 255).astype(np.uint8) | |
| # ── 7. Face-specific sharpening ── | |
| faces = detect_faces(r) | |
| if faces: | |
| for x1,y1,x2,y2 in faces: | |
| face = r[y1:y2, x1:x2].copy() | |
| if face.size == 0: continue | |
| # Strong unsharp on face | |
| g = cv2.GaussianBlur(face, (0,0), 2.0) | |
| sharpened = cv2.addWeighted(face, 1.7, g, -0.7, 0) | |
| # Detail kernel | |
| kernel = np.array([[0,-0.5,0],[-0.5,3.0,-0.5],[0,-0.5,0]]) | |
| sharpened = cv2.filter2D(sharpened, -1, kernel) | |
| # Blend back | |
| fh, fw = sharpened.shape[:2] | |
| mask = np.ones((fh,fw), dtype=np.float32) | |
| border = int(min(fh,fw)*0.15) | |
| for i in range(border): | |
| al = i/border | |
| mask[i,:]*=al; mask[-(i+1),:]*=al; mask[:,i]*=al; mask[:,-(i+1)]*=al | |
| mask = cv2.GaussianBlur(mask, (11,11), 0) | |
| m3 = np.expand_dims(mask, 2) | |
| region = r[y1:y2, x1:x2].astype(np.float32) | |
| r[y1:y2, x1:x2] = np.clip(region*(1-m3) + sharpened.astype(np.float32)*m3, 0, 255).astype(np.uint8) | |
| else: | |
| # No faces — sharpen entire image | |
| g = cv2.GaussianBlur(r, (0,0), 2.0) | |
| r = cv2.addWeighted(r, 1.5, g, -0.5, 0) | |
| kernel = np.array([[0,-0.4,0],[-0.4,2.6,-0.4],[0,-0.4,0]]) | |
| r = cv2.filter2D(r, -1, kernel) | |
| # ── 8. Skin tone fix (prevent blue) ── | |
| if faces: | |
| for x1,y1,x2,y2 in faces: | |
| face = r[y1:y2, x1:x2].copy() | |
| if face.size == 0: continue | |
| sk_face = get_skin_mask(face) | |
| sk_bool = sk_face > 0.5 | |
| if np.sum(sk_bool) < 100: continue | |
| avg_b = np.mean(face[:,:,0][sk_bool]) | |
| avg_r = np.mean(face[:,:,2][sk_bool]) | |
| if avg_b > avg_r * 0.85: | |
| correction = np.ones_like(face, dtype=np.float32) | |
| correction[:,:,0] = 0.92 | |
| correction[:,:,2] = 1.05 | |
| sk3 = np.expand_dims(sk_face, 2) | |
| corrected = face.astype(np.float32)*(1-sk3*0.5) + (face.astype(np.float32)*correction)*sk3*0.5 | |
| face_fixed = np.clip(corrected, 0, 255).astype(np.uint8) | |
| fh, fw = face_fixed.shape[:2] | |
| mask = np.ones((fh,fw), dtype=np.float32) | |
| border = int(min(fh,fw)*0.12) | |
| for i in range(border): | |
| al = i/border | |
| mask[i,:]*=al; mask[-(i+1),:]*=al; mask[:,i]*=al; mask[:,-(i+1)]*=al | |
| mask = cv2.GaussianBlur(mask, (9,9), 0) | |
| m3 = np.expand_dims(mask, 2) | |
| region = r[y1:y2, x1:x2].astype(np.float32) | |
| r[y1:y2, x1:x2] = np.clip(region*(1-m3) + face_fixed.astype(np.float32)*m3, 0, 255).astype(np.uint8) | |
| # ── 9. Warm color grading ── | |
| lab = cv2.cvtColor(r, cv2.COLOR_BGR2LAB).astype(np.float32) | |
| lab[:,:,1] = np.clip(lab[:,:,1] + 0.5, 0, 255) | |
| lab[:,:,2] = np.clip(lab[:,:,2] + 0.3, 0, 255) | |
| r = cv2.cvtColor(lab.astype(np.uint8), cv2.COLOR_LAB2BGR) | |
| # ── 10. Saturation ── | |
| hsv = cv2.cvtColor(r, cv2.COLOR_BGR2HSV).astype(np.float32) | |
| hsv[:,:,1] = np.clip(hsv[:,:,1] * 1.08, 0, 255) | |
| r = cv2.cvtColor(hsv.astype(np.uint8), cv2.COLOR_HSV2BGR) | |
| # ── 11. Vignette ── | |
| Y, X = np.ogrid[:h,:w] | |
| dist = np.sqrt(((X-w/2)/(w/2))**2 + ((Y-h/2)/(h/2))**2) | |
| vig = np.clip(np.expand_dims(1 - 0.04*(dist**2), 2), 0, 1) | |
| r = np.clip(r.astype(np.float32) * vig, 0, 255).astype(np.uint8) | |
| return r | |
| def upscale_smart(img_cv, min_size=1024): | |
| """Smart multi-step upscaling""" | |
| h, w = img_cv.shape[:2] | |
| if max(h, w) >= min_size: | |
| return img_cv | |
| scale = min_size / max(h, w) | |
| # Multi-step for better quality | |
| if scale > 2.5: | |
| # Step 1: 2x | |
| img_cv = cv2.resize(img_cv, (w*2, h*2), interpolation=cv2.INTER_LANCZOS4) | |
| remaining = scale / 2.0 | |
| h, w = img_cv.shape[:2] | |
| img_cv = cv2.resize(img_cv, (int(w*remaining), int(h*remaining)), interpolation=cv2.INTER_LANCZOS4) | |
| else: | |
| img_cv = cv2.resize(img_cv, (int(w*scale), int(h*scale)), interpolation=cv2.INTER_LANCZOS4) | |
| # Unsharp mask | |
| g = cv2.GaussianBlur(img_cv, (0,0), 2.0) | |
| img_cv = cv2.addWeighted(img_cv, 1.5, g, -0.5, 0) | |
| img_cv = cv2.fastNlMeansDenoisingColored(img_cv, None, 3, 3, 7, 21) | |
| return img_cv | |
| def skin_smooth(img): | |
| """Light skin smoothing with texture preservation""" | |
| h, w = img.shape[:2] | |
| if h < 50 or w < 50: return img | |
| sk = get_skin_mask(img) | |
| smoothed = cv2.bilateralFilter(img, 7, 22, 22) | |
| alpha = np.expand_dims(sk * 0.2, 2) | |
| result = np.clip(img.astype(np.float32)*(1-alpha) + smoothed.astype(np.float32)*alpha, 0, 255).astype(np.uint8) | |
| detail = result.astype(np.float32) - cv2.GaussianBlur(result, (0,0), 1.5).astype(np.float32) | |
| result = np.clip(result.astype(np.float32) + detail * 0.5 * np.expand_dims(sk, 2), 0, 255).astype(np.uint8) | |
| return result | |
| def face_sharpen(img): | |
| """Sharpen face regions""" | |
| faces = detect_faces(img) | |
| if not faces: | |
| g = cv2.GaussianBlur(img, (0,0), 2.0) | |
| img = cv2.addWeighted(img, 1.5, g, -0.5, 0) | |
| kernel = np.array([[0,-0.4,0],[-0.4,2.6,-0.4],[0,-0.4,0]]) | |
| return cv2.filter2D(img, -1, kernel) | |
| for x1,y1,x2,y2 in faces: | |
| face = img[y1:y2, x1:x2].copy() | |
| if face.size == 0: continue | |
| g = cv2.GaussianBlur(face, (0,0), 2.0) | |
| sharpened = cv2.addWeighted(face, 1.6, g, -0.6, 0) | |
| kernel = np.array([[0,-0.5,0],[-0.5,3.0,-0.5],[0,-0.5,0]]) | |
| sharpened = cv2.filter2D(sharpened, -1, kernel) | |
| fh, fw = sharpened.shape[:2] | |
| mask = np.ones((fh,fw), dtype=np.float32) | |
| border = int(min(fh,fw)*0.15) | |
| for i in range(border): | |
| al = i/border | |
| mask[i,:]*=al; mask[-(i+1),:]*=al; mask[:,i]*=al; mask[:,-(i+1)]*=al | |
| mask = cv2.GaussianBlur(mask, (11,11), 0) | |
| m3 = np.expand_dims(mask, 2) | |
| region = img[y1:y2, x1:x2].astype(np.float32) | |
| img[y1:y2, x1:x2] = np.clip(region*(1-m3) + sharpened.astype(np.float32)*m3, 0, 255).astype(np.uint8) | |
| return img | |
| def studio_grade(img): | |
| """Studio color grading""" | |
| r = img.copy() | |
| h, w = r.shape[:2] | |
| f = r.astype(np.float32) | |
| for c in range(3): | |
| lo, hi = np.percentile(f[:,:,c], 1), np.percentile(f[:,:,c], 99) | |
| if hi > lo: f[:,:,c] = np.clip((f[:,:,c]-lo)/(hi-lo)*255, 0, 255) | |
| r = f.astype(np.uint8) | |
| lab = cv2.cvtColor(r, cv2.COLOR_BGR2LAB) | |
| l, a, b = cv2.split(lab) | |
| l = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8)).apply(l) | |
| r = cv2.cvtColor(cv2.merge([l, a, b]), cv2.COLOR_LAB2BGR) | |
| lab = cv2.cvtColor(r, cv2.COLOR_BGR2LAB).astype(np.float32) | |
| lab[:,:,1] = np.clip(lab[:,:,1] + 0.5, 0, 255) | |
| lab[:,:,2] = np.clip(lab[:,:,2] + 0.3, 0, 255) | |
| r = cv2.cvtColor(lab.astype(np.uint8), cv2.COLOR_LAB2BGR) | |
| hsv = cv2.cvtColor(r, cv2.COLOR_BGR2HSV).astype(np.float32) | |
| hsv[:,:,1] = np.clip(hsv[:,:,1] * 1.06, 0, 255) | |
| r = cv2.cvtColor(hsv.astype(np.uint8), cv2.COLOR_HSV2BGR) | |
| Y, X = np.ogrid[:h,:w] | |
| dist = np.sqrt(((X-w/2)/(w/2))**2 + ((Y-h/2)/(h/2))**2) | |
| vig = np.clip(np.expand_dims(1 - 0.04*(dist**2), 2), 0, 1) | |
| r = np.clip(r.astype(np.float32) * vig, 0, 255).astype(np.uint8) | |
| return r | |
| def pil_enhance(pil_img): | |
| img = pil_img.copy() | |
| img = ImageEnhance.Contrast(img).enhance(1.08) | |
| img = ImageEnhance.Color(img).enhance(1.06) | |
| img = ImageEnhance.Brightness(img).enhance(1.03) | |
| img = ImageEnhance.Sharpness(img).enhance(1.15) | |
| img = img.filter(ImageFilter.DETAIL) | |
| img = img.filter(ImageFilter.UnsharpMask(radius=1.5, percent=40, threshold=3)) | |
| return img | |
| def fix_skin_tone(img): | |
| faces = detect_faces(img) | |
| if not faces: return img | |
| for x1,y1,x2,y2 in faces: | |
| face = img[y1:y2, x1:x2].copy() | |
| if face.size == 0: continue | |
| sk = get_skin_mask(face) | |
| sk_bool = sk > 0.5 | |
| if np.sum(sk_bool) < 100: continue | |
| avg_b = np.mean(face[:,:,0][sk_bool]) | |
| avg_r = np.mean(face[:,:,2][sk_bool]) | |
| if avg_b > avg_r * 0.85: | |
| correction = np.ones_like(face, dtype=np.float32) | |
| correction[:,:,0] = 0.92; correction[:,:,2] = 1.05 | |
| sk3 = np.expand_dims(sk, 2) | |
| corrected = face.astype(np.float32)*(1-sk3*0.5) + (face.astype(np.float32)*correction)*sk3*0.5 | |
| face_fixed = np.clip(corrected, 0, 255).astype(np.uint8) | |
| fh, fw = face_fixed.shape[:2] | |
| mask = np.ones((fh,fw), dtype=np.float32) | |
| border = int(min(fh,fw)*0.12) | |
| for i in range(border): | |
| al = i/border | |
| mask[i,:]*=al; mask[-(i+1),:]*=al; mask[:,i]*=al; mask[:,-(i+1)]*=al | |
| mask = cv2.GaussianBlur(mask, (9,9), 0) | |
| m3 = np.expand_dims(mask, 2) | |
| region = img[y1:y2, x1:x2].astype(np.float32) | |
| img[y1:y2, x1:x2] = np.clip(region*(1-m3) + face_fixed.astype(np.float32)*m3, 0, 255).astype(np.uint8) | |
| return img | |
| # ═══════════════════════════════════════════════════════════════ | |
| # MAIN PIPELINE | |
| # ═══════════════════════════════════════════════════════════════ | |
| # Dummy GPU function to satisfy ZeroGPU requirement (if hardware is ZeroGPU) | |
| # The actual enhance function runs on CPU - CodeFormer uses REMOTE GPU | |
| def _gpu_placeholder(): | |
| """Dummy function for ZeroGPU compatibility. Does nothing.""" | |
| return True | |
| # NOTE: The actual enhance function runs on CPU. | |
| # CodeFormer AI runs on the REMOTE Space's GPU (sczhou/CodeFormer). | |
| # Set Space hardware to "CPU basic" for unlimited free usage. | |
| def enhance(image_pil, progress=gr.Progress()): | |
| start = time.time() | |
| steps = [] | |
| if image_pil.mode != 'RGB': image_pil = image_pil.convert('RGB') | |
| oh, ow = image_pil.size[1], image_pil.size[0] | |
| try: | |
| # Try CodeFormer | |
| progress(0.05, desc="🔌 Connecting to AI...") | |
| if not STATE.get('connected'): connect() | |
| progress(0.1, desc="🤖 AI face restoration...") | |
| cf_result = None | |
| debug_info = f"connected={STATE.get('connected')}, has_client={STATE.get('client') is not None}, has_gradio={HAS_CLIENT}" | |
| if STATE.get('connected'): | |
| cf_result = call_codeformer(image_pil) | |
| if cf_result: | |
| debug_info += ", cf=SUCCESS" | |
| else: | |
| debug_info += ", cf=FAILED" | |
| else: | |
| debug_info += ", NOT_CONNECTED" | |
| if cf_result: | |
| steps.append("🤖 CodeFormer AI (fidelity=0.1, 4x)") | |
| img_cv = cv2.cvtColor(np.array(cf_result), cv2.COLOR_RGB2BGR) | |
| else: | |
| # ═══ FULL OPENCV PIPELINE ═══ | |
| steps.append("🔧 Advanced OpenCV pipeline (11 stages)") | |
| img_cv = cv2.cvtColor(np.array(image_pil), cv2.COLOR_RGB2BGR) | |
| progress(0.2, desc="🔧 Full enhancement...") | |
| img_cv = opencv_full_enhance(img_cv) | |
| steps.append(" ✓ Denoise + HDR + CLAHE + Gamma + WB") | |
| steps.append(" ✓ Skin smooth + Face sharpen + Tone fix") | |
| steps.append(" ✓ Color grade + Saturation + Vignette") | |
| # Upscale if needed | |
| progress(0.5, desc="⬆️ Resolution...") | |
| img_cv = upscale_smart(img_cv, 1024) | |
| rh, rw = img_cv.shape[:2] | |
| steps.append(f"⬆️ {rw}×{rh}") | |
| # Skin smooth (if CodeFormer was used) | |
| if cf_result: | |
| progress(0.6, desc="✨ Skin...") | |
| img_cv = skin_smooth(img_cv) | |
| steps.append("✨ Skin smoothing") | |
| progress(0.65, desc="🔍 Sharpen...") | |
| img_cv = face_sharpen(img_cv) | |
| steps.append("🔍 Face sharpen") | |
| progress(0.7, desc="🎨 Color...") | |
| img_cv = studio_grade(img_cv) | |
| steps.append("🎨 Studio color grading") | |
| progress(0.75, desc="⚖️ Tone...") | |
| img_cv = fix_skin_tone(img_cv) | |
| steps.append("⚖️ Skin tone fix") | |
| # PIL polish | |
| progress(0.85, desc="🖼️ Final polish...") | |
| result_pil = Image.fromarray(cv2.cvtColor(img_cv, cv2.COLOR_BGR2RGB)) | |
| result_pil = pil_enhance(result_pil) | |
| steps.append("🖼️ PIL polish") | |
| # Save PNG | |
| progress(0.95, desc="💾 Saving...") | |
| tmp = tempfile.NamedTemporaryFile(suffix='.png', delete=False) | |
| result_pil.save(tmp.name, format='PNG') | |
| final = Image.open(tmp.name) | |
| except Exception as e: | |
| logger.error(f"Error: {e}") | |
| steps.append(f"⚠️ Error: {str(e)[:60]}") | |
| final = image_pil.copy() | |
| elapsed = (time.time()-start)*1000 | |
| rw, rh = final.size | |
| progress(1.0, desc=f"✅ {elapsed:.0f}ms") | |
| lines = [f"## ✨ Enhanced in {elapsed:.0f}ms!\n", | |
| f"| Before | After |\n|---|---|\n| {ow}×{oh} | **{rw}×{rh}** |\n", | |
| f"*Debug: {debug_info}*", | |
| "### Pipeline:"] | |
| for s in steps: lines.append(f"- {s}") | |
| if not cf_result: | |
| lines.append("\n> 💡 **Tip:** Add `HF_TOKEN` in Space Settings → Secrets for AI-powered face restoration (even better results)") | |
| return final, "\n".join(lines) | |
| # ═══════════════════════════════════════════════════════════════ | |
| # UI | |
| # ═══════════════════════════════════════════════════════════════ | |
| _T = gr.themes.Soft(primary_hue="purple", secondary_hue="pink") | |
| _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}" | |
| def build_app(): | |
| with gr.Blocks(title="✨ AI Photo Studio", theme=_T, css=_CSS) as app: | |
| gr.HTML('<div class="hdr"><h1>✨ AI Photo Studio</h1><p>Upload any photo → Get enhanced result → Download PNG</p></div>') | |
| with gr.Row(): | |
| with gr.Column(): | |
| inp = gr.Image(label="📸 Upload your photo", type="pil", height=420, sources=["upload","clipboard"]) | |
| btn = gr.Button("✨ Enhance My Photo", variant="primary", size="lg") | |
| with gr.Column(): | |
| out = gr.Image(label="✨ Enhanced Result (PNG)", type="pil", height=420, format="png") | |
| st = gr.Markdown("*Upload a photo and click Enhance*") | |
| btn.click(fn=enhance, inputs=[inp], outputs=[out, st]) | |
| return app | |
| if __name__ == "__main__": | |
| app = build_app() | |
| app.launch(server_name="0.0.0.0", share=False, show_error=True) | |