from fastapi import FastAPI, HTTPExceptionfrom fastapi import FastAPI, HTTP(BaseModel): filename: Optional[str] = "presentation" title: str = "Présentation" subtitle: Optional[str] = None slides: List[SlideData] = Field(default_factory=list) # ---------------------------- # Utilitaires # ---------------------------- def sanitize_filename(name: str) -> str: cleaned = re.sub(r"[^a-zA-Z0-9_-]", "_", name.strip()) return cleaned or "presentation" def add_title_slide(prs: Presentation, title: str, subtitle: Optional[str]) -> None: slide_layout = prs.slide_layouts[6] # blank slide = prs.slides.add_slide(slide_layout) # Fond visuel simple via zone blanche implicite title_box = slide.shapes.add_textbox(Inches(0.6), Inches(1.0), Inches(11.5), Inches(1.0)) tf = title_box.text_frame p = tf.paragraphs[0] p.text = title p.font.size = Pt(24) p.font.bold = True p.alignment = PP_ALIGN.LEFT if subtitle: subtitle_box = slide.shapes.add_textbox(Inches(0.6), Inches(1.8), Inches(11.0), Inches(0.8)) tf2 = subtitle_box.text_frame p2 = tf2.paragraphs[0] p2.text = subtitle p2.font.size = Pt(14) p2.alignment = PP_ALIGN.LEFT def add_content_slide(prs: Presentation, slide_data: SlideData) -> None: slide_layout = prs.slide_layouts[6] # blank slide = prs.slides.add_slide(slide_layout) # Titre title_box = slide.shapes.add_textbox(Inches(0.5), Inches(0.4), Inches(12.0), Inches(0.8)) tf_title = title_box.text_frame p_title = tf_title.paragraphs[0] p_title.text = slide_data.title p_title.font.size = Pt(22) p_title.font.bold = True p_title.alignment = PP_ALIGN.LEFT current_top = 1.3 # Texte libre if slide_data.text: text_box = slide.shapes.add_textbox(Inches(0.7), Inches(current_top), Inches(11.0), Inches(1.2)) tf_text = text_box.text_frame tf_text.word_wrap = True p_text = tf_text.paragraphs[0] p_text.text = slide_data.text p_text.font.size = Pt(16) p_text.alignment = PP_ALIGN.LEFT current_top += 1.0 # Liste à puces if slide_data.bullets: bullets_box = slide.shapes.add_textbox(Inches(1.0), Inches(current_top), Inches(10.8), Inches(4.0)) tf_bullets = bullets_box.text_frame tf_bullets.word_wrap = True first = True for bullet in slide_data.bullets: if first: p = tf_bullets.paragraphs[0] first = False else: p = tf_bullets.add_paragraph() p.text = bullet p.level = 0 p.font.size = Pt(18) p.alignment = PP_ALIGN.LEFT p.bullet = True def generate_pptx(data: PresentationRequest) -> str: prs = Presentation() prs.slide_width = Inches(13.333) # 16:9 prs.slide_height = Inches(7.5) # Métadonnées prs.core_properties.title = data.title prs.core_properties.subject = data.title prs.core_properties.author = "M365 Copilot" prs.core_properties.company = "Votre organisation" # Slide de titre add_title_slide(prs, data.title, data.subtitle) # Slides de contenu for slide in data.slides: add_content_slide(prs, slide) filename = sanitize_filename(data.filename) tmp_dir = tempfile.gettempdir() full_path = os.path.join(tmp_dir, f"{filename}_{uuid.uuid4().hex}.pptx") prs.save(full_path) return full_path # ---------------------------- # Endpoints # ---------------------------- @app.get("/health") def health(): return {"status": "ok"} @app.post("/generate") def generate_presentation(payload: PresentationRequest): try: output_path = generate_pptx(payload) download_name = f"{sanitize_filename(payload.filename)}.pptx" return FileResponse( path=output_path, media_type="application/vnd.openxmlformats-officedocument.presentationml.presentation", filename=download_name ) except Exception as e: raise HTTPException(status_code=500, detail=f"Erreur lors de la génération du PPTX : {str(e)}") @app.get("/") def root(): return JSONResponse({ "service": "pptx-service", "status": "running", "endpoints": { "health": "/health", "generate": "/generate" } }) from fastapi.responses import FileResponse, JSONResponse from pydantic import BaseModel, Field from typing import List, Optional from pptx import Presentation from pptx.util import Inches, Pt from pptx.enum.text import PP_ALIGN import tempfile import os import re import uuid app = FastAPI( title="PPTX JSON Service", description="Génère un fichier PowerPoint (.pptx) à partir d'un JSON", version="1.0.0" ) # ---------------------------- # Modèles de données # ---------------------------- class SlideData(BaseModel): title: str text: Optional[str] = None bullets: Optional[List[str]] = Field(default_factory=list)