Spaces:
Running on Zero
Running on Zero
| """ | |
| 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 MINECRAFT CSS — THE ULTIMATE THEME | |
| # ---------------------------------------------------------------------- | |
| 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; | |
| } | |
| """ | |
| # ---------------------------------------------------------------------- | |
| # PERSISTENT STORAGE PATHS | |
| # ---------------------------------------------------------------------- | |
| BASE_MODEL_ID = "black-forest-labs/FLUX.2-klein-4B" | |
| LORA_ID = "AnimeOverlord/flux2-klein-4b-mc-v2" | |
| # Mount this path in the Space settings | |
| SPACE_DATA_DIR = "/data" | |
| # Put Hugging Face cache on persistent storage too | |
| HF_HOME = os.path.join(SPACE_DATA_DIR, ".huggingface") | |
| HF_HUB_CACHE = os.path.join(HF_HOME, "hub") | |
| # Store the downloaded model files here | |
| 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") | |
| # Set these before any HF downloads happen | |
| 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}") | |
| # Always refresh LoRA in temp storage | |
| 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", | |
| ) | |
| # Force modules to bf16 in case anything stayed fp32 | |
| 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 | |
| # ---------------------------------------------------------------------- | |
| # INFERENCE FUNCTION | |
| # ---------------------------------------------------------------------- | |
| 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 | |
| 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) | |
| 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, | |
| ) | |
| # ---------------------------------------------------------------------- | |
| # UI | |
| # ---------------------------------------------------------------------- | |
| log_event("Building Gradio UI") | |
| with gr.Blocks(**BLOCKS_KWARGS) as demo: | |
| # ── Title Banner ── | |
| 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> | |
| """) | |
| # ── Main Content Row ── | |
| with gr.Row(elem_classes="main-row"): | |
| # ── LEFT: Input Column ── | |
| 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", | |
| ) | |
| # ── CENTER: Arrow ── | |
| with gr.Column(scale=1, min_width=60): | |
| gr.HTML('<div class="mc-arrow">➜</div>') | |
| # ── RIGHT: Output Column ── | |
| 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, | |
| ) | |
| # ── Tips Section ── | |
| 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> | |
| """) | |
| # ── Footer ── | |
| 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> | |
| """) | |
| # ── Event binding ── | |
| 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) | |