はじめに
論文は読んでいますでしょうか。
私は英語がからっきし駄目なので、億劫で読みたい論文が溜まってしまうことがしばしばです
そんな自分でも、少しでも気楽に論文を読めるように、NotionにPDFリンクを貼ると、自動的にChatGPTが翻訳してくれる仕組みを作りました。
PDFを翻訳してくれるサービスはそこそこあるのですが、数式が入ってくるとポンコツになるものが多く、またフォーマットもPDFではブラウザで読みにくいです
そこで、Notion上で全ての内容をオブジェクトとして再構成し、数式や画像、表も含めて読みやすい形式にしました
まず初めに「全体の流れ」でこのフロー全体を説明し、次に「PDFをどうやって和訳したのか」でNotionに書き込む処理(フローチャートで言うCloudRunJobs)を説明します
全体の流れ
各矢印についてそれぞれ説明していきます
NotionAutomationについて
Notionは多機能なオンラインメモツールで、メモやToDo管理だけでなく、データベースとしても使える便利なサービスです
私は論文の管理をNotionで行い、PDFや自分用のメモを一緒に保存しています
NotionにはAPIがあり、ページの読み書きはできるのですが、イベントトリガー機能は現状ないようです
それと似た機能に、「Notion Automation」があります
これは「条件が満たされたときXXする」という機能です
XXに該当するものは無料ユーザーは「Slack通知を送信」のみなので、これを用いました
Slackは社内チャットツールですが、今回は自分専用のBotがいるワークスペースを作り、そこに通知を送るようにしています。
Slack Events APIについて
SlackではSlack Events APIが用意されており、Slack内のイベントが起こった際にリアルタイムでHTTPリクエストを送ることができます
GoogleCloudRunFunctionsは、HTTPリクエストを元に簡単なコードが実行できるサービスです。
今回は
- チャンネル内で投稿(=notionの論文の追加)があった際に、GoogleCloudRunFunctionsを叩きに行くSlackBot
- 叩かれたとき、GoogleCloudRunJobsを起動するGoogleCloudRunFunctions
の2つをつくりました。
# GoogleCloudFunctionsのコード
import os
import functions_framework
import google.auth
from google.auth.transport.requests import Request
import requests
from flask import jsonify
@functions_framework.http
def process(request):
ret = request.get_json(silent=True)
# slackのtokenが適切かチェック
if ret is None:
ret = {"error": "Missing Content"}
return jsonify(ret), 403
elif ret.get("token") != os.environ.get('TOKEN_SLACK'):
ret = {"error": "Invalid token"}
return jsonify(ret), 403
# Cloud Run ジョブのAPIエンドポイント
job_name = os.environ.get('JOB_NAME')
region = os.environ.get('REGION')
project_id = os.environ.get('PROJECT_ID')
url = f"https://{region}-run.googleapis.com/v2/projects/{project_id}/locations/{region}/jobs/{job_name}:run"
# ヘッダーに認証トークンを付与
credentials, project = google.auth.default()
credentials.refresh(Request())
token = credentials.token
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
# ジョブを実行するためのリクエストを送信
response = requests.post(url, headers=headers)
if response.status_code in [200, 202]:
ret["result"] = "Cloud Run job triggered successfully"
else:
ret["result"] = "Failed to trigger Cloud Run job"
ret["response"] = response.text
ret["status_code"] = str(response.status_code)
return jsonify(ret)
一応念のために、slackのtokenが適切かチェックする機構を入れています。
いたずらでURL叩かれまくってしまったら怖いので......
NotionAPIについて
NotionAPIではデータの読み込みと書き込みができます。
CloudRunJobsでは未翻訳のNotionページのPDFリンクを読み込み、翻訳内容を書き込む処理を記述します
PDFをどうやって和訳したのか
PDFではデータの取り回しが面倒なため、一度論文をマークダウンに変換しました
pdf → markdown
変換するライブラリとしてはpix2textをつかいました
pix2textはMathpix(数式周りのWebサービス・API)をopen-sourceで再現しようというプロジェクトで、PDF→HTMLのライブラリを色々試しましたが、こちらは論文に特化しているためか一番精度が良かったです
特に数式に関しては、ほぼミスなくTeX形式に変換してくれます
ちょっと変換に時間がかかるところ以外は最高です
from pix2text import Pix2Text
p2t = Pix2Text.from_config(enable_table=False)
doc = p2t.recognize_pdf("./ronbun.pdf")
doc.to_markdown("./output")
で論文をMarkdown化してくれます
Pix2Text.from_config実行時に必要なファイル(モデルとか)が配置されていないと、自動でダウンロードする処理が入ります。
そのため、環境がリセットされるCloudRun用のDockerImageを作る際は、ダウンロードによる実行時間をなくすため、事前にPix2Text.from_configを実行したイメージを使用すると良いでしょう。
数式に関して
数式は数式の始まり・終わりで記号の置換を行っています
-
$\alpha$
→[$\alpha$]
-
$$\alpha$$
→[$$\alpha$$]
これにより、数式モードと通常のテキストの区別をしやすくしています。
本文に関して
本文の翻訳にはChatGPTを使っています
専門用語が不自然に翻訳されるのを避けるため、科学的な専門用語はそのまま英語表記にするようプロンプトを工夫しています
from openai import OpenAI
def text2jp(text):
# return text # testmode
if len(text) <= 5:
return text
if text.startswith("[$") and text.endswith("$]"):
return text
client = OpenAI(api_key=OPENAI_KEY)
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system",
"content": "あなたは翻訳ツールです。翻訳内容以外の文章は決して出力しないでください。インライン数式は[$, $]で、ブロック数式は[$$, $$]で囲われています。科学的専門用語は無理に訳さず英語表記でおねがいします。この表記を変更しないでください。入力された文章を日本語で意訳しなさい。"},
{"role": "user", "content": text}
],
max_tokens=4096,
temperature=0.0,
)
text_ja = response.choices[0].message.content
return text_ja
画像について
NotionAPIでは画像のアップロード機能はなく、リンク先の画像を表示させる機能しかありません。
そのため、論文中の画像を一度GyazoのAPIでアップロードし、そのリンクをNotionに記載します
file_path = "paper_no_img.png"
with open(file_path, 'rb') as file:
response = requests.post(
GYAZO_UPLOAD_URL,
files={'imagedata': file},
data={'access_token': GYAZO_ACCESS_TOKEN}
)
url = response.json()['url']
notionについて
ライブラリはnotion_clientを用いたものの、レイアウトは気合で作りました
あまりにコードが汚いので公開しないです。
なかなかパワーなコードです
APIには一回で長過ぎる文書を送れない制約があるので、適度に文書を分割する必要があります。
なので適宜文字を分割しつつNotionに送るのがいいでしょう
自分は
def split_text(self, text, chunk_size=2000):
for text_split in text.split("\n"):
for i in range(0, len(text_split), chunk_size):
yield text[i:i + chunk_size]
な関数を多用しています
まとめ
Notionのページ更新をイベントトリガーに、数式混じりPDFを自動翻訳するツールをつくりました
ある程度気軽に論文が読めるようになったので、気になったものをサクッと読んでいきたいです