Claude3を使ってパワポ資料を読み込む処理をLambda関数上で実行してみた

2024.03.14

はじめに

新規事業部 生成AIチーム 山本です。

ChatGPT(OpenAI API)をはじめとしたAIの言語モデル(Large Language Model:以下、LLM)を使用して、チャットボットを構築するケースが増えています。通常、LLMが学習したときのデータに含まれている内容以外に関する質問には回答ができません。そのため、例えば社内システムに関するチャットボットを作成しようとしても、素のLLMでは質問に対してわからないという回答や異なる知識に基づいた回答が(当然ながら)得られてしまいます。

この問題を解決する方法として、Retrieval Augmented Generation(以下、RAG)という手法がよく使用されます。RAGでは、ユーザからの質問に回答するために必要そうな内容が書かれた文章を検索し、その文章をLLMへの入力(プロンプト)に付け加えて渡すことで、ユーザが欲しい情報に関して回答させることができます。

以前の記事では、RAGを構成する際に大きな課題となっている「ドキュメントが意図しない読み込まれ方になってしまう」という問題に対して、Claude3にパワーポイントのスクリーンショットを画像として読ませることで、人間が読むような形のテキストに起こせることを確認しました。

Claude3を使って人間が読むようにパワポ資料を読み込んでみる | DevelopersIO

この記事では、この処理をAWS上で実行するために、コンテナを作成しLambda関数として実行させる方法を試したので、その内容について記載します。

用意したもの

プログラム(app.py)

クリックで開きます(長いので折りたたんであります)
import base64
import json
import re
import subprocess
from concurrent.futures import Future, ThreadPoolExecutor
from pathlib import Path

import boto3
from pdf2image import convert_from_path

s3 = boto3.resource("s3")
bedrock_runtime = boto3.client("bedrock-runtime", region_name="us-east-1")

def convert_pptx_to_pdf(
    s3_bucket_name: str,  # bucket name of input. ex: "bucket-name"
    s3_key_pptx: str,  # object to pptx. ex: "input/setsumeikai_17.pptx"
    s3_key_prefix_output: str | None,  # folder to output in bucket. ex: "output"
):
    # check input
    if not s3_key_pptx.endswith(".pptx"):
        print("WARNING: The input file is not pptx file")
        return None

    # get pptx from S3 bucket
    bucket = s3.Bucket(s3_bucket_name)
    filepath_tmp_pptx = Path("/tmp") / s3_key_pptx

    filepath_tmp_pptx.parent.mkdir(parents=True, exist_ok=True)
    bucket.download_file(s3_key_pptx, filepath_tmp_pptx.as_posix())

    # convert pptx to pdf with libreoffice
    command = " ".join(
        [
            "/opt/libreoffice7.6/program/soffice",
            "--headless",
            "--norestore",
            "--invisible",
            "--nodefault",
            "--nofirststartwizard",
            "--nolockcheck",
            "--nologo",
            "--convert-to pdf:writer_pdf_Export",
            f"--outdir {Path(filepath_tmp_pptx).parent.as_posix()}",
            filepath_tmp_pptx.as_posix(),
        ]
    )
    proc = subprocess.run(
        command,
        shell=True,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
    )
    print(f"STDOUT: {proc.stdout}")
    print(f"STDERR: {proc.stderr}")

    filepath_pdf = filepath_tmp_pptx.with_suffix(".pdf")

    if not filepath_pdf.exists():
        print(f"ERROR: The PDF file not created: {filepath_pdf}")
        return None

    print(f"PDF: {filepath_pdf}")
    print(f"Size: {Path(filepath_pdf).stat().st_size}")

    # put pdf to S3 bucket
    s3_key_pdf = filepath_pdf.relative_to("/tmp")
    if s3_key_prefix_output is not None and s3_key_prefix_output != "":
        s3_key_pdf = Path(s3_key_prefix_output) / s3_key_pdf

    with open(filepath_pdf, "rb") as data:
        bucket.put_object(Key=s3_key_pdf.as_posix(), Body=data)

    # return pdf fielpath and s3 object key
    return filepath_pdf, s3_key_pdf

def convert_pdf_to_images(
    filepath_pdf: Path,  # temp file path of pdf
    s3_bucket_name: str,  # bucket name to output. ex: "bucket-name"
    s3_key_prefix_output: str | None,  # folder to output in bucket. if set None, same folder as input object. ex: "output"
):
    # check input
    if not filepath_pdf.exists() or not filepath_pdf.suffix == ".pdf":
        print("WARNING: The input file is not pdf file")
        return None

    # convert pdf to image with pdf2image
    folderpath_images = Path(filepath_pdf).parent / "images"

    folderpath_images.mkdir(parents=True, exist_ok=True)
    convert_from_path(filepath_pdf, output_folder=folderpath_images, fmt="jpeg")

    filepaths_image = list(folderpath_images.glob("*.jpg"))
    print(filepaths_image)

    # put object to S3 bucket
    bucket = s3.Bucket(s3_bucket_name)
    s3_keys_image = []
    for filepath_img in filepaths_image:

        s3_key_image = filepath_img.relative_to("/tmp")
        if s3_key_prefix_output is not None and s3_key_prefix_output != "":
            s3_key_image = Path(s3_key_prefix_output) / s3_key_image

        with open(filepath_img, "rb") as data:
            bucket.put_object(Key=s3_key_image.as_posix(), Body=data)

        s3_keys_image.append(s3_key_image)

    # return images fielpath and s3 object key
    return filepaths_image, s3_keys_image

def read_image_to_text(
    filepath_image: Path,
    s3_bucket_name: str,  # bucket name to output. ex: "bucket-name"
    s3_key_prefix_output: str | None,  # folder to output in bucket. if set None, same folder as input object. ex: "output"
):
    # check input
    if not filepath_image.exists() and not filepath_image.suffix in [".jpg", ".jpeg"]:
        print(f"WARNING: The input file is not image file: {filepath_image}")
        return None

    # prepare message for bedrock API
    prompt = "これはパワーポイントファイルのスクリーンショットです。文字起こしして、markdown形式で出力してください。出力は、'''などで囲わず、markdownのみ出力してください。人間が認識するような順番・親子関係としてheadingしてください。写真やフロー図・構成図などがスクリーンショット内部に含まれている場合は、内容を説明して[]で囲ってテキストとして出力してください。オブジェクトや図形の説明などは不要です。このスクリーンショットは画像ですが、スクリーンショット自体の説明は不要です"

    with open(filepath_image, "rb") as f:
        bytes_image = f.read()
    bytes_image_b64 = base64.b64encode(bytes_image).decode("utf-8")  # Base64変換

    content_image = {
        "type": "image",
        "source": {
            "type": "base64",
            "media_type": "image/jpeg",
            "data": bytes_image_b64,
        },
    }
    content_prompt = {
        "type": "text",
        "text": prompt,
    }
    content = [content_image, content_prompt]
    messages = [
        {"role": "user", "content": content},
    ]

    # call bedrock API
    body = json.dumps(
        {
            "anthropic_version": "bedrock-2023-05-31",
            "max_tokens": 500,
            "temperature": 0,
            "messages": messages,
        }
    )
    response = bedrock_runtime.invoke_model(
        body=body,
        modelId="anthropic.claude-3-sonnet-20240229-v1:0",
        accept="application/json",
        contentType="application/json",
    )
    response_body = json.loads(response.get("body").read())
    print(json.dumps(response_body, indent=2, ensure_ascii=False).replace("\n", "\r"))

    if response_body["type"] != "message":
        print("ERROR: The response is not message")
        return None

    if response_body["stop_reason"] != "end_turn":
        print("ERROR: The stop_reason is not end_turn")
        return None

    text = response_body["content"][0]["text"]

    # save text to file
    filepath_txt = filepath_image.with_suffix(".txt")
    with open(filepath_txt, "w") as f:
        f.write(text)

    # put object to S3 bucket
    s3_key_text = filepath_txt.relative_to("/tmp")
    if s3_key_prefix_output is not None and s3_key_prefix_output != "":
        s3_key_text = Path(s3_key_prefix_output) / s3_key_text

    bucket = s3.Bucket(s3_bucket_name)
    with open(filepath_txt, "rb") as data:
        bucket.put_object(Key=s3_key_text.as_posix(), Body=data)

    return filepath_txt, s3_key_text

def read_images_to_text(
    filepaths_image: list[Path],  # temp file path of images
    s3_bucket_name: str,  # bucket name to output. ex: "bucket-name"
    s3_key_prefix_output: str | None,  # folder to output in bucket. if set None, same folder as input object. ex: "output"
):
    # check input
    if not all([filepath_image.exists() for filepath_image in filepaths_image]):
        print("WARNING: The input file is not image file")
        return None

    # read image with claude 3
    executor = ThreadPoolExecutor(max_workers=5)
    futures: list[Future[tuple[Path, Path] | None]] = []
    for filepath_image in filepaths_image:
        future = executor.submit(read_image_to_text, filepath_image, s3_bucket_name, s3_key_prefix_output)
        futures.append(future)

    results: list[tuple[Path, Path] | None] = []
    for future in futures:
        result = future.result()
        results.append(result)

    return results

def handler(event, context):
    print(event)

    # get input
    s3_object_uri_input = event.get("s3_object_uri_input")
    s3_key_prefix_output = event.get("s3_key_prefix_output")
    if not s3_object_uri_input:
        return "ERROR: The input file is not set"

    # check input
    s3_object_uri_pattern = re.search(r"s3://(.+?)/(.+)", s3_object_uri_input)
    if not s3_object_uri_pattern:
        print("WARNING: The input file is not s3 object uri")
        return None
    s3_bucket_name = s3_object_uri_pattern.group(1)
    s3_key_pptx = s3_object_uri_pattern.group(2)
    print(f"Bucket: {s3_bucket_name}")
    print(f"Key: {s3_key_pptx}")

    # convert pptx to pdf
    result_pptx_to_pdf = convert_pptx_to_pdf(s3_bucket_name, s3_key_pptx, s3_key_prefix_output)
    if result_pptx_to_pdf is None:
        return "ERROR: The convert (pptx to pdf) failed"
    filepath_pdf, s3_key_pdf = result_pptx_to_pdf

    # convert pdf to image
    result_pdf_to_images = convert_pdf_to_images(filepath_pdf, s3_bucket_name, s3_key_prefix_output)
    if result_pdf_to_images is None:
        return "ERROR: The convert (pdf to images) failed"
    filepaths_image, s3_keys_image = result_pdf_to_images

    # read image with claude 3
    result_images_to_text = read_images_to_text(filepaths_image, s3_bucket_name, s3_key_prefix_output)
    if result_images_to_text is None:
        return "ERROR: The read (images to text) failed"
    for result in result_images_to_text:
        print(result)

    # rm tmp files
    if filepath_pdf.exists():
        filepath_pdf.unlink()
    for filepath_image in filepaths_image:
        if filepath_image.exists():
            filepath_image.unlink()
    for result_images_to_text in result_images_to_text:
        if result_images_to_text is not None:
            filepath_txt = result_images_to_text[0]
            if result_images_to_text is not None and filepath_txt.exists():
                filepath_txt.unlink()

    return ""

動作としては以下のとおりです

  • (前提:S3にパワーポイントのファイルがアップロードされている)
  • 引数として、S3のパワーポイントファイル(オブジェクト)のURIを受け取る
  • S3からパワーポイントファイル(オブジェクト)を取得する
  • libreoffice(モジュール)を使い、パワーポイントファイルをPDFに変換する(pptx→pdf)
  • pdf2img(Pythonライブラリ)を使い、PDFファイルを画像ファイル群に変換する(pptx→jpg)
  • Claude3(boto3経由でBedrockのAPI)を使い、画像を文字起こしする(jpg→txt)
  • (中間ファイルと結果ファイルはS3にアップロードしています。/tmpにも出力していますが、最後に削除しています)

(※ Claude3に渡しているプロンプト(app.py中のprompt)は簡易なものです。実際に使う場合には、プロンプトガイドに沿った形式にしたり、指示を増やす方がオススメです)

依存関係ファイル(requirements.txt)

boto3==1.34.59
botocore==1.34.59
jmespath==1.0.1
pdf2image==1.17.0
pillow==10.2.0
python-dateutil==2.9.0.post0
s3transfer==0.10.0
six==1.16.0
urllib3==2.0.7

使用しているのは以下のライブラリです

  • boto3
  • pdf2image

コンテナ設定ファイル(Dockerfile)

FROM public.ecr.aws/lambda/python:3.11

# コンテナの作業ディレクトリを設定
WORKDIR /var/task

# libreoffice に必要なパッケージをインストール
RUN yum -y install curl wget tar gzip zlib freetype-devel
RUN yum -y install libxslt \
  gcc \
  ghostscript \
  lcms2-devel \
  libffi-devel \
  libjpeg-devel \
  libtiff-devel \
  libwebp-devel \
  make \
  openjpeg2-devel \
  sudo \
  tcl-devel \
  tk-devel \
  tkinter \
  which \
  xorg-x11-server-Xvfb \
  zlib-devel \
  java \
  ipa-gothic-fonts ipa-mincho-fonts ipa-pgothic-fonts ipa-pmincho-fonts \
  && yum clean all
# libreoffice をインストール
RUN wget https://download.documentfoundation.org/libreoffice/stable/7.6.5/rpm/x86_64/LibreOffice_7.6.5_Linux_x86-64_rpm.tar.gz && \
    tar -xvzf LibreOffice_7.6.5_Linux_x86-64_rpm.tar.gz && \
    cd LibreOffice_7.6.5.2_Linux_x86-64_rpm/RPMS && yum -y localinstall *.rpm && \
    cd ../../ && \
    rm -rf LibreOffice_7.6.5_Linux_x86-64_rpm.tar.gz LibreOffice_7.6.5.2_Linux_x86-64_rpm
RUN yum -y install cairo

# pdf2img に必要なパッケージをインストール
RUN yum -y install poppler-utils

# 必要なPythonパッケージをインストール
COPY ./requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 必要なファイルをコンテナにコピー
COPY ./app.py ${LAMBDA_TASK_ROOT}

ENV HOME=/tmp

# ハンドラー情報
CMD ["app.handler"]

動作・設定は以下のとおりです

  • libreofficeに必要なパッケージと、libreofficeをインストール
  • pdf2imgに必要なパッケージをインストール
  • Pythonプログラムに必要なパッケージをインストール
  • プログラムをコピー
  • 環境変数を変更(Lambda関数を実行するときは、/tmpしか変更できないため)
  • Lambda関数では、app.pyのhandler関数を実行する

※ 途中インストールしているモジュールが必要十分かはわかっていません。不要なものがあったり、他のファイルを処理するときに不足するかもしれませんので、ご注意ください。

注意点

  • 使用するタイミングによって、公開されているlibreofficeのバージョンが変わります。必要に応じて、バージョンを変更してください。具体的には、以下の部分の変更が必要です。また、バージョンの最後の部分(7.6.5.”2”)は、tar.gzファイルを展開して確認し、それに合わせて変更する必要がありますので、ご注意ください。
    • app.py:コマンド実行部分(「"/opt/libreoffice7.6/program/soffice",」)
    • Dockerfile:libreofficeインストール部分(「# libreoffice をインストール」以下)

構築方法

以下、マネジメントコンソールで作業しました。(もちろんCDKやSAMを使っても構築可能だと思います。)

リポジトリを作成(ECR)

ECRのページを開き、リポジトリを作成しました。

イメージをビルド・プッシュ(ローカル)

ローカルのマシンに、app.py・requirements.txt・Dockerfileを用意しました。

ローカルで以下のコマンドを実行しました。(role_name)は作業を実行したいAWSのロール名を、(repository_name)は作成したリポジトリ名を、(account_id)はAWSアカウントのID(12桁の数字)を書きました。

awsume (role_name)
docker build -t (repository_name) .
docker tag (repository_name):latest (accound_id).dkr.ecr.ap-northeast-1.amazonaws.com/(repository_name)
aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin (accound_id).dkr.ecr.ap-northeast-1.amazonaws.com
docker push (accound_id).dkr.ecr.ap-northeast-1.amazonaws.com/(repository_name)

※ awsumeについては、こちらの記事をご覧ください

マネコン起動もできるAWSのスイッチロール用CLIツール「AWSume」の紹介 | DevelopersIO

バケットを作成(S3)

デフォルトの設定でバケットを作成しました。

※ 今回はデータを入出力するために用意したものです。処理自体には関係ないので、コードを修正して他から読み込むように変更することも可能です。

関数を作成(Lambda)

Lambdaのページを開き、「関数の作成」を押しました。

3つある作成方法のうち、「コンテナイメージ」を選択しました。コンテナイメージURIには、作成したECRのレポジトリを検索して選択し、プッシュしたイメージ(latest)を選択しました。

作成したLambda関数で以下の設定を変更しました

  • 一般設定
    • 実行時間:3分に変更しました
    • メモリ:512MBに変更しました

    (※ 最適化した値ではありませんので、適宜調整してください)

  • アクセス権限

    • 権限:作成したS3バケットへのアクセス権限(読み書き)、Bedrockへのアクセス権限(読み書き)を追加しました。
    • 今回は簡単なテストとして、AmazonS3FullAccessとAmazonBedrockFullAccessを付与しました(※ 最小権限の原則としては非推奨な設定方法です。実運用では対象バケットや、対象モデル・リージョンなどを絞ってください)

実行方法

パワーポイントの資料をS3のバケットにアップロードしました。

今回、使用したのはこちらのパワーポイント資料です。(特段理由はなく、以前使ったことがある資料です)

https://www.jinji.go.jp/saiyo/siken/senkou/setsumeikai_17.pptx

関数をLambdaのページの「テスト」から実行しました。(もちろん、CLIなどで実行することも可能です)。s3_object_uri_inputに、アップロードしたS3のURIを設定して、「テスト」を実行しました。

結果

変換処理

PDF変換(pptx→pdf)・画像の出力(pdf→jpg)が、ともにエラーなく動き、パワーポイントを開いた時と同じ状態を、画像ファイルとして出力できました。サイズは2167x1500[px]でした(おそらく、設定で変更できると思います。未確認です)

文字起こし

Claude3で文字起こしした処理結果は以下のとおりでした。前回と同様、ほぼ文字を間違えることなく、妥当な形式で文字起こしがされました。

1ページ目

# 経済産業省の Mission

## 日本経済・国民の暮らしを豊かにする

### 産業政策
- 人工知能、IoT、ヘルスケア
- コンテンツ、中小企業、
- 産業構造・・

### 通商・貿易
- EPA、TPP、インフラ輸出
- 新興国戦略、ルール形成
- 戦略・・

### 資源・エネルギー
- 電力自由化、新エネ・省エネ
- 原発、資源外交・・

### 手段
- 経済成長
- 産業競争力の強化
- イノベーション
- 世界の富の取り込み
- エネルギー安定供給

### 目的
- 社会課題の解決
  - Ex 少子高齢化、貧困問題、世界の不安定化
- 豊かな社会の実現

2ページ目

# 活気ある職場・働きやすい環境①

## 職場風景

[オフィスの様子が写っている。複数の人が机に向かって作業をしている]

[3人が机を囲んで打ち合わせをしている様子が写っている]

## 働きやすい職場環境

- テレワーク
- ペーパーレス ※4年で37%削減
- フレックス
- 風通しのよい職場(職員意識調査:職場満足度7割以上)
- 様々な研修制度(年間100回以上の勉強会の開催など)

[ノートパソコンの画像が2つ載っている]

(資料)抜粋パンフレットより抜粋

3ページ目

# 働きやすい環境②

## バリアフリーな環境

- 障害をお持ちの方の働きやすさに配慮し、設備面の環境を整備
  - [車いす用のエレベーター、多機能トイレ、階段の手すり、熱源室の点字プレートの写真]

## サポート体制

- 相談を受け付ける相談員を配置
- 個別的なサポートを必要とする方へ支援者を選任

# 別館入口のスロープ

[別館入口のスロープの写真]

4ページ目

# 募集内容(常勤)

人事院陸曹者選考試験(係員級)に加え、3つの経済産業省独自パス※による募集を開始。多様な活躍の場を御提供。
※: ①係長級、②課長補佐級、③専門スタッフ職

## 官職イメージ

[ピラミッド形の階層図が示されている]

## 採用予定数・応募資格

- 陸曹者選考試験: 11名
- 係長級、課長補佐級、専門スタ1級: 若干名

### 年齢要件

| 区分 | 大卒後 | 高専卒 | 高卒後 |
|------|-------|--------|--------|
| 係長級 | 7年以上 | 10年以上 | 12年以上 |
| 課長補佐級 | 18年以上 | 21年以上 | 23年以上 |
| 専門スタ1級 | 23年以上 | 26年以上 | 28年以上 |

## 選考スケジュール

### 受験申込受付期間
- 11月13日(火曜日)~12月14日(金曜日)正午

### 第1次選考(経歴、論文)通過者発表日
- 1月11日(金曜日)

### 第2次選考日(面接)
- 1月21日(月曜日)~1月25日(金曜日)で指定する日

### 最終合格者発表日(内定日)
- 2月1日(金曜日)(予定)

5ページ目

# 募集内容(非常勤)

## 業務内容は、事務業務から専門業務まで多様、個人の要望を踏まえ柔軟に対応。

### 業務内容例
- 事務業務:
  - 文書の受領、チェック、管理、発送
  - パソコンデータ入力 等
- 専門業務:
  - 政策立案に対する補助、調査、分析業務 等
- 技能労務業務:
  - 自動車運転手、清掃、警備 等

### 採用予定数
- 平成30年度:40名程度
- 平成31年度:65名程度
※勤務実績が良ければ、常勤職員へのステップアップも可能

## 選考スケジュール

- 随時実施中。
- 選考プロセスは以下のとおり。
  1. ハローワークもしくは当省HPで求人確認の上、応募書類を送付。
  2. 書類選考、面接を経て、合否を連絡。

## [問合せ先]
大臣官房総務課雇用者雇用担当まで ※お気軽に御連絡ください!
TEL: (掲載するため削除しました)
Mail: <常勤>(掲載するため削除しました)
      <非常勤>(掲載するため削除しました)

[QRコード: 常勤採用HP] [QRコード: 非常勤採用HP]

※ 「(掲載するため削除しました)」は、電話番号やメールアドレスが正しく文字起こしされていましが、掲載のため削除しました

分析

良い点

  • 下記の「悪い点」に書いた2箇所を除いて、起こされた文字は正しかった
    • ここまで手作業なしなら、全部まかせても良い気がします。
  • 画像の文字起こしもできた
  • 表の文字起こしもできた
  • 表ヘッダを推定して補足した
    • 4ページ目の「### 年齢要件」の表部分で、左列にはもともと表ヘッダがありませんでしたが、「区分」と推定されて追加されてました
    • 勝手に補足されている=文章が変わっているため、これは悪い点とも言えるかもしません。しかし、妥当な形で補足されているので、RAGでLLMの回答時のリファレンスとして使用するには問題なさそうです。また、勝手に補足させたく無い場合は、プロンプトに指示を追加すれば、抑制できそうです。
  • ヘッダ(区切り)を補足した
    • 4ページ目の「### 年齢要件」というものはもとの資料にはありませんが、LLMの出力結果では追加されました
    • たしかに要件について書かれているので、合っていると判断して良いと思われますが、年齢かと言われるとちょっと微妙な気もします。
  • 単語を補足した
    • 4ページ目の「専スタ」という単語が「専門スタ」と置き換えられました
    • 自分も気づきませんでしたが、たしかに専門スタッフのことを指していそうです

実際の場面でも、コンテキストや、資料を作成している人が前提としてしまっている内容などがあります。プロンプトに指示を加えれば、こうした情報を補足させながら、文字起こしをさせることができそうです。

悪い点

  • 一部文字起こしが間違ってしまった
    • 4ページ目で2箇所あります。同じ単語が間違っていて、形の似た単語に置き換えられてしまっています。
    • また、5ページ目で「※勤務実績が良好であれば」が「※勤務実績が良ければ」になってしまっています。(こちらは意味が変わっていないので、大きな問題にはならなさそうです)
    • テキスト自体はドキュメントローダを使って抽出できるので、前処理でテキストを抽出し、結果をプロンプトに入れて文字起こしすることで、このミスは防げそうです。また、ファイルの説明やコンテキストを加えれば、このミスは軽減できそうです。
  • 文字起こしが抜けてしまっている箇所があった
    • 2ページ目の「- テレワーク」の横の注意書きと、右下の画像下のテキストが抜けてしまっています
    • 前回の結果では文字起こしできていたので、画像サイズ・プロンプト・パラメータ設定などを調整する必要がありそうです(画像形式が、前回はpngで今回はjpgなので、この差も影響しているかもしれません)
  • 画像の説明が少し端的だった(もう少し詳しく画像を説明してほしい)
    • これはプロンプトの問題だと思われるので、もう少しプロンプトを凝ると良さそうです
  • 表部分を見出しとして読んでしまった
    • 4ページ目の下部の表部分は、markdownの表として読まれることを想定していましたが、出力結果では見出しになりました。意味はわかるので大きな問題ではないですが、表として読ませたいところです。
    • ただ、4ページ目の中央右の表と比較すると、下部の表はヘッダがある表ではなく、それぞれ独立しているので、見出しとして出力されるのも、おかしくはない気がします。
  • 見出し部分の階層がずれてしまった
    • 5ページ目の「## 選考スケジュール」は、「### 選考スケジュール」の方が正しいと言えそうです

全体通して

基本的にテキストや画像をテキストとして読み込むことができました。一部ミスがありましたが、ドキュメントローダでテキスト抽出する前処理を挟んだり、プロンプトやパラメータ調整をすれば、改善できそうです。パワーポイント資料を読み込ませる手法として利用できそうな印象を持ちました。

また、読者の立場にたって、わかりにくかったり情報が足りない箇所を推定して、補足を追加させながら、資料を読ませることができそうです。

今回は、1回のAPIコールで1つの画像を渡す方法でしたが、APIには複数の画像を含めることも可能です。一気に画像を渡した方が、前後の文脈から推定できる情報が増えるので、より正確にテキストに変換することができると考えられます。このあたりは今後試してみたいです。

まとめ

パワーポイント資料をClaude3を使って読み込む処理を、Lambda関数上で処理する方法を記載しました。具体的には、libreofficeとpdf2imageを使って画像に変換し、BedrockのAPIを実行するコンテナを作成し、テキストに変換できることを確認しました。

文字起こしのミスはほとんどなく、十分使えるレベルだと感じました。今回は簡単に試しただけなので、プロンプトを書き直したりや前処理を追加することで、さらに精度をあげる余地がありそうです。

参考にさせていただいたサイト・ページ

AWS LambdaでLibreOfficeを実行する|ディマージシェア/採用ブログ

Lambda関数でLibreOfficeを使ってExcelファイルからpdfファイルを出力する - Qiita

補足

他の方法をいくつか試したのですが、日本語が文字化けしてしまったり、有料なライブラリを使用する必要があったり、Windowsでしか動かなかったり、更新が行われていないためかライブラリが動かなかったりしました。

必要なパッケージをインストールしたうえで、libreofficeを使うことで、うまく動かすことができました。

謝辞

パワーポイント資料をPDFに変換する処理について、データアナリティクス事業本部の中村さんにアドバイスいただきました。ありがとうございました。

Linuxマシン上で同様の処理を実行する場合については、中村さんの記事に書かれていますので、こちらも併せてご覧ください。

【小ネタ】Officeが入ってないLinuxなどのマシンでPowerPointファイルをスライド一枚ずつJPEGファイルに変換する方法 | DevelopersIO