Geminiで日記を小説風にしてもらう

お疲れさまです、みやもとです。

久しぶりにLINEBOTいじりたいなーと思って日記BOTを作りました。

少し前にGASで作った日記BOTがありましたが、結局あんまり続かなかったのでPythonでやり直そうというのが今回の試みです。

## 日記が続かない
[このへんの記事に](https://qiita.com/Mymt_aggw2208/items/09992425735e4c62b8ef)[書きました](https://qiita.com/Mymt_aggw2208/items/2d5961d2f4d36b71adfb)が、前回作ったLINEBOTは

* 日本語と英語で書いてAIに添削してもらう
* 内容をスプレッドシートに保存する
* 毎日決まった時間にリマインドする

という機能にしていました。
機能自体は悪くなかったのですが、

* 数日に1回ぐらいのペースでGAS実行のための権限を付け直さなくてはいけない
* そんなに毎日特筆するようなこともないので似たようなことばっかり書き続けるのに飽きる

という感じで、結局1か月を超えたぐらいで削除してしまいました。

そもそも生来飽き性というか、淡々と記録をつけ続けるのが苦手なので、普通に日記をつけるだけでは続かないのは当然と言えます。
今回なるべく長く続けようと思ったら、何かしら私がおもしろいと思えるものにしなければ。
どうしたら私は私自身の日常をおもしろいと思えるだろうか……

と考えたところで、短い文章を物語風にしてもらえたら他人事っぽくておもしろいんじゃないかなと思い立った次第です。

## いつの間にかCloud FunctionsがCloud Runにまとまっていた
以前にPythonでLINEBOTを作った際はGoogle Cloud Functionsを使っていました。
ほぼほぼコードを書くだけで私の作業はおしまいなので今回も同じ手でいくつもりでしたが、久しぶりにコンソールを開いたらなんか違う感じになっていました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3477580/b0171dfb-d77c-e4fc-5d7a-40fb02357670.png" width="80%">

しばらく触らない間に取り込まれたようです。
まあ基本は同じっぽいのでCloud Runの画面に移動。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3477580/5f919ca7-d1d2-1aa3-036d-a30efcfcc6f2.png" width="80%">

「関数を作成」から「インラインエディタで関数を作成する」を選択

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3477580/092f40eb-fa3a-d0f6-8751-fc831bd2269a.png" width="80%">

ここからはCloud Functionsの時と同じような設定なので、表で書いていきます。
未記載の部分は初期設定のまま変えてません。

<table>
  <tr>
    <th colspan="3">項目</th>
    <th>値</th>
  </tr>
  <tr>
    <td colspan="3">サービス名</td>
    <td>お好きな名前（linebotとかdiarybotとか）</td>
  </tr>
  <tr>
    <td colspan="3">リージョン</td>
    <td>asia-northeast1(東京)</td>
  </tr>
  <tr>
    <td colspan="3">ランタイム</td>
    <td>Python 3.12</td>
  </tr>
  <tr>
    <td colspan="3">認証</td>
    <td>未認証の呼び出しを許可</td>
  </tr>
  <tr>
    <td colspan="4">コンテナの編集</td>
  </tr>
  <tr>
    <td rowspan="4"></td>
    <td colspan="3">変数とシークレット：環境変数</td>
  </tr>
  <tr>
    <td rowspan="3"></td>
    <td>LINE_CHANNEL_ACCESS_TOKEN</td>
    <td>LINE Messaging APIで作成したチャネルのアクセストークン</td>
  </tr>
  <tr>
    <td>LINE_CHANNEL_SECRET</td>
    <td>LINE Messaging APIで作成したチャネルのチャネルシークレット</td>
  </tr>
  <tr>
    <td>GEMINI_API_KEY</td>
    <td>Google Cloud Platformのプロジェクトに紐づけたGeminiのAPIキー</td>
  </tr>
</table>

ここまで入力したら作成ボタンを押して、コードエディタに移動します。

## コードはあんまり変わりない
コードについては以前作ったLINEBOTとそこまで代わり映えしないので、前回と異なる箇所とプロンプト内容を除いて詳細な説明は省きます。

<details><summary>main.py</summary>


```Python
from flask import abort, jsonify
import os
import base64, hashlib, hmac
import google.generativeai as genai
import json

from linebot import (
    LineBotApi, WebhookParser
)
from linebot.exceptions import (
    InvalidSignatureError
)
from linebot.models import (
    MessageEvent, TextMessage, TextSendMessage
)

# CHANNEL_ACCESS_TOKEN
channel_access_token = os.environ.get('LINE_CHANNEL_ACCESS_TOKEN')
# CANNEL_SECRET
channel_secret = os.environ.get('LINE_CHANNEL_SECRET')
# GEMINI_APIKEY
gemini_api_key = os.environ.get('GEMINI_API_KEY')
# 日記作成のシステムプロンプト
system_prompt_diary = f"""
与えられた文章から日記を生成し、「original」「translation」の2項目で構成されるJSON形式で答えてください。
その際、以下の条件に沿って回答してください。
1. 与えられた文章に場面描写や心情、セリフを追加して、最大300文字以内の短い物語にしてください。登場人物のキャラクター設定は以下に従ってください。
    ・主体となる人物は多少皮肉っぽい、演劇的な言葉選びや言い回しにしてください
    ・その他の人物は以下の例の中からランダムに性格付けしてください
    例：・温和でお人よし、主体となる人物の皮肉っぽい言い回しをたしなめる
        ・陽気でそそっかしい、主体となる人物を振り回す
        ・謎めいて陰を感じさせ、含みのある物言いをする
        ・高圧的でプライドが高く、主体となる人物を下に見ている
        ・内向的で卑屈なところがあり口数が少ない
        ・華やかで気品があり、人の目を引く振る舞いをする
2. 次に、物語を英文にしてください。英文作成の際、探偵小説のような文体にしてください。
3. 上記2.の英文をさらに和訳してください。
4. JSON上、英文は「original」に設定してください。日本語訳は「translation」に設定してください。
"""

def main(request):
    # LINEBOTの設定
    line_bot_api = LineBotApi(channel_access_token)
    parser = WebhookParser(channel_secret)

    # シグネチャの確認
    body = request.get_data(as_text=True)
    hash = hmac.new(channel_secret.encode('utf-8'),
        body.encode('utf-8'), hashlib.sha256).digest()
    signature = base64.b64encode(hash).decode()

    if signature != request.headers['X_LINE_SIGNATURE']:
        return abort(405)

    try:
        events = parser.parse(body, signature)
    except InvalidSignatureError:
        return abort(405)

    for event in events:
        if isinstance(event, MessageEvent):
            # メッセージを受信した場合
            if isinstance(event.message, TextMessage):
                # テキストメッセージなら返信作成
                reply_data = []
                # Gemini AIモデルを生成
                genai.configure(api_key=gemini_api_key)
                # チャットの応答を生成
                model_for_diary = genai.GenerativeModel(
                    "gemini-1.5-flash",
                    system_instruction=system_prompt_diary,
                )
                response = model_for_diary.generate_content(
                    event.message.text,
                    generation_config={"response_mime_type": "application/json"})
                data = json.loads(response.text)
                reply_data.append(TextSendMessage(text=data['original']))
                reply_data.append(TextSendMessage(text=data['translation']))

                # 応答内容をLINEで送信
                line_bot_api.reply_message(
                        event.reply_token,
                        reply_data
                    ) 
            else:
                continue

    return jsonify({ 'message': 'ok'})
```
</details>

<details><summary>requirements.txt</summary>

```
functions-framework==3.*
line-bot-sdk
google-generativeai
```
</details>

### AIに渡せる設定が増えた
GeminiのAPI呼び出しで気づいたのですが、システムプロンプトが設定できるようになっていました。

```Python
                # Gemini AIモデルを生成
                genai.configure(api_key=gemini_api_key)
                # チャットの応答を生成
                model_for_diary = genai.GenerativeModel(
                    "gemini-1.5-flash",
                    system_instruction=system_prompt_diary,
                )
                response = model_for_diary.generate_content(
                    event.message.text,
                    generation_config={"response_mime_type": "application/json"})
                data = json.loads(response.text)
                reply_data.append(TextSendMessage(text=data['original']))
                reply_data.append(TextSendMessage(text=data['translation']))
```

私が気づかなかっただけかもしれませんが、前回作った時はrole:userでプロンプト内容、role:modelで「わかりました」みたいな応答を履歴に持たせていたのでいまいちすっきりしない感じがありました。
また、応答でJSON形式を指定できるようになったことで、「」とか『』とかの記号を指定して応答文の中から切り抜かなくても良くなりました。

### 日記を物語風にしてもらうために
こうして渡せるようになったシステムプロンプトには、以下の内容を指定しました。

```Python
# 日記作成のシステムプロンプト
system_prompt_diary = f"""
与えられた文章から日記を生成し、「original」「translation」の2項目で構成されるJSON形式で答えてください。
その際、以下の条件に沿って回答してください。
1. 与えられた文章に場面描写や心情、セリフを追加して、最大300文字以内の短い物語にしてください。登場人物のキャラクター設定は以下に従ってください。
    ・主体となる人物は多少皮肉っぽい、演劇的な言葉選びや言い回しにしてください
    ・その他の人物は以下の例の中からランダムに性格付けしてください
    例：・温和でお人よし、主体となる人物の皮肉っぽい言い回しをたしなめる
        ・陽気でそそっかしい、主体となる人物を振り回す
        ・謎めいて陰を感じさせ、含みのある物言いをする
        ・高圧的でプライドが高く、主体となる人物を下に見ている
        ・内向的で卑屈なところがあり口数が少ない
        ・華やかで気品があり、人の目を引く振る舞いをする
2. 次に、物語を英文にしてください。英文作成の際、探偵小説のような文体にしてください。
3. 上記2.の英文をさらに和訳してください。
4. JSON上、英文は「original」に設定してください。日本語訳は「translation」に設定してください。
"""
```

response_mime_typeで指定した応答形式をわざわざこちらでも指定しているのは、最初システムプロンプトだけで指定したらjson.loads(response.text)でエラーになったからです。
どうもプロンプトだけで指示するとマークダウン形式のコードブロックをつけてしまうようでした。
それでresponse_mime_typeを指定したら、今度はJSON形式にはなったものの頭に「diary」という大項目をつけてその下にこちらで指定した項目を格納して返される。
固定でその形式になるのなら問題はないのですが、大項目が別の名前になったりするとコード側でエラー起こしそうだったので項目指定をプロンプト側につけました。
今のところはこれでうまくいってます。


ポワロ風と指定したのは私の趣味です。
最近はご無沙汰ですが一時ちょこちょこ読んでました。


:::note warn
2024/11/24追記：
既存の著作物を具体的にさして「○○風」とするのが著作権的に危ういらしい、というお話を見かけたので修正しました。
個人的な用途なら気にしなくてもいいのかもですが、念のため。
:::

あと英語をつけてもらったのは最近めちゃくちゃ英語読んでないので、ちょっとは慣れようというささやかな向上心です。

Cloud Runのインラインエディタは保存と同時にデプロイしてくれるので、デプロイ待ちの間にLINE Messaging API側にWebhookの設定をして、デプロイ完了後に検証OKを確認してできあがりです。


## 試しに動かしてみる
さっそく今日の出来事を簡単に書いて送ってみました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3477580/0bc2d449-2955-07ac-38d2-4634703c6d6d.jpeg" width="70%">

ポワロ風かどうかはともかく、物語としてはなかなかそれっぽく仕上がってる感じがします。
あと英文と和訳で突き合わせると「ここの部分こんな短い文章になる？」みたいなとこが出てきてちょっと楽しいので、両方つけといたのは正解だったかもしれません。

訳のポイントとか熟語とか、なんかもうちょい機能つけても楽しそうな気がしてきました。
気が向いたらまたいじります。

## 余談：日本語→英語→日本語の手間
プロンプトは

1. 日本語で物語構成
2. 英訳
3. 英訳を再和訳
   
という手順を踏んでいます。

この方が翻訳小説っぽい文体になるかな、という思い付きでやってみましたが、「1と2で終わってもいいのでは？」という気持ちもなくはないので、一度Google AI Studioで試してみました。

どちらも元の文章は以下です。

```
今日は自宅で仕事をした。
システムテストはおおむね順調に進んでいる。
それでもいくつか単体テストで見つけられなかった不具合が出てきて頭を抱えてしまった。
```

### 1.と2.だけのプロンプト
まずは物語構成→英訳だけのプロンプトで試してみました。

```プロンプト
与えられた文章から日記を生成し、「original」「translation」の2項目で構成されるJSON形式で答えてください。
その際、以下の条件に沿って回答してください。
1. 与えられた文章に場面描写や心情、セリフを追加して、最大500文字以内の短い物語にしてください。
2. セリフはアガサ・クリスティーの「名探偵ポワロ」の登場人物のような言い回しにしてください。
   例：'私の名前はエルキュール・ポワロ、おそらく世界で最も偉大な探偵です。'
       '人間が頼るべきは脳、小さな灰色の脳細胞だ。外ではなく、内に真実を求めなければならない。'
       'ウィ、お嬢さん。しかし、エドウェア夫人を嫌っていたのは、ムシュウ・マルタンだけではなかったのです。キャロルさん、私はリージェント・ゲートのホールに入っていった女性の身元をあなたにお尋ねしましたね...'
       'そうです。なぜあなたはマダム・ケタリングのコンパートメントに立ち入ったのですか？'
       '私のような人間の頭脳にとって三週間の静寂は永遠に等しい。絶え間ない刺激がなければ、小さな灰色の脳細胞は飢えて死んでしまう。'
       'どんな話題であれ人に長く話をするように仕向けることができれば自分の正体を明かしてしまうに違いないのだ！遅かれ早かれ私を欺くことを諦めるだろう。'
       '口ひげを生やすのは芸術だ！私はその苦難に挑むすべての人に同情する。'
       'ああ、かわいそうなヘイスティングス。しかし思い悩んではならないぞ！きみは自分のことで精一杯だろう？'
       '親愛なるヘイスティングス、これはちょっとしたプレゼントだ。'
       '背の高い本は一番上の段に、次に背の高い本はその下の段に、という具合だ。こうして秩序は保たれる。'
3. 次に、物語を英文にしてください。
4. JSON上、英文は「original」に設定してください。日本語訳は「translation」に設定してください。
```
セリフ部分は元のプロンプトで渡している英語例文をDeepLの力を借りて翻訳したものなので、あんまり元の小説の文脈と合ってない可能性はありますが仕方ない。
で、実行結果がこちら。


```
{
  "original": "The day was spent in the confines of my home office, a symphony of clicking keys and the soft hum of the computer. System testing, as they say, was proceeding smoothly. Yet, there were a few hiccups, anomalies that slipped through the net of unit tests, leaving me with a furrowed brow and a growing sense of unease. 'It is a curious thing, this software,' I muttered, 'like a human mind, it can be so logical and yet still prone to unpredictable errors.'  ",
  "translation": "その日は自宅のオフィスに閉じこもって仕事をした。キーボードを叩く音とコンピューターの静かな唸りが奏でるシンフォニーだ。システムテストは、言うまでもなく順調に進んでいた。しかし、いくつか問題が発生した。単体テストでは見つけられなかった異常だ。眉間にしわを寄せ、不安感が募っていく。  \"ソフトウェアというのは不思議なもので、人間の心のように、論理的なのに、予測不可能なエラーを起こすこともある。\" と、私はつぶやいた。"
}
```


### 1.～3.のプロンプト
次に、物語構成→英訳→再和訳のプロンプト。
BOTで使ったのと同内容なので割愛します。

実行結果はこちら。

```
{
  "original": "The morning sun streamed through my window, illuminating the dust motes dancing in the air. A perfect day for a quiet morning at home, I thought, settling into my chair with a steaming cup of coffee. My fingers danced across the keyboard, a rhythm of clicks and clacks accompanying the steady progress of my work. Today was a day for system tests, and for the most part, they were running smoothly. Yet, as the day wore on, a few glitches emerged, small but persistent bugs that had eluded the scrutiny of unit testing. 'Oh, mon Dieu!' I exclaimed, throwing my hands up in exasperation. 'Why must these little gremlins always choose to reveal themselves at the most inconvenient times?'  My brow furrowed, and I stared at the screen, a symphony of code and error messages mocking my efforts.  The air in the room grew thick with the scent of frustration, and my little gray cells began to tire. I knew I had to push through, to find the root cause of these errors, but the weight of them pressed heavily on my shoulders.  But just as I was about to succumb to the growing despair, a glimmer of hope emerged. A single line of code, a misplaced semicolon, a forgotten comma – a simple oversight, yet a crucial one. With a sigh of relief, I made the necessary adjustments, and my code sprang back to life. The sun was setting by the time I finally shut down my computer, a sense of quiet satisfaction filling me.  'Well, Hastings,' I said to myself,  'It seems we have conquered another day.'  ",
  "translation": "朝の太陽が窓から差し込み、空気中を舞う塵埃を照らしていた。静かな朝の自宅での仕事に最適な日だと思った私は、温かいコーヒーを片手に椅子に腰掛けた。指がキーボードの上を踊り、クリックとカチャカチャというリズムが、着実に進む仕事の伴奏になった。今日はシステムテストの日で、ほとんどは順調に進んでいた。しかし、日が経つにつれて、単体テストでは見つけられなかった小さなバグがいくつか見つかり始めた。「ああ、なんてこった！」と私は両手を上げてため息をついた。「なぜこれらの小さな小悪魔たちは、いつも最も都合の悪い時に姿を現すのだろうか？」と。眉をひそめ、画面を見つめた。コードとエラーメッセージの交響曲が私の努力を嘲笑している。部屋の空気はフラストレーションの匂いでいっぱいになり、私の小さな灰色の細胞は疲れ始めた。私はこれらのエラーの根本原因を見つけなければいけないことを知っていたが、その重圧が私の肩に重くのしかかっていた。しかし、絶望に屈しようとしたまさにその時、一筋の希望の光が差し込んだ。たった1行のコード、セミコロンの置き忘れ、コンマの忘れ - 単純な見落としだが、重要なものだった。安堵のため息をつき、必要な修正を加えると、コードが再び動き出した。コンピュータをシャットダウンしたのは、日が沈みかけていた頃だった。静かな満足感が私を満たしていた。「まあ、ヘイスティングス」と私は自分に言い聞かせた。「今日はまた乗り越えられたようだ。」"
}
```

英訳の文量からしてめちゃくちゃ増えました。
指示が日本語とはいえ、Geminiは英訳段階でアレンジを入れる方が得意なのかもしれません。

## 余談：作品判別が危うい
例文に「ヘイスティングス」の名前が入っているのにお気づきでしょうか。
最初この文章抜きでやってみたところ、友への呼びかけが「親愛なるワトソン」になるケースが散見されました。
各分野の有識者にお叱りを受けそうです。