<目次>
OpenAIでPDFファイルを要約する方法(サンプルプログラムをご紹介)
やりたいこと/概要
サンプルプログラム
動かし方
OpenAIでPDFファイルを要約する方法(サンプルプログラムをご紹介)
やりたいこと/概要
サンプルプログラム
●メイン処理:openai_pdf_summarize.py
import openai import re import time from azure.ai.formrecognizer import DocumentAnalysisClient from azure.core.credentials import AzureKeyCredential from log_utils import prepare_logger import env FR_ENDPOINT = env.get_env_variable("FR_ENDPOINT") FR_KEY = env.get_env_variable("FR_KEY") openai.api_key = env.get_env_variable("OPEN_AI_KEY") openai.api_base = env.get_env_variable("OPEN_AI_BASE") openai.api_type = env.get_env_variable("OPEN_AI_TYPE") openai.api_version = env.get_env_variable("OPEN_AI_VERSION") model_deploy_name = env.get_env_variable("OPEN_AI_DEPLOY_NAME") def analyze_general_documents(binary_file,logger): # ----------------------------------------- # binaryファイルを解析し、ファイル中の文字列を抽出する # ----------------------------------------- # DocumentAnalysisClientクラスのインスタンスを作成 # →Form Recognizerのサービスを使える様にする document_analysis_client = DocumentAnalysisClient(endpoint=FR_ENDPOINT, credential=AzureKeyCredential(FR_KEY)) # ドキュメントの解析 # →ドキュメントの取得方法は「ファイルから」と「URLから」の2通りがある # (1)ファイル形式のドキュメント poller = document_analysis_client.begin_analyze_document("prebuilt-document", binary_file) # (2)URLからドキュメントを取得 # poller = document_analysis_client.begin_analyze_document_from_url("prebuilt-document", docUrl) # 戻り値:AsyncLROPoller のインスタンス # →poller オブジェクトで result() を呼び出して、 を AnalyzeResult返します。 result = poller.result() # 結果格納用の配列 text_of_doc = [] # 結果は「ページ単位」に分かれている for page in result.pages: # 更にその中で「行単位」に分かれる for line_idx, line in enumerate(page.lines): # 行単位に「単語」の情報を抽出 words = line.get_words() for word in words: # リストに追加 text_of_doc.append(word.content) # 配列の中身を、1つの変数に格納しなおす final_text = "" for text in text_of_doc: final_text += text # logger.debug(f"Azure Form Recognizerの文字認識結果: {final_text}") return final_text def get_summary(text, order_wcount, logger): # 始めから、要約文字数×2 以下の場合 if len(text) < order_wcount * 2: return text # 要約文字数×2 以上の場合 while len(text) > order_wcount * 2: # 分割して要約した結果を連結した結果 final_result = __get_summary_internal(text, order_wcount, logger) # while文の判定対象であるtextを更新 text = final_result return final_result # 分割して要約 def __get_summary_internal(text, order_wcount, logger): # TODO:要約の単位は何文字ずつに区切るのが最適か? split_size = order_wcount * 5 # 上限に抵触しない範囲のカタマリに分割した配列を取得 splited_text = split_text(text, split_size) entire_size = len(splited_text) final_result = "" # 上限に抵触しない範囲のカタマリに分割した配列をループし、その単位で要約 logger.info(f"全体サイズ: {entire_size}") for index, text in enumerate(splited_text): logger.info(f"内部要約回数: {index + 1} 、内部要約サイズ: {len(text)}...") prompt = __get_system_prompt(order_wcount) prompt += text # max_tokensの設定方法 # ひらがな/カタカナ/英数字 = 約2Token、漢字 = 約3Token # とすると、最大3*X Tokenが来る想定。 # X=300の場合 3*300*10=6000 response = "" try: response = openai.ChatCompletion.create( engine=model_deploy_name, messages=[{"role": "system", "content": prompt}], temperature=0, max_tokens=5000, top_p=0.95, frequency_penalty=0, presence_penalty=0, stop=None) except openai.error.InvalidRequestError: logger.error( f"openai.ChatCompletion.create() でエラーが発生しました。入力プロンプト : {prompt}") raise except openai.error.RateLimitError as e: logger.error(e) raise final_result += response['choices'][0]['message']['content'] # time.sleep(60) logger.info(f"最終要約結果: {final_result}") return final_result def split_text(text, split_size): # 文章を「。」「.」「!」「?」で分割して配列に格納 # FIXME:「.」による分割はYYYY.MM.DD形式の日付が途中で分割されてしまう sentence_array = re.split("。|.|!|?|\.", text) result,chunk = [],"" # OpenAIに渡せる「最大文字数」になるまで文章を連結し、その単位で配列に格納 # ・enumerate() 関数を使用することで、要素のインデックスと値の両方を取得できます。 for index, sentence in enumerate(sentence_array): chunk += sentence if __check_if_next_iteration_exeeds_limit(chunk, index, sentence_array, split_size): result.append(chunk) chunk = "" return result def __get_system_prompt(order_wcount): instructions = "以下の【文章】を要約してください。" \ "要約する際のルールは以下です。" \ f"【ルール1】{order_wcount}文字以内に要約すること。" \ "【ルール2】要約時に内容と関連が無い内容が含まれている場合、結果からは除外してください。" \ "【ルール3】要約結果は箇条書きではなく、連続した文章として返却すること。" \ "【文章】\n" # logger.debug(f"システムプロンプト: {instructions}") return instructions # 文字列がOpenAIに渡せる「最大文字数」に抵触しないか?のチェック # ・特殊メソッドは、Pythonの内部で特別な役割を果たすため、直接的に呼び出すことは一般的ではありません。 # ・代わりに、特定の操作や構文が使用されるときに自動的に呼び出されます。 def __check_if_next_iteration_exeeds_limit(chunk, idx, sentence_array, split_size): # 次の要素がsentence_arrayの最後の要素の場合はTrue if idx + 1 == len(sentence_array): return True # 次の要素を連結し、split_sizeの値を超す場合はTrue、その他の場合はFalse return (len(chunk) + len(sentence_array[idx + 1])) >= split_size def get_pdf_summary(logger): # PDFをテキスト化 with open("./leaf18.pdf", "rb") as f: prompt_pdf_text = analyze_general_documents(f,logger) # 要約 summary = get_summary(prompt_pdf_text,300,logger) if __name__ == "__main__": logger = prepare_logger() get_pdf_summary(logger)
●ロギング:log_utils.py
import logging def prepare_logger() -> logging.Logger: # ロガーの作成 logger = logging.getLogger(__name__) # ログハンドラの作成 handler = logging.StreamHandler() # ログフォーマットの設定 formatter = logging.Formatter('xxxxxxxxxxxxx : %(asctime)s - %(name)s - %(levelname)s - %(message)s') handler.setFormatter(formatter) # ログハンドラをロガーに追加 logger.addHandler(handler) logger.setLevel(logging.DEBUG) return logger
●環境変数:env_sample.py
def get_env_variable(key): env_variable_dict = { # ------------------------------------ # Azure Form Recognizer # ------------------------------------ "FR_ENDPOINT" : "", "FR_KEY" : "", # ------------------------------------ # OpenAI # ------------------------------------ "OPEN_AI_KEY" : "", "OPEN_AI_BASE" : "", "OPEN_AI_TYPE" : "", "OPEN_AI_VERSION" : "", "OPEN_AI_DEPLOY_NAME" : "", # ------------------------------------ # App名:Slack_Python_Flask # ------------------------------------ # BotユーザーID "BOT_USER_ID" : "", # Botトークン(Flask) "WEBAPPS_SLACK_TOKEN" : "", "WEBAPPS_SIGNING_SECRET" : "", # Botトークン(ソケットモード) "SOCK_SLACK_BOT_TOKEN" : "", "SOCK_SLACK_APP_TOKEN" : "" } ret_val = env_variable_dict.get(key, None) return ret_val
動かし方
> python -m venv .venv
> .venv/Scripts/activate
> pip install -r requirements.txt