AI Agent
update README for CPU basic hardware
ae5d943
Raw
History Blame Contribute Delete
23.6 kB
"""
✨ 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:
@staticmethod
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
@spaces.GPU(duration=5)
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)