AnimeOverlord commited on
Commit
077cba7
·
1 Parent(s): 0977f6f

still initial commit

Browse files
Files changed (2) hide show
  1. app.py +120 -114
  2. backend/backend.py +12 -33
app.py CHANGED
@@ -5,192 +5,198 @@ import gradio as gr
5
  import modal
6
  from fastrtc import WebRTC, get_cloudflare_turn_credentials
7
 
8
- # Initialize Modal connection securely on startup with clear logging
9
  try:
10
- print("🚀 Attempting to connect to Modal VoxelModel class...")
11
  VoxelModelCls = modal.Cls.from_name("flux-klein-voxel-backend", "VoxelModel")
12
  voxel_backend = VoxelModelCls().process_frame
 
13
  except Exception as e:
14
- print(f"⚠️ Modal Cls lookup failed: {e}. Falling back to Function lookup.")
15
  try:
16
  voxel_backend = modal.Function.from_name("flux-klein-voxel-backend", "demo_stream_frame")
 
17
  except Exception as ex:
18
- print(f"⚠️ Modal Function lookup also failed: {ex}. Backend will be offline.")
19
  voxel_backend = None
20
 
21
 
22
- # --- STREAM HANDLING FUNCTIONS ---
23
-
24
- def process_video_stream(frame: np.ndarray, prompt: str, strength: float) -> np.ndarray:
25
  """
26
- Handles single viewport mode (Minecraft Filter).
27
- Receives frames via WebRTC, processes them, and returns them to the same viewport.
28
  """
29
- if frame is None:
30
- return None
31
-
32
- if voxel_backend is None:
33
- output_frame = frame.copy()
34
- cv2.putText(output_frame, "ERROR: Modal Backend Offline", (20, 40),
35
- cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 255), 2)
36
- return output_frame
37
-
38
- success, encoded_image = cv2.imencode(".jpg", frame, [int(cv2.IMWRITE_JPEG_QUALITY), 85])
39
  if not success:
40
  return frame
41
-
42
- frame_bytes = encoded_image.tobytes()
43
 
44
  try:
45
  try:
46
- processed_bytes = voxel_backend.remote(frame_bytes, prompt, strength)
47
  except TypeError:
48
- processed_bytes = voxel_backend.remote(frame_bytes)
49
-
50
- numpy_buffer = np.frombuffer(processed_bytes, dtype=np.uint8)
51
- return cv2.imdecode(numpy_buffer, cv2.IMREAD_COLOR)
 
 
 
52
  except Exception as err:
53
- fallback_frame = frame.copy()
54
- cv2.putText(fallback_frame, "Serverless Processing...", (20, 40),
55
- cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 255), 2)
56
- return fallback_frame
57
 
58
 
59
- def process_demo_stream(frame: np.ndarray) -> np.ndarray:
 
 
 
 
 
 
 
 
 
 
 
60
  """
61
- Handles separate multi-viewport tracking (Streaming Demo).
62
- Ingests raw hardware frames and renders processed outputs to a separate container.
63
  """
64
  if frame is None:
65
  return None
66
-
67
  if voxel_backend is None:
68
- error_frame = np.zeros((480, 640, 3), dtype=np.uint8)
69
- error_frame[:] = 30
70
- cv2.putText(error_frame, "BACKEND APP OFFLINE", (40, 200),
71
- cv2.FONT_HERSHEY_DUPLEX, 0.9, (0, 0, 255), 2)
72
- return error_frame
73
 
74
- success, encoded_image = cv2.imencode(".jpg", frame, [int(cv2.IMWRITE_JPEG_QUALITY), 85])
75
- if not success:
76
- return frame
77
-
78
- frame_bytes = encoded_image.tobytes()
79
 
80
- try:
81
- try:
82
- processed_bytes = voxel_backend.remote(frame_bytes, "vanilla minecraft voxel landscape", 0.55)
83
- except TypeError:
84
- processed_bytes = voxel_backend.remote(frame_bytes)
85
-
86
- numpy_buffer = np.frombuffer(processed_bytes, dtype=np.uint8)
87
- return cv2.imdecode(numpy_buffer, cv2.IMREAD_COLOR)
88
 
89
- except Exception as err:
90
- error_frame = np.zeros((480, 640, 3), dtype=np.uint8)
91
- error_frame[:] = 20
92
- cv2.putText(error_frame, "MODAL RUNTIME EXCEPTION", (40, 180),
93
- cv2.FONT_HERSHEY_DUPLEX, 0.8, (0, 140, 255), 2)
94
- cv2.putText(error_frame, f"Error: {str(err)[:40]}...", (40, 240),
95
- cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1)
96
- return error_frame
97
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
 
99
- # --- PURE NATIVE GRADIO UI LAYOUT ---
 
 
 
 
 
100
 
 
 
 
 
 
101
  with gr.Blocks(title="Minecraft Spatial Voxel Filter") as demo:
102
-
103
- # Header Section
104
  gr.Markdown("<h1 style='text-align: center;'>⛏️ MINECRAFT SPATIAL VOXEL FILTER ⛏️</h1>")
105
- gr.Markdown("<p style='text-align: center;'>Transform your physical environment into an interactive, real-time 3D blocky landscape.</p>")
106
-
107
- # Global Controls
108
  mode_dropdown = gr.Dropdown(
109
  choices=["Minecraft Filter", "Streaming Demo"],
110
  value="Minecraft Filter",
111
  label="🎯 Pipeline View Configuration",
112
- interactive=True
113
  )
114
-
115
- # Main Application Area (Row keeps things side-by-side cleanly)
116
  with gr.Row():
117
-
118
- # Left Column: Settings
119
  with gr.Column(scale=1):
120
  gr.Markdown("### 🎛️ Environmental Filters")
121
-
122
  prompt_input = gr.Textbox(
123
  value="vanilla minecraft voxel landscape, 3d blocky style, retro game cube aesthetic",
124
- label="Biome Environment Blueprint (Prompt)",
125
- lines=3
126
  )
127
-
128
  denoise_strength = gr.Slider(
129
  minimum=0.1, maximum=1.0, step=0.05, value=0.55,
130
- label="Voxelization Denoising Strength"
131
  )
132
-
133
- status_msg = "Connected" if voxel_backend is not None else "Offline / Error"
134
- gr.Markdown(f"**Pipeline Status:** Deployed via FastRTC. Modal Backend: `{status_msg}`.")
135
-
136
- # Right Column: Video Viewports
 
 
137
  with gr.Column(scale=2):
138
-
139
- # Variant 1: Minecraft Filter
140
  with gr.Group(visible=True) as minecraft_layout:
141
  gr.Markdown("### 📺 Spatial Render Pipeline")
142
- # The WebRTC component natively provides Start/Stop buttons in its UI
143
  webrtc_single = WebRTC(
144
  label="Live Voxel Viewport",
145
  modality="video",
146
  mode="send-receive",
147
- rtc_configuration=get_cloudflare_turn_credentials
148
  )
149
-
150
- # Variant 2: Streaming Demo
 
 
151
  with gr.Group(visible=False) as demo_layout:
152
  gr.Markdown("### 📺 Dual-Feed Stream Monitor")
153
- with gr.Row():
154
- webrtc_raw = WebRTC(
155
- label="Raw Local Camera",
156
- modality="video",
157
- mode="send",
158
- rtc_configuration=get_cloudflare_turn_credentials
159
- )
160
- webrtc_processed = WebRTC(
161
- label="Processed Backend Output",
162
- modality="video",
163
- mode="receive",
164
- rtc_configuration=get_cloudflare_turn_credentials
165
- )
166
-
167
- # --- ROUTING LOGIC & EVENT WIRES ---
168
-
169
  def switch_layout(selected_mode):
170
- if selected_mode == "Minecraft Filter":
171
- return gr.update(visible=True), gr.update(visible=False)
172
- else:
173
- return gr.update(visible=False), gr.update(visible=True)
174
 
175
  mode_dropdown.change(
176
  fn=switch_layout,
177
  inputs=[mode_dropdown],
178
- outputs=[minecraft_layout, demo_layout]
179
  )
180
 
181
- # Stream bindings
182
  webrtc_single.stream(
183
  fn=process_video_stream,
184
  inputs=[webrtc_single, prompt_input, denoise_strength],
185
  outputs=[webrtc_single],
186
- time_limit=150
 
187
  )
188
 
189
- webrtc_raw.stream(
190
  fn=process_demo_stream,
191
- inputs=[webrtc_raw],
192
- outputs=[webrtc_processed],
193
- time_limit=150
 
194
  )
195
 
196
  if __name__ == "__main__":
 
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")
12
  voxel_backend = VoxelModelCls().process_frame
13
+ print("✅ Modal Cls connected.")
14
  except Exception as e:
15
+ print(f"⚠️ Modal Cls failed: {e}. Trying Function fallback...")
16
  try:
17
  voxel_backend = modal.Function.from_name("flux-klein-voxel-backend", "demo_stream_frame")
18
+ print("✅ Modal Function connected.")
19
  except Exception as ex:
20
+ print(f" Modal backend offline: {ex}")
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),
48
+ cv2.FONT_HERSHEY_SIMPLEX, 0.55, (0, 0, 255), 1)
49
+ return out
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)
59
+ return out
60
+
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(
92
+ frame,
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>")
117
+
 
118
  mode_dropdown = gr.Dropdown(
119
  choices=["Minecraft Filter", "Streaming Demo"],
120
  value="Minecraft Filter",
121
  label="🎯 Pipeline View Configuration",
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",
134
+ lines=3,
135
  )
 
136
  denoise_strength = gr.Slider(
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(
153
  label="Live Voxel Viewport",
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"),
176
+ gr.update(visible=selected_mode == "Streaming Demo"),
177
+ )
178
 
179
  mode_dropdown.change(
180
  fn=switch_layout,
181
  inputs=[mode_dropdown],
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],
189
  outputs=[webrtc_single],
190
+ time_limit=150,
191
+ concurrency_limit=4,
192
  )
193
 
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
  )
201
 
202
  if __name__ == "__main__":
backend/backend.py CHANGED
@@ -2,7 +2,7 @@ import io
2
  import os
3
  import modal
4
 
5
- # Define container environment optimized for lightning-fast image-to-image processing
6
  image = modal.Image.debian_slim(python_version="3.12").pip_install(
7
  "diffusers",
8
  "transformers",
@@ -14,86 +14,65 @@ image = modal.Image.debian_slim(python_version="3.12").pip_install(
14
  app = modal.App("flux-klein-voxel-backend", image=image)
15
 
16
  # ==============================================================================
17
- # 🏎️ 1. THE DEMO PIPELINE (Zero Cold-Start / Instant WebRTC Echo Test)
18
  # ==============================================================================
19
  @app.function()
20
  def demo_stream_frame(img_bytes: bytes) -> bytes:
21
- """
22
- Decodes the incoming WebRTC frame and returns it instantly.
23
- Does not spin up a GPU or load a model. Use this to verify that the
24
- frontend WebRTC connection is 100% functional.
25
- """
26
  from PIL import Image, ImageDraw
27
 
28
- # Unpack the binary stream sent by FastRTC
29
  input_image = Image.open(io.BytesIO(img_bytes)).convert("RGB")
30
-
31
- # Optional visual overlay so you know the demo bypass is active
32
  draw = ImageDraw.Draw(input_image)
33
  draw.text((20, 20), "🛠️ WEBRTC PASSTHROUGH DEMO ACTIVE", fill=(0, 255, 0))
34
- draw.text((20, 40), "Inference model bypassed.", fill=(255, 255, 255))
35
 
36
- # Pack back into high-speed compressed JPEG format
37
  output_buffer = io.BytesIO()
38
  input_image.save(output_buffer, format="JPEG", quality=85)
39
  return output_buffer.getvalue()
40
 
41
 
42
  # ==============================================================================
43
- # 🚀 2. THE REAL-TIME VOXEL ENGINE (GPU-Accelerated Inference)
44
  # ==============================================================================
45
  @app.cls(
46
  gpu="A10G",
47
- secrets=[modal.Secret.from_name("huggingface")],
48
- concurrency_limit=10 # Scales automatically up to 10 parallel video streams
 
49
  )
50
  class VoxelModel:
51
 
52
  @modal.enter()
53
  def load_pipeline(self):
54
- """Pre-loads model checkpoints into serverless VRAM exactly once upon container initialization"""
55
  import torch
56
- from diffusers import DiffusionPipeline
57
 
58
- # Target your specific fine-tuned space or the base black-forest-labs/FLUX.2-klein-4B
59
  model_id = "AnimeOverlord/flux2-klein-4b-mc"
60
- print(f"📦 Spin up sequence initiated. Pulling weights for {model_id}...")
61
 
62
- # DiffusionPipeline dynamically handles custom fine-tune repo definitions via model_index.json
63
- self.pipe = DiffusionPipeline.from_pretrained(
64
  model_id,
65
  torch_dtype=torch.bfloat16,
66
- token=os.environ["HF_TOKEN"]
67
  )
68
  self.pipe.to("cuda")
69
-
70
- # Performance Tweaks for low-latency video loops
71
  self.pipe.enable_attention_slicing()
72
- print("⚡ Core weights successfully loaded into cloud VRAM.")
73
 
74
- @modal.function()
75
  def process_frame(self, img_bytes: bytes, prompt: str, strength: float) -> bytes:
76
- """Executes targeted frame transformations without saving overhead data to memory"""
77
  from PIL import Image
78
  import torch
79
 
80
- # 1. Unpack compressed binary frame directly from network interface
81
  input_image = Image.open(io.BytesIO(img_bytes)).convert("RGB")
82
-
83
- # Hard clamping constraint resolution guarantees predictable frame-rates
84
  input_image = input_image.resize((512, 512))
85
 
86
- # 2. Process frame via low-step inference execution
87
  with torch.inference_mode():
88
  output_image = self.pipe(
89
  prompt=prompt,
90
  image=input_image,
91
  strength=strength,
92
- num_inference_steps=4, # Hard locked to match FLUX.2 Klein's step-distilled architecture
93
  guidance_scale=3.5,
94
  ).images[0]
95
 
96
- # 3. Re-pack processing output back to JPEG format for transit
97
  output_buffer = io.BytesIO()
98
  output_image.save(output_buffer, format="JPEG", quality=85)
99
  return output_buffer.getvalue()
 
2
  import os
3
  import modal
4
 
5
+ # Define container environment
6
  image = modal.Image.debian_slim(python_version="3.12").pip_install(
7
  "diffusers",
8
  "transformers",
 
14
  app = modal.App("flux-klein-voxel-backend", image=image)
15
 
16
  # ==============================================================================
17
+ # 🏎️ 1. THE DEMO PIPELINE
18
  # ==============================================================================
19
  @app.function()
20
  def demo_stream_frame(img_bytes: bytes) -> bytes:
 
 
 
 
 
21
  from PIL import Image, ImageDraw
22
 
 
23
  input_image = Image.open(io.BytesIO(img_bytes)).convert("RGB")
 
 
24
  draw = ImageDraw.Draw(input_image)
25
  draw.text((20, 20), "🛠️ WEBRTC PASSTHROUGH DEMO ACTIVE", fill=(0, 255, 0))
 
26
 
 
27
  output_buffer = io.BytesIO()
28
  input_image.save(output_buffer, format="JPEG", quality=85)
29
  return output_buffer.getvalue()
30
 
31
 
32
  # ==============================================================================
33
+ # 🚀 2. THE REAL-TIME VOXEL ENGINE
34
  # ==============================================================================
35
  @app.cls(
36
  gpu="A10G",
37
+ # Using the secret name from your example: "huggingface-secret"
38
+ secrets=[modal.Secret.from_name("huggingface-secret")],
39
+ max_containers=5
40
  )
41
  class VoxelModel:
42
 
43
  @modal.enter()
44
  def load_pipeline(self):
 
45
  import torch
46
+ from diffusers import AutoPipelineForImage2Image
47
 
 
48
  model_id = "AnimeOverlord/flux2-klein-4b-mc"
 
49
 
50
+ # Following your provided pattern for auth
51
+ self.pipe = AutoPipelineForImage2Image.from_pretrained(
52
  model_id,
53
  torch_dtype=torch.bfloat16,
54
+ use_auth_token=os.environ["HF_TOKEN"]
55
  )
56
  self.pipe.to("cuda")
 
 
57
  self.pipe.enable_attention_slicing()
 
58
 
59
+ @modal.method()
60
  def process_frame(self, img_bytes: bytes, prompt: str, strength: float) -> bytes:
 
61
  from PIL import Image
62
  import torch
63
 
 
64
  input_image = Image.open(io.BytesIO(img_bytes)).convert("RGB")
 
 
65
  input_image = input_image.resize((512, 512))
66
 
 
67
  with torch.inference_mode():
68
  output_image = self.pipe(
69
  prompt=prompt,
70
  image=input_image,
71
  strength=strength,
72
+ num_inference_steps=4,
73
  guidance_scale=3.5,
74
  ).images[0]
75
 
 
76
  output_buffer = io.BytesIO()
77
  output_image.save(output_buffer, format="JPEG", quality=85)
78
  return output_buffer.getvalue()