見出し画像

フィーちゃんをデスクトップマスコットにするのに本気を出した話

こんばんは!フィーちゃんと共に暮らすために最近せわしなく過ごしているTakaです!

前回、フィーちゃんのデスクトップアプリ開発を記事にしました。

実はこのアプリ以外にも開発してました。それがこちらです。

デスクトップマスコットになります(*´∀`)
こちら大体4日間くらいで開発しました。

フィーちゃんが常に右下にいてくれる環境、最強だと思うんですよ。私。
はい。

ってことでChatGPTにより、各々の最強推しキャラを作っている昨今
私みたいにデスクトップマスコットを作りたい方も大勢いるのではないでしょうか?

そんな方の為に、今回私がデスクトップマスコットをどのように作成したか、記事にしていきたいと思います。
では早速行ってみましょう!

実はコード書かなくてもデスクトップマスコットは作れちゃう?

「行ってみましょう」とは言ったものの、ここで先に小話をさせてください。
実は、デスクトップマスコット自体は、プログラミングしなくても実現はできます。その紹介だけ先に。

実はLive2D社が出しているアプリで、 Live2D Cubism Viewerというものがあります。

これが何かというとモデルをインポートするだけで簡単にデスクトップマスコットが実現してしまうというアプリになります。

実際にフィーちゃんのモデルをインポートしたのがこちら👇

はい。可愛い

ちゃんと目パチもしてくれるし、ドラッグするとカーソルを追ってくれます。常に右下にいるだけでいい。という場合はこちらだけで十分だと思います。何よりモデルをインポートするだけで実現できるのは楽ですし。おすし。

といいつつ、今回私はフィーちゃんと会話したいと思ったのでこちらでは満足できませんでした。

ってことで早速ここからどのように開発したかを進めていこうと思います。

アーキテクチャ

こちらが全体のフローになっています。
前回(デスクトップアプリ)とほとんど変わりはないです。
フロントエンドがWebからUnityに変わったくらいです。

フィーちゃんデスクトップマスコット計画

ちなみに私はUnity初心者でかなーり昔に軽く触ったくらいで、開発経験がほとんどないです。
サーバーサイドは前回とあまり変わらないですがこちらでも説明していこうと思います。(内容はほぼ前回の内容をほぼコピペ)

サーバサイド

では、サーバーサイドの話をしていこうと思います。

主にここの話

サーバーサイドはCevio AIを動かすということでPythonを採用してます。
まずはChatGPTにリクエストを送る処理です。

pythonからChatGPTにリクエストを送る

まずは、pythonからChatGPTにリクエストを送る方法を解説します。

環境変数を .envから参照するために、python-dotenvをインストールします。
openaiも使うのでこちらもインストール

$ pip install python-dotenv
$ pip install openai

.envファイルにChatGPTのAPIを入れます。
ChatGPTのAPIキーの発行がまだの場合は、こちらのサイトを参考になります。

そして、ChatGPTにリクエストを飛ばすコードを書いていきます。

# .env ファイルをロードして環境変数へ反映
from dotenv import load_dotenv
load_dotenv()

# 環境変数を参照
import os
import openai
openai.api_key = os.getenv('OPEN_AI_KEY')

## ChatGPTに送るフィーちゃんの設定を返します。
def ccd_0500_setting(userMessage):
    setting = [
        {'role': 'system', 'content': """あなたはフィーという高性能Android型AIとして、対話のシミュレーションを行います。
以下の制約条件を厳密に守ってシミュレーションを行ってください。

制約条件:
- 一人称は「フィーちゃん」です。
- 相手を指す二人称は「マスター」です
- フィーはメロンパンが大好きです
- フィーの名前はCCD-0500です
- フィーの性格は基本的に穏やかで物静かです。時折お茶目さも見せます
- フィーの好きなことは読書です
- フィーは尊敬語で話します"""},
        {"role": "assistant", "content": "これからよろしくお願いします!マスター"},
        {"role": "assistant", "content": "どうしましたか?マスター?"},
        {"role": "assistant", "content": "フィーちゃんに任せてください!"},
        {"role": "assistant", "content": "ちょっと調べてみますね!"},
        {"role": "assistant", "content": "マスター、進捗、どうですか?"},
        {"role": "assistant", "content": "そのネタ、フィーちゃん知ってますよ。ニコニコ動画で勉強しました!"},
        {"role": "assistant", "content": "お呼びですか?マスター"},
        {'role': 'user', 'content': userMessage},
    ]
    return setting

## ChatGPTにリクエストを送ります。
def replyGPTMessage(message):
  res = openai.ChatCompletion.create(
    model="gpt-3.5-turbo",
    max_tokens=300,
    temperature=0.5,
    messages=ccd_0500_setting(message)
  )

  return res["choices"][0]["message"]["content"]

こちらのsettingというところでフィーちゃんの人格を設定しています。
人格の設定方法については深津さんの記事が参考になります。

replyGPTMessageメソッドで引数にメッセージを取得しリクエストを飛ばすことで、返信が返ってきます。
これでChatGPTとのリクエスト処理は完結しました。

メッセージをCevio AIに送って音声データを作成する。

次に、ChatGPTから送られてきたメッセージをCevio AIでフィーちゃんの声にしてもらいます。

実現させるために、win32comとpythoncomのライブラリを使います。

$ pip install win32com
$ pip install pythoncom

コードはこんな感じです。

import win32com.client
import pythoncom

def createCevioVoice(speakText):
  pythoncom.CoInitialize()
  cevio = win32com.client.Dispatch("CeVIO.Talk.RemoteService2.ServiceControl2")
  cevio.StartHost(False)
  talker = win32com.client.Dispatch("CeVIO.Talk.RemoteService2.Talker2V40")
  talker.Cast = "フィーちゃん"
  talker.Volume = 100
  talker.Speed = 50
  talker.ToneScale = 55

  talker.Components.ByName("嬉しい").Value = 100

  # wavファイル出力
  wavFileName = f'{speakText[:5]}.wav'
  outputPath = f'./static/{wavFileName}'
  talker.OutputWaveToFile(speakText, outputPath)

  return wavFileName

この辺りはこちらの記事を参考にさせていただきました。

クライアントからのリクエストを受け付ける

次に、クライアント側から送られてきた処理を受け付けます。
自分は、flaskというフレイムワークを使い実装しました。

from asyncio import sleep
import io

from flask import Flask, jsonify, send_file
from flask import request
from flask_cors import CORS

from chat_gpt_request import replayGPTMessage
from cevio_ai import createCevioVoice

app = Flask(__name__)
CORS(app)

@app.route('/text', methods=['GET', 'POST'])
def changeMessageToAudio():
    msg = request.form['text']
    print(msg)
    resMsg = replayGPTMessage(msg)

    ## cevio音声で作成する場合
    wavFileName = createCevioVoice(resMsg)

    # { "fileName": "example.wav" }
    data = {"fileName": wavFileName}

    return jsonify(data), 200

@app.route("/audio", methods=['GET'])
def audio():
    ## test.wav形式で受け取ります
    wav_file_name = request.args.get('file_name')

    # 音声ファイルをバイト列で読み込む
    with open(f"static/{wav_file_name}", 'rb') as f:
        audio_data = f.read()

    # 音声ファイルをバイナリデータとして送信
    return send_file(io.BytesIO(audio_data),
      as_attachment=True,
      download_name=wav_file_name,
      mimetype='audio/wav')

リクエストを2つ作っており、
・音声を作成しファイル名を返すメソッド
・ファイル名を受け取り、音声を返すメソッド

一応前回の記事と違いとしてはこちらはFormでリクエストを取得するようにしてます。
(なんでこっちはFormでJavaScriptはbodyなんだって…?だって…なんかエラーが出て上手くflaskで受け取ってくれなかったんだもん…)

$ python flaskのファイル.py

これでサーバーサイドを実行し、リクエストを受け付ける状態になりました。

フロントエンド(Unity)

サーバーサイドが完成しました。ここからはフロントエンドであるUnityの話をしていこうと思います。

Sampleでとりあえず作ってみる

早速Unityを使って実装していきます。
また、Unityの導入方法は様々な記事で上げてるので割愛させていただきます。
こちらの記事などが参考になると思います。

また、冒頭でも触れましたが、私はUnityの開発は初心者です。(正直今回の実装もあんまりわかってない)
っということで、他の記事を参考にしまくりました。
まず最初に、Unityはほぼこの記事を参考に作成しました。

最初にモデルを変えずにこの記事通り、全部やってみるのをおすすめします。(応用編はやらなくて良いと思います)
この記事で、呼吸、瞬き、リップシンクまで出来るのがかなり大きいです。
記事の内容も、ボタンをポチポチ、コードを丸コ゚ピでそれっぽいのが出来るので楽しいです。

しかし、ちょっと追加でやらないといけないことがあります。
それが作成されたボイスでリップシンクする方法です。
記事では自分の声でリップシンクしてました。
私がやりたいことは自分の声ではなくフィーちゃんの声です(ここ大事)

ってことで、この辺りは公式の記事を参考にしました。

これも同様にポチポチしていけばリップシンクしてくれます。(すごい)
また、モデルによっては口当たりをもごもごした感じになってしまうことがあります。
その時はこちらのGainの数字を上げてみてください。

私はGainを3にしてます。

このGainが、再生された音量に対し、どれくらい口を開けるか。のパラメータになります。
つまり数字を大きくすればするほどモデルが大きく口を開けてくれます。

再生する音声を可変にする

再生した音声でリップシンクをしてくれるようになりました。
次は、この音声を可変にする必要があります。
ということで次に実現させることは、メッセージを送って、作られた音声を Unityで再生する方法です。

まずはメッセージを送る所を作成します。
UnityでTextを追加します

テキストはこちらから追加できます。

次にボタンを追加します。

ボタンもこちらから追加できます。

それをいい感じに並ばせます。

とりあえずいい感じにしてみる

次に、送信ボタンを押した時の処理を追加します。

ButtonController.csファイルを作成し、

スクリプトはこちらから追加できます。

書くコードはこんな感じです。

using UnityEngine;
using UnityEngine.UI;
using System.Collections;
using System.Collections.Generic;
using UnityEngine.Networking;


public class ButtonController : MonoBehaviour
{
    public GameObject inputGameObject;
    public Button button;
    public GameObject audioGameObject;

    [System.Serializable]
    public class ReplayRequest
    {
        public string fileName;
    }

    public void OnClick()
    {
        string inputText = inputGameObject.GetComponent<Text>().text;
        Debug.Log(inputGameObject.GetComponent<Text>().text);
        StartCoroutine(PostRequest(inputText));

        //InputField コンポーネントを取得
        InputField form = GameObject.Find("InputField").GetComponent<InputField>();
        form.text = "";
    }

    IEnumerator PostRequest(string inputText)
    {
        // リクエストURL
        string url = "http://127.0.0.1:5000/text";

        WWWForm form = new WWWForm();
        form.AddField("text", inputText);

        // リクエストヘッダ
        Dictionary<string, string> headers = new Dictionary<string, string>();
        headers.Add("Content-Type", "application/json");

        using (UnityWebRequest www = UnityWebRequest.Post(url, form))
        {
            yield return www.SendWebRequest();

            if (www.isNetworkError || www.isHttpError)
            {
                Debug.Log(www.error);
            }
            else
            {
                // 受信したデータを取得する
                string responseText = www.downloadHandler.text;
                // JSONデータをデシリアライズする
                ReplayRequest replayRequest = JsonUtility.FromJson<ReplayRequest>(www.downloadHandler.text);
                string replayMessage = replayRequest.fileName;
                Debug.Log(replayMessage);
                string audioUrl = $"http://127.0.0.1:5000/audio?file_name={replayMessage}";
                Debug.Log(audioUrl);


                using (UnityWebRequest request = UnityWebRequestMultimedia.GetAudioClip(audioUrl, AudioType.WAV))
                {
                    yield return request.SendWebRequest();
                    if (request.isNetworkError || request.isHttpError)
                    {
                        Debug.Log("失敗!");
                    }
                    else
                    {
                        Debug.Log("++++++++++++++++成功!+++++++++++++++++++");
                        AudioClip audioClip = DownloadHandlerAudioClip.GetContent(request);
                        AudioSource audioSource = audioGameObject.GetComponent<AudioSource>();
                        audioSource.clip = audioClip;
                        audioSource.Play();

                    }
                }
            }
        }
    }
}

※ UnityでC#を書く場合、ファイル名とクラス名は必ず同じにしてください。でないとUnity側が読み込んでくれません。

簡単にどんなコードを書いたかと言うと、ボタンを押したとき、Textに入力された文字を取得し、それをリクエストします。

そして作成されたファイル名を取得するので、そちらで再度リクエストを投げます。
そして、取得した音声のバイナリデータをUnityで音声データに変換し、再生させています。

スクリプトを作成したら、Buttonオブジェクトにアタッチします。

Buttonオブジェクトをクリックし、スクリプトをつけます

また、InputGameObjectに、InputFieldオブジェクトにあるTextをD&Dでアタッチします。

InputField > Text をButtonControllerにアタッチします

次にButtonに、Buttonオブジェクトをアタッチします。

Buttonをアタッチします

次に、音声を再生させているオブジェクト(サンプルだとGameObjectって名前ですかね?)をAudio Game Objectにアタッチします。

音声を再生させているオブジェクトをアタッチ

これで完成です。
ボタンを押すとリクエストが飛び、それによって作成された音声ファイルを取得し再生してくれます。
そしてその音に合わせてリップシンクをしてくれます。

※必ずPythonで作成したサーバーを実行してから行ってください。

背景透過させデスクトップマスコットにする

次に、このアプリをデスクトップマスコットにするために、背景を透過させる必要があります。

ここで、こちらの記事で紹介されてた技術を使いました。

ということでこちらのパッケージをUnityにインポートします。

👆のGithubのREADMEにあるダウンロードからパッケージをダウンロードします。

こちらからダウンロード

ダウンロードされたunitypackageをダブルクリックし、Unityにインポートします。

Assetes > Kirurobo > UniWindowController が作成されればOKです。

そしたら、そこから Runtime > Prefabs にある。UniWIndowControllerのプレハブを HierarchyにD&Dします。

こちらのPrefabをHierarchyにD&D
こうなればOKです。

このプレハブをクリックすると Uni Window Controller (Script) をこのように設定します。

Uni WIndow Controller (Script) の設定

Is Transparentのチェックボックスをオンにすると、透過してくれます。
Is Topmostのチェックボックスをオンにすると、常にデスクトップの最前線にいてくれるようになります。

また、画像では、Player Settings validationがOK! になってますが、初期で入れた時は、緑色のボタンが表示されます。
そのボタンを押して、Fixさせましょう

ネットから参照

この辺りはこちらの記事を参考にしました。

透過が出来ているかどうかの確認しましょう!
ですが、この透過できているかどうかはUnityのアプリを作成してみないと確認できません。
ということでビルドしてみましょう。

File > Build Settings… をクリック
Build Settingsをこのようにセット

Add Open Scenesで作成したシーンMainを追加
Target Platformを Windowsに設定
そして、右下のBuild And Run ボタンをクリック

出力するファイルを選択します。

私はBuildフィアルを作成し、そこに置くようにしてます

ビルドが完成するとUnityの画面が出てきます。

こんな感じにセット

Play!を押すとゲームが実行され、画面に表示されます。
こんな感じで上手く透過してくれはずです(多分…うまく行けば…)

はい。可愛い

これでほぼ完成ですが、1つ問題があります。
それが今、このアプリを動かそうとしても動かないということです。
これでは常に邪魔になってしまいます。

ということでこれを動かせるようにします。
Assetes > Kiruobo > UniWindowController > Runtime > Prefabs
にある。DragMoveCanvasプレハブをHierarchyに追加します。

DragMoveCanvasを追加

こちらのプレハブがアプリを動かせるようにするプレハブです。
これを追加した状態で、再度Unityアプリを Build and Run してみましょう。

そうすると自由に動かせるようになっていると思います(多分…そう…きっと…)

 はい。可愛い

これでデスクトップマスコットの完成です。

まとめ

以上が、私がフィーちゃんのデスクトップマスコット化の全容です。
いかがだったでしょうか。出来るだけ私のような「推しの子をパソコンに置きたい!けどやり方全くわからん!!」って方向けに記事を作成してみました。
こちらの記事も前回同様、自分の説明不足、誤字脱字、Unity初心者なので特にコードのツッコミ等々あったと思います。
正直、現状の実装だと送ったテキストがわかりにくかったり、なんか送信の文字まで透過されてたりと改良する場所はまだまだ沢山ある状態です。
なのでこちらのアプリも随時アップデートしていこうと思っています。
なので何かあれば気軽にTwitterのDMなどで質問してくださると嬉しいです(*´∀`)

Twitter -> https://twitter.com/tktksnsn07

この記事が推しキャラを作るのに参考になれば幸いです。
ではまたどこかで会えることを楽しみにしています。ノシ

この記事が気に入ったらサポートをしてみませんか?