Spaces:
Running
on
Zero
Running
on
Zero
Update app.py
Browse files
app.py
CHANGED
|
@@ -5,8 +5,7 @@ import time
|
|
| 5 |
import unicodedata
|
| 6 |
import gc
|
| 7 |
from io import BytesIO
|
| 8 |
-
from typing import Iterable
|
| 9 |
-
from typing import Tuple, Optional, List, Dict, Any
|
| 10 |
|
| 11 |
import gradio as gr
|
| 12 |
import numpy as np
|
|
@@ -99,7 +98,8 @@ print(f"Running on device: {device}")
|
|
| 99 |
|
| 100 |
# --- Load Fara-7B ---
|
| 101 |
print("π Loading Fara-7B...")
|
| 102 |
-
MODEL_ID_V = "microsoft/Fara-7B"
|
|
|
|
| 103 |
try:
|
| 104 |
processor_v = AutoProcessor.from_pretrained(MODEL_ID_V, trust_remote_code=True)
|
| 105 |
model_v = Qwen2_5_VLForConditionalGeneration.from_pretrained(
|
|
@@ -116,7 +116,6 @@ except Exception as e:
|
|
| 116 |
print("π Loading UI-TARS-1.5-7B...")
|
| 117 |
MODEL_ID_X = "ByteDance-Seed/UI-TARS-1.5-7B"
|
| 118 |
try:
|
| 119 |
-
# Important: use_fast=False is often required for custom tokenizers
|
| 120 |
processor_x = AutoProcessor.from_pretrained(MODEL_ID_X, trust_remote_code=True, use_fast=False)
|
| 121 |
model_x = AutoModelForImageTextToText.from_pretrained(
|
| 122 |
MODEL_ID_X,
|
|
@@ -128,9 +127,9 @@ except Exception as e:
|
|
| 128 |
model_x = None
|
| 129 |
processor_x = None
|
| 130 |
|
| 131 |
-
# --- Load Holo2-8B ---
|
| 132 |
print("π Loading Holo2-8B...")
|
| 133 |
-
MODEL_ID_H = "Hcompany/
|
| 134 |
try:
|
| 135 |
processor_h = AutoProcessor.from_pretrained(MODEL_ID_H, trust_remote_code=True)
|
| 136 |
model_h = AutoModelForImageTextToText.from_pretrained(
|
|
@@ -139,7 +138,7 @@ try:
|
|
| 139 |
torch_dtype=torch.float16
|
| 140 |
).to(device).eval()
|
| 141 |
except Exception as e:
|
| 142 |
-
print(f"Failed to load
|
| 143 |
model_h = None
|
| 144 |
processor_h = None
|
| 145 |
|
|
@@ -147,70 +146,58 @@ print("β
Models loading sequence complete.")
|
|
| 147 |
|
| 148 |
|
| 149 |
# -----------------------------------------------------------------------------
|
| 150 |
-
# 3. UTILS &
|
| 151 |
# -----------------------------------------------------------------------------
|
| 152 |
|
| 153 |
def array_to_image(image_array: np.ndarray) -> Image.Image:
|
| 154 |
if image_array is None: raise ValueError("No image provided.")
|
| 155 |
return Image.fromarray(np.uint8(image_array))
|
| 156 |
|
| 157 |
-
|
| 158 |
-
def get_fara_prompt(task, image):
|
| 159 |
-
OS_SYSTEM_PROMPT = """You are a GUI agent. You are given a task and a screenshot of the current status.
|
| 160 |
-
You need to generate the next action to complete the task.
|
| 161 |
-
Output your action inside a <tool_call> block using JSON format.
|
| 162 |
-
Include "coordinate": [x, y] in pixels for interactions.
|
| 163 |
-
Examples:
|
| 164 |
-
<tool_call>{"name": "User", "arguments": {"action": "click", "coordinate": [400, 300]}}</tool_call>
|
| 165 |
-
<tool_call>{"name": "User", "arguments": {"action": "type", "coordinate": [100, 200], "text": "hello"}}</tool_call>
|
| 166 |
"""
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
{
|
| 181 |
-
"role": "user",
|
| 182 |
-
"content": [
|
| 183 |
-
{"type": "image", "image": image},
|
| 184 |
-
{"type": "text", "text": f"{guidelines}\n{task}"}
|
| 185 |
-
],
|
| 186 |
-
}
|
| 187 |
-
]
|
| 188 |
|
| 189 |
-
def get_image_proc_params(processor) -> Dict[str, int]:
|
| 190 |
-
ip = getattr(processor, "image_processor", None)
|
| 191 |
return {
|
| 192 |
-
"patch_size":
|
| 193 |
-
"merge_size":
|
| 194 |
-
"min_pixels":
|
| 195 |
-
"max_pixels":
|
| 196 |
}
|
| 197 |
|
| 198 |
-
# --- Chat/template helpers ---
|
| 199 |
def apply_chat_template_compat(processor, messages: List[Dict[str, Any]]) -> str:
|
|
|
|
| 200 |
tok = getattr(processor, "tokenizer", None)
|
| 201 |
if hasattr(processor, "apply_chat_template"):
|
| 202 |
return processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
|
| 203 |
if tok is not None and hasattr(tok, "apply_chat_template"):
|
| 204 |
return tok.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
|
| 205 |
-
|
|
|
|
| 206 |
texts = []
|
| 207 |
for m in messages:
|
| 208 |
-
|
| 209 |
-
|
|
|
|
| 210 |
if isinstance(c, dict) and c.get("type") == "text":
|
| 211 |
texts.append(c.get("text", ""))
|
| 212 |
-
elif isinstance(
|
| 213 |
-
texts.append(
|
| 214 |
return "\n".join(texts)
|
| 215 |
|
| 216 |
def batch_decode_compat(processor, token_id_batches, **kw):
|
|
@@ -229,9 +216,44 @@ def trim_generated(generated_ids, inputs):
|
|
| 229 |
return generated_ids
|
| 230 |
return [out_ids[len(in_seq):] for in_seq, out_ids in zip(in_ids, generated_ids)]
|
| 231 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 232 |
|
| 233 |
# -----------------------------------------------------------------------------
|
| 234 |
-
#
|
| 235 |
# -----------------------------------------------------------------------------
|
| 236 |
|
| 237 |
def parse_click_response(text: str) -> List[Dict]:
|
|
@@ -255,6 +277,12 @@ def parse_click_response(text: str) -> List[Dict]:
|
|
| 255 |
matches_box = re.findall(r"start_box=['\"]?\(\s*(\d+)\s*,\s*(\d+)\s*\)['\"]?", text, re.IGNORECASE)
|
| 256 |
for m in matches_box:
|
| 257 |
actions.append({"type": "click", "x": int(m[0]), "y": int(m[1]), "text": ""})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 258 |
|
| 259 |
# Remove duplicates
|
| 260 |
unique_actions = []
|
|
@@ -291,10 +319,9 @@ def create_localized_image(original_image: Image.Image, actions: list[dict]) ->
|
|
| 291 |
if not actions: return None
|
| 292 |
img_copy = original_image.copy()
|
| 293 |
draw = ImageDraw.Draw(img_copy)
|
| 294 |
-
width, height = img_copy.size
|
| 295 |
|
| 296 |
try:
|
| 297 |
-
font = ImageFont.load_default(size=
|
| 298 |
except IOError:
|
| 299 |
font = ImageFont.load_default()
|
| 300 |
|
|
@@ -322,20 +349,21 @@ def create_localized_image(original_image: Image.Image, actions: list[dict]) ->
|
|
| 322 |
|
| 323 |
text_pos = (pixel_x + 25, pixel_y - 15)
|
| 324 |
|
| 325 |
-
# Draw text with background
|
| 326 |
try:
|
| 327 |
bbox = draw.textbbox(text_pos, label, font=font)
|
| 328 |
-
|
|
|
|
|
|
|
| 329 |
draw.text(text_pos, label, fill="white", font=font)
|
| 330 |
except Exception as e:
|
| 331 |
-
|
| 332 |
-
# Fallback if font loading/drawing fails
|
| 333 |
draw.text(text_pos, label, fill="white")
|
| 334 |
|
| 335 |
return img_copy
|
| 336 |
|
| 337 |
# -----------------------------------------------------------------------------
|
| 338 |
-
#
|
| 339 |
# -----------------------------------------------------------------------------
|
| 340 |
|
| 341 |
@spaces.GPU(duration=120)
|
|
@@ -352,6 +380,8 @@ def process_screenshot(input_numpy_image: np.ndarray, task: str, model_choice: s
|
|
| 352 |
if model_choice == "Fara-7B":
|
| 353 |
if model_v is None: return "Error: Fara model failed to load on startup.", None
|
| 354 |
print("Using Fara Pipeline...")
|
|
|
|
|
|
|
| 355 |
messages = get_fara_prompt(task, input_pil_image)
|
| 356 |
text_prompt = processor_v.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
|
| 357 |
image_inputs, video_inputs = process_vision_info(messages)
|
|
@@ -386,12 +416,15 @@ def process_screenshot(input_numpy_image: np.ndarray, task: str, model_choice: s
|
|
| 386 |
else:
|
| 387 |
return f"Error: Unknown model '{model_choice}'", None
|
| 388 |
|
| 389 |
-
# 1. Smart Resize
|
|
|
|
| 390 |
ip_params = get_image_proc_params(processor)
|
|
|
|
| 391 |
resized_h, resized_w = smart_resize(
|
| 392 |
input_pil_image.height, input_pil_image.width,
|
| 393 |
factor=ip_params["patch_size"] * ip_params["merge_size"],
|
| 394 |
-
min_pixels=ip_params["min_pixels"],
|
|
|
|
| 395 |
)
|
| 396 |
proc_image = input_pil_image.resize((resized_w, resized_h), Image.Resampling.LANCZOS)
|
| 397 |
|
|
@@ -414,6 +447,7 @@ def process_screenshot(input_numpy_image: np.ndarray, task: str, model_choice: s
|
|
| 414 |
actions = parse_click_response(raw_response)
|
| 415 |
|
| 416 |
# 6. Rescale Coordinates back to Original Image Size
|
|
|
|
| 417 |
if resized_w > 0 and resized_h > 0:
|
| 418 |
scale_x = orig_w / resized_w
|
| 419 |
scale_y = orig_h / resized_h
|
|
@@ -433,7 +467,7 @@ def process_screenshot(input_numpy_image: np.ndarray, task: str, model_choice: s
|
|
| 433 |
return raw_response, output_image
|
| 434 |
|
| 435 |
# -----------------------------------------------------------------------------
|
| 436 |
-
#
|
| 437 |
# -----------------------------------------------------------------------------
|
| 438 |
|
| 439 |
with gr.Blocks(theme=steel_blue_theme, css=css) as demo:
|
|
|
|
| 5 |
import unicodedata
|
| 6 |
import gc
|
| 7 |
from io import BytesIO
|
| 8 |
+
from typing import Iterable, Tuple, Optional, List, Dict, Any
|
|
|
|
| 9 |
|
| 10 |
import gradio as gr
|
| 11 |
import numpy as np
|
|
|
|
| 98 |
|
| 99 |
# --- Load Fara-7B ---
|
| 100 |
print("π Loading Fara-7B...")
|
| 101 |
+
MODEL_ID_V = "microsoft/Fara-7B"
|
| 102 |
+
# Note: Ensure this ID is accessible. If private, use "Qwen/Qwen2.5-VL-7B-Instruct" as fallback for testing.
|
| 103 |
try:
|
| 104 |
processor_v = AutoProcessor.from_pretrained(MODEL_ID_V, trust_remote_code=True)
|
| 105 |
model_v = Qwen2_5_VLForConditionalGeneration.from_pretrained(
|
|
|
|
| 116 |
print("π Loading UI-TARS-1.5-7B...")
|
| 117 |
MODEL_ID_X = "ByteDance-Seed/UI-TARS-1.5-7B"
|
| 118 |
try:
|
|
|
|
| 119 |
processor_x = AutoProcessor.from_pretrained(MODEL_ID_X, trust_remote_code=True, use_fast=False)
|
| 120 |
model_x = AutoModelForImageTextToText.from_pretrained(
|
| 121 |
MODEL_ID_X,
|
|
|
|
| 127 |
model_x = None
|
| 128 |
processor_x = None
|
| 129 |
|
| 130 |
+
# --- Load Holo2-8B (Using Holo1-3B ID as per previous context) ---
|
| 131 |
print("π Loading Holo2-8B...")
|
| 132 |
+
MODEL_ID_H = "Hcompany/Holo1-3B"
|
| 133 |
try:
|
| 134 |
processor_h = AutoProcessor.from_pretrained(MODEL_ID_H, trust_remote_code=True)
|
| 135 |
model_h = AutoModelForImageTextToText.from_pretrained(
|
|
|
|
| 138 |
torch_dtype=torch.float16
|
| 139 |
).to(device).eval()
|
| 140 |
except Exception as e:
|
| 141 |
+
print(f"Failed to load Holo: {e}")
|
| 142 |
model_h = None
|
| 143 |
processor_h = None
|
| 144 |
|
|
|
|
| 146 |
|
| 147 |
|
| 148 |
# -----------------------------------------------------------------------------
|
| 149 |
+
# 3. UTILS & HELPERS
|
| 150 |
# -----------------------------------------------------------------------------
|
| 151 |
|
| 152 |
def array_to_image(image_array: np.ndarray) -> Image.Image:
|
| 153 |
if image_array is None: raise ValueError("No image provided.")
|
| 154 |
return Image.fromarray(np.uint8(image_array))
|
| 155 |
|
| 156 |
+
def get_image_proc_params(processor) -> Dict[str, int]:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
"""
|
| 158 |
+
Robustly retrieve image processing parameters, handling NoneTypes.
|
| 159 |
+
This fixes the 'TypeError: > not supported between int and NoneType' error.
|
| 160 |
+
"""
|
| 161 |
+
ip = getattr(processor, "image_processor", None)
|
| 162 |
+
|
| 163 |
+
# Default fallback values for Qwen2-VL architecture
|
| 164 |
+
default_min = 256 * 256
|
| 165 |
+
default_max = 1280 * 1280
|
| 166 |
|
| 167 |
+
patch_size = getattr(ip, "patch_size", 14)
|
| 168 |
+
merge_size = getattr(ip, "merge_size", 2)
|
| 169 |
+
min_pixels = getattr(ip, "min_pixels", default_min)
|
| 170 |
+
max_pixels = getattr(ip, "max_pixels", default_max)
|
| 171 |
+
|
| 172 |
+
# Explicit check because sometimes getattr returns None if the config key exists but is null
|
| 173 |
+
if min_pixels is None: min_pixels = default_min
|
| 174 |
+
if max_pixels is None: max_pixels = default_max
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 175 |
|
|
|
|
|
|
|
| 176 |
return {
|
| 177 |
+
"patch_size": patch_size,
|
| 178 |
+
"merge_size": merge_size,
|
| 179 |
+
"min_pixels": min_pixels,
|
| 180 |
+
"max_pixels": max_pixels,
|
| 181 |
}
|
| 182 |
|
|
|
|
| 183 |
def apply_chat_template_compat(processor, messages: List[Dict[str, Any]]) -> str:
|
| 184 |
+
"""Helper to apply chat templates across different model types/versions."""
|
| 185 |
tok = getattr(processor, "tokenizer", None)
|
| 186 |
if hasattr(processor, "apply_chat_template"):
|
| 187 |
return processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
|
| 188 |
if tok is not None and hasattr(tok, "apply_chat_template"):
|
| 189 |
return tok.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
|
| 190 |
+
|
| 191 |
+
# Fallback manual construction
|
| 192 |
texts = []
|
| 193 |
for m in messages:
|
| 194 |
+
content = m.get("content", "")
|
| 195 |
+
if isinstance(content, list):
|
| 196 |
+
for c in content:
|
| 197 |
if isinstance(c, dict) and c.get("type") == "text":
|
| 198 |
texts.append(c.get("text", ""))
|
| 199 |
+
elif isinstance(content, str):
|
| 200 |
+
texts.append(content)
|
| 201 |
return "\n".join(texts)
|
| 202 |
|
| 203 |
def batch_decode_compat(processor, token_id_batches, **kw):
|
|
|
|
| 216 |
return generated_ids
|
| 217 |
return [out_ids[len(in_seq):] for in_seq, out_ids in zip(in_ids, generated_ids)]
|
| 218 |
|
| 219 |
+
# -----------------------------------------------------------------------------
|
| 220 |
+
# 4. PROMPTS
|
| 221 |
+
# -----------------------------------------------------------------------------
|
| 222 |
+
|
| 223 |
+
# --- Fara Prompt ---
|
| 224 |
+
def get_fara_prompt(task, image):
|
| 225 |
+
OS_SYSTEM_PROMPT = """You are a GUI agent. You are given a task and a screenshot of the current status.
|
| 226 |
+
You need to generate the next action to complete the task.
|
| 227 |
+
Output your action inside a <tool_call> block using JSON format.
|
| 228 |
+
Include "coordinate": [x, y] in pixels for interactions.
|
| 229 |
+
Examples:
|
| 230 |
+
<tool_call>{"name": "User", "arguments": {"action": "click", "coordinate": [400, 300]}}</tool_call>
|
| 231 |
+
<tool_call>{"name": "User", "arguments": {"action": "type", "coordinate": [100, 200], "text": "hello"}}</tool_call>
|
| 232 |
+
"""
|
| 233 |
+
return [
|
| 234 |
+
{"role": "system", "content": [{"type": "text", "text": OS_SYSTEM_PROMPT}]},
|
| 235 |
+
{"role": "user", "content": [{"type": "image", "image": image}, {"type": "text", "text": f"Instruction: {task}"}]},
|
| 236 |
+
]
|
| 237 |
+
|
| 238 |
+
# --- UI-TARS & Holo Prompt ---
|
| 239 |
+
def get_localization_prompt(task, image):
|
| 240 |
+
guidelines = (
|
| 241 |
+
"Localize an element on the GUI image according to my instructions and "
|
| 242 |
+
"output a click position as Click(x, y) with x num pixels from the left edge "
|
| 243 |
+
"and y num pixels from the top edge."
|
| 244 |
+
)
|
| 245 |
+
return [
|
| 246 |
+
{
|
| 247 |
+
"role": "user",
|
| 248 |
+
"content": [
|
| 249 |
+
{"type": "image", "image": image},
|
| 250 |
+
{"type": "text", "text": f"{guidelines}\n{task}"}
|
| 251 |
+
],
|
| 252 |
+
}
|
| 253 |
+
]
|
| 254 |
|
| 255 |
# -----------------------------------------------------------------------------
|
| 256 |
+
# 5. PARSING & VISUALIZATION
|
| 257 |
# -----------------------------------------------------------------------------
|
| 258 |
|
| 259 |
def parse_click_response(text: str) -> List[Dict]:
|
|
|
|
| 277 |
matches_box = re.findall(r"start_box=['\"]?\(\s*(\d+)\s*,\s*(\d+)\s*\)['\"]?", text, re.IGNORECASE)
|
| 278 |
for m in matches_box:
|
| 279 |
actions.append({"type": "click", "x": int(m[0]), "y": int(m[1]), "text": ""})
|
| 280 |
+
|
| 281 |
+
# Regex 4: Simple tuple (x,y) - Often seen in your error logs
|
| 282 |
+
# We look for a standalone tuple pattern
|
| 283 |
+
matches_tuple = re.findall(r"(?:^|\s)\(\s*(\d+)\s*,\s*(\d+)\s*\)(?:$|\s|,)", text)
|
| 284 |
+
for m in matches_tuple:
|
| 285 |
+
actions.append({"type": "click", "x": int(m[0]), "y": int(m[1]), "text": ""})
|
| 286 |
|
| 287 |
# Remove duplicates
|
| 288 |
unique_actions = []
|
|
|
|
| 319 |
if not actions: return None
|
| 320 |
img_copy = original_image.copy()
|
| 321 |
draw = ImageDraw.Draw(img_copy)
|
|
|
|
| 322 |
|
| 323 |
try:
|
| 324 |
+
font = ImageFont.load_default(size=18)
|
| 325 |
except IOError:
|
| 326 |
font = ImageFont.load_default()
|
| 327 |
|
|
|
|
| 349 |
|
| 350 |
text_pos = (pixel_x + 25, pixel_y - 15)
|
| 351 |
|
| 352 |
+
# Draw text with background (Bounding Box) to make it readable
|
| 353 |
try:
|
| 354 |
bbox = draw.textbbox(text_pos, label, font=font)
|
| 355 |
+
# Add padding to bbox
|
| 356 |
+
padded_bbox = (bbox[0]-4, bbox[1]-2, bbox[2]+4, bbox[3]+2)
|
| 357 |
+
draw.rectangle(padded_bbox, fill="black", outline=color)
|
| 358 |
draw.text(text_pos, label, fill="white", font=font)
|
| 359 |
except Exception as e:
|
| 360 |
+
# Fallback
|
|
|
|
| 361 |
draw.text(text_pos, label, fill="white")
|
| 362 |
|
| 363 |
return img_copy
|
| 364 |
|
| 365 |
# -----------------------------------------------------------------------------
|
| 366 |
+
# 6. CORE LOGIC
|
| 367 |
# -----------------------------------------------------------------------------
|
| 368 |
|
| 369 |
@spaces.GPU(duration=120)
|
|
|
|
| 380 |
if model_choice == "Fara-7B":
|
| 381 |
if model_v is None: return "Error: Fara model failed to load on startup.", None
|
| 382 |
print("Using Fara Pipeline...")
|
| 383 |
+
|
| 384 |
+
# Fara pipeline uses process_vision_info
|
| 385 |
messages = get_fara_prompt(task, input_pil_image)
|
| 386 |
text_prompt = processor_v.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
|
| 387 |
image_inputs, video_inputs = process_vision_info(messages)
|
|
|
|
| 416 |
else:
|
| 417 |
return f"Error: Unknown model '{model_choice}'", None
|
| 418 |
|
| 419 |
+
# 1. Smart Resize
|
| 420 |
+
# We call our robust get_image_proc_params here to avoid the TypeError
|
| 421 |
ip_params = get_image_proc_params(processor)
|
| 422 |
+
|
| 423 |
resized_h, resized_w = smart_resize(
|
| 424 |
input_pil_image.height, input_pil_image.width,
|
| 425 |
factor=ip_params["patch_size"] * ip_params["merge_size"],
|
| 426 |
+
min_pixels=ip_params["min_pixels"],
|
| 427 |
+
max_pixels=ip_params["max_pixels"]
|
| 428 |
)
|
| 429 |
proc_image = input_pil_image.resize((resized_w, resized_h), Image.Resampling.LANCZOS)
|
| 430 |
|
|
|
|
| 447 |
actions = parse_click_response(raw_response)
|
| 448 |
|
| 449 |
# 6. Rescale Coordinates back to Original Image Size
|
| 450 |
+
# The model saw 'resized_w' x 'resized_h', coordinates are likely in that scale
|
| 451 |
if resized_w > 0 and resized_h > 0:
|
| 452 |
scale_x = orig_w / resized_w
|
| 453 |
scale_y = orig_h / resized_h
|
|
|
|
| 467 |
return raw_response, output_image
|
| 468 |
|
| 469 |
# -----------------------------------------------------------------------------
|
| 470 |
+
# 7. UI SETUP
|
| 471 |
# -----------------------------------------------------------------------------
|
| 472 |
|
| 473 |
with gr.Blocks(theme=steel_blue_theme, css=css) as demo:
|