malepati/custom_template_working
0
1# import json2# import os3# import aiohttp4# from typing import Literal5# import uuid6# from fastapi import HTTPException7# from pathvalidate import sanitize_filename8 9# from models.pptx_models import PptxPresentationModel10# from models.presentation_and_path import PresentationAndPath11# from services.pptx_presentation_creator import PptxPresentationCreator12# from services.temp_file_service import TEMP_FILE_SERVICE13# from utils.asset_directory_utils import get_exports_directory14# import uuid15 16 17# async def export_presentation(18# presentation_id: uuid.UUID, title: str, export_as: Literal["pptx", "pdf"]19# ) -> PresentationAndPath:20# if export_as == "pptx":21 22# # Get the converted PPTX model from the Next.js service23# async with aiohttp.ClientSession() as session:24# async with session.get(25# f"http://localhost/api/presentation_to_pptx_model?id={presentation_id}"26# ) as response:27# if response.status != 200:28# error_text = await response.text()29# print(f"Failed to get PPTX model: {error_text}")30# raise HTTPException(31# status_code=500,32# detail="Failed to convert presentation to PPTX model",33# )34# pptx_model_data = await response.json()35 36# # Create PPTX file using the converted model37# pptx_model = PptxPresentationModel(**pptx_model_data)38# temp_dir = TEMP_FILE_SERVICE.create_temp_dir()39# pptx_creator = PptxPresentationCreator(pptx_model, temp_dir)40# await pptx_creator.create_ppt()41 42# export_directory = get_exports_directory()43# pptx_path = os.path.join(44# export_directory,45# f"{sanitize_filename(title or str(uuid.uuid4()))}.pptx",46# )47# pptx_creator.save(pptx_path)48 49# return PresentationAndPath(50# presentation_id=presentation_id,51# path=pptx_path,52# )53# else:54# async with aiohttp.ClientSession() as session:55# async with session.post(56# "http://localhost/api/export-as-pdf",57# json={58# "id": str(presentation_id),59# "title": sanitize_filename(title or str(uuid.uuid4())),60# },61# ) as response:62# response_json = await response.json()63 64# return PresentationAndPath(65# presentation_id=presentation_id,66# path=response_json["path"],67# )68 69 70 71 72import json73import os74import aiohttp75from typing import Literal, Optional76import uuid77from fastapi import HTTPException78from pathvalidate import sanitize_filename79 80from models.pptx_models import PptxPresentationModel81from models.presentation_and_path import PresentationAndPath82from services.pptx_presentation_creator import PptxPresentationCreator83from services.temp_file_service import TEMP_FILE_SERVICE84from utils.asset_directory_utils import get_exports_directory85import uuid86 87 88async def export_presentation(89 presentation_id: uuid.UUID, title: str, export_as: Literal["pptx", "pdf"], auth_token: Optional[str] = None90) -> PresentationAndPath:91 if export_as == "pptx":92 93 # Get the converted PPTX model from the Next.js service94 headers = {}95 if auth_token:96 headers["Authorization"] = auth_token97 98 async with aiohttp.ClientSession() as session:99 async with session.get(100 f"http://localhost:3000/api/presentation_to_pptx_model?id={presentation_id}",101 headers=headers102 ) as response:103 if response.status != 200:104 error_text = await response.text()105 print(f"Failed to get PPTX model: {error_text}")106 raise HTTPException(107 status_code=500,108 detail="Failed to convert presentation to PPTX model",109 )110 pptx_model_data = await response.json()111 112 # Create PPTX file using the converted model113 pptx_model = PptxPresentationModel(**pptx_model_data)114 115 # HACK: Fetch slides from DB to verify if we missed any native charts (since Next.js scraper misses them)116 # We need a new session here or pass one in. Since this function doesn't take session, we must create one or use a context manager if possible.117 # However, making a new session cleanly is hard without dependency injection.118 # ALTERNATIVE: Use the API to fetch raw content? No.119 # Let's use the local `database.py` imports if available, but better: 120 # The presentation object usually passed to export might be better. 121 # But here we only have ID.122 123 # Let's try to fetch the slides via the existing Next.js API? No, Next.js API returns what it sees.124 # We must connect to DB.125 # from services.database import get_async_session126 # from models.sql.slide import SlideModel127 # from sqlalchemy import select128 # from models.pptx_models import (129 # PptxChartModel, 130 # PptxPositionModel, 131 # PptxChartDataPointModel,132 # PptxPictureBoxModel,133 # PptxTextBoxModel,134 # PptxAutoShapeBoxModel135 # )136 137 # # Helper to run async session manually138 # async for session in get_async_session():139 # db_slides = await session.scalars(select(SlideModel).where(SlideModel.presentation == presentation_id).order_by(SlideModel.index))140 # db_slides_list = db_slides.all()141 # 142 # # Inject charts143 # for i, db_slide in enumerate(db_slides_list):144 # if i < len(pptx_model.slides):145 # content = db_slide.content146 # if isinstance(content, dict) and content.get("__chart_data__"):147 # chart_data_dict = content["__chart_data__"]148 # 149 # # Create Chart Model150 # # Position: Default to bottom right or center-right.151 # # Using 50% width on right side.152 # # chart_shape = PptxChartModel(153 # # chart_type=chart_data_dict.get("type", "BAR").upper(),154 # # title=chart_data_dict.get("title", "Chart"),155 # # data=[PptxChartDataPointModel(label=d["label"], value=d["value"]) for d in chart_data_dict.get("data", [])],156 # # position=PptxPositionModel(157 # # left=int(1280 * 0.05), # 5% margin158 # # top=int(720 * 0.2), # Below title159 # # width=int(1280 * 0.9), # Full width minus margin160 # # height=int(720 * 0.7) # large area161 # # )162 # # )163 # # pptx_model.slides[i].shapes.append(chart_shape)164 # break # Consume only one session165 166 # --- DYNAMIC LAYOUT PATCH ---167 # Resize content to full width if image is missing or a placeholder168 SLIDE_WIDTH = 1280169 MARGIN = 60 # Standard margin170 171 for slide in pptx_model.slides:172 image_shapes_indices = []173 content_shapes = []174 175 # Identify shapes176 for idx, shape in enumerate(slide.shapes):177 if isinstance(shape, PptxPictureBoxModel):178 # Check if placeholder or empty179 if not shape.picture.path or "placeholder.jpg" in shape.picture.path:180 image_shapes_indices.append(idx)181 # Note: We treat existing real images as "keeping the layout"182 elif isinstance(shape, (PptxTextBoxModel, PptxAutoShapeBoxModel)):183 # Identify body content (heuristic: not the title)184 # Titles usually have top < 120 (roughly)185 if shape.position.top > 120:186 content_shapes.append(shape)187 188 # If we found placeholders, remove them189 if image_shapes_indices:190 # Remove in reverse order191 for idx in sorted(image_shapes_indices, reverse=True):192 slide.shapes.pop(idx)193 194 # Re-check if any images remain on the slide195 has_real_images = any(isinstance(s, PptxPictureBoxModel) for s in slide.shapes)196 has_charts = any(isinstance(s, PptxChartModel) for s in slide.shapes)197 198 # SAFETY CHECK: Only resize content if we *actually* removed a placeholder/empty image.199 # This prevents destroying layouts that were designed to be text-only (e.g. 2-column text)200 # which naturally have no images but shouldn't be merged into one big block.201 if image_shapes_indices and not has_real_images and not has_charts and content_shapes:202 # print(f"Resizing content for slide with no images")203 for shape in content_shapes:204 # Make full width with margins205 shape.position.left = MARGIN206 shape.position.width = SLIDE_WIDTH - (MARGIN * 2)207 # We don't touch top/height to preserve vertical flow208 # ----------------------------209 210 temp_dir = TEMP_FILE_SERVICE.create_temp_dir()211 pptx_creator = PptxPresentationCreator(pptx_model, temp_dir)212 await pptx_creator.create_ppt()213 214 export_directory = get_exports_directory()215 pptx_path = os.path.join(216 export_directory,217 f"{sanitize_filename(title or str(uuid.uuid4()))}.pptx",218 )219 pptx_creator.save(pptx_path)220 221 return PresentationAndPath(222 presentation_id=presentation_id,223 path=pptx_path,224 )225 else:226 # Send Authorization header if present227 headers = {}228 if auth_token:229 headers["Authorization"] = auth_token230 231 async with aiohttp.ClientSession() as session:232 async with session.post(233 "http://localhost:3000/api/export-as-pdf",234 headers=headers,235 json={236 "id": str(presentation_id),237 "title": sanitize_filename(title or str(uuid.uuid4())),238 },239 ) as response:240 response_json = await response.json()241 242 return PresentationAndPath(243 presentation_id=presentation_id,244 path=response_json["path"],245 )246 