from __future__ import annotations import argparse import json from pathlib import Path from types import SimpleNamespace import gradio as gr import torch from .infer import load_model, run_auralguard try: from .audio_quality import audio_quality_report except Exception: audio_quality_report = None try: from .decision_gate import make_safe_report except Exception: make_safe_report = None try: from .explain_plus import beginner_friendly_explanation_plus except Exception: beginner_friendly_explanation_plus = None def parse_args(): p = argparse.ArgumentParser(description="Visual showcase demo for AuralGuard-AASIST++") p.add_argument("--baseline-checkpoint", default="", help="Optional ASVspoof-only baseline checkpoint") p.add_argument("--final-checkpoint", required=True, help="Final AuralGuard checkpoint") p.add_argument("--baseline-name", default="ASVspoof Baseline") p.add_argument("--final-name", default="AuralGuard Final") p.add_argument("--aasist-root", default="external/aasist") p.add_argument("--aasist-config", default="external/aasist/config/AASIST.conf") p.add_argument("--sample-rate", type=int, default=16000) p.add_argument("--duration-sec", type=float, default=4.0) p.add_argument("--feature-dim", type=int, default=160) p.add_argument("--device", default="cuda" if torch.cuda.is_available() else "cpu") p.add_argument("--share", action="store_true") return p.parse_args() def load_checkpoint(args, checkpoint: str, device): model_args = SimpleNamespace( checkpoint=checkpoint, aasist_root=args.aasist_root, aasist_config=args.aasist_config, sample_rate=args.sample_rate, duration_sec=args.duration_sec, feature_dim=args.feature_dim, device=str(device), ) return load_model(model_args, device) def safe_quality_report(audio_path: str, sample_rate: int): if audio_quality_report is None: return {"warnings": [], "metrics": {}} try: return audio_quality_report(audio_path, target_sr=sample_rate) except Exception as e: return { "warnings": [f"Could not compute audio quality metrics: {e}"], "metrics": {}, } def safe_gate(report: dict, quality: dict): if make_safe_report is None: return report try: return make_safe_report(report, quality_warnings=quality.get("warnings", []), ood_warning=False) except Exception: return report def safe_beginner_explanation(report: dict, quality: dict): if beginner_friendly_explanation_plus is not None: try: gate = report.get("decision_gate", {}) return beginner_friendly_explanation_plus( fake_prob=float(report.get("fake_probability", 0.0)), attack_type=report.get("attack_type", "unknown"), suspicious_segments=report.get("suspicious_segments", []), safe_decision=report.get("decision", "unknown"), warnings=gate.get("warnings", []) + quality.get("warnings", []), ) except Exception: pass fake_prob = float(report.get("fake_probability", 0.0)) decision = report.get("decision", "unknown") attack_type = report.get("attack_type", "unknown") if "real" in decision: return ( "The model thinks this audio is likely real because the fake score is low and the sound patterns " "look closer to real human speech examples." ) if "fake" in decision: return ( f"The model thinks this audio may be fake because the fake score is high and the attack-type head " f"suggests {attack_type} style evidence." ) return ( "The model is not fully sure. The audio should be reviewed by a human rather than automatically labelled fake or real." ) def decision_color(decision: str): d = (decision or "").lower() if "real" in d: return "#16a34a", "✅", "Likely Real" if "fake" in d: return "#dc2626", "🚨", "Likely Fake" return "#f59e0b", "⚠️", "Human Review" def probability_bar(prob: float): prob = max(0.0, min(1.0, float(prob))) if prob < 0.35: color = "#16a34a" elif prob < 0.85: color = "#f59e0b" else: color = "#dc2626" return f"""
Fake probability {prob:.4f}
0.00 Real0.350.851.00 Fake
""" def model_card(name: str, report: dict): decision = report.get("decision", "unknown") fake_prob = float(report.get("fake_probability", 0.0)) attack_type = report.get("attack_type", "unknown") review = report.get("review_required", False) color, icon, label = decision_color(decision) return f"""
{name}
AASIST-based detector
{icon} {label}
{probability_bar(fake_prob)}
Decision
{decision}
Attack clue
{attack_type}
Human review
{'Yes' if review else 'No'}
""" def comparison_banner(baseline_report: dict, final_report: dict): b = float(baseline_report.get("fake_probability", 0.0)) f = float(final_report.get("fake_probability", 0.0)) diff = b - f if diff > 0.20: headline = "False-alarm reduction visible" text = ( f"The final model lowered the fake probability by {diff:.4f}. " "This supports the project goal of reducing false alarms on real-world speech." ) color = "#0f766e" icon = "📉" elif diff < -0.20: headline = "Final model is more suspicious" text = ( f"The final model increased the fake probability by {abs(diff):.4f}. " "This clip should be reviewed carefully." ) color = "#b45309" icon = "🔎" else: headline = "Models broadly agree" text = ( f"The fake probability changed by {diff:.4f}. " "Both models give similar confidence on this clip." ) color = "#2563eb" icon = "📊" return f"""
{icon}
{headline}
{text}
""" def quality_html(quality: dict): metrics = quality.get("metrics", {}) or {} warnings = quality.get("warnings", []) or [] cards = "" for k, v in metrics.items(): if isinstance(v, float): show = f"{v:.4f}" else: show = str(v) cards += f"""
{k.replace('_', ' ').title()}
{show}
""" if not cards: cards = "

No audio quality metrics available.

" if warnings: warning_list = "".join([f"
  • {w}
  • " for w in warnings]) warning_box = f"""
    Reliability warnings
    """ else: warning_box = """
    No major audio-quality warnings detected.
    """ return f"""
    {cards}
    {warning_box} """ def segment_timeline(suspicious_segments): suspicious_segments = suspicious_segments or [] if not suspicious_segments: return """
    No suspicious timestamp region was detected for this clip.
    """ rows = "" for i, s in enumerate(suspicious_segments[:10], start=1): start = float(s.get("start", 0.0)) end = float(s.get("end", 0.0)) score = float(s.get("fake_score", 0.0)) color = "#dc2626" if score >= 0.85 else "#f59e0b" rows += f"""
    #{i}
    {start:.2f}s → {end:.2f}s
    {score:.3f}
    """ return f"""
    Suspicious region Fake score
    {rows}
    """ def make_report_for_model(audio_path, model, model_name, args, device, quality): raw = run_auralguard( audio_path, model, sample_rate=args.sample_rate, duration_sec=args.duration_sec, device=device, ) safe = safe_gate(raw, quality) beginner = safe_beginner_explanation(safe, quality) safe["model_name"] = model_name safe["beginner_explanation"] = beginner safe["quality_report"] = quality return safe def main(): args = parse_args() device = torch.device(args.device) print(f"Using device: {device}") final_model = load_checkpoint(args, args.final_checkpoint, device) baseline_model = None if args.baseline_checkpoint: baseline_model = load_checkpoint(args, args.baseline_checkpoint, device) def analyze(audio_path): if audio_path is None: empty_card = """
    Upload an audio clip to begin analysis.
    """ return empty_card, empty_card, "", "", "", {}, "", None quality = safe_quality_report(audio_path, args.sample_rate) final_report = make_report_for_model( audio_path, final_model, args.final_name, args, device, quality ) if baseline_model is not None: baseline_report = make_report_for_model( audio_path, baseline_model, args.baseline_name, args, device, quality ) else: baseline_report = { "model_name": args.baseline_name, "decision": "not loaded", "fake_probability": 0.0, "attack_type": "not loaded", "review_required": False, "beginner_explanation": "No baseline checkpoint was provided.", "suspicious_segments": [], "evidence": {}, "quality_report": quality, } combined = { "baseline": baseline_report, "final": final_report, "quality_report": quality, "comparison": { "baseline_fake_probability": float(baseline_report.get("fake_probability", 0.0)), "final_fake_probability": float(final_report.get("fake_probability", 0.0)), "difference_baseline_minus_final": float(baseline_report.get("fake_probability", 0.0)) - float(final_report.get("fake_probability", 0.0)), }, } report_path = Path("results") / "demo_showcase_last_report.json" report_path.parent.mkdir(parents=True, exist_ok=True) report_path.write_text(json.dumps(combined, indent=2), encoding="utf-8") return ( model_card(args.baseline_name, baseline_report), model_card(args.final_name, final_report), comparison_banner(baseline_report, final_report), final_report.get("beginner_explanation", ""), segment_timeline(final_report.get("suspicious_segments", [])), final_report.get("evidence", {}), quality_html(quality), str(report_path), ) css = """ body { background: radial-gradient(circle at top left, #e0f2fe 0, transparent 32%), radial-gradient(circle at top right, #fee2e2 0, transparent 28%), linear-gradient(135deg, #f8fafc 0%, #eef2ff 100%) !important; } .gradio-container { max-width: 1180px !important; margin: auto !important; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif !important; } .hero { padding: 30px 28px; border-radius: 28px; color: white; background: linear-gradient(135deg, rgba(15,23,42,.94), rgba(30,64,175,.86)), radial-gradient(circle at 20% 20%, rgba(56,189,248,.32), transparent 30%); box-shadow: 0 24px 60px rgba(15,23,42,.25); margin-bottom: 18px; border: 1px solid rgba(255,255,255,.18); } .hero-title { font-size: 42px; font-weight: 900; letter-spacing: -1.2px; margin-bottom: 8px; } .hero-subtitle { font-size: 17px; line-height: 1.55; opacity: .92; max-width: 880px; } .hero-pills { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 18px; } .pill { padding: 8px 12px; border-radius: 999px; background: rgba(255,255,255,.13); border: 1px solid rgba(255,255,255,.22); font-size: 13px; font-weight: 700; } .upload-card { padding: 18px; border-radius: 22px; background: rgba(255,255,255,.82); border: 1px solid rgba(148,163,184,.25); box-shadow: 0 16px 40px rgba(15,23,42,.08); } .model-card { padding: 20px; border-radius: 24px; background: rgba(255,255,255,.92); border: 1px solid rgba(148,163,184,.25); box-shadow: 0 18px 50px rgba(15,23,42,.10); margin-bottom: 14px; } .card-topline { display: flex; justify-content: space-between; align-items: flex-start; gap: 14px; margin-bottom: 16px; } .model-name { font-size: 20px; font-weight: 850; color: #0f172a; } .model-subtitle { font-size: 13px; color: #64748b; margin-top: 2px; } .badge { color: white; padding: 8px 12px; border-radius: 999px; font-weight: 850; font-size: 13px; box-shadow: 0 8px 18px rgba(15,23,42,.15); white-space: nowrap; } .prob-wrap { margin: 16px 0; } .prob-label { display: flex; justify-content: space-between; color: #334155; font-size: 13px; margin-bottom: 7px; } .prob-track { height: 13px; border-radius: 999px; background: #e2e8f0; overflow: hidden; } .prob-fill { height: 100%; border-radius: 999px; transition: width .45s ease; } .prob-scale { display: flex; justify-content: space-between; font-size: 11px; color: #64748b; margin-top: 6px; } .mini-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; margin-top: 14px; } .mini-stat { padding: 12px; border-radius: 16px; background: #f8fafc; border: 1px solid #e2e8f0; } .mini-label { font-size: 11px; color: #64748b; text-transform: uppercase; letter-spacing: .04em; font-weight: 800; } .mini-value { font-size: 13px; color: #0f172a; margin-top: 5px; font-weight: 800; word-break: break-word; } .comparison-banner { display: flex; gap: 16px; align-items: flex-start; padding: 18px; border-radius: 22px; background: rgba(255,255,255,.9); border: 1px solid rgba(148,163,184,.25); border-left: 8px solid; box-shadow: 0 14px 36px rgba(15,23,42,.08); } .comparison-icon { font-size: 32px; } .comparison-title { font-size: 20px; font-weight: 900; color: #0f172a; margin-bottom: 5px; } .comparison-text { color: #475569; line-height: 1.55; } .quality-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; margin-bottom: 14px; } .quality-card { padding: 14px; border-radius: 18px; background: white; border: 1px solid #e2e8f0; } .quality-label { font-size: 12px; color: #64748b; text-transform: uppercase; font-weight: 850; } .quality-value { font-size: 20px; font-weight: 900; color: #0f172a; margin-top: 4px; } .warning-box { padding: 14px; border-radius: 18px; background: #fff7ed; border: 1px solid #fed7aa; color: #9a3412; } .ok-box { padding: 14px; border-radius: 18px; background: #ecfdf5; border: 1px solid #bbf7d0; color: #166534; } .segment-box { padding: 16px; border-radius: 20px; background: white; border: 1px solid #e2e8f0; } .segment-header, .segment-row { display: grid; grid-template-columns: 80px 1fr 120px; gap: 10px; align-items: center; } .segment-header { font-size: 12px; color: #64748b; text-transform: uppercase; font-weight: 850; margin-bottom: 8px; } .segment-row { padding: 10px 0; border-top: 1px solid #e2e8f0; } .segment-id { font-weight: 900; color: #334155; } .segment-time { color: #0f172a; font-weight: 750; } .segment-score { font-weight: 900; } .empty-state { padding: 18px; border-radius: 18px; background: #f8fafc; border: 1px dashed #cbd5e1; color: #64748b; text-align: center; font-weight: 700; } .empty-state.big { padding: 45px 20px; font-size: 18px; } textarea, input { border-radius: 14px !important; } .footer-note { padding: 16px 18px; border-radius: 20px; background: rgba(15,23,42,.90); color: white; line-height: 1.55; margin-top: 16px; } """ with gr.Blocks(css=css, title="AuralGuard-AASIST++ Showcase") as demo: gr.HTML( """
    AuralGuard-AASIST++
    A robust and explainable audio deepfake detector designed for synthetic speech, real-world accented speech, interview audio, and human-review forensic workflows.
    🎙️ Audio Deepfake Detection 🌍 Accent Robustness ⚖️ False-Alarm Reduction 🧠 Beginner Explanation 📍 Suspicious Timestamps
    """ ) with gr.Row(): with gr.Column(scale=4): with gr.Group(elem_classes=["upload-card"]): audio_input = gr.Audio(type="filepath", label="Upload an audio clip") analyze_btn = gr.Button("Analyze Audio", variant="primary", size="lg") gr.Markdown( """ **Best demo test:** upload the same DECTE/FRED/interview clip and compare the baseline model with the final balanced model. """ ) with gr.Column(scale=6): baseline_card = gr.HTML() final_card = gr.HTML() comparison = gr.HTML() with gr.Tab("Beginner Explanation"): beginner_explanation = gr.Textbox( label="Simple explanation for non-technical users", lines=8, show_copy_button=True, ) with gr.Tab("Suspicious Timestamp Regions"): suspicious_segments_html = gr.HTML() with gr.Tab("Evidence Packet"): evidence_packet = gr.JSON(label="Final model evidence packet") with gr.Tab("Audio Quality"): quality_report_html = gr.HTML() with gr.Tab("Export"): export_path = gr.Textbox(label="Saved JSON report path", show_copy_button=True) gr.Markdown( """ The JSON report can be used as evidence for screenshots, presentations, or your paper appendix. """ ) analyze_btn.click( fn=analyze, inputs=audio_input, outputs=[ baseline_card, final_card, comparison, beginner_explanation, suspicious_segments_html, evidence_packet, quality_report_html, export_path, ], ) gr.HTML( """ """ ) demo.launch(share=args.share, inbrowser=True, show_error=True) if __name__ == "__main__": main()