VOIDER commited on
Commit
9c9f751
·
verified ·
1 Parent(s): fe8b6e2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +95 -176
app.py CHANGED
@@ -4,18 +4,17 @@ import re
4
  import json
5
  import tempfile
6
  import zipfile
7
- from pathlib import Path
8
  from huggingface_hub import hf_hub_download
9
  from llama_cpp import Llama
10
- from llama_cpp.llama_chat_format import Qwen2VLChatHandler
11
  import base64
12
  from PIL import Image
13
  from io import BytesIO
14
 
15
  # Константы
16
  REPO_ID = "mradermacher/VisualQuality-R1-7B-GGUF"
17
- MODEL_FILE = "VisualQuality-R1-7B.Q4_K_M.gguf" # 4.68 GB - баланс качества/размера
18
- MMPROJ_FILE = "VisualQuality-R1-7B.mmproj-Q8_0.gguf" # 853 MB
19
 
20
  # Промпты
21
  PROMPT = (
@@ -29,24 +28,21 @@ QUESTION_TEMPLATE_NO_THINKING = "{Question} Please only output the final answer
29
 
30
  # Глобальные переменные
31
  llm = None
32
- chat_handler = None
33
 
34
 
35
  def download_models():
36
- """Скачивание моделей из HuggingFace"""
37
  print("Downloading model files...")
38
 
39
  model_path = hf_hub_download(
40
  repo_id=REPO_ID,
41
  filename=MODEL_FILE,
42
- resume_download=True,
43
  )
44
  print(f"Model downloaded: {model_path}")
45
 
46
  mmproj_path = hf_hub_download(
47
  repo_id=REPO_ID,
48
  filename=MMPROJ_FILE,
49
- resume_download=True,
50
  )
51
  print(f"MMProj downloaded: {mmproj_path}")
52
 
@@ -55,45 +51,43 @@ def download_models():
55
 
56
  def load_model():
57
  """Загрузка модели"""
58
- global llm, chat_handler
59
 
60
  if llm is not None:
61
  return
62
 
63
  model_path, mmproj_path = download_models()
64
 
65
- print("Loading model into memory...")
66
 
67
- # Создаём chat handler для Qwen2-VL
68
- chat_handler = Qwen2VLChatHandler(
69
  clip_model_path=mmproj_path,
70
  verbose=False
71
  )
72
 
73
- # Загружаем основную модель
74
  llm = Llama(
75
  model_path=model_path,
76
  chat_handler=chat_handler,
77
- n_ctx=4096, # Контекст
78
- n_threads=4, # Потоки CPU
79
- n_gpu_layers=0, # CPU only
80
  verbose=False,
81
  )
82
 
83
- print("Model loaded successfully!")
84
 
85
 
86
- def image_to_base64_uri(image):
87
  """Конвертация PIL Image в data URI"""
88
  if image is None:
89
  return None
90
 
91
- # Конвертируем в RGB если нужно
92
  if image.mode != "RGB":
93
  image = image.convert("RGB")
94
 
95
  # Сжимаем для ускорения
96
- max_size = 1024
97
  if max(image.size) > max_size:
98
  ratio = max_size / max(image.size)
99
  new_size = (int(image.size[0] * ratio), int(image.size[1] * ratio))
@@ -107,14 +101,14 @@ def image_to_base64_uri(image):
107
 
108
 
109
  def extract_score(text):
110
- """Извлечение оценки из текста"""
111
  try:
112
- model_output_matches = re.findall(r'<answer>(.*?)</answer>', text, re.DOTALL)
113
- if model_output_matches:
114
- model_answer = model_output_matches[-1].strip()
115
  else:
116
- model_answer = text.strip()
117
- score_match = re.search(r'\d+(\.\d+)?', model_answer)
118
  if score_match:
119
  score = float(score_match.group())
120
  return min(max(score, 1.0), 5.0)
@@ -124,10 +118,10 @@ def extract_score(text):
124
 
125
 
126
  def extract_thinking(text):
127
- """Извлечение процесса мышления"""
128
- thinking_matches = re.findall(r'<think>(.*?)</think>', text, re.DOTALL)
129
- if thinking_matches:
130
- return thinking_matches[-1].strip()
131
  return ""
132
 
133
 
@@ -138,17 +132,13 @@ def score_single_image(image, use_thinking=True):
138
  load_model()
139
 
140
  if image is None:
141
- yield "❌ Please upload an image first.", "", ""
142
- return
143
 
144
- # Выбор шаблона
145
  template = QUESTION_TEMPLATE_THINKING if use_thinking else QUESTION_TEMPLATE_NO_THINKING
146
  prompt_text = template.format(Question=PROMPT)
147
 
148
- # Конвертируем изображение
149
- image_uri = image_to_base64_uri(image)
150
 
151
- # Формируем сообщение
152
  messages = [
153
  {
154
  "role": "user",
@@ -159,11 +149,9 @@ def score_single_image(image, use_thinking=True):
159
  }
160
  ]
161
 
162
- # Генерация со стримингом
163
  generated_text = ""
164
 
165
- yield "⏳ Processing...", "", "*Analyzing image...*"
166
-
167
  try:
168
  response = llm.create_chat_completion(
169
  messages=messages,
@@ -183,7 +171,7 @@ def score_single_image(image, use_thinking=True):
183
  score = extract_score(generated_text)
184
 
185
  if score is not None:
186
- score_display = f"⭐ **Quality Score: {score:.2f} / 5.00**"
187
  else:
188
  score_display = "*Analyzing...*"
189
 
@@ -196,7 +184,7 @@ def score_single_image(image, use_thinking=True):
196
  if final_score is not None:
197
  score_display = f"⭐ **Quality Score: {final_score:.2f} / 5.00**\n\n📊 **For Leaderboard:** `{final_score:.2f}`"
198
  else:
199
- score_display = "❌ Could not extract score. Please try again."
200
 
201
  yield generated_text, final_thinking, score_display
202
 
@@ -205,23 +193,20 @@ def score_single_image(image, use_thinking=True):
205
 
206
 
207
  def process_batch(files, use_thinking=True, progress=gr.Progress()):
208
- """Обработка пакета изображений"""
209
  global llm
210
 
211
  load_model()
212
 
213
  if not files:
214
- return "❌ No files uploaded", None
215
 
216
  results = []
217
  template = QUESTION_TEMPLATE_THINKING if use_thinking else QUESTION_TEMPLATE_NO_THINKING
218
  prompt_text = template.format(Question=PROMPT)
219
 
220
- progress(0, desc="Starting batch processing...")
221
-
222
  for i, file in enumerate(files):
223
  try:
224
- # Загружаем изображение
225
  if hasattr(file, 'name'):
226
  image = Image.open(file.name)
227
  filename = os.path.basename(file.name)
@@ -229,7 +214,7 @@ def process_batch(files, use_thinking=True, progress=gr.Progress()):
229
  image = Image.open(file)
230
  filename = f"image_{i+1}.jpg"
231
 
232
- image_uri = image_to_base64_uri(image)
233
 
234
  messages = [
235
  {
@@ -241,7 +226,6 @@ def process_batch(files, use_thinking=True, progress=gr.Progress()):
241
  }
242
  ]
243
 
244
- # Генерация
245
  response = llm.create_chat_completion(
246
  messages=messages,
247
  max_tokens=2048 if use_thinking else 256,
@@ -260,7 +244,7 @@ def process_batch(files, use_thinking=True, progress=gr.Progress()):
260
  "raw_output": generated_text
261
  })
262
 
263
- progress((i + 1) / len(files), desc=f"Processed {i+1}/{len(files)}: {filename}")
264
 
265
  except Exception as e:
266
  results.append({
@@ -270,168 +254,103 @@ def process_batch(files, use_thinking=True, progress=gr.Progress()):
270
  "raw_output": str(e)
271
  })
272
 
273
- # Создаём файлы результатов
274
  with tempfile.TemporaryDirectory() as tmpdir:
275
- # Текстовый файл для лидерборда
276
- leaderboard_file = os.path.join(tmpdir, "leaderboard_scores.txt")
277
- with open(leaderboard_file, "w") as f:
278
  for r in results:
279
- score_str = f"{r['score']:.2f}" if isinstance(r['score'], float) else r['score']
280
  f.write(f"{r['filename']}\t{score_str}\n")
281
 
282
- # JSON с полными результатами
283
- json_file = os.path.join(tmpdir, "full_results.json")
284
  with open(json_file, "w") as f:
285
  json.dump(results, f, indent=2, ensure_ascii=False)
286
 
287
- # CSV файл
288
  csv_file = os.path.join(tmpdir, "scores.csv")
289
  with open(csv_file, "w") as f:
290
  f.write("filename,score\n")
291
  for r in results:
292
- score_str = f"{r['score']:.2f}" if isinstance(r['score'], float) else r['score']
293
  f.write(f"{r['filename']},{score_str}\n")
294
 
295
- # Создаём ZIP архив
296
  zip_path = os.path.join(tmpdir, "results.zip")
297
  with zipfile.ZipFile(zip_path, 'w') as zipf:
298
- zipf.write(leaderboard_file, "leaderboard_scores.txt")
299
- zipf.write(json_file, "full_results.json")
300
  zipf.write(csv_file, "scores.csv")
301
 
302
- # Копируем ZIP во временную папку, которая не удалится
303
  final_zip = tempfile.NamedTemporaryFile(delete=False, suffix=".zip")
304
  with open(zip_path, 'rb') as f:
305
  final_zip.write(f.read())
306
  final_zip.close()
307
 
308
- # Формируем summary
309
  valid_scores = [r['score'] for r in results if isinstance(r['score'], float)]
310
- summary = f"""
311
- ## ✅ Batch Processing Complete!
 
312
 
313
  **Processed:** {len(results)} images
314
- **Successful:** {len(valid_scores)} images
315
- **Failed:** {len(results) - len(valid_scores)} images
316
 
317
- ### Statistics:
318
- - **Average Score:** {sum(valid_scores)/len(valid_scores):.2f} (if valid scores exist)
319
- - **Min Score:** {min(valid_scores):.2f if valid_scores else 'N/A'}
320
- - **Max Score:** {max(valid_scores):.2f if valid_scores else 'N/A'}
321
 
322
- ### Preview (first 10):
323
- | Filename | Score |
324
- |----------|-------|
325
- """ + "\n".join([f"| {r['filename']} | {r['score']:.2f if isinstance(r['score'], float) else r['score']} |" for r in results[:10]])
326
 
327
  return summary, final_zip.name
328
 
329
 
330
- def create_interface():
331
- """Создание интерфейса Gradio"""
 
 
332
 
333
- with gr.Blocks(title="VisualQuality-R1 GGUF") as demo:
334
-
335
- gr.Markdown("""
336
- # 🎨 VisualQuality-R1: Image Quality Assessment (GGUF/CPU)
337
-
338
- **Reasoning-Induced Image Quality Assessment** | Running on CPU with GGUF quantization
339
-
340
- [![Paper](https://img.shields.io/badge/arXiv-Paper-red)](https://arxiv.org/abs/2505.14460)
341
- [![Model](https://img.shields.io/badge/🤗-Model-yellow)](https://huggingface.co/TianheWu/VisualQuality-R1-7B)
342
-
343
- > ⚠️ **CPU Mode**: Processing is slower but works without GPU. ~30-60 sec per image.
344
- """)
345
-
346
- with gr.Tabs():
347
- # Вкладка для одного изображения
348
- with gr.TabItem("📷 Single Image"):
349
- with gr.Row():
350
- with gr.Column(scale=1):
351
- image_input = gr.Image(
352
- label="Upload Image",
353
- type="pil",
354
- height=350
355
- )
356
-
357
- thinking_checkbox = gr.Checkbox(
358
- label="🧠 Enable Thinking Mode",
359
- value=True
360
- )
361
-
362
- submit_btn = gr.Button(
363
- "🔍 Analyze Quality",
364
- variant="primary",
365
- size="lg"
366
- )
367
-
368
- with gr.Column(scale=1):
369
- score_output = gr.Markdown(value="*Upload an image to see the score*")
370
- thinking_output = gr.Textbox(label="🧠 Thinking", lines=6, interactive=False)
371
- raw_output = gr.Textbox(label="📝 Full Output", lines=8, interactive=False)
372
 
373
- submit_btn.click(
374
- fn=score_single_image,
375
- inputs=[image_input, thinking_checkbox],
376
- outputs=[raw_output, thinking_output, score_output],
377
- )
378
 
379
- # Вкладка для batch processing
380
- with gr.TabItem("📁 Batch Processing (1000+ images)"):
381
- gr.Markdown("""
382
- ### Batch Processing for Leaderboard
383
- Upload multiple images (ZIP or individual files) to process them all at once.
384
- Results will be saved in a format ready for leaderboard submission.
385
- """)
386
-
387
- with gr.Row():
388
- with gr.Column():
389
- batch_files = gr.File(
390
- label="Upload Images",
391
- file_count="multiple",
392
- file_types=["image"],
393
- )
394
-
395
- batch_thinking = gr.Checkbox(
396
- label="🧠 Enable Thinking Mode (slower but more detailed)",
397
- value=False # По умолчанию выключено для скорости
398
- )
399
-
400
- batch_btn = gr.Button(
401
- "🚀 Process All Images",
402
- variant="primary",
403
- size="lg"
404
- )
405
-
406
- with gr.Column():
407
- batch_summary = gr.Markdown(value="*Upload images and click Process*")
408
- batch_download = gr.File(label="📥 Download Results")
409
-
410
- batch_btn.click(
411
- fn=process_batch,
412
- inputs=[batch_files, batch_thinking],
413
- outputs=[batch_summary, batch_download],
414
- )
415
 
416
- gr.Markdown("""
417
- ---
418
- ### 📊 Score Scale
419
- | Score | Quality |
420
- |-------|---------|
421
- | 1.0 | Very poor |
422
- | 2.0 | Poor |
423
- | 3.0 | Fair |
424
- | 4.0 | Good |
425
- | 5.0 | Excellent |
426
- """)
427
-
428
- return demo
429
-
430
 
431
  if __name__ == "__main__":
432
- demo = create_interface()
433
  demo.queue(max_size=5)
434
- demo.launch(
435
- show_error=True,
436
- ssr_mode=False,
437
- )
 
4
  import json
5
  import tempfile
6
  import zipfile
 
7
  from huggingface_hub import hf_hub_download
8
  from llama_cpp import Llama
9
+ from llama_cpp.llama_chat_format import Llava15ChatHandler
10
  import base64
11
  from PIL import Image
12
  from io import BytesIO
13
 
14
  # Константы
15
  REPO_ID = "mradermacher/VisualQuality-R1-7B-GGUF"
16
+ MODEL_FILE = "VisualQuality-R1-7B.Q4_K_M.gguf"
17
+ MMPROJ_FILE = "VisualQuality-R1-7B.mmproj-Q8_0.gguf"
18
 
19
  # Промпты
20
  PROMPT = (
 
28
 
29
  # Глобальные переменные
30
  llm = None
 
31
 
32
 
33
  def download_models():
34
+ """Скачивание моделей"""
35
  print("Downloading model files...")
36
 
37
  model_path = hf_hub_download(
38
  repo_id=REPO_ID,
39
  filename=MODEL_FILE,
 
40
  )
41
  print(f"Model downloaded: {model_path}")
42
 
43
  mmproj_path = hf_hub_download(
44
  repo_id=REPO_ID,
45
  filename=MMPROJ_FILE,
 
46
  )
47
  print(f"MMProj downloaded: {mmproj_path}")
48
 
 
51
 
52
  def load_model():
53
  """Загрузка модели"""
54
+ global llm
55
 
56
  if llm is not None:
57
  return
58
 
59
  model_path, mmproj_path = download_models()
60
 
61
+ print("Loading model...")
62
 
63
+ # Используем Llava15ChatHandler для vision моделей
64
+ chat_handler = Llava15ChatHandler(
65
  clip_model_path=mmproj_path,
66
  verbose=False
67
  )
68
 
 
69
  llm = Llama(
70
  model_path=model_path,
71
  chat_handler=chat_handler,
72
+ n_ctx=4096,
73
+ n_threads=4,
74
+ n_gpu_layers=0,
75
  verbose=False,
76
  )
77
 
78
+ print("Model loaded!")
79
 
80
 
81
+ def image_to_data_uri(image):
82
  """Конвертация PIL Image в data URI"""
83
  if image is None:
84
  return None
85
 
 
86
  if image.mode != "RGB":
87
  image = image.convert("RGB")
88
 
89
  # Сжимаем для ускорения
90
+ max_size = 768
91
  if max(image.size) > max_size:
92
  ratio = max_size / max(image.size)
93
  new_size = (int(image.size[0] * ratio), int(image.size[1] * ratio))
 
101
 
102
 
103
  def extract_score(text):
104
+ """Извлечение оценки"""
105
  try:
106
+ matches = re.findall(r'<answer>(.*?)</answer>', text, re.DOTALL)
107
+ if matches:
108
+ answer = matches[-1].strip()
109
  else:
110
+ answer = text.strip()
111
+ score_match = re.search(r'\d+(\.\d+)?', answer)
112
  if score_match:
113
  score = float(score_match.group())
114
  return min(max(score, 1.0), 5.0)
 
118
 
119
 
120
  def extract_thinking(text):
121
+ """Извлечение мышления"""
122
+ matches = re.findall(r'<think>(.*?)</think>', text, re.DOTALL)
123
+ if matches:
124
+ return matches[-1].strip()
125
  return ""
126
 
127
 
 
132
  load_model()
133
 
134
  if image is None:
135
+ return "❌ Upload an image first", "", ""
 
136
 
 
137
  template = QUESTION_TEMPLATE_THINKING if use_thinking else QUESTION_TEMPLATE_NO_THINKING
138
  prompt_text = template.format(Question=PROMPT)
139
 
140
+ image_uri = image_to_data_uri(image)
 
141
 
 
142
  messages = [
143
  {
144
  "role": "user",
 
149
  }
150
  ]
151
 
152
+ # Стриминг
153
  generated_text = ""
154
 
 
 
155
  try:
156
  response = llm.create_chat_completion(
157
  messages=messages,
 
171
  score = extract_score(generated_text)
172
 
173
  if score is not None:
174
+ score_display = f"⭐ **Score: {score:.2f} / 5.00**"
175
  else:
176
  score_display = "*Analyzing...*"
177
 
 
184
  if final_score is not None:
185
  score_display = f"⭐ **Quality Score: {final_score:.2f} / 5.00**\n\n📊 **For Leaderboard:** `{final_score:.2f}`"
186
  else:
187
+ score_display = "❌ Could not extract score"
188
 
189
  yield generated_text, final_thinking, score_display
190
 
 
193
 
194
 
195
  def process_batch(files, use_thinking=True, progress=gr.Progress()):
196
+ """Batch processing"""
197
  global llm
198
 
199
  load_model()
200
 
201
  if not files:
202
+ return "❌ No files", None
203
 
204
  results = []
205
  template = QUESTION_TEMPLATE_THINKING if use_thinking else QUESTION_TEMPLATE_NO_THINKING
206
  prompt_text = template.format(Question=PROMPT)
207
 
 
 
208
  for i, file in enumerate(files):
209
  try:
 
210
  if hasattr(file, 'name'):
211
  image = Image.open(file.name)
212
  filename = os.path.basename(file.name)
 
214
  image = Image.open(file)
215
  filename = f"image_{i+1}.jpg"
216
 
217
+ image_uri = image_to_data_uri(image)
218
 
219
  messages = [
220
  {
 
226
  }
227
  ]
228
 
 
229
  response = llm.create_chat_completion(
230
  messages=messages,
231
  max_tokens=2048 if use_thinking else 256,
 
244
  "raw_output": generated_text
245
  })
246
 
247
+ progress((i + 1) / len(files), desc=f"Processed {i+1}/{len(files)}")
248
 
249
  except Exception as e:
250
  results.append({
 
254
  "raw_output": str(e)
255
  })
256
 
257
+ # Создаём файлы
258
  with tempfile.TemporaryDirectory() as tmpdir:
259
+ # TXT для лидерборда
260
+ txt_file = os.path.join(tmpdir, "scores.txt")
261
+ with open(txt_file, "w") as f:
262
  for r in results:
263
+ score_str = f"{r['score']:.2f}" if isinstance(r['score'], float) else str(r['score'])
264
  f.write(f"{r['filename']}\t{score_str}\n")
265
 
266
+ # JSON
267
+ json_file = os.path.join(tmpdir, "results.json")
268
  with open(json_file, "w") as f:
269
  json.dump(results, f, indent=2, ensure_ascii=False)
270
 
271
+ # CSV
272
  csv_file = os.path.join(tmpdir, "scores.csv")
273
  with open(csv_file, "w") as f:
274
  f.write("filename,score\n")
275
  for r in results:
276
+ score_str = f"{r['score']:.2f}" if isinstance(r['score'], float) else str(r['score'])
277
  f.write(f"{r['filename']},{score_str}\n")
278
 
279
+ # ZIP
280
  zip_path = os.path.join(tmpdir, "results.zip")
281
  with zipfile.ZipFile(zip_path, 'w') as zipf:
282
+ zipf.write(txt_file, "scores.txt")
283
+ zipf.write(json_file, "results.json")
284
  zipf.write(csv_file, "scores.csv")
285
 
286
+ # Копируем
287
  final_zip = tempfile.NamedTemporaryFile(delete=False, suffix=".zip")
288
  with open(zip_path, 'rb') as f:
289
  final_zip.write(f.read())
290
  final_zip.close()
291
 
292
+ # Summary
293
  valid_scores = [r['score'] for r in results if isinstance(r['score'], float)]
294
+ avg = sum(valid_scores)/len(valid_scores) if valid_scores else 0
295
+
296
+ summary = f"""## ✅ Done!
297
 
298
  **Processed:** {len(results)} images
299
+ **Success:** {len(valid_scores)}
300
+ **Failed:** {len(results) - len(valid_scores)}
301
 
302
+ **Average:** {avg:.2f}
303
+ **Min:** {min(valid_scores):.2f if valid_scores else 'N/A'}
304
+ **Max:** {max(valid_scores):.2f if valid_scores else 'N/A'}
 
305
 
306
+ ### Preview:
307
+ | File | Score |
308
+ |------|-------|
309
+ """ + "\n".join([f"| {r['filename'][:30]} | {r['score']:.2f if isinstance(r['score'], float) else r['score']} |" for r in results[:10]])
310
 
311
  return summary, final_zip.name
312
 
313
 
314
+ # Интерфейс
315
+ with gr.Blocks(title="VisualQuality-R1") as demo:
316
+ gr.Markdown("""
317
+ # 🎨 VisualQuality-R1 (GGUF/CPU)
318
 
319
+ Image Quality Assessment | CPU Mode (~30-60 sec/image)
320
+
321
+ [![Paper](https://img.shields.io/badge/arXiv-Paper-red)](https://arxiv.org/abs/2505.14460)
322
+ """)
323
+
324
+ with gr.Tabs():
325
+ with gr.TabItem("📷 Single Image"):
326
+ with gr.Row():
327
+ with gr.Column():
328
+ img_input = gr.Image(label="Upload", type="pil", height=350)
329
+ thinking_cb = gr.Checkbox(label="🧠 Thinking Mode", value=True)
330
+ btn = gr.Button("🔍 Analyze", variant="primary", size="lg")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
331
 
332
+ with gr.Column():
333
+ score_out = gr.Markdown("*Upload image*")
334
+ thinking_out = gr.Textbox(label="Thinking", lines=6)
335
+ raw_out = gr.Textbox(label="Output", lines=8)
 
336
 
337
+ btn.click(score_single_image, [img_input, thinking_cb], [raw_out, thinking_out, score_out])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
338
 
339
+ with gr.TabItem("📁 Batch (1000+ images)"):
340
+ gr.Markdown("### Upload multiple images for leaderboard submission")
341
+
342
+ with gr.Row():
343
+ with gr.Column():
344
+ batch_files = gr.File(label="Images", file_count="multiple", file_types=["image"])
345
+ batch_thinking = gr.Checkbox(label="🧠 Thinking (slower)", value=False)
346
+ batch_btn = gr.Button("🚀 Process All", variant="primary", size="lg")
347
+
348
+ with gr.Column():
349
+ batch_summary = gr.Markdown("*Upload and click Process*")
350
+ batch_download = gr.File(label="📥 Download Results")
351
+
352
+ batch_btn.click(process_batch, [batch_files, batch_thinking], [batch_summary, batch_download])
353
 
354
  if __name__ == "__main__":
 
355
  demo.queue(max_size=5)
356
+ demo.launch(server_name="0.0.0.0", server_port=7860)