AnimeOverlord commited on
Commit
ae9ef77
·
1 Parent(s): aebeae1

Add live Minecraft camera mode

Browse files
Files changed (2) hide show
  1. app.py +950 -129
  2. requirements.txt +11 -6
app.py CHANGED
@@ -1,133 +1,954 @@
 
 
 
 
 
 
 
1
  import os
 
2
  import gradio as gr
3
- import cv2
4
- import numpy as np
5
- import modal
6
-
7
- # ── Authentication & Token Guard ────────────────────────────────────────────
8
- token_id = os.environ.get("MODAL_TOKEN_ID")
9
- token_secret = os.environ.get("MODAL_TOKEN_SECRET")
10
- has_tokens = bool(token_id and token_secret)
11
-
12
- status_text = "🟢 Connected to Modal" if has_tokens else "🔴 Offline (Missing Tokens)"
13
- if not has_tokens:
14
- print("⚠️ [AUTH ERROR] Modal tokens missing! Check your Hugging Face Secrets.")
15
-
16
-
17
- # ── Thread-Safe Client Resolver ─────────────────────────────────────────────
18
- _backend_cache = None
19
-
20
- def get_modal_backend():
21
- """Resolves the Modal method lazily and caches it for standard threads."""
22
- global _backend_cache
23
- if not has_tokens:
24
- return None
25
-
26
- if _backend_cache is None:
27
- try:
28
- VoxelModelCls = modal.Cls.from_name("flux-klein-voxel-backend", "VoxelModel")
29
- _backend_cache = VoxelModelCls().process_frame
30
- except Exception as e:
31
- print(f"❌ Failed to resolve Modal class: {e}")
32
- return None
33
-
34
- return _backend_cache
35
-
36
-
37
- # ── Core Sync Execution (No Async/Await!) ───────────────────────────────────
38
- def run_modal_backend(frame: np.ndarray) -> np.ndarray:
39
- """Compresses the frame and executes strictly synchronously."""
40
- backend = get_modal_backend()
41
- if backend is None:
42
- return frame
43
-
44
- # Compress to JPEG to save network bandwidth
45
- success, encoded = cv2.imencode(".jpg", frame, [int(cv2.IMWRITE_JPEG_QUALITY), 80])
46
- if not success:
47
- return frame
48
-
49
- try:
50
- # PURE SYNCHRONOUS CALL: Gradio's background threadpool handles this perfectly safely
51
- processed_bytes = backend.remote(encoded.tobytes())
52
-
53
- # Decode the returning bytes back into an OpenCV image
54
- result = cv2.imdecode(np.frombuffer(processed_bytes, dtype=np.uint8), cv2.IMREAD_COLOR)
55
- return result if result is not None else frame
56
- except Exception as e:
57
- print(f"Modal execution error: {e}")
58
- err_frame = frame.copy()
59
- cv2.putText(err_frame, "Backend Error - Check Console", (10, 40),
60
- cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)
61
- return err_frame
62
-
63
-
64
- # ── Streaming Logic ─────────────────────────────────────────────────────────
65
- def process_video_stream(frame: np.ndarray, mode: str, is_running: bool) -> np.ndarray:
66
- """Handles the webcam feed and respects the Start/Stop toggle."""
67
- if frame is None:
68
- return None
69
-
70
- # CRITICAL: If the user hasn't clicked Start, just return the raw webcam feed.
71
- # No dummy frames, no early wakeups.
72
- if not is_running:
73
- return frame
74
-
75
- # 1. Process the actual webcam frame synchronously
76
- processed = run_modal_backend(frame)
77
-
78
- # 2. Format the output based on the selected UI mode
79
- if mode == "Minecraft Filter":
80
- return processed
81
-
82
- elif mode == "Streaming Demo":
83
- if processed.shape != frame.shape:
84
- processed = cv2.resize(processed, (frame.shape[1], frame.shape[0]))
85
-
86
- raw_labeled = frame.copy()
87
- cv2.putText(raw_labeled, "RAW", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2)
88
- cv2.putText(processed, "MINECRAFT", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2)
89
- return np.hstack([raw_labeled, processed])
90
-
91
- return processed
92
-
93
-
94
- # ── Gradio UI Layout ────────────────────────────────────────────────────────
95
- with gr.Blocks(title="⛏️ Minecraft Spatial Voxel Filter") as demo:
96
- is_running = gr.State(value=False)
97
-
98
- gr.Markdown("# ⛏️ Minecraft Spatial Voxel Filter")
99
-
100
- with gr.Row():
101
- # Left Panel: Controls
102
- with gr.Column(scale=1):
103
- gr.Markdown(f"### Backend Status: {status_text}")
104
-
105
- mode_dropdown = gr.Dropdown(
106
- choices=["Minecraft Filter", "Streaming Demo"],
107
- value="Minecraft Filter",
108
- label="🎯 Pipeline Mode"
109
- )
110
-
111
- with gr.Row():
112
- start_btn = gr.Button("🚀 Start Processing", variant="primary")
113
- stop_btn = gr.Button("🛑 Stop", variant="secondary")
114
-
115
- # Right Panel: Video
116
- with gr.Column(scale=2):
117
- input_stream = gr.Image(sources=["webcam"], streaming=True, label="Live Webcam Input")
118
- output_stream = gr.Image(interactive=False, label="Voxel Output Viewport")
119
-
120
- # Wire buttons to simply toggle the boolean. The video stream loop handles the rest.
121
- start_btn.click(fn=lambda: True, inputs=None, outputs=is_running)
122
- stop_btn.click(fn=lambda: False, inputs=None, outputs=is_running)
123
-
124
- # Main stream loop
125
- input_stream.stream(
126
- fn=process_video_stream,
127
- inputs=[input_stream, mode_dropdown, is_running],
128
- outputs=[output_stream],
129
- trigger_mode="always_last"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
  )
131
 
132
- if __name__ == "__main__":
133
- demo.launch()
 
1
+ """
2
+ Minecraftify! — turn any photo into a Minecraft-style scene.
3
+
4
+ Beautiful Minecraft-themed Gradio UI with a dummy inference function
5
+ (color inversion) for testing. Swap in the real model later.
6
+ """
7
+
8
  import os
9
+ import torch
10
  import gradio as gr
11
+ import spaces
12
+ from PIL import Image
13
+ from huggingface_hub import snapshot_download
14
+ from diffusers import DiffusionPipeline
15
+
16
+ # ----------------------------------------------------------------------
17
+ # CUSTOM MINECRAFT CSS — THE ULTIMATE THEME
18
+ # ----------------------------------------------------------------------
19
+
20
+ CUSTOM_CSS = """
21
+ /* ══════════════════════════════════════════════════════════════════════
22
+ MINECRAFT THEME Premium Pixel-Art UI
23
+ ══════════════════════════════════════════════════════════════════════ */
24
+
25
+ /* ── Fonts ── */
26
+ @import url('https://fonts.googleapis.com/css2?family=Silkscreen:wght@400;700&display=swap');
27
+ @import url('https://fonts.googleapis.com/css2?family=VT323&display=swap');
28
+ @import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');
29
+
30
+ /* ── CSS Variables ── */
31
+ :root {
32
+ --mc-green: #5D8C3E;
33
+ --mc-green-light: #7CB342;
34
+ --mc-green-dark: #3B5E28;
35
+ --mc-grass-top: #6DBE45;
36
+ --mc-dirt: #8B6914;
37
+ --mc-dirt-dark: #6B4F10;
38
+ --mc-dirt-light: #9B7924;
39
+ --mc-stone: #7F7F7F;
40
+ --mc-stone-dark: #5A5A5A;
41
+ --mc-stone-light: #999999;
42
+ --mc-cobble: #6E6E6E;
43
+ --mc-sky-day: #78A7FF;
44
+ --mc-sky-horizon: #B4D4FF;
45
+ --mc-text: #FFFFFF;
46
+ --mc-text-shadow: #3F3F00;
47
+ --mc-gold: #FFAA00;
48
+ --mc-gold-dark: #CC8800;
49
+ --mc-red: #FF5555;
50
+ --mc-aqua: #55FFFF;
51
+ --mc-dark-bg: #1E1E1E;
52
+ --mc-panel-bg: rgba(0, 0, 0, 0.72);
53
+ --mc-panel-inner: rgba(0, 0, 0, 0.35);
54
+ --mc-border: #1A1A1A;
55
+ --mc-border-light: #3A3A3A;
56
+ --mc-highlight: #C6C6C6;
57
+ --mc-inventory-slot: #8B8B8B;
58
+ --mc-inventory-inner: #373737;
59
+ }
60
+
61
+ /* ── Animated Panorama Background ── */
62
+ .gradio-container {
63
+ background:
64
+ linear-gradient(180deg,
65
+ #78A7FF 0%,
66
+ #B4D4FF 30%,
67
+ #C8E0FF 38%,
68
+ #6DBE45 38.5%,
69
+ #5D8C3E 40%,
70
+ #3B5E28 42%,
71
+ #8B6914 42.5%,
72
+ #7A5B10 55%,
73
+ #6B4F10 70%,
74
+ #5A4210 85%,
75
+ #3D2D0A 100%
76
+ ) !important;
77
+ background-attachment: fixed !important;
78
+ font-family: 'VT323', monospace !important;
79
+ min-height: 100vh;
80
+ position: relative;
81
+ overflow-x: hidden;
82
+ }
83
+
84
+ /* ── Floating Block Particles (Pure CSS) ── */
85
+ .gradio-container::before {
86
+ content: '';
87
+ position: fixed;
88
+ top: 0; left: 0;
89
+ width: 100%; height: 100%;
90
+ pointer-events: none;
91
+ z-index: 0;
92
+ background-image:
93
+ radial-gradient(2px 2px at 10% 20%, rgba(255,255,255,0.4) 50%, transparent 50%),
94
+ radial-gradient(2px 2px at 30% 60%, rgba(255,255,255,0.3) 50%, transparent 50%),
95
+ radial-gradient(2px 2px at 50% 10%, rgba(255,255,255,0.5) 50%, transparent 50%),
96
+ radial-gradient(2px 2px at 70% 40%, rgba(255,255,255,0.35) 50%, transparent 50%),
97
+ radial-gradient(2px 2px at 90% 80%, rgba(255,255,255,0.4) 50%, transparent 50%),
98
+ radial-gradient(3px 3px at 20% 85%, rgba(255,255,255,0.2) 50%, transparent 50%),
99
+ radial-gradient(2px 2px at 80% 15%, rgba(255,255,255,0.45) 50%, transparent 50%),
100
+ radial-gradient(2px 2px at 45% 75%, rgba(255,255,255,0.25) 50%, transparent 50%);
101
+ animation: sparkle 4s ease-in-out infinite alternate;
102
+ }
103
+
104
+ @keyframes sparkle {
105
+ 0% { opacity: 0.4; }
106
+ 50% { opacity: 0.8; }
107
+ 100% { opacity: 0.5; }
108
+ }
109
+
110
+ /* ── All content above particles ── */
111
+ .gradio-container > * {
112
+ position: relative;
113
+ z-index: 1;
114
+ }
115
+
116
+ /* ── Main wrapper ── */
117
+ .main-wrap {
118
+ max-width: 1150px;
119
+ margin: 0 auto;
120
+ }
121
+
122
+ /* ══════════════════════════════════════════════════════════════════════
123
+ TITLE AREA Minecraft Logo Style
124
+ ══════════════════════════════════════════════════════════════════════ */
125
+
126
+ .mc-title-area {
127
+ text-align: center;
128
+ padding: 35px 20px 10px 20px;
129
+ position: relative;
130
+ }
131
+
132
+ .mc-title-area h1 {
133
+ font-family: 'Silkscreen', cursive !important;
134
+ font-size: 3.5rem !important;
135
+ color: #FFFFFF !important;
136
+ text-shadow:
137
+ 4px 4px 0px #3F3F00,
138
+ -2px -2px 0px #000,
139
+ 2px -2px 0px #000,
140
+ -2px 2px 0px #000,
141
+ 2px 2px 0px #000,
142
+ 0px 0px 20px rgba(109, 190, 69, 0.4) !important;
143
+ margin: 0 !important;
144
+ letter-spacing: 3px;
145
+ line-height: 1.2 !important;
146
+ animation: title-float 3s ease-in-out infinite;
147
+ }
148
+
149
+ @keyframes title-float {
150
+ 0%, 100% { transform: translateY(0px); }
151
+ 50% { transform: translateY(-6px); }
152
+ }
153
+
154
+ .mc-subtitle {
155
+ font-family: 'VT323', monospace !important;
156
+ font-size: 1.4rem !important;
157
+ color: #E8E8E8 !important;
158
+ text-shadow: 1px 1px 3px rgba(0,0,0,0.9) !important;
159
+ margin-top: 10px !important;
160
+ max-width: 600px;
161
+ margin-left: auto !important;
162
+ margin-right: auto !important;
163
+ line-height: 1.4 !important;
164
+ letter-spacing: 0.5px;
165
+ }
166
+
167
+ .mc-version-tag {
168
+ display: inline-block;
169
+ font-family: 'Press Start 2P', cursive !important;
170
+ font-size: 0.55rem !important;
171
+ color: var(--mc-gold) !important;
172
+ background: rgba(0,0,0,0.6);
173
+ border: 1px solid var(--mc-gold-dark);
174
+ padding: 4px 10px;
175
+ margin-top: 10px;
176
+ letter-spacing: 1px;
177
+ animation: gold-glow 2s ease-in-out infinite alternate;
178
+ }
179
+
180
+ @keyframes gold-glow {
181
+ 0% { box-shadow: 0 0 5px rgba(255, 170, 0, 0.2); }
182
+ 100% { box-shadow: 0 0 15px rgba(255, 170, 0, 0.5); }
183
+ }
184
+
185
+ /* ══════════════════════════════════════════════════════════════════════
186
+ PANELS — Minecraft Inventory Style
187
+ ══════════════════════════════════════════════════════════════════════ */
188
+
189
+ .mc-panel {
190
+ background: var(--mc-panel-bg) !important;
191
+ border: 3px solid var(--mc-border) !important;
192
+ border-radius: 0px !important;
193
+ box-shadow:
194
+ inset 2px 2px 0px rgba(255,255,255,0.08),
195
+ inset -2px -2px 0px rgba(0,0,0,0.4),
196
+ 6px 6px 0px rgba(0,0,0,0.5),
197
+ 0 0 40px rgba(0,0,0,0.3) !important;
198
+ padding: 20px !important;
199
+ position: relative;
200
+ overflow: hidden;
201
+ }
202
+
203
+ /* Subtle noise texture overlay on panels */
204
+ .mc-panel::before {
205
+ content: '';
206
+ position: absolute;
207
+ top: 0; left: 0;
208
+ width: 100%; height: 100%;
209
+ background-image:
210
+ repeating-linear-gradient(
211
+ 0deg,
212
+ transparent,
213
+ transparent 2px,
214
+ rgba(255,255,255,0.015) 2px,
215
+ rgba(255,255,255,0.015) 4px
216
+ ),
217
+ repeating-linear-gradient(
218
+ 90deg,
219
+ transparent,
220
+ transparent 2px,
221
+ rgba(255,255,255,0.01) 2px,
222
+ rgba(255,255,255,0.01) 4px
223
+ );
224
+ pointer-events: none;
225
+ image-rendering: pixelated;
226
+ }
227
+
228
+ /* ── Inner groups ── */
229
+ .gradio-group {
230
+ background: var(--mc-panel-inner) !important;
231
+ border: 2px solid var(--mc-border-light) !important;
232
+ border-radius: 0px !important;
233
+ }
234
+
235
+ /* ══════════════════════════════════════════════════════════════════════
236
+ SECTION HEADERS — Pixel Label Style
237
+ ══════════════════════════════════════════════════════════════════════ */
238
+
239
+ .mc-section-label {
240
+ font-family: 'Silkscreen', cursive !important;
241
+ font-size: 1rem !important;
242
+ color: var(--mc-gold) !important;
243
+ text-shadow: 2px 2px 0px rgba(0,0,0,0.8) !important;
244
+ letter-spacing: 1.5px;
245
+ padding: 6px 14px;
246
+ margin-bottom: 10px;
247
+ background: linear-gradient(90deg, rgba(255,170,0,0.12) 0%, transparent 100%);
248
+ border-left: 3px solid var(--mc-gold);
249
+ display: flex;
250
+ align-items: center;
251
+ gap: 8px;
252
+ }
253
+
254
+ .mc-section-label .mc-icon {
255
+ font-size: 1.2rem;
256
+ filter: drop-shadow(1px 1px 0px rgba(0,0,0,0.6));
257
+ }
258
+
259
+ /* ══════════════════════════════════════════════════════════════════════
260
+ LABELS & TEXT
261
+ ══════════════════════════════════════════════════════════════════════ */
262
+
263
+ .gradio-container label,
264
+ .gradio-container .label-wrap span {
265
+ font-family: 'VT323', monospace !important;
266
+ font-size: 1.2rem !important;
267
+ color: var(--mc-gold) !important;
268
+ text-shadow: 1px 1px 0px rgba(0,0,0,0.7) !important;
269
+ letter-spacing: 1px;
270
+ }
271
+
272
+ .gradio-container .gr-prose p,
273
+ .gradio-container .gr-prose li {
274
+ font-family: 'VT323', monospace !important;
275
+ color: #C8C8C8 !important;
276
+ }
277
+
278
+ /* ══════════════════════════════════════════════════════════════════════
279
+ BUTTON — Minecraft Stone Button (Authentic)
280
+ ══════════════════════════════════════════════════════════════════════ */
281
+
282
+ .mc-btn-primary, .gr-button-primary, button.primary {
283
+ font-family: 'Silkscreen', cursive !important;
284
+ font-size: 1.15rem !important;
285
+ letter-spacing: 1px;
286
+ background:
287
+ linear-gradient(180deg,
288
+ #9A9A9A 0%,
289
+ #888888 20%,
290
+ #6A6A6A 45%,
291
+ #585858 55%,
292
+ #484848 80%,
293
+ #3A3A3A 100%
294
+ ) !important;
295
+ color: var(--mc-text) !important;
296
+ border: 3px solid #000 !important;
297
+ border-radius: 0px !important;
298
+ box-shadow:
299
+ inset 2px 2px 0px rgba(255,255,255,0.3),
300
+ inset -2px -2px 0px rgba(0,0,0,0.5),
301
+ 0 4px 0px #222 !important;
302
+ text-shadow: 2px 2px 0px #3F3F00 !important;
303
+ padding: 14px 40px !important;
304
+ cursor: pointer;
305
+ transition: all 0.05s ease !important;
306
+ text-transform: none !important;
307
+ position: relative;
308
+ image-rendering: pixelated;
309
+ }
310
+
311
+ .mc-btn-primary:hover, .gr-button-primary:hover, button.primary:hover {
312
+ background:
313
+ linear-gradient(180deg,
314
+ #B0B0B0 0%,
315
+ #A0A0A0 20%,
316
+ #888888 45%,
317
+ #787878 55%,
318
+ #686868 80%,
319
+ #585858 100%
320
+ ) !important;
321
+ color: #FFFFAA !important;
322
+ box-shadow:
323
+ inset 2px 2px 0px rgba(255,255,255,0.4),
324
+ inset -2px -2px 0px rgba(0,0,0,0.4),
325
+ 0 4px 0px #222 !important;
326
+ }
327
+
328
+ .mc-btn-primary:active, .gr-button-primary:active, button.primary:active {
329
+ background:
330
+ linear-gradient(180deg,
331
+ #4A4A4A 0%,
332
+ #3A3A3A 50%,
333
+ #5A5A5A 100%
334
+ ) !important;
335
+ box-shadow:
336
+ inset -2px -2px 0px rgba(255,255,255,0.15),
337
+ inset 2px 2px 0px rgba(0,0,0,0.5),
338
+ 0 1px 0px #222 !important;
339
+ transform: translateY(3px);
340
+ }
341
+
342
+ /* ══════════════════════════════════════════════════════════════════════
343
+ IMAGE UPLOAD — Crafting Table Slot Style
344
+ ══════════════════════════════════════════════════════════════════════ */
345
+
346
+ .mc-upload-area {
347
+ position: relative;
348
+ }
349
+
350
+ .image-upload, .gr-image, .gr-file {
351
+ border: 3px solid var(--mc-inventory-inner) !important;
352
+ border-radius: 0px !important;
353
+ background: rgba(0,0,0,0.55) !important;
354
+ box-shadow:
355
+ inset 2px 2px 0px rgba(0,0,0,0.5),
356
+ inset -2px -2px 0px rgba(139,139,139,0.2) !important;
357
+ transition: border-color 0.2s ease, box-shadow 0.2s ease;
358
+ image-rendering: pixelated;
359
+ }
360
+
361
+ .image-upload:hover, .gr-image:hover {
362
+ border-color: var(--mc-gold) !important;
363
+ box-shadow:
364
+ inset 2px 2px 0px rgba(0,0,0,0.5),
365
+ inset -2px -2px 0px rgba(139,139,139,0.2),
366
+ 0 0 15px rgba(255, 170, 0, 0.2) !important;
367
+ }
368
+
369
+ /* ══════════════════════════════════════════════════════════════════════
370
+ INPUT FIELDS — Minecraft Sign / Book Style
371
+ ══════════════════════════════════════════════════════════════════════ */
372
+
373
+ textarea, input[type="text"], input[type="number"] {
374
+ font-family: 'VT323', monospace !important;
375
+ font-size: 1.15rem !important;
376
+ background: rgba(0,0,0,0.6) !important;
377
+ color: var(--mc-text) !important;
378
+ border: 2px solid var(--mc-inventory-inner) !important;
379
+ border-radius: 0px !important;
380
+ box-shadow: inset 1px 1px 0px rgba(0,0,0,0.4) !important;
381
+ image-rendering: pixelated;
382
+ }
383
+
384
+ textarea:focus, input[type="text"]:focus, input[type="number"]:focus {
385
+ border-color: var(--mc-gold) !important;
386
+ outline: none !important;
387
+ box-shadow: 0 0 0 1px var(--mc-gold), inset 1px 1px 0px rgba(0,0,0,0.4) !important;
388
+ }
389
+
390
+ textarea::placeholder, input::placeholder {
391
+ color: #666 !important;
392
+ font-family: 'VT323', monospace !important;
393
+ }
394
+
395
+ /* ══════════════════════════════════════════════════════════════════════
396
+ SLIDERS — Minecraft XP Bar Style
397
+ ══════════════════════════════════════════════════════════════════════ */
398
+
399
+ input[type="range"] {
400
+ accent-color: var(--mc-green-light) !important;
401
+ height: 6px;
402
+ }
403
+
404
+ input[type="range"]::-webkit-slider-thumb {
405
+ background: var(--mc-green-light) !important;
406
+ border: 2px solid #000 !important;
407
+ width: 16px;
408
+ height: 16px;
409
+ border-radius: 0px !important;
410
+ image-rendering: pixelated;
411
+ }
412
+
413
+ /* ══════════════════════════════════════════════════════════════════════
414
+ ACCORDION — Chest Opening Style
415
+ ══════════════════════════════════════════════════════════════════════ */
416
+
417
+ .gradio-accordion {
418
+ background: rgba(0,0,0,0.3) !important;
419
+ border: 2px solid var(--mc-border-light) !important;
420
+ border-radius: 0px !important;
421
+ overflow: hidden;
422
+ }
423
+
424
+ .gradio-accordion .label-wrap {
425
+ background: rgba(0,0,0,0.4) !important;
426
+ border-radius: 0px !important;
427
+ padding: 10px 14px !important;
428
+ transition: background 0.2s ease;
429
+ }
430
+
431
+ .gradio-accordion .label-wrap:hover {
432
+ background: rgba(255,170,0,0.1) !important;
433
+ }
434
+
435
+ .gradio-accordion .label-wrap span {
436
+ font-family: 'VT323', monospace !important;
437
+ font-size: 1.15rem !important;
438
+ color: var(--mc-highlight) !important;
439
+ }
440
+
441
+ /* ══════════════════════════════════════════════════════════════════════
442
+ STATUS / GENERATING — Enchantment Glow
443
+ ══════════════════════════════════════════════════════════════════════ */
444
+
445
+ .generating {
446
+ border-color: var(--mc-aqua) !important;
447
+ animation: enchant-glow 1.5s ease-in-out infinite alternate !important;
448
+ }
449
+
450
+ @keyframes enchant-glow {
451
+ 0% {
452
+ box-shadow: 0 0 8px rgba(85, 255, 255, 0.3),
453
+ inset 0 0 8px rgba(85, 255, 255, 0.1);
454
+ }
455
+ 100% {
456
+ box-shadow: 0 0 25px rgba(85, 255, 255, 0.5),
457
+ inset 0 0 15px rgba(85, 255, 255, 0.2);
458
+ }
459
+ }
460
+
461
+ /* ══════════════════════════════════════════════════════════════════════
462
+ TIPS SECTION — Book & Quill Style
463
+ ══════════════════════════════════════════════════════════════════════ */
464
+
465
+ .mc-tips {
466
+ background: rgba(0,0,0,0.55) !important;
467
+ border: 2px solid var(--mc-inventory-inner) !important;
468
+ border-radius: 0px !important;
469
+ padding: 18px 22px !important;
470
+ margin-top: 16px;
471
+ box-shadow:
472
+ inset 1px 1px 0px rgba(255,255,255,0.05),
473
+ inset -1px -1px 0px rgba(0,0,0,0.3) !important;
474
+ }
475
+
476
+ .mc-tips p, .mc-tips li {
477
+ font-family: 'VT323', monospace !important;
478
+ color: #B8B8B8 !important;
479
+ font-size: 1.1rem !important;
480
+ text-shadow: 1px 1px 0px rgba(0,0,0,0.5) !important;
481
+ line-height: 1.6 !important;
482
+ }
483
+
484
+ .mc-tips strong, .mc-tips .mc-tip-title {
485
+ color: var(--mc-gold) !important;
486
+ font-family: 'Silkscreen', cursive !important;
487
+ font-size: 0.9rem !important;
488
+ }
489
+
490
+ .mc-tips li::marker {
491
+ color: var(--mc-green-light);
492
+ }
493
+
494
+ /* ══════════════════════════════════════════════════════════════════════
495
+ FOOTER
496
+ ══════════════════════════════════════════════════════════════════════ */
497
+
498
+ .mc-footer {
499
+ text-align: center;
500
+ padding: 20px 10px 30px;
501
+ }
502
+
503
+ .mc-footer p {
504
+ font-family: 'VT323', monospace !important;
505
+ color: rgba(255,255,255,0.35) !important;
506
+ font-size: 1rem !important;
507
+ letter-spacing: 0.5px;
508
+ }
509
+
510
+ .mc-footer a {
511
+ color: var(--mc-gold) !important;
512
+ text-decoration: none;
513
+ }
514
+
515
+ /* ══════════════════════════════════════════════════════════════════════
516
+ DIVIDER — Bedrock Layer
517
+ ══════════════════���═══════════════════════════════════════════════════ */
518
+
519
+ .mc-divider {
520
+ height: 4px;
521
+ background: repeating-linear-gradient(
522
+ 90deg,
523
+ #2A2A2A 0px,
524
+ #2A2A2A 8px,
525
+ #1A1A1A 8px,
526
+ #1A1A1A 16px,
527
+ #333333 16px,
528
+ #333333 24px,
529
+ #1A1A1A 24px,
530
+ #1A1A1A 32px
531
+ );
532
+ margin: 16px 0;
533
+ image-rendering: pixelated;
534
+ }
535
+
536
+ /* ══════════════════════════════════════════════════════════════════════
537
+ ARROW BETWEEN COLUMNS — Crafting Arrow
538
+ ══════════════════════════════════════════════════════════════════════ */
539
+
540
+ .mc-arrow {
541
+ display: flex;
542
+ align-items: center;
543
+ justify-content: center;
544
+ font-size: 2.5rem;
545
+ color: var(--mc-highlight);
546
+ text-shadow: 2px 2px 0px rgba(0,0,0,0.7);
547
+ animation: arrow-pulse 1.5s ease-in-out infinite;
548
+ padding-top: 160px;
549
+ }
550
+
551
+ @keyframes arrow-pulse {
552
+ 0%, 100% { opacity: 0.6; transform: translateX(0px); }
553
+ 50% { opacity: 1; transform: translateX(4px); }
554
+ }
555
+
556
+ /* ══════════════════════════════════════════════════════════════════════
557
+ RESPONSIVE
558
+ ══════════════════════════════════════════════════════════════════════ */
559
+
560
+ @media (max-width: 768px) {
561
+ .mc-title-area h1 {
562
+ font-size: 2rem !important;
563
+ }
564
+ .mc-arrow {
565
+ padding-top: 10px;
566
+ transform: rotate(90deg);
567
+ }
568
+ .mc-section-label {
569
+ font-size: 0.85rem !important;
570
+ }
571
+ }
572
+
573
+ /* ══════════════════════════════════════════════════════════════════════
574
+ SCROLLBAR — Stone Brick Style
575
+ ══════════════════════════════════════════════════════════════════════ */
576
+
577
+ ::-webkit-scrollbar {
578
+ width: 12px;
579
+ }
580
+ ::-webkit-scrollbar-track {
581
+ background: #2A2A2A;
582
+ border-left: 1px solid #1A1A1A;
583
+ }
584
+ ::-webkit-scrollbar-thumb {
585
+ background: linear-gradient(180deg, #777 0%, #555 100%);
586
+ border: 2px solid #1A1A1A;
587
+ }
588
+ ::-webkit-scrollbar-thumb:hover {
589
+ background: linear-gradient(180deg, #999 0%, #777 100%);
590
+ }
591
+
592
+ /* ── Dark mode override fix ── */
593
+ .dark .gradio-container,
594
+ .dark {
595
+ background: linear-gradient(180deg,
596
+ #78A7FF 0%, #B4D4FF 30%, #C8E0FF 38%,
597
+ #6DBE45 38.5%, #5D8C3E 40%, #3B5E28 42%,
598
+ #8B6914 42.5%, #7A5B10 55%, #6B4F10 70%,
599
+ #5A4210 85%, #3D2D0A 100%
600
+ ) !important;
601
+ }
602
+ """
603
+
604
+
605
+ # ----------------------------------------------------------------------
606
+ # PERSISTENT STORAGE PATHS
607
+ # ----------------------------------------------------------------------
608
+
609
+ BASE_MODEL_ID = "black-forest-labs/FLUX.2-klein-4B"
610
+ LORA_ID = "AnimeOverlord/flux2-klein-4b-mc"
611
+
612
+ # On Hugging Face Spaces, /data is the usual persistent volume.
613
+ MODEL_DIR = "black-forest-labs/FLUX.2-klein-4B"
614
+ LORA_DIR = "AnimeOverlord/flux2-klein-4b-mc-v2"
615
+
616
+ _pipe = None
617
+ _pipe_ready = False
618
+
619
+ IMAGE_MODE = "Image"
620
+ LIVE_MODE = "Live Camera"
621
+
622
+
623
+ def _ensure_model_files():
624
+ os.makedirs(MODEL_DIR, exist_ok=True)
625
+ os.makedirs(LORA_DIR, exist_ok=True)
626
+
627
+ # Download base model once into the volume
628
+ if not os.path.exists(os.path.join(MODEL_DIR, "model_index.json")):
629
+ snapshot_download(
630
+ repo_id=BASE_MODEL_ID,
631
+ local_dir=MODEL_DIR,
632
+ local_dir_use_symlinks=False,
633
+ )
634
+
635
+ # Download LoRA once into the volume
636
+ if not os.path.exists(os.path.join(LORA_DIR, "pytorch_lora_weights.safetensors")):
637
+ snapshot_download(
638
+ repo_id=LORA_ID,
639
+ local_dir=LORA_DIR,
640
+ local_dir_use_symlinks=False,
641
+ allow_patterns=["pytorch_lora_weights.safetensors"],
642
+ )
643
+
644
+
645
+ def _load_pipe():
646
+ global _pipe, _pipe_ready
647
+
648
+ if _pipe_ready and _pipe is not None:
649
+ return _pipe
650
+
651
+ _ensure_model_files()
652
+
653
+ pipe = DiffusionPipeline.from_pretrained(
654
+ MODEL_DIR,
655
+ torch_dtype=torch.bfloat16,
656
+ device_map="cuda",
657
+ )
658
+
659
+ pipe.load_lora_weights(
660
+ LORA_DIR,
661
+ weight_name="pytorch_lora_weights.safetensors",
662
+ )
663
+
664
+ # Force modules to bf16 in case anything stayed fp32
665
+ for module_name in ["transformer", "vae", "text_encoder", "image_encoder"]:
666
+ if hasattr(pipe, module_name):
667
+ getattr(pipe, module_name).to(dtype=torch.bfloat16)
668
+
669
+ pipe.set_progress_bar_config(disable=False)
670
+
671
+ _pipe = pipe
672
+ _pipe_ready = True
673
+ return pipe
674
+
675
+
676
+ # ----------------------------------------------------------------------
677
+ # INFERENCE FUNCTION
678
+ # ----------------------------------------------------------------------
679
+
680
+ def _minecraftify_image(
681
+ image: Image.Image,
682
+ steps: int,
683
+ guidance_scale: float,
684
+ seed: int,
685
+ extra_details: str,
686
+ ):
687
+ pipe = _load_pipe()
688
+ image = image.convert("RGB")
689
+
690
+ prompt = (
691
+ "Convert this image into a faithful vanilla Minecraft version of the same scene. "
692
+ "Preserve the original composition, camera angle, layout, all existing objects and especially patterns and colours. "
693
+ "Recreate the scene using only vanilla Minecraft blocks, textures, materials, and lighting. "
694
+ "Replace realistic surfaces with cubic Minecraft blocks and pixelated textures. "
695
+ "Convert people, animals, and other living things into custom Minecraft mobs while preserving their pose and placement. "
696
+ "Keep the result visually consistent with an authentic vanilla Minecraft screenshot while preserving the image itself."
697
+ )
698
+
699
+ if extra_details and extra_details.strip():
700
+ prompt += f"\n\nAdditional scene notes: {extra_details.strip()}"
701
+
702
+ generator = torch.Generator(device="cuda").manual_seed(int(seed))
703
+
704
+ with torch.inference_mode():
705
+ out = pipe(
706
+ image=image,
707
+ prompt=prompt,
708
+ num_inference_steps=int(steps),
709
+ guidance_scale=float(guidance_scale),
710
+ generator=generator,
711
+ )
712
+
713
+ return out.images[0]
714
+
715
+
716
+ @spaces.GPU(duration=120)
717
+ def minecraftify(
718
+ image: Image.Image,
719
+ steps: int,
720
+ guidance_scale: float,
721
+ seed: int,
722
+ extra_details: str,
723
+ ):
724
+ if image is None:
725
+ raise gr.Error("⛏️ Please upload a photo first!")
726
+
727
+ return _minecraftify_image(image, steps, guidance_scale, seed, extra_details)
728
+
729
+
730
+ @spaces.GPU(duration=120)
731
+ def minecraftify_live(
732
+ image: Image.Image,
733
+ is_running: bool,
734
+ steps: int,
735
+ guidance_scale: float,
736
+ seed: int,
737
+ extra_details: str,
738
+ ):
739
+ if not is_running or image is None:
740
+ return gr.skip()
741
+
742
+ return _minecraftify_image(image, steps, guidance_scale, seed, extra_details)
743
+
744
+
745
+ def switch_input_mode(mode: str):
746
+ image_mode = mode == IMAGE_MODE
747
+ return (
748
+ gr.update(visible=image_mode),
749
+ gr.update(visible=image_mode),
750
+ gr.update(visible=not image_mode),
751
+ gr.update(interactive=True),
752
+ gr.update(visible=not image_mode, interactive=False),
753
+ False,
754
+ )
755
+
756
+
757
+ def start_live():
758
+ return True, gr.update(interactive=False), gr.update(interactive=True)
759
+
760
+
761
+ def stop_live():
762
+ return False, gr.update(interactive=True), gr.update(interactive=False)
763
+
764
+
765
+ # ----------------------------------------------------------------------
766
+ # UI
767
+ # ----------------------------------------------------------------------
768
+
769
+ with gr.Blocks(
770
+ title="Minecraftify! ⛏️",
771
+ css=CUSTOM_CSS,
772
+ theme=gr.themes.Base(
773
+ primary_hue="green",
774
+ neutral_hue="stone",
775
+ font=gr.themes.GoogleFont("VT323"),
776
+ font_mono=gr.themes.GoogleFont("VT323"),
777
+ ),
778
+ ) as demo:
779
+
780
+ # ── Title Banner ──
781
+ gr.HTML("""
782
+ <div class="mc-title-area">
783
+ <h1>⛏️ MINECRAFTIFY!</h1>
784
+ <p class="mc-subtitle">
785
+ Upload any photo and watch it transform into a
786
+ blocky, pixel-art Minecraft masterpiece
787
+ </p>
788
+ <div class="mc-version-tag">✦ POWERED BY FLUX.2 + MINECRAFT LORA ✦</div>
789
+ </div>
790
+ """)
791
+
792
+ # ── Main Content Row ──
793
+ with gr.Row(elem_classes="main-row"):
794
+
795
+ # ── LEFT: Input Column ──
796
+ with gr.Column(scale=5):
797
+ gr.HTML('<div class="mc-section-label"><span class="mc-icon">📸</span> INPUT IMAGE</div>')
798
+
799
+ with gr.Group(elem_classes="mc-panel"):
800
+ input_mode = gr.Dropdown(
801
+ choices=[IMAGE_MODE, LIVE_MODE],
802
+ value=IMAGE_MODE,
803
+ label="🎮 Input mode",
804
+ interactive=True,
805
+ )
806
+
807
+ with gr.Group(visible=True) as image_input_group:
808
+ input_image = gr.Image(
809
+ type="pil",
810
+ label="Drop your image here",
811
+ sources=["upload", "webcam", "clipboard"],
812
+ elem_classes="image-upload mc-upload-area",
813
+ height=400,
814
+ )
815
+
816
+ with gr.Group(visible=False) as live_input_group:
817
+ live_image = gr.Image(
818
+ type="pil",
819
+ label="Live camera",
820
+ sources=["webcam"],
821
+ streaming=True,
822
+ elem_classes="image-upload mc-upload-area",
823
+ height=400,
824
+ )
825
+
826
+ with gr.Row():
827
+ start_live_btn = gr.Button(
828
+ "▶ START LIVE",
829
+ variant="primary",
830
+ elem_classes="mc-btn-primary",
831
+ )
832
+ stop_live_btn = gr.Button(
833
+ "■ STOP LIVE",
834
+ variant="secondary",
835
+ interactive=False,
836
+ )
837
+
838
+ extra_details = gr.Textbox(
839
+ label="🗒️ Extra scene details (optional)",
840
+ placeholder="e.g. 'this is a beach at sunset' or 'make the dog a wolf mob'...",
841
+ lines=2,
842
+ )
843
+
844
+ with gr.Accordion("⚙️ Advanced Settings", open=False):
845
+ steps = gr.Slider(
846
+ minimum=10, maximum=40, value=25, step=1,
847
+ label="⚡ Inference Steps",
848
+ info="Higher = better quality, slower generation",
849
+ )
850
+ guidance_scale = gr.Slider(
851
+ minimum=1.0, maximum=10.0, value=3.0, step=0.5,
852
+ label="🧭 Guidance Scale",
853
+ )
854
+ seed = gr.Number(
855
+ value=42, precision=0,
856
+ label="🎲 Seed",
857
+ info="Same seed + same image = same output",
858
+ )
859
+
860
+ gr.HTML('<div class="mc-divider"></div>')
861
+
862
+ run_btn = gr.Button(
863
+ "⛏️ MINECRAFTIFY! ⛏️",
864
+ variant="primary",
865
+ size="lg",
866
+ elem_classes="mc-btn-primary",
867
+ )
868
+
869
+ # ── CENTER: Arrow ──
870
+ with gr.Column(scale=1, min_width=60):
871
+ gr.HTML('<div class="mc-arrow">➜</div>')
872
+
873
+ # ── RIGHT: Output Column ──
874
+ with gr.Column(scale=5):
875
+ gr.HTML('<div class="mc-section-label"><span class="mc-icon">🧱</span> MINECRAFT VERSION</div>')
876
+
877
+ with gr.Group(elem_classes="mc-panel"):
878
+ output_image = gr.Image(
879
+ type="pil",
880
+ label="Minecraft version",
881
+ interactive=False,
882
+ height=400,
883
+ )
884
+
885
+ # ── Tips Section ──
886
+ gr.HTML("""
887
+ <div class="mc-tips">
888
+ <div class="mc-tip-title">📖 TIPS & TRICKS</div>
889
+ <div class="mc-divider" style="margin: 10px 0;"></div>
890
+ <ul style="padding-left: 20px; margin: 8px 0;">
891
+ <li>🎯 <strong style="color:#7CB342;">Inference steps:</strong> 25 is the sweet spot. Go higher for more detail.</li>
892
+ <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>
893
+ <li>⏱️ <strong style="color:#7CB342;">Live mode:</strong> The camera stream prioritizes the newest frame whenever the model is free.</li>
894
+ <li>🎲 <strong style="color:#7CB342;">Variations:</strong> Change the seed to get different Minecraft interpretations of the same photo.</li>
895
+ <li>🖼️ <strong style="color:#7CB342;">Best results:</strong> Clear, well-lit photos with distinct objects work great!</li>
896
+ </ul>
897
+ </div>
898
+ """)
899
+
900
+ # ── Footer ──
901
+ gr.HTML("""
902
+ <div class="mc-footer">
903
+ <p>Built with 💚 and blocks · Powered by FLUX.2 + Minecraft LoRA</p>
904
+ <p style="font-size: 0.8rem !important; margin-top: 4px;">Not affiliated with Mojang or Microsoft</p>
905
+ </div>
906
+ """)
907
+
908
+ # ── Event binding ──
909
+ live_running = gr.State(False)
910
+
911
+ input_mode.change(
912
+ fn=switch_input_mode,
913
+ inputs=input_mode,
914
+ outputs=[
915
+ image_input_group,
916
+ run_btn,
917
+ live_input_group,
918
+ start_live_btn,
919
+ stop_live_btn,
920
+ live_running,
921
+ ],
922
+ queue=False,
923
+ )
924
+
925
+ run_btn.click(
926
+ fn=minecraftify,
927
+ inputs=[input_image, steps, guidance_scale, seed, extra_details],
928
+ outputs=output_image,
929
+ )
930
+
931
+ start_live_btn.click(
932
+ fn=start_live,
933
+ inputs=None,
934
+ outputs=[live_running, start_live_btn, stop_live_btn],
935
+ queue=False,
936
+ )
937
+
938
+ stop_live_btn.click(
939
+ fn=stop_live,
940
+ inputs=None,
941
+ outputs=[live_running, start_live_btn, stop_live_btn],
942
+ queue=False,
943
+ )
944
+
945
+ live_image.stream(
946
+ fn=minecraftify_live,
947
+ inputs=[live_image, live_running, steps, guidance_scale, seed, extra_details],
948
+ outputs=output_image,
949
+ stream_every=0.3,
950
+ trigger_mode="always_last",
951
+ concurrency_limit=1,
952
  )
953
 
954
+ demo.queue(max_size=1).launch()
 
requirements.txt CHANGED
@@ -1,6 +1,11 @@
1
- gradio
2
- fastrtc
3
- modal
4
- opencv-python-headless
5
- pillow
6
- numpy
 
 
 
 
 
 
1
+ gradio>=5.0
2
+ pillow>=12.0
3
+ torch>=2.11.0
4
+ transformers>=5.12.0
5
+ accelerate>=1.14.0
6
+ diffusers>=0.38.0
7
+ safetensors>=0.8.0
8
+ sentencepiece>=0.2.1
9
+ huggingface-hub>=1.19.0
10
+ spaces
11
+ peft>=0.11.0