ChatGPT API を使用してMayaを（Pythonスクリプトで）操作してもらう

# はじめに
ChatGPTのAPIがリリースされたのでMaya上で動作させてみました。ただ会話するだけではつまらないので、Pythonスクリプトを書いてもらってそのまま実行するUIを作成してみます。

https://platform.openai.com/docs/guides/chat

https://twitter.com/akasaki1211/status/1631613891120705537


## テスト環境
* Maya 2023 (Python 3.9.7)
* openai 0.27.0

## Mayaに`openai`を入れる
まずはMayaのPythonから`openai`モジュールを呼び出せないといけないので、以下の方法でインストールしました。

1. Anaconda3で仮想環境を作成。Mayaと同じバージョンのPythonを入れる
1. 仮想環境に`openai`を入れる
1. `PYTHONPATH`に仮想環境のパッケージフォルダ`~\site-packages`を追加し、Maya起動

```
conda create -n maya2023 python=3.9.7
conda activate maya2023
pip install openai
```

```bat
@echo off
set PYTHONPATH=%UserProfile%\Anaconda3\envs\maya2023\Lib\site-packages
start "" "%ProgramFiles%\Autodesk\Maya2023\bin\maya.exe"
exit
```

https://twitter.com/akasaki1211/status/1621112444776247296

mayapyとpipでMayaのインストールフォルダに直接パッケージを入れる方法もありますが、Maya本体には手を入れたくないためこのような方法を取っています。

https://knowledge.autodesk.com/ja/support/maya/learn-explore/caas/CloudHelp/cloudhelp/2023/JPN/Maya-Scripting/files/GUID-72A245EC-CDB4-46AB-BEE0-4BBBF9791627-htm.html

# 実装
## APIコールを行う関数を準備

```python
import os
import openai

openai.api_key = os.getenv("OPENAI_API_KEY")

def completion(new_message_text:str, settings_text:str = '', past_messages:list = []):
    if len(past_messages) == 0 and len(settings_text) != 0:
        system = {"role": "system", "content": settings_text}
        past_messages.append(system)

    new_message = {"role": "user", "content": new_message_text}
    past_messages.append(new_message)

    result = openai.ChatCompletion.create(
        model="gpt-3.5-turbo",
        messages=past_messages
    )
    message_text = result.choices[0].message.content
    response_message = {"role": "assistant", "content": message_text}
    past_messages.append(response_message)
    
    return message_text, past_messages
```
こちらは以下の記事を大いに参考にさせていただいてます。（ほぼコピペですが...）

https://qiita.com/sakasegawa/items/db2cff79bd14faf2c8e0

## 返答をPythonコードとその他の部分に分解

`re`モジュールを使って` ```python `~` ``` `で囲まれた部分とそれ以外の部分を切り出します。

```python
import re

def decompose_response(txt):
    pattern = r"```python([\s\S]*?)```"
    
    code_list = re.findall(pattern, txt)
    for i in range(int(len(code_list))):
        code_list[i] = re.sub('\A[\r?\n]', '', code_list[i])
        code_list[i] = re.sub('[\r?\n]\Z', '', code_list[i])
    
    comment = re.sub(pattern, '', txt)
    comment = re.sub('[\r?\n]+', '\n', comment)
    comment = re.sub('[\r?\n]\Z', '', comment)
    
    return comment, code_list
```

例えば返答がこんな感じだった場合、
````
ああああああああああああ。
```python
import os
```
いいいいいいいいいいいいいいい。
```python
import maya.cmds as cmds
```
うううううううううううううううううう。
````
戻り値はこうなります。
```python
comment, code_list = decompose_response(txt)

comment
# Result: ああああああああああああ。
# いいいいいいいいいいいいいいい。
# うううううううううううううううううう。

code_list
# Result: ['import os', 'import maya.cmds as cmds']
```


## UI作成

いよいよMayaでUIを作成します。
かなり即席です。ChatGPTがコードブロックを複数書いてきた場合2つ目以降は消えてなくなります（汗

```python
from maya import cmds

class ChatGPT_Maya(object):

    def __init__(self):
        self.system_settings = "質問に対して、MayaのPythonスクリプトを書いてください。スクリプト以外の文章は短めにまとめてください。"
        self.message_log = []
        self.at_first = True
        self.create_window()

    def reset_session(self, *args):
        self.message_log = []
        self.at_first = True
        cmds.scrollField(self.input_field, e=True, tx='')
        cmds.scrollField(self.ai_comment, e=True, tx='')
        cmds.cmdScrollFieldExecuter(self.script_field, e=True, t='')

    def call(self, *args):
        user_input = cmds.scrollField(self.input_field, q=True, tx=True)
        
        # APIコール
        if self.at_first:
            message_text, self.message_log = completion(user_input, self.system_settings, [])
            self.at_first = False
        else:
            message_text, self.message_log = completion(user_input, '', self.message_log)
            
        # 返答を分解
        comment, code_list = decompose_response(message_text)
        
        # Pythonコード以外の部分をai_commentに表示
        cmds.scrollField(self.ai_comment, e=True, tx=comment)

        # Pythonコードの1つ目をscript_fieldに表示。2つ目以降は無視（汗
        if code_list:
            cmds.cmdScrollFieldExecuter(self.script_field, e=True, t=code_list[0])
            # 実行
            if cmds.checkBox(self.script_exec, q=True, v=True):
                cmds.cmdScrollFieldExecuter(self.script_field, e=True, executeAll=True)
        else:
            cmds.cmdScrollFieldExecuter(self.script_field, e=True, t='')
    
    # UI作成
    def create_window(self, *args):
        cmds.window(title=u'ChatGPTがPythonスクリプトを書くよ！', width=600, sizeable=True)
        
        cmds.columnLayout(adj=True, cat=['both',5], rs=5)
        self.reset_button = cmds.button(label='Reset', c=self.reset_session) #セッションのリセット
        self.input_field = cmds.scrollField(h=50, ed=True, ww=True, tx='', ec=self.call) #テンキーのEnterで送信
        self.script_exec = cmds.checkBox(label=u'実行もする', align='left', v=True) #Checkが入っていたら返答と同時にスクリプトを実行
        cmds.separator(h=10, st='in')
        self.ai_comment = cmds.scrollField(h=100, ed=False, ww=True, tx='') #コードブロック以外の部分を表示する場所
        cmds.text(l='Script:', align='left')
        self.script_field = cmds.cmdScrollFieldExecuter(st='python', h=200) #Pythonコードを表示・実行する場所
        cmds.setParent('..')

        cmds.showWindow()
```

こんなかんじのUIが出来ました！ウィンドウを閉じるかResetボタンを押すまでは、直前までの会話を記憶した状態になります。
![chatgpt_maya_ui](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211093/de280da1-f465-d5ef-8448-78a4f666a892.png)

---
【2023/3/6 追記】少しだけ改良したものをGithubにアップしたのでリンクを記載しました。
【2023/4/18 追記】アップデートが進んでしまったので、執筆時点のコミットへリンクを差し替えました。

https://github.com/akasaki1211/ChatGPT_Maya/tree/beta