Spaces:
Sleeping
Sleeping
| import os | |
| import base64 | |
| from pathlib import Path | |
| def generate_combined_report(results, out_dir): | |
| """ | |
| results is a list of dicts, each containing: | |
| - 'before_path': Path or None | |
| - 'after_path': Path or None | |
| - 'status': 'INTACT' | 'DAMAGED' | 'MISSING' | 'ADDED' | |
| - 'cls_score': float | |
| - 'patch_score': float | |
| - 'geo_inliers': int | |
| - 'confidence': str | |
| - 'stage_used': int | |
| - 'damage_description': str | |
| - 'target_phrases_list': list of str | |
| - 'masked_after_path': Path or None | |
| """ | |
| out = Path(out_dir) | |
| out.mkdir(parents=True, exist_ok=True) | |
| def b64img(path): | |
| if not path or not os.path.exists(path): | |
| return "" | |
| try: | |
| p = Path(path) | |
| data = base64.b64encode(p.read_bytes()).decode() | |
| ext = p.suffix.lstrip(".") or "jpeg" | |
| return f"data:image/{ext};base64,{data}" | |
| except Exception as e: | |
| print(f"Error encoding image {path}: {e}") | |
| return "" | |
| cards = "" | |
| for r in results: | |
| status = r['status'] | |
| # Determine styling based on status | |
| if status == 'INTACT': | |
| color = "#10b981" # Emerald | |
| bg_color = "rgba(16, 185, 129, 0.05)" | |
| border_c = "rgba(16, 185, 129, 0.2)" | |
| icon = "✅" | |
| label = "INTACT / سليم" | |
| elif status == 'DAMAGED': | |
| color = "#f59e0b" # Amber | |
| bg_color = "rgba(245, 158, 11, 0.05)" | |
| border_c = "rgba(245, 158, 11, 0.25)" | |
| icon = "⚠️" | |
| label = "DAMAGED / تالف" | |
| elif status == 'MISSING': | |
| color = "#ef4444" # Red | |
| bg_color = "rgba(239, 68, 68, 0.05)" | |
| border_c = "rgba(239, 68, 68, 0.2)" | |
| icon = "❌" | |
| label = "MISSING / مفقود" | |
| else: # ADDED | |
| color = "#3b82f6" # Blue | |
| bg_color = "rgba(59, 130, 246, 0.05)" | |
| border_c = "rgba(59, 130, 246, 0.25)" | |
| icon = "➕" | |
| label = "ADDED / مضاف" | |
| before_html = "" | |
| if r['before_path']: | |
| before_src = b64img(r['before_path']) | |
| before_name = Path(r['before_path']).name | |
| before_html = f""" | |
| <div class="img-box"> | |
| <div class="box-label">BEFORE IMAGE / الصورة قبل</div> | |
| <img src="{before_src}" alt="{before_name}"/> | |
| <div class="fname">{before_name}</div> | |
| </div>""" | |
| after_html = "" | |
| if r['after_path']: | |
| after_src = b64img(r['after_path']) | |
| after_name = Path(r['after_path']).name | |
| after_html = f""" | |
| {"" if status == "ADDED" else '<div class="arrow">→</div>'} | |
| <div class="img-box"> | |
| <div class="box-label">AFTER IMAGE / الصورة بعد</div> | |
| <img src="{after_src}" alt="{after_name}"/> | |
| <div class="fname">{after_name}</div> | |
| </div>""" | |
| mask_html = "" | |
| if status == 'DAMAGED' and r['masked_after_path']: | |
| mask_src = b64img(r['masked_after_path']) | |
| mask_name = Path(r['masked_after_path']).name | |
| mask_html = f""" | |
| <div class="arrow">→</div> | |
| <div class="img-box highlighted"> | |
| <div class="box-label" style="color:#f59e0b">🎯 DETECTED DAMAGE (SAM MASK) / الضرر المكتشف (قناع SAM)</div> | |
| <img src="{mask_src}" alt="{mask_name}" style="border-color:#f59e0b"/> | |
| <div class="fname" style="color:#f59e0b">{mask_name}</div> | |
| </div>""" | |
| stage_labels = {1: "DINOv2 CLS", 2: "DINOv2 Patch", 3: "SIFT+RANSAC"} | |
| stage_str = stage_labels.get(r.get('stage_used', 0), "None") | |
| # Matching metrics block | |
| scores_html = "" | |
| if status in ['INTACT', 'DAMAGED'] and r['after_path']: | |
| scores_html = f""" | |
| <div class="metric-chip">Match / مطابقة: <b>{stage_str}</b></div> | |
| <div class="metric-chip">CLS score / درجة CLS: <b>{r['cls_score']:.3f}</b></div> | |
| """ | |
| if r['patch_score']: | |
| scores_html += f'<div class="metric-chip">Patch score / درجة الرقعة: <b>{r["patch_score"]:.3f}</b></div>' | |
| if r['geo_inliers']: | |
| scores_html += f'<div class="metric-chip">Geo Inliers / مطابقة هندسية: <b>{r["geo_inliers"]}</b></div>' | |
| if r.get('confidence'): | |
| scores_html += f'<div class="metric-chip">Confidence / ثقة: <b>{r["confidence"]}</b></div>' | |
| # Damage Report Text Block | |
| report_details_html = "" | |
| if status == 'DAMAGED': | |
| phrases_badges = "".join([f'<span class="phrase-tag">{p}</span>' for p in r['target_phrases_list']]) | |
| report_details_html = f""" | |
| <div class="damage-report-box"> | |
| <h4>📝 AI Damage Assessment Report / تقرير فحص التلفيات بالذكاء الاصطناعي</h4> | |
| <p class="desc">{r['damage_description']}</p> | |
| <div class="phrases-container"> | |
| <strong>Segmented features / الأجزاء المحددة:</strong> | |
| <div class="phrase-tags-row">{phrases_badges}</div> | |
| </div> | |
| </div> | |
| """ | |
| elif status == 'INTACT': | |
| report_details_html = f""" | |
| <div class="intact-report-box"> | |
| <p>✨ AI analysis confirms this item is intact. No major damage or negative changes were detected. / يؤكد تحليل الذكاء الاصطناعي أن هذا العنصر سليم، ولم يتم رصد أي تلفيات أو تغييرات سلبية.</p> | |
| </div> | |
| """ | |
| elif status == 'MISSING': | |
| report_details_html = f""" | |
| <div class="missing-report-box"> | |
| <p>🔍 This item from the before inventory could not be matched with any item in the after inventory. It may have been moved, stolen, or removed. / لم يتم العثور على مطابقة لهذا العنصر في صور البعد، قد يكون تم نقله، سرقته أو إزالته.</p> | |
| </div> | |
| """ | |
| else: # ADDED | |
| report_details_html = f""" | |
| <div class="added-report-box"> | |
| <p>➕ New item added in the after inventory that was not present in the before inventory. / تم رصد عنصر جديد مضاف في صور البعد لم يكن موجوداً في صور القبل.</p> | |
| </div> | |
| """ | |
| cards += f""" | |
| <div class="card" data-status="{status}" style="background:{bg_color};border-color:{border_c}"> | |
| <div class="card-header"> | |
| <div class="badge" style="background:{color}">{icon} {label}</div> | |
| <div class="metrics-row">{scores_html}</div> | |
| </div> | |
| <div class="img-row"> | |
| {before_html} | |
| {after_html} | |
| {mask_html} | |
| </div> | |
| {report_details_html} | |
| </div>""" | |
| n_total = len(results) | |
| n_intact = sum(1 for r in results if r['status'] == 'INTACT') | |
| n_damaged = sum(1 for r in results if r['status'] == 'DAMAGED') | |
| n_missing = sum(1 for r in results if r['status'] == 'MISSING') | |
| n_added = sum(1 for r in results if r['status'] == 'ADDED') | |
| html = f"""<!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>AI Inventory & Damage Report</title> | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Cairo:wght@300;400;600;700;800&display=swap'); | |
| * {{ | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| }} | |
| body {{ | |
| font-family: 'Inter', 'Cairo', -apple-system, sans-serif; | |
| background: #0b0f19; | |
| color: #f1f5f9; | |
| padding: 2.5rem 1.5rem; | |
| line-height: 1.5; | |
| }} | |
| .container {{ | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| }} | |
| header {{ | |
| text-align: center; | |
| margin-bottom: 2.5rem; | |
| }} | |
| h1 {{ | |
| font-size: 2.5rem; | |
| font-weight: 800; | |
| margin-bottom: 0.5rem; | |
| letter-spacing: -0.025em; | |
| background: linear-gradient(135deg, #60a5fa, #34d399); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| }} | |
| .sub {{ | |
| color: #94a3b8; | |
| font-size: 1rem; | |
| font-weight: 400; | |
| }} | |
| .summary {{ | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); | |
| gap: 1.25rem; | |
| margin: 2rem 0; | |
| }} | |
| .stat {{ | |
| background: #1e293b; | |
| border-radius: 16px; | |
| padding: 1.25rem; | |
| text-align: center; | |
| border: 1px solid #334155; | |
| transition: transform 0.2s, box-shadow 0.2s; | |
| }} | |
| .stat:hover {{ | |
| transform: translateY(-2px); | |
| box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3); | |
| }} | |
| .stat .num {{ | |
| font-size: 2.25rem; | |
| font-weight: 800; | |
| line-height: 1.2; | |
| }} | |
| .stat .lbl {{ | |
| font-size: 0.85rem; | |
| color: #94a3b8; | |
| font-weight: 500; | |
| margin-top: 0.25rem; | |
| text-transform: uppercase; | |
| letter-spacing: 0.05em; | |
| }} | |
| .filters {{ | |
| text-align: center; | |
| margin-bottom: 2rem; | |
| }} | |
| .fbtn {{ | |
| background: #1e293b; | |
| border: 1px solid #334155; | |
| color: #cbd5e1; | |
| padding: 0.5rem 1.5rem; | |
| border-radius: 9999px; | |
| cursor: pointer; | |
| margin: 0.25rem; | |
| font-size: 0.875rem; | |
| font-weight: 600; | |
| transition: all 0.2s; | |
| }} | |
| .fbtn:hover, .fbtn.active {{ | |
| background: #3b82f6; | |
| color: #ffffff; | |
| border-color: #3b82f6; | |
| box-shadow: 0 4px 6px -1px rgba(59, 130, 246, 0.4); | |
| }} | |
| .card {{ | |
| border-radius: 20px; | |
| padding: 1.75rem; | |
| margin-bottom: 2rem; | |
| border: 1px solid; | |
| box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.4); | |
| background: #111827; | |
| transition: transform 0.2s; | |
| }} | |
| .card:hover {{ | |
| transform: scale(1.005); | |
| }} | |
| .card-header {{ | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| flex-wrap: wrap; | |
| gap: 1rem; | |
| margin-bottom: 1.25rem; | |
| border-bottom: 1px solid rgba(255, 255, 255, 0.05); | |
| padding-bottom: 1rem; | |
| }} | |
| .badge {{ | |
| display: inline-block; | |
| padding: 0.35rem 1.1rem; | |
| border-radius: 9999px; | |
| font-weight: 700; | |
| color: #ffffff; | |
| font-size: 0.85rem; | |
| text-transform: uppercase; | |
| letter-spacing: 0.025em; | |
| }} | |
| .metrics-row {{ | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 0.5rem; | |
| }} | |
| .metric-chip {{ | |
| background: #1e293b; | |
| border: 1px solid #334155; | |
| padding: 0.25rem 0.75rem; | |
| border-radius: 8px; | |
| font-size: 0.75rem; | |
| color: #94a3b8; | |
| }} | |
| .metric-chip b {{ | |
| color: #f1f5f9; | |
| }} | |
| .img-row {{ | |
| display: flex; | |
| align-items: center; | |
| gap: 1.5rem; | |
| flex-wrap: wrap; | |
| margin-bottom: 1.5rem; | |
| }} | |
| .img-box {{ | |
| flex: 1; | |
| min-width: 260px; | |
| text-align: center; | |
| background: #0f172a; | |
| padding: 1rem; | |
| border-radius: 12px; | |
| border: 1px solid #1e293b; | |
| }} | |
| .img-box img {{ | |
| max-width: 100%; | |
| max-height: 280px; | |
| border-radius: 8px; | |
| object-fit: contain; | |
| border: 2px solid #334155; | |
| margin-top: 0.5rem; | |
| background: #0b0f19; | |
| }} | |
| .box-label {{ | |
| font-size: 0.75rem; | |
| font-weight: 700; | |
| color: #64748b; | |
| text-transform: uppercase; | |
| letter-spacing: 0.08em; | |
| }} | |
| .fname {{ | |
| font-size: 0.75rem; | |
| color: #475569; | |
| margin-top: 0.5rem; | |
| word-break: break-all; | |
| font-family: monospace; | |
| }} | |
| .arrow {{ | |
| font-size: 2rem; | |
| color: #475569; | |
| font-weight: 800; | |
| flex-shrink: 0; | |
| }} | |
| .damage-report-box {{ | |
| background: rgba(245, 158, 11, 0.03); | |
| border: 1px dashed rgba(245, 158, 11, 0.2); | |
| border-radius: 12px; | |
| padding: 1.25rem; | |
| margin-top: 1rem; | |
| }} | |
| .damage-report-box h4 {{ | |
| color: #f59e0b; | |
| font-size: 0.95rem; | |
| font-weight: 700; | |
| margin-bottom: 0.5rem; | |
| }} | |
| .damage-report-box .desc {{ | |
| font-size: 0.9rem; | |
| color: #e2e8f0; | |
| margin-bottom: 0.75rem; | |
| }} | |
| .phrases-container {{ | |
| display: flex; | |
| align-items: center; | |
| gap: 0.75rem; | |
| flex-wrap: wrap; | |
| font-size: 0.8rem; | |
| color: #94a3b8; | |
| }} | |
| .phrase-tags-row {{ | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 0.35rem; | |
| }} | |
| .phrase-tag {{ | |
| background: rgba(245, 158, 11, 0.1); | |
| color: #f59e0b; | |
| border: 1px solid rgba(245, 158, 11, 0.2); | |
| padding: 0.15rem 0.5rem; | |
| border-radius: 4px; | |
| font-weight: 600; | |
| font-size: 0.75rem; | |
| }} | |
| .intact-report-box {{ | |
| background: rgba(16, 185, 129, 0.03); | |
| border: 1px solid rgba(16, 185, 129, 0.15); | |
| border-radius: 12px; | |
| padding: 1rem 1.25rem; | |
| font-size: 0.9rem; | |
| color: #a7f3d0; | |
| }} | |
| .missing-report-box {{ | |
| background: rgba(239, 68, 68, 0.03); | |
| border: 1px solid rgba(239, 68, 68, 0.15); | |
| border-radius: 12px; | |
| padding: 1rem 1.25rem; | |
| font-size: 0.9rem; | |
| color: #fca5a5; | |
| }} | |
| .added-report-box {{ | |
| background: rgba(59, 130, 246, 0.03); | |
| border: 1px solid rgba(59, 130, 246, 0.15); | |
| border-radius: 12px; | |
| padding: 1rem 1.25rem; | |
| font-size: 0.9rem; | |
| color: #93c5fd; | |
| }} | |
| @media (max-width: 768px) {{ | |
| .arrow {{ | |
| display: none; | |
| }} | |
| .img-row {{ | |
| flex-direction: column; | |
| }} | |
| .img-box {{ | |
| width: 100%; | |
| }} | |
| .card-header {{ | |
| flex-direction: column; | |
| align-items: flex-start; | |
| }} | |
| }} | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <header> | |
| <h1>🏠 Property Inventory & Damage Report / تقرير جرد وتحديد تلفيات الممتلكات</h1> | |
| <p class="sub">AI Matching & Inspection System · نظام المطابقة والفحص الذكي بالذكاء الاصطناعي (DINOv2 + Grounded-SAM + MLLM)</p> | |
| </header> | |
| <div class="summary"> | |
| <div class="stat"> | |
| <div class="num" style="color:#22c55e">{n_intact}</div> | |
| <div class="lbl">✅ Intact / سليم</div> | |
| </div> | |
| <div class="stat"> | |
| <div class="num" style="color:#f59e0b">{n_damaged}</div> | |
| <div class="lbl">⚠️ Damaged / تالف</div> | |
| </div> | |
| <div class="stat"> | |
| <div class="num" style="color:#ef4444">{n_missing}</div> | |
| <div class="lbl">❌ Missing / مفقود</div> | |
| </div> | |
| <div class="stat"> | |
| <div class="num" style="color:#3b82f6">{n_added}</div> | |
| <div class="lbl">➕ Added / مضاف</div> | |
| </div> | |
| <div class="stat"> | |
| <div class="num" style="color:#a855f7">{n_total}</div> | |
| <div class="lbl">Total Items / إجمالي العناصر</div> | |
| </div> | |
| </div> | |
| <div class="filters"> | |
| <button class="fbtn active" onclick="filt('ALL', this)">All Items / كل العناصر</button> | |
| <button class="fbtn" onclick="filt('INTACT', this)">✅ Intact / سليم</button> | |
| <button class="fbtn" onclick="filt('DAMAGED', this)">⚠️ Damaged / تالف</button> | |
| <button class="fbtn" onclick="filt('MISSING', this)">❌ Missing / مفقود</button> | |
| <button class="fbtn" onclick="filt('ADDED', this)">➕ Added / مضاف</button> | |
| </div> | |
| <div id="cards-container"> | |
| {cards} | |
| </div> | |
| </div> | |
| <script> | |
| function filt(status, btn) {{ | |
| document.querySelectorAll('.fbtn').forEach(b => b.classList.remove('active')); | |
| btn.classList.add('active'); | |
| document.querySelectorAll('.card').forEach(c => {{ | |
| const cardStatus = c.getAttribute('data-status'); | |
| if (status === 'ALL') {{ | |
| c.style.display = 'block'; | |
| }} else {{ | |
| c.style.display = cardStatus === status ? 'block' : 'none'; | |
| }} | |
| }}); | |
| }} | |
| </script> | |
| </body> | |
| </html>""" | |
| # Escape curly braces in css and scripts properly (using %% for format double escape or format string) | |
| report_file = out / "inventory_damage_report.html" | |
| report_file.write_text(html, encoding="utf-8") | |
| return str(report_file) | |