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="Polished 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 model gives this audio a low fake score, so it appears closer to real human speech " "than to generated or manipulated speech." ) if "fake" in decision.lower(): return ( "The model gives this audio a high fake score and found patterns similar to generated " "or manipulated speech examples." ) return ( "The model is not fully confident. The safest interpretation is to treat this clip as requiring human review." ) 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 html_card(model_name: str, report: dict, subtitle: 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) bar_width = max(0.0, min(100.0, fake_prob * 100.0)) escaped_model = html.escape(model_name) escaped_attack = html.escape(attack_type) escaped_subtitle = html.escape(subtitle) return f"""
{escaped_model} {label}
{escaped_subtitle}
Fake probability
{fake_prob:.4f}
Attack-type clue
{escaped_attack}
""" 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 if diff > 0.20: headline = "Final model is less suspicious of this clip" detail = "This suggests the final model may be reducing unnecessary false alarms." badge = "Reduced fake score" cls = "positive" elif diff < -0.20: headline = "Final model is more suspicious of this clip" detail = "This suggests the final model found stronger fake-like evidence than the baseline." badge = "Higher fake score" cls = "caution" else: headline = "Both models give similar confidence" detail = "The fake probabilities are close, so the models mostly agree." badge = "Similar confidence" cls = "neutral" return f"""
{badge}

{headline}

{detail}

Baseline fake probability {base_prob:.4f}
Final fake probability {final_prob:.4f}
Difference {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()) if isinstance(value, float): val = f"{value:.4f}" else: val = 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: return ( "
    Upload an audio file to begin analysis.
    ", "
    No final model result yet.
    ", "
    No comparison yet.
    ", "Upload an audio file to generate an explanation.", [], {}, "
    No audio-quality report yet.
    ", "", ) 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["model_name"] = args.final_name 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) baseline_report["model_name"] = args.baseline_name baseline_report["beginner_explanation"] = simple_explanation(baseline_report) baseline_report["quality_report"] = quality else: baseline_report = { "decision": "not loaded", "fake_probability": 0.0, "attack_type": "not loaded", "review_required": False, "suspicious_segments": [], "evidence": {}, "beginner_explanation": "No baseline checkpoint was provided.", "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_technical_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 ( html_card(args.baseline_name, baseline_report, "Reference model output"), html_card(args.final_name, final_report, "Robust final model output"), 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.82); --panel-2: rgba(20, 37, 65, 0.92); --border: rgba(120, 180, 255, 0.22); --text: #eaf2ff; --muted: #9fb4d0; --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: 1250px !important; } .main-header { position: relative; overflow: hidden; border: 1px solid var(--border); background: linear-gradient(135deg, rgba(20, 44, 77, 0.88), rgba(8, 18, 33, 0.94)); border-radius: 24px; padding: 28px 30px; box-shadow: 0 22px 70px rgba(0, 0, 0, 0.35); margin-bottom: 18px; } .main-header:before { content: ""; position: absolute; inset: -2px; background: linear-gradient(90deg, transparent, rgba(40, 224, 232, 0.18), transparent); transform: translateX(-70%); animation: sweep 6s infinite; } @keyframes sweep { 0% { transform: translateX(-70%); } 55% { transform: translateX(70%); } 100% { transform: translateX(70%); } } .header-content { position: relative; z-index: 2; } .eyebrow { color: var(--cyan); font-size: 13px; letter-spacing: 0.16em; text-transform: uppercase; font-weight: 800; margin-bottom: 10px; } .main-title { font-size: 42px; line-height: 1.05; margin: 0; font-weight: 900; color: var(--text); } .subtitle { font-size: 16px; color: var(--muted); margin-top: 12px; max-width: 820px; } .feature-row { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 18px; } .feature-chip { border: 1px solid rgba(77, 163, 255, 0.22); background: rgba(77, 163, 255, 0.10); color: #d7e9ff; border-radius: 999px; padding: 7px 11px; font-size: 13px; font-weight: 700; } .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; } .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); } .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); } .card-topline { display: flex; justify-content: space-between; align-items: center; gap: 14px; margin-bottom: 8px; } .model-name { font-size: 18px; font-weight: 900; } .card-subtitle { color: var(--muted); font-size: 13px; margin-bottom: 16px; } .status-pill { border-radius: 999px; padding: 6px 10px; font-size: 12px; font-weight: 900; letter-spacing: 0.08em; } .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: 800; } .probability-value { font-size: 34px; font-weight: 950; color: #ffffff; } .attack-value { font-size: 20px; font-weight: 850; 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: 900; font-size: 12px; letter-spacing: 0.08em; text-transform: uppercase; } .comparison-card.positive { border-color: rgba(57, 217, 138, 0.45); } .comparison-card.caution { border-color: rgba(255, 209, 102, 0.5); } .comparison-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: 22px; } .quality-card h3 { margin-top: 0; } .quality-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-bottom: 14px; } .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: 900 !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++ Forensic 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.
    Fake / Real / Human Review Baseline vs Final Model Evidence Packet Audio Quality Diagnostics Suspicious Timestamps
    """ ) 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( """ **Output includes:** fake probability, decision gate, attack-type clue, reliability warnings, timestamp evidence, and a JSON report. """ ) with gr.Column(scale=2): with gr.Row(): baseline_card = gr.HTML() final_card = gr.HTML() with gr.Tab("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=[ baseline_card, final_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()