0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【後編】PowerPointをJSONに変換してLLMに読ませる ── 実装コードで理解するAIスライド編集

0
Last updated at Posted at 2026-04-11

前編はこちら → 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フェーズでスライド本体の全シェイプを走査する。戻り値の mastersslides がそれぞれの成果物。

関数の入り口

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つ。

  1. Presentation() でPPTXを読み込み、空の辞書 presentation_data を初期化
  2. フェーズ1: prs.slide_masters を走査し、各レイアウトのプレースホルダー情報を記録
  3. フェーズ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_typeis_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でスライド構造を丸裸にする【前編】

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?