VOIDER commited on
Commit
9520f85
·
verified ·
1 Parent(s): c37360a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +326 -178
app.py CHANGED
@@ -1,13 +1,21 @@
1
  import gradio as gr
2
- import torch
3
- from transformers import Qwen2_5_VLForConditionalGeneration, AutoProcessor, BitsAndBytesConfig, TextIteratorStreamer
4
- from qwen_vl_utils import process_vision_info
5
- from threading import Thread
6
  import re
7
- import spaces
 
 
 
 
 
 
 
 
 
8
 
9
  # Константы
10
- MODEL_PATH = "TianheWu/VisualQuality-R1-7B"
 
 
11
 
12
  # Промпты
13
  PROMPT = (
@@ -19,40 +27,85 @@ PROMPT = (
19
  QUESTION_TEMPLATE_THINKING = "{Question} First output the thinking process in <think> </think> tags and then output the final answer with only one score in <answer> </answer> tags."
20
  QUESTION_TEMPLATE_NO_THINKING = "{Question} Please only output the final answer with only one score in <answer> </answer> tags."
21
 
22
- # Глобальные переменные для модели
23
- model = None
24
- processor = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
 
27
  def load_model():
28
- """Загрузка модели с 8-bit квантизацией"""
29
- global model, processor
30
 
31
- if model is not None:
32
  return
33
 
34
- print("Loading model...")
35
 
36
- quantization_config = BitsAndBytesConfig(
37
- load_in_8bit=True,
38
- llm_int8_threshold=6.0,
39
- )
40
 
41
- model = Qwen2_5_VLForConditionalGeneration.from_pretrained(
42
- MODEL_PATH,
43
- quantization_config=quantization_config,
44
- device_map="auto",
45
- trust_remote_code=True,
46
- torch_dtype=torch.float16,
47
  )
48
- model.eval()
49
 
50
- processor = AutoProcessor.from_pretrained(MODEL_PATH, trust_remote_code=True)
51
- processor.tokenizer.padding_side = "left"
 
 
 
 
 
 
 
52
 
53
  print("Model loaded successfully!")
54
 
55
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  def extract_score(text):
57
  """Извлечение оценки из текста"""
58
  try:
@@ -65,25 +118,23 @@ def extract_score(text):
65
  if score_match:
66
  score = float(score_match.group())
67
  return min(max(score, 1.0), 5.0)
68
- except Exception as e:
69
- print(f"Error extracting score: {e}")
70
  return None
71
 
72
 
73
  def extract_thinking(text):
74
- """Извлечение процесса мышления из текста"""
75
  thinking_matches = re.findall(r'<think>(.*?)</think>', text, re.DOTALL)
76
  if thinking_matches:
77
  return thinking_matches[-1].strip()
78
- return None
79
 
80
 
81
- @spaces.GPU(duration=180)
82
- def score_image_streaming(image, use_thinking=True):
83
- """Оценка качества изображения со стримингом"""
84
- global model, processor
85
 
86
- # Загрузка модели при первом вызове
87
  load_model()
88
 
89
  if image is None:
@@ -91,189 +142,287 @@ def score_image_streaming(image, use_thinking=True):
91
  return
92
 
93
  # Выбор шаблона
94
- if use_thinking:
95
- question_template = QUESTION_TEMPLATE_THINKING
96
- else:
97
- question_template = QUESTION_TEMPLATE_NO_THINKING
 
98
 
99
- # Формирование сообщения
100
- message = [
101
  {
102
  "role": "user",
103
  "content": [
104
- {'type': 'image', 'image': image},
105
- {"type": "text", "text": question_template.format(Question=PROMPT)}
106
- ],
107
  }
108
  ]
109
 
110
- batch_messages = [message]
 
111
 
112
- # Подготовка входных данных
113
- text = [processor.apply_chat_template(
114
- msg, tokenize=False, add_generation_prompt=True, add_vision_id=True
115
- ) for msg in batch_messages]
116
 
117
- image_inputs, video_inputs = process_vision_info(batch_messages)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
 
119
- inputs = processor(
120
- text=text,
121
- images=image_inputs,
122
- videos=video_inputs,
123
- padding=True,
124
- return_tensors="pt",
125
- )
126
- inputs = inputs.to(model.device)
127
 
128
- # Настройка стриминга
129
- streamer = TextIteratorStreamer(
130
- processor.tokenizer,
131
- skip_prompt=True,
132
- skip_special_tokens=True
133
- )
134
 
135
- generation_kwargs = dict(
136
- **inputs,
137
- streamer=streamer,
138
- max_new_tokens=2048 if use_thinking else 256,
139
- do_sample=True,
140
- top_k=50,
141
- top_p=0.95,
142
- temperature=0.7,
143
- use_cache=True,
144
- )
145
 
146
- # Запуск генерации в отдельном потоке
147
- thread = Thread(target=model.generate, kwargs=generation_kwargs)
148
- thread.start()
149
 
150
- # Стриминг вывода
151
- generated_text = ""
152
- current_thinking = ""
153
- current_score = "*Analyzing...*"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
 
155
- for new_text in streamer:
156
- generated_text += new_text
 
 
 
 
 
 
157
 
158
- # Извлечение мышления (если есть)
159
- thinking = extract_thinking(generated_text)
160
- if thinking:
161
- current_thinking = thinking
162
 
163
- # Извлечение оценки
164
- score = extract_score(generated_text)
165
- if score is not None:
166
- current_score = f"⭐ **Quality Score: {score:.2f} / 5.00**"
 
 
 
167
 
168
- yield generated_text, current_thinking, current_score
169
-
170
- thread.join()
171
-
172
- # Финальное извлечение
173
- final_score = extract_score(generated_text)
174
- final_thinking = extract_thinking(generated_text) if use_thinking else ""
 
 
 
 
 
175
 
176
- if final_score is not None:
177
- score_display = f"⭐ **Quality Score: {final_score:.2f} / 5.00**\n\n📊 **For Leaderboard:** `{final_score:.2f}`"
178
- else:
179
- score_display = "❌ Could not extract score. Please try again."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
 
181
- yield generated_text, final_thinking or "", score_display
182
 
183
 
184
  def create_interface():
185
  """Создание интерфейса Gradio"""
186
 
187
- # Убрали theme из gr.Blocks() - теперь он передаётся в launch()
188
- with gr.Blocks(
189
- title="VisualQuality-R1: Image Quality Assessment",
190
- ) as demo:
191
 
192
  gr.Markdown("""
193
- # 🎨 VisualQuality-R1: Image Quality Assessment
194
 
195
- **Reasoning-Induced Image Quality Assessment via Reinforcement Learning to Rank**
196
-
197
- Upload an image to get a quality score (1-5) with detailed reasoning.
198
 
199
  [![Paper](https://img.shields.io/badge/arXiv-Paper-red)](https://arxiv.org/abs/2505.14460)
200
  [![Model](https://img.shields.io/badge/🤗-Model-yellow)](https://huggingface.co/TianheWu/VisualQuality-R1-7B)
 
 
201
  """)
202
 
203
- with gr.Row():
204
- with gr.Column(scale=1):
205
- image_input = gr.Image(
206
- label="📷 Upload Image",
207
- type="pil",
208
- height=400
209
- )
210
-
211
- thinking_checkbox = gr.Checkbox(
212
- label="🧠 Enable Thinking Mode (detailed reasoning)",
213
- value=True
214
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
215
 
216
- submit_btn = gr.Button(
217
- "🔍 Analyze Image Quality",
218
- variant="primary",
219
- size="lg"
220
  )
221
-
 
 
222
  gr.Markdown("""
223
- ### 📖 Instructions:
224
- 1. Upload an image
225
- 2. Enable/disable thinking mode
226
- 3. Click "Analyze Image Quality"
227
- 4. Wait for the score and reasoning
228
-
229
- ### 📊 Score Scale:
230
- - **1.0**: Very poor quality
231
- - **2.0**: Poor quality
232
- - **3.0**: Fair quality
233
- - **4.0**: Good quality
234
- - **5.0**: Excellent quality
235
  """)
236
-
237
- with gr.Column(scale=1):
238
- score_output = gr.Markdown(
239
- label="Quality Score",
240
- value="*Upload an image to see the score*"
241
- )
242
 
243
- thinking_output = gr.Textbox(
244
- label="🧠 Thinking Process",
245
- lines=8,
246
- max_lines=15,
247
- placeholder="Reasoning will appear here when thinking mode is enabled...",
248
- interactive=False
249
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
250
 
251
- raw_output = gr.Textbox(
252
- label="📝 Full Model Output",
253
- lines=10,
254
- max_lines=20,
255
- placeholder="Full model response will appear here...",
256
- interactive=False
257
  )
258
 
259
- # Обработка события
260
- submit_btn.click(
261
- fn=score_image_streaming,
262
- inputs=[image_input, thinking_checkbox],
263
- outputs=[raw_output, thinking_output, score_output],
264
- )
265
-
266
  gr.Markdown("""
267
  ---
268
- ### 📚 Citation
269
- ```bibtex
270
- @article{wu2025visualquality,
271
- title={{VisualQuality-R1}: Reasoning-Induced Image Quality Assessment via Reinforcement Learning to Rank},
272
- author={Wu, Tianhe and Zou, Jian and Liang, Jie and Zhang, Lei and Ma, Kede},
273
- journal={arXiv preprint arXiv:2505.14460},
274
- year={2025}
275
- }
276
- ```
277
  """)
278
 
279
  return demo
@@ -281,9 +430,8 @@ def create_interface():
281
 
282
  if __name__ == "__main__":
283
  demo = create_interface()
284
- demo.queue(max_size=10)
285
- # Добавлены параметры для Gradio 6.0
286
  demo.launch(
287
- ssr_mode=False, # Отключаем SSR для стабильности
288
  show_error=True,
 
289
  )
 
1
  import gradio as gr
2
+ import os
 
 
 
3
  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 = (
 
27
  QUESTION_TEMPLATE_THINKING = "{Question} First output the thinking process in <think> </think> tags and then output the final answer with only one score in <answer> </answer> tags."
28
  QUESTION_TEMPLATE_NO_THINKING = "{Question} Please only output the final answer with only one score in <answer> </answer> tags."
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
+
53
+ return model_path, mmproj_path
54
 
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))
100
+ image = image.resize(new_size, Image.LANCZOS)
101
+
102
+ buffered = BytesIO()
103
+ image.save(buffered, format="JPEG", quality=85)
104
+ img_base64 = base64.b64encode(buffered.getvalue()).decode("utf-8")
105
+
106
+ return f"data:image/jpeg;base64,{img_base64}"
107
+
108
+
109
  def extract_score(text):
110
  """Извлечение оценки из текста"""
111
  try:
 
118
  if score_match:
119
  score = float(score_match.group())
120
  return min(max(score, 1.0), 5.0)
121
+ except:
122
+ pass
123
  return None
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
 
134
+ def score_single_image(image, use_thinking=True):
135
+ """Оценка одного изображения"""
136
+ global llm
 
137
 
 
138
  load_model()
139
 
140
  if image is None:
 
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",
155
  "content": [
156
+ {"type": "image_url", "image_url": {"url": image_uri}},
157
+ {"type": "text", "text": prompt_text}
158
+ ]
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,
170
+ max_tokens=2048 if use_thinking else 256,
171
+ temperature=0.7,
172
+ top_p=0.95,
173
+ stream=True,
174
+ )
175
+
176
+ for chunk in response:
177
+ delta = chunk.get("choices", [{}])[0].get("delta", {})
178
+ content = delta.get("content", "")
179
+ if content:
180
+ generated_text += content
181
+
182
+ thinking = extract_thinking(generated_text)
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
+
190
+ yield generated_text, thinking, score_display
191
+
192
+ # Финальный результат
193
+ final_score = extract_score(generated_text)
194
+ final_thinking = extract_thinking(generated_text) if use_thinking else ""
195
+
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
+
203
+ except Exception as e:
204
+ yield f"❌ Error: {str(e)}", "", ""
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)
228
+ else:
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
+ {
236
+ "role": "user",
237
+ "content": [
238
+ {"type": "image_url", "image_url": {"url": image_uri}},
239
+ {"type": "text", "text": prompt_text}
240
+ ]
241
+ }
242
+ ]
243
+
244
+ # Генерация
245
+ response = llm.create_chat_completion(
246
+ messages=messages,
247
+ max_tokens=2048 if use_thinking else 256,
248
+ temperature=0.7,
249
+ top_p=0.95,
250
+ )
251
+
252
+ generated_text = response["choices"][0]["message"]["content"]
253
+ score = extract_score(generated_text)
254
+ thinking = extract_thinking(generated_text) if use_thinking else ""
255
+
256
+ results.append({
257
+ "filename": filename,
258
+ "score": score if score else "N/A",
259
+ "thinking": thinking,
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({
267
+ "filename": filename if 'filename' in dir() else f"image_{i+1}",
268
+ "score": "ERROR",
269
+ "thinking": "",
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
 
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
  )