Spaces:
Sleeping
Sleeping
Codex Claude Opus 4.8 commited on
Commit ·
5bdf0f6
1
Parent(s): c1939e1
Modal-backed Space: run models on Modal GPU endpoints, Space is a thin HTTP client
Browse files- modal_app.py: MiniCPM (cards) + Nemotron (boss) as OpenAI-compatible chat
endpoints, SDXL-Turbo (art) returning a data URI; warm A10G containers + a
cached HF volume.
- art.py/clients.py: ModalArtClient + a 'modal' art backend.
- app_hf.py: endpoint-based backends (card/boss over HTTP, art via modal);
endpoint URLs come from Space variables. No GPU on the Space.
- requirements: slim to gradio only; README -> cpu-basic, Modal-backed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- README.md +2 -2
- app_hf.py +23 -16
- art.py +33 -0
- clients.py +13 -2
- modal_app.py +131 -0
- requirements.txt +0 -10
README.md
CHANGED
|
@@ -6,7 +6,7 @@ colorTo: indigo
|
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 6.17.3
|
| 8 |
app_file: app_hf.py
|
| 9 |
-
suggested_hardware:
|
| 10 |
pinned: false
|
| 11 |
license: mit
|
| 12 |
tags:
|
|
@@ -64,7 +64,7 @@ All listed models are under the hackathon's 32B parameter limit.
|
|
| 64 |
| Boss AI | `nvidia/Nemotron-Mini-4B-Instruct` | Enemy play decisions |
|
| 65 |
| Art | `stabilityai/sdxl-turbo` | Fast card illustration |
|
| 66 |
|
| 67 |
-
The Hugging Face Space entry point is [app_hf.py](app_hf.py).
|
| 68 |
|
| 69 |
## Running Locally
|
| 70 |
|
|
|
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 6.17.3
|
| 8 |
app_file: app_hf.py
|
| 9 |
+
suggested_hardware: cpu-basic
|
| 10 |
pinned: false
|
| 11 |
license: mit
|
| 12 |
tags:
|
|
|
|
| 64 |
| Boss AI | `nvidia/Nemotron-Mini-4B-Instruct` | Enemy play decisions |
|
| 65 |
| Art | `stabilityai/sdxl-turbo` | Fast card illustration |
|
| 66 |
|
| 67 |
+
The Hugging Face Space entry point is [app_hf.py](app_hf.py). The Space is a thin client: MiniCPM (cards), Nemotron (boss), and SDXL-Turbo (art) run on dedicated Modal GPU endpoints (see [modal_app.py](modal_app.py)), which the Space calls over HTTP. This keeps heavy compute off the Space (free CPU hardware) and gives each model its own autoscaled GPU.
|
| 68 |
|
| 69 |
## Running Locally
|
| 70 |
|
app_hf.py
CHANGED
|
@@ -1,33 +1,40 @@
|
|
| 1 |
-
"""Hugging Face Space entry point for Tabras.
|
| 2 |
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
"""
|
| 7 |
|
| 8 |
import os
|
| 9 |
|
| 10 |
-
#
|
| 11 |
-
os.environ.setdefault("TABRAS_CARD_BACKEND", "
|
| 12 |
-
os.environ.setdefault("TABRAS_CARD_MODEL", "
|
| 13 |
os.environ.setdefault("TABRAS_CARD_MAX_TOKENS", "192")
|
|
|
|
|
|
|
| 14 |
|
| 15 |
-
#
|
| 16 |
os.environ.setdefault("TABRAS_AI_BOSS", "1")
|
| 17 |
-
os.environ.setdefault("TABRAS_BOSS_BACKEND", "
|
| 18 |
-
os.environ.setdefault("TABRAS_BOSS_MODEL", "
|
| 19 |
os.environ.setdefault("TABRAS_BOSS_MAX_TOKENS", "96")
|
|
|
|
| 20 |
|
| 21 |
-
#
|
| 22 |
-
os.environ.setdefault("TABRAS_ART_BACKEND", "
|
| 23 |
-
os.environ.setdefault("TABRAS_ART_MODEL", "stabilityai/sdxl-turbo")
|
| 24 |
os.environ.setdefault("TABRAS_ART_STEPS", "4")
|
| 25 |
|
| 26 |
from app import CSS, HEAD, build_app # noqa: E402 (import after env is configured)
|
| 27 |
|
| 28 |
-
# Module-level `demo` so Hugging Face's launcher
|
| 29 |
-
#
|
| 30 |
-
# normally (Dev Mode OFF), not Gradio reload mode.
|
| 31 |
demo = build_app()
|
| 32 |
demo.queue()
|
| 33 |
|
|
|
|
| 1 |
+
"""Hugging Face Space entry point for Tabras (Modal-backed).
|
| 2 |
|
| 3 |
+
The Space is a thin client: MiniCPM (cards), Nemotron (boss), and SDXL (art) all
|
| 4 |
+
run on Modal GPU endpoints (see modal_app.py). Set these Space variables to the
|
| 5 |
+
URLs printed by `modal deploy modal_app.py`:
|
| 6 |
+
|
| 7 |
+
TABRAS_CARD_ENDPOINT -> CardModel.chat URL
|
| 8 |
+
TABRAS_BOSS_ENDPOINT -> BossModel.chat URL
|
| 9 |
+
TABRAS_ART_ENDPOINT -> ArtModel.generate URL
|
| 10 |
+
|
| 11 |
+
No GPU is needed on the Space itself, so it runs on free CPU hardware.
|
| 12 |
"""
|
| 13 |
|
| 14 |
import os
|
| 15 |
|
| 16 |
+
# Cards via the Modal MiniCPM endpoint (OpenAI-compatible chat over HTTP).
|
| 17 |
+
os.environ.setdefault("TABRAS_CARD_BACKEND", "llamacpp")
|
| 18 |
+
os.environ.setdefault("TABRAS_CARD_MODEL", "minicpm")
|
| 19 |
os.environ.setdefault("TABRAS_CARD_MAX_TOKENS", "192")
|
| 20 |
+
os.environ.setdefault("TABRAS_CARD_TEMPERATURE", "0.7")
|
| 21 |
+
os.environ.setdefault("TABRAS_CARD_TIMEOUT", "120")
|
| 22 |
|
| 23 |
+
# Boss via the Modal Nemotron endpoint (LocalChatClient HTTP path).
|
| 24 |
os.environ.setdefault("TABRAS_AI_BOSS", "1")
|
| 25 |
+
os.environ.setdefault("TABRAS_BOSS_BACKEND", "openai")
|
| 26 |
+
os.environ.setdefault("TABRAS_BOSS_MODEL", "nemotron")
|
| 27 |
os.environ.setdefault("TABRAS_BOSS_MAX_TOKENS", "96")
|
| 28 |
+
os.environ.setdefault("TABRAS_BOSS_TIMEOUT", "120")
|
| 29 |
|
| 30 |
+
# Art via the Modal SDXL endpoint.
|
| 31 |
+
os.environ.setdefault("TABRAS_ART_BACKEND", "modal")
|
|
|
|
| 32 |
os.environ.setdefault("TABRAS_ART_STEPS", "4")
|
| 33 |
|
| 34 |
from app import CSS, HEAD, build_app # noqa: E402 (import after env is configured)
|
| 35 |
|
| 36 |
+
# Module-level `demo` so Hugging Face's launcher finds the Blocks; css/head are
|
| 37 |
+
# applied via launch() below (keep the Space's Dev Mode off so this runs).
|
|
|
|
| 38 |
demo = build_app()
|
| 39 |
demo.queue()
|
| 40 |
|
art.py
CHANGED
|
@@ -1,8 +1,10 @@
|
|
| 1 |
import base64
|
|
|
|
| 2 |
import threading
|
| 3 |
from dataclasses import dataclass, field, replace
|
| 4 |
from io import BytesIO
|
| 5 |
from typing import Any, Callable, Protocol, Sequence
|
|
|
|
| 6 |
|
| 7 |
from budget import Card
|
| 8 |
from zerogpu import gpu
|
|
@@ -13,6 +15,37 @@ NEGATIVE_ART_PROMPT = (
|
|
| 13 |
)
|
| 14 |
|
| 15 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
class ArtClient(Protocol):
|
| 17 |
# Return a browser-renderable image URI for one prompt.
|
| 18 |
def create_art(self, prompt: str) -> str: # pragma: no cover
|
|
|
|
| 1 |
import base64
|
| 2 |
+
import json
|
| 3 |
import threading
|
| 4 |
from dataclasses import dataclass, field, replace
|
| 5 |
from io import BytesIO
|
| 6 |
from typing import Any, Callable, Protocol, Sequence
|
| 7 |
+
from urllib import request
|
| 8 |
|
| 9 |
from budget import Card
|
| 10 |
from zerogpu import gpu
|
|
|
|
| 15 |
)
|
| 16 |
|
| 17 |
|
| 18 |
+
@dataclass(frozen=True)
|
| 19 |
+
class ModalArtClient:
|
| 20 |
+
"""Generate card art by calling a Modal SDXL endpoint; returns a data URI."""
|
| 21 |
+
|
| 22 |
+
endpoint: str
|
| 23 |
+
steps: int = 4
|
| 24 |
+
guidance_scale: float = 0.0
|
| 25 |
+
width: int = 512
|
| 26 |
+
height: int = 320
|
| 27 |
+
timeout_seconds: int = 120
|
| 28 |
+
|
| 29 |
+
# POST one prompt to the Modal art endpoint and return its image data URI.
|
| 30 |
+
def create_art(self, prompt: str) -> str:
|
| 31 |
+
payload = {
|
| 32 |
+
"prompt": prompt,
|
| 33 |
+
"steps": self.steps,
|
| 34 |
+
"guidance": self.guidance_scale,
|
| 35 |
+
"width": self.width,
|
| 36 |
+
"height": self.height,
|
| 37 |
+
"negative_prompt": NEGATIVE_ART_PROMPT,
|
| 38 |
+
}
|
| 39 |
+
req = request.Request(
|
| 40 |
+
self.endpoint,
|
| 41 |
+
data=json.dumps(payload).encode("utf-8"),
|
| 42 |
+
headers={"Content-Type": "application/json"},
|
| 43 |
+
method="POST",
|
| 44 |
+
)
|
| 45 |
+
with request.urlopen(req, timeout=self.timeout_seconds) as response:
|
| 46 |
+
return str(json.loads(response.read().decode("utf-8"))["image"])
|
| 47 |
+
|
| 48 |
+
|
| 49 |
class ArtClient(Protocol):
|
| 50 |
# Return a browser-renderable image URI for one prompt.
|
| 51 |
def create_art(self, prompt: str) -> str: # pragma: no cover
|
clients.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
import os
|
| 2 |
|
| 3 |
-
from art import ArtClient, DiffusersImageClient, LazyArtClient
|
| 4 |
from boss import BossClient, NemotronBossClient
|
| 5 |
from generator import CardPackClient, LlamaCppCardClient, MiniCPMCardClient
|
| 6 |
from local_llm import (
|
|
@@ -107,7 +107,18 @@ def boss_client_from_env() -> BossClient | None:
|
|
| 107 |
# Build an art-generation client from environment variables.
|
| 108 |
def art_client_from_env() -> ArtClient | None:
|
| 109 |
global _art_client_cache
|
| 110 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
return None
|
| 112 |
if _art_client_cache is None:
|
| 113 |
model_id = os.environ.get("TABRAS_ART_MODEL", DEFAULT_ART_MODEL)
|
|
|
|
| 1 |
import os
|
| 2 |
|
| 3 |
+
from art import ArtClient, DiffusersImageClient, LazyArtClient, ModalArtClient
|
| 4 |
from boss import BossClient, NemotronBossClient
|
| 5 |
from generator import CardPackClient, LlamaCppCardClient, MiniCPMCardClient
|
| 6 |
from local_llm import (
|
|
|
|
| 107 |
# Build an art-generation client from environment variables.
|
| 108 |
def art_client_from_env() -> ArtClient | None:
|
| 109 |
global _art_client_cache
|
| 110 |
+
backend = os.environ.get("TABRAS_ART_BACKEND")
|
| 111 |
+
if backend == "modal":
|
| 112 |
+
if _art_client_cache is None:
|
| 113 |
+
_art_client_cache = ModalArtClient(
|
| 114 |
+
endpoint=os.environ["TABRAS_ART_ENDPOINT"],
|
| 115 |
+
steps=int(os.environ.get("TABRAS_ART_STEPS", "4")),
|
| 116 |
+
guidance_scale=float(os.environ.get("TABRAS_ART_GUIDANCE", "0.0")),
|
| 117 |
+
width=int(os.environ.get("TABRAS_ART_WIDTH", "512")),
|
| 118 |
+
height=int(os.environ.get("TABRAS_ART_HEIGHT", "320")),
|
| 119 |
+
)
|
| 120 |
+
return _art_client_cache
|
| 121 |
+
if backend != "diffusers":
|
| 122 |
return None
|
| 123 |
if _art_client_cache is None:
|
| 124 |
model_id = os.environ.get("TABRAS_ART_MODEL", DEFAULT_ART_MODEL)
|
modal_app.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Modal GPU endpoints for Tabras: MiniCPM card authoring, Nemotron boss play,
|
| 2 |
+
and SDXL-Turbo art. The Gradio Space calls these over HTTP, so all heavy compute
|
| 3 |
+
runs on dedicated, autoscaled Modal GPUs instead of the Space.
|
| 4 |
+
|
| 5 |
+
Deploy: modal deploy modal_app.py
|
| 6 |
+
Then set the printed URLs as Space variables:
|
| 7 |
+
TABRAS_CARD_ENDPOINT -> CardModel.chat URL
|
| 8 |
+
TABRAS_BOSS_ENDPOINT -> BossModel.chat URL
|
| 9 |
+
TABRAS_ART_ENDPOINT -> ArtModel.generate URL
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
import modal
|
| 13 |
+
|
| 14 |
+
CACHE = "/cache"
|
| 15 |
+
hf_cache = modal.Volume.from_name("tabras-hf-cache", create_if_missing=True)
|
| 16 |
+
|
| 17 |
+
MINICPM = "openbmb/MiniCPM-V-4"
|
| 18 |
+
NEMOTRON = "nvidia/Nemotron-Mini-4B-Instruct"
|
| 19 |
+
SDXL = "stabilityai/sdxl-turbo"
|
| 20 |
+
|
| 21 |
+
llm_image = (
|
| 22 |
+
modal.Image.debian_slim(python_version="3.11")
|
| 23 |
+
.pip_install(
|
| 24 |
+
"torch", "transformers==4.49.0", "accelerate", "sentencepiece",
|
| 25 |
+
"torchvision", "einops", "pillow", "fastapi[standard]",
|
| 26 |
+
)
|
| 27 |
+
.env({"HF_HOME": CACHE})
|
| 28 |
+
)
|
| 29 |
+
art_image = (
|
| 30 |
+
modal.Image.debian_slim(python_version="3.11")
|
| 31 |
+
.pip_install(
|
| 32 |
+
"torch", "diffusers", "transformers==4.49.0", "accelerate",
|
| 33 |
+
"safetensors", "pillow", "fastapi[standard]",
|
| 34 |
+
)
|
| 35 |
+
.env({"HF_HOME": CACHE})
|
| 36 |
+
)
|
| 37 |
+
|
| 38 |
+
app = modal.App("tabras-models")
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
# ---- MiniCPM: card authoring (OpenAI-compatible chat) ----
|
| 42 |
+
@app.cls(image=llm_image, gpu="A10G", volumes={CACHE: hf_cache}, min_containers=1, max_containers=2, scaledown_window=600, timeout=600)
|
| 43 |
+
class CardModel:
|
| 44 |
+
@modal.enter()
|
| 45 |
+
def load(self) -> None:
|
| 46 |
+
import torch
|
| 47 |
+
from transformers import AutoModel, AutoTokenizer
|
| 48 |
+
|
| 49 |
+
self.tok = AutoTokenizer.from_pretrained(MINICPM, trust_remote_code=True)
|
| 50 |
+
self.model = (
|
| 51 |
+
AutoModel.from_pretrained(MINICPM, trust_remote_code=True, attn_implementation="sdpa", torch_dtype=torch.float16)
|
| 52 |
+
.eval()
|
| 53 |
+
.cuda()
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
@modal.fastapi_endpoint(method="POST")
|
| 57 |
+
def chat(self, item: dict) -> dict:
|
| 58 |
+
msgs = item.get("messages", [])
|
| 59 |
+
system = " ".join(m["content"] for m in msgs if m.get("role") == "system")
|
| 60 |
+
user = " ".join(m["content"] for m in msgs if m.get("role") == "user")
|
| 61 |
+
temp = float(item.get("temperature", 0.7))
|
| 62 |
+
text = str(
|
| 63 |
+
self.model.chat(
|
| 64 |
+
msgs=[{"role": "user", "content": user}],
|
| 65 |
+
image=None,
|
| 66 |
+
tokenizer=self.tok,
|
| 67 |
+
system_prompt=system,
|
| 68 |
+
sampling=temp > 0,
|
| 69 |
+
temperature=max(temp, 0.01),
|
| 70 |
+
max_new_tokens=int(item.get("max_tokens", 192)),
|
| 71 |
+
)
|
| 72 |
+
)
|
| 73 |
+
return {"choices": [{"message": {"role": "assistant", "content": text}}]}
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
# ---- Nemotron: boss play (OpenAI-compatible chat) ----
|
| 77 |
+
@app.cls(image=llm_image, gpu="A10G", volumes={CACHE: hf_cache}, min_containers=1, max_containers=2, scaledown_window=600, timeout=600)
|
| 78 |
+
class BossModel:
|
| 79 |
+
@modal.enter()
|
| 80 |
+
def load(self) -> None:
|
| 81 |
+
import torch
|
| 82 |
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 83 |
+
|
| 84 |
+
self.tok = AutoTokenizer.from_pretrained(NEMOTRON)
|
| 85 |
+
self.model = AutoModelForCausalLM.from_pretrained(NEMOTRON, torch_dtype=torch.float16).eval().cuda()
|
| 86 |
+
|
| 87 |
+
@modal.fastapi_endpoint(method="POST")
|
| 88 |
+
def chat(self, item: dict) -> dict:
|
| 89 |
+
import torch
|
| 90 |
+
|
| 91 |
+
msgs = item.get("messages", [])
|
| 92 |
+
inputs = self.tok.apply_chat_template(msgs, tokenize=True, add_generation_prompt=True, return_tensors="pt").to("cuda")
|
| 93 |
+
temp = float(item.get("temperature", 0.2))
|
| 94 |
+
with torch.no_grad():
|
| 95 |
+
out = self.model.generate(
|
| 96 |
+
inputs,
|
| 97 |
+
max_new_tokens=int(item.get("max_tokens", 96)),
|
| 98 |
+
do_sample=temp > 0,
|
| 99 |
+
temperature=max(temp, 0.01),
|
| 100 |
+
)
|
| 101 |
+
text = self.tok.decode(out[0][inputs.shape[-1]:], skip_special_tokens=True)
|
| 102 |
+
return {"choices": [{"message": {"role": "assistant", "content": text}}]}
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
# ---- SDXL-Turbo: card art (returns a JPEG data URI) ----
|
| 106 |
+
@app.cls(image=art_image, gpu="A10G", volumes={CACHE: hf_cache}, min_containers=1, max_containers=2, scaledown_window=600, timeout=600)
|
| 107 |
+
class ArtModel:
|
| 108 |
+
@modal.enter()
|
| 109 |
+
def load(self) -> None:
|
| 110 |
+
import torch
|
| 111 |
+
from diffusers import AutoPipelineForText2Image
|
| 112 |
+
|
| 113 |
+
self.pipe = AutoPipelineForText2Image.from_pretrained(SDXL, torch_dtype=torch.float16).to("cuda")
|
| 114 |
+
self.pipe.set_progress_bar_config(disable=True)
|
| 115 |
+
|
| 116 |
+
@modal.fastapi_endpoint(method="POST")
|
| 117 |
+
def generate(self, item: dict) -> dict:
|
| 118 |
+
import base64
|
| 119 |
+
from io import BytesIO
|
| 120 |
+
|
| 121 |
+
result = self.pipe(
|
| 122 |
+
prompt=item["prompt"],
|
| 123 |
+
num_inference_steps=int(item.get("steps", 4)),
|
| 124 |
+
guidance_scale=float(item.get("guidance", 0.0)),
|
| 125 |
+
width=int(item.get("width", 512)),
|
| 126 |
+
height=int(item.get("height", 320)),
|
| 127 |
+
negative_prompt=item.get("negative_prompt"),
|
| 128 |
+
)
|
| 129 |
+
buffer = BytesIO()
|
| 130 |
+
result.images[0].save(buffer, format="JPEG", quality=85)
|
| 131 |
+
return {"image": "data:image/jpeg;base64," + base64.b64encode(buffer.getvalue()).decode("ascii")}
|
requirements.txt
CHANGED
|
@@ -1,11 +1 @@
|
|
| 1 |
gradio==6.17.3
|
| 2 |
-
diffusers
|
| 3 |
-
torch
|
| 4 |
-
transformers==4.49.0
|
| 5 |
-
accelerate
|
| 6 |
-
safetensors
|
| 7 |
-
pillow
|
| 8 |
-
sentencepiece
|
| 9 |
-
torchvision
|
| 10 |
-
einops
|
| 11 |
-
spaces
|
|
|
|
| 1 |
gradio==6.17.3
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|