Spaces:
Sleeping
Sleeping
| """ | |
| 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) | |