kevinkal commited on
Commit
9f59de3
·
verified ·
1 Parent(s): d8949af

Update ppt router with a second template

Browse files
Files changed (1) hide show
  1. routers/powerpoint.py +411 -130
routers/powerpoint.py CHANGED
@@ -19,6 +19,7 @@ from datetime import datetime, timedelta
19
  from collections import deque
20
  from threading import Lock
21
  import json
 
22
 
23
  router = APIRouter(prefix="/powerpoint", tags=["powerpoint"])
24
 
@@ -35,20 +36,41 @@ class ChartDataPoint(BaseModel):
35
  label: str
36
  value: float
37
 
38
- class PresentationData(BaseModel):
 
 
 
 
 
 
 
 
 
 
 
 
39
  title: str
 
 
 
40
  main_text: str
41
  graph_title: str
42
  chart_type: Literal["bar", "column", "line", "pie", "area"]
43
  chart_data: List[ChartDataPoint]
44
  side_text: str
45
 
46
- class MultimodalModel(str, Enum):
47
- gemini_25_flash = "gemini-2.5-flash"
48
- gemini_25_flash_lite = "gemini-2.5-flash-lite"
49
- gemini_20_flash = "gemini-2.0-flash"
50
- gemini_20_flash_lite = "gemini-2.0-flash-lite"
51
- gemini_25_pro = "gemini-2.5-pro"
 
 
 
 
 
 
52
 
53
  MULTIMODAL_MODEL_LIMITS = {
54
  "gemini-2.5-flash": {
@@ -109,7 +131,7 @@ def get_available_model():
109
  detail="All Gemini multimodal models have reached their rate limits (daily or RPM)."
110
  )
111
 
112
- def create_chart_from_data(slide, chart_data, chart_type, chart_title):
113
  """Create a chart in the slide based on the provided data and type"""
114
 
115
  # Chart type mapping
@@ -126,148 +148,361 @@ def create_chart_from_data(slide, chart_data, chart_type, chart_title):
126
  chart_data_obj.categories = [item.label for item in chart_data]
127
  chart_data_obj.add_series('Series 1', [item.value for item in chart_data])
128
 
129
- # Add chart to slide (replacing the graph textbox position)
130
- x, y, cx, cy = Inches(0.39), Inches(2.96), Inches(9.23), Inches(3.64)
131
  chart = slide.shapes.add_chart(
132
  chart_type_mapping.get(chart_type, XL_CHART_TYPE.COLUMN_CLUSTERED),
133
  x, y, cx, cy, chart_data_obj
134
  ).chart
135
 
136
- chart.chart_title.text_frame.text = "" # instead of chart_title
137
 
138
  return chart
139
 
140
- def generate_pptx_from_data(presentation_data: PresentationData) -> str:
141
- """Generate PPTX file from presentation data and return file path"""
 
 
 
142
 
143
- prs = Presentation()
144
- prs.slide_width = Inches(13.33) # standard 16:9
145
- prs.slide_height = Inches(7.5)
146
- slide = prs.slides.add_slide(prs.slide_layouts[6])
147
 
148
- # Shape 1: Title
149
- title_box = slide.shapes.add_textbox(Inches(0.39), Inches(0.24), Inches(12.53), Inches(0.9))
150
- title_tf = title_box.text_frame
151
- title_tf.word_wrap = True
152
- title_tf.text = presentation_data.title
153
- title_tf.paragraphs[0].font.size = Pt(24)
154
- title_tf.paragraphs[0].font.name = "Verdana"
155
-
156
- # Shape 2: Sources
157
- src_box = slide.shapes.add_textbox(Inches(0.38), Inches(6.95), Inches(12.53), Inches(0.19))
158
- src_tf = src_box.text_frame
159
- src_tf.text = "Sources: TBD manually"
160
- src_tf.paragraphs[0].font.size = Pt(10)
161
-
162
- # Shape 4: Ligne horizontale sous le titre
163
- line1 = slide.shapes.add_connector(
164
- MSO_CONNECTOR.STRAIGHT,
165
- Inches(0.39), Inches(1.23),
166
- Inches(12.92), Inches(1.23)
167
- )
168
- line1.line.width = Pt(0.5)
169
- line1.shadow.inherit = False
170
- line1.line.fill.solid()
171
- line1.line.fill.fore_color.rgb = RGBColor(1, 1, 1)
172
-
173
- # Shape 7: [Text] gauche
174
- text_box = slide.shapes.add_textbox(Inches(0.37), Inches(1.66), Inches(9.61), Inches(0.35))
175
- text_tf = text_box.text_frame
176
- text_tf.word_wrap = True
177
- text_tf.text = presentation_data.main_text
178
- text_tf.paragraphs[0].font.size = Pt(16)
179
- text_tf.paragraphs[0].font.bold = True
180
- text_tf.paragraphs[0].font.name = "Verdana"
181
-
182
- # Shape 6: Ligne horizontale sous [Text]
183
- line2 = slide.shapes.add_connector(
184
- MSO_CONNECTOR.STRAIGHT,
185
- Inches(0.37), Inches(2.09),
186
- Inches(9.99), Inches(2.09)
187
- )
188
- line2.line.width = Pt(6)
189
- line2.shadow.inherit = False
190
- line2.line.fill.solid()
191
- line2.line.fill.fore_color.rgb = RGBColor(0, 32, 96)
192
-
193
- # Shape 5: Graph title
194
- graph_title = slide.shapes.add_textbox(Inches(0.37), Inches(2.16), Inches(9.23), Inches(0.48))
195
- gtf = graph_title.text_frame
196
- gtf.word_wrap = True
197
- gtf.text = presentation_data.graph_title
198
- gtf.paragraphs[0].font.size = Pt(14)
199
- gtf.paragraphs[0].font.name = "Verdana"
200
-
201
- # Shape 10: Create actual chart instead of text box
202
- try:
203
- create_chart_from_data(slide, presentation_data.chart_data,
204
- presentation_data.chart_type, presentation_data.graph_title)
205
- except Exception as e:
206
- # Fallback to text box if chart creation fails
207
- graph_box = slide.shapes.add_textbox(Inches(0.39), Inches(2.96), Inches(9.23), Inches(3.64))
208
- graph_tf = graph_box.text_frame
209
- graph_tf.text = f"Chart ({presentation_data.chart_type}): {presentation_data.graph_title}"
210
- graph_tf.paragraphs[0].alignment = PP_ALIGN.CENTER
211
- graph_tf.paragraphs[0].font.italic = True
212
 
213
- # Shape 8: Ligne verticale séparatrice
214
- line3 = slide.shapes.add_connector(
215
- MSO_CONNECTOR.STRAIGHT,
216
- Inches(10.0), Inches(2.23),
217
- Inches(10.0), Inches(6.6)
218
- )
219
- line3.line.width = Pt(1.5)
220
- line3.shadow.inherit = False
221
- line3.line.fill.solid()
222
- line3.line.fill.fore_color.rgb = RGBColor(0, 32, 96)
223
 
224
- # Shape 11: Texte colonne droite
225
- side_box = slide.shapes.add_textbox(Inches(10.19), Inches(2.2), Inches(2.72), Inches(4.39))
226
- fill = side_box.fill
227
- fill.solid()
228
- fill.fore_color.rgb = RGBColor(247, 248, 250)
229
- side_tf = side_box.text_frame
230
- side_tf.word_wrap = True
231
- side_tf.text = presentation_data.side_text
232
- for paragraph in side_tf.paragraphs:
233
- paragraph.font.size = Pt(12)
234
- paragraph.font.name = "Verdana"
235
- side_tf.vertical_anchor = MSO_ANCHOR.MIDDLE
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
 
237
- # Save to temporary file
238
- temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.pptx')
239
- prs.save(temp_file.name)
240
 
241
- return temp_file.name
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
242
 
243
  @router.post("/image-to-pptx")
244
  async def image_to_pptx(
245
  token: Annotated[str, Depends(verify_token)],
246
  file: UploadFile = File(..., description="Image file to convert to PowerPoint"),
247
- model: MultimodalModel = Form(..., description="Gemini model to use for processing (Gemini 2.5 Flash recommended"),
 
248
  ):
249
  """
250
- Analyze an image with Gemini and generate a PowerPoint presentation based on the analysis
251
  """
252
  try:
253
  # Get available model
254
  model = get_available_model()
255
 
256
- # Hardcoded prompt for presentation generation
257
- prompt = """
258
- Analyze this image and create a professional presentation slide based on what you see. Don't modify or invent text.
259
-
260
- Please provide:
261
- 1. A title from the image
262
- 2. Secondary title from the image
263
- 3. A graph title that describes any data visualization or key metric
264
- 4. Chart data with meaningful labels and values (create realistic data if needed based on the image)
265
- 5. Choose the most appropriate chart type (bar, column, line, pie, or area)
266
- 6. Side text with bullet points •, dash - and /n
267
-
268
- Make sure the content is professional, data-driven, and suitable for a business presentation.
269
- The chart data should have 3-7 data points with meaningful labels and realistic values.
270
- """
271
 
272
  # Read uploaded file
273
  file_content = await file.read()
@@ -280,28 +515,74 @@ async def image_to_pptx(
280
  data=file_content,
281
  mime_type=file.content_type,
282
  ),
283
- prompt
284
  ],
285
  config={
286
  'response_mime_type': 'application/json',
287
- 'response_schema': PresentationData,
288
  }
289
  )
290
 
291
  # Parse the response
292
- presentation_data = PresentationData.model_validate(json.loads(response.text))
293
 
294
  # Generate PPTX file
295
- pptx_path = generate_pptx_from_data(presentation_data)
296
 
297
  # Return file as download
298
  return FileResponse(
299
  path=pptx_path,
300
- filename=f"presentation_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pptx",
301
  media_type="application/vnd.openxmlformats-officedocument.presentationml.presentation"
302
  )
303
 
304
  except HTTPException:
305
  raise
306
  except Exception as e:
307
- return APIResponse(success=False, error=f"Failed to generate presentation: {str(e)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  from collections import deque
20
  from threading import Lock
21
  import json
22
+ from abc import ABC, abstractmethod
23
 
24
  router = APIRouter(prefix="/powerpoint", tags=["powerpoint"])
25
 
 
36
  label: str
37
  value: float
38
 
39
+ class MultimodalModel(str, Enum):
40
+ gemini_25_flash = "gemini-2.5-flash"
41
+ gemini_25_flash_lite = "gemini-2.5-flash-lite"
42
+ gemini_20_flash = "gemini-2.0-flash"
43
+ gemini_20_flash_lite = "gemini-2.0-flash-lite"
44
+ gemini_25_pro = "gemini-2.5-pro"
45
+
46
+ class TemplateType(str, Enum):
47
+ single_graph = "single_graph"
48
+ two_column_graph = "two_column_graph"
49
+
50
+ # Base class for all presentation data models
51
+ class BasePresentationData(BaseModel, ABC):
52
  title: str
53
+
54
+ # Single graph template (original)
55
+ class SingleGraphPresentationData(BasePresentationData):
56
  main_text: str
57
  graph_title: str
58
  chart_type: Literal["bar", "column", "line", "pie", "area"]
59
  chart_data: List[ChartDataPoint]
60
  side_text: str
61
 
62
+ # Two column graph template (new)
63
+ class TwoColumnGraphPresentationData(BasePresentationData):
64
+ left_text: str
65
+ left_graph_title: str
66
+ left_chart_type: Literal["bar", "column", "line", "pie", "area"]
67
+ left_chart_data: List[ChartDataPoint]
68
+ left_side_text: str
69
+ right_text: str
70
+ right_graph_title: str
71
+ right_chart_type: Literal["bar", "column", "line", "pie", "area"]
72
+ right_chart_data: List[ChartDataPoint]
73
+ right_side_text: str
74
 
75
  MULTIMODAL_MODEL_LIMITS = {
76
  "gemini-2.5-flash": {
 
131
  detail="All Gemini multimodal models have reached their rate limits (daily or RPM)."
132
  )
133
 
134
+ def create_chart_from_data(slide, chart_data, chart_type, x, y, cx, cy):
135
  """Create a chart in the slide based on the provided data and type"""
136
 
137
  # Chart type mapping
 
148
  chart_data_obj.categories = [item.label for item in chart_data]
149
  chart_data_obj.add_series('Series 1', [item.value for item in chart_data])
150
 
151
+ # Add chart to slide
 
152
  chart = slide.shapes.add_chart(
153
  chart_type_mapping.get(chart_type, XL_CHART_TYPE.COLUMN_CLUSTERED),
154
  x, y, cx, cy, chart_data_obj
155
  ).chart
156
 
157
+ chart.chart_title.text_frame.text = ""
158
 
159
  return chart
160
 
161
+ # Abstract base class for template generators
162
+ class TemplateGenerator(ABC):
163
+ @abstractmethod
164
+ def generate_pptx(self, presentation_data: BasePresentationData) -> str:
165
+ pass
166
 
167
+ @abstractmethod
168
+ def get_prompt(self) -> str:
169
+ pass
 
170
 
171
+ @abstractmethod
172
+ def get_response_schema(self):
173
+ pass
174
+
175
+ class SingleGraphTemplateGenerator(TemplateGenerator):
176
+ def get_prompt(self) -> str:
177
+ return """
178
+ Analyze this image and create a professional presentation slide based on what you see. Don't modify or invent text.
179
+
180
+ Please provide:
181
+ 1. A title from the image
182
+ 2. Secondary title from the image (main_text)
183
+ 3. A graph title that describes any data visualization or key metric
184
+ 4. Chart data with meaningful labels and values (create realistic data if needed based on the image)
185
+ 5. Choose the most appropriate chart type (bar, column, line, pie, or area)
186
+ 6. Side text with bullet points •, dash - and \\n
187
+
188
+ Make sure the content is professional, data-driven, and suitable for a business presentation.
189
+ The chart data should have 3-7 data points with meaningful labels and realistic values.
190
+ """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
191
 
192
+ def get_response_schema(self):
193
+ return SingleGraphPresentationData
 
 
 
 
 
 
 
 
194
 
195
+ def generate_pptx(self, presentation_data: SingleGraphPresentationData) -> str:
196
+ """Generate PPTX file from single graph presentation data"""
197
+
198
+ prs = Presentation()
199
+ prs.slide_width = Inches(13.33) # standard 16:9
200
+ prs.slide_height = Inches(7.5)
201
+ slide = prs.slides.add_slide(prs.slide_layouts[6])
202
+
203
+ # Shape 1: Title
204
+ title_box = slide.shapes.add_textbox(Inches(0.39), Inches(0.24), Inches(12.53), Inches(0.9))
205
+ title_tf = title_box.text_frame
206
+ title_tf.word_wrap = True
207
+ title_tf.text = presentation_data.title
208
+ title_tf.paragraphs[0].font.size = Pt(24)
209
+ title_tf.paragraphs[0].font.name = "Verdana"
210
+
211
+ # Shape 2: Sources
212
+ src_box = slide.shapes.add_textbox(Inches(0.38), Inches(6.95), Inches(12.53), Inches(0.19))
213
+ src_tf = src_box.text_frame
214
+ src_tf.text = "Sources: TBD manually"
215
+ src_tf.paragraphs[0].font.size = Pt(10)
216
+
217
+ # Shape 4: Ligne horizontale sous le titre
218
+ line1 = slide.shapes.add_connector(
219
+ MSO_CONNECTOR.STRAIGHT,
220
+ Inches(0.39), Inches(1.23),
221
+ Inches(12.92), Inches(1.23)
222
+ )
223
+ line1.line.width = Pt(0.5)
224
+ line1.shadow.inherit = False
225
+ line1.line.fill.solid()
226
+ line1.line.fill.fore_color.rgb = RGBColor(1, 1, 1)
227
+
228
+ # Shape 7: [Text] gauche
229
+ text_box = slide.shapes.add_textbox(Inches(0.37), Inches(1.66), Inches(9.61), Inches(0.35))
230
+ text_tf = text_box.text_frame
231
+ text_tf.word_wrap = True
232
+ text_tf.text = presentation_data.main_text
233
+ text_tf.paragraphs[0].font.size = Pt(16)
234
+ text_tf.paragraphs[0].font.bold = True
235
+ text_tf.paragraphs[0].font.name = "Verdana"
236
+
237
+ # Shape 6: Ligne horizontale sous [Text]
238
+ line2 = slide.shapes.add_connector(
239
+ MSO_CONNECTOR.STRAIGHT,
240
+ Inches(0.37), Inches(2.09),
241
+ Inches(9.99), Inches(2.09)
242
+ )
243
+ line2.line.width = Pt(6)
244
+ line2.shadow.inherit = False
245
+ line2.line.fill.solid()
246
+ line2.line.fill.fore_color.rgb = RGBColor(0, 32, 96)
247
+
248
+ # Shape 5: Graph title
249
+ graph_title = slide.shapes.add_textbox(Inches(0.37), Inches(2.16), Inches(9.23), Inches(0.48))
250
+ gtf = graph_title.text_frame
251
+ gtf.word_wrap = True
252
+ gtf.text = presentation_data.graph_title
253
+ gtf.paragraphs[0].font.size = Pt(14)
254
+ gtf.paragraphs[0].font.name = "Verdana"
255
+
256
+ # Shape 10: Create actual chart
257
+ try:
258
+ create_chart_from_data(slide, presentation_data.chart_data,
259
+ presentation_data.chart_type,
260
+ Inches(0.39), Inches(2.96), Inches(9.23), Inches(3.64))
261
+ except Exception as e:
262
+ # Fallback to text box if chart creation fails
263
+ graph_box = slide.shapes.add_textbox(Inches(0.39), Inches(2.96), Inches(9.23), Inches(3.64))
264
+ graph_tf = graph_box.text_frame
265
+ graph_tf.text = f"Chart ({presentation_data.chart_type}): {presentation_data.graph_title}"
266
+ graph_tf.paragraphs[0].alignment = PP_ALIGN.CENTER
267
+ graph_tf.paragraphs[0].font.italic = True
268
+
269
+ # Shape 8: Ligne verticale séparatrice
270
+ line3 = slide.shapes.add_connector(
271
+ MSO_CONNECTOR.STRAIGHT,
272
+ Inches(10.0), Inches(2.23),
273
+ Inches(10.0), Inches(6.6)
274
+ )
275
+ line3.line.width = Pt(1.5)
276
+ line3.shadow.inherit = False
277
+ line3.line.fill.solid()
278
+ line3.line.fill.fore_color.rgb = RGBColor(0, 32, 96)
279
+
280
+ # Shape 11: Texte colonne droite
281
+ side_box = slide.shapes.add_textbox(Inches(10.19), Inches(2.2), Inches(2.72), Inches(4.39))
282
+ fill = side_box.fill
283
+ fill.solid()
284
+ fill.fore_color.rgb = RGBColor(247, 248, 250)
285
+ side_tf = side_box.text_frame
286
+ side_tf.word_wrap = True
287
+ side_tf.text = presentation_data.side_text
288
+ for paragraph in side_tf.paragraphs:
289
+ paragraph.font.size = Pt(12)
290
+ paragraph.font.name = "Verdana"
291
+ side_tf.vertical_anchor = MSO_ANCHOR.MIDDLE
292
+
293
+ # Save to temporary file
294
+ temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.pptx')
295
+ prs.save(temp_file.name)
296
+
297
+ return temp_file.name
298
+
299
+ class TwoColumnGraphTemplateGenerator(TemplateGenerator):
300
+ def get_prompt(self) -> str:
301
+ return """
302
+ Analyze this image and create a professional presentation slide with two columns based on what you see. Don't modify or invent text.
303
+
304
+ Please provide:
305
+ 1. A title from the image
306
+ 2. Left column text (main insight or finding)
307
+ 3. Left graph title that describes the first data visualization
308
+ 4. Left chart data with meaningful labels and values (3-5 data points)
309
+ 5. Choose appropriate chart type for left chart (bar, column, line, pie, or area)
310
+ 6. Left side text with bullet points •, dash - and \\n
311
+ 7. Right column text (secondary insight or comparison)
312
+ 8. Right graph title that describes the second data visualization
313
+ 9. Right chart data with meaningful labels and values (3-5 data points)
314
+ 10. Choose appropriate chart type for right chart (bar, column, line, pie, or area)
315
+ 11. Right side text with bullet points •, dash - and \\n
316
+
317
+ Make sure the content is professional, data-driven, and suitable for a business presentation.
318
+ The two columns should complement each other and tell a coherent story.
319
+ """
320
 
321
+ def get_response_schema(self):
322
+ return TwoColumnGraphPresentationData
 
323
 
324
+ def generate_pptx(self, presentation_data: TwoColumnGraphPresentationData) -> str:
325
+ """Generate PPTX file from two column graph presentation data"""
326
+
327
+ prs = Presentation()
328
+ prs.slide_width = Inches(13.33) # 16:9 widescreen
329
+ prs.slide_height = Inches(7.5)
330
+ slide = prs.slides.add_slide(prs.slide_layouts[6]) # blank layout
331
+
332
+ # === Shape 1 : Title ===
333
+ title_box = slide.shapes.add_textbox(Inches(0.39), Inches(0.24), Inches(12.53), Inches(0.9))
334
+ title_tf = title_box.text_frame
335
+ title_tf.text = presentation_data.title
336
+ title_tf.paragraphs[0].font.name = "Verdana"
337
+ title_tf.paragraphs[0].font.size = Pt(22)
338
+ title_tf.vertical_anchor = MSO_ANCHOR.MIDDLE
339
+
340
+ # === Shape 2 : Sources ===
341
+ src_box = slide.shapes.add_textbox(Inches(0.38), Inches(6.91), Inches(12.53), Inches(0.23))
342
+ src_tf = src_box.text_frame
343
+ src_tf.text = "Sources: TBD manually"
344
+ src_tf.paragraphs[0].font.size = Pt(10)
345
+ src_tf.paragraphs[0].font.name = "Verdana"
346
+
347
+ # === Shape 3 : Placeholder (page num / legend) ===
348
+ ph_box = slide.shapes.add_textbox(Inches(10.15), Inches(7.04), Inches(3.0), Inches(0.4))
349
+ ph_tf = ph_box.text_frame
350
+ ph_tf.text = "1"
351
+ ph_tf.paragraphs[0].alignment = PP_ALIGN.RIGHT
352
+ ph_tf.paragraphs[0].font.size = Pt(10)
353
+
354
+ # Shape 4: Ligne horizontale sous le titre
355
+ line1 = slide.shapes.add_connector(
356
+ MSO_CONNECTOR.STRAIGHT,
357
+ Inches(0.39), Inches(1.23),
358
+ Inches(12.92), Inches(1.23)
359
+ )
360
+ line1.line.width = Pt(0.5)
361
+ line1.shadow.inherit = False
362
+ line1.line.fill.solid()
363
+ line1.line.fill.fore_color.rgb = RGBColor(1, 1, 1)
364
+
365
+ # === Shape 5 : Ligne horizontale gauche ===
366
+ line2 = slide.shapes.add_connector(
367
+ MSO_CONNECTOR.STRAIGHT,
368
+ Inches(0.37), Inches(2.09),
369
+ Inches(6.3), Inches(2.09)
370
+ )
371
+ line2.line.width = Pt(6)
372
+ line2.shadow.inherit = False
373
+ line2.line.fill.solid()
374
+ line2.line.fill.fore_color.rgb = RGBColor(0, 32, 96)
375
+
376
+ # === Shape 6 : Graph title gauche ===
377
+ gt_left = slide.shapes.add_textbox(Inches(0.38), Inches(2.21), Inches(5.9), Inches(0.39))
378
+ gtl_tf = gt_left.text_frame
379
+ gtl_tf.text = presentation_data.left_graph_title
380
+ gtl_tf.paragraphs[0].font.size = Pt(12)
381
+ gtl_tf.paragraphs[0].font.name = "Verdana"
382
+ gtl_tf.paragraphs[0].font.color.rgb = RGBColor(0, 32, 96)
383
+
384
+ # === Shape 7 : Ligne horizontale droite ===
385
+ line3 = slide.shapes.add_connector(
386
+ MSO_CONNECTOR.STRAIGHT,
387
+ Inches(7.07), Inches(2.09),
388
+ Inches(12.97), Inches(2.09)
389
+ )
390
+ line3.line.width = Pt(6)
391
+ line3.shadow.inherit = False
392
+ line3.line.fill.solid()
393
+ line3.line.fill.fore_color.rgb = RGBColor(0, 32, 96)
394
+
395
+ # === Shape 8 : Graph title droite ===
396
+ gt_right = slide.shapes.add_textbox(Inches(7.07), Inches(2.21), Inches(5.9), Inches(0.39))
397
+ gtr_tf = gt_right.text_frame
398
+ gtr_tf.text = presentation_data.right_graph_title
399
+ gtr_tf.paragraphs[0].font.size = Pt(12)
400
+ gtr_tf.paragraphs[0].font.name = "Verdana"
401
+ gtr_tf.paragraphs[0].font.color.rgb = RGBColor(0, 32, 96)
402
+
403
+ # === Shape 9 : Bloc texte gauche ===
404
+ text_left = slide.shapes.add_textbox(Inches(4.47), Inches(2.85), Inches(1.8), Inches(3.6))
405
+ tl_tf_fill = text_left.fill
406
+ tl_tf_fill.solid()
407
+ tl_tf_fill.fore_color.rgb = RGBColor(247, 248, 250)
408
+ tl_tf = text_left.text_frame
409
+ tl_tf.text = presentation_data.left_side_text
410
+ tl_tf.paragraphs[0].font.size = Pt(12)
411
+ tl_tf.paragraphs[0].font.name = "Verdana"
412
+ tl_tf.vertical_anchor = MSO_ANCHOR.MIDDLE
413
+
414
+ # === Shape 10 : Bloc texte droite ===
415
+ text_right = slide.shapes.add_textbox(Inches(11.13), Inches(2.85), Inches(1.8), Inches(3.6))
416
+ tr_tf_fill = text_right.fill
417
+ tr_tf_fill.solid()
418
+ tr_tf_fill.fore_color.rgb = RGBColor(247, 248, 250)
419
+ tr_tf = text_right.text_frame
420
+ tr_tf.text = presentation_data.right_side_text
421
+ tr_tf.paragraphs[0].font.size = Pt(12)
422
+ tr_tf.paragraphs[0].font.name = "Verdana"
423
+ tr_tf.vertical_anchor = MSO_ANCHOR.MIDDLE
424
+
425
+ # === Shape 11 : Ligne verticale séparation ===
426
+ line4 = slide.shapes.add_connector(
427
+ MSO_CONNECTOR.STRAIGHT,
428
+ Inches(6.69), Inches(2.23),
429
+ Inches(6.69), Inches(6.32)
430
+ )
431
+ line4.line.width = Pt(0.25)
432
+ line4.shadow.inherit = False
433
+ line4.line.fill.solid()
434
+ line4.line.fill.fore_color.rgb = RGBColor(217, 217, 217)
435
+
436
+ # === Shape 12 : Texte haut gauche ===
437
+ txt12 = slide.shapes.add_textbox(Inches(0.38), Inches(1.52), Inches(5.9), Inches(0.44))
438
+ txt12_tf = txt12.text_frame
439
+ txt12_tf.text = presentation_data.left_text
440
+ txt12_tf.paragraphs[0].font.size = Pt(14)
441
+ txt12_tf.paragraphs[0].font.name = "Verdana"
442
+ txt12_tf.paragraphs[0].font.bold = True
443
+
444
+ # === Shape 13 : Texte haut droite ===
445
+ txt13 = slide.shapes.add_textbox(Inches(7.07), Inches(1.52), Inches(5.9), Inches(0.44))
446
+ txt13_tf = txt13.text_frame
447
+ txt13_tf.text = presentation_data.right_text
448
+ txt13_tf.paragraphs[0].font.size = Pt(14)
449
+ txt13_tf.paragraphs[0].font.name = "Verdana"
450
+ txt13_tf.paragraphs[0].font.bold = True
451
+
452
+ # === Shape 14 : Graph gauche ===
453
+ try:
454
+ create_chart_from_data(slide, presentation_data.left_chart_data,
455
+ presentation_data.left_chart_type,
456
+ Inches(0.38), Inches(2.85), Inches(4.1), Inches(3.6))
457
+ except Exception as e:
458
+ graph_left = slide.shapes.add_shape(1, Inches(0.38), Inches(2.85), Inches(4.1), Inches(3.6))
459
+ gl_tf = graph_left.text_frame
460
+ gl_tf.text = f"Graph ({presentation_data.left_chart_type})"
461
+ gl_tf.paragraphs[0].alignment = PP_ALIGN.CENTER
462
+ gl_tf.paragraphs[0].font.italic = True
463
+
464
+ # === Shape 15 : Graph droite ===
465
+ try:
466
+ create_chart_from_data(slide, presentation_data.right_chart_data,
467
+ presentation_data.right_chart_type,
468
+ Inches(7.07), Inches(2.85), Inches(4.06), Inches(3.6))
469
+ except Exception as e:
470
+ graph_right = slide.shapes.add_shape(1, Inches(7.07), Inches(2.85), Inches(4.06), Inches(3.6))
471
+ gr_tf = graph_right.text_frame
472
+ gr_tf.text = f"Graph ({presentation_data.right_chart_type})"
473
+ gr_tf.paragraphs[0].alignment = PP_ALIGN.CENTER
474
+ gr_tf.paragraphs[0].font.italic = True
475
+
476
+ # Save to temporary file
477
+ temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.pptx')
478
+ prs.save(temp_file.name)
479
+
480
+ return temp_file.name
481
+
482
+ # Template registry
483
+ TEMPLATE_GENERATORS = {
484
+ TemplateType.single_graph: SingleGraphTemplateGenerator(),
485
+ TemplateType.two_column_graph: TwoColumnGraphTemplateGenerator()
486
+ }
487
 
488
  @router.post("/image-to-pptx")
489
  async def image_to_pptx(
490
  token: Annotated[str, Depends(verify_token)],
491
  file: UploadFile = File(..., description="Image file to convert to PowerPoint"),
492
+ template: TemplateType = Form(..., description="Template type to use for the presentation"),
493
+ model: MultimodalModel = Form(..., description="Gemini model (Gemini 2.5 Flash recommended"),
494
  ):
495
  """
496
+ Analyze an image with Gemini and generate a PowerPoint presentation based on the analysis using the selected template
497
  """
498
  try:
499
  # Get available model
500
  model = get_available_model()
501
 
502
+ # Get template generator
503
+ template_generator = TEMPLATE_GENERATORS.get(template)
504
+ if not template_generator:
505
+ raise HTTPException(status_code=400, detail=f"Unsupported template: {template}")
 
 
 
 
 
 
 
 
 
 
 
506
 
507
  # Read uploaded file
508
  file_content = await file.read()
 
515
  data=file_content,
516
  mime_type=file.content_type,
517
  ),
518
+ template_generator.get_prompt()
519
  ],
520
  config={
521
  'response_mime_type': 'application/json',
522
+ 'response_schema': template_generator.get_response_schema(),
523
  }
524
  )
525
 
526
  # Parse the response
527
+ presentation_data = template_generator.get_response_schema().model_validate(json.loads(response.text))
528
 
529
  # Generate PPTX file
530
+ pptx_path = template_generator.generate_pptx(presentation_data)
531
 
532
  # Return file as download
533
  return FileResponse(
534
  path=pptx_path,
535
+ filename=f"presentation_{template.value}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pptx",
536
  media_type="application/vnd.openxmlformats-officedocument.presentationml.presentation"
537
  )
538
 
539
  except HTTPException:
540
  raise
541
  except Exception as e:
542
+ return APIResponse(success=False, error=f"Failed to generate presentation: {str(e)}")
543
+
544
+ @router.get("/templates")
545
+ async def get_available_templates(token: Annotated[str, Depends(verify_token)]):
546
+ """Get list of available presentation templates"""
547
+ templates = []
548
+ for template_type in TemplateType:
549
+ templates.append({
550
+ "name": template_type.value,
551
+ "description": {
552
+ TemplateType.single_graph: "Single graph with sidebar - ideal for focusing on one key insight",
553
+ TemplateType.two_column_graph: "Two column layout with graphs and sidebars - perfect for comparisons"
554
+ }.get(template_type, "")
555
+ })
556
+
557
+ return APIResponse(success=True, data=templates)
558
+
559
+ @router.get("/model-stats")
560
+ async def get_model_stats(token: Annotated[str, Depends(verify_token)]):
561
+ """Get current model usage statistics"""
562
+ now = datetime.utcnow()
563
+ model_stats = []
564
+
565
+ with usage_lock:
566
+ for model, limits in MULTIMODAL_MODEL_LIMITS.items():
567
+ usage = model_usage[model]
568
+
569
+ # Reset daily count if 24h passed
570
+ if now - usage["last_daily_reset"] > timedelta(days=1):
571
+ usage["daily_count"] = 0
572
+ usage["last_daily_reset"] = now
573
+
574
+ # Clean up old RPM timestamps
575
+ rpm_window = timedelta(seconds=60)
576
+ while usage["rpm_timestamps"] and now - usage["rpm_timestamps"][0] > rpm_window:
577
+ usage["rpm_timestamps"].popleft()
578
+
579
+ model_stats.append({
580
+ "model": model,
581
+ "daily_count": usage["daily_count"],
582
+ "daily_limit": limits["daily"],
583
+ "rpm_count": len(usage["rpm_timestamps"]),
584
+ "rpm_limit": limits["rpm"],
585
+ "last_daily_reset": usage["last_daily_reset"].isoformat(),
586
+ })
587
+
588
+ return APIResponse(success=True, data=model_stats)