CrossRoad

XRを中心とした技術ブログ。 Check also "English" category.

Microsoft Copilot Studioを使って、資料添削AIエージェントを作ってみる (3) PowerPointファイルからテキストを抽出

前回に引き続き、AIエージェントを作るための処理をつくっていきます。

今回は、Copilotが判断するためにテキスト抽出する処理を書きます。

前回まではこちらです。

www.crossroad-tech.com

www.crossroad-tech.com

注意事項
・Copilot Studioを使うのが初めて、Power Automateもあまり使っていないため、MS CopilotとGithub Copilotを使って試行錯誤しながら進めています。そのため、最適な方法ではない可能性があります。

1. 全体の処理の流れ

前回の記事の再掲ですが、このように考えています。

[1] ユーザからPowerPointファイルのアップロードを受け付ける [Copilot Studio]

[2] 受け取ったPowerPointファイルをPower Automateに渡す [Copilot Studio -> Power Automate]

[3] Power Automateの中で、Azure Functionに処理を渡す [Power Automate -> Azure Function (関数アプリ)]

[4] Azure Functionの中でPowerPointファイルを受け取ってテキストを抽出。抽出した結果をPower Automateに返す [Azure Function -> Power Automate]

[5] Power AutomateからCopilot Studioに結果を渡す [Power Automate -> Copilot Studio]

[6] Copilot Studioで誤字脱字チェックを実行する [Copilot Studio]

これまでで[1], [2]に対応しました。今回は先に[4]に書かれているテキスト抽出処理を説明します。Azure Functionでの動作、[2]->[3], [3]->[4]は後日確認して動いたら別の記事で紹介します。

2. PowerPointファイルを受け取ってテキストを抽出

GitHub Copilotを使いつつ、PythonでPowerPointファイルのテキストを抽出する処理を書きました。

フォルダ構成はこのようになっています。

Folder configuration of extracting text by Python

個別ファイルの内容です。

◾️init.py

import logging
import azure.functions as func
import io
from pptx import Presentation

def main(req: func.HttpRequest) -> func.HttpResponse:
    logging.info('Python HTTP trigger function processed a request.')

    try:
        # リクエストからファイルを取得
        file = req.files.get('file')
        
        if not file:
            return func.HttpResponse(
                "ファイルがアップロードされていません。PowerPointファイルをアップロードしてください。",
                status_code=400
            )

        # ファイル内容を読み込み
        file_contents = file.stream.read()
        
        # PowerPointファイルとして読み込む
        presentation = Presentation(io.BytesIO(file_contents))
        
        # テキストを抽出
        extracted_text = []
        for slide in presentation.slides:
            for shape in slide.shapes:
                if hasattr(shape, "text"):
                    extracted_text.append(shape.text)
        
        # 結果を返す
        return func.HttpResponse(
            f"抽出されたテキスト:\n\n{''.join(extracted_text)}",
            mimetype="text/plain"
        )
    
    except Exception as e:
        logging.error(f"エラーが発生しました: {str(e)}")
        return func.HttpResponse(
            f"エラーが発生しました: {str(e)}",
            status_code=500
        )

◾️function.json

{
  "scriptFile": "__init__.py",
  "bindings": [
    {
      "authLevel": "function",
      "type": "httpTrigger",
      "direction": "in",
      "name": "req",
      "methods": ["post"],
      "route": null
    },
    {
      "type": "http",
      "direction": "out",
      "name": "$return"
    }
  ]
}

◾️host.json

{
  "version": "2.0",
  "logging": {
    "applicationInsights": {
      "samplingSettings": {
        "isEnabled": true,
        "excludedTypes": "Request"
      }
    }
  },
  "extensionBundle": {
    "id": "Microsoft.Azure.Functions.ExtensionBundle",
    "version": "[3.*, 4.0.0)"
  }
}

◾️local.setting.json

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "python"
  },
  "Host": {
    "LocalHttpPort": 7071,
    "CORS": "*",
    "CORSCredentials": false
  }
}

◾️requirements.txt

python-pptx
azure-functions

◾️index.html (ローカル環境でのテスト用。配置場所はどこでもよい)

<!DOCTYPE html>
<html>
<head>
    <title>PowerPoint テキスト抽出</title>
    <meta charset="UTF-8">
    <style>
        body { font-family: sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
        .error { color: red; }
        .success { color: green; }
        pre { background: #f5f5f5; padding: 10px; border-radius: 5px; overflow: auto; }
    </style>
</head>
<body>
    <h1>PowerPointファイルからテキストを抽出</h1>
    
    <!-- フォーム部分 -->
    <form id="uploadForm" enctype="multipart/form-data">
        <input type="file" name="file" id="fileInput" accept=".pptx">
        <button type="button" onclick="uploadFile()">アップロード</button>
    </form>
    
    <!-- 状態表示 -->
    <div id="status" style="margin-top: 10px;"></div>
    
    <!-- 結果表示エリア -->
    <pre id="result" style="margin-top: 20px;"></pre>

    <script>
        const API_URL = 'http://localhost:7071/api/source';
        const statusDiv = document.getElementById('status');
        const resultDiv = document.getElementById('result');
        
        // デバッグ情報の表示
        console.log(`APIエンドポイント: ${API_URL}`);
        
        function uploadFile() {
            const fileInput = document.getElementById('fileInput');
            
            if (!fileInput.files[0]) {
                statusDiv.textContent = 'ファイルを選択してください';
                statusDiv.className = 'error';
                return;
            }
            
            const formData = new FormData();
            formData.append('file', fileInput.files[0]);
            
            statusDiv.textContent = '処理中...';
            statusDiv.className = '';
            resultDiv.textContent = '';
            
            // デバッグ情報
            console.log('リクエスト送信開始...');
            
            fetch(API_URL, {
                method: 'POST',
                body: formData
            })
            .then(response => {
                console.log('レスポンス受信:', response.status);
                
                if (!response.ok) {
                    return response.text().then(text => {
                        throw new Error(`HTTP error! status: ${response.status}, message: ${text}`);
                    });
                }
                
                const contentType = response.headers.get('content-type');
                if (contentType && contentType.includes('application/json')) {
                    return response.json();
                } else {
                    return response.text().then(text => {
                        return { rawResponse: text };
                    });
                }
            })
            .then(data => {
                statusDiv.textContent = '処理完了!';
                statusDiv.className = 'success';
                resultDiv.textContent = JSON.stringify(data, null, 2);
                console.log('処理結果:', data);
            })
            .catch(error => {
                statusDiv.textContent = `エラーが発生しました`;
                statusDiv.className = 'error';
                resultDiv.textContent = error.message;
                console.error('エラー詳細:', error);
            });
        }
    </script>
</body>
</html>

これらのファイルを準備したら、Pythonの仮想環境を作ってAPIの呼び出しを受け付けるようにします。以下は私の環境での例です。

# プロジェクトフォルダに移動  
cd /Users/limes/Documents/AzureFunction/ExtractText  

# Python 仮想環境を作成  
python -m venv .venv    

# 仮想環境をアクティベート
source .venv/bin/activate  

# requirements.txt からライブラリをインストール
pip install -r requirements.txt  

# ローカル実行
func start  
Found Python version 3.13.3 (python3).

Azure Functions Core Tools
Core Tools Version:       4.1.0+7ff2567e43c1ae7471cea7394452c1447661e175 (64-bit)
Function Runtime Version: 4.1040.300.25317

[2025-07-25T22:18:54.759Z] Attempt to remove module cache for google._upb but failed with 'google'. Using the original module cache.
[2025-07-25T22:18:54.759Z] Attempt to remove module cache for google._upb but failed with 'google'. Using the original module cache.
[2025-07-25T22:18:54.759Z] Attempt to remove module cache for google._upb but failed with 'google'. Using the original module cache.
[2025-07-25T22:18:54.759Z] Attempt to remove module cache for google._upb but failed with 'google'. Using the original module cache.
[2025-07-25T22:18:55.262Z] Worker process started and initialized.

Functions:

        source: [POST] http://localhost:7071/api/

ローカル環境でindex.htmlをダブルクリックするとブラウザで表示されます。

ここで任意のPowerPointファイルをアップロードすると、抽出されたテキストが表示されます。

今回使ったPowerPointファイルです。以前Babylon.jsレシピ集の執筆者募集に使ったファイルから2ページまで減らしてみました。

An example PowerPoint file for text extraction

これをテキスト抽出させた結果です。

Extracted text from a PowerPoint file

赤枠部分が抽出されたテキストです。これだと見えないので書き出しました。

{ "rawResponse": "抽出されたテキスト:\n\n執筆したことないけど?毎回初めての方が参加されます。\nGitHubで原稿を管理し、オンラインで完結します。\nテキストエディタとブラウザさえあれば執筆・製本確認できるので、気軽に参加できます。どうやって執筆するの?Re:VIEWという書籍向けのマークアップ言語で記述します。\n使用するGitHubリポジトリには、Re:VIEW記法の例を書いているので、それを参考にしてください。" }

ページの区分けは読み取れませんが、一通りのテキストは読めました。ただし、スライドテンプレートに埋め込んだBabylon.js User Community in Japanなどは対象外でした。

3. おわりに

相変わらず進みが遅いですが、PythonでPowerPointのテキストを抽出するところまではできました。次はこれをAzure Functionで動かすところを試してみます。