はじめに
こんにちは、Dirbatoの社内技術横断支援組織Backbeatに所属しているアーキテクトの福岡です。
今回は、画像生成AIで作った「スライド画像」をそのまま編集可能なPowerPoint(.pptx)に変換する方法をご紹介いたします。
前回の記事では「Nano Banana Pro🍌で高解像度の分解図を作るプロンプト設計」をご紹介しました。
👉 Nano Banana Proで「高解像度の分解図」を生成する:LLM×画像モデルの二段構えプロンプト設計今回はその続編的な位置付けで、**「画像モデルで作った成果物を、編集可能なPPTXとして実務に落とし込む」**ところまで踏み込みます。
最近X(旧Twitter)で「画像 → 編集可能PowerPoint変換がサクッとできたらいいな!」みたいな発信をちょこちょこ見かけるようになり、ニーズはありそうだなと。
ただ、手順をオープンにしてる方は少なかったので、今回はプロンプトと生成されたコードも全部出します。
なぜ「画像 → 編集可能PPTX」が嬉しいのか
画像生成AI(gpt-image-2 / Nano Banana Pro / Gemini 3 Pro Imageなど)でスライドのモックを作るのは、いまや一瞬です。
が、できあがるのはPNG画像のため実務利用の際、以下の点で詰みがちです...
- 「ここの文言をちょっと変えて」と言われて詰む
- 「色合いだけ自社カラーに変えたい」と言われて詰む
- 「文字を翻訳、コピペしたい」と言われて詰む
- 「編集できる状態のPowerPointで欲しい」と言われて詰む
→ 結局PowerPointで一から作り直しという残念な結末...😇
ここで 画像 → 編集可能PPTX変換ができれば文字も図形も後から自由に編集できる「叩き台」として実務利用ができます。
役割
過去記事と同じく、役割分担が大事です。
- 画像生成モデル(デザイン係): スライドの見た目・レイアウトを高品質に作る
- マルチモーダルLLM(解釈係): 画像から文字・図形・座標を読み取ってpython-pptxコードに変換する
- python-pptx(レンダリング係): コードを実行してPowerPointネイティブの図形・テキストとしてPPTXに書き出す
python-pptxはPowerPoint純正のOOXML図形を組み立てるライブラリだということをご認識置きください。
出来上がったPPTXは、画像を貼り付けたPPTXではなく最初からPowerPointで作ったかのように文字や図形が後から編集できる状態になります。
環境前提
- Windows + Python 3.12(インストール済みであること)
- ChatGPT (GPT-5系) または Claude Sonnet 4.5以降のモデルが利用できること
-
pip install python-pptxでライブラリがインストールされていること
Macでも同じ手順でいけるはずです。文字コード関連の罠が一段減るのでむしろ楽かもしれません。
手順
ものすごくシンプルです。
- 画像生成AIでスライドイメージを作成**(gpt-image-2 / Nano Banana Pro / Gemini 3 Pro Image等)
- マルチモーダルLLMに画像 + 後述のプロンプトを投げる → python-pptxコードが出力される
- 出力コードを
.pyファイルで保存して実行 →output.pptxが生成される - ズレや気に入らない箇所を手で微調整して完成
Step 1: スライドイメージを作る
ここは過去記事のテクニックがそのまま使えます。
今回は試しに「生成AI活用による業務効率化提案」というテーマで1枚作ってみました。
画像生成のプロンプト設計は前回の記事を参考にしてください。「ホワイトリスト方式」が効きます🍌
今回はこのプロンプトで可視化をしてみます!
{
"model": "Nano Banana Pro",
"task": "Create a single 16:9 professional consulting proposal slide as a high-resolution business presentation visual.",
"output_format": {
"aspect_ratio": "16:9",
"resolution": "4K or high-resolution presentation-ready",
"canvas": "wide PowerPoint slide",
"language": "Japanese",
"text_quality": "All Japanese text must be sharp, readable, and correctly spelled."
},
"theme": {
"title": "生成AI活用による業務効率化提案",
"subtitle": "AIエージェントとRAG基盤で、定型業務を“人が判断する仕事”へシフト",
"tone": "IT consulting proposal, enterprise AI transformation, executive presentation",
"visual_style": "clean corporate strategy slide, McKinsey-style structure, Microsoft Azure-inspired blue palette, modern SaaS architecture diagram, premium Japanese business presentation"
},
"design_direction": {
"background": "clean white to very light blue gradient background with subtle grid lines and faint digital network patterns",
"primary_colors": ["deep navy", "royal blue", "cyan accent", "white", "light gray"],
"typography": "bold modern Japanese sans-serif, highly legible, strong hierarchy",
"layout": "left-to-right transformation flow with layered horizontal process bands and a right-side roadmap panel",
"mood": "strategic, trustworthy, high-tech, polished, boardroom-ready"
},
"slide_structure": {
"top_header": {
"main_title": "生成AI活用による業務効率化提案",
"subtitle": "AIエージェントとRAG基盤で、定型業務を“人が判断する仕事”へシフト",
"placement": "top-left, large bold navy text, cyan underline accent"
},
"main_diagram": {
"placement": "center-left, occupying about 70% of slide width",
"style": "four stacked horizontal layers, slightly 3D beveled cards, each layer flowing to the right with arrow connectors",
"layers": [
{
"label": "1 現状業務",
"description": "手作業が中心で時間と品質にばらつき",
"icons": ["問い合わせ対応", "資料作成", "議事録", "社内検索"],
"visual": "gray-blue business workflow icons"
},
{
"label": "2 生成AI基盤",
"description": "安全・高精度・再現性のあるAI基盤を構築",
"icons": ["セキュアLLM", "RAG", "プロンプト標準化", "権限管理"],
"visual": "secure cloud, database, prompt template, user permission icons"
},
{
"label": "3 業務変革",
"description": "AIが定型業務を担い、人は判断・付加価値業務へ",
"icons": ["自動要約", "ドラフト生成", "ナレッジ検索", "FAQ応答"],
"visual": "teal-highlighted automation icons"
},
{
"label": "4 効果",
"description": "定量的な成果と競争力強化を実現",
"icons": ["工数削減", "品質平準化", "意思決定高速化"],
"visual": "KPI chart, quality badge, speedometer icons"
}
]
},
"right_panel": {
"title": "ロードマップ",
"placement": "right side vertical panel, dark navy header, white cards",
"items": [
{
"phase": "Phase 1",
"headline": "業務棚卸",
"bullets": ["対象業務の可視化", "課題と優先順位の整理", "要件定義/データ整理"],
"icon": "clipboard with magnifying glass"
},
{
"phase": "Phase 2",
"headline": "PoC",
"bullets": ["ユースケース選定", "プロトタイプ構築", "効果検証/改善"],
"icon": "laboratory flask"
},
{
"phase": "Phase 3",
"headline": "全社展開",
"bullets": ["本番環境構築・運用", "利用拡大・定着化支援", "効果モニタリング/改善"],
"icon": "office building"
}
]
},
"bottom_kpi_bar": {
"placement": "bottom horizontal KPI strip",
"title": "期待KPI(目標値)",
"cards": [
{
"kpi": "30–50%",
"label": "作業時間削減目標",
"description": "定型業務の工数を大幅削減",
"icon": "clock and arrow"
},
{
"kpi": "回答品質の標準化",
"label": "ばらつき抑制",
"description": "正確で一貫したアウトプットを実現",
"icon": "shield check"
},
{
"kpi": "ガバナンス強化",
"label": "安全なAI活用",
"description": "権限管理とログ監査で安心運用",
"icon": "lock"
}
]
}
},
"visual_requirements": {
"composition": "highly organized consulting slide, no clutter, strong whitespace, clear reading order from left to right",
"diagram_style": "semi-flat vector icons mixed with subtle 3D layered cards",
"lighting": "soft corporate lighting, slight glassmorphism, subtle shadows",
"icon_style": "consistent outline icons, navy and cyan accents",
"data_visualization": "simple KPI cards, arrows, roadmap timeline, layered process architecture",
"professionalism": "looks like a slide from a top-tier IT consulting proposal for enterprise executives"
},
"text_constraints": {
"must_include_exact_text": [
"生成AI活用による業務効率化提案",
"AIエージェントとRAG基盤で、定型業務を“人が判断する仕事”へシフト",
"現状業務",
"生成AI基盤",
"業務変革",
"効果",
"ロードマップ",
"Phase 1 業務棚卸",
"Phase 2 PoC",
"Phase 3 全社展開",
"期待KPI(目標値)",
"30–50%",
"作業時間削減目標",
"回答品質の標準化",
"ガバナンス強化"
],
"font_instruction": "Use bold, clean Japanese sans-serif typography. Keep all labels short and readable.",
"avoid_text_errors": "Prioritize accurate Japanese text rendering over decorative complexity."
},
"final_instruction": "Generate one polished 16:9 Japanese business proposal slide. Make it look like an IT consulting firm’s executive proposal deck page about enterprise generative AI transformation. Ensure all text is legible, the diagram is logically structured, and the design is suitable for direct insertion into a PowerPoint presentation."
}
Step 2: マルチモーダルLLMに変換させる
先ほど生成されたスライドイメージ(画像)と以下のプロンプト一緒に投げます。
当記事ではGPT-5.5(思考拡張)を使用しております。
→ Claude Opus 4.7も試したけど今のところGPTの方が優勢・精度良しかなと
添付したスライド画像を、python-pptx で完全再現するPythonコードを書いてください。
【絶対要件】
- 画像をそのまま貼り付けるのは禁止
- すべての要素をテキストボックス、図形(shape)、表(table)として配置
- 編集可能な状態で再現すること
【スライド設定】
- サイズ: 13.33 x 7.5 inch (16:9)
- 背景: 白
【再現精度】
- レイアウト・座標・サイズは画像に忠実に
- 色は画像から目視推定したHEX値で指定
- フォントは日本語: "Yu Gothic UI" または "Noto Sans JP"、英数字: "Inter" または "Segoe UI"
- 角丸の有無、塗りつぶし有無、枠線の有無を正確に判別
【出力】
- 完全に動作する単一の .py ファイル
- 実行すると output.pptx が生成される
- コードのみ。説明文は最小限
【追加ルール】
- 存在しないモジュール・クラス・属性を絶対に使わないこと
- python-pptx 1.0系の標準APIのみで実装すること
最後の「追加ルール」が地味に効くので入れるようにしてください。
これが無いとLLMが架空のAPIをデッチ上げてくる事故(エラー)が増えるので...
→ ハルシネーションってやつですね
(後述の「ハマりポイント」で詳しく書きます)
▼ 出力結果はこちら
from pptx import Presentation
from pptx.util import Inches, Pt
from pptx.dml.color import RGBColor
from pptx.enum.shapes import MSO_SHAPE, MSO_CONNECTOR
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
from pptx.enum.dml import MSO_LINE_DASH_STYLE
FONT_JP = "Yu Gothic UI"
FONT_EN = "Segoe UI"
NAVY = "001C59"
DARK = "061B47"
BLUE = "0B3F95"
BLUE2 = "005B93"
TEAL = "11A7C7"
TEXT = "0B1737"
MUTED = "1D2942"
LIGHT = "F4FAFF"
PALE = "ECF7FD"
BORDER = "BDD4EA"
BORDER2 = "AFC9E6"
WHITE = "FFFFFF"
BLACK = "111111"
def I(v):
return Inches(float(v))
def rgb(hexstr):
hexstr = hexstr.replace("#", "")
return RGBColor(int(hexstr[0:2], 16), int(hexstr[2:4], 16), int(hexstr[4:6], 16))
def line_format(shape, color=None, width=1.0, dash=False):
if color is None:
shape.line.fill.background()
return shape
shape.line.color.rgb = rgb(color)
shape.line.width = Pt(width)
if dash:
shape.line.dash_style = MSO_LINE_DASH_STYLE.DASH
return shape
def fill_format(shape, color=None):
if color is None:
shape.fill.background()
else:
shape.fill.solid()
shape.fill.fore_color.rgb = rgb(color)
return shape
def add_shape(slide, kind, x, y, w, h, fill=WHITE, line=BORDER, lw=1.0, radius=None):
shp = slide.shapes.add_shape(kind, I(x), I(y), I(w), I(h))
fill_format(shp, fill)
line_format(shp, line, lw)
if radius is not None:
try:
shp.adjustments[0] = radius
except Exception:
pass
return shp
def add_poly(slide, pts, fill=WHITE, line=BORDER, lw=1.0):
b = slide.shapes.build_freeform(I(pts[0][0]), I(pts[0][1]), 1.0)
b.add_line_segments([(I(x), I(y)) for x, y in pts[1:]], close=True)
shp = b.convert_to_shape()
fill_format(shp, fill)
line_format(shp, line, lw)
return shp
def add_text(slide, x, y, w, h, text, size=14, color=TEXT, bold=False, align=PP_ALIGN.LEFT,
valign=MSO_ANCHOR.TOP, font=FONT_JP):
tb = slide.shapes.add_textbox(I(x), I(y), I(w), I(h))
tf = tb.text_frame
tf.clear()
tf.margin_left = I(0)
tf.margin_right = I(0)
tf.margin_top = I(0)
tf.margin_bottom = I(0)
tf.word_wrap = True
tf.vertical_anchor = valign
for i, line in enumerate(str(text).split("\n")):
p = tf.paragraphs[0] if i == 0 else tf.add_paragraph()
p.alignment = align
r = p.add_run()
r.text = line
r.font.name = font
r.font.size = Pt(size)
r.font.bold = bold
r.font.color.rgb = rgb(color)
return tb
def add_line(slide, x1, y1, x2, y2, color=NAVY, width=1.0, dash=False):
ln = slide.shapes.add_connector(MSO_CONNECTOR.STRAIGHT, I(x1), I(y1), I(x2), I(y2))
ln.line.color.rgb = rgb(color)
ln.line.width = Pt(width)
if dash:
ln.line.dash_style = MSO_LINE_DASH_STYLE.DASH
return ln
def add_circle(slide, cx, cy, r, fill=None, line=NAVY, lw=1.5):
return add_shape(slide, MSO_SHAPE.OVAL, cx-r, cy-r, 2*r, 2*r, fill, line, lw)
def add_icon_people_chat(slide, x, y, s=1.0, color=NAVY):
add_circle(slide, x+0.30*s, y+0.28*s, 0.10*s, None, color, 1.6)
add_shape(slide, MSO_SHAPE.ARC, x+0.17*s, y+0.40*s, 0.26*s, 0.22*s, None, color, 1.6)
add_circle(slide, x+0.55*s, y+0.20*s, 0.10*s, None, color, 1.6)
add_shape(slide, MSO_SHAPE.ARC, x+0.42*s, y+0.32*s, 0.26*s, 0.22*s, None, color, 1.6)
bubble = add_shape(slide, MSO_SHAPE.ROUNDED_RECTANGLE, x+0.44*s, y+0.03*s, 0.36*s, 0.23*s, None, color, 1.5)
bubble.adjustments[0] = 0.18
add_line(slide, x+0.52*s, y+0.11*s, x+0.72*s, y+0.11*s, color, 1.1)
add_line(slide, x+0.52*s, y+0.17*s, x+0.66*s, y+0.17*s, color, 1.1)
def add_icon_report_chart(slide, x, y, s=1.0, color=NAVY):
add_shape(slide, MSO_SHAPE.FOLDED_CORNER, x+0.22*s, y+0.02*s, 0.48*s, 0.58*s, None, color, 1.6)
for dx, ht in [(0.31, 0.10), (0.43, 0.17), (0.55, 0.25)]:
add_shape(slide, MSO_SHAPE.RECTANGLE, x+dx*s, y+(0.52-ht)*s, 0.07*s, ht*s, color, None, 0)
add_line(slide, x+0.31*s, y+0.29*s, x+0.45*s, y+0.18*s, color, 1.5)
add_line(slide, x+0.45*s, y+0.18*s, x+0.55*s, y+0.23*s, color, 1.5)
add_line(slide, x+0.55*s, y+0.23*s, x+0.66*s, y+0.10*s, color, 1.5)
def add_icon_meeting(slide, x, y, s=1.0, color=NAVY):
for dx, dy in [(0.25,0.20),(0.50,0.20),(0.74,0.20),(0.37,0.44),(0.62,0.44)]:
add_circle(slide, x+dx*s, y+dy*s, 0.07*s, None, color, 1.4)
add_shape(slide, MSO_SHAPE.ARC, x+(dx-0.10)*s, y+(dy+0.08)*s, 0.20*s, 0.16*s, None, color, 1.4)
add_shape(slide, MSO_SHAPE.ROUNDED_RECTANGLE, x+0.43*s, y+0.01*s, 0.24*s, 0.18*s, None, color, 1.4)
def add_icon_doc_search(slide, x, y, s=1.0, color=NAVY):
add_shape(slide, MSO_SHAPE.FOLDED_CORNER, x+0.21*s, y+0.01*s, 0.44*s, 0.60*s, None, color, 1.6)
for yy in [0.19,0.29,0.39]:
add_line(slide, x+0.31*s, y+yy*s, x+0.56*s, y+yy*s, color, 1.1)
add_circle(slide, x+0.67*s, y+0.51*s, 0.12*s, None, color, 2.0)
add_line(slide, x+0.75*s, y+0.60*s, x+0.86*s, y+0.72*s, color, 2.0)
def add_icon_cloud_lock(slide, x, y, s=1.0, color=NAVY):
add_shape(slide, MSO_SHAPE.CLOUD, x+0.14*s, y+0.10*s, 0.58*s, 0.38*s, None, color, 1.7)
add_circle(slide, x+0.43*s, y+0.30*s, 0.12*s, None, color, 1.5)
add_shape(slide, MSO_SHAPE.ROUNDED_RECTANGLE, x+0.37*s, y+0.32*s, 0.13*s, 0.13*s, None, color, 1.4)
add_line(slide, x+0.40*s, y+0.32*s, x+0.40*s, y+0.27*s, color, 1.3)
add_line(slide, x+0.47*s, y+0.32*s, x+0.47*s, y+0.27*s, color, 1.3)
def add_icon_db(slide, x, y, s=1.0, color=NAVY):
add_shape(slide, MSO_SHAPE.CAN, x+0.24*s, y+0.05*s, 0.38*s, 0.55*s, None, color, 1.6)
for yy in [0.24, 0.41]:
add_line(slide, x+0.26*s, y+yy*s, x+0.60*s, y+yy*s, color, 1.1)
add_line(slide, x+0.64*s, y+0.20*s, x+0.79*s, y+0.12*s, color, 1.5)
add_line(slide, x+0.64*s, y+0.38*s, x+0.82*s, y+0.45*s, color, 1.5)
add_circle(slide, x+0.82*s, y+0.10*s, 0.035*s, None, color, 1.3)
add_circle(slide, x+0.86*s, y+0.46*s, 0.035*s, None, color, 1.3)
def add_icon_doc_lines(slide, x, y, s=1.0, color=NAVY):
add_shape(slide, MSO_SHAPE.FOLDED_CORNER, x+0.28*s, y+0.02*s, 0.40*s, 0.60*s, None, color, 1.6)
for yy in [0.18,0.30,0.42]:
add_line(slide, x+0.36*s, y+yy*s, x+0.60*s, y+yy*s, color, 1.2)
def add_icon_person_lock(slide, x, y, s=1.0, color=NAVY):
add_circle(slide, x+0.38*s, y+0.18*s, 0.14*s, None, color, 1.7)
add_shape(slide, MSO_SHAPE.ARC, x+0.18*s, y+0.36*s, 0.40*s, 0.27*s, None, color, 1.7)
add_shape(slide, MSO_SHAPE.ROUNDED_RECTANGLE, x+0.56*s, y+0.42*s, 0.20*s, 0.16*s, color, color, 1)
add_line(slide, x+0.60*s, y+0.42*s, x+0.60*s, y+0.33*s, color, 1.6)
add_line(slide, x+0.72*s, y+0.42*s, x+0.72*s, y+0.33*s, color, 1.6)
def add_icon_spark_doc(slide, x, y, s=1.0, color=TEAL):
add_shape(slide, MSO_SHAPE.RECTANGLE, x+0.22*s, y+0.10*s, 0.34*s, 0.48*s, None, color, 1.7)
for yy in [0.24,0.34,0.44]:
add_line(slide, x+0.30*s, y+yy*s, x+0.49*s, y+yy*s, color, 1.1)
for dx, dy, sz in [(0.62,0.06,0.12),(0.70,0.28,0.10),(0.60,0.43,0.09)]:
add_shape(slide, MSO_SHAPE.STAR_4_POINT, x+dx*s, y+dy*s, sz*s, sz*s, None, color, 1.2)
def add_icon_pencil_doc(slide, x, y, s=1.0, color=BLUE2):
add_shape(slide, MSO_SHAPE.RECTANGLE, x+0.22*s, y+0.08*s, 0.34*s, 0.50*s, None, color, 1.6)
for yy in [0.24,0.36]:
add_line(slide, x+0.30*s, y+yy*s, x+0.48*s, y+yy*s, color, 1.0)
add_shape(slide, MSO_SHAPE.RECTANGLE, x+0.56*s, y+0.22*s, 0.12*s, 0.42*s, None, color, 1.5).rotation = 45
add_shape(slide, MSO_SHAPE.ISOSCELES_TRIANGLE, x+0.68*s, y+0.54*s, 0.10*s, 0.10*s, color, color, 1).rotation = 45
def add_icon_db_search(slide, x, y, s=1.0, color=BLUE2):
add_shape(slide, MSO_SHAPE.CAN, x+0.18*s, y+0.10*s, 0.38*s, 0.47*s, None, color, 1.6)
add_circle(slide, x+0.62*s, y+0.46*s, 0.16*s, None, color, 2.0)
add_line(slide, x+0.72*s, y+0.58*s, x+0.86*s, y+0.70*s, color, 2.0)
def add_icon_faq(slide, x, y, s=1.0, color=BLUE2):
add_shape(slide, MSO_SHAPE.ROUNDED_RECTANGLE, x+0.22*s, y+0.10*s, 0.55*s, 0.42*s, None, color, 1.7)
add_shape(slide, MSO_SHAPE.ISOSCELES_TRIANGLE, x+0.33*s, y+0.45*s, 0.12*s, 0.13*s, None, color, 1.5).rotation = 180
add_text(slide, x+0.31*s, y+0.21*s, 0.38*s, 0.18*s, "FAQ", size=12*s, color=color, bold=True, align=PP_ALIGN.CENTER)
def add_icon_down_chart(slide, x, y, s=1.0, color=BLUE2):
for i, ht in enumerate([0.44,0.30,0.22]):
add_shape(slide, MSO_SHAPE.RECTANGLE, x+(0.18+i*0.15)*s, y+(0.56-ht)*s, 0.09*s, ht*s, color, None, 0)
add_line(slide, x+0.62*s, y+0.12*s, x+0.86*s, y+0.37*s, color, 2.1)
add_line(slide, x+0.86*s, y+0.37*s, x+0.73*s, y+0.36*s, color, 2.1)
add_line(slide, x+0.86*s, y+0.37*s, x+0.84*s, y+0.24*s, color, 2.1)
def add_icon_badge_check(slide, x, y, s=1.0, color=NAVY):
add_shape(slide, MSO_SHAPE.STAR_16_POINT, x+0.19*s, y+0.03*s, 0.48*s, 0.48*s, color, color, 1.2)
add_circle(slide, x+0.43*s, y+0.27*s, 0.16*s, WHITE, WHITE, 1)
add_line(slide, x+0.34*s, y+0.28*s, x+0.41*s, y+0.35*s, color, 2)
add_line(slide, x+0.41*s, y+0.35*s, x+0.54*s, y+0.20*s, color, 2)
add_line(slide, x+0.44*s, y+0.52*s, x+0.37*s, y+0.68*s, color, 2)
add_line(slide, x+0.44*s, y+0.52*s, x+0.52*s, y+0.68*s, color, 2)
def add_icon_speed(slide, x, y, s=1.0, color=NAVY):
add_circle(slide, x+0.46*s, y+0.36*s, 0.30*s, None, color, 1.8)
add_line(slide, x+0.46*s, y+0.36*s, x+0.68*s, y+0.17*s, color, 2.3)
add_circle(slide, x+0.46*s, y+0.36*s, 0.03*s, color, color, 1)
def add_icon_clipboard(slide, x, y, s=1.0, color=NAVY):
add_shape(slide, MSO_SHAPE.ROUNDED_RECTANGLE, x+0.26*s, y+0.09*s, 0.42*s, 0.56*s, None, color, 1.8)
add_shape(slide, MSO_SHAPE.ROUNDED_RECTANGLE, x+0.37*s, y+0.02*s, 0.20*s, 0.11*s, WHITE, color, 1.8)
add_circle(slide, x+0.73*s, y+0.58*s, 0.15*s, None, color, 2)
add_line(slide, x+0.83*s, y+0.69*s, x+0.96*s, y+0.82*s, color, 2)
for yy in [0.26,0.38,0.50]:
add_line(slide, x+0.36*s, y+yy*s, x+0.56*s, y+yy*s, color, 1.0)
def add_icon_flask(slide, x, y, s=1.0, color=BLUE2):
add_shape(slide, MSO_SHAPE.TRAPEZOID, x+0.28*s, y+0.24*s, 0.52*s, 0.50*s, None, color, 1.9)
add_shape(slide, MSO_SHAPE.RECTANGLE, x+0.45*s, y+0.03*s, 0.17*s, 0.28*s, None, color, 1.9)
add_line(slide, x+0.40*s, y+0.03*s, x+0.67*s, y+0.03*s, color, 1.9)
for cx, cy, r in [(0.47,0.52,0.03),(0.63,0.42,0.025),(0.57,0.62,0.02)]:
add_circle(slide, x+cx*s, y+cy*s, r*s, color, color, 1)
def add_icon_building(slide, x, y, s=1.0, color=NAVY):
add_shape(slide, MSO_SHAPE.RECTANGLE, x+0.18*s, y+0.18*s, 0.42*s, 0.56*s, None, color, 1.7)
add_shape(slide, MSO_SHAPE.RECTANGLE, x+0.64*s, y+0.36*s, 0.20*s, 0.38*s, None, color, 1.7)
for col in [0.27,0.40,0.53]:
for row in [0.28,0.42,0.56]:
add_shape(slide, MSO_SHAPE.RECTANGLE, x+col*s, y+row*s, 0.05*s, 0.07*s, color, color, 1)
add_line(slide, x+0.12*s, y+0.76*s, x+0.90*s, y+0.76*s, color, 2)
def add_icon_shield(slide, x, y, s=1.0, color=NAVY):
add_poly(slide, [(x+0.22*s,y+0.08*s),(x+0.84*s,y+0.18*s),(x+0.77*s,y+0.55*s),(x+0.53*s,y+0.80*s),(x+0.28*s,y+0.55*s)], None, color, 2.5)
add_poly(slide, [(x+0.32*s,y+0.18*s),(x+0.74*s,y+0.25*s),(x+0.69*s,y+0.52*s),(x+0.53*s,y+0.69*s),(x+0.37*s,y+0.52*s)], BLUE, BLUE, 1)
add_line(slide, x+0.44*s, y+0.43*s, x+0.52*s, y+0.51*s, WHITE, 3)
add_line(slide, x+0.52*s, y+0.51*s, x+0.66*s, y+0.32*s, WHITE, 3)
def add_icon_lock_big(slide, x, y, s=1.0, color=NAVY):
add_shape(slide, MSO_SHAPE.ROUNDED_RECTANGLE, x+0.22*s, y+0.36*s, 0.58*s, 0.38*s, color, color, 1)
add_shape(slide, MSO_SHAPE.ARC, x+0.32*s, y+0.08*s, 0.38*s, 0.44*s, None, color, 4)
add_circle(slide, x+0.51*s, y+0.54*s, 0.06*s, WHITE, WHITE, 1)
add_shape(slide, MSO_SHAPE.RECTANGLE, x+0.49*s, y+0.58*s, 0.04*s, 0.12*s, WHITE, WHITE, 1)
def process_row(slide, n, y, label, sub, tiles, accent=NAVY):
x, w, h = 0.28, 8.48, 0.98
add_poly(slide, [(x,y),(x+w-0.14,y),(x+w,y+h/2),(x+w-0.14,y+h),(x,y+h)], LIGHT, BORDER2, 1.2)
add_poly(slide, [(x+0.03,y+0.06),(x+0.68,y+0.06),(x+0.80,y+h/2),(x+0.68,y+h-0.06),(x+0.03,y+h-0.06)], accent, accent, 1)
add_text(slide, x+0.20, y+0.16, 0.35, 0.55, str(n), size=25, color=WHITE, bold=True,
align=PP_ALIGN.CENTER, valign=MSO_ANCHOR.MIDDLE, font=FONT_EN)
add_text(slide, x+0.93, y+0.16, 2.05, 0.30, label, size=19, color=NAVY if n < 4 else accent, bold=True)
add_text(slide, x+0.94, y+0.57, 2.15, 0.29, sub, size=11, color=MUTED, bold=True)
for tx, title, iconfn, icc in tiles:
add_shape(slide, MSO_SHAPE.ROUNDED_RECTANGLE, tx, y+0.09, 1.10, 0.82, PALE, BORDER, 0.8, 0.10)
iconfn(slide, tx+0.16, y+0.16, 0.75, icc)
add_text(slide, tx+0.05, y+0.71, 1.00, 0.15, title, size=10, color=MUTED, bold=True, align=PP_ALIGN.CENTER)
prs = Presentation()
prs.slide_width = I(13.333)
prs.slide_height = I(7.5)
slide = prs.slides.add_slide(prs.slide_layouts[6])
add_shape(slide, MSO_SHAPE.RECTANGLE, 0, 0, 13.333, 7.5, WHITE, None, 0)
net_pts = [(7.8,0.05),(8.45,0.34),(9.15,0.10),(9.82,0.34),(10.42,0.08),(11.08,0.35),(11.70,0.08),(12.34,0.35),(13.02,0.07),
(7.95,0.82),(8.70,0.72),(9.22,1.28),(9.88,0.92),(10.48,1.21),(11.12,0.82),(11.78,1.13),(12.42,0.78),(13.18,1.16)]
for i in range(len(net_pts)-4):
if i % 2 == 0:
add_line(slide, net_pts[i][0], net_pts[i][1], net_pts[i+4][0], net_pts[i+4][1], "D6E6F5", 0.45)
for x, y in net_pts:
add_circle(slide, x, y, 0.032, "D2E5F7", "D2E5F7", 0.5)
add_text(slide, 0.27, 0.16, 7.45, 0.55, "生成AI活用による業務効率化提案", size=30, color=DARK, bold=True)
add_text(slide, 0.27, 0.84, 7.35, 0.34, "AIエージェントとRAG基盤で、定型業務を“人が判断する仕事”へシフト", size=19, color=MUTED, bold=True)
add_shape(slide, MSO_SHAPE.RECTANGLE, 0.27, 1.21, 0.81, 0.035, TEAL, TEAL, 0)
process_row(slide, 1, 1.38, "現状業務", "手作業が中心で時間と品質にばらつき",
[(3.66,"問い合わせ対応",add_icon_people_chat,NAVY),(4.87,"資料作成",add_icon_report_chart,NAVY),(6.09,"議事録",add_icon_meeting,NAVY),(7.30,"社内検索",add_icon_doc_search,NAVY)], NAVY)
process_row(slide, 2, 2.48, "生成AI基盤", "安全・高精度・再現性のある\nAI基盤を構築",
[(3.60,"セキュアLLM",add_icon_cloud_lock,NAVY),(4.95,"RAG",add_icon_db,NAVY),(6.12,"プロンプト標準化",add_icon_doc_lines,NAVY),(7.36,"権限管理",add_icon_person_lock,NAVY)], NAVY)
process_row(slide, 3, 3.59, "業務変革", "AIが定型業務を担い、\n人は判断・付加価値業務へ",
[(3.60,"自動要約",add_icon_spark_doc,BLUE2),(4.95,"ドラフト生成",add_icon_pencil_doc,BLUE2),(6.12,"ナレッジ検索",add_icon_db_search,BLUE2),(7.36,"FAQ応答",add_icon_faq,BLUE2)], BLUE2)
process_row(slide, 4, 4.69, "効果", "定量的な成果と競争力強化を実現",
[(3.60,"工数削減",add_icon_down_chart,BLUE2),(5.10,"品質平準化",add_icon_badge_check,NAVY),(6.70,"意思決定高速化",add_icon_speed,NAVY)], BLUE2)
for x, y, c in [(8.47,1.66,"2E67B8"), (8.47,2.75,"2E67B8"), (8.47,3.86,TEAL), (8.47,4.96,TEAL)]:
add_shape(slide, MSO_SHAPE.RIGHT_ARROW, x, y, 0.92, 0.45, c, None, 0)
add_shape(slide, MSO_SHAPE.ROUNDED_RECTANGLE, 9.60, 0.78, 3.54, 5.10, WHITE, NAVY, 1.5, 0.08)
add_shape(slide, MSO_SHAPE.RECTANGLE, 9.60, 0.78, 3.54, 0.56, NAVY, NAVY, 0)
add_text(slide, 9.88, 0.94, 2.95, 0.23, "ロードマップ", size=19, color=WHITE, bold=True, align=PP_ALIGN.CENTER)
add_line(slide, 9.82, 1.91, 9.82, 5.02, "8897AA", 1.1, True)
for cy in [2.12, 3.51, 4.95]:
add_circle(slide, 9.82, cy, 0.075, WHITE, BLUE, 1.8)
road_cards = [
(1.53, "Phase 1 業務棚卸", "・対象業務の可視化\n・課題と優先順位の整理\n・要件定義/データ整理", add_icon_clipboard, NAVY),
(2.99, "Phase 2 PoC", "・ユースケース選定\n・プロトタイプ構築\n・効果検証/改善", add_icon_flask, BLUE2),
(4.44, "Phase 3 全社展開", "・本番環境構築・運用\n・利用拡大・定着化支援\n・効果モニタリング/改善", add_icon_building, NAVY),
]
for y, title, body, iconfn, icc in road_cards:
add_shape(slide, MSO_SHAPE.ROUNDED_RECTANGLE, 9.93, y, 3.02, 1.27, "FCFCFD", "D4DCE6", 0.9, 0.08)
iconfn(slide, 10.07, y+0.22, 0.92, icc)
add_text(slide, 10.92, y+0.16, 1.70, 0.28, title, size=17, color=NAVY, bold=True)
add_text(slide, 10.92, y+0.54, 1.85, 0.52, body, size=10.8, color=BLACK, bold=True)
add_poly(slide, [(0.00,5.84),(2.45,5.84),(2.70,6.04),(2.45,6.24),(0.00,6.24)], NAVY, NAVY, 1)
add_text(slide, 0.42, 5.91, 2.00, 0.24, "期待KPI(目標値)", size=17, color=WHITE, bold=True)
add_shape(slide, MSO_SHAPE.ROUNDED_RECTANGLE, 0.27, 6.12, 4.34, 1.11, WHITE, "CFDAE8", 0.9, 0.08)
add_circle(slide, 1.02, 6.68, 0.34, WHITE, NAVY, 2.2)
add_line(slide, 1.02, 6.68, 0.91, 6.55, NAVY, 2)
add_line(slide, 1.02, 6.68, 1.23, 6.68, NAVY, 2)
add_shape(slide, MSO_SHAPE.RIGHT_ARROW, 1.13, 6.72, 0.34, 0.24, BLUE, BLUE, 0).rotation = -35
add_text(slide, 1.83, 6.18, 1.55, 0.42, "30-50%", size=31, color=BLUE, bold=True, font=FONT_EN)
add_text(slide, 1.82, 6.62, 1.78, 0.23, "作業時間削減目標", size=14, color=BLACK, bold=True)
add_text(slide, 1.82, 6.94, 2.03, 0.18, "定型業務の工数を大幅削減", size=10, color=BLACK, bold=True)
add_shape(slide, MSO_SHAPE.ROUNDED_RECTANGLE, 4.82, 6.12, 3.76, 1.11, WHITE, "CFDAE8", 0.9, 0.08)
add_icon_shield(slide, 5.09, 6.22, 1.05, NAVY)
add_text(slide, 6.15, 6.30, 1.90, 0.28, "回答品質の標準化", size=18, color=BLUE, bold=True)
add_text(slide, 6.16, 6.72, 1.20, 0.21, "ばらつき抑制", size=13, color=BLACK, bold=True)
add_text(slide, 6.16, 6.99, 2.05, 0.18, "正確で一貫したアウトプットを実現", size=10, color=BLACK, bold=True)
add_shape(slide, MSO_SHAPE.ROUNDED_RECTANGLE, 8.78, 6.12, 4.35, 1.11, WHITE, "CFDAE8", 0.9, 0.08)
add_icon_lock_big(slide, 9.07, 6.22, 1.10, NAVY)
add_text(slide, 10.06, 6.30, 2.10, 0.28, "ガバナンス強化", size=19, color=BLUE, bold=True)
add_text(slide, 10.06, 6.72, 1.50, 0.21, "安全なAI活用", size=13, color=BLACK, bold=True)
add_text(slide, 10.06, 6.99, 2.70, 0.18, "権限管理とログ監査で安心運用", size=10, color=BLACK, bold=True)
prs.save("output.pptx")
Step 3: 出力コードを保存して実行
LLMが返してきたPythonコードを slide_gen.py などの名前で保存して実行するだけです。
python slide_gen.py
これで output.pptx が同じフォルダに生成されます。
体感ですが、7割は一発で再現できる印象です。残り3割はズレを手で直すか、LLMに「ここがズレてるので直して」と追加指示で詰めていく感じです。
実行結果
PowerPointで開いてみるとこの通り 文字や図形が編集可能な状態で再現されています。
当然ながら画像と完全一致ではないですが、レイアウト・配色・テキスト内容はほぼ忠実に再現できているかなと。
あとは、ズレている箇所や気に食わない箇所(アイコンとか...)を PowerPointで直接修正して仕上げるだけです。


(差分比較用)

ハマりポイント
実際にやってみるといくつかの罠を踏むので、先に共有しておきます🙌
① メモ帳保存はShift_JISになる
Windowsのメモ帳でコードを保存すると、デフォルトで**ANSI(=Shift_JIS)**保存になります。
これだとPythonが読み込み時にエラーを吐きます。
SyntaxError: Non-UTF-8 code starting with '\x96' in file ...
対処: 保存ダイアログ下部の「エンコード」を 「UTF-8」 に変更してください。
あるいはVS Codeを使えば標準でUTF-8なので何も気にしなくてOKです。
② LLMが架空のAPIをでっち上げる
LLMは python-pptx の存在しないクラスや属性を平気で使ってきます(ハルシネーション)。
よく出るやつ:
-
MSO_ARROWHEAD← 存在しない -
shape.shadow.add_shadow()← 存在しない -
MSO_LINE_DASH_STYLE← 実在するが、整数指定の方が安全
ImportError が出たら該当行を削除するか、LLMに**「これ実在しないAPIなので、別の方法で書き直して」**と返すのが速いです。
プロンプトに「python-pptx 1.0系の標準APIのみで実装」と入れておくと体感半減します。
③ Markdown経由でコピペすると . がURL化される
LLMの出力をSlackやNotion経由でコピペすると、prs.save(OUT) のような記法が [prs.save](http://prs.save)(OUT) に化けることがあります。
クライアント側がドット区切りをURLと誤解する現象です。
対処: VS Codeで [(.+?)\]\(http://.+?\) を $1 に正規表現置換すると一括で直ります。
実務拡張アイデア
ここまでで「手動で画像 → PPTX変換」はできました。これを実務に落とし込むなら、こんな拡張が考えられます。
Power Automate for desktopでフォルダ監視 → 自動PPTX化
[ローカルtmpフォルダ]
↓ 新規ファイル検知
[Power Automate for desktop]
↓ Pythonスクリプト実行 (.py)
[python-pptx]
↓ pptx生成
[outputs フォルダに保存]
PADの「フォルダ内のファイルを待機」アクションでPNG/JPG検知 → 「DOSコマンドの実行」でPython起動、という流れで組めます。
※ PADの「Pythonスクリプト」アクションは IronPython 2.7 ベースなので python-pptx は動きません。必ず
python.exeを別プロセスで呼ぶ構成にしてください。
Claude Code or Ollama or Azureでプライベート化
社内データを扱う場合、外部APIにスライド画像を投げるのが許容されないケースもあります。
そんなときは:
- Claude Code: CLIから画像を渡せるので、ローカル完結のエージェントとして組める
- Ollama + llama3.2-vision: 完全ローカル。ただし座標推定の精度はクラウドLLMに劣る印象
- Azure OpenAI: テナント内完結。閉域案件ならこれが本命 👈生成AIでパワポ作成機能作ってる会社さんとか
おわりに
今回は、画像生成AIで作ったスライド画像を、編集可能なPowerPointに変換する手順をご紹介しました。
ポイントをまとめると・・・
- 画像モデルで作ったスライド画像は、そのままだとただのPNGで実務では扱いづらい
- マルチモーダルLLMに python-pptx コード生成を任せれば、編集可能なPPTXに変換できる
- python-pptxはPowerPointネイティブのOOXML図形を組み立てるので、出来上がったPPTXは文字も図形も自由に編集できる
- プロンプトに「存在しないAPIを使わない」と明記すると、ハルシネーション事故が大幅に減る
- 体感7割は一発で再現できるので、残りは手で微調整して仕上げる
正直、この手のテクニックはサクッと共有・実装しつつ叩いてもらって改善していく方が界隈全体が前に進むと思っています。
画像生成AIで「おしゃれな見た目」を作るのは簡単になった一方、実務に乗せるための最後のひと工夫こそ知見として価値があるはず。
引き続き、なんかコレありかも?というネタを書いていこうと思います📝
ここまで読んでいただきありがとうございました!
では、次回の更新まで🙌
