AnimeOverlord commited on
Commit
f47cc76
·
1 Parent(s): 7f377fc

still initial commit

Browse files
Files changed (1) hide show
  1. app.py +53 -48
app.py CHANGED
@@ -1,40 +1,44 @@
1
  import os
 
2
  import gradio as gr
3
  import cv2
4
  import numpy as np
5
  import modal
6
- # ── Authentication & Modal Connection ───────────────────────────────────────
 
7
  token_id = os.environ.get("MODAL_TOKEN_ID")
8
  token_secret = os.environ.get("MODAL_TOKEN_SECRET")
9
  has_tokens = bool(token_id and token_secret)
10
 
11
- status_text = "🟢 Ready" if has_tokens else "🔴 Offline (Missing Tokens)"
12
  if not has_tokens:
13
  print("⚠️ [AUTH ERROR] Modal tokens missing! Check your Hugging Face Secrets.")
14
 
15
- # Cache to prevent Python 3.13 cross-thread loop crashes
16
- _voxel_backend = None
17
 
 
18
  def get_modal_backend():
19
- """Lazy-loads the Modal client inside the execution thread to protect the event loop."""
20
- global _voxel_backend
21
- if _voxel_backend is None and has_tokens:
22
- print("🔑 Connecting to Modal...")
23
- VoxelModelCls = modal.Cls.from_name("flux-klein-voxel-backend", "VoxelModel")
24
- _voxel_backend = VoxelModelCls().process_frame
25
- print("✅ Successfully connected to Modal backend.")
26
- return _voxel_backend
27
-
28
-
29
- # ── Core Backend Execution ──────────────────────────────────────────────────
30
- def run_modal_backend(frame: np.ndarray) -> np.ndarray:
31
- """Compresses the frame, sends it to Modal, and decodes the returned bytes."""
32
- # Look up dynamically to prevent cross-thread event loop pollution
33
  try:
34
  VoxelModelCls = modal.Cls.from_name("flux-klein-voxel-backend", "VoxelModel")
35
- backend = VoxelModelCls().process_frame
36
  except Exception as e:
37
  print(f"❌ Failed to resolve Modal class: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  return frame
39
 
40
  # Compress to JPEG to save network bandwidth
@@ -43,59 +47,62 @@ def run_modal_backend(frame: np.ndarray) -> np.ndarray:
43
  return frame
44
 
45
  try:
46
- # Fire bytes to Modal serverless container synchronously
47
- processed_bytes = backend.remote(encoded.tobytes())
 
 
 
48
  # Decode the returning bytes back into an OpenCV image
49
  result = cv2.imdecode(np.frombuffer(processed_bytes, dtype=np.uint8), cv2.IMREAD_COLOR)
50
  return result if result is not None else frame
51
  except Exception as e:
52
  print(f"Modal execution error: {e}")
53
- # Draw error text on frame if backend crashes
54
  err_frame = frame.copy()
55
  cv2.putText(err_frame, "Backend Error - Check Console", (10, 40),
56
  cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)
57
  return err_frame
58
 
 
59
  # ── Activation & Pre-Warming Logic ──────────────────────────────────────────
60
- def start_and_warmup_container():
61
- """Forces the Modal container to start up before enabling the webcam stream."""
62
  print("🚀 [START CLICKED] Waking up Modal container to prevent cold-start lag...")
63
- try:
64
- VoxelModelCls = modal.Cls.from_name("flux-klein-voxel-backend", "VoxelModel")
65
- backend = VoxelModelCls().process_frame
66
-
67
- # Create a tiny 1x1 blank image payload
68
- dummy_frame = np.zeros((1, 1, 3), dtype=np.uint8)
69
- success, encoded = cv2.imencode(".jpg", dummy_frame)
70
- if success:
71
- print("⏳ Sending ignition payload to remote container...")
72
- backend.remote(encoded.tobytes())
73
- print("✅ [CONTAINER READY] Modal container is hot and ready for frames.")
74
- except Exception as e:
75
- print(f"ℹ️ [CONTAINER NOTIFICATION] Warmup pipeline updated: {e}")
76
-
 
77
  return True
78
 
 
79
  # ── Streaming Logic ─────────────────────────────────────────────────────────
80
- def process_video_stream(frame: np.ndarray, mode: str, is_running: bool) -> np.ndarray:
81
  """Handles the webcam feed and respects the Start/Stop toggle."""
82
  if frame is None:
83
  return None
84
 
85
  # CRITICAL: If the user hasn't clicked Start, do NOT send to Modal.
86
- # Just loop the raw webcam feed back to the UI.
87
  if not is_running:
88
  return frame
89
 
90
- # 1. Process the frame through the Modal network
91
- processed = run_modal_backend(frame)
92
 
93
  # 2. Format the output based on the selected UI mode
94
  if mode == "Minecraft Filter":
95
  return processed
96
 
97
  elif mode == "Streaming Demo":
98
- # Force matching dimensions for side-by-side concatenation
99
  if processed.shape != frame.shape:
100
  processed = cv2.resize(processed, (frame.shape[1], frame.shape[0]))
101
 
@@ -109,7 +116,6 @@ def process_video_stream(frame: np.ndarray, mode: str, is_running: bool) -> np.n
109
 
110
  # ── Gradio UI Layout ────────────────────────────────────────────────────────
111
  with gr.Blocks(title="⛏️ Minecraft Spatial Voxel Filter") as demo:
112
- # State tracking variable: Controls whether data flows to Modal or not
113
  is_running = gr.State(value=False)
114
 
115
  gr.Markdown("# ⛏️ Minecraft Spatial Voxel Filter")
@@ -134,17 +140,16 @@ with gr.Blocks(title="⛏️ Minecraft Spatial Voxel Filter") as demo:
134
  input_stream = gr.Image(sources=["webcam"], streaming=True, label="Live Webcam Input")
135
  output_stream = gr.Image(interactive=False, label="Voxel Output Viewport")
136
 
137
- # Wire the buttons to manage the state boolean
138
- # The start button runs the ignition function first before setting state to True
139
  start_btn.click(fn=start_and_warmup_container, inputs=None, outputs=is_running)
140
  stop_btn.click(fn=lambda: False, inputs=None, outputs=is_running)
141
 
142
- # The core continuous loop
143
  input_stream.stream(
144
  fn=process_video_stream,
145
  inputs=[input_stream, mode_dropdown, is_running],
146
  outputs=[output_stream],
147
- trigger_mode="always_last" # Drops frames if network backs up to prevent lag
148
  )
149
 
150
  if __name__ == "__main__":
 
1
  import os
2
+ import asyncio
3
  import gradio as gr
4
  import cv2
5
  import numpy as np
6
  import modal
7
+
8
+ # ── Authentication & Token Guard ────────────────────────────────────────────
9
  token_id = os.environ.get("MODAL_TOKEN_ID")
10
  token_secret = os.environ.get("MODAL_TOKEN_SECRET")
11
  has_tokens = bool(token_id and token_secret)
12
 
13
+ status_text = "🟢 Connected to Modal" if has_tokens else "🔴 Offline (Missing Tokens)"
14
  if not has_tokens:
15
  print("⚠️ [AUTH ERROR] Modal tokens missing! Check your Hugging Face Secrets.")
16
 
 
 
17
 
18
+ # ── Thread-Safe Client Resolver ─────────────────────────────────────────────
19
  def get_modal_backend():
20
+ """Resolves the Modal method safely on the active worker thread."""
21
+ if not has_tokens:
22
+ return None
 
 
 
 
 
 
 
 
 
 
 
23
  try:
24
  VoxelModelCls = modal.Cls.from_name("flux-klein-voxel-backend", "VoxelModel")
25
+ return VoxelModelCls().process_frame
26
  except Exception as e:
27
  print(f"❌ Failed to resolve Modal class: {e}")
28
+ return None
29
+
30
+
31
+ # ── Sync Execution Core (Isolated) ─────────────────────────────────────────
32
+ def _execute_remote_call(backend, payload_bytes: bytes) -> bytes:
33
+ """The raw network request executed completely outside the event loop."""
34
+ return backend.remote(payload_bytes)
35
+
36
+
37
+ # ── Core Async Wrapper ──────────────────────────────────────────────────────
38
+ async def run_modal_backend(frame: np.ndarray) -> np.ndarray:
39
+ """Compresses the frame and uses to_thread to bypass event loop deadlocks."""
40
+ backend = get_modal_backend()
41
+ if backend is None:
42
  return frame
43
 
44
  # Compress to JPEG to save network bandwidth
 
47
  return frame
48
 
49
  try:
50
+ # CRITICAL: asyncio.to_thread completely bypasses Python 3.13 loop deadlocks!
51
+ processed_bytes = await asyncio.to_thread(
52
+ _execute_remote_call, backend, encoded.tobytes()
53
+ )
54
+
55
  # Decode the returning bytes back into an OpenCV image
56
  result = cv2.imdecode(np.frombuffer(processed_bytes, dtype=np.uint8), cv2.IMREAD_COLOR)
57
  return result if result is not None else frame
58
  except Exception as e:
59
  print(f"Modal execution error: {e}")
 
60
  err_frame = frame.copy()
61
  cv2.putText(err_frame, "Backend Error - Check Console", (10, 40),
62
  cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)
63
  return err_frame
64
 
65
+
66
  # ── Activation & Pre-Warming Logic ──────────────────────────────────────────
67
+ async def start_and_warmup_container():
68
+ """Forces the Modal container to start up via an isolated worker thread."""
69
  print("🚀 [START CLICKED] Waking up Modal container to prevent cold-start lag...")
70
+
71
+ backend = get_modal_backend()
72
+ if backend is not None:
73
+ try:
74
+ # Create a tiny 1x1 blank image payload
75
+ dummy_frame = np.zeros((1, 1, 3), dtype=np.uint8)
76
+ success, encoded = cv2.imencode(".jpg", dummy_frame)
77
+ if success:
78
+ print("⏳ Sending ignition payload to remote container...")
79
+ # Fire the warmup safely on its own isolated thread context
80
+ await asyncio.to_thread(_execute_remote_call, backend, encoded.tobytes())
81
+ print("✅ [CONTAINER READY] Modal container is hot and ready for frames.")
82
+ except Exception as e:
83
+ print(f"ℹ️ [CONTAINER NOTIFICATION] Warmup call dispatched: {e}")
84
+
85
  return True
86
 
87
+
88
  # ── Streaming Logic ─────────────────────────────────────────────────────────
89
+ async def process_video_stream(frame: np.ndarray, mode: str, is_running: bool) -> np.ndarray:
90
  """Handles the webcam feed and respects the Start/Stop toggle."""
91
  if frame is None:
92
  return None
93
 
94
  # CRITICAL: If the user hasn't clicked Start, do NOT send to Modal.
 
95
  if not is_running:
96
  return frame
97
 
98
+ # 1. Process the frame through our async-safe Modal bridge
99
+ processed = await run_modal_backend(frame)
100
 
101
  # 2. Format the output based on the selected UI mode
102
  if mode == "Minecraft Filter":
103
  return processed
104
 
105
  elif mode == "Streaming Demo":
 
106
  if processed.shape != frame.shape:
107
  processed = cv2.resize(processed, (frame.shape[1], frame.shape[0]))
108
 
 
116
 
117
  # ── Gradio UI Layout ────────────────────────────────────────────────────────
118
  with gr.Blocks(title="⛏️ Minecraft Spatial Voxel Filter") as demo:
 
119
  is_running = gr.State(value=False)
120
 
121
  gr.Markdown("# ⛏️ Minecraft Spatial Voxel Filter")
 
140
  input_stream = gr.Image(sources=["webcam"], streaming=True, label="Live Webcam Input")
141
  output_stream = gr.Image(interactive=False, label="Voxel Output Viewport")
142
 
143
+ # Wire buttons to manage state and trigger container wakeup
 
144
  start_btn.click(fn=start_and_warmup_container, inputs=None, outputs=is_running)
145
  stop_btn.click(fn=lambda: False, inputs=None, outputs=is_running)
146
 
147
+ # Main non-blocking stream loop
148
  input_stream.stream(
149
  fn=process_video_stream,
150
  inputs=[input_stream, mode_dropdown, is_running],
151
  outputs=[output_stream],
152
+ trigger_mode="always_last"
153
  )
154
 
155
  if __name__ == "__main__":