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** -> text text = re.sub(r"\*\*(.*?)\*\*", r"\1", text) # Italic: *text* -> text text = re.sub(r"\*(.*?)\*", r"\1", text) # Inline code: `code` -> code text = re.sub(r"`(.*?)`", r'\1', 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("\\", "\\\\"), "Stability = 1.0 - min(1.0, |Score_original - Score_telephony| / 0.40)") 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("\\", "\\"), "Stability = 1.0 - min(1.0, |Score_original - Score_telephony| / 0.40)") text = text.replace("$$\\text{Stability} = 1.0 - \\text{min}\\left(1.0, \\frac{|\\text{Score}_{\\text{original}} - \\text{Score}_{\\text{telephony}}|}{0.40}\\right)$$", "Stability = 1.0 - min(1.0, |Score_original - Score_telephony| / 0.40)") text = text.replace("$$\\Delta_{\\text{mel}} = \\frac{1}{T-1} \\sum_{t=1}^{T-1} |M_{t+1} - M_t|$$".replace("\\", "\\\\"), "Delta_mel = (1 / (T-1)) * Sum(|M_t+1 - M_t|)") text = text.replace("$$\\Delta_{\\text{mel}} = \\frac{1}{T-1} \\sum_{t=1}^{T-1} |M_{t+1} - M_t|$$", "Delta_mel = (1 / (T-1)) * Sum(|M_t+1 - M_t|)") text = text.replace("$$\\text{DC Offset} = \\left| \\frac{1}{N} \\sum_{i=1}^N x[i] \\right|$$".replace("\\", "\\\\"), "DC Offset = | (1/N) * Sum(x[i]) |") text = text.replace("$$\\text{DC Offset} = \\left| \\frac{1}{N} \\sum_{i=1}^N x[i] \\right|$$", "DC Offset = | (1/N) * Sum(x[i]) |") text = text.replace("$$\\text{Std}(F_0)$$".replace("\\", "\\\\"), "Std(F0)") text = text.replace("$$\\text{Std}(F_0)$$", "Std(F0)") text = text.replace("$$\\text{semitones}$$", "semitones") text = text.replace("$$\\text{dB}$$", "dB") text = text.replace("$", "") return text # Build Story Flow story = [] # Title Banner header_table = Table([ [ Paragraph("FAKESHIELD FORENSIC LABORATORIES", 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"Figure {diag_idx + 1}: {'Audio Lab Processing Workflow' if diag_idx == 0 else 'Hierarchical Fusion Decision Tree'}", 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", "
").replace(" ", " "), 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"{alert_type.upper()}: {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"{clean_markdown_formatting(h)}", 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)