from __future__ import annotations import argparse import html 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="Clear AuralGuard-AASIST++ forensic dashboard demo") p.add_argument("--baseline-checkpoint", default="", help="Optional baseline checkpoint") p.add_argument("--final-checkpoint", required=True, help="Final AuralGuard checkpoint") p.add_argument("--baseline-name", default="Baseline AASIST") p.add_argument("--final-name", default="AuralGuard-AASIST++") 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) -> dict: if audio_quality_report is None: return {"warnings": [], "metrics": {}} try: return audio_quality_report(audio_path, target_sr=sample_rate) except Exception as exc: return {"warnings": [f"Could not compute audio quality report: {exc}"], "metrics": {}} def safe_make_report(raw_report: dict, quality: dict) -> dict: if make_safe_report is None: out = dict(raw_report) out["raw_decision"] = raw_report.get("decision", "unknown") out["review_required"] = "suspicious" in str(out.get("decision", "")).lower() out["decision_gate"] = {"warnings": quality.get("warnings", [])} return out return make_safe_report( raw_report, quality_warnings=quality.get("warnings", []), ood_warning=False, ) def simple_explanation(report: dict) -> str: fake_prob = float(report.get("fake_probability", 0.0)) attack_type = str(report.get("attack_type", "unknown")) decision = str(report.get("decision", "unknown")) warnings = report.get("decision_gate", {}).get("warnings", []) suspicious_segments = report.get("suspicious_segments", []) if beginner_friendly_explanation_plus is not None: return beginner_friendly_explanation_plus( fake_prob=fake_prob, attack_type=attack_type, suspicious_segments=suspicious_segments, safe_decision=decision, warnings=warnings, ) if "real" in decision.lower(): return "The final model gives this audio a low fake score, so it is treated as likely real." if "fake" in decision.lower(): return "The final model gives this audio a high fake score, so it is treated as likely fake." return "The model is not fully confident, so this audio should be reviewed by a human." def status_class(decision: str) -> tuple[str, str]: d = (decision or "").lower() if "real" in d: return "real", "LIKELY REAL" if "fake" in d: return "fake", "LIKELY FAKE" return "review", "HUMAN REVIEW" def final_decision_banner(report: dict, model_name: str) -> str: decision = str(report.get("decision", "unknown")) fake_prob = float(report.get("fake_probability", 0.0)) attack_type = str(report.get("attack_type", "unknown")) review = bool(report.get("review_required", False)) cls, label = status_class(decision) return f"""
FINAL AURALGUARD RESULT
{label}
This is the main decision from the improved final model.
Fake probability {fake_prob:.4f}
Attack-type clue {html.escape(attack_type)}
Human review {"Yes" if review else "No"}
""" def html_card(model_name: str, report: dict, subtitle: str, is_final: bool = False) -> str: decision = str(report.get("decision", "unknown")) fake_prob = float(report.get("fake_probability", 0.0)) attack_type = str(report.get("attack_type", "unknown")) review = bool(report.get("review_required", False)) cls, label = status_class(decision) bar_width = max(0.0, min(100.0, fake_prob * 100.0)) if is_final: tag = "USE THIS RESULT" role = "Final improved model" else: tag = "COMPARISON ONLY" role = "Older baseline model" return f"""
{tag}
{html.escape(model_name)}
{label}
{html.escape(role)} ยท {html.escape(subtitle)}
Fake probability
{fake_prob:.4f}
Attack-type clue
{html.escape(attack_type)}
""" def comparison_panel(baseline_report: dict, final_report: dict) -> str: base_prob = float(baseline_report.get("fake_probability", 0.0)) final_prob = float(final_report.get("fake_probability", 0.0)) diff = base_prob - final_prob base_decision = baseline_report.get("decision", "unknown") final_decision = final_report.get("decision", "unknown") return f"""
MODEL COMPARISON

Baseline vs Final AuralGuard

The baseline is shown only to demonstrate how the older model behaves. The final AuralGuard-AASIST++ result is the main result to report.

Baseline decision {html.escape(str(base_decision))}
Final decision {html.escape(str(final_decision))}
Fake probability change {diff:.4f}
""" def quality_panel(quality: dict) -> str: metrics = quality.get("metrics", {}) or {} warnings = quality.get("warnings", []) or [] metric_html = "" for key, value in metrics.items(): label = html.escape(str(key).replace("_", " ").title()) val = f"{value:.4f}" if isinstance(value, float) else str(value) metric_html += f"
{label}{html.escape(val)}
" if not metric_html: metric_html = "

No audio-quality metrics available.

" if warnings: warnings_html = "".join([f"
  • {html.escape(str(w))}
  • " for w in warnings]) warning_block = f"""
    Reliability warnings
    """ else: warning_block = """
    No major audio-quality warnings detected.
    """ return f"""

    Audio Quality Diagnostics

    {metric_html}
    {warning_block}
    """ 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 = "
    Upload an audio file to begin analysis.
    " return empty, empty, empty, empty, "Upload an audio file to generate an explanation.", [], {}, empty, "" quality = safe_quality_report(audio_path, args.sample_rate) raw_final = run_auralguard( audio_path, final_model, sample_rate=args.sample_rate, duration_sec=args.duration_sec, device=device, ) final_report = safe_make_report(raw_final, quality) final_report["beginner_explanation"] = simple_explanation(final_report) final_report["quality_report"] = quality if baseline_model is not None: raw_base = run_auralguard( audio_path, baseline_model, sample_rate=args.sample_rate, duration_sec=args.duration_sec, device=device, ) baseline_report = safe_make_report(raw_base, quality) else: baseline_report = { "decision": "not loaded", "fake_probability": 0.0, "attack_type": "not loaded", "review_required": False, "suspicious_segments": [], "evidence": {}, } combined = { "baseline_comparison_only": baseline_report, "final_auralguard_result": final_report, "quality_report": quality, } report_path = Path("results") / "demo_clear_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 ( final_decision_banner(final_report, args.final_name), html_card(args.final_name, final_report, "Main forensic decision", is_final=True), html_card(args.baseline_name, baseline_report, "Older model for comparison", is_final=False), comparison_panel(baseline_report, final_report), final_report.get("beginner_explanation", ""), final_report.get("suspicious_segments", []), final_report.get("evidence", {}), quality_panel(quality), str(report_path), ) css = """ :root { --bg-1: #07111f; --bg-2: #0b1b32; --panel: rgba(15, 28, 50, 0.86); --panel-2: rgba(20, 37, 65, 0.94); --border: rgba(120, 180, 255, 0.24); --text: #f2f7ff; --muted: #adc0dc; --blue: #4da3ff; --cyan: #28e0e8; --green: #39d98a; --yellow: #ffd166; --red: #ff5c7a; } body, .gradio-container { background: radial-gradient(circle at top left, rgba(40, 224, 232, 0.18), transparent 32%), radial-gradient(circle at top right, rgba(77, 163, 255, 0.18), transparent 35%), linear-gradient(135deg, var(--bg-1), var(--bg-2)) !important; color: var(--text) !important; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif !important; } .gradio-container { max-width: 1260px !important; } .main-header { border: 1px solid var(--border); background: linear-gradient(135deg, rgba(20, 44, 77, 0.92), rgba(8, 18, 33, 0.96)); border-radius: 24px; padding: 28px 30px; box-shadow: 0 22px 70px rgba(0, 0, 0, 0.35); margin-bottom: 18px; } .eyebrow { color: var(--cyan); font-size: 13px; letter-spacing: 0.16em; text-transform: uppercase; font-weight: 900; margin-bottom: 10px; } .main-title { font-size: 42px; line-height: 1.05; margin: 0; font-weight: 950; color: var(--text); } .subtitle { font-size: 16px; color: var(--muted); margin-top: 12px; max-width: 850px; } .feature-row { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 18px; } .feature-chip { border: 1px solid rgba(77, 163, 255, 0.24); background: rgba(77, 163, 255, 0.10); color: #d7e9ff; border-radius: 999px; padding: 7px 11px; font-size: 13px; font-weight: 800; } .final-banner { border-radius: 24px; padding: 24px; margin-bottom: 16px; border: 1px solid var(--border); box-shadow: 0 22px 60px rgba(0, 0, 0, 0.25); background: linear-gradient(135deg, rgba(20,37,65,0.96), rgba(7,17,31,0.96)); } .final-banner.real { border-color: rgba(57, 217, 138, 0.55); } .final-banner.fake { border-color: rgba(255, 92, 122, 0.58); } .final-banner.review { border-color: rgba(255, 209, 102, 0.55); } .final-label { color: var(--cyan); font-size: 12px; font-weight: 950; letter-spacing: 0.16em; text-transform: uppercase; } .final-decision { font-size: 44px; font-weight: 950; margin-top: 6px; letter-spacing: -0.03em; } .final-banner.real .final-decision { color: var(--green); } .final-banner.fake .final-decision { color: var(--red); } .final-banner.review .final-decision { color: var(--yellow); } .final-subtitle { color: var(--muted); margin-top: 8px; font-size: 15px; } .final-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin-top: 18px; } .final-grid div { background: rgba(255,255,255,0.055); border: 1px solid rgba(255,255,255,0.08); border-radius: 16px; padding: 14px; } .final-grid span { display: block; color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: 0.08em; font-weight: 850; } .final-grid b { display: block; margin-top: 6px; font-size: 24px; color: #fff; } .panel { border: 1px solid var(--border) !important; background: var(--panel) !important; border-radius: 20px !important; box-shadow: 0 18px 45px rgba(0,0,0,0.22) !important; padding: 14px !important; } .result-card, .comparison-card, .quality-card { border: 1px solid var(--border); background: var(--panel-2); border-radius: 20px; padding: 18px; box-shadow: 0 18px 45px rgba(0,0,0,0.22); color: var(--text); } .final-card { border-color: rgba(40, 224, 232, 0.45); } .baseline-card { opacity: 0.88; border-style: dashed; } .result-card.real { border-color: rgba(57, 217, 138, 0.45); } .result-card.fake { border-color: rgba(255, 92, 122, 0.52); } .result-card.review { border-color: rgba(255, 209, 102, 0.50); } .role-tag { display: inline-block; border-radius: 999px; padding: 5px 9px; font-size: 11px; font-weight: 950; letter-spacing: 0.09em; margin-bottom: 8px; } .final-tag { background: rgba(40, 224, 232, 0.15); color: var(--cyan); } .baseline-tag { background: rgba(255, 255, 255, 0.09); color: var(--muted); } .card-topline { display: flex; justify-content: space-between; align-items: flex-start; gap: 14px; margin-bottom: 8px; } .model-name { font-size: 18px; font-weight: 950; } .card-subtitle { color: var(--muted); font-size: 13px; margin-bottom: 16px; } .status-pill { border-radius: 999px; padding: 6px 10px; font-size: 12px; font-weight: 950; letter-spacing: 0.08em; white-space: nowrap; } .status-pill.real { background: rgba(57, 217, 138, 0.14); color: var(--green); } .status-pill.fake { background: rgba(255, 92, 122, 0.14); color: var(--red); } .status-pill.review { background: rgba(255, 209, 102, 0.16); color: var(--yellow); } .probability-row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 14px; } .metric-label, .small-label { color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: 0.08em; font-weight: 850; } .probability-value { font-size: 34px; font-weight: 950; color: #ffffff; } .attack-value { font-size: 20px; font-weight: 900; color: #dbeafe; margin-top: 6px; } .score-track { height: 10px; border-radius: 999px; background: rgba(255,255,255,0.10); overflow: hidden; margin: 12px 0 16px; } .score-fill { height: 100%; border-radius: 999px; transition: width 0.5s ease; } .score-fill.real { background: linear-gradient(90deg, var(--green), var(--cyan)); } .score-fill.fake { background: linear-gradient(90deg, #ff8a9f, var(--red)); } .score-fill.review { background: linear-gradient(90deg, var(--yellow), #ff9f1c); } .card-footer { display: flex; justify-content: space-between; gap: 12px; color: var(--muted); font-size: 13px; } .comparison-card h3 { margin: 8px 0 6px; font-size: 24px; } .comparison-card p { color: var(--muted); } .comparison-badge { display: inline-block; border-radius: 999px; padding: 6px 10px; background: rgba(77, 163, 255, 0.14); color: #b9dcff; font-weight: 950; font-size: 12px; letter-spacing: 0.08em; text-transform: uppercase; } .comparison-grid, .quality-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin-top: 14px; } .comparison-grid div, .quality-metric { background: rgba(255,255,255,0.055); border: 1px solid rgba(255,255,255,0.08); border-radius: 14px; padding: 12px; } .comparison-grid b { display: block; margin-top: 5px; font-size: 20px; } .quality-card h3 { margin-top: 0; } .quality-metric span { display: block; color: var(--muted); font-size: 12px; } .quality-metric b { display: block; margin-top: 4px; font-size: 18px; } .warning-box { border: 1px solid rgba(255, 209, 102, 0.45); background: rgba(255, 209, 102, 0.10); border-radius: 14px; padding: 12px; color: #ffe8a3; } .ok-box { border: 1px solid rgba(57, 217, 138, 0.35); background: rgba(57, 217, 138, 0.10); border-radius: 14px; padding: 12px; color: #b7f7d4; } .empty-state { border: 1px dashed rgba(159, 180, 208, 0.40); border-radius: 18px; padding: 18px; color: var(--muted); background: rgba(255,255,255,0.035); } textarea, input, .gradio-dropdown, .gradio-textbox { background: rgba(255,255,255,0.05) !important; color: var(--text) !important; border-color: rgba(120, 180, 255, 0.22) !important; } .prose, .markdown, label, .wrap, .json-holder { color: var(--text) !important; } button.primary { background: linear-gradient(90deg, var(--blue), var(--cyan)) !important; color: #03101f !important; border: none !important; font-weight: 950 !important; border-radius: 14px !important; box-shadow: 0 12px 35px rgba(40, 224, 232, 0.25) !important; } """ with gr.Blocks(css=css, title="AuralGuard-AASIST++ Dashboard", theme=gr.themes.Soft()) as demo: gr.HTML( """
    Forensic Audio Intelligence

    AuralGuard-AASIST++

    Robust and explainable audio deepfake detection for synthetic speech, accented English, and interview-style recordings.
    Final Result First Baseline Comparison Evidence Packet Audio Quality Diagnostics Suspicious Timestamps
    """ ) final_banner = gr.HTML() with gr.Row(): with gr.Column(scale=1, elem_classes=["panel"]): gr.Markdown("### Upload Audio") audio_input = gr.Audio(type="filepath", label="Audio file") analyze_btn = gr.Button("Run Forensic Analysis", variant="primary") gr.Markdown( """ The **Final AuralGuard Result** is the main result. The baseline is shown only for comparison. """ ) with gr.Column(scale=2): final_card = gr.HTML() baseline_card = gr.HTML() with gr.Tab("Model Comparison"): comparison = gr.HTML() with gr.Tab("Beginner Explanation"): beginner_explanation = gr.Textbox(label="Simple explanation", lines=8) with gr.Tab("Suspicious Timestamp Evidence"): suspicious_segments = gr.JSON(label="Suspicious timestamp regions") with gr.Tab("Evidence Packet"): evidence_packet = gr.JSON(label="Final model evidence packet") with gr.Tab("Audio Quality Diagnostics"): quality_report = gr.HTML() with gr.Tab("Export"): export_path = gr.Textbox(label="Saved JSON report path") analyze_btn.click( fn=analyze, inputs=audio_input, outputs=[ final_banner, final_card, baseline_card, comparison, beginner_explanation, suspicious_segments, evidence_packet, quality_report, export_path, ], ) gr.HTML( """
    This dashboard provides forensic decision support, not legal proof. Uncertain, noisy, or out-of-domain audio should be reviewed by a human.
    """ ) demo.launch(share=args.share, inbrowser=True, show_error=True) if __name__ == "__main__": main()