前編はこちら → PowerPointの中身は「入れ子の箱」だった ── python-pptxでスライド構造を丸裸にする【前編】
前編では、PPTXの5層構造と python-pptx の基本を押さえた。
後編では、その構造を JSON に変換するanalyze_pptx()と、JSON から PPTX を復元するcreate_pptx_from_json()の実装コードを読み解く。
この2つの関数が揃えば、「PPTX → JSON → LLM が編集 → JSON → PPTX」というラウンドトリップが完成する。
コードを根拠に、AIがスライドを読み書きする仕組みの全体像を掴もう。
先に要点
-
analyze_pptx()がPPTXの全情報をJSON化する。マスター構造とスライド本体の2系統を抽出 -
create_pptx_from_json()がJSONとテンプレートからPPTXを再生成する - LLMはJSONを受け取り、テキスト・書式・配置を理解したうえで編集できる
- テンプレートがなければ復元できない。JSONはコンテンツ、テンプレートはデザインの担当
この記事で分かること
-
analyze_pptx()が何を抽出し、どんなJSONを生成するか - テキスト抽出の核心
extract_text_frame_details()の実装 -
create_pptx_from_json()がJSONをどうPPTXに戻すか - LLM × JSON で実現できるユースケースと、注意すべき制約
今回使う Mermaid 図の種類
| 図の種類 | 用途 |
|---|---|
| flowchart | ラウンドトリップの全体像、テンプレートの役割 |
| sequenceDiagram | analyze_pptx() の処理フロー |
| erDiagram | JSONデータ構造の関係性 |
| stateDiagram-v2 | create_pptx_from_json() のShape処理分岐 |
| quadrantChart | LLMへの情報伝達手法の比較 |
| mindmap | LLM × JSON のユースケース |
1. 全体アーキテクチャ ── 2つの関数が作るラウンドトリップ
今回の仕組みは、pptx_processor.py に実装された2つの関数で成り立っている。
analyze_pptx()が「分解」、create_pptx_from_json()が「組み立て」を担当する。元のPPTXはデータソースであると同時に、復元時のテンプレートとしても使われる。
2. analyze_pptx() を読む ── PPTX → JSON の全処理
この関数は pptx_processor.py の中核で、PPTXファイルを受け取り、マスター構造とスライド本体の2系統をJSON化して返す。
処理は2フェーズ。第1フェーズでテンプレート側のレイアウト情報を収集し、第2フェーズでスライド本体の全シェイプを走査する。戻り値の
mastersとslidesがそれぞれの成果物。
関数の入り口
def analyze_pptx(file_like_object):
prs = Presentation(file_like_object)
presentation_data = {"masters": []}
# === フェーズ1: マスター/レイアウト ===
for master in prs.slide_masters:
master_data = {"layouts": []}
for layout in master.slide_layouts:
layout_data = {"name": layout.name, "shapes": []}
# ... 各shapeの情報を収集 ...
master_data["layouts"].append(layout_data)
presentation_data["masters"].append(master_data)
# === フェーズ2: スライド本体 ===
presentation_data["slides"] = []
for slide_idx, slide in enumerate(prs.slides):
slide_data = {
"layout": prs.slide_layouts.index(slide.slide_layout),
"shapes": [],
"notes": slide.notes_slide.notes_text_frame.text if slide.has_notes_slide else ""
}
# ... 各shapeの情報を収集 ...
presentation_data["slides"].append(slide_data)
return presentation_data
このコードがやっていることは3つ。
-
Presentation()でPPTXを読み込み、空の辞書presentation_dataを初期化 -
フェーズ1:
prs.slide_mastersを走査し、各レイアウトのプレースホルダー情報を記録 -
フェーズ2:
prs.slidesを走査し、各スライドのシェイプ情報を記録
slide_data の "layout" キーには、そのスライドが使っているレイアウトの インデックス番号 が入る。復元時にはこの番号で prs.slide_layouts[n] を参照するため、テンプレートとインデックスが一致していることが前提になる。
Shape情報の収集 ── 種類に応じた分岐
フェーズ2のシェイプ走査では、Shape の種類ごとに取得する情報が異なる。
for shape in slide.shapes:
shape_data = {
"name": shape.name,
"left": shape.left.pt, "top": shape.top.pt,
"width": shape.width.pt, "height": shape.height.pt,
"rotation": shape.rotation,
"shape_type": shape.shape_type.name,
"is_placeholder": shape.is_placeholder,
}
# 画像の処理
if shape.shape_type == MSO_SHAPE_TYPE.PICTURE:
image = shape.image
b64_image = base64.b64encode(image.blob).decode('utf-8')
shape_data["image_data"] = {
"content": b64_image,
"content_type": image.content_type
}
# 塗りつぶし・線の取得
if hasattr(shape, 'fill'):
shape_data["fill"] = get_fill_info(shape.fill)
if hasattr(shape, 'line'):
shape_data["line"] = get_line_info(shape.line)
# プレースホルダー情報
if shape.is_placeholder:
ph = shape.placeholder_format
shape_data["placeholder"] = {"type": ph.type.name, "idx": ph.idx}
# テキストフレーム(最も情報量が多い)
if hasattr(shape, 'has_text_frame') and shape.has_text_frame:
shape_data["text_frame"] = extract_text_frame_details(shape.text_frame)
全Shape共通の属性(位置、サイズ、名前)をまず記録し、そのあと shape_type や is_placeholder で分岐して追加情報を取得する。画像は base64.b64encode() でテキスト化し、JSONに埋め込める形にしている。
3. extract_text_frame_details() ── 書式抽出の核心
テキスト情報の抽出はこの関数が担う。5層構造の下3層(TextFrame → Paragraph → Run)を再帰的に辿り、書式まで含めて辞書化する。
def extract_text_frame_details(tf):
text_frame_data = {
"text": tf.text,
"margin_left": tf.margin_left.pt if tf.margin_left else 0,
"margin_right": tf.margin_right.pt if tf.margin_right else 0,
"margin_top": tf.margin_top.pt if tf.margin_top else 0,
"margin_bottom": tf.margin_bottom.pt if tf.margin_bottom else 0,
"word_wrap": tf.word_wrap,
"auto_size": tf.auto_size.name if tf.auto_size else "NONE",
"paragraphs": []
}
for para in tf.paragraphs:
para_data = {
"text": para.text,
"level": para.level,
"alignment": para.alignment.name if para.alignment else "NONE",
"space_before": para.space_before.pt if para.space_before else 0,
"space_after": para.space_after.pt if para.space_after else 0,
"runs": []
}
for run in para.runs:
font_data = {
"text": run.text,
"bold": run.font.bold,
"italic": run.font.italic,
"size": run.font.size.pt if run.font.size else None,
"name": run.font.name,
}
if run.font.color.type:
font_data["color"] = get_color_info(run.font.color)
para_data["runs"].append(font_data)
text_frame_data["paragraphs"].append(para_data)
return text_frame_data
この関数が返す辞書の構造を見ると、LLMにとって何が「読める」ようになるか がわかる。
- TextFrame レベル: 余白、折り返し、自動サイズ調整の設定
- Paragraph レベル: 配置(左寄せ/中央)、インデント、段落間スペース
- Run レベル: テキスト本体、太字、イタリック、フォント名、サイズ、色
つまりこの関数は、人間がPowerPoint上で目にしている書式情報を、すべてテキスト(辞書)に翻訳している。LLMはこの辞書を受け取ることで、「3枚目のタイトルは游ゴシック28pt太字の青文字」といった情報まで把握できるようになる。
4. JSONの全体構造 ── データの関係性
analyze_pptx() が出力するJSONは、PPTXの5層構造を忠実に反映している。
JSONのルートには
masters(テンプレート構造)とslides(スライド本体)がある。Slide は Layout をインデックスで参照する。Shape から下は TextFrame → Paragraph → Run が入れ子になり、Run に書式情報が詰まっている。
5. create_pptx_from_json() を読む ── JSON → PPTX の復元
JSON化されたデータを、テンプレートと組み合わせてPPTXに戻す関数。処理の中心はShapeの種類に応じた分岐だ。
def create_pptx_from_json(json_data, template_path):
prs = Presentation(template_path)
data = json.loads(json_data)
for slide_data in data.get("slides", []):
layout_index = slide_data.get("layout", 1)
slide_layout = prs.slide_layouts[layout_index]
slide = prs.slides.add_slide(slide_layout)
for shape_data in slide_data.get("shapes", []):
shape = None
is_placeholder = shape_data.get("is_placeholder", False)
if is_placeholder:
# テンプレートの既存プレースホルダーを取得
ph_data = shape_data.get("placeholder")
shape = slide.placeholders[ph_data["idx"]]
else:
# 新しいシェイプを追加
shape_type = shape_data.get("shape_type")
if shape_type == "AUTO_SHAPE":
shape = slide.shapes.add_shape(
MSO_AUTO_SHAPE_TYPE.RECTANGLE,
Pt(shape_data.get("left", 0)), Pt(shape_data.get("top", 0)),
Pt(shape_data.get("width", 100)), Pt(shape_data.get("height", 100))
)
elif shape_type == "TEXT_BOX":
shape = slide.shapes.add_textbox(
Pt(shape_data.get("left", 0)), Pt(shape_data.get("top", 0)),
Pt(shape_data.get("width", 100)), Pt(shape_data.get("height", 100))
)
if shape is None:
continue
_apply_fill(shape, shape_data.get("fill"))
_apply_line(shape, shape_data.get("line"))
if "text_frame" in shape_data:
_populate_text_frame(shape, shape_data["text_frame"])
bio = io.BytesIO()
prs.save(bio)
bio.seek(0)
return bio
この関数の処理フローを分岐で整理する。
is_placeholderで大きく分岐する。プレースホルダーなら既存の領域を取得して上書き、そうでなければ新規Shapeを追加する。どちらの場合も、最後に fill → line → テキストの順で書式を適用する。
ここで重要なのは Presentation(template_path) でテンプレートを渡している点だ。JSONの layout_index は、このテンプレートの slide_layouts 配列のインデックスに対応する。テンプレートが異なれば、同じインデックスでも全く違うレイアウトになるため、JSONとテンプレートは常にペアで管理する必要がある。
6. なぜ JSON なのか ── LLMへの伝達手法の比較
LLMにスライド情報を渡す方法は他にもある。なぜJSONが最適なのか。
JSON構造化は情報の正確性と編集可能性の両方で優れている。テキスト抽出は書式情報が消え、スクリーンショットは見た目は伝わるが編集に使えない。
| アプローチ | 書式情報 | 配置情報 | 編集可能 | トークン効率 |
|---|---|---|---|---|
| テキスト抽出 | 消える | 消える | 内容のみ | 高い |
| スクリーンショット | 目視のみ | 目視のみ | 不可 | 低い |
| JSON構造化 | Run単位で保持 | pt単位で保持 | 完全対応 | 中程度 |
7. LLM × JSON でできること
LLMの活用は「分析」「編集」「生成」の3カテゴリ。いずれもJSONを入力に受け取り、修正済みJSONを返す形で実現できる。
具体例:LLMへの指示
編集の場合: 「3枚目のスライドのタイトルを、もっとキャッチーなものに書き換えてください」
→ LLMは slides[2].shapes から placeholder.type == "TITLE" の Shape を特定し、runs[0].text を書き換えたJSONを返す
生成の場合: 企画書テキスト + テンプレートの masters 構造を渡し、「この企画書の内容で5枚のスライドを作ってください。レイアウトとプレースホルダーは masters の情報を参考に選んでください」
→ LLMは適切な layout インデックスを選び、各 placeholder.idx にコンテンツを配置したJSONを生成する
8. 注意点・ハマりどころ
-
テンプレートとレイアウト番号の一致: JSONの
layout: 2はテンプレートのslide_layouts[2]を指す。テンプレートを差し替えると対応関係が壊れる - 画像のBase64肥大化: 画像入りスライドをJSON化すると数MBになることがある。LLMのトークン制限に注意が必要
-
テーマカラーの互換性:
"type": "THEME", "value": "ACCENT_1"はテンプレート依存。異なるテンプレートでは異なる色になる -
LLMが返すJSONの構文エラー: 閉じ括弧の欠落やカンマ過不足が起きることがある。
json.loads()でバリデーションを挟むのが安全 -
プレースホルダーの idx 不一致: テンプレートに存在しない
idxを指定するとKeyErrorになる。create_pptx_from_json()内でtry/exceptで保護されているが、シェイプが欠落する
一言で言うと何者か
analyze_pptx() と create_pptx_from_json() は、PowerPoint の情報を「LLM が読み書きできるJSON」との間で往復変換する関数ペア だ。テンプレートと組み合わせれば、AIがスライドを分析・編集・生成するための完全な基盤が手に入る。
まとめ
-
analyze_pptx()はPPTXの全情報を{"masters": [...], "slides": [...]}のJSONに変換する - 書式抽出の核心は
extract_text_frame_details()で、Run単位のフォント情報まで辞書化する -
create_pptx_from_json()はis_placeholderで分岐し、テンプレートのレイアウトに沿ってShapeを配置・復元する - LLMはJSONを介してスライドの 分析・編集・生成 いずれも実行可能
- JSONとテンプレートは常にペア。JSONはコンテンツ担当、テンプレートはデザイン担当。どちらが欠けても完全なPPTXは作れない
PowerPointは「人が手で作るもの」から「AIと協働で作るもの」へ変わりつつある。
その橋渡しをするのが、pptx_processor.py に実装された2つの関数と、JSONという共通言語だ。
GitHub リポジトリ
この記事で解説しているコードの全体は、以下のリポジトリで公開しています。
PPTX → JSON 変換、JSON → PPTX 復元、Streamlit Web UI、CLI 実行スクリプトなど、すべてのソースコードを含んでいます。
前編はこちら → PowerPointの中身は「入れ子の箱」だった ── python-pptxでスライド構造を丸裸にする【前編】