初めてのGitHub公開~AItuberをつくる~

## はじめに
当記事は[@sald_ra](https://qiita.com/sald_ra)さんの著書、
『AITuberを作ってみたら生成AIプログラミングがよくわかった件』(以下、本書)より、[GitHub](https://github.com/sr2mg/aituber_python_programing_example/blob/main/.python-version)に公開されているソースコードをベースに作成しています。

https://bookplus.nikkei.com/atcl/catalog/23/10/31/01079/

また、当記事で記載しているコード等は私のGitHubで公開しているので
詳細も確認されたい方はあわせてご覧いただくことをおすすめします。

https://github.com/t4ned4/create-aituber

## 製作のきっかけ
AItuber…なにやら聞き馴染みがない言葉でした。
AIを利用してユーザーとインタラクティブな会話を実現するYouTuber。
なんと近未来的で興味深いテーマなんだろうと思いました。
そうして職場でたまたま手に取った日経コンピュータで紹介されていた本書に
私は一目惚れして購入しました。半年以上も前のことです。
当時はPythonの勉強を始めたてで、四則演算ができる程度の知識しかありませんでした。
本書は入門書の類ではないため、基本的な構文の説明はあまりなかったので
購入した後はしばらくわからないなりに読み物として楽しんでいました。

Pythonの学習を進める中でせっかくなら手近な成果物を1つ生み出したいと考えるようになり、
そんな中で、AIを利用したサービスのプロトタイプを思いついたので
どうせなら勉強とその成果の確認がてら、本書の内容をアウトプットしたいと考えました。

実は読み物として進めていく中で、VSCODEの使い方も勉強中だったので
操作確認ついでに本書の内容だけはある程度写経したデータが既にありました。

ところが月日が流れる中でOpenAI APIのメジャーバージョンアップに伴い仕様が変わってしまいました。
本書ではバージョンを指定して環境を作ることを前提としていましたが、
写経していた当時の私は「仮想環境ってなに…?」というレベルだったため
随分と適当な環境構築を行っていました。

そこで、どうせならOpenAI APIのメジャーバージョンに対応した形で
AItuberをつくる軌跡を最初のアウトプットにしようと考えたのが製作のきっかけです。

## 環境
ざっくりと開発時の環境をまとめます。

* Python3.12.4
* openai1.43.0

※上記以外での動作確認は実施していない点をご留意ください。

## 前提
ここからいよいよ本題に入っていきますが、
Python環境の構築やエディタの準備など…
このあたりは**前提**として説明は割愛して進めます。
したがって、本題の内容とは「どのようなコードを書いたのか」
「そのコードにはどのような意味があるのか」
「その中でどのような苦労があり、何を学べたのか」
というような体験のお話が主になることもあわせてご留意ください。
また、私のGitHub上ではAItuberとして実際に動かせるよう当記事で記載しているコード以外の全プログラムを公開していますが、内容的に本書と差があまりない部分については当記事の中での説明は割愛します。

## 作り方
環境の構築は割愛すると書きましたが
せっかくなので自戒も兼ねて仮想環境の作成をするところからお話します。

基礎中の基礎ですが、ライブラリのバージョンアップなどによる動作不良やコード間の干渉を防ぐために、特定のバージョンのままでプロジェクトごとに開発を行える仮想環境を作成します。
```bash
# プロジェクトのルートディレクトリにて
python -m venv .venv
```
上記を実行することで`.venv`というフォルダが作成されます。
続いて`.venv`の中のファイルを起動します。
```bash
.venv/Scripts/activate.bat
```
これで仮想環境のアクティベートが完了します。
あとは通常どおり`pip install hogehoge`で
必要なライブラリをインストールするだけです。

それでは早速作っていきましょう。
今回つくるシステムはざっくり以下の流れになります。

1. `pytchat`でYouTubeLive上のコメントを取得
2. `openai`でコメントに対する応答を作成
3. VOICEVOXによる音声合成
4. `obsws-python`によるテキストの表示変更
5. 3で作成した音声の再生
6. 上記のプログラムを組み合わせて継続的に実行

上記の内、今回は本書の内容と異なる、またはテンプレート的な内容の記述を避けるため1,2の説明のみを行います。

#### 1. `pytchat`でYouTubeLive上のコメントを取得
`pip install pytchat`で`pytchat`をインストールした後、
以下のようなコードを記述します。
```connect_comments.py
import pytchat

import os


class ConnectComments:
    def __init__(self, video_id) -> None:
        self.chat = pytchat.create(video_id=video_id, interruptable=False)

    def get_comment(self):
        # コメントの一覧を取得
        comments = self.__get_comments()
        if comments is None:
            return None
        # 新着コメントのテキストのみを抽出
        message = comments[-1]
        print(message.message)
        return message.message

    def __get_comments(self):
        # 配信が開始されているかを確認
        if self.chat.is_alive() is False:
            print('Streaming is not started')
            return None
        # 配信が開始されている場合はコメントを取得できるまで待機
        while self.chat.is_alive():
            print("Loading comments...")
            comments = list(self.chat.get().sync_items())
            if comments == []:
                print('Failed to load comments')
            return comments


if __name__ == '__main__':
    import time

    video_id = os.getenv('VIDEO_ID')
    chat = ConnectComments(video_id)
    print(chat.get_comment())
```
本書ではjson形式でコメント情報を取得するコードが書かれていたのですが、
私の環境ではうまくいかなかったため、動悸したコメントのメッセージ部分のみを直接抽出して対応しました。
jsonで情報を取得しておけばプロンプトにユーザー情報や会話内容を残すといった"人間らしい"AIにしていくことができると思うのですが、
LLMではなくAPIを使う以上、プロンプトに使用するトークン量が膨大になります。
それに伴い寂しくなるお財布を想像すると涙を禁じ得なかったので、
後述しますが今回はコメントを記憶しないAItuberのテンプレートになっています。

#### 2. `openai`でコメントに対する応答を作成
続いて、上記で取得したコメントをOpenAI APIに渡して応答を生成するプログラムです。
`pip install openai`にて`openai`をインストールしてください。
以下、コードを記載します。

```connect_openai.py
from openai import OpenAI

client = OpenAI()

# prompt_example.txtに記述したプロンプトを読み取る
with open("test\\prompt_example.txt", "r", encoding="utf-8") as f:
    prompt = f.read()


class ConnectOpenAI:
    def __init__(self):
        # 会話のログを記憶するため、assistantを採用
        self.assistant = client.beta.assistants.create(
          name="Example_AItuber",  # 自身を認識する名前
          instructions=prompt,  # 指定するプロンプト
          model="gpt-4o-mini",  # 使用する言語モデル
        )
        pass

    def create_thread(self):
        # アシスタントを利用するためのスレッド作成
        self.thread = client.beta.threads.create()
        return self.thread

    def create_chat(self, thread, question):
        print("Waiting for API response... ")
        print(self.thread.id)
        if not question:
            question = question
        messages = client.beta.threads.messages.create(
          thread_id=self.thread.id,
          role="user",
          content=question  # ユーザーからのコメント内容が入る
        )

        run = client.beta.threads.runs.create_and_poll(
          model="gpt-4o-mini",
          thread_id=self.thread.id,
          assistant_id=self.assistant.id,
        )
        if run.status == 'completed':
            messages = list(client.beta.threads.messages.list(
                thread_id=self.thread.id
                )
            )
            # プロンプトに使用されたトークン数を表示
            print(f"prompt tokens: {run.usage.prompt_tokens}")
            # 応答の生成のために使用されたトークン数を表示
            print(f"completion tokens: {run.usage.completion_tokens}")
            # 生成されたテキストを表示
            print(f"{messages[0].content[0].text.value}")
            question = None
            return messages[0].content[0].text.value

        # ステータスがincompletedだった場合のごく簡易的な処理
        else:
            print(run.status)
            print(run.incomplete_details)
            client.beta.threads.delete(self.thread.id)
            thread = self.create_thread()
            self.create_chat(thread, question)


if __name__ == '__main__':
    print(prompt)
    adapter = ConnectOpenAI()
    thread = adapter.create_thread()
    response_text = adapter.create_chat(thread, "please introduce yourself")
```
余談ですが、今回の開発では一番ここに労力を費やしました。
なにせメジャーバージョンアップに伴い、応答生成のフォーマットが一新していたため、APIリファレンスを読みながらああでもないこうでもないとテストコードの実行をして形にするという初めての試みだったからです。
逆に、自分の力で調べて作って試して…という過程を得たからこそ力が身についたなという気もしているので楽しかったです。

ところでステータスが`incompleted`時の処理などに名残があるのですが、
私の中で実験段階だったためここでは省略していることがあります。
実は`run`の中で`max_completion_tokens`や`max_prompt_tokens`を指定することにより使用可能なトークン数の上限を設けることができるようになっています。
これを利用してプロンプトの上書きを処理の間に挟むことによって一時的に記憶を維持させながら応答をさせることができるようになりますが、先述の通りトークン数がかさむとお財布が寂しくなるのと、どの程度までの記憶をどのように残すかというところに検討の余地があるため、興味がある方はご自身で色々試してみてください。

ちなみに、記憶なんて維持しなくていいからAPIを通じて一定のプロンプトのもとでAIが生成した応答を取得したい場合には以下のように記述することでコードがかなりスマートになります。

```openai_chat_example.py
from openai import OpenAI

client = OpenAI()

# ptompt.txtからプロンプトを取得
with open("statics\\prompt.txt", "r", encoding="utf-8") as f:
    prompt = f.read()


class CreateTextOpenAI:
    def __init__(self, content):
        self.model = "gpt-4o-mini"  # 使用するモデルを選択
        self.messages = [
            {
              "role": "system",
              "content": prompt
            },
            {
              "role": "user",
              "content": content　# ユーザーからの質問内容
            }
        ]
        pass

    def create_text(self):
        print("Waiting a response from API...\n")
        chat = client.beta.chat.completions.parse(model=self.model,
                                                  messages=self.messages)
        # 生成された応答のテキスト部分のみを抽出
        text = chat.choices[0].message.content
        # ここでは使用されたトークンの合計数のみ表示
        print(f"【total tokens: {chat.usage.total_tokens}】")
        print(f"{text}\n")
        return text


if __name__ == '__main__':
    content = "こんにちは"
    response = CreateTextOpenAI(content)
    response.create_text()

```
いかがでしょうか。
後の拡張性を視野にいれるなら`assistant`を利用するほうが使い勝手はいいですが、
ChatGPT的な楽しみ方をするのみであれば`chat`で十分ですし、全体として2分の1以下に記述量を抑えることができるのは大きな魅力だと感じます。
(`assistant`を使っても短くする方法はあると思いますが…)

## いざGitHubへ！
なんやかんや残りのプログラムをせかせかと作成しまして、
いよいよGitおよびGitHubを使ってみることに。

実務未経験の身としては個人開発の範囲でGitHubを使うイメージがあまり湧かなかったのですが、
やらないよりはやったほうがいいということでチャレンジしました。

インストール時の流れやサインアップなど細かな流れは省略しますが意外とやり方自体は簡単でした。
私はVSCODEを使っているので[Git](https://git-scm.com/book/ja/v2/%E4%BD%BF%E3%81%84%E5%A7%8B%E3%82%81%E3%82%8B-Git%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB)をインストールした後、VSCODEでGitと連携しコマンドをポチポチと実行していくだけでした。

とはいえ始めたとき地味に詰まった初期設定の部分から書いていこうと思います。
まずは下記でgit初期設定を済ませます。
``` bash
git config --global user.name "名前"
git config --global user.email "メールアドレス"
```

VSCODEの場合はGUIでここからの操作は可能ですが、
私はターミナルから行っているのでプロジェクトのルートディレクトリで下記を実行します。
```bash
git init
```
これによりソースコードなどの倉庫となる**リポジトリ**を生成(初期化)できます。
この初期化したリポジトリに丹精込めて作ったファイル達を詰め込む作業があるのですが、その前にもしも倉庫じゃなくて手元にだけ置いておきたいファイルや上げる必要のないファイル(パスワードが記載されているファイルや環境構築のためのファイル)がある場合には`.gitignore`というファイルをプロジェクトのルートディレクトリに作成し
```.gitignore
.venv/
__pycache__/
prompt.txt
password.json
```
上記のように記述しておくことで倉庫に入れないファイルやフォルダを指定することができます。
上記の準備が整ったら下記を実行します。
```bash
git add .
```
上記で`.gitignore`に記載されている以外のすべてのファイルをリポジトリに追加することができます。
ちなみに`git add "filename"`という形でファイルを個別に追加することも可能です。

リポジトリへの追加が完了したら次は履歴として倉庫の中身を1つのバージョンとして保存します。コミットというやつですね。
コミットは下記のように行います。
```bash
git commit -m "commit message"
```
`-m`を引数にとることでコミットするバージョンがどういうものなのかを`"commit message"`という形で残すことができます。
渡しの場合は初回のコミットは`"initial commit"`としました。

これで、ひとまずGitに作成したファイルを格納することができました。
複数人で開発を行う場合の進捗管理や、ネット上で公開するためにはここからGitHubへリポジトリを送る必要があります。
この、自身が持つローカルリポジトリからGitHub上のリモートリポジトリへデータを送信する操作をプッシュといいます。
まずは下記のようにリモートリポジトリへ接続し、ファイルの送り先を明確にします。
```bash
git remote add origin https://github.com/username/repository-name
```
続いて、以下のように`main`ブランチへリモートリポジトリに初回のプッシュを行います。
```bash
git push -u origin main
```
上記の後にGitHubを確認して、`main`ブランチにファイルが上がっていれば成功です。

ここで少し込み入った話なのですが、ブランチというのは文字通り「枝」を表しており、作業の分岐を示します。
たとえば、機能を追加する`feature/`ブランチやバグ修正を行う`bugfix`ブランチがあります。
これらを最終的に`main`に結合することで完全体となるイメージですね、ざっくりですが。
今回私はGitHub Flowリスペクトでリポジトリを運用したかったので
以下のように`main`、`bugfix`の2つのブランチでリポジトリの更新を行いました。
```bash
git branch bugfix
```
念の為`git branch`でブランチの作成が成功しているかと、現在接続しているブランチがどれかも確認するとよいでしょう。
ちなみに、上記の例だと`git checkout -b bugfix`とすることで`bugfix`ブランチを作成すると同時に`main`から`bugfix`に接続を切り替えることもできます。
この後は同様の流れで修正を行ったコードなどを`bugfix`にプッシュし、問題がなければ`main`に結合、つまりマージします。
このマージは以下のように行います。
```bash
git checkout main
git merge bugfix
```
`main`にブランチを切り替えた後、`bugfix`の中身を`main`に結合するイメージです。
ざっくりでしたが、これにて作成したプログラムをGitHubに上げることができました。
本来はコンフリクトの修正やプルリクエストの提出も行うと思いますが、今回は個人開発の範疇なので割愛します。
まだまだ基礎中の基礎ですが、仕事としてプログラミングを行うようになったらこの辺りも自分なりにまたまとめたいです。

## 総括
ということで、以上が私の初めてGitHubへリポジトリを公開するまでの流れとその経緯でした。
まだまだ語り尽くせていない苦労もありますが、ひとまずここまでの軌跡を残せた達成感をばねに、今後もたくさん勉強しつつ開発を楽しんでいきたいと思います。

また、当記事では説明を割愛した「`obsws-python`によるテキストの表示変更」については、別の記事で今回公開したリポジトリよりもう少し踏み込んだ内容を記載しています。
よろしければ以下の記事もあわせてみていただければ嬉しいです。

https://qiita.com/t4ned4/items/083a231062e8aac5fda8