| """ |
| Minecraftify! — turn any photo into a Minecraft-style scene. |
| |
| Gradio UI with FLUX.2 diffusion model + Minecraft LoRA for converting images to Minecraft style. |
| """ |
|
|
| import os |
| import sys |
| import logging |
| import threading |
| import time |
| import torch |
| import gradio as gr |
| import spaces |
| from PIL import Image |
| from huggingface_hub import snapshot_download |
| from diffusers import DiffusionPipeline |
|
|
| logging.basicConfig( |
| level=logging.INFO, |
| format="[%(asctime)s] [%(levelname)s] %(message)s", |
| force=True, |
| ) |
| logger = logging.getLogger("minecraftify") |
|
|
|
|
| def log_event(message: str): |
| logger.info(message) |
| print(f"[minecraftify] {message}", flush=True) |
|
|
|
|
| _original_unraisablehook = sys.unraisablehook |
|
|
|
|
| def _ignore_asyncio_closed_fd_warning(unraisable): |
| object_name = getattr(unraisable.object, "__qualname__", "") |
| is_asyncio_loop_cleanup = object_name == "BaseEventLoop.__del__" |
| is_closed_fd_warning = ( |
| unraisable.exc_type is ValueError |
| and "Invalid file descriptor: -1" in str(unraisable.exc_value) |
| ) |
|
|
| if is_asyncio_loop_cleanup and is_closed_fd_warning: |
| return |
|
|
| _original_unraisablehook(unraisable) |
|
|
|
|
| sys.unraisablehook = _ignore_asyncio_closed_fd_warning |
| log_event("App module imported") |
|
|
| |
| |
| |
|
|
| CUSTOM_CSS = """ |
| /* ══════════════════════════════════════════════════════════════════════ |
| MINECRAFT THEME — Premium Pixel-Art UI |
| ══════════════════════════════════════════════════════════════════════ */ |
| |
| /* ── Fonts ── */ |
| @import url('https://fonts.googleapis.com/css2?family=Silkscreen:wght@400;700&display=swap'); |
| @import url('https://fonts.googleapis.com/css2?family=VT323&display=swap'); |
| @import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap'); |
| |
| /* ── CSS Variables ── */ |
| :root { |
| --mc-green: #5D8C3E; |
| --mc-green-light: #7CB342; |
| --mc-green-dark: #3B5E28; |
| --mc-grass-top: #6DBE45; |
| --mc-dirt: #8B6914; |
| --mc-dirt-dark: #6B4F10; |
| --mc-dirt-light: #9B7924; |
| --mc-stone: #7F7F7F; |
| --mc-stone-dark: #5A5A5A; |
| --mc-stone-light: #999999; |
| --mc-cobble: #6E6E6E; |
| --mc-sky-day: #78A7FF; |
| --mc-sky-horizon: #B4D4FF; |
| --mc-text: #FFFFFF; |
| --mc-text-shadow: #3F3F00; |
| --mc-gold: #FFAA00; |
| --mc-gold-dark: #CC8800; |
| --mc-red: #FF5555; |
| --mc-aqua: #55FFFF; |
| --mc-dark-bg: #1E1E1E; |
| --mc-panel-bg: rgba(0, 0, 0, 0.72); |
| --mc-panel-inner: rgba(0, 0, 0, 0.35); |
| --mc-border: #1A1A1A; |
| --mc-border-light: #3A3A3A; |
| --mc-highlight: #C6C6C6; |
| --mc-inventory-slot: #8B8B8B; |
| --mc-inventory-inner: #373737; |
| } |
| |
| /* ── Animated Panorama Background ── */ |
| .gradio-container { |
| background: |
| linear-gradient(180deg, |
| #78A7FF 0%, |
| #B4D4FF 30%, |
| #C8E0FF 38%, |
| #6DBE45 38.5%, |
| #5D8C3E 40%, |
| #3B5E28 42%, |
| #8B6914 42.5%, |
| #7A5B10 55%, |
| #6B4F10 70%, |
| #5A4210 85%, |
| #3D2D0A 100% |
| ) !important; |
| background-attachment: fixed !important; |
| font-family: 'VT323', monospace !important; |
| min-height: 100vh; |
| position: relative; |
| overflow-x: hidden; |
| } |
| |
| /* ── Floating Block Particles (Pure CSS) ── */ |
| .gradio-container::before { |
| content: ''; |
| position: fixed; |
| top: 0; left: 0; |
| width: 100%; height: 100%; |
| pointer-events: none; |
| z-index: 0; |
| background-image: |
| radial-gradient(2px 2px at 10% 20%, rgba(255,255,255,0.4) 50%, transparent 50%), |
| radial-gradient(2px 2px at 30% 60%, rgba(255,255,255,0.3) 50%, transparent 50%), |
| radial-gradient(2px 2px at 50% 10%, rgba(255,255,255,0.5) 50%, transparent 50%), |
| radial-gradient(2px 2px at 70% 40%, rgba(255,255,255,0.35) 50%, transparent 50%), |
| radial-gradient(2px 2px at 90% 80%, rgba(255,255,255,0.4) 50%, transparent 50%), |
| radial-gradient(3px 3px at 20% 85%, rgba(255,255,255,0.2) 50%, transparent 50%), |
| radial-gradient(2px 2px at 80% 15%, rgba(255,255,255,0.45) 50%, transparent 50%), |
| radial-gradient(2px 2px at 45% 75%, rgba(255,255,255,0.25) 50%, transparent 50%); |
| animation: sparkle 4s ease-in-out infinite alternate; |
| } |
| |
| @keyframes sparkle { |
| 0% { opacity: 0.4; } |
| 50% { opacity: 0.8; } |
| 100% { opacity: 0.5; } |
| } |
| |
| /* ── All content above particles ── */ |
| .gradio-container > * { |
| position: relative; |
| z-index: 1; |
| } |
| |
| /* ── Main wrapper ── */ |
| .main-wrap { |
| max-width: 1150px; |
| margin: 0 auto; |
| } |
| |
| /* ══════════════════════════════════════════════════════════════════════ |
| TITLE AREA — Minecraft Logo Style |
| ══════════════════════════════════════════════════════════════════════ */ |
| |
| .mc-title-area { |
| text-align: center; |
| padding: 35px 20px 10px 20px; |
| position: relative; |
| } |
| |
| .mc-title-area h1 { |
| font-family: 'Silkscreen', cursive !important; |
| font-size: 3.5rem !important; |
| color: #FFFFFF !important; |
| text-shadow: |
| 4px 4px 0px #3F3F00, |
| -2px -2px 0px #000, |
| 2px -2px 0px #000, |
| -2px 2px 0px #000, |
| 2px 2px 0px #000, |
| 0px 0px 20px rgba(109, 190, 69, 0.4) !important; |
| margin: 0 !important; |
| letter-spacing: 3px; |
| line-height: 1.2 !important; |
| animation: title-float 3s ease-in-out infinite; |
| } |
| |
| @keyframes title-float { |
| 0%, 100% { transform: translateY(0px); } |
| 50% { transform: translateY(-6px); } |
| } |
| |
| .mc-subtitle { |
| font-family: 'VT323', monospace !important; |
| font-size: 1.4rem !important; |
| color: #E8E8E8 !important; |
| text-shadow: 1px 1px 3px rgba(0,0,0,0.9) !important; |
| margin-top: 10px !important; |
| max-width: 600px; |
| margin-left: auto !important; |
| margin-right: auto !important; |
| line-height: 1.4 !important; |
| letter-spacing: 0.5px; |
| } |
| |
| .mc-version-tag { |
| display: inline-block; |
| font-family: 'Press Start 2P', cursive !important; |
| font-size: 0.55rem !important; |
| color: var(--mc-gold) !important; |
| background: rgba(0,0,0,0.6); |
| border: 1px solid var(--mc-gold-dark); |
| padding: 4px 10px; |
| margin-top: 10px; |
| letter-spacing: 1px; |
| animation: gold-glow 2s ease-in-out infinite alternate; |
| } |
| |
| @keyframes gold-glow { |
| 0% { box-shadow: 0 0 5px rgba(255, 170, 0, 0.2); } |
| 100% { box-shadow: 0 0 15px rgba(255, 170, 0, 0.5); } |
| } |
| |
| /* ══════════════════════════════════════════════════════════════════════ |
| PANELS — Minecraft Inventory Style |
| ══════════════════════════════════════════════════════════════════════ */ |
| |
| .mc-panel { |
| background: var(--mc-panel-bg) !important; |
| border: 3px solid var(--mc-border) !important; |
| border-radius: 0px !important; |
| box-shadow: |
| inset 2px 2px 0px rgba(255,255,255,0.08), |
| inset -2px -2px 0px rgba(0,0,0,0.4), |
| 6px 6px 0px rgba(0,0,0,0.5), |
| 0 0 40px rgba(0,0,0,0.3) !important; |
| padding: 20px !important; |
| position: relative; |
| overflow: hidden; |
| } |
| |
| /* Subtle noise texture overlay on panels */ |
| .mc-panel::before { |
| content: ''; |
| position: absolute; |
| top: 0; left: 0; |
| width: 100%; height: 100%; |
| background-image: |
| repeating-linear-gradient( |
| 0deg, |
| transparent, |
| transparent 2px, |
| rgba(255,255,255,0.015) 2px, |
| rgba(255,255,255,0.015) 4px |
| ), |
| repeating-linear-gradient( |
| 90deg, |
| transparent, |
| transparent 2px, |
| rgba(255,255,255,0.01) 2px, |
| rgba(255,255,255,0.01) 4px |
| ); |
| pointer-events: none; |
| image-rendering: pixelated; |
| } |
| |
| /* ── Inner groups ── */ |
| .gradio-group { |
| background: var(--mc-panel-inner) !important; |
| border: 2px solid var(--mc-border-light) !important; |
| border-radius: 0px !important; |
| } |
| |
| /* ══════════════════════════════════════════════════════════════════════ |
| SECTION HEADERS — Pixel Label Style |
| ══════════════════════════════════════════════════════════════════════ */ |
| |
| .mc-section-label { |
| font-family: 'Silkscreen', cursive !important; |
| font-size: 1rem !important; |
| color: var(--mc-gold) !important; |
| text-shadow: 2px 2px 0px rgba(0,0,0,0.8) !important; |
| letter-spacing: 1.5px; |
| padding: 6px 14px; |
| margin-bottom: 10px; |
| background: linear-gradient(90deg, rgba(255,170,0,0.12) 0%, transparent 100%); |
| border-left: 3px solid var(--mc-gold); |
| display: flex; |
| align-items: center; |
| gap: 8px; |
| } |
| |
| .mc-section-label .mc-icon { |
| font-size: 1.6rem; |
| filter: drop-shadow(1px 1px 0px rgba(0,0,0,0.6)); |
| } |
| |
| /* ══════════════════════════════════════════════════════════════════════ |
| LABELS & TEXT |
| ══════════════════════════════════════════════════════════════════════ */ |
| |
| .gradio-container label, |
| .gradio-container .label-wrap span { |
| font-family: 'VT323', monospace !important; |
| font-size: 1.2rem !important; |
| color: var(--mc-gold) !important; |
| text-shadow: 1px 1px 0px rgba(0,0,0,0.7) !important; |
| letter-spacing: 1px; |
| } |
| |
| .gradio-container .gr-prose p, |
| .gradio-container .gr-prose li { |
| font-family: 'VT323', monospace !important; |
| color: #C8C8C8 !important; |
| } |
| |
| /* ══════════════════════════════════════════════════════════════════════ |
| BUTTON — Minecraft Stone Button (Authentic) |
| ══════════════════════════════════════════════════════════════════════ */ |
| |
| .mc-btn-primary, .gr-button-primary, button.primary { |
| font-family: 'Silkscreen', cursive !important; |
| font-size: 1.15rem !important; |
| letter-spacing: 1px; |
| background: |
| linear-gradient(180deg, |
| #9A9A9A 0%, |
| #888888 20%, |
| #6A6A6A 45%, |
| #585858 55%, |
| #484848 80%, |
| #3A3A3A 100% |
| ) !important; |
| color: var(--mc-text) !important; |
| border: 3px solid #000 !important; |
| border-radius: 0px !important; |
| box-shadow: |
| inset 2px 2px 0px rgba(255,255,255,0.3), |
| inset -2px -2px 0px rgba(0,0,0,0.5), |
| 0 4px 0px #222 !important; |
| text-shadow: 2px 2px 0px #3F3F00 !important; |
| padding: 14px 40px !important; |
| cursor: pointer; |
| transition: all 0.05s ease !important; |
| text-transform: none !important; |
| position: relative; |
| image-rendering: pixelated; |
| } |
| |
| .mc-btn-primary:hover, .gr-button-primary:hover, button.primary:hover { |
| background: |
| linear-gradient(180deg, |
| #B0B0B0 0%, |
| #A0A0A0 20%, |
| #888888 45%, |
| #787878 55%, |
| #686868 80%, |
| #585858 100% |
| ) !important; |
| color: #FFFFAA !important; |
| box-shadow: |
| inset 2px 2px 0px rgba(255,255,255,0.4), |
| inset -2px -2px 0px rgba(0,0,0,0.4), |
| 0 4px 0px #222 !important; |
| } |
| |
| .mc-btn-primary:active, .gr-button-primary:active, button.primary:active { |
| background: |
| linear-gradient(180deg, |
| #4A4A4A 0%, |
| #3A3A3A 50%, |
| #5A5A5A 100% |
| ) !important; |
| box-shadow: |
| inset -2px -2px 0px rgba(255,255,255,0.15), |
| inset 2px 2px 0px rgba(0,0,0,0.5), |
| 0 1px 0px #222 !important; |
| transform: translateY(3px); |
| } |
| |
| /* ══════════════════════════════════════════════════════════════════════ |
| IMAGE UPLOAD — Crafting Table Slot Style |
| ══════════════════════════════════════════════════════════════════════ */ |
| |
| .mc-upload-area { |
| position: relative; |
| } |
| |
| .image-upload, .gr-image, .gr-file { |
| border: 3px solid var(--mc-inventory-inner) !important; |
| border-radius: 0px !important; |
| background: rgba(0,0,0,0.55) !important; |
| box-shadow: |
| inset 2px 2px 0px rgba(0,0,0,0.5), |
| inset -2px -2px 0px rgba(139,139,139,0.2) !important; |
| transition: border-color 0.2s ease, box-shadow 0.2s ease; |
| image-rendering: pixelated; |
| } |
| |
| .image-upload:hover, .gr-image:hover { |
| border-color: var(--mc-gold) !important; |
| box-shadow: |
| inset 2px 2px 0px rgba(0,0,0,0.5), |
| inset -2px -2px 0px rgba(139,139,139,0.2), |
| 0 0 15px rgba(255, 170, 0, 0.2) !important; |
| } |
| |
| /* ══════════════════════════════════════════════════════════════════════ |
| INPUT FIELDS — Minecraft Sign / Book Style |
| ══════════════════════════════════════════════════════════════════════ */ |
| |
| textarea, input[type="text"], input[type="number"] { |
| font-family: 'VT323', monospace !important; |
| font-size: 1.15rem !important; |
| background: rgba(0,0,0,0.6) !important; |
| color: var(--mc-text) !important; |
| border: 2px solid var(--mc-inventory-inner) !important; |
| border-radius: 0px !important; |
| box-shadow: inset 1px 1px 0px rgba(0,0,0,0.4) !important; |
| image-rendering: pixelated; |
| } |
| |
| textarea:focus, input[type="text"]:focus, input[type="number"]:focus { |
| border-color: var(--mc-gold) !important; |
| outline: none !important; |
| box-shadow: 0 0 0 1px var(--mc-gold), inset 1px 1px 0px rgba(0,0,0,0.4) !important; |
| } |
| |
| textarea::placeholder, input::placeholder { |
| color: #666 !important; |
| font-family: 'VT323', monospace !important; |
| } |
| |
| /* ══════════════════════════════════════════════════════════════════════ |
| SLIDERS — Minecraft XP Bar Style |
| ══════════════════════════════════════════════════════════════════════ */ |
| |
| input[type="range"] { |
| accent-color: var(--mc-green-light) !important; |
| height: 6px; |
| } |
| |
| input[type="range"]::-webkit-slider-thumb { |
| background: var(--mc-green-light) !important; |
| border: 2px solid #000 !important; |
| width: 16px; |
| height: 16px; |
| border-radius: 0px !important; |
| image-rendering: pixelated; |
| } |
| |
| /* ══════════════════════════════════════════════════════════════════════ |
| ACCORDION — Chest Opening Style |
| ══════════════════════════════════════════════════════════════════════ */ |
| |
| .gradio-accordion { |
| background: rgba(0,0,0,0.3) !important; |
| border: 2px solid var(--mc-border-light) !important; |
| border-radius: 0px !important; |
| overflow: hidden; |
| } |
| |
| .gradio-accordion .label-wrap { |
| background: rgba(0,0,0,0.4) !important; |
| border-radius: 0px !important; |
| padding: 10px 14px !important; |
| transition: background 0.2s ease; |
| } |
| |
| .gradio-accordion .label-wrap:hover { |
| background: rgba(255,170,0,0.1) !important; |
| } |
| |
| .gradio-accordion .label-wrap span { |
| font-family: 'VT323', monospace !important; |
| font-size: 1.15rem !important; |
| color: var(--mc-highlight) !important; |
| } |
| |
| /* ══════════════════════════════════════════════════════════════════════ |
| STATUS / GENERATING — Enchantment Glow |
| ══════════════════════════════════════════════════════════════════════ */ |
| |
| .generating { |
| border-color: var(--mc-aqua) !important; |
| animation: enchant-glow 1.5s ease-in-out infinite alternate !important; |
| } |
| |
| @keyframes enchant-glow { |
| 0% { |
| box-shadow: 0 0 8px rgba(85, 255, 255, 0.3), |
| inset 0 0 8px rgba(85, 255, 255, 0.1); |
| } |
| 100% { |
| box-shadow: 0 0 25px rgba(85, 255, 255, 0.5), |
| inset 0 0 15px rgba(85, 255, 255, 0.2); |
| } |
| } |
| |
| /* ══════════════════════════════════════════════════════════════════════ |
| TIPS SECTION — Book & Quill Style |
| ══════════════════════════════════════════════════════════════════════ */ |
| |
| .mc-tips { |
| background: rgba(0,0,0,0.55) !important; |
| border: 2px solid var(--mc-inventory-inner) !important; |
| border-radius: 0px !important; |
| padding: 18px 22px !important; |
| margin-top: 16px; |
| box-shadow: |
| inset 1px 1px 0px rgba(255,255,255,0.05), |
| inset -1px -1px 0px rgba(0,0,0,0.3) !important; |
| } |
| |
| .mc-tips p, .mc-tips li { |
| font-family: 'VT323', monospace !important; |
| color: #B8B8B8 !important; |
| font-size: 1.1rem !important; |
| text-shadow: 1px 1px 0px rgba(0,0,0,0.5) !important; |
| line-height: 1.6 !important; |
| } |
| |
| .mc-tips strong, .mc-tips .mc-tip-title { |
| color: var(--mc-gold) !important; |
| font-family: 'Silkscreen', cursive !important; |
| font-size: 0.9rem !important; |
| } |
| |
| .mc-tips li::marker { |
| color: var(--mc-green-light); |
| } |
| |
| /* ══════════════════════════════════════════════════════════════════════ |
| FOOTER |
| ══════════════════════════════════════════════════════════════════════ */ |
| |
| .mc-footer { |
| text-align: center; |
| padding: 20px 10px 30px; |
| } |
| |
| .mc-footer p { |
| font-family: 'VT323', monospace !important; |
| color: rgba(255,255,255,0.35) !important; |
| font-size: 1rem !important; |
| letter-spacing: 0.5px; |
| } |
| |
| .mc-footer a { |
| color: var(--mc-gold) !important; |
| text-decoration: none; |
| } |
| |
| /* ══════════════════════════════════════════════════════════════════════ |
| DIVIDER — Bedrock Layer |
| ══════════════════════════════════════════════════════════════════════ */ |
| |
| .mc-divider { |
| height: 4px; |
| background: repeating-linear-gradient( |
| 90deg, |
| #2A2A2A 0px, |
| #2A2A2A 8px, |
| #1A1A1A 8px, |
| #1A1A1A 16px, |
| #333333 16px, |
| #333333 24px, |
| #1A1A1A 24px, |
| #1A1A1A 32px |
| ); |
| margin: 16px 0; |
| image-rendering: pixelated; |
| } |
| |
| /* ══════════════════════════════════════════════════════════════════════ |
| ARROW BETWEEN COLUMNS — Crafting Arrow |
| ══════════════════════════════════════════════════════════════════════ */ |
| |
| .mc-arrow { |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| font-size: 2.5rem; |
| color: var(--mc-highlight); |
| text-shadow: 2px 2px 0px rgba(0,0,0,0.7); |
| animation: arrow-pulse 1.5s ease-in-out infinite; |
| padding-top: 160px; |
| } |
| |
| @keyframes arrow-pulse { |
| 0%, 100% { opacity: 0.6; transform: translateX(0px); } |
| 50% { opacity: 1; transform: translateX(4px); } |
| } |
| |
| /* ══════════════════════════════════════════════════════════════════════ |
| RESPONSIVE |
| ══════════════════════════════════════════════════════════════════════ */ |
| |
| @media (max-width: 768px) { |
| .mc-title-area h1 { |
| font-size: 2rem !important; |
| } |
| .mc-arrow { |
| padding-top: 10px; |
| transform: rotate(90deg); |
| } |
| .mc-section-label { |
| font-size: 0.85rem !important; |
| } |
| } |
| |
| /* ══════════════════════════════════════════════════════════════════════ |
| SCROLLBAR — Stone Brick Style |
| ══════════════════════════════════════════════════════════════════════ */ |
| |
| ::-webkit-scrollbar { |
| width: 12px; |
| } |
| ::-webkit-scrollbar-track { |
| background: #2A2A2A; |
| border-left: 1px solid #1A1A1A; |
| } |
| ::-webkit-scrollbar-thumb { |
| background: linear-gradient(180deg, #777 0%, #555 100%); |
| border: 2px solid #1A1A1A; |
| } |
| ::-webkit-scrollbar-thumb:hover { |
| background: linear-gradient(180deg, #999 0%, #777 100%); |
| } |
| |
| /* ── Dark mode override fix ── */ |
| .dark .gradio-container, |
| .dark { |
| background: linear-gradient(180deg, |
| #78A7FF 0%, #B4D4FF 30%, #C8E0FF 38%, |
| #6DBE45 38.5%, #5D8C3E 40%, #3B5E28 42%, |
| #8B6914 42.5%, #7A5B10 55%, #6B4F10 70%, |
| #5A4210 85%, #3D2D0A 100% |
| ) !important; |
| } |
| """ |
|
|
|
|
| |
| |
| |
|
|
| BASE_MODEL_ID = "black-forest-labs/FLUX.2-klein-4B" |
| LORA_ID = "AnimeOverlord/flux2-klein-4b-mc-v2" |
|
|
| |
| SPACE_DATA_DIR = "/data" |
|
|
| |
| HF_HOME = os.path.join(SPACE_DATA_DIR, ".huggingface") |
| HF_HUB_CACHE = os.path.join(HF_HOME, "hub") |
|
|
| |
| MODEL_ROOT = os.path.join(SPACE_DATA_DIR, "models") |
| MODEL_DIR = os.path.join(MODEL_ROOT, "FLUX.2-klein-4B") |
| import tempfile |
| import shutil |
|
|
| LORA_DIR = os.path.join(tempfile.gettempdir(), "flux2-klein-4b-mc-v2") |
| LORA_CACHE_DIR = os.path.join(tempfile.gettempdir(), "hf-lora-cache") |
|
|
| |
| os.environ["HF_HOME"] = HF_HOME |
| os.environ["HF_HUB_CACHE"] = HF_HUB_CACHE |
|
|
| os.makedirs(MODEL_DIR, exist_ok=True) |
| os.makedirs(LORA_DIR, exist_ok=True) |
|
|
| _pipe = None |
| _pipe_ready = False |
| _live_lock = threading.Lock() |
| _live_latest_frame = None |
| _live_latest_frame_id = 0 |
| _live_capture_count = 0 |
| _live_active = False |
|
|
| IMAGE_MODE = "Image" |
| LIVE_MODE = "Live Camera" |
| GRADIO_MAJOR = int(gr.__version__.split(".", 1)[0]) |
|
|
| MC_THEME = gr.themes.Base( |
| primary_hue="green", |
| neutral_hue="stone", |
| font=gr.themes.GoogleFont("VT323"), |
| font_mono=gr.themes.GoogleFont("VT323"), |
| ) |
|
|
| BLOCKS_KWARGS = {"title": "Minecraftify! ⛏️"} |
| LAUNCH_KWARGS = {} |
|
|
| if GRADIO_MAJOR >= 6: |
| LAUNCH_KWARGS.update({"css": CUSTOM_CSS, "theme": MC_THEME, "ssr_mode": False}) |
| else: |
| BLOCKS_KWARGS.update({"css": CUSTOM_CSS, "theme": MC_THEME}) |
|
|
| log_event(f"Gradio version detected: {gr.__version__}") |
| log_event(f"Blocks kwargs: {BLOCKS_KWARGS}") |
| log_event(f"Launch kwargs: {LAUNCH_KWARGS}") |
|
|
| def _ensure_model_files(): |
| """Download models if not already cached.""" |
| log_event("Checking model files...") |
|
|
| os.makedirs(MODEL_DIR, exist_ok=True) |
|
|
| base_marker = os.path.join(MODEL_DIR, "model_index.json") |
|
|
| if not os.path.exists(base_marker): |
| log_event(f"Downloading base model: {BASE_MODEL_ID}") |
| snapshot_download( |
| repo_id=BASE_MODEL_ID, |
| local_dir=MODEL_DIR, |
| local_dir_use_symlinks=False, |
| ) |
| log_event(f"Base model downloaded to {MODEL_DIR}") |
| else: |
| log_event(f"Base model already cached at {MODEL_DIR}") |
|
|
| |
| shutil.rmtree(LORA_DIR, ignore_errors=True) |
| shutil.rmtree(LORA_CACHE_DIR, ignore_errors=True) |
| os.makedirs(LORA_DIR, exist_ok=True) |
| os.makedirs(LORA_CACHE_DIR, exist_ok=True) |
|
|
| log_event(f"Downloading LoRA fresh to temp: {LORA_ID}") |
| snapshot_download( |
| repo_id=LORA_ID, |
| local_dir=LORA_DIR, |
| cache_dir=LORA_CACHE_DIR, |
| local_dir_use_symlinks=False, |
| allow_patterns=["pytorch_lora_weights.safetensors"], |
| ) |
| log_event(f"LoRA downloaded to temp: {LORA_DIR}") |
|
|
| def _load_pipe(): |
| global _pipe, _pipe_ready |
|
|
| if _pipe_ready and _pipe is not None: |
| log_event("Pipeline already loaded") |
| return _pipe |
|
|
| log_event("Loading pipeline") |
| _ensure_model_files() |
|
|
| pipe = DiffusionPipeline.from_pretrained( |
| MODEL_DIR, |
| torch_dtype=torch.bfloat16, |
| device_map="cuda", |
| ) |
|
|
| pipe.load_lora_weights( |
| LORA_DIR, |
| weight_name="pytorch_lora_weights.safetensors", |
| ) |
|
|
| |
| for module_name in ["transformer", "vae", "text_encoder", "image_encoder"]: |
| if hasattr(pipe, module_name): |
| getattr(pipe, module_name).to(dtype=torch.bfloat16) |
|
|
| pipe.set_progress_bar_config(disable=False) |
|
|
| _pipe = pipe |
| _pipe_ready = True |
| log_event("Pipeline ready") |
| return pipe |
|
|
|
|
|
|
| |
| |
| |
|
|
| def _minecraftify_image( |
| image: Image.Image, |
| steps: int, |
| guidance_scale: float, |
| seed: int, |
| extra_details: str, |
| ): |
| log_event( |
| f"Starting inference: size={getattr(image, 'size', None)}, " |
| f"steps={steps}, guidance={guidance_scale}, seed={seed}" |
| ) |
| pipe = _load_pipe() |
| image = image.convert("RGB") |
|
|
| prompt = ( |
| "Convert this image into a faithful vanilla Minecraft version of the same scene. " |
| "Preserve the original composition, camera angle, layout, all existing objects and especially colour and patterns. " |
| "Recreate the scene using only vanilla Minecraft blocks, textures, materials, and lighting. " |
| "Turn terrain into blocky Minecraft voxels with visible voxel structure and stepped geometry. " |
| "Replace realistic surfaces with cubic Minecraft blocks and pixelated textures. " |
| "Convert people, animals, and other living things into custom Minecraft mobs while preserving their pose, placement, colours and patterns. " |
| "Let human faces be in the style of Minecraft characters, cuboid structure. " |
| "Keep the result visually consistent with an authentic vanilla Minecraft screenshot. " |
| "MOST IMPORTANT THING IS PRESERVE THE IMAGE CONTENTS(colours etc) AND REMOVE EXTRA ITEMS." |
| ) |
|
|
| if extra_details and extra_details.strip(): |
| prompt += f"\n\nAdditional scene notes: {extra_details.strip()}" |
|
|
| generator = torch.Generator(device="cuda").manual_seed(int(seed)) |
|
|
| with torch.inference_mode(): |
| out = pipe( |
| image=image, |
| prompt=prompt, |
| num_inference_steps=int(steps), |
| guidance_scale=float(guidance_scale), |
| generator=generator, |
| height=512, |
| width=896 |
| ) |
|
|
| log_event("Inference complete") |
| output_img = out.images[0] |
| log_event(f"Output image size: {output_img.size}") |
| torch.cuda.empty_cache() |
| return output_img |
|
|
|
|
| @spaces.GPU(duration=120) |
| def minecraftify( |
| image: Image.Image, |
| steps: int, |
| guidance_scale: float, |
| seed: int, |
| extra_details: str, |
| ): |
| if image is None: |
| log_event("Still image inference blocked: no image") |
| raise gr.Error("⛏️ Please upload a photo first!") |
|
|
| log_event("Still image inference requested") |
| return _minecraftify_image(image, steps, guidance_scale, seed, extra_details) |
|
|
|
|
| @spaces.GPU(duration=240) |
| def _minecraftify_live_frame( |
| frame: Image.Image, |
| steps: int, |
| guidance_scale: float, |
| seed: int, |
| extra_details: str, |
| ): |
| """GPU-wrapped inference for individual live frames.""" |
| log_event(f"GPU-allocated: Processing live frame with steps={steps}") |
| return _minecraftify_image(frame, steps, guidance_scale, seed, extra_details) |
|
|
|
|
| def capture_live_frame(image: Image.Image): |
| global _live_latest_frame, _live_latest_frame_id, _live_capture_count |
|
|
| if image is None: |
| return None |
|
|
| with _live_lock: |
| _live_latest_frame = image.copy() |
| _live_latest_frame_id += 1 |
| _live_capture_count += 1 |
| frame_id = _live_latest_frame_id |
| capture_count = _live_capture_count |
|
|
| if capture_count == 1 or capture_count % 30 == 0: |
| log_event(f"Captured live webcam frame id={frame_id}, size={getattr(image, 'size', None)}") |
|
|
| return None |
|
|
|
|
| def minecraftify_live_loop( |
| steps: int, |
| guidance_scale: float, |
| seed: int, |
| extra_details: str, |
| ): |
| log_event("Live GPU loop started") |
| last_processed_frame_id = 0 |
|
|
| while True: |
| with _live_lock: |
| is_active = _live_active |
| frame = _live_latest_frame.copy() if _live_latest_frame is not None else None |
| frame_id = _live_latest_frame_id |
|
|
| if not is_active: |
| log_event("Live GPU loop stopped") |
| return |
|
|
| if frame is None: |
| time.sleep(0.1) |
| continue |
|
|
| if frame_id == last_processed_frame_id: |
| time.sleep(0.02) |
| continue |
|
|
| last_processed_frame_id = frame_id |
| log_event(f"Processing newest live frame id={frame_id}") |
| output = _minecraftify_live_frame(frame, steps, guidance_scale, seed, extra_details) |
| yield output |
|
|
|
|
| def _set_live_active(image: Image.Image): |
| """Auto-enable live processing when recording starts.""" |
| global _live_active |
| if image is not None and not _live_active: |
| _live_active = True |
| log_event("Live recording detected, auto-starting processing") |
| return None |
|
|
|
|
| def switch_input_mode(mode: str): |
| global _live_active |
|
|
| log_event(f"Input mode changed: {mode}") |
| image_mode = mode == IMAGE_MODE |
| if image_mode: |
| _live_active = False |
|
|
| return ( |
| gr.update(visible=image_mode), |
| gr.update(visible=image_mode), |
| gr.update(visible=not image_mode), |
| gr.update(interactive=True), |
| gr.update(visible=not image_mode, interactive=False), |
| False, |
| ) |
|
|
|
|
| |
| |
| |
|
|
| log_event("Building Gradio UI") |
|
|
| with gr.Blocks(**BLOCKS_KWARGS) as demo: |
|
|
| |
| gr.HTML(""" |
| <div class="mc-title-area"> |
| <h1>⛏️ MINECRAFTIFY!</h1> |
| <p class="mc-subtitle"> |
| Upload any photo and watch it transform into a |
| blocky, pixel-art Minecraft masterpiece |
| </p> |
| <div class="mc-version-tag">✦ POWERED BY FLUX.2 + MINECRAFT LORA ✦</div> |
| </div> |
| """) |
|
|
| |
| with gr.Row(elem_classes="main-row"): |
|
|
| |
| with gr.Column(scale=5): |
| gr.HTML('<div class="mc-section-label"><span class="mc-icon">📸</span> INPUT IMAGE</div>') |
|
|
| with gr.Group(elem_classes="mc-panel"): |
| input_mode = gr.Dropdown( |
| choices=[IMAGE_MODE, LIVE_MODE], |
| value=IMAGE_MODE, |
| label="🎮 Input mode", |
| interactive=True, |
| ) |
|
|
| with gr.Group(visible=True) as image_input_group: |
| input_image = gr.Image( |
| type="pil", |
| label="Drop your image here", |
| sources=["upload", "webcam", "clipboard"], |
| elem_classes="image-upload mc-upload-area", |
| height=400, |
| ) |
|
|
| with gr.Group(visible=False) as live_input_group: |
| live_image = gr.Image( |
| type="pil", |
| label="Live camera", |
| sources=["webcam"], |
| streaming=True, |
| elem_classes="image-upload mc-upload-area", |
| height=400, |
| ) |
| gr.HTML( |
| ''' |
| <div style=" |
| color: var(--mc-gold); |
| font-family: VT323; |
| font-size: 28px; |
| text-align: center; |
| margin-top: 10px; |
| "> |
| ▶ Click Record to start MINECRAFTIFICATION OF LIVE FRAMES |
| </div> |
| ''' |
| ) |
|
|
| extra_details = gr.Textbox( |
| label="🗒️ Extra scene details (optional)", |
| placeholder="e.g. 'this is a beach at sunset' or 'make the dog a wolf mob'...", |
| lines=2, |
| ) |
|
|
| with gr.Accordion("⚙️ Advanced Settings", open=False): |
| guidance_scale = gr.Slider( |
| minimum=1.0, maximum=10.0, value=3.0, step=0.5, |
| label="🧭 Guidance Scale", |
| ) |
|
|
| steps = gr.Slider( |
| minimum=1, maximum=30, value=2, step=1, |
| label="⚡ Inference Steps", |
| info="Higher = better quality, slower generation", |
| ) |
|
|
| seed = gr.Number( |
| value=42, precision=0, |
| label="🎲 Seed", |
| info="Same seed + same image = same output", |
| ) |
|
|
| gr.HTML('<div class="mc-divider"></div>') |
|
|
| run_btn = gr.Button( |
| "⛏️ MINECRAFTIFY! ⛏️", |
| variant="primary", |
| size="lg", |
| elem_classes="mc-btn-primary", |
| ) |
|
|
| |
| with gr.Column(scale=1, min_width=60): |
| gr.HTML('<div class="mc-arrow">➜</div>') |
|
|
| |
| with gr.Column(scale=5): |
| gr.HTML('<div class="mc-section-label"><span class="mc-icon">🧱</span> MINECRAFT STYLE</div>') |
|
|
| with gr.Group(elem_classes="mc-panel"): |
| output_image = gr.Image( |
| type="pil", |
| label="Minecraft Style", |
| interactive=False, |
| height=400, |
| ) |
|
|
| |
| gr.HTML(""" |
| <div class="mc-tips"> |
| <div class="mc-tip-title">📖 TIPS & TRICKS</div> |
| <div class="mc-divider" style="margin: 10px 0;"></div> |
| <ul style="padding-left: 20px; margin: 8px 0;"> |
| <li>🎯 <strong style="color:#7CB342;">Inference steps:</strong> 2 is the sweet spot. Go higher for more detail and Craziness!</li> |
| <li>🐺 <strong style="color:#7CB342;">Still too real?</strong> Describe tricky parts in the details box (e.g. "convert the dog into a wolf mob").</li> |
| <li>⏱️ <strong style="color:#7CB342;">Live mode:</strong> The camera stream prioritizes the newest frame whenever the model is free.</li> |
| <li>🎲 <strong style="color:#7CB342;">Variations:</strong> Change the seed to get different Minecraft interpretations of the same photo.</li> |
| <li>🖼️ <strong style="color:#7CB342;">Best results:</strong> Clear, well-lit photos with distinct objects work great!</li> |
| </ul> |
| </div> |
| """) |
|
|
| |
| gr.HTML(""" |
| <div class="mc-footer"> |
| <p>Built with 💚 and blocks · Powered by FLUX.2 + Minecraft LoRA</p> |
| <p style="font-size: 0.8rem !important; margin-top: 4px;">Not affiliated with Mojang or Microsoft</p> |
| </div> |
| """) |
|
|
| |
| live_running = gr.State(False) |
|
|
| input_mode.change( |
| fn=switch_input_mode, |
| inputs=input_mode, |
| outputs=[ |
| image_input_group, |
| run_btn, |
| live_input_group, |
| live_running, |
| ], |
| queue=False, |
| ) |
|
|
| run_btn.click( |
| fn=minecraftify, |
| inputs=[input_image, steps, guidance_scale, seed, extra_details], |
| outputs=output_image, |
| ) |
|
|
| live_image.stream( |
| fn=lambda img: _set_live_active(img), |
| inputs=live_image, |
| outputs=None, |
| stream_every=0.05, |
| trigger_mode="always_last", |
| queue=False, |
| ).then( |
| fn=minecraftify_live_loop, |
| inputs=[steps, guidance_scale, seed, extra_details], |
| outputs=[output_image], |
| concurrency_limit=1, |
| ) |
|
|
| live_image.stream( |
| fn=capture_live_frame, |
| inputs=live_image, |
| outputs=None, |
| stream_every=0.05, |
| trigger_mode="always_last", |
| queue=False, |
| ) |
|
|
| log_event("Launching Gradio app") |
| demo.queue(max_size=1).launch(**LAUNCH_KWARGS) |
|
|