luigi12345 commited on
Commit
1f2f701
1 Parent(s): 06b73eb

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +225 -376
app.py CHANGED
@@ -60,6 +60,10 @@ class TempImageFile:
60
  try:
61
  if self.path and os.path.exists(self.path):
62
  os.remove(self.path)
 
 
 
 
63
  except Exception as e:
64
  print(f"Error cleaning up temporary file: {e}")
65
  finally:
@@ -160,130 +164,15 @@ MEASUREMENT_RANGES = {
160
  "astigmatism": (0.0, 6.0) # D
161
  }
162
 
163
- def get_case_specific_settings(measurements):
164
- """Determine appropriate settings based on measurements"""
165
- settings = DEFAULT_SETTINGS.copy()
166
-
167
- # Extract measurements (implement measurement extraction logic)
168
- axial_length = measurements.get('axial_length', 23.5)
169
- astigmatism = measurements.get('astigmatism', 0.0)
170
-
171
- # Adjust settings based on measurements
172
- if axial_length < 22.0:
173
- settings.update(LENS_MODELS['short_eye'])
174
- elif astigmatism > 2.0:
175
- settings.update(LENS_MODELS['toric'])
176
-
177
- return settings
178
-
179
- DESCRIPTION = """
180
- # IOL Report Analyzer
181
- Upload an IOL screenshot to get a detailed analysis of measurements and recommendations.
182
-
183
- ## Instructions:
184
- 1. Upload your IOL report image
185
- 2. Click Submit to generate analysis
186
- 3. Review the detailed output below
187
- """
188
-
189
- SYSTEM_PROMPT = """Please analyze the uploaded IOL report image carefully and provide a detailed analysis using these settings:
190
- Manufacturer: {manufacturer}
191
- Lens Model: {lens_model}
192
- A-Constant: {a_constant}
193
- Target Refraction: {target_refraction} D
194
- Surgeon Factor: {surgeon_factor}
195
- Personalization Factor: {personalization_factor}
196
- AC Constant: {anterior_chamber_constant}
197
- Vertex Distance: {vertex_distance} mm
198
-
199
- Please provide analysis in the following format:
200
-
201
- 1. BIOMETRY MEASUREMENTS:
202
- - Axial Length (AL): [value] mm
203
- - Keratometry: K1: [value] D @ [axis]°, K2: [value] D @ [axis]°
204
- - Mean K: [value] D
205
- - Astigmatism: [value] D
206
- - Anterior Chamber Depth (ACD): [value] mm
207
- - Lens Thickness (LT): [value] mm
208
- - White-to-White Distance (WTW): [value] mm
209
- - Central Corneal Thickness (CCT): [value] μm
210
-
211
- 2. IOL POWER CALCULATIONS:
212
- A. Barrett Universal II Formula:
213
- - Recommended IOL Power: [value] D
214
- - Predicted Refraction: [value] D
215
- - Target Refraction: [value] D
216
-
217
- B. Other Formula Calculations:
218
- - Cooke K6: [value] D
219
- - EVO Formula: [value] D
220
- - Hill-RBF Method: [value] D
221
- - Hoffer QST: [value] D
222
- - Kane Formula: [value] D
223
- - PEARL-DGS: [value] D
224
-
225
- 3. ANALYSIS:
226
- - Optimal IOL Power Range: [range] D
227
- - Recommended A-Constant: [value]
228
- - Risk Factors: [list any concerns]
229
-
230
- 4. RECOMMENDATIONS:
231
- - Primary IOL Choice: [power] D ([manufacturer/model])
232
- - Alternative Options: [list alternatives]
233
- - Special Considerations: [note any relevant factors]
234
-
235
- Please ensure all measurements are accurately extracted and calculations are precise. If any values are missing or unclear, please indicate this in your response."""
236
-
237
- css = """
238
- #component-0 {
239
- max-width: 1200px;
240
- margin: auto;
241
- padding: 20px;
242
- }
243
-
244
- .container {
245
- border: 1px solid #e0e0e0;
246
- border-radius: 10px;
247
- padding: 20px;
248
- margin-bottom: 20px;
249
- background: white;
250
- }
251
-
252
- .output-box {
253
- height: 600px;
254
- overflow: auto;
255
- font-family: 'Source Code Pro', monospace;
256
- line-height: 1.5;
257
- padding: 15px;
258
- background: #f8f9fa;
259
- border-radius: 5px;
260
  }
261
 
262
- .title {
263
- font-size: 24px;
264
- font-weight: bold;
265
- margin-bottom: 20px;
266
- color: #2c3e50;
267
- }
268
-
269
- .submit-btn {
270
- background: #2ecc71 !important;
271
- border: none !important;
272
- }
273
-
274
- .submit-btn:hover {
275
- background: #27ae60 !important;
276
- }
277
- """
278
-
279
  def format_error(error_type, message):
280
- error_messages = {
281
- "ValueError": "⚠️ Invalid Input",
282
- "RuntimeError": "🔧 Processing Error",
283
- "torch.cuda.OutOfMemoryError": "💾 Memory Error",
284
- "Exception": "❌ Unexpected Error"
285
- }
286
- return f"{error_messages.get(error_type.__name__, '❌ Error')}: {message}"
287
 
288
  def validate_image(image_array):
289
  if image_array is None:
@@ -337,16 +226,6 @@ def normalize_image_size(image, max_size=1024):
337
  image = image.resize(new_size, Image.LANCZOS)
338
  return image
339
 
340
- def validate_examples():
341
- example_files = ["example1.png", "example2.png"]
342
- valid_examples = []
343
-
344
- for file in example_files:
345
- if os.path.exists(file):
346
- valid_examples.append([file])
347
-
348
- return valid_examples if valid_examples else None
349
-
350
  def extract_measurements(image_array):
351
  """
352
  Extract measurements from the image using OCR or vision model
@@ -466,280 +345,250 @@ EXTRACTED MEASUREMENTS:
466
  """
467
 
468
  def generate_analysis_prompt(measurements, settings, warnings=None, risk_factors=None, quality_score=None):
469
- """Enhanced prompt generation with quality indicators"""
470
- quality_warning = ""
471
- if quality_score is not None:
472
- if quality_score < 0.5:
473
- quality_warning = "\n⚠️ WARNING: Low image quality may affect measurement accuracy"
474
-
475
- risk_text = ""
476
- if risk_factors:
477
- risk_text = "\nRISK FACTORS:\n" + "\n".join(f"• {r}" for r in risk_factors)
478
-
479
- settings_text = f"""
480
- ANALYSIS SETTINGS:
481
- ╔═════════════════��══════╦═══════════╗
482
- Setting ║ Value ║
483
- ╠════════════════════════╬═══════════╣
484
- Manufacturer ║ {settings['manufacturer']}
485
- ║ Lens Model ║ {settings['lens_model']} ║
486
- ║ A-Constant ║ {settings['a_constant']} ║
487
- Target Refraction ║ {settings['target_refraction']} D ║
488
- Surgeon Factor ║ {settings['surgeon_factor']} ║
489
- Personalization ║ {settings['personalization_factor']}
490
- AC Constant ║ {settings['anterior_chamber_constant']} ║
491
- ║ Vertex Distance ║ {settings['vertex_distance']} mm ║
492
- ╚════════════════════════╩═══════════╝
493
- """
494
-
495
- measurements_text = format_measurements_text(measurements) if measurements else "\nNo measurements could be extracted."
496
-
497
- prompt = f"""As an expert ophthalmologist, please analyze this IOL report carefully.
498
-
499
- {settings_text}
500
- {measurements_text}
501
- {risk_text}
502
- {quality_warning}
503
-
504
- Please provide a detailed analysis following this structure:
505
-
506
- 1. MEASUREMENTS VALIDATION
507
- - Verify all measurements are within normal ranges
508
- - Flag any concerning values
509
-
510
- 2. IOL POWER CALCULATIONS
511
- - Primary Formula Recommendation
512
- - Cross-validation with other formulas
513
- - Optimal power range
514
-
515
- 3. RISK ASSESSMENT
516
- - Identify potential complications
517
- - Special considerations
518
-
519
- 4. RECOMMENDATIONS
520
- - Specific IOL choice and power
521
- - Alternative options
522
- - Surgical approach suggestions
523
-
524
- Please be thorough and highlight any concerns or special considerations."""
525
-
526
  return prompt
527
 
528
  def validate_settings(settings):
 
529
  if not isinstance(settings, dict):
530
- raise ValueError("Settings must be a dictionary")
531
 
532
- required_fields = {
533
- 'manufacturer': str,
534
- 'lens_model': str,
535
- 'a_constant': (int, float),
536
- 'target_refraction': (int, float),
537
- 'surgeon_factor': (int, float),
538
- 'personalization_factor': (int, float),
539
- 'anterior_chamber_constant': (int, float),
540
- 'vertex_distance': (int, float)
541
- }
 
 
 
 
 
542
 
543
- for field, expected_type in required_fields.items():
544
- if field not in settings:
545
- raise ValueError(f"Missing required setting: {field}")
546
- if not isinstance(settings[field], expected_type):
547
- raise ValueError(f"Invalid type for {field}: expected {expected_type}, got {type(settings[field])}")
548
-
549
- # Validate numeric ranges
550
- if isinstance(settings[field], (int, float)):
551
- if field == 'a_constant' and not 115 <= settings[field] <= 122:
552
- raise ValueError("A-constant must be between 115 and 122")
553
- elif field == 'target_refraction' and not -10 <= settings[field] <= 10:
554
- raise ValueError("Target refraction must be between -10 and 10")
555
-
556
- @spaces.GPU(duration=120)
557
  def run_example(image, model_id="Qwen/Qwen2-VL-7B-Instruct", settings=None, progress=gr.Progress()):
 
 
 
 
558
  try:
559
- if settings:
560
- validate_settings(settings)
561
-
562
- # Enhanced validation flow with quality feedback
563
- validate_image(image)
564
- quality_score, quality_feedback = validate_image_quality(image)
565
-
566
- if quality_score < 0.3:
567
- return f"⚠️ Image quality too low for reliable analysis: {quality_feedback}"
568
-
569
- progress(0.2, "Extracting measurements...")
570
- measurements = extract_measurements(image)
571
-
572
- # Add error handling for measurement extraction
573
- if not measurements:
574
- return "❌ Failed to extract measurements from image. Please ensure the image contains clear IOL measurements."
575
 
576
- warnings, risk_factors = validate_measurements(measurements)
 
577
 
578
- if not settings:
579
- settings = get_case_specific_settings(measurements)
580
-
581
- analysis_prompt = generate_analysis_prompt(
582
- measurements,
583
- settings,
584
- warnings=warnings,
585
- risk_factors=risk_factors,
586
- quality_score=quality_score
587
- )
588
-
589
- progress(0.4, "Loading model...")
590
- model = model_manager.get_model(model_id)
591
- processor = model_manager.get_processor(model_id)
592
 
593
- progress(0.6, "Processing image...")
594
- # Save temporary image for model processing
595
- with TempImageFile(image) as temp_path:
596
- # Process image and generate response
597
- inputs = processor(text=analysis_prompt, images=temp_path, return_tensors="pt")
598
- inputs = {k: v.to(model.device) for k, v in inputs.items()}
 
 
 
 
 
599
 
600
- progress(0.8, "Generating analysis...")
601
- with torch.no_grad():
602
- output = model.generate(
603
- **inputs,
604
- max_new_tokens=1000,
605
- do_sample=True,
606
- temperature=0.7,
607
- top_p=0.9,
 
 
 
 
608
  )
 
 
609
 
610
- response = processor.decode(output[0], skip_special_tokens=True)
611
-
612
- # Cleanup
613
- model_manager.cleanup_unused_models(keep_model_id=model_id)
614
- cleanup_temp_files()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
615
 
616
- return response
617
-
618
- except torch.cuda.OutOfMemoryError:
619
- return "💾 GPU memory exceeded. Please try with a smaller image or wait a moment."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
620
  except Exception as e:
621
- return format_error(type(e), str(e))
 
 
 
 
 
 
 
 
 
 
622
 
623
  with gr.Blocks(css=css, theme=gr.themes.Soft()) as demo:
624
  gr.Markdown(DESCRIPTION)
625
 
626
  with gr.Row():
627
- with gr.Column(scale=1):
628
- with gr.Group(elem_classes="container"):
629
- gr.Markdown("### Upload Image")
630
- input_img = gr.Image(
631
- label="IOL Report Image",
632
- type="numpy",
633
- elem_classes="input-image"
 
 
 
 
 
 
634
  )
635
-
636
- # Add collapsible advanced settings
637
- with gr.Accordion("Advanced Settings", open=False):
638
- manufacturer = gr.Dropdown(
639
- choices=["Alcon", "Johnson & Johnson", "Zeiss", "Bausch & Lomb"],
640
- value=DEFAULT_SETTINGS["manufacturer"],
641
- label="Manufacturer"
642
- )
643
- lens_model = gr.Textbox(
644
- value=DEFAULT_SETTINGS["lens_model"],
645
- label="Lens Model"
646
- )
647
- a_constant = gr.Number(
648
- value=DEFAULT_SETTINGS["a_constant"],
649
- label="A-Constant"
650
- )
651
- target_refraction = gr.Number(
652
- value=DEFAULT_SETTINGS["target_refraction"],
653
- label="Target Refraction (D)"
654
- )
655
- with gr.Row():
656
- surgeon_factor = gr.Number(
657
- value=DEFAULT_SETTINGS["surgeon_factor"],
658
- label="Surgeon Factor"
659
- )
660
- personalization_factor = gr.Number(
661
- value=DEFAULT_SETTINGS["personalization_factor"],
662
- label="Personalization Factor"
663
- )
664
- with gr.Row():
665
- ac_constant = gr.Number(
666
- value=DEFAULT_SETTINGS["anterior_chamber_constant"],
667
- label="AC Constant"
668
- )
669
- vertex_distance = gr.Number(
670
- value=DEFAULT_SETTINGS["vertex_distance"],
671
- label="Vertex Distance (mm)"
672
- )
673
-
674
- model_selector = gr.Dropdown(
675
- choices=model_manager.available_models,
676
- value="Qwen/Qwen2-VL-7B-Instruct",
677
- label="Model Selection",
678
- interactive=True
679
  )
680
- submit_btn = gr.Button(
681
- "📊 Analyze Report",
682
- variant="primary",
683
- elem_classes="submit-btn"
684
  )
685
-
686
- with gr.Column(scale=1):
687
- with gr.Group(elem_classes="container"):
688
- gr.Markdown("### Analysis Results")
689
- output_text = gr.Textbox(
690
- label="IOL Analysis",
691
- lines=25,
692
- show_copy_button=True,
693
- elem_classes="output-box"
694
  )
695
-
696
- # Add example images if available
697
- examples = validate_examples()
698
- if examples:
699
- gr.Examples(
700
- examples=examples,
701
- inputs=input_img,
702
- outputs=output_text,
703
- fn=run_example,
704
- cache_examples=True,
705
- )
706
-
707
- # Event handler
708
  submit_btn.click(
709
  fn=run_example,
710
  inputs=[
711
  input_img,
712
- model_selector,
713
- gr.State(lambda: {
714
- "manufacturer": manufacturer.value,
715
- "lens_model": lens_model.value,
716
- "a_constant": a_constant.value,
717
- "target_refraction": target_refraction.value,
718
- "surgeon_factor": surgeon_factor.value,
719
- "personalization_factor": personalization_factor.value,
720
- "anterior_chamber_constant": ac_constant.value,
721
- "vertex_distance": vertex_distance.value
722
- })
723
  ],
724
- outputs=output_text,
725
- api_name="analyze"
726
  )
727
 
728
- # Modify the queue configuration
729
- demo.queue(
730
- max_size=1,
731
- api_open=False,
732
- concurrency_count=1, # Limit concurrent executions
733
- status_update_rate=0.1, # More frequent updates
734
- timeout=600, # 10 minute timeout
735
- default_concurrency_limit=1
736
- )
737
-
738
- # Launch with queue
739
- demo.launch(
740
- debug=True,
741
- show_error=True,
742
- share=False,
743
- server_name="0.0.0.0",
744
- server_port=7860,
745
- )
 
60
  try:
61
  if self.path and os.path.exists(self.path):
62
  os.remove(self.path)
63
+ # Also remove parent directory if empty
64
+ parent_dir = os.path.dirname(self.path)
65
+ if os.path.exists(parent_dir) and not os.listdir(parent_dir):
66
+ os.rmdir(parent_dir)
67
  except Exception as e:
68
  print(f"Error cleaning up temporary file: {e}")
69
  finally:
 
164
  "astigmatism": (0.0, 6.0) # D
165
  }
166
 
167
+ ERROR_MESSAGES = {
168
+ "ValueError": "⚠️ Invalid Input",
169
+ "RuntimeError": "🔧 Processing Error",
170
+ "torch.cuda.OutOfMemoryError": "💾 Memory Error",
171
+ "Exception": "❌ Unexpected Error"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
  }
173
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
  def format_error(error_type, message):
175
+ return f"{ERROR_MESSAGES.get(error_type.__name__, '❌ Error')}: {message}"
 
 
 
 
 
 
176
 
177
  def validate_image(image_array):
178
  if image_array is None:
 
226
  image = image.resize(new_size, Image.LANCZOS)
227
  return image
228
 
 
 
 
 
 
 
 
 
 
 
229
  def extract_measurements(image_array):
230
  """
231
  Extract measurements from the image using OCR or vision model
 
345
  """
346
 
347
  def generate_analysis_prompt(measurements, settings, warnings=None, risk_factors=None, quality_score=None):
348
+ """Enhanced prompt generation for IOL Master and similar biometry reports"""
349
+
350
+ # Detect case type
351
+ case_types = detect_case_type(measurements)
352
+
353
+ # Build comprehensive analysis request
354
+ prompt = f"""Analyze this IOL biometry report image.
355
+
356
+ TASK:
357
+ 1. Verify all visible measurements
358
+ 2. Confirm measurement accuracy and consistency
359
+ 3. Provide IOL power calculation using:
360
+ - SRK/T formula
361
+ - Barrett Universal II
362
+ - Hill-RBF if applicable
363
+ - Other formulas as appropriate for this case
364
+
365
+ CONSIDERATIONS:
366
+ 1. Image Quality Assessment
367
+ 2. Measurement Validation
368
+ 3. Formula Selection Rationale
369
+ 4. Risk Factor Analysis
370
+
371
+ {format_measurements_text(measurements)}
372
+
373
+ SELECTED IOL PARAMETERS:
374
+ Manufacturer: {settings.get('manufacturer')}
375
+ • Model: {settings.get('lens_model')}
376
+ A-Constant: {settings.get('a_constant')}
377
+ • Target: {settings.get('target_refraction', 0)} D
378
+
379
+ Please provide:
380
+ 1. Measurement verification
381
+ 2. IOL power calculations with multiple formulas
382
+ 3. Specific recommendations for:
383
+ - Optimal IOL power
384
+ - Formula selection rationale
385
+ - Surgical approach
386
+ 4. Risk assessment and special considerations
387
+ 5. Alternative options if needed"""
388
+
389
+ # Add case-specific guidance if detected
390
+ if case_types:
391
+ prompt += "\n\nSpecial Considerations:"
392
+ for _, description in case_types:
393
+ prompt += f"\n• {description}"
394
+
 
 
 
 
 
 
 
 
 
 
395
  return prompt
396
 
397
  def validate_settings(settings):
398
+ """Enhanced settings validation with better error handling"""
399
  if not isinstance(settings, dict):
400
+ return DEFAULT_SETTINGS # Fallback to defaults instead of raising error
401
 
402
+ validated_settings = DEFAULT_SETTINGS.copy()
403
+ for key, value in settings.items():
404
+ if key in validated_settings:
405
+ try:
406
+ # Type conversion and validation
407
+ if isinstance(DEFAULT_SETTINGS[key], (int, float)):
408
+ value = float(value)
409
+ # Validate ranges
410
+ if key == 'a_constant' and not 115 <= value <= 122:
411
+ continue # Skip invalid value, keep default
412
+ if key == 'target_refraction' and not -10 <= value <= 10:
413
+ continue # Skip invalid value, keep default
414
+ validated_settings[key] = value
415
+ except (ValueError, TypeError):
416
+ continue # Keep default value if conversion fails
417
 
418
+ return validated_settings
419
+
420
+ @spaces.GPU
 
 
 
 
 
 
 
 
 
 
 
421
  def run_example(image, model_id="Qwen/Qwen2-VL-7B-Instruct", settings=None, progress=gr.Progress()):
422
+ """Enhanced error handling and resource management"""
423
+ cleanup_temp_files()
424
+ model = None
425
+
426
  try:
427
+ progress(0, desc="Validating input...")
428
+ if image is None:
429
+ return "Error: No image provided"
 
 
 
 
 
 
 
 
 
 
 
 
 
430
 
431
+ image = validate_image(image)
432
+ settings = validate_settings(settings or {})
433
 
434
+ progress(0.2, desc="Loading model...")
435
+ try:
436
+ model = model_manager.get_model(model_id)
437
+ processor = model_manager.get_processor(model_id)
438
+ except Exception as e:
439
+ return f"Error loading model: {str(e)}"
 
 
 
 
 
 
 
 
440
 
441
+ progress(0.4, desc="Processing image...")
442
+ with TempImageFile(image) as image_path:
443
+ if not image_path:
444
+ return "Error: Failed to save temporary image"
445
+
446
+ # Convert and normalize image
447
+ try:
448
+ image = Image.fromarray(image).convert("RGB")
449
+ image = normalize_image_size(image)
450
+ except Exception as e:
451
+ return f"Error processing image: {str(e)}"
452
 
453
+ # Generate analysis prompt
454
+ try:
455
+ measurements = extract_measurements(image)
456
+ warnings, risk_factors = validate_measurements(measurements)
457
+ quality_score, quality_feedback = validate_image_quality(image)
458
+
459
+ prompt = generate_analysis_prompt(
460
+ measurements=measurements,
461
+ settings=settings,
462
+ warnings=warnings,
463
+ risk_factors=risk_factors,
464
+ quality_score=quality_score
465
  )
466
+ except Exception as e:
467
+ return f"Error analyzing image: {str(e)}"
468
 
469
+ # Prepare model inputs
470
+ try:
471
+ messages = [{
472
+ "role": "user",
473
+ "content": [
474
+ {"type": "image", "image": image_path},
475
+ {"type": "text", "text": prompt}
476
+ ]
477
+ }]
478
+
479
+ text = processor.apply_chat_template(
480
+ messages,
481
+ tokenize=False,
482
+ add_generation_prompt=True
483
+ )
484
+
485
+ image_inputs, video_inputs = process_vision_info(messages)
486
+ inputs = processor(
487
+ text=[text],
488
+ images=image_inputs,
489
+ videos=video_inputs,
490
+ padding=True,
491
+ return_tensors="pt"
492
+ ).to(model.device)
493
+ except Exception as e:
494
+ return f"Error preparing model inputs: {str(e)}"
495
 
496
+ # Model inference with proper resource management
497
+ try:
498
+ progress(0.6, desc="Generating analysis...")
499
+ with torch.inference_mode():
500
+ generated_ids = model.generate(
501
+ **inputs,
502
+ max_new_tokens=1024,
503
+ do_sample=True,
504
+ temperature=0.7,
505
+ top_p=0.9
506
+ )
507
+
508
+ generated_ids_trimmed = [
509
+ out_ids[len(in_ids):]
510
+ for in_ids, out_ids in zip(inputs.input_ids, generated_ids)
511
+ ]
512
+
513
+ output_text = processor.batch_decode(
514
+ generated_ids_trimmed,
515
+ skip_special_tokens=True,
516
+ clean_up_tokenization_spaces=False
517
+ )[0]
518
+
519
+ progress(1.0, desc="Complete!")
520
+ return output_text
521
+
522
+ except torch.cuda.OutOfMemoryError:
523
+ return "Error: GPU memory exhausted. Please try with a smaller image."
524
+ except Exception as e:
525
+ return f"Error during analysis: {str(e)}"
526
+
527
  except Exception as e:
528
+ return f"Unexpected error: {str(e)}"
529
+
530
+ finally:
531
+ # Ensure proper cleanup
532
+ try:
533
+ if model is not None:
534
+ model_manager.cleanup_unused_models(keep_model_id=model_id)
535
+ torch.cuda.empty_cache()
536
+ gc.collect()
537
+ except Exception as e:
538
+ print(f"Cleanup error: {e}")
539
 
540
  with gr.Blocks(css=css, theme=gr.themes.Soft()) as demo:
541
  gr.Markdown(DESCRIPTION)
542
 
543
  with gr.Row():
544
+ with gr.Column():
545
+ # Image upload
546
+ input_img = gr.Image(
547
+ label="IOL Report Image",
548
+ type="numpy"
549
+ )
550
+
551
+ # Essential settings only
552
+ with gr.Accordion("Settings", open=False):
553
+ manufacturer = gr.Dropdown(
554
+ choices=["Alcon", "Johnson & Johnson", "Zeiss", "Bausch & Lomb"],
555
+ value=DEFAULT_SETTINGS["manufacturer"],
556
+ label="Manufacturer"
557
  )
558
+ lens_model = gr.Textbox(
559
+ value=DEFAULT_SETTINGS["lens_model"],
560
+ label="Lens Model"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
561
  )
562
+ a_constant = gr.Number(
563
+ value=DEFAULT_SETTINGS["a_constant"],
564
+ label="A-Constant"
 
565
  )
566
+ target_refraction = gr.Number(
567
+ value=DEFAULT_SETTINGS["target_refraction"],
568
+ label="Target Refraction (D)"
 
 
 
 
 
 
569
  )
570
+
571
+ submit_btn = gr.Button("Analyze Report", variant="primary")
572
+
573
+ # Output column
574
+ with gr.Column():
575
+ output_text = gr.Textbox(
576
+ label="Analysis Results",
577
+ lines=20,
578
+ show_copy_button=True
579
+ )
580
+
581
+ # Simplified event handler
 
582
  submit_btn.click(
583
  fn=run_example,
584
  inputs=[
585
  input_img,
586
+ gr.State("Qwen/Qwen2-VL-7B-Instruct"),
587
+ gr.State(DEFAULT_SETTINGS)
 
 
 
 
 
 
 
 
 
588
  ],
589
+ outputs=output_text
 
590
  )
591
 
592
+ # Simplified launch configuration
593
+ demo.queue(concurrency_count=1)
594
+ demo.launch(server_name="0.0.0.0", server_port=7860)