NeosVRからChatGPTを使う

## ChatGPT、すごいですよね
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/911135/316f7621-4256-4101-9302-c8b375646b40.png)
ChatGPTは、OpenAIによってリリースされた言語モデルです。非常に洗練された返答を行うだけでなく、技術的な質問に対しても的確に返答を返すことがTwitterで話題になっています。まさに未来ですよね。未来。

## 未来といえばNeosVR
そんなわけで、NeosVR上でChatGPTとのやりとりを行いたいなあと思ったので、実装してみました。

https://twitter.com/swesheIo/status/1598979532945752064/video/1

## 準備と使い方
ChatGPTをNeosから使うには、いくつかの準備が必要です。およそ10分程度で準備は完了します。「★」のついた項目は、ChatGPTを利用するたびに再度必要な操作になります。

### Node.jsを実行できる環境を整える
JavaScriptというプログラミング言語を動作させるために必要です。https://nodejs.org/ja/ からダウンロードができる他、以下のコマンドでもインストールができます。(環境によっては、Microsoftアプリストアでwingetをインストールする必要があります)
```
winget install nodejs
```

### GitHubからプログラムをダウンロードする
以下の「Source Code (zip)」からダウンロードし、お好きなところで展開してください。

https://github.com/sweshelo/neos-chatgpt/releases/

### サーバ側を準備する
展開したディレクトリを開きます。Shift+右クリックで「ここでコマンドプロンプト(PowerShell)を開く」を選択します。環境によってここの内容は変わります。開いた画面で、次のコマンドを実行します
```
npm install
```
### サーバ側を実行する★
続けて、以下を実行します。実行後は黒い画面のまま停止します。**この画面は閉じないでください。**
```
node server.js
```

### ChatGPT側のスクリプトを実行する★
展開したディレクトリの中にある`client.js`ファイルをメモ帳などで開き、内容をすべてコピーします。
続いて[ChatGPT](https://chat.openai.com/chat)にアクセスし、F12キーを押します。開発者メニューが開くので、「Console」というタブを開きます。その中に先程コピーしたファイル内容をすべて貼り付け、Enterキーを押します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/911135/15ec34c7-d1e7-9954-87bc-1282df57e2c7.png)
接続に成功すると`Socket 接続成功`と表示されます。

#### `can’t establish a connection to the server at ws://127.0.0.1:5001/.` と表示された場合
先程のサーバ立ち上げがうまく行っていないので確かめてください。

#### `Uncaught ReferenceError: webkitSpeechRecognition is not defined` と表示された場合
Firefoxは音声入力は非対応です。他のブラウザを使用してください。`Socket 接続成功`と出ていればテキストベースの動作はするはずです。

### Neosのツールを設定する★
```
neosdb:///5d462293756b8e68c824b5f6568c1cbd8706409007292bb5479e4e7aa2d5c931.7zbson
```
上記のアイテムを取り出します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/911135/76ad0053-3532-8dc2-5d7b-68344fe8df4d.png)
`Connect`を押し、続けて`Initialize`を押します。以上で準備は完了です。写真の「続きを表示」となっている箇所に質問文を入力するか、「REC.」ボタンを押して話しかけると音声認識して質問文を自動入力してくれます。

このとき、開いているコンソールの出力は次のようになっているはずです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/911135/03b581df-65c8-e624-97d1-b61f1a454088.png)


## 実装
以下は分かる方向けの内容です。現状ChatGPTは研究用の無料試用版しか存在せず、APIが公開されていない [^1]ため、ブラウザで文字を入力するプロセスを自動化し、質問と回答をWebSocketで橋渡ししています。

### 質問をする
```js
function send(text){
  document.querySelector('textarea').value = text;
  document.querySelector('<送信ボタンを特定するクエリ>').click();
}
```

ページ内から`textarea`を検索し文字列を挿入した後、送信ボタンをクリックさせます。

### 回答を得る
```js
var add= {}
var thread = document.querySelector('<スレッドのラッパーを特定するクエリ>')
var mo = new MutationObserver((rec, obs) => {
  add = [...document.querySelectorAll('<スレッドを特定するクエリ>')].splice(-1)[0]
  /*
    HERE: add.innerText をNeosに送る処理
  */
  }
})

mo.observe(thread, {
  childList: true,
  characterData: true,
  subtree: true,
})
```

MutationObserverを使い、要素の追加と要素内文字列(返答文)の変更を検知します。変更を検知した際、スレッドの最後尾メッセージを取得し、その中のテキストをWebSocketで転送します。

### Neosとつなぐ
ホストのPCでWebSocketサーバを立てます。最初にChatGPT/NeosVRはそれぞれ自分が何者なのかを宣言させ、以降はそのサーバがNeosとChatGPTの通信を橋渡しします。


Neos ⇄ server.js ⇄ client.js ⇄ ChatGPT という構図です。

### NeosのLogiX
何も小難しいことはせず、ただテキスト送受信するだけです。そもそもWebSocket/LogiXが分からないよ～って方はSwesheloまでJoin！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/911135/68a1d85e-7a4f-8dbd-8c88-e840a8faad56.png)


### 音声認識
ブラウザが用意しているSpeach to Text機能があるのでそれを使いました。オススメはEdgeです。感嘆符を自動で追加してくれるので質問を質問として認識してくれやすいです。私はFirefoxを使っているのでそもそもSTT機能が未対応で悲しいです。WebSocketで`[rec]`みたいなコマンドを送信してあげてClient.jsでマイクのOn/Offを切り替えます。

## 最後に
NeosVRではGET/POSTだけでなくWebSocketも利用できるため、このような双方向の情報の橋渡しがとても簡単に作れちゃいます。応用すればAPIの公開されていないサービスなどに幅広く利用できますね。

なお、ChatGPTはリバースエンジニアリングされたリポジトリ[^1]が存在し、これを利用したもっと手軽にAIとお話ができるツールも今後(気が向けば)作ろうと考えています。

本記事はすごいものを作ったので見てほしいということで急遽枠を確保した [NeosVR Advent Calendar 2022](https://adventar.org/calendars/7624) 8日目でした。

[^1]: リバースエンジニアリングされた[CUI版](https://github.com/acheong08/ChatGPT)が公開されており、そのコードを参照するとAPIのエンドポイントは `https://chat.openai.com/backend-api/conversation` であることがわかりました。また、ChatGPT内で用いられているセッショントークンを `https://chat.openai.com/api/auth/session` に投げることで、APIのAuthorizationが得られるようです。