notionに論文を登録すると数式もふくめよしなに翻訳してくれるフローを構築した

# はじめに

論文は読んでいますでしょうか。
私は英語がからっきし駄目なので、億劫で読みたい論文が溜まってしまうことがしばしばです
そんな自分でも、少しでも気楽に論文を読めるように、NotionにPDFリンクを貼ると、自動的にChatGPTが翻訳してくれる仕組みを作りました。

![{86449148-2F85-462E-9CC6-718545B14965}.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2950973/0e0df6ae-3822-c9ee-1fe5-fd72c2cca96a.png)
↑ こんな感じのページが自動で作られます

PDFを翻訳してくれるサービスはそこそこあるのですが、数式が入ってくるとポンコツになるものが多く、またフォーマットもPDFではブラウザで読みにくいです
そこで、Notion上で全ての内容をオブジェクトとして再構成し、数式や画像、表も含めて読みやすい形式にしました

まず初めに｢全体の流れ｣でこのフロー全体を説明し、次に｢PDFをどうやって和訳したのか｣でNotionに書き込む処理(フローチャートで言うCloudRunJobs)を説明します

# 全体の流れ

```mermaid
graph TD
notion[(notion)] -->|Notion Automation| slack
slack -->|Slack Events API| CloudRunFunctions
CloudRunFunctions --> CloudRunJobs
CloudRunJobs -->|NotionAPI| notion_end[(notion)]
```

各矢印についてそれぞれ説明していきます

## NotionAutomationについて

Notionは多機能なオンラインメモツールで、メモやToDo管理だけでなく、データベースとしても使える便利なサービスです
私は論文の管理をNotionで行い、PDFや自分用のメモを一緒に保存しています

NotionにはAPIがあり、ページの読み書きはできるのですが、イベントトリガー機能は現状ないようです
それと似た機能に、｢Notion Automation｣があります
これは｢条件が満たされたときXXする｣という機能です
XXに該当するものは無料ユーザーは｢Slack通知を送信｣のみなので、これを用いました

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2950973/27f1953e-cbcb-1b2a-1ae5-35f39287acf5.png)

Slackは社内チャットツールですが、今回は自分専用のBotがいるワークスペースを作り、そこに通知を送るようにしています。

## Slack Events APIについて

SlackではSlack Events APIが用意されており、Slack内のイベントが起こった際にリアルタイムでHTTPリクエストを送ることができます
GoogleCloudRunFunctionsは、HTTPリクエストを元に簡単なコードが実行できるサービスです。

今回は
- チャンネル内で投稿(=notionの論文の追加)があった際に、GoogleCloudRunFunctionsを叩きに行くSlackBot
- 叩かれたとき、GoogleCloudRunJobsを起動するGoogleCloudRunFunctions

の2つをつくりました。

```python
# 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をどうやって和訳したのか

```mermaid
graph TD
pdf --> |pix2text| markdown
markdown --> section名
markdown --> 数式
markdown --> 本文[本文\n英語]
markdown --> img[画像･表]

本文 -->|ChatGPT| A[本文\n日本語]
img -->|Gyazo| 画像リンク

section名 --> notion
数式 --> notion
A --> notion
本文 --> notion
画像リンク --> notion

```

PDFではデータの取り回しが面倒なため、一度論文をマークダウンに変換しました

## pdf → markdown

変換するライブラリとしては[pix2text](https://github.com/breezedeus/Pix2Text)をつかいました
pix2textはMathpix(数式周りのWebサービス･API)をopen-sourceで再現しようというプロジェクトで、PDF→HTMLのライブラリを色々試しましたが、こちらは論文に特化しているためか一番精度が良かったです
特に数式に関しては、ほぼミスなくTeX形式に変換してくれます
ちょっと変換に時間がかかるところ以外は最高です

```python
from pix2text import Pix2Text

p2t = Pix2Text.from_config(enable_table=False)
doc = p2t.recognize_pdf("./ronbun.pdf")
doc.to_markdown("./output")
```
で論文をMarkdown化してくれます

:::note info
Pix2Text.from_config実行時に必要なファイル(モデルとか)が配置されていないと、自動でダウンロードする処理が入ります。
そのため、環境がリセットされるCloudRun用のDockerImageを作る際は、ダウンロードによる実行時間をなくすため、事前にPix2Text.from_configを実行したイメージを使用すると良いでしょう。
:::


## 数式に関して

数式は数式の始まり･終わりで記号の置換を行っています
- `$\alpha$` → `[$\alpha$]`
- `$$\alpha$$` → `[$$\alpha$$]`

これにより、数式モードと通常のテキストの区別をしやすくしています。

## 本文に関して

本文の翻訳にはChatGPTを使っています
専門用語が不自然に翻訳されるのを避けるため、科学的な専門用語はそのまま英語表記にするようプロンプトを工夫しています

```python
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](https://gyazo.com)のAPIでアップロードし、そのリンクをNotionに記載します

```python
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](https://pypi.org/project/notion-client/)を用いたものの、レイアウトは気合で作りました
あまりにコードが汚いので公開しないです。
なかなかパワーなコードです

APIには一回で長過ぎる文書を送れない制約があるので、適度に文書を分割する必要があります。
なので適宜文字を分割しつつNotionに送るのがいいでしょう
自分は
```python
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を自動翻訳するツールをつくりました
ある程度気軽に論文が読めるようになったので、気になったものをサクッと読んでいきたいです