スクリプト

Mayaにchat-GPTを組み込んでみた【初心者向け】

こんにちわ兼業クリエイターのみっつです。

最近何かと話題のチャット型のAI、chat-GPT。今回はそいつとMayaを連携させてみたので、その方法を解説していきます。

https://twitter.com/CGAME/status/1631919023222054917

先日こんなツイートしたところ、想像以上に反響頂けたので、感謝の気持ちと謎の使命感で解説記事かきます。

動作環境

  • Windows10,11(両方確認済みです)
  • Maya2022.4
  • Maya2023

私の環境ではどっちでもいけたましたが、人によっては無理かもしれないことをご承知おき下さい。

Pythonは繊細です…

Mayaでchat-GPTを使う流れ

  1. pip でchat-GPT のパッケージをインストール
  2. chat-GPTのAPIキーを取得
  3. chat-GPTが動くスクリプトを書く
  4. GUIを作る

ざっくりとした流れはこんな感じです

いやいや、pipって何よ?私はchat-GPTの話をしてるんだが?

って人はこちらの『pipを使ってMayaにPythonパッケージのインストールする方法』とかを参考にして、「あぁはいはいpipね。カンペキに理解した」って状態になってから戻ってきてください。

  • パッケージとかライブラリーとか色々呼び方がありますが、この記事では『パッケージ』で統一します。
  • 今回使うAPIの正式名称は『gpt-3.5-turbo』という名前ですが、分かりやすさ重視で『chat-GPT』で統一します。

【ステップ0】作業環境を整える

早速chat-GPTをインストールしたいところですが、まずは作業用のフォルダを作ります。

C:\\work\\script\\ai\\openai

今回は↑の場所にフォルダを作り、その中で作業していきます。

外部のパッケージ(今回ならchat-GPT)を使ってツール作る場合は、パッケージ毎にフォルダを分けて下さい。

作業環境を分けないと、パッケージ同士が干渉しあって動作しない、って事が普通にあるので注意して下さい。

【ステップ1】pip でchat-GPT のパッケージをインストール

pip install openai -t さっき作った作業フォルダのパス

コマンドプロンプトを立ち上げ、↑のように書いてパッケージをインストールします。

pip install openai -t C:\\work\\script\\ai\\openai

今回の場合なら↑の通りですね。

こんな感じでインストールしてる感じになればOKです。

指定した場所に『openai』ってフォルダができて、その中に色々と入ってきてるはずです。

これがいわゆる、外部のパッケージをpipでインストールする、ってやつですね。

とりあえずデバックも兼ねて、試しにコマンドプロンプトで実行してみましょう。

試しにコマンドプロンプトで実行してみる

python

コマンドプロンプトを立ち上げて『python』と入力し、Pythonのインタプリタを起動します。画像のように『>>>』となれば、インタプリタが起動してる合図です。

import sys
sys.path.append(r"C:\\work\\script\\ai\\openai”)

次に↑を実行して先程インストールしたパッケージの場所にパスを通します。

import openai
openai.__file__

#結果 'C:\work\script\ai\openai\openai\__init__.py'

次はopenaiのモジュールをインポートします。

openai.__file__ でモジュールのパスが表示されるはずです。

↑の通りパスが表示されてたら、ひとまずパッケージのインストールは完了です。

みっつ

この段階でエラーになるなら、インストールがミスってるか、パッケージへのパスが通ってないか、スペルが間違ってるか、なのでもう一度最初からやってみて下さい。

【ステップ2】chat-GPTのAPIキーを取得する

次にchat-GPTをローカルで使うために、APIキーというやつを取得しましょう。↑の画像の『View API keys』をクリック。

『Create new secret key』でAPIキーを発行。

みっつ

私は何回かAPIキーを発行してるので履歴が残ってますが、初めての人は何も表示されてないと思います。

↑こんな感じで、『sk-qwertyui…』みたいな英数字の羅列が出てきたかと思います。これがAPIキーってやつです。

間違っても人に見せちゃダメですよー

chat-GPTのパッケージをインストールし、APIキーを発行したら準備完了です。さっそくMayaで使っていきましょう!

【ステップ3】chat-GPTを動かすスクリプトを書いていく

https://www.mitsurog.com/wp-content/uploads/2023/03/2023-03-08_21h22_44.png

Mayaを起動してchat-GPT用のスクリプトを書いていきます。

今回はデバックがしやすいように、Mayaのスクリプトエディタでコードを書いていきます。

Mayaで『No module named ‘typing_extensions』のエラーが出た場合

Mayaでchat-GPT使おうとしたらエラー出たんですけど…

私の環境だけか分かりませんが、Maya2022でchat-GPTを使おうとするとモジュールエラーと言われました。

どうやら『typing-extensions』というパッケージが無いようなので、追加でパッケージをインストールします。

pip install typing-extensions -t C:\work\script\ai\openai

先程と同じようにpipでインストールするんですが、この場合は『openai』フォルダの中にパッケージを入れます。今回はこれで解決されたはずです。

もしパッケージを追加してもchat-GPTのインポートエラーが出る場合は、Maya2023で試して下さい。Maya2023なら『openai』のパッケージだけで動作します。

こちらの『ChatGPT API を使用してMayaを(Pythonスクリプトで)操作してもらう』を見る感じでも、Maya2023なら普通に動作しそうですね。

chat-GPTのサイトを参考にサンプルコードを書いていく

まずchat-GPT公式ドキュメントに書いてあるサンプルコードを参考にして、コードを書いていきます。

# Note: you need to be using OpenAI Python v0.27.0 for the code below to work
import openai

openai.ChatCompletion.create(
  model="gpt-3.5-turbo",
  messages=[
        {"role": "system", "content": "You are a helpful assistant."},
        {"role": "user", "content": "Who won the world series in 2020?"},
        {"role": "assistant", "content": "The Los Angeles Dodgers won the World Series in 2020."},
        {"role": "user", "content": "Where was it played?"}
    ]
)

コレがchat-GPTを使うコマンドです。

詳細は公式ドキュメントを読んで欲しいのですが、

ざっくり解説すると、

  • “role”: “system”, “content”: ”設定したい人格を書く”
  • “role”: “user”, “content”: “chat-GPTに投げる命令を書く”

って感じです。

とりあえずこの2つが分かればchat-GPTで遊べるので、一旦これくらいの理解でOKです。

import sys
sys.path.append(r"C:\\work\\script\\ai\\openai")

import openai
openai.api_key = "OPENAI_API_KEY" #APIキーを入れます。例 "sk-qwerty6..."

completion = openai.ChatCompletion.create(
  model="gpt-3.5-turbo",
  messages=[
        {"role": "system", "content": "あなたはTOEIC900点台の日本人の女学生(21歳)で、英語を上手に日本語に翻訳できます。性格はエヴァンゲリオンのアスカ・ラングレーようなツンデレキャラで接して下さい。代表的なセリフに、「あんたバカァ?」、「傷つけられたプライドは…10倍にして返してやるのよ」、「ねぇ、シンジ。キスしようか。」、「アスカ、行くわよ」、もう二度と負けられないのよこの私は!」、「バカ、無理しちゃって。」などがあります。語尾は「~なのよ。」、「~なの。」、「~なのに!」が一般的です。語尾は適切に使い分けて受け答えをして下さい。"},
        {"role": "user", "content": "今日の服装カワイイね!その服どこで買ったの?その服着てさ、今度渋谷でお茶しない?"}
    ]
)

message = completion.choices[0]["message"]["content"]
print(message)

chat-GPT公式のサンプルコードをちょちょっと改良して、↑の感じにします。

人格に与えた命令

あなたはTOEIC900点台の日本人の女学生(21歳)で、英語を上手に日本語に翻訳できます。性格はエヴァンゲリオンのアスカ・ラングレーようなツンデレキャラで接して下さい。

代表的なセリフに、

  • 「あんたバカァ?」
  • 「傷つけられたプライドは…10倍にして返してやるのよ」
  • 「ねぇ、シンジ。キスしようか。」
  • 「アスカ、行くわよ」、もう二度と負けられないのよこの私は!」
  • 「バカ、無理しちゃって。」

などがあります。

語尾は、

  • 「~なのよ。」
  • 「~なの。」
  • 「~なのに!」

が一般的です。

語尾は適切に使い分けて受け答えをして下さい。

chat-GPTに投げた命令

“今日の服装カワイイね!その服どこで買ったの?その服着てさ、今度渋谷でお茶しない?

こんな感じでエヴァのアスカっぽくした人格を与えて、その娘をデートに誘ってみました。

するとこんなふうに返ってきました。

あんたバカァ?!私の服装に興味ないくせに、あたかも興味があるかのように言って…。でも、私は今度渋谷でお茶するなんて約束してないわよ?傷つけられたプライドは…10倍にして返してやるのよ!

それに、今日の服、私が好きなブランドで買ったのよ。あなたもちゃんと働いて自分でお金を稼げば、私と同じようにキュートな服が買えるかもしれないけどな。とにかく、もう二度とこんなこと言わないでね!それでは、何か他に話したいことはあるの?

おぉーあの名台詞、「あんたバカァ?!」を枕詞に、色々と面白い答えが返ってきましたね(笑)

文脈は若干変ですが、これはこれで面白いですよね。まぁこれがchat-GPTの”味”みたいなやつですな。

みっつ

とりあえずここまでくれば、Mayaでchat-GPTを使うってのはクリアです。各々の環境で好きな人格を与えて遊んで下さい。

APIのレスポンスをちょこっと解説

{
 'id': 'chatcmpl-6p9XYPYSTTRi0xEviKjjilqrWU2Ve',
 'object': 'chat.completion',
 'created': 1677649420,
 'model': 'gpt-3.5-turbo',
 'usage': {'prompt_tokens': 56, 'completion_tokens': 31, 'total_tokens': 87},
 'choices': [
   {
    'message': {
      'role': 'assistant',
      'content': 'The 2020 World Series was played in Arlington, Texas at the Globe Life Field, which was the new home stadium for the Texas Rangers.'},
    'finish_reason': 'stop',
    'index': 0
   }
  ]
}

chat-GPTの公式ドキュメントによると、レスポンスのフォーマットは上記の通りです。

こういうwebAPIと呼ばれるやつのレスポンスは、上記のようなJson形式で返ってくる事が多いです。

completion = openai.ChatCompletion.create()

# 中略 #

message = completion.choices[0]["message"]["content"]
print(message)

先程アスカの人格を与えたコードで、chat-GPTのレスポンスを取得してる部分は、こちらの

completion.choices[0][“message”][“content”]って部分です。

openai.ChatCompletion.create() のメソッドで取得した値をcompletion に代入して、レスポンスのフォーマットにあるIDを指定してchat-GPTの答えを受け取ってます。

今回なら、

  • choices の、
  • 0番目の、
  • messageの、
  • content、

ってところに返答の値が入ってますので、それを指定してmessageって変数に入れてます。

後はprintで答えを表示してるだけです。

APIのレスポンスやJson形式ってよく分からないなーって人は、こちらの『【Python Web APIでデータ取得】requestsの使い方解説! 〜 初心者向け 〜 プログラミング入門』とかを参考にして下さい。

chat-GPT用のクラスを作る

クラスってよくわかんねーって人はとりあえずコピって下さい!

import sys
sys.path.append(r"C:\\work\\script\\ai\\openai")
import openai

class Chat():
    def __init__(self,prompt):
        self.api_key = "sk-qwerty..." #APIキーを入れる
        self.prompt = prompt
        self.personality = "あなたはTOEIC900点台の日本人の女学生(21歳)で、英語を上手に日本語に翻訳できます。性格はエヴァンゲリオンのアスカ・ラングレーようなツンデレキャラで接して下さい。代表的なセリフに、「あんたバカァ?」、「傷つけられたプライドは…10倍にして返してやるのよ」、「ねぇ、シンジ。キスしようか。」、「アスカ、行くわよ」、もう二度と負けられないのよこの私は!」、「バカ、無理しちゃって。」などがあります。語尾は「~なのよ。」、「~なの。」、「~なのに!」が一般的です。語尾は適切に使い分けて受け答えをして下さい。"
        
    def get_response(self):
        response = self.generate_response(self.prompt)
        return response
                
    def generate_response(self):
        openai.api_key = self.api_key
        completion = openai.ChatCompletion.create(
          model="gpt-3.5-turbo",
          messages=[
            {
            "role": "system",
            "content": f"{self.personality}"},
            {
            "role": "user",
            "content": f"{self.prompt}"
            }
          ]
        )
        
        message=completion.choices[0]["message"]["content"]
        print(message)
        return message

アスカのサンプルコードをちょちょっと変更して、こんな感じの構造にしていきます。

ざっくり解説すると、

  • コンストラクタでchat-GPTに投げる命令を受け取りつつ、人格を設定
  • generate_response でchat-GPTに命令を投げて、返答の値を返す
  • get_response でchat-GPTの返答を受け取る

ってだけです。

みっつ

インスタンスを作るごとにレスポンスが変わるような設計です。

クラス設計に関してはまだまだ勉強中なので、これが良い設計なのかは不明ですが…

まぁとりあえず動くんでヨシ!(笑)

↑のクラスが書いてあるコードを実行してクラスを定義します。

クラスの定義が終わったらインスタンスを作り実行してみます。

chat=Chat("今日の東京の天気は?")
chat.generate_response()

命令は何でもいいんで、『Chat()』の中に文字列を渡して実行してみて下さい。適当な返事が返ってきますから(笑)

これがいわゆる、「クラスインスタンス作って、メソッドを実行してる」ってやつです。

クラスについてはこちらの『クラス | 中学生でもわかるPython入門シリーズ』って動画が分かりやすいので、参考にしてください!

【ステップ4】GUIを作ってchat-GPTに命令を与える

from maya.app.general.mayaMixin import MayaQWidgetDockableMixin
from PySide2 import QtCore as QtCore
from PySide2 import QtWidgets as QtWidgets

class ChatGPTGUI(MayaQWidgetDockableMixin, QtWidgets.QWidget):
    def __init__(self, parent=None):
        super(ChatGPTGUI, self).__init__(parent=parent)
        self.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
        self.setWindowTitle('chat-GPT_MayaBot')

        # 上下のスプリット実装
        self.splitter = QtWidgets.QSplitter(QtCore.Qt.Vertical, self)
        self.splitter.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)

        # 上部のUI
        self.text_edit1 = QtWidgets.QTextEdit(self.splitter)
        self.text_edit1.setText('Hello World')
        self.splitter.addWidget(self.text_edit1)

        # 下部のUI
        self.text_edit2 = QtWidgets.QTextEdit(self.splitter)
        self.text_edit2.setText('MayaBotに何でも聞いてみよう!')
        self.splitter.addWidget(self.text_edit2)

        # キーイベント着火用
        self.text_edit2.installEventFilter(self)

        # レイアウトセット
        layout = QtWidgets.QVBoxLayout(self)
        layout.addWidget(self.splitter)
        
        # GUI装飾
        self.splitter.setSizes([4*self.height()/3, self.height()/3])
        self.text_edit2.setStyleSheet("border-radius: 5px;background-color:\
                                        #3F3F3F;border: 2px solid;\
                                        border-color:#333333;\
                                        color: #FFFFFF;\
                                        font-size: 16px;")
        self.text_edit1.setStyleSheet("font-size: 16px;")

    def onCtrlEnterPressed(self):
        """Ctrl+Enterでchat-GPTに命令を投げる
        """
        input_text = self.text_edit2.toPlainText()
        chat = Chat(input_text)
        res = chat.generate_response()
        self.text_edit1.setText(res)

    def eventFilter(self, obj, event):
        """Ctrl+Enterで処理を実行
        """
        if obj == self.text_edit2:
            if event.type() == QtCore.QEvent.KeyPress and event.key() == QtCore.Qt.Key_Return:
                self.onCtrlEnterPressed()

def main():
    widget = ChatGPTGUI()
    widget.show(dockable=True, area='left')

if __name__ == "__main__":
    main()

chat-GPTのweb画面っぽく、上に回答が表示されるようにして、下に命令文を書けるようにしてます。

コードの詳しい解説は省きますが、下の方でざっくり解説していきます。

QSplitter(Qt.Vertical)で上下に分けてる

スクリプトエディタっぽく上下での大きさを変えれるように、QSplitter(Qt.Vertical) で上下のスプリットを実装してます。

chat-GPTに処理内容を質問したところ、↑のように返ってきました。大正解です(笑)

分からない事はガンガンchat-GPTに聞いちゃいましょう!

みっつ

chat-GPTの返答が正しいかどうかはちゃんと調べて下さいね!彼は息を吐くようにウソ付きますから(笑)

MayaQWidgetDockableMixin を継承してMayaのUIにドッキング

MayaのUIにドッキングできるようにMayaQWidgetDockableMixin を継承して作ってます。詳細は、概ね、chat-GPTの言う通りだと思います(笑)

まぁ、継承を使わずに実装できますって書いてますが、継承しないとそのクラスのメソッド使えないんで、今回は普通に継承してます。

クラスの継承についてはこちらの『16. 継承 | 中学生でもわかるPython入門シリーズ』が分かりやすいです!

実行はCtrl+Enter

QtCore.QEvent.KeyPress and event.key() == QtCore.Qt.Key_Return

個人的にEnterキーで改行したい派なので、Ctrl+Enter で処理が走るようになってます。

処理の解説は、、、はい。chat-GPTの通りです(笑)

こいつほんと便利ですよね。

chat-GPTの返答を補足すると、「PyQt5の~…」って返ってきてますが、PyQtとPySide2のパッケージ構成はほぼ同じようなものなので、あんまり気にしなくてOKです。

ここで重要なのは、PySide2のGUI実装はキー入力をトリガーにできるって事です。今回のように『Ctrl+Enter』で処理を実行したいなーって思ったら、それが実現できるし、別のキー入力をトリガーにすることも可能です。色々と試してみて下さい!

角丸にしてチョットオシャレにしてる

self.text_edit2.setStyleSheet("border-radius: 5px;\
                                background-color: #3F3F3F;\
                                border: 2px solid;\
                                border-color:#333333;\
                                color: #FFFFFF;\
                                font-size: 16px;")

ほんとに細かいですが、下のUIは色相を若干淡くして、ちょっと角丸にしてます(笑)

PySide2はスタイルシートが適用できるので、maya.cmds なんかで作るより、かなりオシャレなGUIを作れます。CSSライクに書けるってことは、実質webサイトみたいなもんですからね。

まぁ角丸だからなんだよって人もいると思いますが、もうここまで来たらただの自己満足ですよね(笑)

装飾したいから装飾する。ただそれだけ…

PySide2でおしゃれなGUIを作りたいですって人は、とりあえず、『unpyside』さんのサイトを参考にして下さい。めちゃくちゃ分かりやすいんで超おすすめです!

GUIの説明は以上です。

chat-GPTとMayaの連携まとめ

import sys
sys.path.append(r"C:\\work\\script\\ai\\openai")
import openai

from maya.app.general.mayaMixin import MayaQWidgetDockableMixin
from PySide2 import QtCore as QtCore
from PySide2 import QtWidgets as QtWidgets

class Chat():
    def __init__(self,prompt):
        self.api_key = "sk-qwerty..." #APIキーを入れる
        self.prompt = prompt
        self.personality = "あなたはTOEIC900点台の日本人の女学生(21歳)で、英語を上手に日本語に翻訳できます。性格はエヴァンゲリオンのアスカ・ラングレーようなツンデレキャラで接して下さい。代表的なセリフに、「あんたバカァ?」、「傷つけられたプライドは…10倍にして返してやるのよ」、「ねぇ、シンジ。キスしようか。」、「アスカ、行くわよ」、もう二度と負けられないのよこの私は!」、「バカ、無理しちゃって。」などがあります。語尾は「~なのよ。」、「~なの。」、「~なのに!」が一般的です。語尾は適切に使い分けて受け答えをして下さい。"
        
    def get_response(self):
        response = self.generate_response(self.prompt)
        return response
                
    def generate_response(self):
        openai.api_key = self.api_key
        completion = openai.ChatCompletion.create(
          model="gpt-3.5-turbo",
          messages=[
            {
            "role": "system",
            "content": f"{self.personality}"},
            {
            "role": "user",
            "content": f"{self.prompt}"
            }
          ]
        )
        
        message=completion.choices[0]["message"]["content"]
        print(message)
        return message

class ChatGPTGUI(MayaQWidgetDockableMixin, QtWidgets.QWidget):
    def __init__(self, parent=None):
        super(ChatGPTGUI, self).__init__(parent=parent)
        self.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
        self.setWindowTitle('chat-GPT_MayaBot')

        # 上下のスプリット実装
        self.splitter = QtWidgets.QSplitter(QtCore.Qt.Vertical, self)
        self.splitter.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)

        # 上部のUI
        self.text_edit1 = QtWidgets.QTextEdit(self.splitter)
        self.text_edit1.setText('Hello World')
        self.splitter.addWidget(self.text_edit1)

        # 下部のUI
        self.text_edit2 = QtWidgets.QTextEdit(self.splitter)
        self.text_edit2.setText('MayaBotに何でも聞いてみよう!')
        self.splitter.addWidget(self.text_edit2)

        # キーイベント着火用
        self.text_edit2.installEventFilter(self)

        # レイアウトセット
        layout = QtWidgets.QVBoxLayout(self)
        layout.addWidget(self.splitter)
        
        # GUI装飾
        self.splitter.setSizes([4*self.height()/3, self.height()/3])
        self.text_edit2.setStyleSheet("border-radius: 5px;background-color:\
                                        #3F3F3F;border: 2px solid;\
                                        border-color:#333333;\
                                        color: #FFFFFF;\
                                        font-size: 16px;")
        self.text_edit1.setStyleSheet("font-size: 16px;")

    def onCtrlEnterPressed(self):
        """Ctrl+Enterでchat-GPTに命令を投げる
        """
        input_text = self.text_edit2.toPlainText()
        chat = Chat(input_text)
        res = chat.generate_response()
        self.text_edit1.setText(res)

    def eventFilter(self, obj, event):
        """Ctrl+Enterで処理を実行
        """
        if obj == self.text_edit2:
            if event.type() == QtCore.QEvent.KeyPress and event.key() == QtCore.Qt.Key_Return:
                self.onCtrlEnterPressed()

def main():
    widget = ChatGPTGUI()
    widget.show(dockable=True, area='left')

if __name__ == "__main__":
    main()

紹介したコードをまとめると、こんな感じになります。APIキーの部分はあなたのAPIキーを入れて下さい。

chat-GPTとMayaを連携した感想【ブラウザで良くね?】

chat-GPT便利ですねー。いや~便利便利…。これでいちいちwebサイトに行かなくてもいいよね~…

って思いたいんですけど、ぶっちゃけブラウザでやったほうが良くないか…?って思います…。ここまでやっておいて元も子もないんですけど…。

というのも、ブラウザだと履歴が残るじゃないですか。これがやっぱり便利なんですよね。

chat-GPTを使ってスクリプトを書かせるのって、一発で狙った通りのコードになることは少ないです。(普通にウソつくし…)

↑の画像のように、返答をもとに「シンタックスハイライトで書き直して」とか、「このコマンド無いから別のやつでお願い」とか、「さっきの処理にこれも加えて」とかってフィードバックを繰り返しながら有用性のあるコードに調整していきます。

まぁ一応APIでも履歴を保持する方法はありますが、保持する履歴が増えるほどコストがかかります。

って考えると、ブラウザの方が良かったりするんじゃないですかねーってのが現在の所感です。

まぁ、だからと言ってAPIを触らない理由にはならないですけどね。

新しい技術が出たらそれを触るって姿勢は、全ての開発者が持つべきマインドだと思います。今回の記事をきっかけにPython触ってみよっかなー、外部パッケージをMayaで使いたいなーって人が増えれば幸いです。

それでは、みっつでした!!

ABOUT ME
みっつ
CGアニメーター/リガー テクニカルアーティスト(TA)目指して精進中です 都内でゲーム作ってます。