Mr7Explorer commited on
Commit
d2789be
Β·
verified Β·
1 Parent(s): 3d56a8d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +299 -67
app.py CHANGED
@@ -183,92 +183,262 @@ def compute_loudness(y, sr):
183
  return None
184
 
185
  # ============================================================
186
- # UPDATED ISSUE DETECTION (HF thresholds corrected)
 
 
187
  # ============================================================
188
 
189
  def detect_audio_issues(spectral, time_stats):
190
- """Detect common audio processing artifacts"""
 
191
  issues = []
192
  energy = spectral["energy_distribution"]
 
 
 
 
 
193
 
194
- # ============================
195
- # UPDATED HF LOSS LOGIC
196
- # ============================
197
 
198
  hf_8_12 = energy["8k_12khz"]
199
  highest_freq = spectral["highest_freq_minus60db"]
200
 
201
- # 1️⃣ Severe HF cutoff (actual filtering / NR damage)
202
  if hf_8_12 < 0.01 and highest_freq < 9000:
203
  issues.append((
204
  "HF_LOSS", "HIGH",
205
- f"Possible HF cutoff: only {hf_8_12:.3f}% in 8–12kHz and rolloff at {highest_freq:.1f} Hz."
206
  ))
207
-
208
- # 2️⃣ Low HF energy β€” common in normal speech
209
  elif hf_8_12 < 0.02:
210
  issues.append((
211
  "HF_LOSS", "LOW",
212
- f"Low HF energy ({hf_8_12:.3f}% in 8–12kHz). Normal for speech."
213
  ))
214
 
215
- # ============================
216
- # High-pass filter check
217
- # ============================
218
- if energy["below_100hz"] < 0.5:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
219
  issues.append((
220
- "HIGH_PASS_FILTER",
221
- "HIGH",
222
- f"Very low energy <100Hz ({energy['below_100hz']:.2f}%). Possible HPF."
223
  ))
224
 
225
- # ============================
226
- # Brick-wall filter detection
227
- # ============================
 
228
  if spectral["brick_wall_detected"]:
229
  issues.append((
230
- "BRICK_WALL",
231
- "HIGH",
232
- f"Brick-wall behavior detected at {spectral['brick_wall_freq']:.0f} Hz."
233
  ))
234
 
235
- # ============================
236
- # Spectral notches
237
- # ============================
238
- if len(spectral["spectral_notches"]) > 0:
239
- issues.append((
240
- "SPECTRAL_NOTCHES",
241
- "MEDIUM",
242
- f"{len(spectral['spectral_notches'])} spectral notches found."
243
- ))
244
 
245
- # ============================
246
- # Compression / dynamics
247
- # ============================
248
- if time_stats["crest_factor_db"] < 3:
249
  issues.append((
250
- "OVER_COMPRESSION",
251
- "HIGH",
252
- f"Very low crest factor ({time_stats['crest_factor_db']:.1f} dB)."
253
  ))
254
- elif time_stats["crest_factor_db"] < 6:
255
  issues.append((
256
- "COMPRESSION",
257
- "MEDIUM",
258
- f"Low crest factor ({time_stats['crest_factor_db']:.1f} dB)."
259
  ))
260
 
261
- # ============================
262
- # Clipping
263
- # ============================
 
264
  if time_stats["peak"] >= 0.999:
265
  issues.append((
266
- "CLIPPING",
267
- "CRITICAL",
268
  f"Peak amplitude {time_stats['peak']:.6f}. Possible clipping."
269
  ))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
270
 
271
- # FINAL RETURN β€” MUST BE INDENTED INSIDE FUNCTION
272
  return issues
273
 
274
  # ============================================================
@@ -501,7 +671,7 @@ def create_report(audio_data, output_path):
501
 
502
 
503
  # ============================
504
- # ISSUES PANEL
505
  # ============================
506
 
507
  ax_issues = fig.add_subplot(gs[3, 0:3])
@@ -514,9 +684,12 @@ def create_report(audio_data, output_path):
514
  "═" * 80
515
  ]
516
 
 
517
  if not issues:
518
  issue_lines.append("βœ… No significant issues detected.")
 
519
  else:
 
520
  severity_icons = {
521
  "CRITICAL": "πŸ”΄ CRITICAL",
522
  "HIGH": "🟠 HIGH",
@@ -524,29 +697,48 @@ def create_report(audio_data, output_path):
524
  "LOW": "🟒 LOW"
525
  }
526
 
 
527
  for issue_type, severity, description in issues:
528
  icon = severity_icons.get(severity, "βšͺ INFO")
529
  issue_lines.append(f"\n{icon} β€” {issue_type}")
530
  issue_lines.append(f" β†’ {description}")
531
 
532
- # If spectral notches exist, list them
 
 
 
533
  if spec["spectral_notches"]:
534
- issue_lines.append(f"\n🎡 SPECTRAL NOTCHES DETECTED: {len(spec['spectral_notches'])}")
 
 
535
  for i, notch in enumerate(spec["spectral_notches"][:5], start=1):
536
  issue_lines.append(
537
- f" {i}. Frequency: {notch['freq']:.1f} Hz, Depth: {notch['depth_db']:.1f} dB"
538
  )
 
539
  if len(spec["spectral_notches"]) > 5:
540
- issue_lines.append(f" ... and {len(spec['spectral_notches']) - 5} more")
 
 
 
 
 
 
541
 
542
- # Brickwall detection notice
543
  if spec["brick_wall_detected"]:
544
- issue_lines.append(f"\n⚠️ BRICK-WALL FILTER: Detected at {spec['brick_wall_freq']:.0f} Hz")
 
 
 
 
 
 
545
 
546
  issues_text = "\n".join(issue_lines)
547
 
548
  ax_issues.text(
549
- 0.05, 0.95, issues_text,
 
550
  transform=ax_issues.transAxes,
551
  fontsize=11,
552
  verticalalignment="top",
@@ -558,8 +750,9 @@ def create_report(audio_data, output_path):
558
  linewidth=2
559
  )
560
  )
 
561
  # ============================
562
- # QUALITY SCORE PANEL
563
  # ============================
564
 
565
  ax_score = fig.add_subplot(gs[3, 3])
@@ -567,28 +760,59 @@ def create_report(audio_data, output_path):
567
 
568
  issues = audio_data["issues"]
569
 
570
- # Score penalties
571
  critical = sum(1 for _, sev, _ in issues if sev == "CRITICAL")
572
  high = sum(1 for _, sev, _ in issues if sev == "HIGH")
573
  medium = sum(1 for _, sev, _ in issues if sev == "MEDIUM")
 
574
 
 
 
 
575
  score = 100
576
- score -= critical * 30
577
- score -= high * 15
578
- score -= medium * 5
579
- score = max(0, score)
580
 
581
- # Grade + Color
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
582
  if score >= 90:
583
  grade, quality, color = "A", "EXCELLENT", "#00C853"
 
584
  elif score >= 75:
585
  grade, quality, color = "B", "GOOD", "#64DD17"
 
586
  elif score >= 60:
587
  grade, quality, color = "C", "FAIR", "#FFD600"
 
588
  elif score >= 40:
589
  grade, quality, color = "D", "POOR", "#FF6D00"
 
590
  else:
591
  grade, quality, color = "F", "CRITICAL", "#D50000"
 
 
 
 
 
 
 
592
 
593
  score_lines = [
594
  "QUALITY ASSESSMENT",
@@ -598,11 +822,21 @@ def create_report(audio_data, output_path):
598
  f"GRADE: {grade}",
599
  f"QUALITY: {quality}",
600
  "",
 
 
 
 
 
 
 
 
 
601
  "ISSUES SUMMARY",
602
  "─" * 28,
603
  f"πŸ”΄ Critical: {critical}",
604
  f"🟠 High: {high}",
605
  f"🟑 Medium: {medium}",
 
606
  "",
607
  "─" * 28,
608
  "Generated:",
@@ -628,10 +862,7 @@ def create_report(audio_data, output_path):
628
  fontweight="bold"
629
  )
630
 
631
- # ============================
632
  # SAVE REPORT
633
- # ============================
634
-
635
  plt.savefig(
636
  output_path,
637
  dpi=300,
@@ -642,6 +873,7 @@ def create_report(audio_data, output_path):
642
  plt.close()
643
 
644
  return output_path
 
645
  # ============================================================
646
  # MAIN ANALYSIS FUNCTION (GRADIO CALLBACK)
647
  # ============================================================
 
183
  return None
184
 
185
  # ============================================================
186
+ # ADVANCED ISSUE DETECTION ENGINE
187
+ # Includes: HF-loss logic, LPF detector, HPF detector,
188
+ # NR artifacts, spectral anomalies, compression, clipping
189
  # ============================================================
190
 
191
  def detect_audio_issues(spectral, time_stats):
192
+ """Detect audio processing artifacts with advanced forensic analysis."""
193
+
194
  issues = []
195
  energy = spectral["energy_distribution"]
196
+ freqs = spectral["freqs"]
197
+ hf_env = spectral.get("hf_env", None)
198
+ lf_env = spectral.get("lf_env", None)
199
+ flatness = spectral.get("spectral_flatness", None)
200
+ notches = spectral.get("spectral_notches", [])
201
 
202
+ # ============================================================
203
+ # 1️⃣ HF LOSS LOGIC (Speech-safe Thresholds)
204
+ # ============================================================
205
 
206
  hf_8_12 = energy["8k_12khz"]
207
  highest_freq = spectral["highest_freq_minus60db"]
208
 
209
+ # Severe HF cutoff β†’ Real LPF or aggressive NR
210
  if hf_8_12 < 0.01 and highest_freq < 9000:
211
  issues.append((
212
  "HF_LOSS", "HIGH",
213
+ f"Severe HF cutoff: {hf_8_12:.3f}% in 8–12k and rolloff at {highest_freq:.1f} Hz."
214
  ))
215
+ # Mild HF weakness β†’ Normal for speech
 
216
  elif hf_8_12 < 0.02:
217
  issues.append((
218
  "HF_LOSS", "LOW",
219
+ f"Low HF energy ({hf_8_12:.3f}%). Normal for speech."
220
  ))
221
 
222
+ # ============================================================
223
+ # 2️⃣ LPF DETECTOR (Low-pass filter)
224
+ # ============================================================
225
+
226
+ if hf_env is not None:
227
+ hf_region = (freqs >= 5000) & (freqs <= 12000)
228
+ hf_vals = hf_env[hf_region]
229
+ hf_freq = freqs[hf_region]
230
+
231
+ if len(hf_vals) > 10:
232
+ coef = np.polyfit(hf_freq, hf_vals, 1)
233
+ slope_per_hz = coef[0]
234
+ slope_db_oct = slope_per_hz * np.log2(2) * 12000
235
+
236
+ # Hard LPF cutoff
237
+ if highest_freq < 10000:
238
+ issues.append((
239
+ "LPF_DETECTED", "HIGH",
240
+ f"Low-pass filter near {highest_freq:.0f} Hz."
241
+ ))
242
+
243
+ # Soft HF tilt (EQ shelf)
244
+ elif slope_db_oct < -6:
245
+ issues.append((
246
+ "HF_EQ_SHELF", "LOW",
247
+ f"HF rolloff detected (~{slope_db_oct:.1f} dB/oct)."
248
+ ))
249
+
250
+ # ============================================================
251
+ # 3️⃣ HPF DETECTOR (High-pass filter)
252
+ # ============================================================
253
+
254
+ if lf_env is not None:
255
+ low_region = (freqs >= 20) & (freqs <= 300)
256
+ lf_vals = lf_env[low_region]
257
+ lf_freq = freqs[low_region]
258
+
259
+ if len(lf_vals) > 10:
260
+ coef_l = np.polyfit(lf_freq, lf_vals, 1)
261
+ slope_l = coef_l[0]
262
+ slope_db_oct_l = slope_l * np.log2(2) * 300
263
+
264
+ if energy["below_100hz"] < 0.5:
265
+ if slope_db_oct_l > 6:
266
+ issues.append((
267
+ "HPF_DETECTED", "HIGH",
268
+ f"High-pass filter detected (~{slope_db_oct_l:.1f} dB/oct)."
269
+ ))
270
+ else:
271
+ issues.append((
272
+ "HPF_SUSPECTED", "LOW",
273
+ f"Possible mild HPF (LF rolloff)."
274
+ ))
275
+
276
+ # ============================================================
277
+ # 4️⃣ Noise Reduction Artifact Detector
278
+ # ============================================================
279
+
280
+ if flatness is not None:
281
+ hf_flat = np.mean(flatness[-20:]) # Flattening in top HF region
282
+
283
+ # Strong NR β†’ metallic artifacts, HF flattening + notches
284
+ if hf_flat > 0.40 and len(notches) >= 3:
285
+ issues.append((
286
+ "NOISE_REDUCTION_ARTIFACTS", "HIGH",
287
+ f"NR artifacts: HF flattening ({hf_flat:.2f}) + {len(notches)} notches."
288
+ ))
289
+
290
+ # Mild NR
291
+ elif hf_flat > 0.35:
292
+ issues.append((
293
+ "NR_SOFT", "LOW",
294
+ f"Mild noise reduction detected (HF flattening={hf_flat:.2f})."
295
+ ))
296
+
297
+ # ============================================================
298
+ # 5️⃣ Spectral Notches (Resonance Removal / NR)
299
+ # ============================================================
300
+
301
+ if len(notches) > 0:
302
  issues.append((
303
+ "SPECTRAL_NOTCHES", "MEDIUM",
304
+ f"{len(notches)} spectral notches detected."
 
305
  ))
306
 
307
+ # ============================================================
308
+ # 6️⃣ Brick-wall LPF (from original code)
309
+ # ============================================================
310
+
311
  if spectral["brick_wall_detected"]:
312
  issues.append((
313
+ "BRICK_WALL", "HIGH",
314
+ f"Brick-wall behavior at {spectral['brick_wall_freq']:.0f} Hz."
 
315
  ))
316
 
317
+ # ============================================================
318
+ # 7️⃣ Compression / Dynamics
319
+ # ============================================================
 
 
 
 
 
 
320
 
321
+ crest = time_stats["crest_factor_db"]
322
+
323
+ if crest < 3:
 
324
  issues.append((
325
+ "OVER_COMPRESSION", "HIGH",
326
+ f"Very low crest factor ({crest:.1f} dB)."
 
327
  ))
328
+ elif crest < 6:
329
  issues.append((
330
+ "COMPRESSION", "MEDIUM",
331
+ f"Moderate compression ({crest:.1f} dB)."
 
332
  ))
333
 
334
+ # ============================================================
335
+ # 8️⃣ Clipping
336
+ # ============================================================
337
+
338
  if time_stats["peak"] >= 0.999:
339
  issues.append((
340
+ "CLIPPING", "CRITICAL",
 
341
  f"Peak amplitude {time_stats['peak']:.6f}. Possible clipping."
342
  ))
343
+ # ============================================================
344
+ # 9️⃣ DE-ESSER DETECTOR (HF transient suppression)
345
+ # ============================================================
346
+
347
+ # Presence & sibilance bands
348
+ band_3_6k = (freqs >= 3000) & (freqs <= 6000)
349
+ band_6_10k = (freqs >= 6000) & (freqs <= 10000)
350
+
351
+ if hf_env is not None:
352
+ presence_energy = np.mean(hf_env[band_3_6k])
353
+ sibilance_energy = np.mean(hf_env[band_6_10k])
354
+
355
+ # Ratio of presence energy to sibilance energy
356
+ if sibilance_energy < (presence_energy * 0.20):
357
+ issues.append((
358
+ "DE_ESSER_DETECTED", "MEDIUM",
359
+ "Sibilance band (6–10 kHz) strongly reduced relative to presence band (3–6 kHz). Possible de-essing."
360
+ ))
361
+ # ============================================================
362
+ # πŸ”Ÿ MULTIBAND COMPRESSION DETECTOR
363
+ # ============================================================
364
+
365
+ lf_band = (freqs >= 80) & (freqs <= 300)
366
+ mf_band = (freqs >= 300) & (freqs <= 3000)
367
+ hf_band = (freqs >= 3000) & (freqs <= 8000)
368
+
369
+ def band_crest(env, band):
370
+ vals = env[band]
371
+ if len(vals) == 0:
372
+ return None
373
+ return np.max(vals) - np.mean(vals)
374
+
375
+ if hf_env is not None:
376
+ cf_lf = band_crest(hf_env, lf_band)
377
+ cf_mf = band_crest(hf_env, mf_band)
378
+ cf_hf = band_crest(hf_env, hf_band)
379
+
380
+ # Compression fingerprint: MF and HF crest factor collapse
381
+ if cf_mf is not None and cf_hf is not None and cf_lf is not None:
382
+
383
+ # Heavy multiband compression signature
384
+ if cf_hf < (cf_lf * 0.4):
385
+ issues.append((
386
+ "MULTIBAND_COMPRESSION", "MEDIUM",
387
+ "HF crest factor significantly lower than LF. Possible multiband compression."
388
+ ))
389
+
390
+ if cf_mf < (cf_lf * 0.5):
391
+ issues.append((
392
+ "MULTIBAND_COMPRESSION", "LOW",
393
+ "Mid-band crest factor unusually compressed vs LF."
394
+ ))
395
+ # ============================================================
396
+ # 1️⃣1️⃣ EQ CURVE CLASSIFIER
397
+ # ============================================================
398
+
399
+ if hf_env is not None:
400
+ # Smooth envelope for stability
401
+ smooth = sps.medfilt(hf_env, kernel_size=9)
402
+
403
+ # Evaluate global tilt (HF slope)
404
+ coef_eq = np.polyfit(freqs, smooth, 1)
405
+ tilt = coef_eq[0]
406
+
407
+ # Check curvature β€” identifies shelves and peaking EQ
408
+ curvature = np.polyfit(freqs, smooth, 2)[0]
409
+
410
+ # Detect HF shelf boost
411
+ if tilt > 0.00002:
412
+ issues.append((
413
+ "EQ_HF_BOOST", "LOW",
414
+ "HF shelf boost detected (positive spectral tilt)."
415
+ ))
416
+
417
+ # Detect HF shelf cut
418
+ elif tilt < -0.00002:
419
+ issues.append((
420
+ "EQ_HF_CUT", "LOW",
421
+ "HF shelf cut detected (negative spectral tilt)."
422
+ ))
423
+
424
+ # Detect midrange peaking EQ
425
+ if curvature > 1e-12:
426
+ issues.append((
427
+ "EQ_PEAKING", "LOW",
428
+ "Spectral curvature indicates possible midrange peaking EQ."
429
+ ))
430
+
431
+ # Detect tilt EQ
432
+ if abs(tilt) > 0.00001 and abs(curvature) < 1e-12:
433
+ issues.append((
434
+ "EQ_TILT", "LOW",
435
+ "Tilt EQ detected (linear upward/downward spectral tilt)."
436
+ ))
437
+
438
+ # ============================================================
439
+ # Final return
440
+ # ============================================================
441
 
 
442
  return issues
443
 
444
  # ============================================================
 
671
 
672
 
673
  # ============================
674
+ # ISSUES PANEL (UPDATED)
675
  # ============================
676
 
677
  ax_issues = fig.add_subplot(gs[3, 0:3])
 
684
  "═" * 80
685
  ]
686
 
687
+ # No issues
688
  if not issues:
689
  issue_lines.append("βœ… No significant issues detected.")
690
+
691
  else:
692
+ # Updated severity mapping
693
  severity_icons = {
694
  "CRITICAL": "πŸ”΄ CRITICAL",
695
  "HIGH": "🟠 HIGH",
 
697
  "LOW": "🟒 LOW"
698
  }
699
 
700
+ # Dynamic issue listing (supports all new detectors)
701
  for issue_type, severity, description in issues:
702
  icon = severity_icons.get(severity, "βšͺ INFO")
703
  issue_lines.append(f"\n{icon} β€” {issue_type}")
704
  issue_lines.append(f" β†’ {description}")
705
 
706
+ # ============================
707
+ # SPECTRAL NOTCH DETAILS
708
+ # ============================
709
+
710
  if spec["spectral_notches"]:
711
+ issue_lines.append("\n🎡 SPECTRAL NOTCHES DETECTED:")
712
+ issue_lines.append(f" Total: {len(spec['spectral_notches'])}")
713
+
714
  for i, notch in enumerate(spec["spectral_notches"][:5], start=1):
715
  issue_lines.append(
716
+ f" {i}. {notch['freq']:.1f} Hz (Depth: {notch['depth_db']:.1f} dB)"
717
  )
718
+
719
  if len(spec["spectral_notches"]) > 5:
720
+ issue_lines.append(
721
+ f" ... and {len(spec['spectral_notches']) - 5} more notches"
722
+ )
723
+
724
+ # ============================
725
+ # BRICK-WALL FILTER NOTICE
726
+ # ============================
727
 
 
728
  if spec["brick_wall_detected"]:
729
+ issue_lines.append(
730
+ f"\n⚠️ BRICK-WALL FILTER DETECTED at {spec['brick_wall_freq']:.0f} Hz"
731
+ )
732
+
733
+ # ==================================================================
734
+ # FINAL OUTPUT
735
+ # ==================================================================
736
 
737
  issues_text = "\n".join(issue_lines)
738
 
739
  ax_issues.text(
740
+ 0.05, 0.95,
741
+ issues_text,
742
  transform=ax_issues.transAxes,
743
  fontsize=11,
744
  verticalalignment="top",
 
750
  linewidth=2
751
  )
752
  )
753
+
754
  # ============================
755
+ # QUALITY SCORE PANEL (UPDATED)
756
  # ============================
757
 
758
  ax_score = fig.add_subplot(gs[3, 3])
 
760
 
761
  issues = audio_data["issues"]
762
 
763
+ # Separate counts by severity
764
  critical = sum(1 for _, sev, _ in issues if sev == "CRITICAL")
765
  high = sum(1 for _, sev, _ in issues if sev == "HIGH")
766
  medium = sum(1 for _, sev, _ in issues if sev == "MEDIUM")
767
+ low = sum(1 for _, sev, _ in issues if sev == "LOW")
768
 
769
+ # --------------------------------------------
770
+ # NEW: Weighted scoring model
771
+ # --------------------------------------------
772
  score = 100
 
 
 
 
773
 
774
+ score -= critical * 35 # Hard-damage issues
775
+ score -= high * 20 # Major processing
776
+ score -= medium * 8 # Subtle but relevant
777
+ score -= low * 3 # Minor processing
778
+
779
+ # Additional penalties for heavy processing
780
+ if len(issues) >= 6:
781
+ score -= 10
782
+
783
+ if (critical + high) >= 3:
784
+ score -= 10
785
+
786
+ # Bonus for clean files
787
+ if len(issues) == 0:
788
+ score += 5
789
+
790
+ score = max(0, min(score, 100))
791
+
792
+ # --------------------------------------------
793
+ # GRADE + COLOR MAPPING
794
+ # --------------------------------------------
795
  if score >= 90:
796
  grade, quality, color = "A", "EXCELLENT", "#00C853"
797
+ recommendation = "Excellent for TTS dataset"
798
  elif score >= 75:
799
  grade, quality, color = "B", "GOOD", "#64DD17"
800
+ recommendation = "Very good quality; suitable for TTS"
801
  elif score >= 60:
802
  grade, quality, color = "C", "FAIR", "#FFD600"
803
+ recommendation = "Usable but may contain processing artifacts"
804
  elif score >= 40:
805
  grade, quality, color = "D", "POOR", "#FF6D00"
806
+ recommendation = "Not recommended for TTS (heavy processing)"
807
  else:
808
  grade, quality, color = "F", "CRITICAL", "#D50000"
809
+ recommendation = "Severely degraded or processed; avoid for TTS"
810
+
811
+ # --------------------------------------------
812
+ # NEW: CLEANLINESS & PROCESSING INDEX
813
+ # --------------------------------------------
814
+ cleanliness_score = max(0, 100 - (medium * 5 + low * 3))
815
+ processing_severity = (critical * 3) + (high * 2) + medium
816
 
817
  score_lines = [
818
  "QUALITY ASSESSMENT",
 
822
  f"GRADE: {grade}",
823
  f"QUALITY: {quality}",
824
  "",
825
+ "RECOMMENDATION:",
826
+ f"{recommendation}",
827
+ "",
828
+ "CLEANLINESS SCORE:",
829
+ f"{cleanliness_score}/100",
830
+ "",
831
+ "PROCESSING SEVERITY INDEX:",
832
+ f"{processing_severity}",
833
+ "",
834
  "ISSUES SUMMARY",
835
  "─" * 28,
836
  f"πŸ”΄ Critical: {critical}",
837
  f"🟠 High: {high}",
838
  f"🟑 Medium: {medium}",
839
+ f"🟒 Low: {low}",
840
  "",
841
  "─" * 28,
842
  "Generated:",
 
862
  fontweight="bold"
863
  )
864
 
 
865
  # SAVE REPORT
 
 
866
  plt.savefig(
867
  output_path,
868
  dpi=300,
 
873
  plt.close()
874
 
875
  return output_path
876
+
877
  # ============================================================
878
  # MAIN ANALYSIS FUNCTION (GRADIO CALLBACK)
879
  # ============================================================