Financial2API / builders /build_pptx.py
Shivam4096's picture
Upload folder using huggingface_hub
325cb74 verified
Raw History Blame Contribute Delete
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)