MashiroLn commited on
Commit
b49cf86
·
verified ·
1 Parent(s): 46afed9

Upload folder using huggingface_hub

Browse files
Files changed (4) hide show
  1. app.py +5 -1
  2. apps/paper_image_tool.py +456 -0
  3. apps/text_tools.py +20 -8
  4. requirements.txt +3 -1
app.py CHANGED
@@ -1,5 +1,5 @@
1
  import gradio as gr
2
- from apps import pdf_cropper, text_tools
3
 
4
  def create_main_interface():
5
  with gr.Blocks(title="我的科研工具箱") as main_app:
@@ -16,6 +16,10 @@ def create_main_interface():
16
  with gr.TabItem("📝 Token Stats"):
17
  text_tools.create_ui()
18
 
 
 
 
 
19
  # --- 可以在这里继续添加更多 Tab ---
20
 
21
  return main_app
 
1
  import gradio as gr
2
+ from apps import pdf_cropper, text_tools, paper_image_tool
3
 
4
  def create_main_interface():
5
  with gr.Blocks(title="我的科研工具箱") as main_app:
 
16
  with gr.TabItem("📝 Token Stats"):
17
  text_tools.create_ui()
18
 
19
+ # --- 工具 3: 科研配图助手 ---
20
+ with gr.TabItem("📑 Paper Image Helper"):
21
+ paper_image_tool.create_paper_tool()
22
+
23
  # --- 可以在这里继续添加更多 Tab ---
24
 
25
  return main_app
apps/paper_image_tool.py ADDED
@@ -0,0 +1,456 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import os
3
+ import zipfile
4
+ import pandas as pd
5
+ from PIL import Image, ImageOps, ImageChops
6
+ import shutil
7
+ from pathlib import Path
8
+ import re
9
+ import tempfile
10
+ import json
11
+ import requests
12
+
13
+ # --- LLM Configuration ---
14
+ LLM_API_KEY = "sk-fa6c38ce957e4c7b946ccbeed33237ec" # Replace with your actual key or use env var
15
+ LLM_API_URL = "https://api.deepseek.com/v1/chat/completions" # Example URL
16
+
17
+ def call_llm_structure_inference(file_list):
18
+ """
19
+ Calls DeepSeek API to infer structure from a list of file paths.
20
+ """
21
+ if not file_list:
22
+ return []
23
+
24
+ # Take a sample if too many files to save tokens/context
25
+ sample_files = file_list[:50] if len(file_list) > 50 else file_list
26
+
27
+ prompt = f"""
28
+ I have a list of file paths from a research project. I need to organize them into a structured format.
29
+ The structure should identify a 'Sample ID' (unique identifier for the experiment/sample) and a 'Type' (category of the image, e.g., input, heatmap, result).
30
+
31
+ Here are the file paths:
32
+ {json.dumps(sample_files, indent=2)}
33
+
34
+ Please analyze the naming patterns and directory structure.
35
+ Return a JSON object with a list of rules or a direct mapping.
36
+ For this task, simply return a JSON list where each item corresponds to the input files, with 'path', 'sample_id', and 'type' fields.
37
+ If you can infer a regex pattern, please include it in a 'pattern' field in the root of the JSON.
38
+
39
+ Format:
40
+ {{
41
+ "files": [
42
+ {{"path": "path/to/file1.png", "sample_id": "exp1", "type": "input"}},
43
+ ...
44
+ ]
45
+ }}
46
+ Only return the JSON, no markdown formatting.
47
+ """
48
+
49
+ headers = {
50
+ "Authorization": f"Bearer {LLM_API_KEY}",
51
+ "Content-Type": "application/json"
52
+ }
53
+
54
+ data = {
55
+ "model": "deepseek-chat", # Or appropriate model name
56
+ "messages": [
57
+ {"role": "system", "content": "You are a helpful assistant that parses file paths into structured data."},
58
+ {"role": "user", "content": prompt}
59
+ ],
60
+ "stream": False
61
+ }
62
+
63
+ try:
64
+ response = requests.post(LLM_API_URL, headers=headers, json=data, timeout=30)
65
+ response.raise_for_status()
66
+ result = response.json()
67
+ content = result['choices'][0]['message']['content']
68
+
69
+ # Clean up markdown code blocks if present
70
+ if "```json" in content:
71
+ content = content.split("```json")[1].split("```")[0]
72
+ elif "```" in content:
73
+ content = content.split("```")[1].split("```")[0]
74
+
75
+ parsed = json.loads(content.strip())
76
+ return parsed.get('files', [])
77
+ except Exception as e:
78
+ print(f"LLM API Error: {e}")
79
+ return []
80
+
81
+ class ImageMatrix:
82
+ def __init__(self):
83
+ self.temp_dir = None
84
+ self.file_map = [] # List of {'path': str, 'type': str, 'sample_id': str}
85
+ self.types = []
86
+ self.samples = []
87
+ self.use_llm = False # Toggle for LLM usage
88
+
89
+ def load_zip(self, zip_path, use_llm=False):
90
+ self.use_llm = use_llm
91
+ self.temp_dir = tempfile.mkdtemp()
92
+ with zipfile.ZipFile(zip_path, 'r') as zip_ref:
93
+ zip_ref.extractall(self.temp_dir)
94
+
95
+ # Scan files
96
+ image_extensions = {'.png', '.jpg', '.jpeg', '.bmp', '.tiff'}
97
+ files = []
98
+ for root, _, filenames in os.walk(self.temp_dir):
99
+ for filename in filenames:
100
+ if Path(filename).suffix.lower() in image_extensions:
101
+ full_path = os.path.join(root, filename)
102
+ rel_path = os.path.relpath(full_path, self.temp_dir)
103
+ files.append(rel_path)
104
+
105
+ if self.use_llm:
106
+ self._infer_structure_llm(files)
107
+ else:
108
+ self._infer_structure(files)
109
+
110
+ return self.get_summary()
111
+
112
+ def _infer_structure_llm(self, files):
113
+ print("Using LLM for structure inference...")
114
+ llm_results = call_llm_structure_inference(files)
115
+
116
+ if not llm_results:
117
+ print("LLM failed or returned empty, falling back to heuristic.")
118
+ self._infer_structure(files)
119
+ return
120
+
121
+ # Map LLM results back to full file list (if we only sampled)
122
+ # For now, let's assume the LLM returns a mapping for the provided files.
123
+ # If we sampled, we might need to generalize the pattern.
124
+ # To keep it simple for this iteration, we'll trust the LLM for the sample
125
+ # and try to apply the logic to the rest if possible, or just use what we have.
126
+ # Ideally, we ask LLM for a Regex.
127
+
128
+ parsed_data = []
129
+ all_types = set()
130
+ all_samples = set()
131
+
132
+ # Create a lookup for the LLM results
133
+ llm_lookup = {item['path']: item for item in llm_results}
134
+
135
+ for f in files:
136
+ if f in llm_lookup:
137
+ item = llm_lookup[f]
138
+ t = item.get('type', 'unknown')
139
+ s = item.get('sample_id', 'unknown')
140
+ else:
141
+ # Fallback for files not in LLM sample
142
+ # In a real app, we'd use the Regex returned by LLM
143
+ t = 'unknown'
144
+ s = 'unknown'
145
+
146
+ parsed_data.append({
147
+ 'rel_path': f,
148
+ 'full_path': os.path.join(self.temp_dir, f),
149
+ 'type': t,
150
+ 'sample_id': s
151
+ })
152
+ all_types.add(t)
153
+ all_samples.add(s)
154
+
155
+ self.types = sorted(list(all_types))
156
+ self.samples = sorted(list(all_samples))
157
+ self.file_map = parsed_data
158
+
159
+ def _infer_structure(self, files):
160
+ # Simple Heuristic:
161
+ # 1. Look at directory structure.
162
+ # Case A: root/type/sample.png
163
+ # Case B: root/sample/type.png
164
+
165
+ parsed_data = []
166
+ all_types = set()
167
+ all_samples = set()
168
+
169
+ # Naive approach: assume parent folder is one dimension, filename is another
170
+ for f in files:
171
+ p = Path(f)
172
+ parent = p.parent.name
173
+ stem = p.stem
174
+
175
+ # Heuristic: If parent is empty (root), use filename parts
176
+ if str(p.parent) == '.':
177
+ # Try splitting by _ or -
178
+ parts = re.split(r'[_-]', stem)
179
+ if len(parts) > 1:
180
+ type_guess = parts[-1] # Suffix as type
181
+ sample_guess = "_".join(parts[:-1])
182
+ else:
183
+ type_guess = "default"
184
+ sample_guess = stem
185
+ else:
186
+ # Check if parent looks like a type (fewer unique values) or sample (more unique values)
187
+ # For now, let's just store both and let user swap if needed?
188
+ # Let's default to: Parent = Group/Type, Filename = Sample (Common in datasets)
189
+ # But in paper writing, often Sample/Type.png is common too.
190
+
191
+ # We will treat the folder name as "Category A" and filename as "Category B"
192
+ # We'll decide which is 'Type' based on which set is smaller.
193
+ type_guess = parent
194
+ sample_guess = stem
195
+
196
+ parsed_data.append({
197
+ 'rel_path': f,
198
+ 'full_path': os.path.join(self.temp_dir, f),
199
+ 'dim1': type_guess,
200
+ 'dim2': sample_guess
201
+ })
202
+ all_types.add(type_guess)
203
+ all_samples.add(sample_guess)
204
+
205
+ # Decide which dimension is 'Type' (Columns) and which is 'Sample' (Rows)
206
+ # Usually Types are fewer than Samples.
207
+ if len(all_types) <= len(all_samples):
208
+ self.types = sorted(list(all_types))
209
+ self.samples = sorted(list(all_samples))
210
+ for item in parsed_data:
211
+ item['type'] = item['dim1']
212
+ item['sample_id'] = item['dim2']
213
+ else:
214
+ self.types = sorted(list(all_samples))
215
+ self.samples = sorted(list(all_types))
216
+ for item in parsed_data:
217
+ item['type'] = item['dim2']
218
+ item['sample_id'] = item['dim1']
219
+
220
+ self.file_map = parsed_data
221
+
222
+ def get_summary(self):
223
+ return pd.DataFrame(self.file_map)[['type', 'sample_id', 'rel_path']]
224
+
225
+ def get_sample_images(self, sample_id):
226
+ # Return a dict of {type: image_path} for a given sample
227
+ result = {}
228
+ for item in self.file_map:
229
+ if item['sample_id'] == sample_id:
230
+ result[item['type']] = item['full_path']
231
+ return result
232
+
233
+ def process_all(self, rules):
234
+ # rules: dict of {type: rule_config}
235
+ output_zip = tempfile.mktemp(suffix='.zip')
236
+ output_dir = tempfile.mkdtemp()
237
+
238
+ for item in self.file_map:
239
+ img_type = item['type']
240
+ # Default to None rule if not specified
241
+ rule = rules.get(img_type, {'action': 'None'})
242
+
243
+ try:
244
+ img = Image.open(item['full_path'])
245
+
246
+ # Apply Rules
247
+ img = apply_image_rule(img, rule)
248
+
249
+ # Save
250
+ # Structure: Output/Type/Sample.png
251
+ save_dir = os.path.join(output_dir, img_type)
252
+ os.makedirs(save_dir, exist_ok=True)
253
+
254
+ # Use original filename or sample_id?
255
+ # Using sample_id ensures consistency
256
+ save_path = os.path.join(save_dir, f"{item['sample_id']}.png")
257
+ img.save(save_path)
258
+ except Exception as e:
259
+ print(f"Error processing {item['full_path']}: {e}")
260
+
261
+ shutil.make_archive(output_zip.replace('.zip', ''), 'zip', output_dir)
262
+ return output_zip
263
+
264
+ def apply_image_rule(img, rule):
265
+ # rule: {'action': str, 'params': dict}
266
+ action = rule.get('action', 'None')
267
+ params = rule.get('params', {})
268
+
269
+ if action == 'Auto Trim':
270
+ threshold = params.get('threshold', 50)
271
+ bg = Image.new(img.mode, img.size, img.getpixel((0,0)))
272
+ diff = ImageChops.difference(img, bg)
273
+ diff = ImageOps.grayscale(diff)
274
+ # Threshold
275
+ diff = diff.point(lambda x: 255 if x > threshold else 0)
276
+ bbox = diff.getbbox()
277
+ if bbox:
278
+ img = img.crop(bbox)
279
+
280
+ elif action == 'Manual Crop':
281
+ x = int(params.get('x', 0))
282
+ y = int(params.get('y', 0))
283
+ w = int(params.get('w', 100))
284
+ h = int(params.get('h', 100))
285
+ # Ensure crop is within bounds
286
+ img_w, img_h = img.size
287
+ x = max(0, min(x, img_w))
288
+ y = max(0, min(y, img_h))
289
+ w = max(1, min(w, img_w - x))
290
+ h = max(1, min(h, img_h - y))
291
+ img = img.crop((x, y, x+w, y+h))
292
+
293
+ elif action == 'Resize':
294
+ w = int(params.get('w', 512))
295
+ h = int(params.get('h', 512))
296
+ img = img.resize((w, h))
297
+
298
+ return img
299
+
300
+ # Global State
301
+ matrix = ImageMatrix()
302
+ # Store rules globally for this session (Not multi-user safe, but fits current architecture)
303
+ global_rules = {}
304
+
305
+ def handle_upload(file, use_llm_chk):
306
+ if file is None:
307
+ return None, gr.update(choices=[])
308
+
309
+ df = matrix.load_zip(file.name, use_llm=use_llm_chk)
310
+ types = matrix.types
311
+ samples = matrix.samples
312
+
313
+ # Reset rules
314
+ global_rules.clear()
315
+
316
+ summary_text = f"Found {len(samples)} samples and {len(types)} types.\nTypes: {', '.join(types)}"
317
+
318
+ return df, gr.update(choices=samples, value=samples[0] if samples else None), gr.update(choices=types, value=types[0] if types else None), summary_text
319
+
320
+ def save_rule(type_sel, action, p1, p2, p3, p4):
321
+ if not type_sel:
322
+ return "No type selected."
323
+
324
+ params = {}
325
+ if action == 'Manual Crop':
326
+ params = {'x': p1, 'y': p2, 'w': p3, 'h': p4}
327
+ elif action == 'Resize':
328
+ params = {'w': p1, 'h': p2}
329
+ elif action == 'Auto Trim':
330
+ params = {'threshold': p1}
331
+
332
+ rule = {'action': action, 'params': params}
333
+ global_rules[type_sel] = rule
334
+ return f"Saved rule for {type_sel}: {action}"
335
+
336
+ def update_preview(sample_id, type_sel, action, p1, p2, p3, p4):
337
+ # p1-p4 are generic params mapped based on action
338
+ if not sample_id:
339
+ return None
340
+
341
+ images = matrix.get_sample_images(sample_id)
342
+
343
+ # Construct rule for preview
344
+ params = {}
345
+ if action == 'Manual Crop':
346
+ params = {'x': p1, 'y': p2, 'w': p3, 'h': p4}
347
+ elif action == 'Resize':
348
+ params = {'w': p1, 'h': p2}
349
+ elif action == 'Auto Trim':
350
+ params = {'threshold': p1}
351
+
352
+ rule = {'action': action, 'params': params}
353
+
354
+ results = []
355
+
356
+ # Show the selected type first/highlighted?
357
+ # For now just show the selected type processed
358
+ if type_sel in images:
359
+ path = images[type_sel]
360
+ orig_img = Image.open(path)
361
+ proc_img = apply_image_rule(orig_img.copy(), rule)
362
+ results.append((orig_img, f"{type_sel} (Original)"))
363
+ results.append((proc_img, f"{type_sel} (Processed)"))
364
+
365
+ return results
366
+
367
+ def run_batch_process():
368
+ if not matrix.file_map:
369
+ return None
370
+ return matrix.process_all(global_rules)
371
+
372
+ def generate_code_prompt(df_json, user_req):
373
+ # Fallback for complex needs
374
+ prompt = f"""
375
+ I have a directory of images with the following structure (sample):
376
+ {str(df_json)[:1000]}...
377
+
378
+ My goal is: {user_req}
379
+
380
+ Please write a Python script using Pillow and os/shutil to process these files.
381
+ """
382
+ return prompt
383
+
384
+ def create_paper_tool():
385
+ # Note: This function is called inside a gr.Blocks context in app.py
386
+ # So we don't need to create a new gr.Blocks() here unless we want a nested one.
387
+ # To keep it clean and consistent with other tools, we'll just define the layout.
388
+ if True: # Placeholder to keep indentation
389
+ gr.Markdown("## 📑 Paper Image Assistant (科研配图助手)")
390
+ gr.Markdown("Upload a zip of your images. The tool will try to organize them by Type and Sample ID.")
391
+
392
+ with gr.Row():
393
+ with gr.Column(scale=1):
394
+ zip_input = gr.File(label="Upload Zip", file_types=['.zip'])
395
+ use_llm_chk = gr.Checkbox(label="Use LLM for Structure Inference (DeepSeek)", value=False)
396
+ analyze_btn = gr.Button("Analyze Structure")
397
+ structure_info = gr.Markdown("No data loaded.")
398
+
399
+ with gr.Column(scale=2):
400
+ file_table = gr.Dataframe(label="Detected Structure", headers=['type', 'sample_id', 'rel_path'], interactive=False)
401
+
402
+ gr.Markdown("### 🛠️ Rule Configuration & Preview")
403
+
404
+ with gr.Row():
405
+ with gr.Column():
406
+ sample_selector = gr.Dropdown(label="Preview Sample ID", choices=[])
407
+ type_selector = gr.Dropdown(label="Configure Type", choices=[])
408
+
409
+ action_selector = gr.Dropdown(label="Action", choices=["None", "Manual Crop", "Resize", "Auto Trim"], value="None")
410
+
411
+ # Dynamic inputs
412
+ with gr.Group():
413
+ p1 = gr.Number(label="Param 1 (X / Width / Threshold)", value=0)
414
+ p2 = gr.Number(label="Param 2 (Y / Height)", value=0)
415
+ p3 = gr.Number(label="Param 3 (W)", value=100)
416
+ p4 = gr.Number(label="Param 4 (H)", value=100)
417
+
418
+ with gr.Row():
419
+ preview_btn = gr.Button("Preview Effect")
420
+ save_rule_btn = gr.Button("Save Rule for Type")
421
+
422
+ rule_status = gr.Markdown("")
423
+
424
+ with gr.Column():
425
+ preview_gallery = gr.Gallery(label="Preview", columns=2)
426
+
427
+ gr.Markdown("### 🚀 Batch Process")
428
+ process_btn = gr.Button("Apply Rules & Download", variant="primary")
429
+ download_output = gr.File(label="Download Result")
430
+
431
+ # Event Wiring
432
+ analyze_btn.click(handle_upload, inputs=[zip_input, use_llm_chk], outputs=[file_table, sample_selector, type_selector, structure_info])
433
+
434
+ preview_btn.click(update_preview,
435
+ inputs=[sample_selector, type_selector, action_selector, p1, p2, p3, p4],
436
+ outputs=[preview_gallery])
437
+
438
+ save_rule_btn.click(save_rule,
439
+ inputs=[type_selector, action_selector, p1, p2, p3, p4],
440
+ outputs=[rule_status])
441
+
442
+ process_btn.click(run_batch_process, inputs=[], outputs=[download_output])
443
+
444
+ with gr.Accordion("💻 Code Gen Helper (Fallback)", open=False):
445
+ user_req = gr.Textbox(label="Describe your requirement")
446
+ gen_prompt_btn = gr.Button("Generate Prompt")
447
+ prompt_output = gr.Code(label="Copy this prompt to an LLM", language="markdown")
448
+
449
+ gen_prompt_btn.click(generate_code_prompt, inputs=[file_table, user_req], outputs=[prompt_output])
450
+
451
+ pass
452
+
453
+ if __name__ == "__main__":
454
+ with gr.Blocks() as demo:
455
+ create_paper_tool()
456
+ demo.launch()
apps/text_tools.py CHANGED
@@ -290,19 +290,19 @@ def create_ui():
290
 
291
  # Group 2 (Hidden by default)
292
  with gr.Row(visible=False) as group_2:
293
- img_c_2 = gr.Number(value=1, label="图片数量 (Group 2)", precision=0)
294
  img_w_2 = gr.Number(value=1024, label="宽 (px)")
295
  img_h_2 = gr.Number(value=1024, label="高 (px)")
296
 
297
  # Group 3 (Hidden by default)
298
  with gr.Row(visible=False) as group_3:
299
- img_c_3 = gr.Number(value=1, label="图片数量 (Group 3)", precision=0)
300
  img_w_3 = gr.Number(value=1024, label="宽 (px)")
301
  img_h_3 = gr.Number(value=1024, label="高 (px)")
302
 
303
  # Group 4 (Hidden by default)
304
  with gr.Row(visible=False) as group_4:
305
- img_c_4 = gr.Number(value=1, label="图片数量 (Group 4)", precision=0)
306
  img_w_4 = gr.Number(value=1024, label="宽 (px)")
307
  img_h_4 = gr.Number(value=1024, label="高 (px)")
308
 
@@ -312,13 +312,25 @@ def create_ui():
312
  visible_groups = gr.State(1)
313
 
314
  def add_group(curr_count):
315
- next_count = curr_count + 1
316
- # Return updates for group_2, group_3, group_4
 
 
 
 
 
 
 
 
 
 
 
 
317
  return (
318
  next_count,
319
- gr.update(visible=next_count >= 2),
320
- gr.update(visible=next_count >= 3),
321
- gr.update(visible=next_count >= 4)
322
  )
323
 
324
  add_group_btn.click(
 
290
 
291
  # Group 2 (Hidden by default)
292
  with gr.Row(visible=False) as group_2:
293
+ img_c_2 = gr.Number(value=0, label="图片数量 (Group 2)", precision=0)
294
  img_w_2 = gr.Number(value=1024, label="宽 (px)")
295
  img_h_2 = gr.Number(value=1024, label="高 (px)")
296
 
297
  # Group 3 (Hidden by default)
298
  with gr.Row(visible=False) as group_3:
299
+ img_c_3 = gr.Number(value=0, label="图片数量 (Group 3)", precision=0)
300
  img_w_3 = gr.Number(value=1024, label="宽 (px)")
301
  img_h_3 = gr.Number(value=1024, label="高 (px)")
302
 
303
  # Group 4 (Hidden by default)
304
  with gr.Row(visible=False) as group_4:
305
+ img_c_4 = gr.Number(value=0, label="图片数量 (Group 4)", precision=0)
306
  img_w_4 = gr.Number(value=1024, label="宽 (px)")
307
  img_h_4 = gr.Number(value=1024, label="高 (px)")
308
 
 
312
  visible_groups = gr.State(1)
313
 
314
  def add_group(curr_count):
315
+ next_count = min(curr_count + 1, 4)
316
+
317
+ # Helper to create update for a group
318
+ def get_update(group_idx):
319
+ if next_count == group_idx:
320
+ # Just revealed, set count to 1
321
+ return gr.update(visible=True, value=1)
322
+ elif next_count > group_idx:
323
+ # Already visible, keep as is (don't reset value)
324
+ return gr.update(visible=True)
325
+ else:
326
+ # Still hidden
327
+ return gr.update(visible=False)
328
+
329
  return (
330
  next_count,
331
+ get_update(2),
332
+ get_update(3),
333
+ get_update(4)
334
  )
335
 
336
  add_group_btn.click(
requirements.txt CHANGED
@@ -4,4 +4,6 @@ img2pdf
4
  huggingface_hub
5
  transformers
6
  tiktoken
7
- qwen-vl-utils
 
 
 
4
  huggingface_hub
5
  transformers
6
  tiktoken
7
+ qwen-vl-utils
8
+ pandas
9
+ requests