import os import glob import re import fitz import shutil import subprocess from datetime import datetime from markdown_pdf import MarkdownPdf, Section # List of key technical terms to search and index across all documents KEY_TERMS = [ "AI", "MySQL", "Zabbix", "Docker", "Streamlit", "Nginx", "Airflow", "RAG", "Allergens", "Vitamins", "Minerals", "Clinical", "WSL", "Ollama", "LLM", "Database", "Security", "Telemetry", "Backup", "Firewall", "SMTP" ] def clean_header_text(text): # Remove markdown links: [text](url) -> text text = re.sub(r'\[([^\]]+)\]\([^)]+\)', r'\1', text) # Remove bold/italic/code formatting text = re.sub(r'[*_`]', '', text) return text.strip() def extract_headers(md_content): headers = [] first_h1 = True for line in md_content.splitlines(): if line.startswith('#'): match = re.match(r'^(#{1,3})\s+(.+)$', line) if match: level = len(match.group(1)) t = match.group(2).strip() # Skip the document main title (first H1) if level == 1 and first_h1: first_h1 = False continue headers.append((level, t)) return headers def generate_references(md_content): links = re.findall(r'\[([^\]]+)\]\((https?://[^\)]+)\)', md_content) lines = ["\n\n## References\n"] if links: seen = set() count = 1 for text, url in links: if url not in seen: seen.add(url) clean_text = re.sub(r'[*_`]', '', text) lines.append(f"{count}. **{clean_text}**: {url}") count += 1 if count > 10: break else: lines.append("1. **OpenFoodFacts Dataset & API Catalog**: Detailed food ingredients database. (https://world.openfoodfacts.org/)") lines.append("2. **Ollama Local LLM Inference Engine**: Lightweight instruction-following llama3.2 runtimes. (https://ollama.com/)") lines.append("3. **Zabbix Enterprise Telemetry and Monitoring**: System health and performance logging. (https://www.zabbix.com/)") return "\n".join(lines) def sanitize_name(name): if not name: return "Francois Lange" name_lower = name.lower() if "fran" in name_lower or "lange" in name_lower or "lanfr" in name_lower: return "Francois Lange" return name def get_git_info_for_file(file_path): try: cmd = [ "git", "log", "-1", "--date=format:%Y/%m/%d %H:%M:%S", "--format=%an|%ae|%ad|%cn|%ce|%cd|%H|%D|%N", "--", file_path ] out = subprocess.check_output(cmd, stderr=subprocess.DEVNULL).decode('utf-8', errors='ignore').strip() if out: parts = out.split('|') if len(parts) == 9: parts[0] = sanitize_name(parts[0]) parts[3] = sanitize_name(parts[3]) return parts except Exception: pass author_name = "Francois Lange" try: author_email = subprocess.check_output(["git", "config", "user.email"], stderr=subprocess.DEVNULL).decode('utf-8', errors='ignore').strip() or "lanfr144@school.lu" except Exception: author_email = "lanfr144@school.lu" now_str = datetime.now().strftime("%Y/%m/%d %H:%M:%S") return [author_name, author_email, now_str, author_name, author_email, now_str, "Not Committed Yet", "local", "none"] def compile_pdf_document(md_file, title, md_content, toc_content, index_content, references_content, root_dir, user_css): pdf = MarkdownPdf(optimize=True, plugins={"mermaid": {}}) base_name = os.path.basename(md_file) # 1. Section 1: Cover Page cover_html = f"""

{title}

Project Deliverable

Course Class:B1AI
Author:François LANGE
Date:2026/06/19
""" pdf.add_section(Section(cover_html, paper_size="A4", root=root_dir, borders=(36, 60, -36, -36)), user_css=user_css) # 2. Section 2: Blank Page blank_content = "
 
" pdf.add_section(Section(blank_content, paper_size="A4", root=root_dir, borders=(36, 60, -36, -36)), user_css=user_css) # 3. Section 3: Printed Table of Contents pdf.add_section(Section(toc_content, paper_size="A4", root=root_dir, borders=(36, 60, -36, -36)), user_css=user_css) # 4. Section 4: Main Content (with potential landscape splitting for project_report) if base_name == 'project_report.md': parts = md_content.split('## 2. Project File Catalog & Documentation') if len(parts) == 2: portrait_part1 = parts[0] remaining = parts[1] parts2 = remaining.split('## 3. Directory Structure Map') if len(parts2) == 2: landscape_part = '## 2. Project File Catalog & Documentation' + parts2[0] portrait_part2 = '## 3. Directory Structure Map' + parts2[1] pdf.add_section(Section(portrait_part1, paper_size="A4", root=root_dir, borders=(36, 60, -36, -36)), user_css=user_css) pdf.add_section(Section(landscape_part, paper_size="A4-L", root=root_dir, borders=(36, 60, -36, -36)), user_css=user_css) pdf.add_section(Section(portrait_part2 + "\n" + references_content, paper_size="A4", root=root_dir, borders=(36, 60, -36, -36)), user_css=user_css) else: pdf.add_section(Section(md_content + "\n" + references_content, paper_size="A4", root=root_dir, borders=(36, 60, -36, -36)), user_css=user_css) else: pdf.add_section(Section(md_content + "\n" + references_content, paper_size="A4", root=root_dir, borders=(36, 60, -36, -36)), user_css=user_css) else: pdf.add_section(Section(md_content + "\n" + references_content, paper_size="A4", root=root_dir, borders=(36, 60, -36, -36)), user_css=user_css) # 5. Section 5: Printed Index pdf.add_section(Section(index_content, paper_size="A4", root=root_dir, borders=(36, 60, -36, -36)), user_css=user_css) return pdf def generate_html_portal(root_dir, md_files): portal_path = os.path.join(root_dir, 'index.html') doc_metadata = { "technical_document": "Core technical specification details of the local clinical Food AI including system architecture and data models.", "distributed_deployment": "Installation topology guide for deploying the application stack across multiple hypervisors (WSL, Hyper-V, VirtualBox) or physical servers.", "disaster_recovery_plan": "Recovery policies, backup strategies, and restore drills to ensure zero data loss during infrastructure outages.", "Uninstall_Guide": "Clean uninstallation workflow to remove all database, cache, airflow, and container resources from physical hosts.", "Operator_Installation_Guide": "Naked OS installation guide covering package installations, python dependencies, and native system configs.", "project_report": "Comprehensive sprint report detailing system catalog, directory mappings, and final scrum delivery notes.", "retro_planning": "Timeline planning showing daily milestones, task allocations, and sprint scheduling sheets.", "User_Guide": "User manual outlining Streamlit login, meal planning, plate building, and chat assistant instructions.", "zabbix_monitoring": "SNMP trap configurations, alert channels, and Zabbix server integration mappings.", "Global_Index": "Unified global term index cross-referencing keywords across all documentation files." } cards_html = [] for md_file in sorted(md_files): base = os.path.basename(md_file).replace('.md', '') if "Header_Footer" in base: continue pdf_rel_path = f"docs/{base}.pdf" md_rel_path = f"docs/{base}.md" title = base.replace('_', ' ').replace('-', ' ').title() if base == "project_report": pdf_rel_path = "Project.pdf" title = "Project Report" elif base == "retro_planning": pdf_rel_path = "Retro Planning.pdf" title = "Retro Planning" desc = doc_metadata.get(base, "Project documentation deliverable covering specifications and guides.") cards_html.append(f"""

{title}

{desc}

""") cards_str = "\n".join(cards_html) html_content = f""" Local Food AI Documentation Portal

Local Food AI

Unified Project Documentation Portal

Class Course: B1AI
Student Author: François LANGE
Academic Year: 2025/2026
{cards_str}
""" with open(portal_path, 'w', encoding='utf-8') as f: f.write(html_content) print("Created index.html portal successfully.") def main(): docs_dir = os.path.join(os.path.dirname(__file__), '..', 'docs') md_files = glob.glob(os.path.join(docs_dir, '*.md')) # Exclude Header/Footer files and previously created Global_Index if any md_files = [f for f in md_files if "Header_Footer" not in os.path.basename(f) and "Global_Index" not in os.path.basename(f)] if not md_files: print("No markdown files found in docs/") return root_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')).replace('\\', '/') user_css = """ * { color: #1a1a1a !important; } body { font-family: 'Helvetica', 'Arial', sans-serif !important; color: #1a1a1a !important; background-color: #ffffff !important; } h1, h2, h3, h4, h5, h6, h1 *, h2 *, h3 *, h4 *, h5 *, h6 * { color: #000000 !important; margin-top: 18px !important; margin-bottom: 8px !important; } pre { background-color: #f8f9fa !important; border: 1px solid #e9ecef !important; padding: 2px !important; margin-top: 8px !important; margin-bottom: 16px !important; border-radius: 3px !important; font-family: 'Courier New', 'Courier', monospace !important; font-size: 10pt !important; white-space: pre-wrap !important; word-break: break-all !important; } pre code, pre * { font-family: 'Courier New', 'Courier', monospace !important; font-size: 10pt !important; color: #212529 !important; background-color: #f8f9fa !important; white-space: pre-wrap !important; word-break: break-all !important; } code { font-family: 'Courier New', 'Courier', monospace !important; font-size: 10pt !important; color: #b02a37 !important; background-color: #f8f9fa !important; padding: 2px 4px !important; border-radius: 3px !important; white-space: pre-wrap !important; word-break: break-all !important; } a, a * { color: #0d6efd !important; } blockquote, blockquote * { color: #555555 !important; border-left: 4px solid #ccc !important; padding-left: 10px !important; } table, tr, td, th, table * { color: #1a1a1a !important; border-color: #cccccc !important; } th { background-color: #f2f2f2 !important; } ul, li { list-style-type: disc !important; } """ global_term_index = {} # Structure: { term: [ (doc_title, pdf_path, [page_numbers]) ] } doc_data = {} # Structure: { md_file: { title, md_content, headers, references } } # ========================================== # PASS 1: Generate Draft PDFs and Find Pages # ========================================== print("=== STARTING DRAFT COMPILATION AND ANALYSIS PASS ===") for md_file in md_files: print(f"Analyzing {os.path.basename(md_file)}...") with open(md_file, 'r', encoding='utf-8') as f: md_content = f.read() # Format replacements base_name = os.path.basename(md_file) info = get_git_info_for_file(md_file) replacement = f"$Format:LocalFoodAI_lanfr144:generate_pdfs.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$" pattern = r'\$Format:LocalFoodAI_lanfr144:generate_pdfs.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$' md_content = re.sub(pattern, replacement, md_content) md_content = re.sub(r'file:///.*?/docs/([a-zA-Z0-9_-]+)\.md', r'\1.pdf', md_content, flags=re.IGNORECASE) md_content = re.sub(r'file:///.*?/Food/([a-zA-Z0-9_.-]+)', r'../\1', md_content, flags=re.IGNORECASE) md_content = re.sub(r'(```[a-zA-Z0-9_-]*\n[\s\S]*?\n```)', r'\1\n\n', md_content) md_content = re.sub(r'\]\((?!http|https)([a-zA-Z0-9_./-]+)\.md(#[a-zA-Z0-9_-]+)?\)', r'](\1.pdf\2)', md_content, flags=re.IGNORECASE) # Title parsing title_match = re.search(r'^#\s+(.+)$', md_content, re.MULTILINE) if title_match: doc_title = title_match.group(1).strip() doc_title = re.sub(r'[*_`#]', '', doc_title).strip() else: doc_title = base_name.replace('.md', '').replace('_', ' ').title() headers = extract_headers(md_content) references = generate_references(md_content) # Build placeholders for TOC and Index in the draft toc_placeholder = "# Table of Contents\n\n" for level, t in headers: indent = " " * (level - 1) toc_placeholder += f"{indent}- {t} .................................................... Page 00\n" index_placeholder = "## Index\n\n" for term in KEY_TERMS: index_placeholder += f"- **{term}**: Page 00\n" doc_data[md_file] = { "title": doc_title, "md_content": md_content, "headers": headers, "references": references } try: # Compile draft PDF pdf = compile_pdf_document( md_file, doc_title, md_content, toc_placeholder, index_placeholder, references, root_dir, user_css ) draft_pdf_file = md_file.replace('.md', '_draft.pdf') pdf.save(draft_pdf_file) pdf.temp_files.clean() # Analyze draft PDF using fitz doc = fitz.open(draft_pdf_file) # 1. Map headers to printed page numbers page_map = {} for level, raw_t in headers: clean_t = clean_header_text(raw_t) found_page = None for page_idx in range(3, len(doc)): # Main content starts on page index 3 (Page 2) page = doc[page_idx] rects = page.search_for(clean_t) if rects: found_page = page_idx - 1 # printed Page 1 is index 2 (TOC page) break if found_page is None: found_page = 2 # default fallback page_map[raw_t] = found_page # 2. Map key terms to printed page numbers term_pages = {} for term in KEY_TERMS: matches = [] for page_idx in range(2, len(doc)): # TOC page (index 2) onwards page = doc[page_idx] rects = page.search_for(term) if rects: matches.append(page_idx - 1) if matches: term_pages[term] = matches # Populate global index if term not in global_term_index: global_term_index[term] = [] global_term_index[term].append((doc_title, md_file.replace('.md', '.pdf'), matches)) doc_data[md_file]["page_map"] = page_map doc_data[md_file]["term_pages"] = term_pages doc.close() # Clean up draft PDF if os.path.exists(draft_pdf_file): os.remove(draft_pdf_file) except Exception as err: print(f"Draft analysis failed for {os.path.basename(md_file)}: {err}") # ========================================== # PASS 2: Create Global Index and Compile Finals # ========================================== print("\n=== COMPILING GLOBAL INDEX DOCUMENT ===") global_index_md_path = os.path.join(docs_dir, 'Global_Index.md') global_index_pdf_path = os.path.join(docs_dir, 'Global_Index.pdf') # Generate Global_Index.md content global_lines = [ "# Global Project Index\n", "This document provides a unified cross-reference index of all key technical terms and concepts across the LocalFoodAI documentation suite. Click on any document link to open it at the specific page in your viewer.\n", "## Index of Terms\n" ] for term in sorted(global_term_index.keys()): occurrences = global_term_index[term] global_lines.append(f"### {term}\n") for doc_title, pdf_path, pages in occurrences: pages_str = ", ".join([f"Page {p}" for p in pages]) pdf_rel = os.path.basename(pdf_path) first_page = pages[0] # Format link to open PDF on exact page index global_lines.append(f"- **{doc_title}**: {pages_str} | [Open PDF at Page {first_page}]({pdf_rel}#page={first_page + 1})") global_lines.append("") with open(global_index_md_path, 'w', encoding='utf-8') as f: f.write("\n".join(global_lines)) print("Saved docs/Global_Index.md") # Add Global_Index.md to the compilation files list md_files.append(global_index_md_path) # Generate draft analysis for Global_Index.md as well with open(global_index_md_path, 'r', encoding='utf-8') as f: global_md_content = f.read() global_headers = extract_headers(global_md_content) global_references = generate_references(global_md_content) toc_placeholder = "# Table of Contents\n\n" for level, t in global_headers: indent = " " * (level - 1) toc_placeholder += f"{indent}- {t} .................................................... Page 00\n" index_placeholder = "## Index\n\n" for term in KEY_TERMS: index_placeholder += f"- **{term}**: Page 00\n" doc_data[global_index_md_path] = { "title": "Global Project Index", "md_content": global_md_content, "headers": global_headers, "references": global_references } # Run a quick draft pass for Global Index itself to find header and index pages try: pdf = compile_pdf_document( global_index_md_path, "Global Project Index", global_md_content, toc_placeholder, index_placeholder, global_references, root_dir, user_css ) draft_global_pdf = global_index_pdf_path.replace('.pdf', '_draft.pdf') pdf.save(draft_global_pdf) pdf.temp_files.clean() doc = fitz.open(draft_global_pdf) global_page_map = {} for level, raw_t in global_headers: clean_t = clean_header_text(raw_t) found_page = None for page_idx in range(3, len(doc)): page = doc[page_idx] rects = page.search_for(clean_t) if rects: found_page = page_idx - 1 break if found_page is None: found_page = 2 global_page_map[raw_t] = found_page global_term_pages = {} for term in KEY_TERMS: matches = [] for page_idx in range(2, len(doc)): page = doc[page_idx] rects = page.search_for(term) if rects: matches.append(page_idx - 1) if matches: global_term_pages[term] = matches doc_data[global_index_md_path]["page_map"] = global_page_map doc_data[global_index_md_path]["term_pages"] = global_term_pages doc.close() if os.path.exists(draft_global_pdf): os.remove(draft_global_pdf) except Exception as err: print(f"Draft analysis failed for Global_Index.md: {err}") # ========================================== # PASS 3: Generate Final PDFs # ========================================== print("\n=== COMPILING FINAL GENERATED PDF FILES ===") for md_file in md_files: pdf_file = md_file.replace('.md', '.pdf') base_name = os.path.basename(md_file) data = doc_data.get(md_file) if not data: continue title = data["title"] md_content = data["md_content"] headers = data["headers"] references = data["references"] page_map = data.get("page_map", {}) term_pages = data.get("term_pages", {}) # 1. Construct final printed TOC toc_lines = ["# Table of Contents\n\n"] for level, t in headers: indent = " " * (level - 1) p_num = page_map.get(t, 2) toc_lines.append(f"{indent}- {t} .................................................... Page {p_num}") toc_content = "\n".join(toc_lines) # 2. Construct final printed Index index_lines = ["## Index\n\n"] has_index_terms = False for term in KEY_TERMS: if term in term_pages: has_index_terms = True pages_str = ", ".join([f"Page {p}" for p in term_pages[term]]) index_lines.append(f"- **{term}**: {pages_str}") if not has_index_terms: index_lines.append("- No key terms found in this document.") index_content = "\n".join(index_lines) try: pdf = compile_pdf_document( md_file, title, md_content, toc_content, index_content, references, root_dir, user_css ) doc = pdf._make_doc() total_pages = len(doc) am_path = os.path.join(docs_dir, 'am.png') bts_path = os.path.join(docs_dir, 'Bts.png') width_am, height_am = 146.5, 24.5 if os.path.exists(am_path): try: pix_am = fitz.Pixmap(am_path) width_am = pix_am.width * 0.1 height_am = pix_am.height * 0.1 except Exception: pass width_bts, height_bts = 67.1, 36.8 if os.path.exists(bts_path): try: pix_bts = fitz.Pixmap(bts_path) width_bts = pix_bts.width * 0.1 height_bts = pix_bts.height * 0.1 except Exception: pass for page_idx in range(total_pages): # COVER & BLANK page should not contain headers, footers, or logos if page_idx in [0, 1]: continue page = doc[page_idx] width = page.rect.width height = page.rect.height # Header layout if os.path.exists(am_path): image_rect = fitz.Rect(48, 12, 48 + width_am, 12 + height_am) page.insert_image(image_rect, filename=am_path, overlay=False) if os.path.exists(bts_path): bts_rect = fitz.Rect(width - 48 - width_bts, 8, width - 48, 8 + height_bts) page.insert_image(bts_rect, filename=bts_path, overlay=False) # Title text fontsize = 8 fontname = "helv" m_width = fitz.get_text_length(title, fontname=fontname, fontsize=fontsize) space_start = 48 + width_am space_end = width - 48 - width_bts title_x = space_start + (space_end - space_start - m_width) / 2 page.insert_text( fitz.Point(title_x, 26), title, fontname=fontname, fontsize=fontsize, color=(0.5, 0.5, 0.5) ) # Footer layout (Page numbers start counting at TOC as Page 1) l_footer = "lanfr144" footer_text = f"Page {page_idx - 1} of {total_pages - 2}" f_width = fitz.get_text_length(footer_text, fontname=fontname, fontsize=fontsize) page.insert_text( fitz.Point(48, height - 22), l_footer, fontname=fontname, fontsize=fontsize, color=(0.5, 0.5, 0.5) ) page.insert_text( fitz.Point((width - f_width) / 2, height - 22), footer_text, fontname=fontname, fontsize=fontsize, color=(0.5, 0.5, 0.5) ) r_footer = "DOPRO1" r_footer_width = fitz.get_text_length(r_footer, fontname=fontname, fontsize=fontsize) page.insert_text( fitz.Point(width - 48 - r_footer_width, height - 22), r_footer, fontname=fontname, fontsize=fontsize, color=(0.5, 0.5, 0.5) ) doc.save(pdf_file, clean=True, garbage=3, deflate=True) doc.close() pdf.temp_files.clean() print(f"Saved {os.path.basename(pdf_file)}") # Copy to workspace root if applicable if base_name == 'project_report.md': dest = os.path.join(root_dir, 'Project.pdf') if os.path.exists(dest): os.remove(dest) shutil.copy2(pdf_file, dest) print("Successfully updated root Project.pdf") elif base_name == 'retro_planning.md': dest = os.path.join(root_dir, 'Retro Planning.pdf') if os.path.exists(dest): os.remove(dest) shutil.copy2(pdf_file, dest) print("Successfully updated root Retro Planning.pdf") except Exception as e: print(f"WARNING: Could not save {os.path.basename(pdf_file)}. File might be locked or open in a viewer. Error: {e}") # Generate HTML catalog portal in the workspace root generate_html_portal(root_dir, md_files) if __name__ == "__main__": main()