File size: 6,197 Bytes
3bafd90
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4d50a3e
3bafd90
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
"""
PDF Report Generator for Audio Analysis results.
"""
from fpdf import FPDF
from datetime import datetime


def generate_pdf_report(result) -> bytes:
    """Generate a PDF report from AnalysisResult."""
    pdf = FPDF()
    pdf.set_auto_page_break(auto=True, margin=15)
    pdf.add_page()

    # Header
    pdf.set_font("Helvetica", "B", 18)
    pdf.cell(0, 12, "Audio Analysis Report", new_x="LMARGIN", new_y="NEXT", align="C")
    pdf.set_font("Helvetica", "", 10)
    pdf.cell(0, 6, f"Test ID: {result.test_id}", new_x="LMARGIN", new_y="NEXT", align="C")
    pdf.cell(0, 6, f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M')}", new_x="LMARGIN", new_y="NEXT", align="C")
    pdf.ln(8)

    # Risk Score
    score = result.risk_score
    label = result.risk_label
    r, g, b = _risk_rgb(score)
    pdf.set_fill_color(r, g, b)
    pdf.set_text_color(255, 255, 255)
    pdf.set_font("Helvetica", "B", 28)
    pdf.cell(40, 18, str(score), fill=True, align="C")
    pdf.set_font("Helvetica", "B", 14)
    pdf.cell(50, 18, f"  {label} RISK", new_x="LMARGIN", new_y="NEXT")
    pdf.set_text_color(0, 0, 0)
    pdf.ln(6)

    # Audio Summary
    _section(pdf, "Audio Summary")
    pdf.set_font("Helvetica", "", 10)
    pdf.cell(0, 6, f"File: {result.filename}", new_x="LMARGIN", new_y="NEXT")
    pdf.cell(0, 6, f"Duration: {result.duration_seconds:.1f}s", new_x="LMARGIN", new_y="NEXT")
    pdf.cell(0, 6, f"Analyzed: {result.analyzed_at}", new_x="LMARGIN", new_y="NEXT")
    pdf.ln(4)

    # Speaker Summary
    _section(pdf, "Speaker Summary")
    if result.main_speaker:
        m = result.main_speaker
        pdf.set_font("Helvetica", "", 10)
        pdf.cell(0, 6,
                 f"Main Speaker: {m.voiceprint_id} | Quality: {m.quality} | "
                 f"Synthetic: {m.synthetic_score:.0%} | Seen: {m.times_seen}x",
                 new_x="LMARGIN", new_y="NEXT")
    if result.additional_speakers:
        for i, s in enumerate(result.additional_speakers):
            pdf.cell(0, 6,
                     f"Speaker {chr(66+i)}: {s.voiceprint_id} | "
                     f"{s.total_seconds:.1f}s | Synthetic: {s.synthetic_score:.0%}",
                     new_x="LMARGIN", new_y="NEXT")
    else:
        pdf.cell(0, 6, "No additional speakers detected.", new_x="LMARGIN", new_y="NEXT")
    pdf.ln(4)

    # Detection Flags Table
    _section(pdf, "Detection Flags")
    _row(pdf, ["Flag", "Detected", "Score / Detail"], bold=True)
    synth = result.main_speaker.is_synthetic if result.main_speaker else False
    synth_s = f"{result.main_speaker.synthetic_score:.0%}" if result.main_speaker else "N/A"
    _row(pdf, ["Synthetic Voice", "Yes" if synth else "No", synth_s])
    _row(pdf, ["Playback", "Yes" if result.playback_detected else "No", f"{result.playback_score:.0%}"])
    _row(pdf, ["Reading Pattern", "Yes" if result.reading_pattern_detected else "No", f"{result.reading_confidence:.0%}"])
    _row(pdf, ["Whispers", "Yes" if result.whisper_detected else "No", f"{len(result.whisper_instances or [])} instances"])
    _row(pdf, ["Suspicious Pauses", "Yes" if result.suspicious_pauses_detected else "No",
               f"{len(result.suspicious_pauses or [])} (max {result.longest_pause:.1f}s)"])
    _row(pdf, ["Wake Words", str(len(result.wake_words or [])),
               ", ".join(w.get('word', '') for w in (result.wake_words or [])) or "None"])
    pdf.ln(4)

    # Alert Details
    has_alerts = (result.wake_words or
                  (result.whisper_instances and len(result.whisper_instances) > 0) or
                  (result.suspicious_pauses and len(result.suspicious_pauses) > 0))
    if has_alerts:
        _section(pdf, "Alert Details")
        pdf.set_font("Helvetica", "", 9)
        for ww in (result.wake_words or []):
            pdf.cell(0, 5,
                     f"  Wake Word: \"{ww.get('word', '')}\" at {ww.get('time', 0):.1f}s "
                     f"(confidence: {ww.get('confidence', 0):.0%})",
                     new_x="LMARGIN", new_y="NEXT")
        for w in (result.whisper_instances or []):
            pdf.cell(0, 5,
                     f"  Whisper: {w.get('start', 0):.1f}s - {w.get('end', 0):.1f}s "
                     f"(confidence: {w.get('confidence', 0):.0%})",
                     new_x="LMARGIN", new_y="NEXT")
        for p in (result.suspicious_pauses or []):
            pdf.cell(0, 5,
                     f"  Pause: {p.get('start', 0):.1f}s - {p.get('end', 0):.1f}s "
                     f"({p.get('duration', 0):.1f}s)",
                     new_x="LMARGIN", new_y="NEXT")
        pdf.ln(4)

    # Risk Score Breakdown
    _section(pdf, "Risk Score Breakdown")
    pdf.set_font("Helvetica", "", 10)
    if result.main_speaker:
        pdf.cell(0, 6, f"Synthetic voice: {result.main_speaker.synthetic_score*25:.0f} / 25", new_x="LMARGIN", new_y="NEXT")
    pdf.cell(0, 6, f"Playback: {result.playback_score*15:.0f} / 15", new_x="LMARGIN", new_y="NEXT")
    pdf.cell(0, 6, f"Reading pattern: {result.reading_confidence*20:.0f} / 20", new_x="LMARGIN", new_y="NEXT")
    wc = len(result.whisper_instances or [])
    pdf.cell(0, 6, f"Whispers: {min(wc,3)/3*15:.0f} / 15", new_x="LMARGIN", new_y="NEXT")
    pc = len(result.suspicious_pauses or [])
    pdf.cell(0, 6, f"Suspicious pauses: {min(pc,3)/3*10:.0f} / 10", new_x="LMARGIN", new_y="NEXT")
    wkc = len(result.wake_words or [])
    pdf.cell(0, 6, f"Wake words: {min(wkc,2)/2*10:.0f} / 10", new_x="LMARGIN", new_y="NEXT")
    pdf.ln(6)

    # Footer
    pdf.set_font("Helvetica", "I", 8)
    pdf.cell(0, 5, "Generated by Audio Analyzer PoC", align="C")

    return bytes(pdf.output())


def _section(pdf, title):
    pdf.set_font("Helvetica", "B", 12)
    pdf.set_fill_color(240, 240, 240)
    pdf.cell(0, 8, f"  {title}", fill=True, new_x="LMARGIN", new_y="NEXT")
    pdf.ln(2)


def _row(pdf, cols, bold=False):
    pdf.set_font("Helvetica", "B" if bold else "", 9)
    widths = [50, 25, 115]
    for i, col in enumerate(cols):
        pdf.cell(widths[i], 6, str(col), border=1)
    pdf.ln()


def _risk_rgb(score):
    if score <= 30:
        return (34, 197, 94)
    elif score <= 60:
        return (234, 179, 8)
    else:
        return (239, 68, 68)