# export/ppt.py from __future__ import annotations from typing import Dict, List, Tuple from datetime import datetime try: from pptx import Presentation from pptx.util import Inches, Pt from pptx.enum.text import PP_ALIGN from pptx.dml.color import RGBColor except ModuleNotFoundError as e: # 呼び出し側でこのエラーをユーザに分かりやすく返すため、敢えてそのまま投げる raise MAX_BULLETS = 10 def _add_title(prs: Presentation, title: str, subtitle: str | None = None): slide_layout = prs.slide_layouts[0] # Title Slide slide = prs.slides.add_slide(slide_layout) slide.shapes.title.text = title if subtitle is not None: slide.placeholders[1].text = subtitle def _add_title_and_bullets(prs: Presentation, title: str, bullets: List[str]): slide_layout = prs.slide_layouts[1] # Title and Content slide = prs.slides.add_slide(slide_layout) slide.shapes.title.text = title tx = slide.shapes.placeholders[1].text_frame tx.clear() # 先頭段落 if not bullets: bullets = ["(該当なし)"] bullets = bullets[:MAX_BULLETS] p = tx.paragraphs[0] p.text = bullets[0] p.level = 0 for b in bullets[1:]: rp = tx.add_paragraph() rp.text = b rp.level = 0 def _add_sources(prs: Presentation, links: List[str]): slide_layout = prs.slide_layouts[5] # Title Only slide = prs.slides.add_slide(slide_layout) slide.shapes.title.text = "根拠リンク / 参考資料" left = Inches(0.8); top = Inches(1.8); width = Inches(9); height = Inches(5) box = slide.shapes.add_textbox(left, top, width, height) tf = box.text_frame tf.word_wrap = True if not links: tf.text = "(なし)" return tf.clear() for i, url in enumerate(links, 1): p = tf.add_paragraph() if i > 1 else tf.paragraphs[0] p.text = f"{i}. {url}" p.level = 0 def _split_lines(s: str, sep: str = "\n") -> List[str]: s = (s or "").strip() if not s: return [] out = [ln.strip(" \t\u3000") for ln in s.split(sep)] return [ln for ln in out if ln] def build_deck(sections: Dict[str, str | List[str]], links: List[str] | None = None) -> Presentation: """ sections 例: { "highlights": "• 売上+10%\n• 営業益+5%\n...", "outlook": "...", "segments": "...", "finance": "...", "shareholder": "...", "esg": "...", "risks": "..." } """ prs = Presentation() # 1) タイトル today = datetime.now().strftime("%Y-%m-%d") _add_title(prs, "IR/PR Co-Pilot Pro 自動生成スライド", f"作成日: {today}") # 2) 目次 toc = [ "業績ハイライト", "見通し", "セグメント", "財務", "株主還元", "ESG", "リスク", "想定Q&A(抜粋)", "参考リンク", ] _add_title_and_bullets(prs, "目次", toc) # 3) 各セクション def bullets_of(key: str) -> List[str]: v = sections.get(key, "") if sections else "" if isinstance(v, list): return [str(x) for x in v if str(x).strip()] return _split_lines(str(v or "")) _add_title_and_bullets(prs, "業績ハイライト", bullets_of("highlights")) _add_title_and_bullets(prs, "見通し", bullets_of("outlook")) _add_title_and_bullets(prs, "セグメント", bullets_of("segments")) _add_title_and_bullets(prs, "財務", bullets_of("finance")) _add_title_and_bullets(prs, "株主還元", bullets_of("shareholder")) _add_title_and_bullets(prs, "ESG", bullets_of("esg")) _add_title_and_bullets(prs, "リスク", bullets_of("risks")) # 4) 想定Q&A(別モジュール側で上位N件だけ詰めて来てもOK) qas = sections.get("qa_top", []) if isinstance(qas, str): qas = _split_lines(qas) qa_bullets = [] if qas: # "Q: ... / A: ..." 形式を1行ずつ箇条書き for qa in qas[:MAX_BULLETS]: qa_bullets.append(qa) _add_title_and_bullets(prs, "想定Q&A(抜粋)", qa_bullets or ["(別添CSV参照)"]) # 5) 参考リンク _add_sources(prs, list(dict.fromkeys(links or []))) return prs def save_pptx(prs: Presentation, path: str) -> None: import os os.makedirs(os.path.dirname(path), exist_ok=True) prs.save(path)