Ngrok,ChatGPT API,Pythonをもちいて名刺から会社名を抜き取るLINEBOTを作る

名刺の情報を手作業でうちこむ人を見て，そんなのはいやだな…と思い，自動でCSVに打ち込んでもらえるまではいかなくても，情報を抜き取るくらいはできるんじゃないかと思い立ち，これを作った。LINEBOTにしたのはLINEならだれでも使ってもらえると考えたからだ。

また，この記事は初学者によるもので出来たらなんでもいいやという精神でやっているので，コードの汚さや無駄さなどは目をつむってほしい。
## 紹介しないもの
### LINEのBOT作成
LINE側でBOTの作成をする必要がある。ただ，簡単だし無料なので安心してほしい
LINEの公式アカウントの作り方やAPIキーの取得方法などは適宜ほかのサイトを参照してほしい。
### Ngrok
Ngrokはよくわからないけどローカルサーバーを無料でやってくれるてきなやつ。これも簡単だし無料。
これらはこの記事を見ればすぐできると思う。

https://qiita.com/NaruNeko/items/482ed8dadc919d85f658
### OCR
pyocrをつかうには，事前にダウンロードが必要なので注意。

早速だがコードの全文は以下のようになる。
```
import os
import requests
from io import BytesIO
import base64
import cv2
import pyocr
from flask import Flask, abort, request
from PIL import Image
import openai
from linebot import LineBotApi
from linebot.v3.webhook import (
    WebhookHandler
)
from linebot.v3.exceptions import (
    InvalidSignatureError
)
from linebot.v3.messaging import (
    Configuration,
    ApiClient,
    MessagingApi,
    ReplyMessageRequest,
    TextMessage,
    ImageMessage
)
from linebot.v3.webhooks import (
    MessageEvent,
    TextMessageContent,
    ImageMessageContent

)

#Flask
app = Flask(__name__)
#各key
handler = WebhookHandler('channel sedcret keyを入れてね')
configuration = Configuration(access_token='access token keyをいれてね')
openai.api_key="OpenAPI keyをいれてね"
accesstoken='ここは無駄なところだけど後で参照してるからまたaccess token keyをいれてるよ'

#chatgptの関数
def generate_flex_message(theme):
    response = openai.ChatCompletion.create(
        model="gpt-4o",
        messages=[
            {"role": "system", "content": """
            base64データから会社名，役職，名前，メールアドレスを単語のみで書き出してください。できなかったらその理由を返してください。
             """},
            {"role": "user", "content": theme},
        ],
    )
    result = response.choices[0].message['content']
    return result

#LINE webhookとのやりとり
@app.route("/callback", methods=['POST'])
def callback():
    # get X-Line-Signature header value
    signature = request.headers['X-Line-Signature']

    # get request body as text
    body = request.get_data(as_text=True)
    app.logger.info("Request body: " + body)

    # handle webhook body
    try:
        handler.handle(body, signature)
    except InvalidSignatureError:
        app.logger.info("Invalid signature. Please check your channel access token/channel secret.")
        abort(400)

    return 'OK'

#LINEにおくられたメッセージに対しての反応
#文字だったら画像を送るよう指示
@handler.add(MessageEvent, message=TextMessageContent)
def handle_message(event):
    #MessagingAPIの利用
    with ApiClient(configuration) as api_client: 
        line_bot_api = MessagingApi(api_client)
        
        if event.message.text == 'あ':
            message1='画像を送信してね'
        else: 
            message1='画像を送信してね'
        #LINEで返信する構文
        line_bot_api.reply_message_with_http_info(
            ReplyMessageRequest(
                reply_token=event.reply_token,
                messages=[TextMessage(text=message1)]
            )
        )
#画像が送られてきたときの対応
@handler.add(MessageEvent, message=ImageMessageContent)
def handle_image(event):
     #MessaagingApiの利用
     with ApiClient(configuration) as api_client:
        a='1'
        line_bot_api = MessagingApi(api_client)
        #get_message_contentを使うためにLineBotApiも
        line_bot_api1 = LineBotApi(accesstoken)
        #画像のid入手
        message_id = event.message.id
        #画像のバイナリコードの入手
        message_content = line_bot_api1.get_message_content(message_id)
        cwd =f".../image/{message_id}.png"
        
        with open(cwd, "wb") as f:
        # バイナリを1024バイトずつ書き込む
            for chunk in message_content.iter_content():
                f.write(chunk)
        #画像の二値化
        img = cv2.imread(cwd, 0)
        ret2, img_otsu = cv2.threshold(img, 0, 255, cv2.THRESH_OTSU)
        cv2.imwrite(cwd,img_otsu)

        img1 = Image.open(cwd)

        TESSERACT_PATH = '.../AppData/Local/Programs/Tesseract-OCR'
        os.environ["PATH"] += os.pathsep + TESSERACT_PATH
        #OCRエンジン取得
        tools = pyocr.get_available_tools()
        tool = tools[0]
        #OCRの設定 ※tesseract_layout=6が精度には重要。デフォルトは3
        builder = pyocr.builders.TextBuilder(tesseract_layout=6)
        #解析画像読み込み
        img = Image.open(f'.../image/{message_id}.png') #他の拡張子でもOK
        #画像からOCRで日本語を読んで、文字列として取り出す
        txt_pyocr = tool.image_to_string(img , lang='jpn', builder=builder)

        #半角スペースを消す ※読みやすくするため
        txt_pyocr = txt_pyocr.replace(' ', '')
        # 読み込んだ画像の幅、高さを取得し半分に
       ## (width, height) = (img1.width // 6, img1.height // 6)
        # 画像をリサイズする
      #  img_resized = img1.resize((width, height))
        # ファイルを保存
      #  img_resized.save(cwd, quality=90)
        #画像をbase64コードに変換
      #  with open(cwd , 'br') as f0:
      #      encode_data = base64.b64encode(f0.read()).decode()
        
    #    print(encode_data)
        
        #chatgptへ渡す
        output_flex_message = generate_flex_message(txt_pyocr)
    #結果の返信の構文
        line_bot_api.reply_message_with_http_info(
            ReplyMessageRequest(
                reply_token=event.reply_token,
                messages=[TextMessage(text=output_flex_message)]
        )
    )
 
if __name__ == "__main__":
    port = int(os.getenv("PORT", 3000))
    app.run(host="0.0.0.0", port=port, debug=False)

```
では各部分を簡単に解説していこう
### ライブラリのインポート
```
import os
import requests
from io import BytesIO
import base64
import cv2
import pyocr
from flask import Flask, abort, request
from PIL import Image
import openai
from linebot import LineBotApi
from linebot.v3.webhook import (
    WebhookHandler
)
from linebot.v3.exceptions import (
    InvalidSignatureError
)
from linebot.v3.messaging import (
    Configuration,
    ApiClient,
    MessagingApi,
    ReplyMessageRequest,
    TextMessage,
    ImageMessage
)
from linebot.v3.webhooks import (
    MessageEvent,
    TextMessageContent,
    ImageMessageContent

)
```
くそなげえｗというのも使ってないやつもとりあえず入ってるから、絶対に全部んきゃいけないわけじゃないんだけど面倒だったからかえてない、ご了承。一応，requestsはurlから情報をとれる。BytesIOはバイナリをメモリ上で使えるようになるらしい。Base64は画像データをBase64コードに変換するのに使った。pyocrはOCRという画像から文字を摘出する方法を簡単にできるもの。flaskはよくわかんない（おい）。PILは画像の加工ができる。openaiはchatgpt apiを使えるようになる。以下はline bot についてのライブラリ。こんな感じでくそ適当に使ってる。

###ChatGPT
```
def generate_flex_message(theme):
    response = openai.ChatCompletion.create(
        model="gpt-4o",
        messages=[
            {"role": "system", "content": """
            base64データから会社名，役職，名前，メールアドレスを単語のみで書き出してください。できなかったらその理由を返してください。
             """},
            {"role": "user", "content": theme},
        ],
    )
    result = response.choices[0].message['content']
    return result
```
ここではChatGPTとのやり取りを書いている。このような構文で，簡単にChatGPTに質問できる。returnは回答の文章をとってきている。
```
#LINE webhookとのやりとり
@app.route("/callback", methods=['POST'])
def callback():
    # get X-Line-Signature header value
    signature = request.headers['X-Line-Signature']

    # get request body as text
    body = request.get_data(as_text=True)
    app.logger.info("Request body: " + body)

    # handle webhook body
    try:
        handler.handle(body, signature)
    except InvalidSignatureError:
        app.logger.info("Invalid signature. Please check your channel access token/channel secret.")
        abort(400)

    return 'OK'
```
ここは正直意味が分かってないが，参考にした記事を見るにおそらくNgrokを通してLINEとやりとりしてる感じだと思う？
### 実際の応答
```
#LINEにおくられたメッセージに対しての反応
#文字だったら画像を送るよう指示
@handler.add(MessageEvent, message=TextMessageContent)
def handle_message(event):
    #MessagingAPIの利用
    with ApiClient(configuration) as api_client: 
        line_bot_api = MessagingApi(api_client)
        
        if event.message.text == 'あ':
            message1='画像を送信してね'
        else: 
            message1='画像を送信してね'
        #LINEで返信する構文
        line_bot_api.reply_message_with_http_info(
            ReplyMessageRequest(
                reply_token=event.reply_token,
                messages=[TextMessage(text=message1)]
            )
        )
```
ここから実際にメッセージに対しての応答になる。
message=TextMessageContentとすれば，テキストが送られてきたときの応答をかける。MessagingApiをつかって，event.message.textで送られてきた文章を得ることができる。ここでif文で分けてるのは作成中にいろいろ確認してたからで，たぶん丸っとなくても動く。line_bot_api.reply_message_with_http_infoで返信する内容を書いている。このメソッドではテキストのみ送れる。名刺を画像で入手したいので，テキストが送られたら画像を送信してねと返すようにしてある。
```
#画像が送られてきたときの対応
@handler.add(MessageEvent, message=ImageMessageContent)
def handle_image(event):
     #MessaagingApiの利用
     with ApiClient(configuration) as api_client:
        a='1'
        line_bot_api = MessagingApi(api_client)
        #get_message_contentを使うためにLineBotApiも
        line_bot_api1 = LineBotApi(accesstoken)
        #画像のid入手
        message_id = event.message.id
        #画像のバイナリコードの入手
        message_content = line_bot_api1.get_message_content(message_id)
        cwd =f".../image/{message_id}.png"
        
        with open(cwd, "wb") as f:
        # バイナリを1024バイトずつ書き込む
            for chunk in message_content.iter_content():
                f.write(chunk)
        #画像の二値化
        img = cv2.imread(cwd, 0)
        ret2, img_otsu = cv2.threshold(img, 0, 255, cv2.THRESH_OTSU)
        cv2.imwrite(cwd,img_otsu)

        img1 = Image.open(cwd)

        TESSERACT_PATH = '.../AppData/Local/Programs/Tesseract-OCR'
        os.environ["PATH"] += os.pathsep + TESSERACT_PATH
        #OCRエンジン取得
        tools = pyocr.get_available_tools()
        tool = tools[0]
        #OCRの設定 ※tesseract_layout=6が精度には重要。デフォルトは3
        builder = pyocr.builders.TextBuilder(tesseract_layout=6)
        #解析画像読み込み
        img = Image.open(f'.../image/{message_id}.png') #他の拡張子でもOK
        #画像からOCRで日本語を読んで、文字列として取り出す
        txt_pyocr = tool.image_to_string(img , lang='jpn', builder=builder)
        
        #chatgptへ渡す
        output_flex_message = generate_flex_message(txt_pyocr)
    #結果の返信の構文
        line_bot_api.reply_message_with_http_info(
            ReplyMessageRequest(
                reply_token=event.reply_token,
                messages=[TextMessage(text=output_flex_message)]
        )
    )
```
画像が送られたら本番だ。message=ImageMessageContentとすれば画像が送られたときに対応できる。基本的に画像のバイナリコードは.get_message_contentで得られるが，これはMessagingapiでは対応してないらしく，LineBotApiも入れている。（たぶんほかにやりようはありそう）
画像を保存したいところをcwdでPATHを通して，容量が大きいので，1024バイトずつ書き込んで保存している。これでLINEから画像を保存できたが，このままでは容量が大きい。そのために，今回は名刺の情報を読み取ればいいので，文字さえ分かればいいということで，二値化の処理をしている。二値化とは，画像で，ある値以上は黒，それ以下は白とすることで，白黒にするという処理。今回はcv2.thresholdで勝手に閾値を決めてくれている。二値化したところで，OCRをする。pyocrを使うには事前にtesseractのダウンロードが必要なので注意。
```
TESSERACT_PATH = '.../AppData/Local/Programs/Tesseract-OCR'
        os.environ["PATH"] += os.pathsep + TESSERACT_PATH
        #OCRエンジン取得
        tools = pyocr.get_available_tools()
        tool = tools[0]
        #OCRの設定 ※tesseract_layout=6が精度には重要。デフォルトは3
        builder = pyocr.builders.TextBuilder(tesseract_layout=6)
```
でtesseractの準備を終えたところで，txt_pyocr = tool.image_to_string(img , lang='jpn', builder=builder)で簡単にOCR処理をしてくれて，文字列を返してくれる。（制度はまあまあといったところ）
画像から文字列が入手できたところで，この文字列をchatgptに渡して，会社名やら名前やらを摘出してもらう。上のほうでchatgptの関数を作っていたので，関数にOCRでもらった文字列を渡すだけでできる。chatgptで判断してもらった回答を返信の構文に入れて，無事LINEの画面に名刺の情報がおくられることになる。

そんなこんなで，結果的には名刺の情報を抜き出すことができた。記事を見てもらえばわかる通り，初学者なので，多少の無駄は目をつむってほしい。
### 苦労したところ
chatgpt apiが画像データを渡しても反応してくれないので，何らかのコードの形で渡す必要がある。だが，普通の写真をバイナリコードやbase64などに変換しても，とても重くて使い物にならなかった。そこで，今回はOCRを使用した。
地味な落とし穴として，LineBot v3ではmessage=ImageMessageではいけないこと，ImageMessageContentをwebhookのところでインポートしなければならないことが地味にわかりづらかった。

あとは制度の問題がどうしてもあるので，実用するなら最終的には目で確認しなければならないことはある。

見づらい記事だったと思うが，見てくださってありがとうございました。
### 参考にした記事

https://qiita.com/NaruNeko/items/482ed8dadc919d85f658

https://qiita.com/tamago324/items/4df361fd6ac5b51a8a07

https://web-lh.fromation.co.jp/archives/10000061001

https://qiita.com/ku_a_i/items/93fdbd75edacb34ec610