upsclr / app.py
areebsatin's picture
Fix: monkey-patch gradio_client bool schema crash + bind 0.0.0.0 for HF Spaces
092f2f4
Raw
History Blame Contribute Delete
22.1 kB
"""4K Upscaler Pro — Premium AI-powered upscaler with Lumina Synth design."""
# ── Monkey-patch gradio_client to fix bool schema crash ─────────────
# Gradio 5.x generates JSON schemas with `additionalProperties: False`
# (a boolean). The gradio_client parser tries `"const" in schema` on it,
# which crashes with TypeError because you can't do `"x" in False`.
import gradio_client.utils as _gc_utils
_original_json_schema_to_python_type = _gc_utils._json_schema_to_python_type
def _patched_json_schema_to_python_type(schema, defs=None):
"""Safe wrapper that handles bool schemas."""
if isinstance(schema, bool):
return "Any"
# Guard against additionalProperties being bool
if isinstance(schema, dict) and "additionalProperties" in schema:
if isinstance(schema["additionalProperties"], bool):
schema = {k: v for k, v in schema.items() if k != "additionalProperties"}
return _original_json_schema_to_python_type(schema, defs)
_gc_utils._json_schema_to_python_type = _patched_json_schema_to_python_type
_original_get_type = _gc_utils.get_type
def _patched_get_type(schema):
"""Safe wrapper that handles bool schemas in get_type."""
if isinstance(schema, bool):
return "Any"
if not isinstance(schema, dict):
return "Any"
return _original_get_type(schema)
_gc_utils.get_type = _patched_get_type
# ── End monkey-patch ────────────────────────────────────────────────
import gradio as gr
from core import upscale_image, upscale_video
# ─────────────────────────────────────────────────────────────────────
# Lumina Synth Design System — Generated by Stitch
# ─────────────────────────────────────────────────────────────────────
CSS = """
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=Inter:wght@300;400;500;600;700&display=swap');
/* ── Design Tokens ── */
:root {
--bg: #0e0e13;
--surface: #131319;
--surface-high: #1f1f26;
--surface-highest: #25252d;
--surface-bright: #2c2b33;
--primary: #ba9eff;
--primary-dim: #8455ef;
--secondary: #53ddfc;
--secondary-dim: #40ceed;
--tertiary: #ec63ff;
--on-surface: #f9f5fd;
--on-surface-variant: #acaab1;
--outline: #76747b;
--outline-variant: #48474d;
--error: #ff6e84;
--gradient-primary: linear-gradient(135deg, #8455ef, #53ddfc);
--gradient-glow: linear-gradient(135deg, rgba(132,85,239,0.4), rgba(83,221,252,0.4));
--glass-bg: rgba(19, 19, 25, 0.75);
--glass-border: rgba(72, 71, 77, 0.25);
--radius-md: 16px;
--radius-lg: 24px;
--radius-full: 9999px;
--transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
/* ── Global Reset ── */
*, *::before, *::after { box-sizing: border-box; }
body, .gradio-container {
background: var(--bg) !important;
font-family: 'Inter', -apple-system, sans-serif !important;
color: var(--on-surface) !important;
min-height: 100vh;
}
.gradio-container {
max-width: 1400px !important;
margin: 0 auto !important;
padding: 0 24px !important;
}
/* ── Ambient Background Glow ── */
.gradio-container::before {
content: '';
position: fixed;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(ellipse at 30% 20%, rgba(132,85,239,0.08) 0%, transparent 50%),
radial-gradient(ellipse at 70% 80%, rgba(83,221,252,0.05) 0%, transparent 50%);
pointer-events: none;
z-index: 0;
}
/* ── Hero Title ── */
.hero-title {
text-align: center;
padding: 48px 0 8px;
}
.hero-title h1 {
font-family: 'Space Grotesk', sans-serif !important;
font-size: 3.2rem !important;
font-weight: 700 !important;
letter-spacing: -0.03em !important;
background: var(--gradient-primary) !important;
-webkit-background-clip: text !important;
-webkit-text-fill-color: transparent !important;
background-clip: text !important;
line-height: 1.1 !important;
margin: 0 !important;
padding: 0 !important;
}
.hero-subtitle p {
text-align: center !important;
color: var(--on-surface-variant) !important;
font-size: 1.05rem !important;
font-weight: 400 !important;
letter-spacing: 0.01em !important;
margin-top: 8px !important;
padding-bottom: 24px !important;
}
/* ── Badge ── */
.status-badge p {
display: inline-block !important;
background: rgba(132, 85, 239, 0.15) !important;
border: 1px solid rgba(186, 158, 255, 0.2) !important;
color: var(--primary) !important;
font-family: 'Inter', sans-serif !important;
font-size: 0.7rem !important;
font-weight: 600 !important;
text-transform: uppercase !important;
letter-spacing: 0.08em !important;
padding: 4px 14px !important;
border-radius: var(--radius-full) !important;
text-align: center !important;
width: auto !important;
margin: 0 auto !important;
}
/* ── Tabs — Glowing Indicators ── */
.tab-nav {
display: flex !important;
justify-content: center !important;
gap: 4px !important;
padding: 6px !important;
background: var(--surface) !important;
border-radius: var(--radius-full) !important;
border: 1px solid var(--glass-border) !important;
margin-bottom: 32px !important;
width: fit-content !important;
margin-left: auto !important;
margin-right: auto !important;
}
.tab-nav button {
font-family: 'Inter', sans-serif !important;
font-size: 0.875rem !important;
font-weight: 500 !important;
color: var(--on-surface-variant) !important;
background: transparent !important;
border: none !important;
padding: 10px 28px !important;
border-radius: var(--radius-full) !important;
cursor: pointer !important;
transition: var(--transition) !important;
position: relative !important;
}
.tab-nav button:hover {
color: var(--on-surface) !important;
background: rgba(132, 85, 239, 0.08) !important;
}
.tab-nav button.selected {
background: var(--gradient-primary) !important;
color: #fff !important;
font-weight: 600 !important;
box-shadow: 0 0 20px rgba(132, 85, 239, 0.4),
0 0 60px rgba(83, 221, 252, 0.15) !important;
}
/* ── Glass Panels ── */
.glass-panel {
background: var(--glass-bg) !important;
backdrop-filter: blur(24px) !important;
-webkit-backdrop-filter: blur(24px) !important;
border-radius: var(--radius-lg) !important;
border: 1px solid var(--glass-border) !important;
padding: 28px !important;
transition: var(--transition) !important;
position: relative !important;
overflow: hidden !important;
}
.glass-panel::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0;
height: 1px;
background: linear-gradient(90deg, transparent, rgba(186,158,255,0.3), transparent);
pointer-events: none;
}
.glass-panel:hover {
border-color: rgba(132, 85, 239, 0.2) !important;
box-shadow: 0 0 40px rgba(132, 85, 239, 0.06) !important;
}
/* ── Section Labels ── */
.section-label p {
font-family: 'Inter', sans-serif !important;
font-size: 0.7rem !important;
font-weight: 600 !important;
text-transform: uppercase !important;
letter-spacing: 0.1em !important;
color: var(--on-surface-variant) !important;
margin-bottom: 12px !important;
}
/* ── Input Components ── */
.gradio-image, .gradio-video {
border-radius: var(--radius-md) !important;
overflow: hidden !important;
}
.gradio-image .image-container,
.gradio-image .upload-container {
background: var(--surface) !important;
border: 1.5px dashed var(--outline-variant) !important;
border-radius: var(--radius-md) !important;
min-height: 280px !important;
transition: var(--transition) !important;
}
.gradio-image .upload-container:hover {
border-color: var(--secondary) !important;
background: var(--surface-high) !important;
box-shadow: 0 0 30px rgba(83, 221, 252, 0.08) !important;
}
/* ── Radio Buttons — Pill Style ── */
.gradio-radio label {
background: var(--surface-high) !important;
border: 1px solid transparent !important;
border-radius: var(--radius-full) !important;
padding: 8px 20px !important;
color: var(--on-surface-variant) !important;
font-size: 0.85rem !important;
font-weight: 500 !important;
cursor: pointer !important;
transition: var(--transition) !important;
}
.gradio-radio label:hover {
background: var(--surface-bright) !important;
color: var(--on-surface) !important;
}
.gradio-radio label.selected {
background: var(--primary-dim) !important;
color: #fff !important;
border-color: var(--primary) !important;
box-shadow: 0 0 15px rgba(132, 85, 239, 0.3) !important;
}
/* ── Dropdown ── */
.gradio-dropdown {
border-radius: var(--radius-md) !important;
}
.gradio-dropdown select, .gradio-dropdown input {
background: var(--surface-high) !important;
border: 1px solid var(--outline-variant) !important;
border-radius: var(--radius-md) !important;
color: var(--on-surface) !important;
padding: 10px 16px !important;
font-size: 0.875rem !important;
transition: var(--transition) !important;
}
.gradio-dropdown select:focus, .gradio-dropdown input:focus {
border-color: var(--primary) !important;
box-shadow: 0 0 0 3px rgba(132, 85, 239, 0.15) !important;
}
/* ── Primary Button — Gradient with Glow ── */
.gr-button-primary, button.primary {
background: var(--gradient-primary) !important;
border: none !important;
border-radius: var(--radius-full) !important;
color: #fff !important;
font-family: 'Inter', sans-serif !important;
font-weight: 600 !important;
font-size: 0.95rem !important;
letter-spacing: 0.02em !important;
padding: 14px 32px !important;
cursor: pointer !important;
transition: var(--transition) !important;
position: relative !important;
overflow: hidden !important;
text-shadow: 0 1px 2px rgba(0,0,0,0.3) !important;
}
.gr-button-primary::before, button.primary::before {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(135deg, rgba(255,255,255,0.15), transparent);
pointer-events: none;
}
.gr-button-primary:hover, button.primary:hover {
transform: translateY(-1px) !important;
box-shadow: 0 8px 30px rgba(132, 85, 239, 0.4),
0 0 60px rgba(83, 221, 252, 0.2) !important;
}
.gr-button-primary:active, button.primary:active {
transform: translateY(0) !important;
}
/* ── Secondary Button ── */
.gr-button-secondary, button.secondary {
background: transparent !important;
border: 1px solid var(--outline-variant) !important;
border-radius: var(--radius-full) !important;
color: var(--secondary) !important;
font-weight: 500 !important;
padding: 12px 24px !important;
transition: var(--transition) !important;
}
.gr-button-secondary:hover, button.secondary:hover {
border-color: var(--secondary) !important;
background: rgba(83, 221, 252, 0.08) !important;
box-shadow: 0 0 20px rgba(83, 221, 252, 0.15) !important;
}
/* ── Info Textbox ── */
.gradio-textbox textarea, .gradio-textbox input {
background: var(--surface) !important;
border: 1px solid var(--outline-variant) !important;
border-radius: var(--radius-md) !important;
color: var(--on-surface) !important;
font-family: 'Inter', monospace !important;
font-size: 0.85rem !important;
line-height: 1.6 !important;
}
/* ── File Download ── */
.gradio-file {
border-radius: var(--radius-md) !important;
}
/* ── Progress Bar — Neon Gradient ── */
.progress-bar {
background: var(--surface) !important;
border-radius: var(--radius-full) !important;
overflow: hidden !important;
}
.progress-bar > div {
background: var(--gradient-primary) !important;
box-shadow: 0 0 15px rgba(132, 85, 239, 0.5) !important;
transition: width 0.4s ease !important;
}
/* Gradio's built-in progress */
.progress-level {
background: var(--gradient-primary) !important;
}
.progress-level span {
color: #fff !important;
font-family: 'Inter', sans-serif !important;
font-weight: 500 !important;
}
.meta-text span {
color: var(--on-surface-variant) !important;
font-size: 0.85rem !important;
}
/* ── Loading Shimmer Animation ── */
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
@keyframes pulse-glow {
0%, 100% { box-shadow: 0 0 20px rgba(132, 85, 239, 0.2); }
50% { box-shadow: 0 0 40px rgba(132, 85, 239, 0.5), 0 0 80px rgba(83, 221, 252, 0.2); }
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.generating {
animation: pulse-glow 2s ease-in-out infinite !important;
}
/* ── Scrollbar ── */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: var(--bg); }
::-webkit-scrollbar-thumb {
background: var(--outline-variant);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover { background: var(--outline); }
/* ── Labels ── */
label, .label-wrap {
color: var(--on-surface-variant) !important;
font-weight: 500 !important;
font-size: 0.85rem !important;
}
/* ── Markdown in API tab ── */
.prose {
color: var(--on-surface) !important;
}
.prose h2, .prose h3 {
font-family: 'Space Grotesk', sans-serif !important;
color: var(--on-surface) !important;
}
.prose code {
background: var(--surface-high) !important;
color: var(--secondary) !important;
padding: 2px 6px !important;
border-radius: 6px !important;
font-size: 0.85em !important;
}
.prose pre {
background: var(--surface) !important;
border: 1px solid var(--outline-variant) !important;
border-radius: var(--radius-md) !important;
padding: 16px !important;
}
.prose pre code {
background: transparent !important;
color: var(--on-surface) !important;
}
.prose hr {
border-color: var(--outline-variant) !important;
opacity: 0.3 !important;
}
.prose strong {
color: var(--primary) !important;
}
/* ── Footer Hide ── */
footer { display: none !important; }
/* ── Result Card Styling ── */
.result-card {
background: var(--surface) !important;
border: 1px solid var(--glass-border) !important;
border-radius: var(--radius-lg) !important;
padding: 20px !important;
position: relative !important;
}
.result-card::after {
content: '';
position: absolute;
bottom: 0; left: 0; right: 0;
height: 1px;
background: linear-gradient(90deg, transparent, rgba(83, 221, 252, 0.3), transparent);
pointer-events: none;
}
"""
# ─────────────────────────────────────────────────────────────────────
# Build Gradio UI
# ─────────────────────────────────────────────────────────────────────
RESOLUTIONS = [
"4K (3840×2160)",
"2K (2560×1440)",
"1080p (1920×1080)",
"2x Original",
]
METHODS = ["Real-ESRGAN (AI)", "Lanczos (Fast)"]
def process_image(image_path, method, target, progress=gr.Progress()):
"""Wrapper that bridges Gradio progress to core function."""
return upscale_image(image_path, method, target, progress=progress)
def process_video(video_path, method, target, progress=gr.Progress()):
"""Wrapper that bridges Gradio progress to core function."""
return upscale_video(video_path, method, target, progress=progress)
with gr.Blocks(
css=CSS,
title="4K Upscaler Pro — AI Image & Video Enhancement",
theme=gr.themes.Base(
primary_hue=gr.themes.colors.purple,
secondary_hue=gr.themes.colors.cyan,
neutral_hue=gr.themes.colors.gray,
font=gr.themes.GoogleFont("Inter"),
font_mono=gr.themes.GoogleFont("Space Mono"),
),
) as demo:
# ── Hero Section ──
gr.Markdown("# ⬆ 4K Upscaler Pro", elem_classes=["hero-title"])
gr.Markdown(
"AI-powered image & video enhancement • Up to 4K resolution • REST API ready",
elem_classes=["hero-subtitle"],
)
gr.Markdown("✦ POWERED BY REAL-ESRGAN", elem_classes=["status-badge"])
with gr.Tabs():
# ━━━━━━━━━━━━ IMAGE TAB ━━━━━━━━━━━━
with gr.TabItem("🖼 Image Upscaler", id="image-tab"):
with gr.Row(equal_height=True):
with gr.Column(scale=1, elem_classes=["glass-panel"]):
gr.Markdown("**INPUT**", elem_classes=["section-label"])
img_input = gr.Image(
type="filepath",
label="Upload Image",
height=300,
sources=["upload", "clipboard"],
)
img_method = gr.Radio(
METHODS,
value="Lanczos (Fast)",
label="Upscaling Method",
)
img_target = gr.Dropdown(
RESOLUTIONS,
value="4K (3840×2160)",
label="Target Resolution",
)
img_btn = gr.Button(
"⬆ Upscale Image",
variant="primary",
size="lg",
)
with gr.Column(scale=1, elem_classes=["glass-panel"]):
gr.Markdown("**RESULT**", elem_classes=["section-label"])
img_output = gr.Image(
label="Upscaled Result",
height=300,
interactive=False,
)
img_info = gr.Textbox(
label="Processing Details",
interactive=False,
lines=3,
)
img_download = gr.File(label="⬇ Download PNG")
img_btn.click(
process_image,
inputs=[img_input, img_method, img_target],
outputs=[img_output, img_info, img_download],
)
# ━━━━━━━━━━━━ VIDEO TAB ━━━━━━━━━━━━
with gr.TabItem("🎬 Video Upscaler", id="video-tab"):
with gr.Row(equal_height=True):
with gr.Column(scale=1, elem_classes=["glass-panel"]):
gr.Markdown("**INPUT**", elem_classes=["section-label"])
vid_input = gr.Video(label="Upload Video", height=300)
vid_method = gr.Radio(
METHODS,
value="Lanczos (Fast)",
label="Upscaling Method",
)
vid_target = gr.Dropdown(
RESOLUTIONS,
value="4K (3840×2160)",
label="Target Resolution",
)
vid_btn = gr.Button(
"⬆ Upscale Video",
variant="primary",
size="lg",
)
with gr.Column(scale=1, elem_classes=["glass-panel"]):
gr.Markdown("**RESULT**", elem_classes=["section-label"])
vid_output = gr.Video(
label="Upscaled Result",
height=300,
interactive=False,
)
vid_info = gr.Textbox(
label="Processing Details",
interactive=False,
lines=3,
)
vid_btn.click(
process_video,
inputs=[vid_input, vid_method, vid_target],
outputs=[vid_output, vid_info],
)
# ━━━━━━━━━━━━ API DOCS TAB ━━━━━━━━━━━━
with gr.TabItem("📡 API Docs", id="api-tab"):
gr.Markdown("""
## REST API — Use from n8n, curl, or any HTTP client
Hugging Face Spaces expose a built-in API automatically.
---
### 🖼 Image Upscaling
**POST** `https://areebsa-upsclr.hf.space/api/predict`
```json
{
"data": [
"<base64_image_or_url>",
"Lanczos (Fast)",
"4K (3840×2160)"
]
}
```
**Response:**
```json
{
"data": [
"<upscaled_image_url>",
"✅ Upscaled: 1280×720 → 3840×2160",
"<download_url>"
]
}
```
---
### 🎬 Video Upscaling
**POST** `https://areebsa-upsclr.hf.space/api/predict`
Use `fn_index: 1` for the video endpoint.
```json
{
"fn_index": 1,
"data": [
"<video_url_or_path>",
"Lanczos (Fast)",
"4K (3840×2160)"
]
}
```
---
### 🔗 n8n / Zapier Integration
Use the **HTTP Request** node:
- **Method:** POST
- **URL:** `https://areebsa-upsclr.hf.space/api/predict`
- **Body:** JSON with `data` array (see above)
- **Headers:** `Content-Type: application/json`
For private spaces, add:
- **Header:** `Authorization: Bearer <your_HF_token>`
""")
# ── Launch ──
if __name__ == "__main__":
demo.queue(max_size=5).launch(
ssr_mode=False,
server_name="0.0.0.0",
server_port=7860,
)