CCCMKホールディングス TECH LABの Tech Blog

TECH LABのエンジニアが技術情報を発信しています

ブログタイトル

databricks Model Servingを使ってPDF・PPTXテキスト抽出APIを動かしてみる

こんにちは、CCCMKホールディングスAIエンジニアの三浦です。

ファイルを読み込み、その内容について回答が出来るLLM Agentを作ることが時々あるのですが、毎度ファイル読み込み処理を作るのが面倒だな、と感じることがありました。PDFやPPTXファイルからテキストデータを抽出したいというニーズはLLM Agentに限らず色々なシーンで発生するので、WebAPIにして様々なアプリケーションから利用できるようにしてみようと考えました。

稼働する環境はdatabricksのModel Servingで構築しました。Model Servingを利用することで、databricksを利用できる環境さえあれば追加でサーバーなどを構築する必要なくAPIを提供することが可能です。

今回の記事ではModel ServingでPDF/PPTXファイルからテキストデータを抽出するAPIの構築から提供するまでの手順をソースコードを交えてご紹介したいと思います。

Model Servingに任意のPythonコードをデプロイするために

databricksのModel ServingはMLflowで管理されたモデルをデプロイするものですが、MLflowのPyFuncというモデルを使うことで任意のPythonコードや関数をデプロイすることが出来ます。

learn.microsoft.com

PyFuncを使う場合、上のドキュメントにあるようにモデルロード時に1度だけ実行されるload_context関数とリクエストが来た際に実行されるpredict関数を実装します。あとはPyFuncモデルを通常のモデルと同様にlog_modelでExperimentに記録し、register_modelでUnity Catalogに登録したのちModel Serving Endpointにデプロイする手順を取ります。

PythonでPDFとPPTXの内容をテキスト化する

肝心のPDFやPPTXの内容をテキストデータで取得する処理は、PDFはPyMuPDFを、PPTXはpython-pptxというライブラリを使って実装しました。

・PyMuPDFのドキュメント

pymupdf.readthedocs.io

・python-pptxのドキュメント

python-pptx.readthedocs.io

どちらも軽量で、CPU環境で動かすことが出来ました。ただOCR機能はついていないので、その機能を付けようとするともしかしたらもう少しリソースが必要になるかもしれません。

具体的な実装

ユーティリティ関数

PyFuncモデルで使用する関数をPythonファイルに以下の内容で実装し、保存しておきます(とりあえず"utils.py"という名前で作成)。

import base64
import io
from typing import Any, Dict, Optional, Tuple, List

import fitz
from pptx import Presentation

def decode_base64_to_bytes(file_base64: str) -> bytes:
  """base64文字列をbytesに変換する"""
  return base64.decodebytes(file_base64.encode("utf-8"))

def detect_ext(filename: str) -> str:
  """ファイル名から拡張子を判定する"""
  if not filename or "." not in filename:
    return ""
  return filename.lower().rsplit(".",1)[-1]

def extract_pdf_text(file_bytes: bytes)->Tuple[str, dict]:
  """PDF形式のファイルbytesからテキストを抽出する"""
  doc = fitz.open(stream=file_bytes, filetype="pdf")
  pages = []
  for i, page in enumerate(doc):
    text = page.get_text("text")
    pages.append(text)
  text = "==============================\n".join(pages) # ページ区切り
  meta = {"pages": len(doc), "filetype": "pdf"}
  doc.close()
  return text.strip(), meta

def extract_pptx_text(file_bytes: bytes)->Tuple[str, dict]:
  """PPTX形式のファイルbytesからテキストを抽出する"""
  prs = Presentation(io.BytesIO(file_bytes))
  pages = []
  for si, slide in enumerate(prs.slides):
    pages.append(f"[Slide {si+1}]")
    for shape in slide.shapes:
      if hasattr(shape, "text"):
        pages.append(shape.text)
  text = "==============================\n".join(pages)
  meta = {"pages": len(prs.slides), "filetype": "pptx"}
  return text.strip(), meta
  • decode_base64_to_bytes: リクエストで受け取るbase64文字列に変換されたファイルデータをbytesに変換
  • detect_ext: リクエストで受け取るファイル名からファイル拡張子を抽出
  • extract_pdf_text: PDF形式のbytesからテキストデータとメタデータを抽出
  • extract_pptx_text: PPTX形式のbytesからテキストデータとメタデータを抽出

モデル記録用のNotebook

databricksのWorkspaceで先ほど作成した"utlis.py"と同階層に新しくNotebookを作成し、PyFuncモデルの定義と記録用の処理を書いていきます。

まず必要なライブラリをインストールします。

%pip install mlflow>=3.2 pymupdf python-pptx
dbutils.library.restartPython()

Model Servingにデプロイするモデルを以下のように実装します。Model Servingでは、リクエストはList形式で受け付けるため、predictmodel_inputはListが格納されることを想定して実装する必要があります。

このモデルはfile_base64filenameをリクエストで受け取ります。それぞれ対象のファイルデータのbase64文字列、対象のファイル名が格納されることを想定しています。

import base64
from utils import (
    decode_base64_to_bytes, 
    detect_ext, 
    extract_pdf_text, 
    extract_pptx_text
)

class TextExtractorPyFunc(mlflow.pyfunc.PythonModel):
  
  def load_context(self, context):
    """モデルロード時に1度だけ実行される。今回は使用しない。"""
    pass
  
  def predict(self, context, model_input:list[Dict[str, str]]) -> list[Dict[str, Dict]]:
    """リクエスト処理"""
    b64 = model_input[0].get("file_base64")
    if not b64:
        raise ValueError("file_base64 is required")
      
    filename = model_input[0].get("filename","")
    ext = detect_ext(filename)
    file_bytes = decode_base64_to_bytes(b64)

    if ext == "pdf":
        text, meta = extract_pdf_text(file_bytes)
    elif ext == "pptx":
        text, meta = extract_pptx_text(file_bytes)
    else:
        raise ValueError(f"Unsupported file type: {ext}")

    return [{"text": text, "meta": meta}]

MLflowのExperimentへの記録からUnity Catalogへの登録まで、次のコードで進めることが出来ます。log_modelを実行する際にモデルへの入力例を与えるため、サンプルのPDFファイルを読み込んでinput_exampleに渡すようにしました。

import base64
import mlflow

# input_example用にサンプルファイルを読み込む
with open("sample.pdf", "rb") as f:
  b64 = base64.b64encode(f.read()).decode("utf-8")

input_example={
    "file_base64": b64,
    "filename":"sample.pdf"
}

model_name = "text_extractor"

with mlflow.start_run() as run:
    #モデルの記録
    model_info = mlflow.pyfunc.log_model(
        name=model_name,
        python_model=TextExtractorPyFunc(),
        code_paths=["utils.py"], # ユーティリティ関数のPythonファイル
        input_example=[input_example]
    )
    #モデルのUnity Catalogへの記録
    model_uri = model_info.model_uri
    reg = mlflow.register_model(model_uri, f"db.schema.{model_name}")

ちなみにモデルのUnity Catalogへの登録はGUIでも出来るのですが、何か問題があった時の原因が掴みにくいのと、登録までに時間がかかるように感じられるため、コードで実現したほうが良いと思います。

Unity Catalogへの登録が完了したら、あとはModel Servingへのデプロイに進むことが出来ます。こちらの手順は前回の記事と同様ですので、そちらをご覧ください。

techblog.cccmkhd.co.jp

Model Servingにデプロイしたテキスト抽出APIを使ってみる

Model ServingのAPIはアクセストークンを利用して接続が出来ます。

以下はdatabricksの同じWorkspaceから接続を試みた際に作成したコードです。

import requests

# xxxはWorkspace固有の文字列を設定
endpoint = "https://adb-xxxxxxxxxxx.azuredatabricks.net/serving-endpoints/text-extractor/invocations"

# PDFファイルの読み込み
with open("test.pdf", "rb") as f:
  b64_pdf = base64.b64encode(f.read()).decode("utf-8")

input_data = {
    "inputs": [{
        "file_base64": b64_pdf,
        "filename":"test.pdf"
    }]
}

headers = {
    # トークンを取得
    "Authorization": f"Bearer {dbutils.notebook.entry_point.getDbutils().notebook().getContext().apiToken().get()}"
}

response = requests.request(method="POST", headers=headers, url=endpoint, json=input_data)
print(response.json())

この処理をツールにしてLLM Agentに渡すことでPDFやPPTXのファイルの中身を参照して回答出来るアプリなどを開発することが出来ます。

まとめ

ということで今回はdatabricksのModel ServingでPDFやPPTXファイルの中身をテキスト化するAPIを構築した話をご紹介しました。Model Servingに今回のように色々な機能を実装していくことでAgentのツールをどんどん拡張していくとこも可能になりそうです。今度は画像解析などのもう少し処理にリソースが必要になりそうな機能もModel Servingで動かしてみたいと思います。