fakeshield-api / generate_audio_lab_pdf.py
Akash4911's picture
Production Deploy: Improved robustness and logging
66b6851
Raw
History Blame Contribute Delete
15.5 kB
import os
import re
import subprocess
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import cm
from reportlab.lib import colors
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table,
TableStyle, HRFlowable, PageBreak, Image, KeepTogether
)
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_JUSTIFY
# Paths
DOCS_DIR = r"C:\Users\office\Documents\Final_year_project\fakeshield\docs"
MD_FILE = os.path.join(DOCS_DIR, "audio_lab_documentation.md")
PDF_FILE = os.path.join(DOCS_DIR, "AI_Audio_Lab_Technical_Specs.pdf")
MMDC_PATH = r"C:\Users\office\AppData\Roaming\npm\mmdc.ps1"
# Extract and compile Mermaid diagrams
print("Extracting Mermaid blocks from markdown...")
with open(MD_FILE, "r", encoding="utf-8") as f:
md_content = f.read()
mermaid_pattern = r"```mermaid\n(.*?)\n```"
mermaid_blocks = re.findall(mermaid_pattern, md_content, re.DOTALL)
png_diagrams = []
for idx, mmd_code in enumerate(mermaid_blocks):
temp_mmd = os.path.join(DOCS_DIR, f"temp_diag_{idx}.mmd")
out_png = os.path.join(DOCS_DIR, f"audio_lab_diagram_{idx}.png")
with open(temp_mmd, "w", encoding="utf-8") as f:
f.write(mmd_code)
print(f"Compiling Mermaid diagram {idx} to PNG...")
try:
# Run mmdc using powershell
cmd = ["powershell", "-File", MMDC_PATH, "-i", temp_mmd, "-o", out_png, "-t", "dark", "-b", "#0f172a"]
subprocess.run(cmd, check=True)
png_diagrams.append(out_png)
except Exception as e:
print(f"Error compiling diagram {idx}: {e}")
# If compilation fails, check if we can fall back to standard styling or keep going
png_diagrams.append(None)
finally:
if os.path.exists(temp_mmd):
os.remove(temp_mmd)
# Set up ReportLab Document
doc = SimpleDocTemplate(
PDF_FILE, pagesize=A4,
rightMargin=1.5*cm, leftMargin=1.5*cm,
topMargin=2.0*cm, bottomMargin=2.0*cm
)
# Color Scheme
DARK = colors.HexColor("#0F172A") # Primary slate
CYAN = colors.HexColor("#00E5CC") # Accent cyan
INDIGO = colors.HexColor("#6366F1") # Accent indigo
GRAY = colors.HexColor("#64748B") # Muted text
LIGHT_BG = colors.HexColor("#F8FAFC") # Page backgrounds
BORDER = colors.HexColor("#E2E8F0") # Light border
ALERT_BG = colors.HexColor("#EFF6FF") # Alert callout bg
ALERT_BORDER = colors.HexColor("#3B82F6") # Alert callout border
styles = getSampleStyleSheet()
# Custom Paragraph Styles
styles.add(ParagraphStyle(
"DocTitle",
parent=styles["Normal"],
fontName="Helvetica-Bold",
fontSize=22,
leading=26,
textColor=DARK,
spaceAfter=15,
alignment=TA_LEFT
))
styles.add(ParagraphStyle(
"DocSubtitle",
parent=styles["Normal"],
fontName="Helvetica",
fontSize=10,
leading=14,
textColor=GRAY,
spaceAfter=30,
alignment=TA_LEFT
))
styles.add(ParagraphStyle(
"CustomH1",
parent=styles["Heading1"],
fontName="Helvetica-Bold",
fontSize=13,
leading=17,
textColor=DARK,
spaceBefore=22,
spaceAfter=10,
keepWithNext=True
))
styles.add(ParagraphStyle(
"CustomH2",
parent=styles["Heading2"],
fontName="Helvetica-Bold",
fontSize=11,
leading=15,
textColor=DARK,
spaceBefore=16,
spaceAfter=8,
keepWithNext=True
))
styles.add(ParagraphStyle(
"CustomH3",
parent=styles["Heading3"],
fontName="Helvetica-Bold",
fontSize=9,
leading=13,
textColor=DARK,
spaceBefore=12,
spaceAfter=6,
keepWithNext=True
))
styles.add(ParagraphStyle(
"Body",
parent=styles["Normal"],
fontName="Helvetica",
fontSize=8.5,
leading=12.5,
textColor=colors.HexColor("#334155"),
spaceAfter=8
))
styles.add(ParagraphStyle(
"ListItem",
parent=styles["Normal"],
fontName="Helvetica",
fontSize=8.5,
leading=12.5,
textColor=colors.HexColor("#334155"),
leftIndent=15,
firstLineIndent=-10,
spaceAfter=4
))
styles.add(ParagraphStyle(
"CodeBlock",
parent=styles["Normal"],
fontName="Courier",
fontSize=7.5,
leading=10.5,
textColor=colors.HexColor("#0F172A"),
backColor=colors.HexColor("#F1F5F9"),
borderColor=BORDER,
borderWidth=0.5,
borderPadding=8,
spaceAfter=8,
borderRadius=4
))
styles.add(ParagraphStyle(
"AlertText",
parent=styles["Normal"],
fontName="Helvetica",
fontSize=8.5,
leading=12.5,
textColor=colors.HexColor("#1E3A8A")
))
styles.add(ParagraphStyle(
"TableText",
parent=styles["Normal"],
fontName="Helvetica",
fontSize=8,
leading=11,
textColor=colors.HexColor("#334155")
))
styles.add(ParagraphStyle(
"TableHeaderText",
parent=styles["Normal"],
fontName="Helvetica-Bold",
fontSize=8,
leading=11,
textColor=colors.white
))
def clean_markdown_formatting(text):
# Bold: **text** -> <b>text</b>
text = re.sub(r"\*\*(.*?)\*\*", r"<b>\1</b>", text)
# Italic: *text* -> <i>text</i>
text = re.sub(r"\*(.*?)\*", r"<i>\1</i>", text)
# Inline code: `code` -> <font face="Courier" size="8" color="#0F172A">code</font>
text = re.sub(r"`(.*?)`", r'<font face="Courier" size="7.5" color="#0F172A"><b>\1</b></font>', text)
# Math notation: $$Formula$$ or $Formula$ -> simplified html
text = text.replace("$$\\text{Stability} = 1.0 - \\text{min}\\left(1.0, \\frac{|\\text{Score}_{\\text{original}} - \\text{Score}_{\\text{telephony}}|}{0.40}\\right)$$".replace("\\", "\\\\"), "<b>Stability = 1.0 - min(1.0, |Score_original - Score_telephony| / 0.40)</b>")
text = text.replace("$$\\text{Stability} = 1.0 - \\text{min}\\left(1.0, \\frac{|\\text{Score}_{\\text{original}} - \\text{Score}_{\\text{telephony}}|}{0.40}\\right)$$".replace("\\", "\\"), "<b>Stability = 1.0 - min(1.0, |Score_original - Score_telephony| / 0.40)</b>")
text = text.replace("$$\\text{Stability} = 1.0 - \\text{min}\\left(1.0, \\frac{|\\text{Score}_{\\text{original}} - \\text{Score}_{\\text{telephony}}|}{0.40}\\right)$$", "<b>Stability = 1.0 - min(1.0, |Score_original - Score_telephony| / 0.40)</b>")
text = text.replace("$$\\Delta_{\\text{mel}} = \\frac{1}{T-1} \\sum_{t=1}^{T-1} |M_{t+1} - M_t|$$".replace("\\", "\\\\"), "<b>Delta_mel = (1 / (T-1)) * Sum(|M_t+1 - M_t|)</b>")
text = text.replace("$$\\Delta_{\\text{mel}} = \\frac{1}{T-1} \\sum_{t=1}^{T-1} |M_{t+1} - M_t|$$", "<b>Delta_mel = (1 / (T-1)) * Sum(|M_t+1 - M_t|)</b>")
text = text.replace("$$\\text{DC Offset} = \\left| \\frac{1}{N} \\sum_{i=1}^N x[i] \\right|$$".replace("\\", "\\\\"), "<b>DC Offset = | (1/N) * Sum(x[i]) |</b>")
text = text.replace("$$\\text{DC Offset} = \\left| \\frac{1}{N} \\sum_{i=1}^N x[i] \\right|$$", "<b>DC Offset = | (1/N) * Sum(x[i]) |</b>")
text = text.replace("$$\\text{Std}(F_0)$$".replace("\\", "\\\\"), "<b>Std(F0)</b>")
text = text.replace("$$\\text{Std}(F_0)$$", "<b>Std(F0)</b>")
text = text.replace("$$\\text{semitones}$$", "<b>semitones</b>")
text = text.replace("$$\\text{dB}$$", "<b>dB</b>")
text = text.replace("$", "")
return text
# Build Story Flow
story = []
# Title Banner
header_table = Table([
[
Paragraph("<b>FAKESHIELD FORENSIC LABORATORIES</b>", ParagraphStyle("HeaderL", parent=styles["Normal"], fontSize=9, textColor=GRAY, fontName="Helvetica-Bold")),
Paragraph("TECHNICAL SPECIFICATION REPORT", ParagraphStyle("HeaderR", parent=styles["Normal"], fontSize=9, textColor=GRAY, fontName="Helvetica-Bold", alignment=2))
]
], colWidths=[9*cm, 9*cm])
header_table.setStyle(TableStyle([
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
("BOTTOMPADDING", (0,0), (-1,-1), 4),
("TOPPADDING", (0,0), (-1,-1), 4),
]))
story.append(header_table)
story.append(HRFlowable(width="100%", color=DARK, thickness=1.5, spaceAfter=15))
# Document Title
story.append(Paragraph("🎙️ AI Audio Forensic Lab (Process 4.0)", styles["DocTitle"]))
story.append(Paragraph("System Architecture, Forensic Models, Multi-Phase Fusion, and Integration Specifications", styles["DocSubtitle"]))
# Parse markdown sections manually to create flows
lines = md_content.split("\n")
i = 0
in_list = False
in_table = False
table_headers = []
table_rows = []
in_code = False
code_text = []
# Tracker for mermaid diagrams (since we stripped them out and compiled them)
diag_idx = 0
while i < len(lines):
line = lines[i].strip()
# Handle Code Blocks
if line.startswith("```"):
if line.startswith("```mermaid"):
# Skip mermaid block since we render it separately as an Image flowable
# Just advance until the closing block
i += 1
while i < len(lines) and not lines[i].strip().startswith("```"):
i += 1
# Place Compiled Diagram Image
if diag_idx < len(png_diagrams) and png_diagrams[diag_idx]:
story.append(Spacer(1, 0.4*cm))
img_path = png_diagrams[diag_idx]
# Scale image to fit A4 width nicely (max ~17cm width)
# Keep aspect ratio
story.append(KeepTogether([
Paragraph(f"<b>Figure {diag_idx + 1}: {'Audio Lab Processing Workflow' if diag_idx == 0 else 'Hierarchical Fusion Decision Tree'}</b>",
ParagraphStyle("FigCap", parent=styles["Normal"], fontName="Helvetica-Bold", fontSize=8, textColor=DARK, spaceAfter=8, alignment=TA_CENTER)),
Image(img_path, width=17*cm, height=9.5*cm),
Spacer(1, 0.5*cm)
]))
diag_idx += 1
i += 1
continue
elif in_code:
in_code = False
# Render Code block
code_content = "\n".join(code_text)
story.append(Paragraph(code_content.replace("\n", "<br/>").replace(" ", "&nbsp;"), styles["CodeBlock"]))
code_text = []
else:
in_code = True
i += 1
continue
if in_code:
code_text.append(line)
i += 1
continue
# Skip empty lines
if not line:
i += 1
continue
# Handle Headers
if line.startswith("# "):
# Document title is handled separately, skip main H1
i += 1
continue
elif line.startswith("## "):
story.append(Spacer(1, 0.3*cm))
header_text = clean_markdown_formatting(line[3:])
story.append(Paragraph(header_text, styles["CustomH1"]))
story.append(HRFlowable(width="100%", color=BORDER, thickness=0.8, spaceAfter=8))
i += 1
continue
elif line.startswith("### "):
header_text = clean_markdown_formatting(line[4:])
story.append(Paragraph(header_text, styles["CustomH2"]))
i += 1
continue
elif line.startswith("#### "):
header_text = clean_markdown_formatting(line[5:])
story.append(Paragraph(header_text, styles["CustomH3"]))
i += 1
continue
# Handle Bullet Lists
if line.startswith("* ") or line.startswith("- "):
list_text = clean_markdown_formatting(line[2:])
story.append(Paragraph(f"• {list_text}", styles["ListItem"]))
i += 1
continue
# Handle Numbered Lists
if re.match(r"^\d+\.\s+", line):
parts = line.split(".", 1)
num = parts[0].strip()
list_text = clean_markdown_formatting(parts[1].strip())
story.append(Paragraph(f"{num}. {list_text}", styles["ListItem"]))
i += 1
continue
# Handle Alert Blocks (GitHub style blockquotes)
if line.startswith("> [!"):
alert_type = re.search(r"\[!(.*?)\]", line).group(1)
alert_body_lines = []
i += 1
while i < len(lines) and lines[i].strip().startswith(">"):
alert_body_lines.append(lines[i].strip().lstrip(">").strip())
i += 1
alert_body = clean_markdown_formatting(" ".join(alert_body_lines))
# Render beautiful Callout Table
alert_para = Paragraph(f"<b>{alert_type.upper()}:</b> {alert_body}", styles["AlertText"])
alert_table = Table([[alert_para]], colWidths=[18*cm])
alert_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), ALERT_BG),
("BOX", (0,0), (-1,-1), 0.5, ALERT_BORDER),
("LEFTPADDING", (0,0), (-1,-1), 10),
("RIGHTPADDING", (0,0), (-1,-1), 10),
("TOPPADDING", (0,0), (-1,-1), 8),
("BOTTOMPADDING", (0,0), (-1,-1), 8),
]))
story.append(Spacer(1, 0.2*cm))
story.append(alert_table)
story.append(Spacer(1, 0.2*cm))
continue
# Handle Markdown Tables
if line.startswith("|"):
# We parse the table headers and lines
table_lines = []
while i < len(lines) and lines[i].strip().startswith("|"):
table_lines.append(lines[i].strip())
i += 1
# Parse headers
headers_raw = [h.strip() for h in table_lines[0].split("|")[1:-1]]
headers_p = [Paragraph(f"<b>{clean_markdown_formatting(h)}</b>", styles["TableHeaderText"]) for h in headers_raw]
rows_p = []
# Skip table divider (index 1)
for row_line in table_lines[2:]:
cells_raw = [c.strip() for c in row_line.split("|")[1:-1]]
cells_p = [Paragraph(clean_markdown_formatting(c), styles["TableText"]) for c in cells_raw]
rows_p.append(cells_p)
# Table layout
all_table_data = [headers_p] + rows_p
# Dynamic col widths based on column count
col_count = len(headers_raw)
if col_count == 3:
col_widths = [4.5*cm, 3.5*cm, 10.0*cm]
elif col_count == 4:
col_widths = [6.0*cm, 3.0*cm, 3.0*cm, 6.0*cm]
else:
col_widths = [18.0 / col_count * cm] * col_count
p_table = Table(all_table_data, colWidths=col_widths)
p_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), DARK),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
("ROWBACKGROUNDS", (0,1), (-1,-1), [colors.white, LIGHT_BG]),
("GRID", (0,0), (-1,-1), 0.5, BORDER),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 6),
("RIGHTPADDING", (0,0), (-1,-1), 6),
]))
story.append(Spacer(1, 0.2*cm))
story.append(p_table)
story.append(Spacer(1, 0.3*cm))
continue
# Regular Paragraph
p_text = clean_markdown_formatting(line)
story.append(Paragraph(p_text, styles["Body"]))
i += 1
# Page Numbering & Footer Callback
def add_footer(canvas, doc):
canvas.saveState()
canvas.setStrokeColor(BORDER)
canvas.setLineWidth(0.5)
canvas.line(1.5*cm, 1.5*cm, 19.5*cm, 1.5*cm)
canvas.setFont("Helvetica", 7.5)
canvas.setFillColor(GRAY)
canvas.drawString(1.5*cm, 1.1*cm, "FakeShield Automated Intelligence Systems — AI Audio Lab v3.2.1-PRO")
canvas.drawRightString(19.5*cm, 1.1*cm, f"Page {doc.page}")
canvas.restoreState()
# Build PDF
print(f"Building document to {PDF_FILE}...")
doc.build(story, onFirstPage=add_footer, onLaterPages=add_footer)
print("PDF compilation complete!")
# Cleanup diagram PNGs (if desired)
# for path in png_diagrams:
# if path and os.path.exists(path):
# os.remove(path)