From bec5b542569bd935a9d79d73c75419ba83da371b Mon Sep 17 00:00:00 2001 From: Stephane DAVID Date: Sat, 23 May 2026 21:31:34 +0200 Subject: [PATCH] First version --- Dockerfile | 12 ++++ app/main.py | 172 +++++++++++++++++++++++++++++++++++++++++++++ docker-compose.yml | 12 ++++ requirements.txt | 4 ++ 4 files changed, 200 insertions(+) create mode 100644 Dockerfile create mode 100644 app/main.py create mode 100644 docker-compose.yml create mode 100644 requirements.txt diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1b364a6 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM python:3.12-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY app ./app + +EXPOSE 8000 + +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..0d275de --- /dev/null +++ b/app/main.py @@ -0,0 +1,172 @@ +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) + + diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..30c3bc4 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,12 @@ +services: + pptx-service: + build: + context: . + dockerfile: Dockerfile + container_name: pptx-service-python + ports: + - "18000:8000" + environment: + PORT: 8000 + PYTHONUNBUFFERED: "1" + restart: unless-stopped diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..02f37aa --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +fastapi==0.115.0 +uvicorn[standard]==0.30.6 +python-pptx==1.0.2 +python-multipart==0.0.9 \ No newline at end of file