import os import re import sys import shutil import base64 import argparse import logging import json from pathlib import Path # -- Third-party -- try: from PIL import Image except ImportError: print("❌ Pillow missing. Run: pip install pillow") sys.exit(1) try: import openpyxl from openpyxl import load_workbook except ImportError: print("❌ openpyxl missing. Run: pip install openpyxl") sys.exit(1) try: from reportlab.lib.pagesizes import letter, landscape from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer from reportlab.lib.styles import getSampleStyleSheet from reportlab.lib import colors except ImportError: print("❌ reportlab missing. Run: pip install reportlab") sys.exit(1) try: import google.generativeai as genai GEMINI_AVAILABLE = True except ImportError: GEMINI_AVAILABLE = False # -- Logging -- logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)-8s %(message)s", datefmt="%H:%M:%S") log = logging.getLogger(__name__) DEFAULT_BASE_DIR = Path.home() /"PycharmProjects" / "structure" / "projet" # Where projects are created DEFAULT_DEPART_DIR = Path.home() /"PycharmProjects" / "structure" / "départ" # Source folder with pole sub-folders GEMINI_API_KEY = os.environ.get("AIzaSyDq2LmX_fwKGAwxGAtmBfX940vT2wDQzBU", "AIzaSyDq2LmX_fwKGAwxGAtmBfX940vT2wDQzBU") # Set via env var GEMINI_MODEL = "models/gemini-2.5-flash" FOLDER_PATTERN = re.compile(r"^(?P\d+)(?Psd|d)(?P\d+)-(?P[^\s]+)(?:\s+(?P
.+))?$", re.IGNORECASE) def create_project_structure(project_name: str, base_dir: Path) -> Path: project_root = base_dir / project_name for folder in ["compilation", "ingénierie", "RSA-UDS", "plan"]: (project_root / folder).mkdir(parents=True, exist_ok=True) return project_root def parse_pole_folder_name(folder_name: str) -> dict | None: m = FOLDER_PATTERN.match(folder_name) if not m: return None return { "seq": int(m.group("seq")), "dero": m.group("dero").lower(), "anchors": int(m.group("anchors")), "pole_ref": m.group("pole_ref"), "address": (m.group("address") or "").strip(), "original": folder_name, } def compute_new_folder_name(parsed: dict, current_seq: int) -> str: # Always starts with the sequence provided (1, 3, 5...) if parsed["dero"] == "sd": return f"{current_seq}0-{parsed['pole_ref']}" else: addr = f" {parsed['address']}" if parsed["address"] else "" return f"{current_seq}1-{parsed['pole_ref']}{addr}" def rename_excel_files(folder_path: Path, new_folder_name: str): for f in folder_path.iterdir(): if f.suffix.lower() in (".xlsx", ".xls"): # According to your request, UDS (Excel) should be named 10- # We will handle the specific 10- prefix during the PDF conversion in process_pole_folder pass def identify_images_with_gemini(folder_path: Path) -> dict: jpg_files = sorted(list(folder_path.glob("*.jpg")) + list(folder_path.glob("*.JPG")) + list(folder_path.glob("*.jpeg"))) if not jpg_files: return {"pole": [], "anchor": []} if not GEMINI_AVAILABLE: return _fallback_image_classification(jpg_files) genai.configure(api_key=GEMINI_API_KEY) model = genai.GenerativeModel(GEMINI_MODEL) file_names = [f.name for f in jpg_files] prompt = f"Classify these images as 'pole' or 'anchor'. Files: {file_names}. Respond ONLY in JSON." content = [prompt] for img_path in jpg_files: with open(img_path, "rb") as f: content.append({"mime_type": "image/jpeg", "data": base64.b64encode(f.read()).decode("utf-8")}) try: response = model.generate_content(content) raw = re.sub(r"^```json\s*|```$", "", response.text, flags=re.MULTILINE).strip() classification = json.loads(raw) result = {"pole": [], "anchor": []} for fname, label in classification.items(): if label.lower() in result: result[label.lower()].append(fname) return result except: return _fallback_image_classification(jpg_files) def _fallback_image_classification(jpg_files): mid = len(jpg_files) // 2 return {"pole": [f.name for f in jpg_files[:mid]], "anchor": [f.name for f in jpg_files[mid:]]} #-------------------------------------------- # Transforme les photos en pdf #-------------------------------------------- def images_to_pdf(image_paths: list[Path], output_pdf: Path): pil_images = [Image.open(p).convert("RGB") for p in image_paths if p.exists()] if pil_images: pil_images[0].save(output_pdf, save_all=True, append_images=pil_images[1:]) def excel_to_pdf(excel_path: Path, output_pdf: Path): wb = load_workbook(excel_path, data_only=True) doc = SimpleDocTemplate(str(output_pdf), pagesize=landscape(letter)) styles = getSampleStyleSheet() story = [] for sheet_name in wb.sheetnames: ws = wb[sheet_name] data = [[str(cell) if cell is not None else "" for cell in row] for row in ws.iter_rows(values_only=True)] if not data: continue t = Table(data, repeatRows=1) t.setStyle(TableStyle([('FONTSIZE', (0,0), (-1,-1), 7), ('GRID', (0,0), (-1,-1), 0.5, colors.grey)])) story.append(t) doc.build(story) def create_dummy_cdc(output_path: Path, label: str): """Creates a placeholder PDF for the CDC requirement.""" c = canvas.Canvas(str(output_path), pagesize=letter) c.drawString(100, 750, f"CDC Document for {label}") c.save() #--------------------------- #This function is the "Orchestrator" for each individual pole. #It decides the order of your documents and ensures the numbering matches your specific requirements. #----------------------------- def process_pole_folder(pole_folder: Path, compilation_dir: Path, folder_parsed: dict, current_pdf_idx: int) -> int: base_label = folder_parsed['pole_ref'] address = f" {folder_parsed['address']}" if folder_parsed['address'] else "" full_label = f"{base_label}{address}" # Includes address if present (e.g. PHA627-3 Route 132) # 1. Identify Images classification = identify_images_with_gemini(pole_folder) # -- A. CDC (Calcul de Charge) - Only if 'd' -- if folder_parsed["dero"] == "d": cdc_name = f"{current_pdf_idx}-CDC-{full_label}.pdf" # Logic to create/move CDC goes here log.info(f" 📑 CDC assigned: {cdc_name}") current_pdf_idx += 1 # -- B. UDS (Excel) -> Example: 10-RSA PHA627-1.pdf -- for xlsx_file in list(pole_folder.glob("*.xlsx")) + list(pole_folder.glob("*.xls")): excel_pdf_name = f"{current_pdf_idx}-RSA {full_label}.pdf" excel_to_pdf(xlsx_file, compilation_dir / excel_pdf_name) log.info(f" 📊 UDS Excel → {excel_pdf_name}") current_pdf_idx += 1 # -- C. Poteau (Pole Images) -> Example: 11-PHOTO PHA627-1.pdf -- pole_images = [pole_folder / n for n in classification["pole"]] if pole_images: pole_pdf_name = f"{current_pdf_idx}-PHOTO {full_label}.pdf" images_to_pdf(pole_images, compilation_dir / pole_pdf_name) log.info(f" 🖼 Poteau → {pole_pdf_name}") current_pdf_idx += 1 # -- D. Ancre (Anchor Images) -> Example: 12-PHOTO ANCRE PHA627-1.pdf -- anchor_images = [pole_folder / n for n in classification["anchor"]] if anchor_images: anchor_pdf_name = f"{current_pdf_idx}-PHOTO ANCRE {full_label}.pdf" images_to_pdf(anchor_images, compilation_dir / anchor_pdf_name) log.info(f" ⚓ Ancre → {anchor_pdf_name}") current_pdf_idx += 1 # We return the new index so the NEXT pole knows where to start return current_pdf_idx def run(project_name: str, base_dir: Path, depart_dir: Path): # 1. Create the project structure project_root = create_project_structure(project_name, base_dir) rsa_uds_dir = project_root / "RSA-UDS" compilation_dir = project_root / "compilation" # --- ADDED: INITIALIZE ENTRIES --- entries = [] if not depart_dir.exists(): log.error(f"❌ Source folder not found: {depart_dir}") return # Loop through the 'départ' folder to find pole sub-folders for item in depart_dir.iterdir(): if item.is_dir(): parsed = parse_pole_folder_name(item.name) if parsed: entries.append((parsed, item)) else: log.warning(f"⚠️ Skipping folder (unrecognized format): {item.name}") # --------------------------------- # 2. Sort folders to ensure 1sd1 comes before 2d2 entries.sort(key=lambda x: x[0]["seq"]) # GLOBAL PDF COUNTER (Starts at 10) global_pdf_count = 10 for parsed, src_path in entries: # 3. Create the RSA-UDS folder name new_folder_name = compute_new_folder_name(parsed, parsed["seq"]) dst_path = rsa_uds_dir / new_folder_name # Avoid error if folder already exists if dst_path.exists(): shutil.rmtree(dst_path) shutil.copytree(src_path, dst_path) # 4. Process documents and update the global counter log.info(f"\n🔧 Processing folder: {new_folder_name}") global_pdf_count = process_pole_folder(dst_path, compilation_dir, parsed, global_pdf_count) log.info(f"\n✅ All done! PDFs generated in: {compilation_dir}") if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("project_name") parser.add_argument("--base-dir", type=Path, default=DEFAULT_BASE_DIR) parser.add_argument("--depart-dir", type=Path, default=DEFAULT_DEPART_DIR) args = parser.parse_args() run(args.project_name, args.base_dir, args.depart_dir)