AnimeOverlord commited on
Commit
176f153
·
1 Parent(s): 077cba7

still initial commit

Browse files
Files changed (1) hide show
  1. app.py +51 -53
app.py CHANGED
@@ -5,7 +5,7 @@ import gradio as gr
5
  import modal
6
  from fastrtc import WebRTC, get_cloudflare_turn_credentials
7
 
8
- # ── Modal Backend Connection ────────────────────────────────────────────────
9
  try:
10
  print("🚀 Connecting to Modal VoxelModel...")
11
  VoxelModelCls = modal.Cls.from_name("flux-klein-voxel-backend", "VoxelModel")
@@ -21,27 +21,18 @@ except Exception as e:
21
  voxel_backend = None
22
 
23
 
24
- # ── Shared Core Processing ──────────────────────────────────────────────────
25
  def _run_voxel_backend(frame: np.ndarray, prompt: str, strength: float) -> np.ndarray:
26
- """
27
- Encodes frame → sends to Modal → decodes result.
28
- Returns processed frame, or annotated original on failure.
29
- """
30
  success, encoded = cv2.imencode(".jpg", frame, [int(cv2.IMWRITE_JPEG_QUALITY), 85])
31
  if not success:
32
  return frame
33
-
34
  try:
35
  try:
36
  processed_bytes = voxel_backend.remote(encoded.tobytes(), prompt, strength)
37
  except TypeError:
38
- # Backend may not accept prompt/strength yet
39
  processed_bytes = voxel_backend.remote(encoded.tobytes())
40
-
41
  result = cv2.imdecode(np.frombuffer(processed_bytes, dtype=np.uint8), cv2.IMREAD_COLOR)
42
- # Guard against decode failure
43
  return result if result is not None else frame
44
-
45
  except Exception as err:
46
  out = frame.copy()
47
  cv2.putText(out, f"Modal error: {str(err)[:45]}", (10, 30),
@@ -50,9 +41,7 @@ def _run_voxel_backend(frame: np.ndarray, prompt: str, strength: float) -> np.nd
50
 
51
 
52
  def _offline_frame(frame: np.ndarray, message: str) -> np.ndarray:
53
- """Returns an error-annotated frame matching the input dimensions."""
54
- # FIX BUG 3: match input frame shape instead of hardcoded (480, 640)
55
- h, w = frame.shape[:2] if frame is not None else (480, 640)
56
  out = np.zeros((h, w, 3), dtype=np.uint8)
57
  cv2.putText(out, message, (max(10, w // 8), h // 2),
58
  cv2.FONT_HERSHEY_DUPLEX, 0.8, (0, 0, 220), 2)
@@ -61,31 +50,18 @@ def _offline_frame(frame: np.ndarray, message: str) -> np.ndarray:
61
 
62
  # ── Stream Handlers ─────────────────────────────────────────────────────────
63
  def process_video_stream(frame: np.ndarray, prompt: str, strength: float) -> np.ndarray:
64
- """
65
- Minecraft Filter mode: transforms frame with user-controlled prompt + strength.
66
- Returns single processed frame → goes back to the same send-receive WebRTC.
67
- """
68
  if frame is None:
69
  return None
70
  if voxel_backend is None:
71
  return _offline_frame(frame, "Modal Backend Offline")
72
-
73
  return _run_voxel_backend(frame, prompt, strength)
74
 
75
 
76
  def process_demo_stream(frame: np.ndarray) -> np.ndarray:
77
- """
78
- Streaming Demo mode: fixed prompt, side-by-side raw + processed.
79
-
80
- FIX BUG 1+2: Instead of routing to a separate mode='receive' WebRTC
81
- (which FastRTC doesn't support via stream()), we combine both feeds
82
- into one frame and return it through the same send-receive component.
83
- """
84
  if frame is None:
85
  return None
86
  if voxel_backend is None:
87
  err = _offline_frame(frame, "Backend Offline")
88
- # Still return side-by-side so layout stays consistent
89
  return np.hstack([frame, err])
90
 
91
  processed = _run_voxel_backend(
@@ -93,24 +69,49 @@ def process_demo_stream(frame: np.ndarray) -> np.ndarray:
93
  prompt="vanilla minecraft voxel landscape, 3d blocky style, cube aesthetic",
94
  strength=0.55,
95
  )
96
-
97
- # Resize processed to match raw height if they differ (rare but safe)
98
  if processed.shape[0] != frame.shape[0]:
99
  processed = cv2.resize(processed, (frame.shape[1], frame.shape[0]))
100
 
101
- # Label both panels
102
  raw_labeled = frame.copy()
103
- cv2.putText(raw_labeled, "RAW", (10, 28),
104
- cv2.FONT_HERSHEY_SIMPLEX, 0.9, (255, 255, 255), 2)
105
- cv2.putText(processed, "MINECRAFT", (10, 28),
106
- cv2.FONT_HERSHEY_SIMPLEX, 0.9, (255, 255, 255), 2)
107
-
108
- # Side-by-side in a single frame returned to one WebRTC component
109
  return np.hstack([raw_labeled, processed])
110
 
111
 
112
- # ── UI ──────────────────────────────────────────────────────────────────────
113
- with gr.Blocks(title="Minecraft Spatial Voxel Filter") as demo:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
 
115
  gr.Markdown("<h1 style='text-align: center;'>⛏️ MINECRAFT SPATIAL VOXEL FILTER ⛏️</h1>")
116
  gr.Markdown("<p style='text-align: center;'>Transform your environment into a real-time blocky landscape.</p>")
@@ -122,12 +123,11 @@ with gr.Blocks(title="Minecraft Spatial Voxel Filter") as demo:
122
  interactive=True,
123
  )
124
 
125
- with gr.Row():
126
 
127
  # ── Left: Controls ─────────────────────────────────────────────────
128
- with gr.Column(scale=1):
129
  gr.Markdown("### 🎛️ Environmental Filters")
130
-
131
  prompt_input = gr.Textbox(
132
  value="vanilla minecraft voxel landscape, 3d blocky style, retro game cube aesthetic",
133
  label="Biome Environment Blueprint",
@@ -137,16 +137,15 @@ with gr.Blocks(title="Minecraft Spatial Voxel Filter") as demo:
137
  minimum=0.1, maximum=1.0, step=0.05, value=0.55,
138
  label="Voxelization Denoising Strength",
139
  )
140
-
141
  status_color = "🟢" if voxel_backend is not None else "🔴"
142
  status_text = "Connected" if voxel_backend is not None else "Offline"
143
  gr.Markdown(f"**Modal Backend:** {status_color} `{status_text}`")
144
  gr.Markdown("_Controls only apply in Minecraft Filter mode._")
145
 
146
- # ── Right: Viewports ───────────────────────────────────────────────
147
- with gr.Column(scale=2):
148
 
149
- # Mode 1: Minecraft Filter — single send-receive, processed output
150
  with gr.Group(visible=True) as minecraft_layout:
151
  gr.Markdown("### 📺 Spatial Render Pipeline")
152
  webrtc_single = WebRTC(
@@ -154,22 +153,21 @@ with gr.Blocks(title="Minecraft Spatial Voxel Filter") as demo:
154
  modality="video",
155
  mode="send-receive",
156
  rtc_configuration=get_cloudflare_turn_credentials,
 
157
  )
158
 
159
- # Mode 2: Streaming Demo
160
- # FIX BUG 1+2: single send-receive WebRTC, side-by-side frame
161
- # returned internally — no separate mode="receive" component needed
162
  with gr.Group(visible=False) as demo_layout:
163
- gr.Markdown("### 📺 Dual-Feed Stream Monitor")
164
- gr.Markdown("_Left: Raw camera · Right: Minecraft output_")
165
  webrtc_demo = WebRTC(
166
  label="Live Side-by-Side Feed",
167
  modality="video",
168
  mode="send-receive",
169
  rtc_configuration=get_cloudflare_turn_credentials,
 
170
  )
171
 
172
- # ── Mode Switch ────────────────────────────────────���────────────────────
173
  def switch_layout(selected_mode):
174
  return (
175
  gr.update(visible=selected_mode == "Minecraft Filter"),
@@ -182,7 +180,7 @@ with gr.Blocks(title="Minecraft Spatial Voxel Filter") as demo:
182
  outputs=[minecraft_layout, demo_layout],
183
  )
184
 
185
- # ── Stream Bindings ─────────────────────────────────────────────────────
186
  webrtc_single.stream(
187
  fn=process_video_stream,
188
  inputs=[webrtc_single, prompt_input, denoise_strength],
@@ -194,7 +192,7 @@ with gr.Blocks(title="Minecraft Spatial Voxel Filter") as demo:
194
  webrtc_demo.stream(
195
  fn=process_demo_stream,
196
  inputs=[webrtc_demo],
197
- outputs=[webrtc_demo], # ← same component, not a separate one
198
  time_limit=150,
199
  concurrency_limit=4,
200
  )
 
5
  import modal
6
  from fastrtc import WebRTC, get_cloudflare_turn_credentials
7
 
8
+ # ── Modal Backend ───────────────────────────────────────────────────────────
9
  try:
10
  print("🚀 Connecting to Modal VoxelModel...")
11
  VoxelModelCls = modal.Cls.from_name("flux-klein-voxel-backend", "VoxelModel")
 
21
  voxel_backend = None
22
 
23
 
24
+ # ── Helpers ─────────────────────────────────────────────────────────────────
25
  def _run_voxel_backend(frame: np.ndarray, prompt: str, strength: float) -> np.ndarray:
 
 
 
 
26
  success, encoded = cv2.imencode(".jpg", frame, [int(cv2.IMWRITE_JPEG_QUALITY), 85])
27
  if not success:
28
  return frame
 
29
  try:
30
  try:
31
  processed_bytes = voxel_backend.remote(encoded.tobytes(), prompt, strength)
32
  except TypeError:
 
33
  processed_bytes = voxel_backend.remote(encoded.tobytes())
 
34
  result = cv2.imdecode(np.frombuffer(processed_bytes, dtype=np.uint8), cv2.IMREAD_COLOR)
 
35
  return result if result is not None else frame
 
36
  except Exception as err:
37
  out = frame.copy()
38
  cv2.putText(out, f"Modal error: {str(err)[:45]}", (10, 30),
 
41
 
42
 
43
  def _offline_frame(frame: np.ndarray, message: str) -> np.ndarray:
44
+ h, w = (frame.shape[:2] if frame is not None else (480, 640))
 
 
45
  out = np.zeros((h, w, 3), dtype=np.uint8)
46
  cv2.putText(out, message, (max(10, w // 8), h // 2),
47
  cv2.FONT_HERSHEY_DUPLEX, 0.8, (0, 0, 220), 2)
 
50
 
51
  # ── Stream Handlers ─────────────────────────────────────────────────────────
52
  def process_video_stream(frame: np.ndarray, prompt: str, strength: float) -> np.ndarray:
 
 
 
 
53
  if frame is None:
54
  return None
55
  if voxel_backend is None:
56
  return _offline_frame(frame, "Modal Backend Offline")
 
57
  return _run_voxel_backend(frame, prompt, strength)
58
 
59
 
60
  def process_demo_stream(frame: np.ndarray) -> np.ndarray:
 
 
 
 
 
 
 
61
  if frame is None:
62
  return None
63
  if voxel_backend is None:
64
  err = _offline_frame(frame, "Backend Offline")
 
65
  return np.hstack([frame, err])
66
 
67
  processed = _run_voxel_backend(
 
69
  prompt="vanilla minecraft voxel landscape, 3d blocky style, cube aesthetic",
70
  strength=0.55,
71
  )
 
 
72
  if processed.shape[0] != frame.shape[0]:
73
  processed = cv2.resize(processed, (frame.shape[1], frame.shape[0]))
74
 
 
75
  raw_labeled = frame.copy()
76
+ cv2.putText(raw_labeled, "RAW", (10, 28), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (255,255,255), 2)
77
+ cv2.putText(processed, "MINECRAFT", (10, 28), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (255,255,255), 2)
 
 
 
 
78
  return np.hstack([raw_labeled, processed])
79
 
80
 
81
+ # ── CSS: KEY FIX — constrain video to its column, never full-screen ─────────
82
+ css = """
83
+ /* Contain both WebRTC viewports inside their grid columns */
84
+ #webrtc-single, #webrtc-demo {
85
+ max-height: 420px !important;
86
+ height: 420px !important;
87
+ overflow: hidden !important;
88
+ }
89
+
90
+ /* The actual <video> element inside each component */
91
+ #webrtc-single video, #webrtc-demo video {
92
+ width: 100% !important;
93
+ height: 420px !important;
94
+ max-height: 420px !important;
95
+ object-fit: contain !important; /* letterbox, never crop or overflow */
96
+ background: #000;
97
+ }
98
+
99
+ /* Gradio wraps WebRTC in .webrtc-container — constrain that too */
100
+ #webrtc-single .webrtc-container,
101
+ #webrtc-demo .webrtc-container {
102
+ max-height: 420px !important;
103
+ overflow: hidden !important;
104
+ }
105
+
106
+ /* Prevent the right column from stretching taller than controls */
107
+ .viewport-col {
108
+ max-height: 520px !important;
109
+ overflow: hidden !important;
110
+ }
111
+ """
112
+
113
+ # ── UI ───────────────────────────────────────────────────────────────────────
114
+ with gr.Blocks(title="Minecraft Spatial Voxel Filter", css=css) as demo:
115
 
116
  gr.Markdown("<h1 style='text-align: center;'>⛏️ MINECRAFT SPATIAL VOXEL FILTER ⛏️</h1>")
117
  gr.Markdown("<p style='text-align: center;'>Transform your environment into a real-time blocky landscape.</p>")
 
123
  interactive=True,
124
  )
125
 
126
+ with gr.Row(equal_height=True):
127
 
128
  # ── Left: Controls ─────────────────────────────────────────────────
129
+ with gr.Column(scale=1, min_width=280):
130
  gr.Markdown("### 🎛️ Environmental Filters")
 
131
  prompt_input = gr.Textbox(
132
  value="vanilla minecraft voxel landscape, 3d blocky style, retro game cube aesthetic",
133
  label="Biome Environment Blueprint",
 
137
  minimum=0.1, maximum=1.0, step=0.05, value=0.55,
138
  label="Voxelization Denoising Strength",
139
  )
 
140
  status_color = "🟢" if voxel_backend is not None else "🔴"
141
  status_text = "Connected" if voxel_backend is not None else "Offline"
142
  gr.Markdown(f"**Modal Backend:** {status_color} `{status_text}`")
143
  gr.Markdown("_Controls only apply in Minecraft Filter mode._")
144
 
145
+ # ── Right: Viewports — fixed-height column ─────────────────────────
146
+ with gr.Column(scale=2, elem_classes=["viewport-col"]):
147
 
148
+ # Mode 1: Minecraft Filter
149
  with gr.Group(visible=True) as minecraft_layout:
150
  gr.Markdown("### 📺 Spatial Render Pipeline")
151
  webrtc_single = WebRTC(
 
153
  modality="video",
154
  mode="send-receive",
155
  rtc_configuration=get_cloudflare_turn_credentials,
156
+ elem_id="webrtc-single", # ← targeted by CSS above
157
  )
158
 
159
+ # Mode 2: Streaming Demo (side-by-side in one component)
 
 
160
  with gr.Group(visible=False) as demo_layout:
161
+ gr.Markdown("### 📺 Dual-Feed · _Left: Raw · Right: Minecraft_")
 
162
  webrtc_demo = WebRTC(
163
  label="Live Side-by-Side Feed",
164
  modality="video",
165
  mode="send-receive",
166
  rtc_configuration=get_cloudflare_turn_credentials,
167
+ elem_id="webrtc-demo", # ← targeted by CSS above
168
  )
169
 
170
+ # ── Mode Switch ──────────────────────────────────────────────────────────
171
  def switch_layout(selected_mode):
172
  return (
173
  gr.update(visible=selected_mode == "Minecraft Filter"),
 
180
  outputs=[minecraft_layout, demo_layout],
181
  )
182
 
183
+ # ── Stream Bindings ─────────────────────────────────────────────────────
184
  webrtc_single.stream(
185
  fn=process_video_stream,
186
  inputs=[webrtc_single, prompt_input, denoise_strength],
 
192
  webrtc_demo.stream(
193
  fn=process_demo_stream,
194
  inputs=[webrtc_demo],
195
+ outputs=[webrtc_demo],
196
  time_limit=150,
197
  concurrency_limit=4,
198
  )