Spaces:
Sleeping
Sleeping
Download builders/build_pptx.py from Shivam4096/Financial2API: direct link, hf CLI and curl.
- Browser
- Download file 18.8 kB
-
https://huggingface.co/spaces/Shivam4096/Financial2API/resolve/main/builders/build_pptx.py
- Command line
-
hf download hf://spaces/Shivam4096/Financial2API/builders/build_pptx.py
-
curl -L -o build_pptx.py https://huggingface.co/spaces/Shivam4096/Financial2API/resolve/main/builders/build_pptx.py
18.8 kB
| """ | |
| builders/build_pptx.py -- PowerPoint presentation builder. | |
| Generates a widescreen (16:9) PPTX presentation summarizing the financial analysis | |
| and AI narrative, strictly matching user-defined theme, colors, and layout structure. | |
| """ | |
| import os | |
| import sys | |
| import datetime | |
| from pathlib import Path | |
| from typing import Dict, List | |
| # import python-pptx classes | |
| from pptx import Presentation | |
| from pptx.util import Inches, Pt | |
| from pptx.dml.color import RGBColor | |
| from pptx.enum.text import PP_ALIGN | |
| from pptx.enum.shapes import MSO_SHAPE | |
| from pptx.enum.shapes import MSO_CONNECTOR | |
| # Color Palette defined in User Rules | |
| COLOR_DARK_TEXT = RGBColor(0, 0, 0) # 000000 (Black) | |
| COLOR_LIGHT_BG = RGBColor(255, 255, 255) # FFFFFF (White) | |
| COLOR_DARK_GREEN = RGBColor(0, 132, 0) # 008400 (Green Bar 1) | |
| COLOR_LIGHT_GREEN = RGBColor(149, 203, 137) # 95CB89 (Green Bar 2) | |
| COLOR_GRAY_LINE = RGBColor(0, 0, 0) # 000000 (Black bottom line) | |
| COLOR_DARK_SLATE = RGBColor(69, 95, 81) # 455F51 (Dark slate green) | |
| COLOR_LIGHT_GRAY = RGBColor(227, 222, 209) # E3DED1 (Light warm gray/beige) | |
| COLOR_ACCENT = RGBColor(84, 158, 57) # 549E39 (Accent green) | |
| FONT_MAJOR = "Helvetica Neue Bold" | |
| FONT_MINOR = "Helvetica Neue Light" | |
| FONT_MAJOR_FALLBACK = "Arial" | |
| FONT_MINOR_FALLBACK = "Arial" | |
| def _apply_theme_decorations(slide, prs): | |
| """ | |
| Applies the layout rectangles and bottom line strictly defined in the | |
| Slide Master Layout. | |
| """ | |
| # 1. Decorative top-left dark green bar | |
| # rect: x: 0.15, y: 0.0, w: 0.36, h: 1.16 | |
| rect1 = slide.shapes.add_shape( | |
| MSO_SHAPE.RECTANGLE, | |
| Inches(0.15), Inches(0.0), Inches(0.36), Inches(1.16) | |
| ) | |
| rect1.fill.solid() | |
| rect1.fill.fore_color.rgb = COLOR_DARK_GREEN | |
| rect1.line.fill.background() # No border line | |
| # 2. Decorative top-left light green bar | |
| # rect: x: 0.55, y: 0.0, w: 0.11, h: 1.39 | |
| rect2 = slide.shapes.add_shape( | |
| MSO_SHAPE.RECTANGLE, | |
| Inches(0.55), Inches(0.0), Inches(0.11), Inches(1.39) | |
| ) | |
| rect2.fill.solid() | |
| rect2.fill.fore_color.rgb = COLOR_LIGHT_GREEN | |
| rect2.line.fill.background() # No border line | |
| # 3. Decorative bottom line | |
| # line: x: 0.67, y: 6.83, w: 12.0, h: 0.0 | |
| connector = slide.shapes.add_connector( | |
| MSO_CONNECTOR.STRAIGHT, | |
| Inches(0.67), Inches(6.83), Inches(12.67), Inches(6.83) | |
| ) | |
| connector.line.color.rgb = COLOR_GRAY_LINE | |
| connector.line.width = Pt(1) | |
| def _add_slide_title(slide, text: str): | |
| """ | |
| Adds the Slide Title using Layout 7 Title configurations: | |
| placeholder: x: 0.81, y: 0.29, w: 11.74, h: 0.38, align: left, valign: top | |
| """ | |
| tx_box = slide.shapes.add_textbox( | |
| Inches(0.81), Inches(0.29), Inches(11.74), Inches(0.38) | |
| ) | |
| tf = tx_box.text_frame | |
| tf.word_wrap = True | |
| tf.margin_top = 0 | |
| tf.margin_bottom = 0 | |
| tf.margin_left = 0 | |
| tf.margin_right = 0 | |
| p = tf.paragraphs[0] | |
| p.text = text | |
| p.font.name = FONT_MAJOR | |
| p.font.size = Pt(20) | |
| p.font.bold = True | |
| p.font.color.rgb = COLOR_DARK_TEXT | |
| p.alignment = PP_ALIGN.LEFT | |
| def _add_bullet_points(slide, bullets: List[str], top_inch=1.56, height_inch=5.14): | |
| """ | |
| Adds bullet points body text block using Master 2 Body: | |
| placeholder: x: 0.67, y: top, w: 12.0, h: height | |
| """ | |
| tx_box = slide.shapes.add_textbox( | |
| Inches(0.67), Inches(top_inch), Inches(12.0), Inches(height_inch) | |
| ) | |
| tf = tx_box.text_frame | |
| tf.word_wrap = True | |
| tf.margin_top = 0 | |
| tf.margin_bottom = 0 | |
| tf.margin_left = 0 | |
| tf.margin_right = 0 | |
| for i, b in enumerate(bullets): | |
| p = tf.add_paragraph() if i > 0 else tf.paragraphs[0] | |
| p.text = "• " + b | |
| p.font.name = FONT_MINOR | |
| p.font.size = Pt(15) | |
| p.font.color.rgb = COLOR_DARK_TEXT | |
| p.space_after = Pt(8) | |
| p.line_spacing = 1.2 | |
| def _style_table_cell(cell, text: str, font_name=FONT_MINOR, font_size=11, bold=False, text_color=COLOR_DARK_TEXT, bg_color=None, align=PP_ALIGN.CENTER): | |
| """Styles cell texts and background fills.""" | |
| cell.text = text | |
| p = cell.text_frame.paragraphs[0] | |
| p.alignment = align | |
| p.font.name = font_name | |
| p.font.size = Pt(font_size) | |
| p.font.bold = bold | |
| p.font.color.rgb = text_color | |
| if bg_color: | |
| cell.fill.solid() | |
| cell.fill.fore_color.rgb = bg_color | |
| def build_presentation(data: Dict, output_path: str): | |
| """ | |
| Assembles a widescreen PowerPoint report from scraped financial data and AI models output. | |
| """ | |
| prs = Presentation() | |
| # Strictly configure widescreen 16:9 aspect ratio | |
| prs.slide_width = Inches(13.333) | |
| prs.slide_height = Inches(7.5) | |
| blank_layout = prs.slide_layouts[6] # Blank slide template | |
| company_name = data.get("company", {}).get("name", "Unknown Company") | |
| ticker = data.get("company", {}).get("ticker", "N/A") | |
| industry = data.get("company", {}).get("industry", "N/A") | |
| sector = data.get("company", {}).get("sector", "N/A") | |
| date_str = data.get("company", {}).get("report_date", datetime.date.today().isoformat()) | |
| years = data.get("years", []) | |
| # ========================================== | |
| # SLIDE 1: Title Slide | |
| # ========================================== | |
| slide_title = prs.slides.add_slide(blank_layout) | |
| _apply_theme_decorations(slide_title, prs) | |
| # Title Text Frame | |
| title_box = slide_title.shapes.add_textbox( | |
| Inches(1.0), Inches(2.2), Inches(11.3), Inches(2.0) | |
| ) | |
| tf = title_box.text_frame | |
| tf.word_wrap = True | |
| p1 = tf.paragraphs[0] | |
| p1.text = company_name | |
| p1.font.name = FONT_MAJOR | |
| p1.font.size = Pt(40) | |
| p1.font.bold = True | |
| p1.font.color.rgb = COLOR_DARK_TEXT | |
| p2 = tf.add_paragraph() | |
| p2.text = f"Financial Analysis & Investment Insights ({ticker})" | |
| p2.font.name = FONT_MINOR | |
| p2.font.size = Pt(22) | |
| p2.font.color.rgb = COLOR_DARK_SLATE | |
| p2.space_before = Pt(8) | |
| # Subtitle Info (Date / Authors) | |
| sub_box = slide_title.shapes.add_textbox( | |
| Inches(1.0), Inches(5.0), Inches(11.3), Inches(1.2) | |
| ) | |
| tf_sub = sub_box.text_frame | |
| p_sub = tf_sub.paragraphs[0] | |
| p_sub.text = f"Report Date: {date_str} | Sector: {sector} | Industry: {industry}" | |
| p_sub.font.name = FONT_MINOR | |
| p_sub.font.size = Pt(13) | |
| p_sub.font.color.rgb = COLOR_DARK_SLATE | |
| p_sub2 = tf_sub.add_paragraph() | |
| p_sub2.text = "Disclaimer: This report is for informational purposes only and does not constitute investment advice." | |
| p_sub2.font.name = FONT_MINOR | |
| p_sub2.font.size = Pt(11) | |
| p_sub2.font.color.rgb = COLOR_DARK_SLATE | |
| p_sub2.space_before = Pt(6) | |
| # ========================================== | |
| # DYNAMIC AI SLIDES (From ppt_json) | |
| # ========================================== | |
| import json | |
| import re | |
| ppt_json_str = data.get("ppt_json", "") | |
| ai_slides = [] | |
| if ppt_json_str: | |
| match = re.search(r'\[.*\]', ppt_json_str, re.DOTALL) | |
| if match: | |
| try: | |
| ai_slides = json.loads(match.group(0)) | |
| except Exception: | |
| pass | |
| if ai_slides: | |
| for slide_data in ai_slides: | |
| title = slide_data.get("title", "Analysis Slide") | |
| bullets = slide_data.get("bullets", []) | |
| if not bullets: continue | |
| s = prs.slides.add_slide(blank_layout) | |
| _apply_theme_decorations(s, prs) | |
| _add_slide_title(s, title) | |
| clean_bullets = [str(b).strip() for b in bullets if b] | |
| _add_bullet_points(s, clean_bullets) | |
| else: | |
| # Fallback to Exec Summary if JSON parsing failed | |
| exec_summary_text = data.get("executive_summary", "") | |
| if exec_summary_text and len(exec_summary_text) > 50: | |
| slide_exec = prs.slides.add_slide(blank_layout) | |
| _apply_theme_decorations(slide_exec, prs) | |
| _add_slide_title(slide_exec, "Executive Summary") | |
| paragraphs = [p.strip() for p in exec_summary_text.split('\n') if p.strip()] | |
| _add_bullet_points(slide_exec, paragraphs[:5]) | |
| # ========================================== | |
| # SLIDE 3: Key Financial Highlights Table | |
| # ========================================== | |
| if years: | |
| slide_metrics = prs.slides.add_slide(blank_layout) | |
| _apply_theme_decorations(slide_metrics, prs) | |
| _add_slide_title(slide_metrics, "Key Financial Performance") | |
| # Build Grid Table | |
| cols = len(years) + 1 | |
| rows = 6 | |
| table_shape = slide_metrics.shapes.add_table( | |
| rows, cols, Inches(0.8), Inches(1.6), Inches(11.7), Inches(3.8) | |
| ) | |
| table = table_shape.table | |
| # Column Headers | |
| _style_table_cell(table.cell(0, 0), "Financial Metric", FONT_MAJOR, 12, True, COLOR_LIGHT_BG, COLOR_DARK_SLATE, PP_ALIGN.LEFT) | |
| for idx, y in enumerate(years): | |
| _style_table_cell(table.cell(0, idx+1), str(y), FONT_MAJOR, 12, True, COLOR_LIGHT_BG, COLOR_DARK_SLATE) | |
| metrics = [ | |
| ("Revenue", "revenue", "income_statement", 1.0), | |
| ("Gross Profit", "gross_profit", "income_statement", 1.0), | |
| ("Operating Income (EBIT)", "operating_income", "income_statement", 1.0), | |
| ("Net Income", "net_income", "income_statement", 1.0), | |
| ("Operating Cash Flow (OCF)", "cash_from_operations", "cash_flow", 1.0) | |
| ] | |
| currency = data.get("company", {}).get("currency", "USD") | |
| units = data.get("company", {}).get("units", "millions") | |
| for r_idx, (m_label, dict_key, sheet_name, scale) in enumerate(metrics): | |
| curr_row = r_idx + 1 | |
| bg_color = COLOR_LIGHT_GRAY if curr_row % 2 == 1 else COLOR_LIGHT_BG | |
| # Label cell | |
| _style_table_cell(table.cell(curr_row, 0), f"{m_label} ({currency} {units.capitalize()})", FONT_MAJOR, 11, False, COLOR_DARK_TEXT, bg_color, PP_ALIGN.LEFT) | |
| # Values cells | |
| sheet_data = data.get(sheet_name, {}) | |
| val_list = sheet_data.get(dict_key, []) | |
| for c_idx, y in enumerate(years): | |
| val_str = "N/A" | |
| if c_idx < len(val_list) and val_list[c_idx] is not None: | |
| val_str = f"{val_list[c_idx]:,.1f}" | |
| _style_table_cell(table.cell(curr_row, c_idx+1), val_str, FONT_MINOR, 11, False, COLOR_DARK_TEXT, bg_color) | |
| # Brief Narrative summary underneath table | |
| rev_commentary = data.get("trend_analysis", {}).get("revenue_and_profitability", "") | |
| if rev_commentary: | |
| comment_box = slide_metrics.shapes.add_textbox(Inches(0.8), Inches(5.6), Inches(11.7), Inches(1.1)) | |
| tf_c = comment_box.text_frame | |
| tf_c.word_wrap = True | |
| p_c = tf_c.paragraphs[0] | |
| p_c.text = "Key Takeaway: " + (rev_commentary[:260] + "..." if len(rev_commentary) > 260 else rev_commentary) | |
| p_c.font.name = FONT_MINOR | |
| p_c.font.size = Pt(12) | |
| p_c.font.italic = True | |
| p_c.font.color.rgb = COLOR_DARK_SLATE | |
| # ========================================== | |
| # SLIDE 4: Dupont ROE Decomposition | |
| # ========================================== | |
| dp_data = data.get("dupont", {}) | |
| if dp_data and "years" in dp_data: | |
| slide_dp = prs.slides.add_slide(blank_layout) | |
| _apply_theme_decorations(slide_dp, prs) | |
| _add_slide_title(slide_dp, "DuPont ROE Analysis (Latest 3 Years)") | |
| dp_years = dp_data.get("years", []) | |
| cols = len(dp_years) + 1 | |
| rows = 5 | |
| table_shape = slide_dp.shapes.add_table( | |
| rows, cols, Inches(0.8), Inches(1.6), Inches(11.7), Inches(3.2) | |
| ) | |
| table = table_shape.table | |
| # Headers | |
| _style_table_cell(table.cell(0, 0), "DuPont Component", FONT_MAJOR, 12, True, COLOR_LIGHT_BG, COLOR_DARK_SLATE, PP_ALIGN.LEFT) | |
| for idx, y in enumerate(dp_years): | |
| _style_table_cell(table.cell(0, idx+1), str(y), FONT_MAJOR, 12, True, COLOR_LIGHT_BG, COLOR_DARK_SLATE) | |
| components = [ | |
| ("Net Profit Margin (%)", "Net Margin"), | |
| ("Asset Turnover (x)", "Asset Turnover"), | |
| ("Equity Multiplier (x)", "Equity Multiplier"), | |
| ("Return on Equity (ROE %)", "ROE") | |
| ] | |
| for r_idx, (c_label, key) in enumerate(components): | |
| curr_row = r_idx + 1 | |
| bg_color = COLOR_LIGHT_GRAY if curr_row % 2 == 1 else COLOR_LIGHT_BG | |
| is_bold = (curr_row == 4) # Bold ROE row | |
| _style_table_cell(table.cell(curr_row, 0), c_label, FONT_MAJOR if is_bold else FONT_MINOR, 11, is_bold, COLOR_DARK_TEXT, bg_color, PP_ALIGN.LEFT) | |
| vals_list = dp_data.get(key, []) | |
| for c_idx, y in enumerate(dp_years): | |
| val_str = "N/A" | |
| if c_idx < len(vals_list) and vals_list[c_idx] is not None: | |
| val_val = vals_list[c_idx] | |
| if "%" in c_label or "ROE" in key: | |
| val_str = f"{val_val * 100:.1f}%" if val_val < 1.0 else f"{val_val:.1f}%" | |
| else: | |
| val_str = f"{val_val:.2f}x" | |
| _style_table_cell(table.cell(curr_row, c_idx+1), val_str, FONT_MAJOR if is_bold else FONT_MINOR, 11, is_bold, COLOR_DARK_TEXT, bg_color) | |
| # DuPont Commentary Text Box | |
| commentary = data.get("trend_analysis", {}).get("balance_sheet_strength", "") | |
| if commentary: | |
| comment_box = slide_dp.shapes.add_textbox(Inches(0.8), Inches(5.0), Inches(11.7), Inches(1.6)) | |
| tf_c = comment_box.text_frame | |
| tf_c.word_wrap = True | |
| p_c = tf_c.paragraphs[0] | |
| p_c.text = "DuPont Drivers & Balance Sheet Strength: " + (commentary[:380] + "..." if len(commentary) > 380 else commentary) | |
| p_c.font.name = FONT_MINOR | |
| p_c.font.size = Pt(12) | |
| p_c.font.color.rgb = COLOR_DARK_TEXT | |
| p_c.line_spacing = 1.2 | |
| # ========================================== | |
| # SLIDE 5: Competitor Benchmarking | |
| # ========================================== | |
| peers_list = data.get("peers", []) | |
| if peers_list: | |
| slide_peers = prs.slides.add_slide(blank_layout) | |
| _apply_theme_decorations(slide_peers, prs) | |
| _add_slide_title(slide_peers, "Competitor Benchmarking") | |
| # Build Grid Table | |
| cols = 5 | |
| rows = min(len(peers_list) + 1, 6) | |
| table_shape = slide_peers.shapes.add_table( | |
| rows, cols, Inches(0.8), Inches(1.6), Inches(11.7), Inches(3.2) | |
| ) | |
| table = table_shape.table | |
| # Headers | |
| headers = ["Company", "Revenue Growth", "Operating Margin", "P/E Ratio", "ROE"] | |
| for c_idx, h in enumerate(headers): | |
| _style_table_cell(table.cell(0, c_idx), h, FONT_MAJOR, 12, True, COLOR_LIGHT_BG, COLOR_DARK_SLATE, PP_ALIGN.LEFT if c_idx == 0 else PP_ALIGN.CENTER) | |
| for r_idx in range(rows - 1): | |
| peer = peers_list[r_idx] | |
| curr_row = r_idx + 1 | |
| bg_color = COLOR_LIGHT_GRAY if curr_row % 2 == 1 else COLOR_LIGHT_BG | |
| # Format peer outputs | |
| rev_gr = peer.get("revenue_growth", "N/A") | |
| if isinstance(rev_gr, float): rev_gr = f"{rev_gr*100:.1f}%" | |
| op_marg = peer.get("operating_margin", "N/A") | |
| if isinstance(op_marg, float): op_marg = f"{op_marg*100:.1f}%" | |
| pe_r = peer.get("pe_ratio", "N/A") | |
| if isinstance(pe_r, float): pe_r = f"{pe_r:.1f}x" | |
| roe_val = peer.get("roe", "N/A") | |
| if isinstance(roe_val, float): roe_val = f"{roe_val*100:.1f}%" | |
| _style_table_cell(table.cell(curr_row, 0), peer.get("name", "N/A"), FONT_MAJOR, 11, False, COLOR_DARK_TEXT, bg_color, PP_ALIGN.LEFT) | |
| _style_table_cell(table.cell(curr_row, 1), str(rev_gr), FONT_MINOR, 11, False, COLOR_DARK_TEXT, bg_color) | |
| _style_table_cell(table.cell(curr_row, 2), str(op_marg), FONT_MINOR, 11, False, COLOR_DARK_TEXT, bg_color) | |
| _style_table_cell(table.cell(curr_row, 3), str(pe_r), FONT_MINOR, 11, False, COLOR_DARK_TEXT, bg_color) | |
| _style_table_cell(table.cell(curr_row, 4), str(roe_val), FONT_MINOR, 11, False, COLOR_DARK_TEXT, bg_color) | |
| # Peer comparison commentary | |
| peer_comm = data.get("peer_comparison_commentary", "") | |
| if peer_comm: | |
| comment_box = slide_peers.shapes.add_textbox(Inches(0.8), Inches(5.0), Inches(11.7), Inches(1.6)) | |
| tf_c = comment_box.text_frame | |
| tf_c.word_wrap = True | |
| p_c = tf_c.paragraphs[0] | |
| p_c.text = "Benchmarking Insights: " + (peer_comm[:380] + "..." if len(peer_comm) > 380 else peer_comm) | |
| p_c.font.name = FONT_MINOR | |
| p_c.font.size = Pt(12) | |
| p_c.font.color.rgb = COLOR_DARK_TEXT | |
| p_c.line_spacing = 1.2 | |
| # ========================================== | |
| # SLIDE 6: Financial Visualizations | |
| # ========================================== | |
| try: | |
| from builders.chart_builder import generate_all_charts | |
| charts = generate_all_charts(data) | |
| for chart_key, title in [ | |
| ("revenue_net_income", "Revenue & Net Income Trend"), | |
| ("margins", "Profitability Margins"), | |
| ("cash_flow", "Cash Flow & CAPEX Overview") | |
| ]: | |
| if charts.get(chart_key): | |
| slide_chart = prs.slides.add_slide(blank_layout) | |
| _apply_theme_decorations(slide_chart, prs) | |
| _add_slide_title(slide_chart, title) | |
| # Add picture centrally | |
| slide_chart.shapes.add_picture( | |
| charts[chart_key], | |
| Inches(1.5), Inches(1.5), width=Inches(10.0) | |
| ) | |
| except Exception as e: | |
| import sys | |
| print(f"Warning: Could not generate PPTX charts: {e}", file=sys.stderr) | |
| # Save Presentation | |
| os.makedirs(os.path.dirname(os.path.abspath(output_path)), exist_ok=True) | |
| prs.save(output_path) | |