Agacy commited on
Commit
1004e6e
·
verified ·
1 Parent(s): 093f65f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +171 -141
app.py CHANGED
@@ -1,18 +1,21 @@
1
  """
2
- Segmentação Panóptica de Cenas Urbanas
3
- Projeto PDI — Antonio Agacy & Maria Divina
4
- HuggingFace Spaces · Gradio · OneFormer
5
  """
6
 
 
 
 
7
  import gradio as gr
8
  import numpy as np
9
  import torch
10
  from PIL import Image
11
  import matplotlib
 
12
  matplotlib.use("Agg")
13
  import matplotlib.pyplot as plt
14
  import matplotlib.patches as mpatches
15
- import io
16
 
17
  from transformers import (
18
  OneFormerProcessor,
@@ -21,164 +24,184 @@ from transformers import (
21
 
22
  # ─── Configuração ────────────────────────────────────────────
23
  MODEL_NAME = "Agacy/PDI"
24
- DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
25
 
26
- # Paleta de cores fixa para as classes
 
 
 
 
 
 
 
 
 
 
 
27
  PALETTE = [
28
- [220, 20, 60], [119, 11, 32], [ 0, 0, 142], [ 0, 0, 230],
29
- [106, 0, 228], [ 0, 60, 100], [ 0, 80, 100], [ 0, 0, 70],
30
- [ 0, 0, 192], [250, 170, 30], [100, 170, 30], [220, 220, 0],
31
- [175, 116, 175], [250, 0, 30], [165, 42, 42], [255, 77, 255],
32
- [ 0, 226, 252], [182, 182, 255], [ 0, 82, 0], [120, 166, 157],
33
  ]
34
 
35
- # ─── Carrega modelo uma vez ───────────────────────────────────
36
- print(f"Carregando OneFormer ({MODEL_NAME}) no dispositivo: {DEVICE}")
37
- processor = OneFormerProcessor.from_pretrained(MODEL_NAME)
38
- model = OneFormerForUniversalSegmentation.from_pretrained(MODEL_NAME, is_training=False)
39
- model = model.to(DEVICE).eval()
40
- print("✅ Modelo pronto!")
41
 
42
 
43
- # ─── Função principal ─────────────────────────────────────────
44
- def segmentar(imagem_pil, tarefa: str, opacidade: float):
45
- """
46
- Recebe uma imagem PIL, roda o OneFormer no modo escolhido
47
- e devolve a imagem com as máscaras sobrepostas + descrição.
48
- """
 
 
 
 
 
 
 
 
 
49
  if imagem_pil is None:
50
  return None, "⚠️ Envie uma imagem primeiro."
51
 
52
- # Mapeia label da UI para task token do OneFormer
 
 
53
  task_map = {
54
  "🔀 Panóptico (things + stuff)": "panoptic",
55
- "🎨 Semântico (regiões)": "semantic",
56
- "📦 Instâncias (objetos)": "instance",
57
  }
58
  task_token = task_map.get(tarefa, "panoptic")
59
 
60
- # Pré-processamento
 
 
 
 
 
 
 
 
61
  inputs = processor(
62
- images=imagem_pil,
63
- task_inputs=[task_token],
64
- return_tensors="pt",
65
  )
66
  inputs = {
67
  k: v.to(DEVICE) if isinstance(v, torch.Tensor) else v
68
  for k, v in inputs.items()
69
  }
70
 
71
- # Inferência
72
  with torch.no_grad():
73
  outputs = model(**inputs)
74
 
75
- # Pós-processamento
76
  img_size = [imagem_pil.size[::-1]] # (H, W)
 
 
 
 
 
 
77
 
78
  if task_token == "panoptic":
79
  resultado = processor.post_process_panoptic_segmentation(
80
  outputs, target_sizes=img_size
81
  )[0]
82
- seg_map = resultado["segmentation"].cpu().numpy()
83
  seg_info = resultado["segments_info"]
84
- id2label = model.config.id2label
85
-
86
- # Monta overlay
87
- img_np = np.array(imagem_pil.convert("RGB"))
88
- overlay = img_np.copy().astype(float)
89
- handles = []
90
- classes_detectadas = []
91
 
92
- for i, seg in enumerate(seg_info):
93
- cor = np.array(PALETTE[i % len(PALETTE)]) / 255.0
94
- cor_255 = np.array(PALETTE[i % len(PALETTE)])
95
- mask = (seg_map == seg["id"])
96
- overlay[mask] = (
97
- overlay[mask] * (1 - opacidade) + cor_255 * opacidade
98
- )
99
- label = id2label.get(seg.get("label_id", 0), "?")
100
- score = seg.get("score", 0)
101
- tipo = "thing" if seg.get("isthing", False) else "stuff"
102
- classes_detectadas.append(
103
- f"**{label}** ({tipo}) — confiança: {score:.0%}"
104
- )
105
- handles.append(
106
- mpatches.Patch(color=cor, label=f"{label} ({score:.2f})")
107
  )
 
 
 
 
 
108
 
109
  elif task_token == "semantic":
110
  resultado = processor.post_process_semantic_segmentation(
111
  outputs, target_sizes=img_size
112
  )[0]
113
- seg_map = resultado.cpu().numpy()
114
- id2label = model.config.id2label
115
-
116
- img_np = np.array(imagem_pil.convert("RGB"))
117
- overlay = img_np.copy().astype(float)
118
- handles = []
119
- classes_detectadas = []
120
  ids_presentes = np.unique(seg_map)
121
 
122
- for i, cid in enumerate(ids_presentes):
123
- cor = np.array(PALETTE[i % len(PALETTE)]) / 255.0
124
- cor_255 = np.array(PALETTE[i % len(PALETTE)])
125
- mask = (seg_map == cid)
126
- overlay[mask] = overlay[mask] * (1 - opacidade) + cor_255 * opacidade
127
- label = id2label.get(int(cid), "?")
128
- classes_detectadas.append(f"**{label}**")
129
- handles.append(mpatches.Patch(color=cor, label=label))
130
-
131
- seg_info = [{"id": int(c)} for c in ids_presentes]
 
 
 
132
 
133
  else: # instance
134
  resultado = processor.post_process_instance_segmentation(
135
  outputs, target_sizes=img_size
136
  )[0]
137
- seg_map = resultado["segmentation"].cpu().numpy()
138
- seg_info = resultado["segments_info"]
139
- id2label = model.config.id2label
140
-
141
- img_np = np.array(imagem_pil.convert("RGB"))
142
- overlay = img_np.copy().astype(float)
143
- handles = []
144
- classes_detectadas = []
145
 
146
  for i, seg in enumerate(seg_info):
147
- cor = np.array(PALETTE[i % len(PALETTE)]) / 255.0
148
- cor_255 = np.array(PALETTE[i % len(PALETTE)])
149
- mask = (seg_map == seg["id"])
150
- overlay[mask] = overlay[mask] * (1 - opacidade) + cor_255 * opacidade
151
- label = id2label.get(seg.get("label_id", 0), "?")
152
- score = seg.get("score", 0)
153
- classes_detectadas.append(f"**{label}** #{i+1} {score:.0%}")
154
- handles.append(
155
- mpatches.Patch(color=cor, label=f"{label} #{i+1}")
 
156
  )
 
 
 
 
 
157
 
158
- # Gera figura com matplotlib
159
  fig, axes = plt.subplots(1, 2, figsize=(14, 6))
160
- axes[0].imshow(np.array(imagem_pil.convert("RGB")))
161
  axes[0].set_title("Original", fontsize=13)
162
  axes[0].axis("off")
163
 
164
  axes[1].imshow(overlay.astype(np.uint8))
165
- axes[1].set_title(f"Segmentação {task_token.capitalize()} — OneFormer", fontsize=13)
 
 
166
  axes[1].axis("off")
 
 
167
  if handles:
168
  axes[1].legend(
169
- handles=handles[:15], # máximo 15 classes na legenda
170
- loc="lower left",
171
- fontsize=8,
172
- framealpha=0.85,
 
173
  )
174
 
175
- plt.suptitle(
176
- "Segmentação Panóptica de Cenas Urbanas · Agacy/PDI",
177
- fontsize=11, color="#555", y=1.01
178
- )
179
  plt.tight_layout()
180
-
181
- # Converte figura → PIL
182
  buf = io.BytesIO()
183
  plt.savefig(buf, format="png", dpi=120, bbox_inches="tight")
184
  plt.close(fig)
@@ -186,43 +209,44 @@ def segmentar(imagem_pil, tarefa: str, opacidade: float):
186
  img_resultado = Image.open(buf).copy()
187
  buf.close()
188
 
189
- # Texto de resumo
190
- n = len(seg_info)
191
  resumo = (
192
- f"**{n} segmento(s) detectado(s)** no modo **{task_token}**.\n\n"
193
- + "\n".join(f"- {c}" for c in classes_detectadas[:20])
 
 
194
  )
 
 
195
 
196
  return img_resultado, resumo
197
 
198
 
199
- # ─── Interface Gradio ────────────────────────────────────────
200
  DESCRICAO = """
201
- ## 🏙️ Segmentação Panóptica de Cenas Urbanas
202
 
203
- **Projeto PDI — Antonio Agacy Silva Lima Júnior & Maria Divina**
204
- Instituto Federal do Tocantins (IFTO) · 2025
205
 
206
- Modelo: [OneFormer](https://arxiv.org/abs/2211.06220) (Jain et al., CVPR 2023)
207
- pré-treinado no **Cityscapes** via [HuggingFace Transformers](https://huggingface.co/shi-labs/oneformer_cityscapes_swin_large)
 
208
 
209
- ---
210
- Envie uma foto de cena urbana (rua, trânsito, calçada) e veja a segmentação panóptica em tempo real.
211
  """
212
 
213
- EXEMPLOS_TEXTO = """
214
- **Como usar:**
215
- 1. Envie uma foto de rua ou trânsito
216
- 2. Escolha o modo de segmentação
217
- 3. Ajuste a opacidade das máscaras
218
- 4. Clique em **Segmentar**
219
- """
220
 
221
- with gr.Blocks(
222
- theme=gr.themes.Soft(primary_hue="violet"),
223
- title="Segmentação Panóptica — PDI IFTO",
224
- ) as demo:
225
 
 
 
 
 
 
226
  gr.Markdown(DESCRICAO)
227
 
228
  with gr.Row():
@@ -230,7 +254,7 @@ with gr.Blocks(
230
  img_input = gr.Image(
231
  type="pil",
232
  label="📷 Imagem de entrada",
233
- sources=["upload", "clipboard"],
234
  )
235
  tarefa = gr.Radio(
236
  choices=[
@@ -242,20 +266,30 @@ with gr.Blocks(
242
  label="Modo de segmentação",
243
  )
244
  opacidade = gr.Slider(
245
- minimum=0.3,
246
- maximum=0.9,
247
- value=0.55,
248
- step=0.05,
249
  label="Opacidade das máscaras",
250
  )
251
  btn = gr.Button("🚀 Segmentar", variant="primary", size="lg")
252
 
253
- with gr.Column(scale=2):
254
- img_output = gr.Image(
255
- type="pil",
256
- label="Resultado da segmentação",
257
  )
258
- texto_output = gr.Markdown(label="Classes detectadas")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
259
 
260
  btn.click(
261
  fn=segmentar,
@@ -263,11 +297,7 @@ with gr.Blocks(
263
  outputs=[img_output, texto_output],
264
  )
265
 
266
- gr.Markdown("---")
267
- gr.Markdown(
268
- "**Referências:** Kirillov et al. (2019) · Jain et al. (2023) · "
269
- "Ren et al. (2024) · Ravi et al. (2024)"
270
- )
271
 
272
  if __name__ == "__main__":
273
- demo.launch()
 
1
  """
2
+ Segmentação Panóptica de Cenas Urbanas — Araguatins/TO
3
+ Projeto PDI — Antonio Agacy & Maria Divina · IFTO · 2026
4
+ HuggingFace Spaces · Gradio · OneFormer (fine-tuned)
5
  """
6
 
7
+ import io
8
+ import time
9
+
10
  import gradio as gr
11
  import numpy as np
12
  import torch
13
  from PIL import Image
14
  import matplotlib
15
+
16
  matplotlib.use("Agg")
17
  import matplotlib.pyplot as plt
18
  import matplotlib.patches as mpatches
 
19
 
20
  from transformers import (
21
  OneFormerProcessor,
 
24
 
25
  # ─── Configuração ────────────────────────────────────────────
26
  MODEL_NAME = "Agacy/PDI"
27
+ DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
28
 
29
+ # ─── Carrega modelo uma vez ──────────────────────────────────
30
+ print(f"Carregando OneFormer ({MODEL_NAME}) no dispositivo: {DEVICE}")
31
+ processor = OneFormerProcessor.from_pretrained(MODEL_NAME)
32
+ model = OneFormerForUniversalSegmentation.from_pretrained(
33
+ MODEL_NAME, is_training=False
34
+ )
35
+ model = model.to(DEVICE).eval()
36
+ ID2LABEL = {int(k): v for k, v in model.config.id2label.items()}
37
+ NUM_CLASSES = len(ID2LABEL)
38
+ print(f"✅ Modelo pronto! {NUM_CLASSES} classes: {list(ID2LABEL.values())}")
39
+
40
+ # ─── Paleta FIXA POR CLASSE (cor consistente entre imagens) ──
41
  PALETTE = [
42
+ [228, 26, 28], [55, 126, 184], [77, 175, 74], [152, 78, 163],
43
+ [255, 127, 0], [255, 255, 51], [166, 86, 40], [247, 129, 191],
44
+ [153, 153, 153], [0, 206, 209], [124, 252, 0], [138, 43, 226],
45
+ [255, 99, 71], [70, 130, 180], [240, 230, 140], [219, 112, 147],
 
46
  ]
47
 
48
+
49
+ def cor_da_classe(label_id: int) -> np.ndarray:
50
+ return np.array(PALETTE[label_id % len(PALETTE)], dtype=float)
 
 
 
51
 
52
 
53
+ # Nomes em português para exibição
54
+ PT_LABELS = {
55
+ "building": "edifício", "bus": "ônibus", "car": "carro",
56
+ "motorcycle": "moto", "person": "pessoa", "river": "rio",
57
+ "road": "rua", "sidewalk": "calçada", "sky": "céu",
58
+ "terrain": "terreno", "truck": "caminhão", "vegetation": "vegetação",
59
+ }
60
+
61
+
62
+ def nome_pt(label: str) -> str:
63
+ return PT_LABELS.get(label, label)
64
+
65
+
66
+ # ─── Função principal ────────────────────────────────────────
67
+ def segmentar(imagem_pil, tarefa: str, opacidade: float, progress=gr.Progress()):
68
  if imagem_pil is None:
69
  return None, "⚠️ Envie uma imagem primeiro."
70
 
71
+ t0 = time.time()
72
+ progress(0.1, desc="Pré-processando…")
73
+
74
  task_map = {
75
  "🔀 Panóptico (things + stuff)": "panoptic",
76
+ "🎨 Semântico (regiões)": "semantic",
77
+ "📦 Instâncias (objetos)": "instance",
78
  }
79
  task_token = task_map.get(tarefa, "panoptic")
80
 
81
+ imagem_pil = imagem_pil.convert("RGB")
82
+
83
+ # Reduz imagens muito grandes (CPU do Space agradece)
84
+ MAX_SIDE = 1024
85
+ if max(imagem_pil.size) > MAX_SIDE:
86
+ ratio = MAX_SIDE / max(imagem_pil.size)
87
+ novo = (int(imagem_pil.width * ratio), int(imagem_pil.height * ratio))
88
+ imagem_pil = imagem_pil.resize(novo, Image.BILINEAR)
89
+
90
  inputs = processor(
91
+ images=imagem_pil, task_inputs=[task_token], return_tensors="pt"
 
 
92
  )
93
  inputs = {
94
  k: v.to(DEVICE) if isinstance(v, torch.Tensor) else v
95
  for k, v in inputs.items()
96
  }
97
 
98
+ progress(0.3, desc="Rodando o OneFormer… (CPU pode levar ~1 min)")
99
  with torch.no_grad():
100
  outputs = model(**inputs)
101
 
102
+ progress(0.8, desc="Gerando máscaras…")
103
  img_size = [imagem_pil.size[::-1]] # (H, W)
104
+ img_np = np.array(imagem_pil)
105
+ overlay = img_np.copy().astype(float)
106
+ total_px = img_np.shape[0] * img_np.shape[1]
107
+
108
+ handles_por_classe = {}
109
+ linhas_resumo = []
110
 
111
  if task_token == "panoptic":
112
  resultado = processor.post_process_panoptic_segmentation(
113
  outputs, target_sizes=img_size
114
  )[0]
115
+ seg_map = resultado["segmentation"].cpu().numpy()
116
  seg_info = resultado["segments_info"]
 
 
 
 
 
 
 
117
 
118
+ for seg in seg_info:
119
+ cls = seg.get("label_id", 0)
120
+ cor = cor_da_classe(cls)
121
+ mask = seg_map == seg["id"]
122
+ overlay[mask] = overlay[mask] * (1 - opacidade) + cor * opacidade
123
+
124
+ label = nome_pt(ID2LABEL.get(cls, "?"))
125
+ pct = 100 * mask.sum() / total_px
126
+ score = seg.get("score", 0)
127
+ linhas_resumo.append(
128
+ f"| {label} | {pct:.1f}% | {score:.0%} |"
 
 
 
 
129
  )
130
+ if cls not in handles_por_classe:
131
+ handles_por_classe[cls] = mpatches.Patch(
132
+ color=cor / 255.0, label=label
133
+ )
134
+ n_seg = len(seg_info)
135
 
136
  elif task_token == "semantic":
137
  resultado = processor.post_process_semantic_segmentation(
138
  outputs, target_sizes=img_size
139
  )[0]
140
+ seg_map = resultado.cpu().numpy()
 
 
 
 
 
 
141
  ids_presentes = np.unique(seg_map)
142
 
143
+ for cid in ids_presentes:
144
+ cls = int(cid)
145
+ cor = cor_da_classe(cls)
146
+ mask = seg_map == cid
147
+ overlay[mask] = overlay[mask] * (1 - opacidade) + cor * opacidade
148
+
149
+ label = nome_pt(ID2LABEL.get(cls, "?"))
150
+ pct = 100 * mask.sum() / total_px
151
+ linhas_resumo.append(f"| {label} | {pct:.1f}% | — |")
152
+ handles_por_classe[cls] = mpatches.Patch(
153
+ color=cor / 255.0, label=label
154
+ )
155
+ n_seg = len(ids_presentes)
156
 
157
  else: # instance
158
  resultado = processor.post_process_instance_segmentation(
159
  outputs, target_sizes=img_size
160
  )[0]
161
+ seg_map = resultado["segmentation"].cpu().numpy()
162
+ seg_info = resultado["segments_info"]
 
 
 
 
 
 
163
 
164
  for i, seg in enumerate(seg_info):
165
+ cls = seg.get("label_id", 0)
166
+ cor = cor_da_classe(cls)
167
+ mask = seg_map == seg["id"]
168
+ overlay[mask] = overlay[mask] * (1 - opacidade) + cor * opacidade
169
+
170
+ label = nome_pt(ID2LABEL.get(cls, "?"))
171
+ pct = 100 * mask.sum() / total_px
172
+ score = seg.get("score", 0)
173
+ linhas_resumo.append(
174
+ f"| {label} #{i + 1} | {pct:.1f}% | {score:.0%} |"
175
  )
176
+ if cls not in handles_por_classe:
177
+ handles_por_classe[cls] = mpatches.Patch(
178
+ color=cor / 255.0, label=label
179
+ )
180
+ n_seg = len(seg_info)
181
 
182
+ # ─── Figura lado a lado ──────────────────────────────────
183
  fig, axes = plt.subplots(1, 2, figsize=(14, 6))
184
+ axes[0].imshow(img_np)
185
  axes[0].set_title("Original", fontsize=13)
186
  axes[0].axis("off")
187
 
188
  axes[1].imshow(overlay.astype(np.uint8))
189
+ axes[1].set_title(
190
+ f"Segmentação — modo {task_token}", fontsize=13
191
+ )
192
  axes[1].axis("off")
193
+
194
+ handles = list(handles_por_classe.values())
195
  if handles:
196
  axes[1].legend(
197
+ handles=handles,
198
+ loc="upper left",
199
+ bbox_to_anchor=(1.01, 1.0),
200
+ fontsize=9,
201
+ framealpha=0.9,
202
  )
203
 
 
 
 
 
204
  plt.tight_layout()
 
 
205
  buf = io.BytesIO()
206
  plt.savefig(buf, format="png", dpi=120, bbox_inches="tight")
207
  plt.close(fig)
 
209
  img_resultado = Image.open(buf).copy()
210
  buf.close()
211
 
212
+ # ─── Resumo em tabela Markdown ──────────────────────────
213
+ dt = time.time() - t0
214
  resumo = (
215
+ f"### {n_seg} segmento(s) · modo **{task_token}** · {dt:.1f}s\n\n"
216
+ "| Classe | Área da imagem | Confiança |\n"
217
+ "|---|---|---|\n"
218
+ + "\n".join(linhas_resumo[:25])
219
  )
220
+ if len(linhas_resumo) > 25:
221
+ resumo += f"\n\n*…e mais {len(linhas_resumo) - 25} segmento(s).*"
222
 
223
  return img_resultado, resumo
224
 
225
 
226
+ # ─── Interface Gradio ────────────────────────────────────────
227
  DESCRICAO = """
228
+ # 🏙️ Segmentação Panóptica de Cenas Urbanas — Araguatins/TO
229
 
230
+ **Projeto PDI — Antonio Agacy Silva Lima Júnior & Maria Divina** · Instituto Federal do Tocantins (IFTO) · 2026
 
231
 
232
+ Modelo [OneFormer](https://arxiv.org/abs/2211.06220) (Jain et al., CVPR 2023), pré-treinado no **Cityscapes**
233
+ e **ajustado (fine-tuned) com fotos reais de Araguatins-TO** anotadas pela equipe — 12 classes,
234
+ incluindo cenas do rio Araguaia, ruas de bloquete e vegetação do cerrado.
235
 
236
+ ⏱️ *Rodando em CPU gratuita: a inferência leva de 30 a 90 segundos.*
 
237
  """
238
 
239
+ RODAPE = """
240
+ ---
241
+ **Classes do modelo:** edifício · ônibus · carro · moto · pessoa · rio · rua · calçada · céu · terreno · caminhão · vegetação
 
 
 
 
242
 
243
+ **Pipeline:** anotação no Roboflow → fine-tuning no Kaggle (T4) → publicação no 🤗 Hub → demo neste Space
 
 
 
244
 
245
+ **Referências:** Kirillov et al. (2019) — *Panoptic Segmentation* · Jain et al. (2023) — *OneFormer* ·
246
+ Ren et al. (2024) · Ravi et al. (2024)
247
+ """
248
+
249
+ with gr.Blocks(title="Segmentação Panóptica — PDI IFTO") as demo:
250
  gr.Markdown(DESCRICAO)
251
 
252
  with gr.Row():
 
254
  img_input = gr.Image(
255
  type="pil",
256
  label="📷 Imagem de entrada",
257
+ sources=["upload", "clipboard", "webcam"],
258
  )
259
  tarefa = gr.Radio(
260
  choices=[
 
266
  label="Modo de segmentação",
267
  )
268
  opacidade = gr.Slider(
269
+ minimum=0.3, maximum=0.9, value=0.55, step=0.05,
 
 
 
270
  label="Opacidade das máscaras",
271
  )
272
  btn = gr.Button("🚀 Segmentar", variant="primary", size="lg")
273
 
274
+ gr.Markdown(
275
+ "**Como usar:** envie uma foto de rua/trânsito, "
276
+ "escolha o modo e clique em Segmentar."
 
277
  )
278
+
279
+ with gr.Column(scale=2):
280
+ img_output = gr.Image(type="pil", label="Resultado")
281
+ texto_output = gr.Markdown()
282
+
283
+ # Exemplos prontos (adicione os arquivos na pasta examples/ do Space)
284
+ gr.Examples(
285
+ examples=[
286
+ ["examples/ifto_estacionamento.jpg"],
287
+ ["examples/rio_araguaia.jpg"],
288
+ ["examples/rua_centro.jpg"],
289
+ ],
290
+ inputs=[img_input],
291
+ label="📁 Exemplos de Araguatins (clique para carregar)",
292
+ )
293
 
294
  btn.click(
295
  fn=segmentar,
 
297
  outputs=[img_output, texto_output],
298
  )
299
 
300
+ gr.Markdown(RODAPE)
 
 
 
 
301
 
302
  if __name__ == "__main__":
303
+ demo.launch(theme=gr.themes.Soft(primary_hue="violet"))