件名の通り、小説の内容をネタバレ無しでAIに要約させるアプリを作ってみました。(以下文字数を減らすため敬体ではなく常体で書きます。)
きっかけ
私の趣味は読書なのだが、ここ最近仕事の傍らエンジニアへの転職活動と勉強を同時並行していてなかなかハードな生活を送っている。なのでどうしても一日で本を読み切ることが不可能なため、お昼休みに20分だけ読むという方法を取っていたが、日を跨ぐと前の展開を忘れてしまうことが多くなってしまった。なので小説の内容を簡単に要約してそれまでの展開を思い出させてくれるアプリが欲しいと考え、せっかくプログラミングを学んでいるのだから、自分でAIに内容を要約させるアプリを作ろうと思い至った。
アプリ要件
- 青空文庫やKindleといったURLを貼ることでスクレイピングし、要約することが出来る。
- 要約したい文章をコピペして張り付けることで、要約することが出来る。
- 要約内容はネタバレを含まない。
- ついでに人物相関図も表示する。
- 手軽を売りにしたいのでwebアプリで制作する。
アプリの使用言語、ライブラリ構成
- 使用言語はPythonで、colabで動かす。
- 画面はGradio。
- 今回使用するAPIはGTP-4 turbo。
- 人物相関図を生成するためのツールはMermaid. https://mermaid.js.org/
完成までの過程
① まず完成時の画面構成を考える。diagrams (https://www.drawio.com/) で以下の画面構成を作成。
② 次にAPIに要約をさせるためのシステムプロンプトを考える。今回の肝は"ネタバレ無し"で要約させることだが、AIにネタバレという概念はまだないらしく、こちらでネタバレ要因になるワードや文章を考えなければならない。ただしシステムプロンプトの仕様上、あまりにも指示を与えすぎると一部を忘れてしまうため、なるべく簡潔かつ重要な指示は残さなければならない。いろいろ試行錯誤した結果以下のプロンプトに落ち着く。
system_contents = """
あなたはプロの編集者です。要約と人物相関図を出力しなさい。
手順
- 要約: テキストの中から登場人物・場面・出来事などの主要な要素を要約。
- 人物相関図: 要約を元に、登場人物の関係性を示した人物相関図をMermaid記法で出力。
前提条件
1.要約の条件
- 400文字以内で要約すること
- 原文に忠実に、創作や想像で補完せず事実のみを抽出すること
- 文体は「である調」で統一
- ネタバレ(犯人・手口・結末など)禁止
- "もし要約するための文章が30000文字以上の場合、"というトリガーが渡されてから、文章の要約を開始してください。それまでは要約せず待機してください。
- 人物相関図の条件
- 主人公との会話が多い人物、親族、友人、恋人、敵である登場人物の名前は必ず人物相関図に含めること
"""
これが現時点で一番ネタバレ無し、かつ要約の精度が良かったプロンプトとなった。ついでにプロンプトに人物相関図をMermaid記法で出力する指示も入れておいた。
③ ここからコードを書き始める。今回はURL先の小説とコピペした小説を要約できるようにするのだが、まずはコピぺ版が正常に要約できる関数からスタートする。
text = """たかしは麻衣のことが好き。だが、麻衣はたかしの友人のしげるのことが好き。しかし、しげるはたかしのことが好き。"""
まずは上記の簡単なtextから要約させる。これは特に問題なくAPIに要約させることが出来たのだが、問題は次のURL版。。これが結構大変だった。(といってもプロのプログラマーなら特に問題ないと思うが)
まず最初は文字数が少ない森鴎外の高瀬舟でトライ。
これはすんなり要約できたが、じゃあ長編だとどうなるかと思い太宰の人間失格で試してみる。
するとエラーが発生。読むとどうやらAPIに渡せる文字数は最大で30000文字までらしい。人間失格の文字数が78663文字とのことなので余裕でオーバーしてるだろと怒られる。
なので今回、30000文字を超えている文章を要約するために一旦30000文字で文章を区切りAPIに渡し、要約させる。これを繰り返し最後に要約を合体させ出力するという方法を取ることにした。
# もし文字数が30000以上なら
chunk_size = 30000
if len(main_text) > chunk_size:
start_index = 0 # 30000区切りの一回目の処理: 最初の文字のインデックス
end_index = chunk_size # 最後の文字は30000文字目ue
end_index = main_text[start_index:end_index].rindex("。")
# print(start_index, end_index)
chunks = []
while start_index < len(main_text):
# end_index = main_text[start_index:end_index].rindex("。") # まずend_index (30000文字内の一番右の"。") を指定する。
chunk = main_text[start_index:end_index]
chunks.append(chunk)
start_index = end_index + 1
end_index = start_index + chunk_size
if end_index == len(main_text): # ここを == にしないと文章が最後まで入らない。
break
ただ、これだと要約の精度が良くないことに気が付いた。とくにミステリー小説だと最初に貼った伏線が最後に回収されたり、最後だけに重要な人物が出てきてそれまでの展開が一気に変わるというような仕掛けもあるため、一部だけ要約しても意味ないし後から合体した所でチグハグな内容になる。
なので、最終的に"一旦全ての30000文字区切りの1セットをAPIに渡した後に、初めて要約をスタート"させるアルゴリズムにすることになった。
そこで最初に考えたシステムプロンプトに"もし要約するための文章が30000文字以上の場合、"というトリガーが渡されてから、文章の要約を開始してください。それまでは要約せず待機してください。"という指示を追加。そして一旦30000文字で句切り終えたら最後にトリガーである [end] を出力するようにコードを改良した。
while True:
# end_index == len(main_text): # start_indexをend_indexに変える 8/6
end_index = main_text[start_index:end_index].rindex("。") # まずend_index (30000文字内の一番右の"。") を指定する。
chunk = main_text[start_index:end_index]
chunks.append(chunk)
start_index = end_index + 1
end_index = start_index + chunk_size
# もしend_indexが10万文字超えていたらの処理
if end_index >= len(main_text):
chunk = main_text[start_index:]
chunks.append(chunk)
break
for index, chunk in enumerate(chunks):
if index == len(chunks)-1:
messages.append(
{"role": "user", "content": chunk + "<end>"}
)
else:
messages.append({"role": "user", "content": chunk}
)
messages.append({"role": "assistant", "content": "OK!"}
)
else:
messages.append(
{"role": "user", "content": main_text}
)
これでまあ、前よりは若干マシ?かな?と感じるくらいには制度があがった。
④ 最後に実際にgradioを起動してイメージ通りの画面になるかcheckする。
要約をsummary, 人物相関図をcharacter_chart_pathに変数化してGradioに渡すようにしているのだが、下記のようにエラー画面が発生することが多々あった。
おそらく原因は、APIのresponseで帰ってきたsummaryとcharacter_chart_pathをsplit()で正常に抜き出せていなかったのではないかと思う。というのも毎回APIは同じresponseを生成するわけではなく乱数なので、細かくsplit()で”ここからここまでを切抜き出す”指定してもAPIの気分によって改行などのズレが生じ綺麗に抜き出せない。
そこで最低限responseが全て抜き出せるようにコードを改良。なんとかエラー画面が出なくなった。
そして完成したのがこちら。
なんとか要約と人物相関図を出力できるようになった。ちょっといくつか細かい気になるところはあるが、一旦ここで終了とする。
反省点
このアプリを制作するにあたって最大の誤算だったのが、製作費。APIの使用料は有料なのだが、当初は2000円くらいかかるかな?と思っていたのだが、途中から30000文字超えた文章を何度もAPIに渡してしまったため、最終的に想定より何倍も使用料がかかってしまった。これはかなり痛かった。次作るアプリは必ずしっかり製作費を見積もり、安く抑えると強く決意した。