173 lines
5.0 KiB
Python
173 lines
5.0 KiB
Python
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)
|
|
|
|
|