Janani-V commited on
Commit
085470c
·
verified ·
1 Parent(s): 3bf404d

Upload app.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +600 -59
app.py CHANGED
@@ -1,7 +1,12 @@
1
  import gradio as gr
2
  from huggingface_hub import snapshot_download
3
  import sys
 
 
4
 
 
 
 
5
  local_dir = snapshot_download(repo_id="Janani-V/pcb-defect-yolov8s-deeppcb")
6
  sys.path.append(local_dir)
7
 
@@ -9,96 +14,632 @@ from inspector import PCBDefectInspector
9
 
10
  inspector = PCBDefectInspector(weights_path=f"{local_dir}/best.pt")
11
 
 
 
 
12
  SEVERITY_COLORS = {
13
  "Critical": "#ff4d4f",
14
  "High": "#ff7a45",
15
  "Medium-High": "#ffa940",
16
- "Medium": "#ffc53d",
17
  "Low-Medium": "#95de64",
18
  }
19
 
20
  SEVERITY_RANK = {
21
- "Critical": 4,
22
- "High": 3,
23
- "Medium-High": 2,
24
- "Medium": 1,
25
- "Low-Medium": 0,
26
  }
27
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  def detect(image, conf):
29
  inspector.conf = conf
30
  result = inspector.inspect(image)
31
 
32
- if not result["findings"]:
33
- return result["annotated_image"], "<p>No defects detected above the confidence threshold.</p>"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
 
35
- # Group findings by class so repeated instances don't repeat the same text
 
 
36
  grouped = {}
37
- for f in result["findings"]:
38
- grouped.setdefault(f["class"], {"count": 0, "confidences": [], "info": f})
39
- grouped[f["class"]]["count"] += 1
40
- grouped[f["class"]]["confidences"].append(f["confidence"])
41
 
42
- # Sort groups by severity, worst first
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  sorted_items = sorted(
44
  grouped.items(),
45
- key=lambda x: SEVERITY_RANK.get(x[1]["info"]["severity"], 0),
46
  reverse=True
47
  )
48
 
49
- # Overall severity banner
50
- highest_severity = sorted_items[0][1]["info"]["severity"]
51
- banner_color = SEVERITY_COLORS.get(highest_severity, "#333")
 
 
 
 
 
52
 
53
- html = f"""
54
- <div style="padding:14px 18px; border-radius:10px; margin-bottom:18px; background:{banner_color}; color:#000;">
55
- <b style="font-size:16px;">{result['summary']}</b><br/>
56
- <span style="font-size:14px;">Highest severity found: <b>{highest_severity}</b></span>
 
 
57
  </div>
58
  """
59
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  for cls, data in sorted_items:
61
  info = data["info"]
62
- color = SEVERITY_COLORS.get(info["severity"], "#d9d9d9")
63
- conf_list = ", ".join(str(c) for c in data["confidences"])
64
-
65
- html += f"""
66
- <div style="border:1px solid #444; border-radius:10px; padding:16px; margin-bottom:14px; background:#1e1e1e;">
67
- <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:8px;">
68
- <span style="font-size:18px; font-weight:600; color:#fff;">
69
- {cls.upper()} <span style="font-weight:400; color:#aaa;" {data['count']}</span>
70
- </span>
71
- <span style="background:{color}; color:#000; padding:4px 12px; border-radius:20px; font-size:13px; font-weight:600;">
72
- {info['severity']}
73
- </span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  </div>
75
- <p style="color:#bbb; font-size:13px; margin:4px 0;"><b>Confidence:</b> {conf_list}</p>
76
- <p style="color:#eee; margin:10px 0 6px 0;"><b>Explanation:</b> {info['explanation']}</p>
77
- <p style="color:#eee; margin:6px 0;"><b>Root Cause:</b> {info['root_cause']}</p>
78
- <p style="color:#eee; margin:6px 0;"><b>Impact:</b> {info['impact']}</p>
79
- <p style="color:#eee; margin:6px 0;"><b>Recommended Action:</b> {info['action']}</p>
80
  </div>
81
  """
82
 
83
- return result["annotated_image"], html
84
-
85
- demo = gr.Interface(
86
- fn=detect,
87
- inputs=[
88
- gr.Image(type="numpy", label="Upload PCB Image"),
89
- gr.Slider(0.1, 0.9, value=0.25, step=0.05, label="Confidence Threshold")
90
- ],
91
- outputs=[
92
- gr.Image(type="numpy", label="Detected Defects"),
93
- gr.HTML(label="Inspection Report")
94
- ],
95
- title="PCB Defect Detector — YOLOv8s (DeepPCB)",
96
- description="Upload a PCB image to detect defects and get severity, root cause, impact, and recommended action for each defect type, sorted by severity. [Model card](https://huggingface.co/Janani-V/pcb-defect-yolov8s-deeppcb)",
97
- examples=[
98
- ["sample1_input.jpg", 0.25],
99
- ["sample2_input.jpg", 0.25],
100
- ["sample3_input.jpg", 0.25]
101
- ]
102
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
 
104
  demo.launch()
 
1
  import gradio as gr
2
  from huggingface_hub import snapshot_download
3
  import sys
4
+ import statistics
5
+ from html import escape
6
 
7
+ # -----------------------------
8
+ # Load model + inspector
9
+ # -----------------------------
10
  local_dir = snapshot_download(repo_id="Janani-V/pcb-defect-yolov8s-deeppcb")
11
  sys.path.append(local_dir)
12
 
 
14
 
15
  inspector = PCBDefectInspector(weights_path=f"{local_dir}/best.pt")
16
 
17
+ # -----------------------------
18
+ # Severity config
19
+ # -----------------------------
20
  SEVERITY_COLORS = {
21
  "Critical": "#ff4d4f",
22
  "High": "#ff7a45",
23
  "Medium-High": "#ffa940",
24
+ "Medium": "#fadb14",
25
  "Low-Medium": "#95de64",
26
  }
27
 
28
  SEVERITY_RANK = {
29
+ "Critical": 5,
30
+ "High": 4,
31
+ "Medium-High": 3,
32
+ "Medium": 2,
33
+ "Low-Medium": 1,
34
  }
35
 
36
+ DEFAULT_SEVERITY_COLOR = "#8c8c8c"
37
+
38
+ # -----------------------------
39
+ # Helper: render KPI card
40
+ # -----------------------------
41
+ def kpi_card(title, value, subtitle="", accent="#5B8FF9"):
42
+ return f"""
43
+ <div class="kpi-card" style="border-top: 4px solid {accent};">
44
+ <div class="kpi-title">{escape(str(title))}</div>
45
+ <div class="kpi-value">{escape(str(value))}</div>
46
+ <div class="kpi-subtitle">{escape(str(subtitle))}</div>
47
+ </div>
48
+ """
49
+
50
+ # -----------------------------
51
+ # Helper: defect display names
52
+ # -----------------------------
53
+ def pretty_label(name: str) -> str:
54
+ return name.replace("-", " ").replace("_", " ").title()
55
+
56
+ # -----------------------------
57
+ # Main detection function
58
+ # -----------------------------
59
  def detect(image, conf):
60
  inspector.conf = conf
61
  result = inspector.inspect(image)
62
 
63
+ findings = result.get("findings", [])
64
+ annotated_image = result.get("annotated_image", image)
65
+ summary = result.get("summary", "Inspection completed.")
66
+
67
+ # No detections
68
+ if not findings:
69
+ no_defects_html = """
70
+ <div class="empty-state">
71
+ <div class="empty-icon">✓</div>
72
+ <div class="empty-title">No defects detected</div>
73
+ <div class="empty-text">
74
+ No PCB defects were detected above the selected confidence threshold.
75
+ </div>
76
+ </div>
77
+ """
78
+ return annotated_image, no_defects_html
79
 
80
+ # -----------------------------
81
+ # Group findings by class
82
+ # -----------------------------
83
  grouped = {}
84
+ all_confidences = []
 
 
 
85
 
86
+ for f in findings:
87
+ cls = f.get("class", "unknown")
88
+ conf_score = float(f.get("confidence", 0.0))
89
+ all_confidences.append(conf_score)
90
+
91
+ if cls not in grouped:
92
+ grouped[cls] = {
93
+ "count": 0,
94
+ "confidences": [],
95
+ "info": f
96
+ }
97
+
98
+ grouped[cls]["count"] += 1
99
+ grouped[cls]["confidences"].append(conf_score)
100
+
101
+ # Sort by severity descending
102
  sorted_items = sorted(
103
  grouped.items(),
104
+ key=lambda x: SEVERITY_RANK.get(x[1]["info"].get("severity", ""), 0),
105
  reverse=True
106
  )
107
 
108
+ # -----------------------------
109
+ # Overall summary metrics
110
+ # -----------------------------
111
+ total_defects = len(findings)
112
+ unique_types = len(grouped)
113
+ highest_severity = sorted_items[0][1]["info"].get("severity", "Unknown")
114
+ highest_severity_color = SEVERITY_COLORS.get(highest_severity, DEFAULT_SEVERITY_COLOR)
115
+ avg_conf = statistics.mean(all_confidences) if all_confidences else 0.0
116
 
117
+ summary_cards_html = f"""
118
+ <div class="kpi-grid">
119
+ {kpi_card("Total Defects", total_defects, "All detected instances", accent="#5B8FF9")}
120
+ {kpi_card("Defect Types", unique_types, "Unique classes found", accent="#36CFC9")}
121
+ {kpi_card("Highest Severity", highest_severity, "Most critical detected class", accent=highest_severity_color)}
122
+ {kpi_card("Avg Confidence", f"{avg_conf:.2f}", "Mean detection confidence", accent="#9254DE")}
123
  </div>
124
  """
125
 
126
+ # -----------------------------
127
+ # Summary banner
128
+ # -----------------------------
129
+ summary_banner_html = f"""
130
+ <div class="summary-banner" style="border-left: 6px solid {highest_severity_color};">
131
+ <div class="summary-title">Inspection Summary</div>
132
+ <div class="summary-text">{escape(summary)}</div>
133
+ <div class="summary-meta">
134
+ Highest severity detected:
135
+ <span class="severity-pill" style="background:{highest_severity_color}; color:#111;">
136
+ {escape(highest_severity)}
137
+ </span>
138
+ </div>
139
+ </div>
140
+ """
141
+
142
+ # -----------------------------
143
+ # Defect cards
144
+ # -----------------------------
145
+ defect_cards_html = '<div class="section-title">Detected Defect Analysis</div>'
146
+
147
  for cls, data in sorted_items:
148
  info = data["info"]
149
+ severity = info.get("severity", "Unknown")
150
+ severity_color = SEVERITY_COLORS.get(severity, DEFAULT_SEVERITY_COLOR)
151
+ display_name = pretty_label(cls)
152
+
153
+ conf_list = ", ".join(f"{c:.2f}" for c in data["confidences"])
154
+ avg_cls_conf = statistics.mean(data["confidences"]) if data["confidences"] else 0.0
155
+
156
+ explanation = info.get("explanation", "No explanation available.")
157
+ root_cause = info.get("root_cause", "Not available.")
158
+ impact = info.get("impact", "Not available.")
159
+ action = info.get("action", "Not available.")
160
+
161
+ defect_cards_html += f"""
162
+ <div class="defect-card">
163
+ <div class="defect-header">
164
+ <div>
165
+ <div class="defect-name">{escape(display_name)}</div>
166
+ <div class="defect-submeta">
167
+ Count: <b>{data['count']}</b> &nbsp;|&nbsp;
168
+ Avg confidence: <b>{avg_cls_conf:.2f}</b>
169
+ </div>
170
+ </div>
171
+ <div class="severity-pill" style="background:{severity_color}; color:#111;">
172
+ {escape(severity)}
173
+ </div>
174
+ </div>
175
+
176
+ <div class="defect-body">
177
+ <div class="defect-row">
178
+ <span class="defect-label">Confidence Scores</span>
179
+ <span class="defect-value">{escape(conf_list)}</span>
180
+ </div>
181
+
182
+ <div class="defect-row">
183
+ <span class="defect-label">Explanation</span>
184
+ <span class="defect-value">{escape(explanation)}</span>
185
+ </div>
186
+
187
+ <div class="defect-row">
188
+ <span class="defect-label">Likely Root Cause</span>
189
+ <span class="defect-value">{escape(root_cause)}</span>
190
+ </div>
191
+
192
+ <div class="defect-row">
193
+ <span class="defect-label">Potential Impact</span>
194
+ <span class="defect-value">{escape(impact)}</span>
195
+ </div>
196
+
197
+ <div class="defect-row">
198
+ <span class="defect-label">Recommended Action</span>
199
+ <span class="defect-value">{escape(action)}</span>
200
+ </div>
201
  </div>
 
 
 
 
 
202
  </div>
203
  """
204
 
205
+ # -----------------------------
206
+ # Compact defect summary table
207
+ # -----------------------------
208
+ table_html = """
209
+ <div class="section-title" style="margin-top: 22px;">Defect Summary Table</div>
210
+ <div class="table-wrap">
211
+ <table class="summary-table">
212
+ <thead>
213
+ <tr>
214
+ <th>Defect</th>
215
+ <th>Count</th>
216
+ <th>Severity</th>
217
+ <th>Avg Confidence</th>
218
+ </tr>
219
+ </thead>
220
+ <tbody>
221
+ """
222
+
223
+ for cls, data in sorted_items:
224
+ info = data["info"]
225
+ severity = info.get("severity", "Unknown")
226
+ severity_color = SEVERITY_COLORS.get(severity, DEFAULT_SEVERITY_COLOR)
227
+ display_name = pretty_label(cls)
228
+ avg_cls_conf = statistics.mean(data["confidences"]) if data["confidences"] else 0.0
229
+
230
+ table_html += f"""
231
+ <tr>
232
+ <td>{escape(display_name)}</td>
233
+ <td>{data['count']}</td>
234
+ <td>
235
+ <span class="severity-pill small" style="background:{severity_color}; color:#111;">
236
+ {escape(severity)}
237
+ </span>
238
+ </td>
239
+ <td>{avg_cls_conf:.2f}</td>
240
+ </tr>
241
+ """
242
+
243
+ table_html += """
244
+ </tbody>
245
+ </table>
246
+ </div>
247
+ """
248
+
249
+ final_html = summary_cards_html + summary_banner_html + defect_cards_html + table_html
250
+ return annotated_image, final_html
251
+
252
+
253
+ # -----------------------------
254
+ # Custom CSS
255
+ # -----------------------------
256
+ custom_css = """
257
+ :root {
258
+ --bg: #0f1117;
259
+ --panel: #161b22;
260
+ --panel-2: #1d232d;
261
+ --border: #2b3240;
262
+ --text: #f5f7fa;
263
+ --muted: #a8b0bd;
264
+ --accent: #5B8FF9;
265
+ --success: #36cfc9;
266
+ }
267
+
268
+ body, .gradio-container {
269
+ background: var(--bg) !important;
270
+ color: var(--text) !important;
271
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
272
+ }
273
+
274
+ .gradio-container {
275
+ max-width: 1400px !important;
276
+ margin: 0 auto !important;
277
+ padding-top: 8px !important;
278
+ }
279
+
280
+ /* Top header */
281
+ .app-header {
282
+ padding: 18px 20px 8px 20px;
283
+ margin-bottom: 6px;
284
+ }
285
+
286
+ .app-title {
287
+ font-size: 30px;
288
+ font-weight: 800;
289
+ color: #ffffff;
290
+ margin-bottom: 8px;
291
+ letter-spacing: 0.2px;
292
+ }
293
+
294
+ .app-subtitle {
295
+ font-size: 15px;
296
+ color: var(--muted);
297
+ line-height: 1.6;
298
+ max-width: 1000px;
299
+ }
300
+
301
+ /* Panels */
302
+ .panel-card {
303
+ background: linear-gradient(180deg, #151a22 0%, #11161d 100%);
304
+ border: 1px solid var(--border);
305
+ border-radius: 16px;
306
+ padding: 14px;
307
+ box-shadow: 0 10px 24px rgba(0, 0, 0, 0.22);
308
+ }
309
+
310
+ /* KPI cards */
311
+ .kpi-grid {
312
+ display: grid;
313
+ grid-template-columns: repeat(4, 1fr);
314
+ gap: 14px;
315
+ margin-bottom: 18px;
316
+ }
317
+
318
+ .kpi-card {
319
+ background: var(--panel);
320
+ border: 1px solid var(--border);
321
+ border-radius: 14px;
322
+ padding: 16px;
323
+ min-height: 110px;
324
+ box-shadow: 0 8px 18px rgba(0,0,0,0.18);
325
+ }
326
+
327
+ .kpi-title {
328
+ color: var(--muted);
329
+ font-size: 13px;
330
+ margin-bottom: 10px;
331
+ letter-spacing: 0.2px;
332
+ }
333
+
334
+ .kpi-value {
335
+ color: #fff;
336
+ font-size: 28px;
337
+ font-weight: 800;
338
+ line-height: 1.1;
339
+ margin-bottom: 8px;
340
+ }
341
+
342
+ .kpi-subtitle {
343
+ color: #8b95a7;
344
+ font-size: 12px;
345
+ }
346
+
347
+ /* Summary banner */
348
+ .summary-banner {
349
+ background: linear-gradient(180deg, #171d26 0%, #121821 100%);
350
+ border: 1px solid var(--border);
351
+ border-radius: 14px;
352
+ padding: 16px 18px;
353
+ margin-bottom: 18px;
354
+ }
355
+
356
+ .summary-title {
357
+ font-size: 18px;
358
+ font-weight: 700;
359
+ color: #fff;
360
+ margin-bottom: 8px;
361
+ }
362
+
363
+ .summary-text {
364
+ color: #d7dde7;
365
+ font-size: 14px;
366
+ line-height: 1.6;
367
+ margin-bottom: 10px;
368
+ }
369
+
370
+ .summary-meta {
371
+ color: #b8c1cf;
372
+ font-size: 14px;
373
+ }
374
+
375
+ /* Section titles */
376
+ .section-title {
377
+ color: #ffffff;
378
+ font-size: 18px;
379
+ font-weight: 700;
380
+ margin: 12px 0 14px 0;
381
+ }
382
+
383
+ /* Defect cards */
384
+ .defect-card {
385
+ background: linear-gradient(180deg, #161b22 0%, #12171f 100%);
386
+ border: 1px solid var(--border);
387
+ border-radius: 16px;
388
+ padding: 16px;
389
+ margin-bottom: 14px;
390
+ box-shadow: 0 8px 20px rgba(0,0,0,0.16);
391
+ }
392
+
393
+ .defect-header {
394
+ display: flex;
395
+ justify-content: space-between;
396
+ align-items: flex-start;
397
+ gap: 12px;
398
+ margin-bottom: 12px;
399
+ }
400
+
401
+ .defect-name {
402
+ font-size: 20px;
403
+ font-weight: 800;
404
+ color: #fff;
405
+ margin-bottom: 4px;
406
+ }
407
+
408
+ .defect-submeta {
409
+ color: var(--muted);
410
+ font-size: 13px;
411
+ }
412
+
413
+ .defect-body {
414
+ display: flex;
415
+ flex-direction: column;
416
+ gap: 10px;
417
+ }
418
+
419
+ .defect-row {
420
+ display: grid;
421
+ grid-template-columns: 180px 1fr;
422
+ gap: 14px;
423
+ align-items: start;
424
+ padding: 10px 0;
425
+ border-top: 1px solid rgba(255,255,255,0.05);
426
+ }
427
+
428
+ .defect-row:first-child {
429
+ border-top: none;
430
+ padding-top: 0;
431
+ }
432
+
433
+ .defect-label {
434
+ color: #c4ccd8;
435
+ font-size: 13px;
436
+ font-weight: 700;
437
+ text-transform: uppercase;
438
+ letter-spacing: 0.3px;
439
+ }
440
+
441
+ .defect-value {
442
+ color: #eef2f7;
443
+ font-size: 14px;
444
+ line-height: 1.6;
445
+ }
446
+
447
+ /* Severity pills */
448
+ .severity-pill {
449
+ display: inline-block;
450
+ padding: 6px 12px;
451
+ border-radius: 999px;
452
+ font-size: 12px;
453
+ font-weight: 800;
454
+ letter-spacing: 0.2px;
455
+ white-space: nowrap;
456
+ }
457
+
458
+ .severity-pill.small {
459
+ padding: 4px 10px;
460
+ font-size: 11px;
461
+ }
462
+
463
+ /* Table */
464
+ .table-wrap {
465
+ overflow-x: auto;
466
+ margin-top: 8px;
467
+ }
468
+
469
+ .summary-table {
470
+ width: 100%;
471
+ border-collapse: collapse;
472
+ background: var(--panel);
473
+ border: 1px solid var(--border);
474
+ border-radius: 14px;
475
+ overflow: hidden;
476
+ }
477
+
478
+ .summary-table th {
479
+ background: #1d2430;
480
+ color: #eef2f7;
481
+ text-align: left;
482
+ font-size: 13px;
483
+ padding: 14px 16px;
484
+ border-bottom: 1px solid var(--border);
485
+ }
486
+
487
+ .summary-table td {
488
+ padding: 14px 16px;
489
+ color: #dce3ed;
490
+ border-bottom: 1px solid rgba(255,255,255,0.05);
491
+ font-size: 14px;
492
+ }
493
+
494
+ .summary-table tr:last-child td {
495
+ border-bottom: none;
496
+ }
497
+
498
+ /* Empty state */
499
+ .empty-state {
500
+ background: linear-gradient(180deg, #161b22 0%, #12171f 100%);
501
+ border: 1px solid var(--border);
502
+ border-radius: 16px;
503
+ padding: 36px 20px;
504
+ text-align: center;
505
+ }
506
+
507
+ .empty-icon {
508
+ font-size: 42px;
509
+ margin-bottom: 12px;
510
+ color: #36cfc9;
511
+ }
512
+
513
+ .empty-title {
514
+ color: #fff;
515
+ font-size: 20px;
516
+ font-weight: 800;
517
+ margin-bottom: 8px;
518
+ }
519
+
520
+ .empty-text {
521
+ color: var(--muted);
522
+ font-size: 14px;
523
+ max-width: 600px;
524
+ margin: 0 auto;
525
+ line-height: 1.6;
526
+ }
527
+
528
+ /* Gradio element tweaks */
529
+ .gr-button-primary {
530
+ border-radius: 12px !important;
531
+ }
532
+
533
+ .gr-box, .gr-panel {
534
+ border-radius: 14px !important;
535
+ }
536
+
537
+ footer {
538
+ display: none !important;
539
+ }
540
+
541
+ /* Responsive */
542
+ @media (max-width: 1100px) {
543
+ .kpi-grid {
544
+ grid-template-columns: repeat(2, 1fr);
545
+ }
546
+
547
+ .defect-row {
548
+ grid-template-columns: 1fr;
549
+ gap: 6px;
550
+ }
551
+ }
552
+
553
+ @media (max-width: 700px) {
554
+ .kpi-grid {
555
+ grid-template-columns: 1fr;
556
+ }
557
+
558
+ .app-title {
559
+ font-size: 24px;
560
+ }
561
+
562
+ .defect-header {
563
+ flex-direction: column;
564
+ align-items: flex-start;
565
+ }
566
+ }
567
+ """
568
+
569
+ # -----------------------------
570
+ # Build Gradio UI
571
+ # -----------------------------
572
+ with gr.Blocks(css=custom_css, theme=gr.themes.Soft()) as demo:
573
+ gr.HTML("""
574
+ <div class="app-header">
575
+ <div class="app-title">PCB Defect Detector — YOLOv8s (DeepPCB)</div>
576
+ <div class="app-subtitle">
577
+ Upload a PCB image to detect manufacturing defects and receive an inspection report with
578
+ <b>severity assessment</b>, <b>likely root cause</b>, <b>impact</b>, and
579
+ <b>recommended action</b> for each detected defect type.
580
+ This interface combines a fine-tuned <b>YOLOv8s defect detector</b> with a structured
581
+ PCB defect interpretation layer for more informative inspection outputs.
582
+ </div>
583
+ </div>
584
+ """)
585
+
586
+ with gr.Row(equal_height=False):
587
+ # LEFT PANEL
588
+ with gr.Column(scale=5):
589
+ with gr.Group():
590
+ input_image = gr.Image(type="numpy", label="Upload PCB Image")
591
+ conf_slider = gr.Slider(
592
+ minimum=0.10,
593
+ maximum=0.90,
594
+ value=0.25,
595
+ step=0.05,
596
+ label="Confidence Threshold"
597
+ )
598
+
599
+ with gr.Row():
600
+ detect_btn = gr.Button("Run Inspection", variant="primary")
601
+ clear_btn = gr.ClearButton([input_image], value="Clear")
602
+
603
+ with gr.Group():
604
+ output_image = gr.Image(
605
+ type="numpy",
606
+ label="Detected Defects / Annotated Output"
607
+ )
608
+
609
+ # RIGHT PANEL
610
+ with gr.Column(scale=7):
611
+ report_html = gr.HTML(label="Inspection Report")
612
+
613
+ # Examples section
614
+ gr.Examples(
615
+ examples=[
616
+ ["sample1_input.jpg", 0.25],
617
+ ["sample2_input.jpg", 0.25],
618
+ ["sample3_input.jpg", 0.25]
619
+ ],
620
+ inputs=[input_image, conf_slider],
621
+ label="Example PCB Images"
622
+ )
623
+
624
+ # Footer / note
625
+ gr.HTML("""
626
+ <div style="margin-top:18px; color:#98a2b3; font-size:13px; line-height:1.6;">
627
+ <b>Note:</b> Severity, root cause, impact, and recommended action are generated from a
628
+ structured defect knowledge layer on top of the model’s class predictions. The YOLO model
629
+ itself performs visual defect detection; higher-level interpretation should still be reviewed
630
+ in the context of PCB manufacturing and inspection workflows.
631
+ <br/><br/>
632
+ <b>Model card:</b>
633
+ <a href="https://huggingface.co/Janani-V/pcb-defect-yolov8s-deeppcb" target="_blank">
634
+ Janani-V/pcb-defect-yolov8s-deeppcb
635
+ </a>
636
+ </div>
637
+ """)
638
+
639
+ detect_btn.click(
640
+ fn=detect,
641
+ inputs=[input_image, conf_slider],
642
+ outputs=[output_image, report_html]
643
+ )
644
 
645
  demo.launch()