New version
This commit is contained in:
+7
-6
@@ -1,12 +1,13 @@
|
|||||||
FROM python:3.12-slim
|
FROM python:3.11-slim
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY requirements.txt .
|
RUN apt-get update && apt-get install -y \
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
libxml2 libxslt1.1 libjpeg62-turbo && \
|
||||||
|
pip install --no-cache-dir fastapi uvicorn python-pptx pydantic
|
||||||
|
|
||||||
COPY app ./app
|
COPY app /app
|
||||||
|
|
||||||
EXPOSE 8000
|
EXPOSE 5000
|
||||||
|
|
||||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "5000"]
|
||||||
+16
-167
@@ -1,172 +1,21 @@
|
|||||||
from fastapi import FastAPI, HTTPExceptionfrom fastapi import FastAPI, HTTP(BaseModel):
|
from fastapi import FastAPI
|
||||||
filename: Optional[str] = "presentation"
|
from fastapi.responses import FileResponse
|
||||||
title: str = "Présentation"
|
from pydantic import BaseModel
|
||||||
subtitle: Optional[str] = None
|
from pptx_generator import generate_pptx
|
||||||
slides: List[SlideData] = Field(default_factory=list)
|
import uuid
|
||||||
|
import os
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
# ----------------------------
|
class Slide(BaseModel):
|
||||||
# Utilitaires
|
title: str
|
||||||
# ----------------------------
|
bullets: list[str]
|
||||||
|
|
||||||
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"}
|
|
||||||
|
|
||||||
|
class PresentationData(BaseModel):
|
||||||
|
slides: list[Slide]
|
||||||
|
|
||||||
@app.post("/generate")
|
@app.post("/generate")
|
||||||
def generate_presentation(payload: PresentationRequest):
|
def generate(data: PresentationData):
|
||||||
try:
|
output_file = f"/tmp/{uuid.uuid4()}.pptx"
|
||||||
output_path = generate_pptx(payload)
|
generate_pptx(data.slides, output_file)
|
||||||
download_name = f"{sanitize_filename(payload.filename)}.pptx"
|
return FileResponse(output_file, media_type="application/vnd.openxmlformats-officedocument.presentationml.presentation")
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
from pptx import Presentation
|
||||||
|
|
||||||
|
def generate_pptx(slides, output_file):
|
||||||
|
prs = Presentation("template.pptx")
|
||||||
|
|
||||||
|
for slide_data in slides:
|
||||||
|
slide_layout = prs.slide_layouts[1] # Title + Content
|
||||||
|
slide = prs.slides.add_slide(slide_layout)
|
||||||
|
|
||||||
|
title = slide.shapes.title
|
||||||
|
body = slide.placeholders[1].text_frame
|
||||||
|
|
||||||
|
title.text = slide_data.title
|
||||||
|
body.clear()
|
||||||
|
|
||||||
|
for bullet in slide_data.bullets:
|
||||||
|
p = body.add_paragraph()
|
||||||
|
p.text = bullet
|
||||||
|
p.level = 0
|
||||||
|
|
||||||
|
prs.save(output_file)
|
||||||
Binary file not shown.
Reference in New Issue
Block a user