import logging import pathlib import re import tempfile from typing import List, Tuple import json5 import pptx from pptx.enum.shapes import MSO_AUTO_SHAPE_TYPE from global_config import GlobalConfig # English Metric Unit (used by PowerPoint) to inches EMU_TO_INCH_SCALING_FACTOR = 1.0 / 914400 INCHES_1_5 = pptx.util.Inches(1.5) INCHES_1 = pptx.util.Inches(1) INCHES_0_5 = pptx.util.Inches(0.5) INCHES_0_4 = pptx.util.Inches(0.4) INCHES_0_3 = pptx.util.Inches(0.3) STEP_BY_STEP_PROCESS_MARKER = '>> ' PATTERN = re.compile(r"^slide[ ]+\d+:", re.IGNORECASE) SAMPLE_JSON_FOR_PPTX = ''' { "title": "Understanding AI", "slides": [ { "heading": "Introduction", "bullet_points": [ "Brief overview of AI", [ "Importance of understanding AI" ] ] } ] } ''' logger = logging.getLogger(__name__) def remove_slide_number_from_heading(header: str) -> str: """ Remove the slide number from a given slide header. :param header: The header of a slide. """ if PATTERN.match(header): idx = header.find(':') header = header[idx + 1:] return header def generate_powerpoint_presentation( structured_data: str, slides_template: str, output_file_path: pathlib.Path ) -> List: """ Create and save a PowerPoint presentation file containing the content in JSON format. :param structured_data: The presentation contents as "JSON" (may contain trailing commas). :param slides_template: The PPTX template to use. :param output_file_path: The path of the PPTX file to save as. :return A list of presentation title and slides headers. """ # The structured "JSON" might contain trailing commas, so using json5 parsed_data = json5.loads(structured_data) logger.debug( '*** Using PPTX template: %s', GlobalConfig.PPTX_TEMPLATE_FILES[slides_template]['file'] ) presentation = pptx.Presentation(GlobalConfig.PPTX_TEMPLATE_FILES[slides_template]['file']) slide_width_inch = EMU_TO_INCH_SCALING_FACTOR * presentation.slide_width slide_height_inch = EMU_TO_INCH_SCALING_FACTOR * presentation.slide_height logger.debug('Slide width: %f, height: %f', slide_width_inch, slide_height_inch) # The title slide title_slide_layout = presentation.slide_layouts[0] slide = presentation.slides.add_slide(title_slide_layout) title = slide.shapes.title subtitle = slide.placeholders[1] title.text = parsed_data['title'] logger.info( 'PPT title: %s | #slides: %d', title.text, len(parsed_data['slides']) ) subtitle.text = 'by Myself and SlideDeck AI :)' all_headers = [title.text, ] # background = slide.background # background.fill.solid() # background.fill.fore_color.rgb = RGBColor.from_string('C0C0C0') # Silver # title.text_frame.paragraphs[0].font.color.rgb = RGBColor(0, 0, 128) # Navy blue # Add contents in a loop for a_slide in parsed_data['slides']: bullet_slide_layout = presentation.slide_layouts[1] slide = presentation.slides.add_slide(bullet_slide_layout) if not _handle_step_by_step_process( slide=slide, slide_json=a_slide, slide_width_inch=slide_width_inch, slide_height_inch=slide_height_inch, ): _handle_default_display(slide, a_slide) _handle_key_message( slide=slide, slide_json=a_slide, slide_width_inch=slide_width_inch, slide_height_inch=slide_height_inch, ) # The thank-you slide last_slide_layout = presentation.slide_layouts[0] slide = presentation.slides.add_slide(last_slide_layout) title = slide.shapes.title title.text = 'Thank you!' presentation.save(output_file_path) return all_headers def get_flat_list_of_contents(items: list, level: int) -> List[Tuple]: """ Flatten a (hierarchical) list of bullet points to a single list containing each item and its level. :param items: A bullet point (string or list). :param level: The current level of hierarchy. :return: A list of (bullet item text, hierarchical level) tuples. """ flat_list = [] for item in items: if isinstance(item, str): flat_list.append((item, level)) elif isinstance(item, list): flat_list = flat_list + get_flat_list_of_contents(item, level + 1) return flat_list def _handle_default_display( slide: pptx.slide.Slide, slide_json: dict, ): """ Display a list of text in a slide. :param slide: The slide to be processed. :param slide_json: The content of the slide as JSON data. """ shapes = slide.shapes title_shape = shapes.title body_shape = shapes.placeholders[1] title_shape.text = remove_slide_number_from_heading(slide_json['heading']) text_frame = body_shape.text_frame # The bullet_points may contain a nested hierarchy of JSON arrays # In some scenarios, it may contain objects (dictionaries) because the LLM generated so # ^ The second scenario is not covered flat_items_list = get_flat_list_of_contents(slide_json['bullet_points'], level=0) for an_item in flat_items_list: paragraph = text_frame.add_paragraph() paragraph.text = an_item[0].removeprefix(STEP_BY_STEP_PROCESS_MARKER) paragraph.level = an_item[1] def _handle_step_by_step_process( slide: pptx.slide.Slide, slide_json: dict, slide_width_inch: float, slide_height_inch: float ) -> bool: """ Add shapes to display a step-by-step process in the slide, if available. :param slide: The slide to be processed. :param slide_json: The content of the slide as JSON data. :param slide_width_inch: The width of the slide in inches. :param slide_height_inch: The height of the slide in inches. :return True is this slide has a step-by-step process depiction; False otherwise. """ if 'bullet_points' in slide_json and slide_json['bullet_points']: steps = slide_json['bullet_points'] # Ensure that it is a single list of strings without any sub-list for step in steps: if not isinstance(step, str): return False if not step.startswith(STEP_BY_STEP_PROCESS_MARKER): return False shapes = slide.shapes shapes.title.text = remove_slide_number_from_heading(slide_json['heading']) n_steps = len(steps) if 3 <= n_steps <= 4: # Horizontal display height = INCHES_1_5 width = pptx.util.Inches(slide_width_inch / n_steps - 0.01) top = pptx.util.Inches(slide_height_inch / 2) left = pptx.util.Inches((slide_width_inch - width.inches * n_steps) / 2 + 0.05) for step in steps: shape = shapes.add_shape(MSO_AUTO_SHAPE_TYPE.CHEVRON, left, top, width, height) shape.text = step.removeprefix(STEP_BY_STEP_PROCESS_MARKER) left += width - INCHES_0_4 elif n_steps > 4: # Vertical display height = pptx.util.Inches(0.65) width = pptx.util.Inches(slide_width_inch * 2/ 3) top = pptx.util.Inches(slide_height_inch / 4) left = INCHES_1 # slide_width_inch - width.inches) for step in steps: shape = shapes.add_shape(MSO_AUTO_SHAPE_TYPE.PENTAGON, left, top, width, height) shape.text = step.removeprefix(STEP_BY_STEP_PROCESS_MARKER) top += height + INCHES_0_3 left += INCHES_0_5 else: # Two steps -- probably not a process return False return True def _handle_key_message( slide: pptx.slide.Slide, slide_json: dict, slide_width_inch: float, slide_height_inch: float ): """ Add a shape to display the key message in the slide, if available. :param slide: The slide to be processed. :param slide_json: The content of the slide as JSON data. :param slide_width_inch: The width of the slide in inches. :param slide_height_inch: The height of the slide in inches. """ if 'key_message' in slide_json and slide_json['key_message']: height = pptx.util.Inches(1.6) width = pptx.util.Inches(slide_width_inch / 2.3) top = pptx.util.Inches(slide_height_inch - height.inches - 0.1) left = pptx.util.Inches((slide_width_inch - width.inches) / 2) shape = slide.shapes.add_shape( MSO_AUTO_SHAPE_TYPE.ROUNDED_RECTANGLE, left=left, top=top, width=width, height=height ) shape.text = slide_json['key_message'] if __name__ == '__main__': _JSON_DATA = ''' { "title": "Understanding AI", "slides": [ { "heading": "Introduction", "bullet_points": [ "Brief overview of AI", [ "Importance of understanding AI" ] ], "key_message": "" }, { "heading": "What is AI?", "bullet_points": [ "Definition of AI", [ "Types of AI", [ "Narrow or weak AI", "General or strong AI" ] ], "Differences between AI and machine learning" ], "key_message": "" }, { "heading": "How AI Works", "bullet_points": [ "Overview of AI algorithms", [ "Types of AI algorithms", [ "Rule-based systems", "Decision tree systems", "Neural networks" ] ], "How AI processes data" ], "key_message": "" }, { "heading": "Building AI Models", "bullet_points": [ ">> Collect data", ">> Select model or architecture to use", ">> Set appropriate parameters", ">> Train model with data", ">> Run inference", ], "key_message": "" }, { "heading": "Pros of AI", "bullet_points": [ "Increased efficiency and productivity", "Improved accuracy and precision", "Enhanced decision-making capabilities", "Personalized experiences" ], "key_message": "AI can be used for many different purposes" }, { "heading": "Cons of AI", "bullet_points": [ "Job displacement and loss of employment", "Bias and discrimination", "Privacy and security concerns", "Dependence on technology" ], "key_message": "" }, { "heading": "Future Prospects of AI", "bullet_points": [ "Advancements in fields such as healthcare and finance", "Increased use" ], "key_message": "" } ] }''' temp = tempfile.NamedTemporaryFile(delete=False, suffix='.pptx') path = pathlib.Path(temp.name) generate_powerpoint_presentation( json5.loads(_JSON_DATA), output_file_path=path, slides_template='Basic' ) print(f'File path: {path}') temp.close()