メインコンテンツへスキップ
見出し画像

フィーちゃんのデスクトップアプリに本気を出した話

    こんばんは!Takaです!
    突然ですが皆さんは、CCD-0500、フィーちゃんをご存じですか?

    こちらのキャラクターですが、音声合成ソフトと呼ばれるテキストを入力したらその子の声で音声を作成してくれるソフトCevio AIに存在する1キャラクターです。

    そしてキャラクターにはそれぞれどんな子なのかの設定が存在します。
    その中でこの子の設定は、
    --- 人々のあらゆる生活シーンをサポートするため、現代に生まれた汎用型アンドロイド「カリカチュア」---
    となっております。つまり、メイドAIといった所でしょうか。

    声も非常に可愛らしく、とても癒される声が特徴となっております。

    そしてなぜこの子について触れたかというと、最近話題のChatGPTですが、どうやら人格を付与できる。ということを知った私は、フィーちゃんと会話できるツールを作りたいと思いました。

    ちなみにそれはこちらの方から教えていただきました。

    そして3日間くらいで作ったのがこちらです(´∇`)

    とっても可愛らしいですね(*´∀`)
    今回は、このデスクトップアプリをどのように作成したか、記事にしたいと思います。
    では前段が長くなってはしまいましたが、早速話していきましょう!!

    アーキテクチャ

    まずは、このアプリのアーキテクチャを紹介します。
    このアプリは、大まかにこのような流れで作られています。

    画像
    フィーちゃんデスクトップアプリ計画

    リクエストしたメッセージをChatGPTに送り、返ってきたメッセージを音声変換してもらい、エンドユーザーに提供する。
    というのが全体のフローです。

    では、それぞれをもっと詳しく見ていきましょう。

    サーバーサイド

    今回は、Cevio AIのソフトをコードから動かすということでpythonで実装を行いました。

    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というフレイムワークを使い実装しました。

    import io
    
    from flask import Flask, jsonify, send_file
    from flask import request
    from flask_cors import CORS
    
    from chat_gpt_request import replyGPTMessage
    from cevio_ai import createCevioVoice
    
    app = Flask(__name__)
    CORS(app)
    
    ## 来たメッセージから返信ボイスのファイル名を返します。
    @app.route('/getAudio', methods=['POST'])
    def getAudioFromText():
        msg = request.json["text"]
        print(msg)
        resMsg = replyGPTMessage(msg)
        print(resMsg)
        ## 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つ作っており、
    ・音声を作成しファイル名を返すメソッド
    ・ファイル名を受け取り、音声を返すメソッド

    これでサーバーからはリクエストを受け付けます。
    これでサーバーサイドは実装完了しました!

    フロントエンド

    ここからはフロントエンドを実装していきます。
    今回は、Live2d for webを使って実装しました。

    また、Live2dのモデルはこちらを購入しました

    (高かった…)

    モデルを配置する

    ってことでフロントエンドを作っていくために、公式サイトからLive2d for webをダウンロードします。

    こちらをダウンロードすると、
    live2d-for-web/Samples/Typescript/Demo
    で、デモプロジェクトを見ることが出来ます。
    ターミナルでそこに移動し、次の事を実行します。

    $ cd live2d-for-web/Samples/Typescript/Demo
    $ yarn
    $ yarn start

    するとブラウザでこのような画面が表示されます。

    画像
    ※公式サイトから引用

    ギアをポチポチすると色んなモデルに切り替えることが出来ます。
    今回はフィーちゃんのモデルを使いたいので、フィーちゃんのモデルを、
    live2d-for-web/Samples/Resources/ 配下にfeeちゃんのモデルを追加します。
    必ずファイル名は fee にしましょう。
    ※ファイル名を fee にしないとモデルを読みこんでくれません…

    画像
    こんな感じで配置

    次に、モデルをLive2d for webで読みこんでくれるようにします。
    live2d-for-web/Samples/TypeScript/Demo/src/lappdefine.ts の51行目くらい…? を修正します。

    // モデル定義---------------------------------------------
    // モデルを配置したディレクトリ名の配列
    // ディレクトリ名とmodel3.jsonの名前を一致させておくこと
    export const ModelDir: string[] = [
      'fee',
      // 'Hiyori',
    ];

    このModelDirに読み込ませたいモデルの名前をセットすると読み込んでくれます。

    画像
    はい。可愛い

    そしてリロードすると、入れたモデルを読み込んでくれます。
    この辺はこちらの記事が参考になりました。

    UIを作成する

    次にこのUIを自分好みにチューニングします。
    私はニコニコ動画が好きなのでニコニコ動画のUIを参考にさせていただきました。

    index.htmlを次の様に実装しました。

    <!DOCTYPE html>
    <html>
    <head>
      <meta charset="utf-8">
      <meta name="viewport" content="width=1900">
      <title>フィーちゃんのお部屋</title>
      <link rel="stylesheet" href="index.css">
      <!-- Live2DCubismCore script -->
      <script src = "../../../Core/live2dcubismcore.js"></script>
      <!-- Build script -->
      <script src = "./dist/bundle.js"></script>
    </head>
    <body>
      <div class="MainContainer">
        <div class="VideoSymbolContainer">
          <canvas id="myCanvas"></canvas>
          <div class="CommentArea">
            <section class="CommentPostContainer">
              <div class="CommentPostContainer-inner">
                <div class="CommentPostContainer-commentInput">
                  <div class="RecaptchaRequirer-comment">
                    <div class="CommentInput">
                      <input class="CommentInput-textarea" id="CommentInputArea" placeholder="コメント" maxlength="75" rows="1"></input>
                    </div>
                  </div>
                  <div class="RecaptchaRequirer-button">
                   <button type="button" class="CommentPostButton" id="comment-post-button">
                      コメント
                    </button>
                  </div>
                </div>
              </div>
            </section>
          </div>
        </div>
      </div>
    </body>
    </html>
    

    index.cssは次の様に修正

    html, body {
      font-family: Avenir,Lato,-apple-system,BlinkMacSystemFont,Helvetica Neue,Hiragino Kaku Gothic ProN,Meiryo,メイリオ,sans-serif;
      margin: 0;
      overflow: hidden;
      background-color: #f4f4f4;
    }
    
    .MainContainer {
      display: flex;
    }
    
    .VideoSymbolContainer {
      display: flex;
      flex-direction: column;
    }
    
    
    /* キャンバスの大きさ */
    #myCanvas {
      display: block;
      margin: 16px 16px 0px 16px;
    }
    
    /* コメントを打つところ */
    .CommentArea {
      margin: 0px 16px 16px 16px;
      background-color: #fff;
    }
    
    .CommentPostContainer {
      margin-right: 8px;
      width: 100%;
      display: block;
    }
    
    .CommentPostContainer-inner {
      display: flex;
    }
    
    .CommentPostContainer-commentInput {
      display: flex;
      flex-direction: row;
      background-color: #fff;
      border-bottom: 2px solid #ddd;
      border-top: 2px solid #ddd;
      flex: 1 0 1px;
      min-width: 1px;
      vertical-align: middle;
    }
    
    .RecaptchaRequirer-comment {
      margin: 8px 0px 8px 16px;
      width: 100%;
    }
    .RecaptchaRequirer-button {
      margin: 8px 16px 8px 0px;
    }
    
    .CommentInput {
      font-size: 14px;
      line-height: 20px;
      padding: 4px 8px;
      border-top: 2px solid #ddd;
      border-bottom: 2px solid #ddd;
      border-left: 2px solid #ddd;
      border-radius: 4px 0px 0px 4px;
    }
    
    .CommentInput-textarea {
      background-color: transparent;
      border: none;
      outline: none;
      display: inline-block;
      height: 100%;
      line-height: inherit;
      overflow: hidden;
      padding: 0;
      resize: none;
      vertical-align: top;
      white-space: pre;
      width: 100%;
    }
    
    .CommentPostButton {
      align-items: center;
      background-color: #007cff;
      color: #fff;
      cursor: pointer;
      display: flex;
      font-size: 12px;
      font-weight: 700;
      height: 32px;
      justify-content: center;
      line-height: 1.5;
      width: 92px;
      border-top: 2px solid #ddd;
      border-right: 2px solid #ddd;
      border-bottom: 2px solid #ddd;
      border-left: none;
      border-radius: 0px 4px 4px 0px;
    }
    

    またフィーちゃんが表示されているcanvasはTypeScript側で実装されていたのでそちらも修正します。
    live2d-for-web\Samples\TypeScript\Demo\src\lappdelegate.ts

      /**
       * APPに必要な物を初期化する。
       */
      public initialize(): boolean {
        // キャンバスの作成
        // canvas = document.createElement('canvas');
        canvas = document.getElementById('myCanvas') as HTMLCanvasElement;
      
        ...
    
        // キャンバスを DOM に追加
        // document.body.appendChild(canvas);

    createElementされているところをgetElementByIdでhtmlで作られているcanvasの方を参照するようにします。

    また、キャンバスをDOMに追加するところもあるのでそこもコメントアウトしておきます。

    画像
    はい。可愛い

    まだcanvasが大きいですね。
    こちらニコニコ動画の動画を見る所っぽくいい感じに修正します。

    live2d-for-web\Samples\TypeScript\Demo\src\lappdelegate.ts
    にある。  private _resizeCanvas(): void を次のように修正します。

    export class LAppDelegate {
    
      ...
    
      /**
       * Resize the canvas to fill the screen.
       */
      private _resizeCanvas(): void {
        let {canvasWidth, canvasHeight} = resizeCanvas(window.innerWidth - 40, window.innerHeight - 80)
        canvas.width = canvasWidth
        canvas.height = canvasHeight
      }
    
      ...
    
    }
    
    /** 16:9のアスペクト比のWidth, Heightを返します */
    const resizeCanvas = (width: number, height: number) => {
      const ratio = 16 / 9;
      let canvasWidth = 0;
      let canvasHeight = 0;
      if (width / height > ratio) {
        canvasWidth = height * ratio;
        canvasHeight = height;
      } else {
        canvasWidth = width;
        canvasHeight = width / ratio;
      }
    
      return {canvasWidth, canvasHeight}
    }

    canvasの大きさを16:9にするように関数を追加してます。

    画像
    はい。可愛い

    すると、いい感じにそれっぽくなりました。
    次はフィーちゃんをもっとこっちに寄ってもらいましょう。
    live2d-for-web\Samples\TypeScript\Demo\src\lapplive2dmanager.ts の
    public inUpdate(): void メソッドを修正します。

    export class LAppLive2DManager {  
    
      ...
    
      /**
       * 画面を更新するときの処理
       * モデルの更新処理及び描画処理を行う
       */
      public onUpdate(): void {
        const { width, height } = canvas;
    
        const modelCount: number = this._models.getSize();
    
        for (let i = 0; i < modelCount; ++i) {
          const projection: CubismMatrix44 = new CubismMatrix44();
          const model: LAppModel = this.getModel(i);
    
          if (model.getModel()) {
            if (model.getModel().getCanvasWidth() > 1.0 && width < height) {
              // 横に長いモデルを縦長ウィンドウに表示する際モデルの横サイズでscaleを算出する
              model.getModelMatrix().setWidth(2.0);
              // projection.scale(1.0, width / height);
              projection.scale(2.0, width / height * 2.0);  //表示サイズを3倍に変更
              projection.translate(0, -2.5);  
            } else {
              // projection.scale(height / width, 1.0);
              projection.scale(2.0, width / height * 2.0);  //表示サイズを3倍に変更
              projection.translate(0, -2.5);  //高さをなんこういい感じに調整
            }
    
            // 必要があればここで乗算
            if (this._viewMatrix != null) {
              projection.multiplyByMatrix(this._viewMatrix);
            }
          }
    
          model.update();
          model.draw(projection); // 参照渡しなのでprojectionは変質する。
        }
      }
    
    ...
    
    }
    画像
    はい。可愛い

    このフィーちゃんモデルは、まだゆらゆら揺れるだけで、目パチや口パクはしてくれません。
    なので実装しましょう。

    目パチを実装する

    目パチは結構簡単で、fee.model3.jsonファイルの中身をこのように修正します。

    "Groups": [
    		{
    			"Target": "Parameter",
    			"Name": "EyeBlink",
    			"Ids": [
    				"ParamEyeLOpen",
    				"ParamEyeROpen"
    			]
    		}
    	]

    これを追加するだけで目パチをしてくれます。可愛いですね(*´∀`)

    口パクを実装する

    次に口パクの実装をします。
    口パクはLive2dでは「リップシンク」と呼んでいるのでこちらも以後リップシンクと呼びます。

    こちらのリップシンクを実装するために、まずは fee.model3.jsonに追加実装します。

    	"Groups": [
    		{
    			"Target": "Parameter",
    			"Name": "LipSync",
    			"Ids": [
    				"ParamMouthOpenY"
    			]
    		},
    		{
    			"Target": "Parameter",
    			"Name": "EyeBlink",
    			"Ids": [
    				"ParamEyeLOpen",
    				"ParamEyeROpen"
    			]
    		}
    	]

    次に音声に合わせてリップシンクしてもらうために、tsファイルを修正します。
    live2d-for-web\Samples\TypeScript\Demo\src\lappmodel.ts を修正します。

    /**
     * ユーザーが実際に使用するモデルの実装クラス<br>
     * モデル生成、機能コンポーネント生成、更新処理とレンダリングの呼び出しを行う。
     */
    export class LAppModel extends CubismUserModel { 
    
      ...
      
      /**
       * 更新
       */
      public update(): void {
        ...
        // リップシンクの設定
        if (this._lipsync) {
          let value = 1.0; // リアルタイムでリップシンクを行う場合、システムから音量を取得して、0~1の範囲で値を入力します。
          this._wavFileHandler.update(deltaTimeSeconds);
          value = this._wavFileHandler.getRms();  // ここでどれくらい口を開けるかのパラメータを取得できる
          value = value * 4;
    
          this._model.addParameterValueById(this._lipSyncIds.at(0), value, 0.8);
        }
        ...
      }
      
      
      /**
       * (自前)リップシンクの音を流します。
       * @param path Audip_path
       */
      public async startVoice() {
        document.body.addEventListener("click", () => {
          const path = "test.wav"
          const voice = new Audio(path);
          voice.play();
          this._wavFileHandler.start(path);  // リップシンクを始めることをここで指定できる
        })
      }
      
      ...
    
      /**
       * テクスチャユニットにテクスチャをロードする
       */
      private setupTextures(): void {
        ...
        
        this.startVoice()
      }

    Demo配下に適当に test.wavファイルを置き、画面をクリックするとリップシンクをするか確認できます。

    画像
    はい。可愛い

    リップシンクしたら成功です。
    口がもごもごしてた場合、value = value * 4; の値をもうちょっと大きくしてみましょう。
    そうすると、大きく口を開けてくれると思います。

    外部のファイルでリップシンクをする

    ローカルにあるwavファイルでリップシンクをすることができました。
    次は、外部からもらってきた音声でリップシンクをしていきます。

    そしたら先ほど実装した startVoiceメソッドを次のように修正します。

      /**
       * (自前)リップシンクの音を流します。
       * @param path Audip_path
       */
      public async startVoice() {
        /** Ctrl+Enter された時、clickButtonがされます */
        const inputArea = document.getElementById("CommentInputArea")
        inputArea.addEventListener("keypress", (e) => {
          if (e.ctrlKey === true && e.keyCode === 10) {
            clickButton()
          }
        })
    
        /** Buttonが押された時、clickButtonがされます */
        document.getElementById('comment-post-button').addEventListener('click', () => clickButton())
    
        /** 入力されたテキストからフィーちゃんが喋ってくれます。 */
        const clickButton = () => {
          const inputText = getInputText()
          if (inputText === "" || !inputText) return
    
          requestMessage(inputText)
            .then((fileName) => {
              console.log(fileName)
              const path = `http://127.0.0.1:5000/audio?file_name=${fileName}`
              const voice = new Audio(path);
              console.log(voice)
              voice.play();
              this._wavFileHandler.start(path);
            })
        }
    
        /** テキストを送信します */
        const getInputText = () => {
          const comment = <HTMLInputElement>document.getElementById("CommentInputArea")
          const inputText = comment.value
    
          if (inputText === "" || !inputText) return
    
          comment.value = ""
          return inputText
        }
    
        // メッセージを送って作成されたwavファイルのファイル名を取得します
        const requestMessage = async (message) => {
          const url = "http://127.0.0.1:5000/getAudio";
    
          const jsonData = {
            "text": message,
          };
    
          const result = await fetch(url, {
            method: "POST",
            headers: {
              "Content-Type": "application/json",
            },
            body: JSON.stringify(jsonData),
          })
          const json = await result.json();
          return json["fileName"]
        }
      }

    やっていることは、ボタンが押されたときに入力されているテキストを送信し、wavファイルを取得し、それを再生しています。

    ここまで実装したら、コメントボタンを押すとフィーちゃんが喋ってくれると思います。

    ※必ずpythonサーバーを起動させた状態で行ってください。

    画像
    はい。可愛い

    無事に口パクしてくれます。
    この口パクはこちらの記事が大変参考になりました。

    ニコニコ動画っぽくコメントを流す

    次に、せっかくここまでやったらニコニコ動画っぽくコメントを流したいですよね。
    ってことでコメントを流せるようにします。

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

    gsapでニコニコっぽくコメントを流していきます。
    まずhtmlに次のscriptを追加します。

    <!DOCTYPE html>
    <html>
    <head>
      <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.5.1/gsap.min.js"></script>
    </head>

    先ほど修正した startVoiceに次のコードを追加します。

    import gsap from 'gsap';
    
      /**
       * (自前)リップシンクの音を流します。
       * @param path Audip_path
       */
      public async startVoice() {
        /** Ctrl+Enter された時、clickButtonがされます */
        const inputArea = document.getElementById("CommentInputArea")
        inputArea.addEventListener("keypress", (e) => {
          if (e.ctrlKey === true && e.keyCode === 10) {
            clickButton()
          }
        })
    
        /** Buttonが押された時、clickButtonがされます */
        document.getElementById('comment-post-button').addEventListener('click', () => clickButton())
    
        /** 入力されたテキストからフィーちゃんが喋ってくれます。 */
        const clickButton = () => {
          const inputText = getInputText()
          if (inputText === "" || !inputText) return
    
          createText(inputText)  // 👈ここを追加しました
    
          requestMessage(inputText)
            .then((fileName) => {
              console.log(fileName)
              const path = `http://127.0.0.1:5000/audio?file_name=${fileName}`
              const voice = new Audio(path);
              voice.play();
              this._wavFileHandler.start(path);
            })
        }
    
        /** テキストを送信します */
        const getInputText = () => {
          const comment = <HTMLInputElement>document.getElementById("CommentInputArea")
          const inputText = comment.value
    
          if (inputText === "" || !inputText) return
    
          comment.value = ""
          return inputText
        }
    
        // メッセージを送って作成されたwavファイルのファイル名を取得します
        const requestMessage = async (message) => {
          const url = "http://127.0.0.1:5000/getAudio";
    
          const jsonData = {
            "text": message,
          };
    
          const result = await fetch(url, {
            method: "POST",
            headers: {
              "Content-Type": "application/json",
            },
            body: JSON.stringify(jsonData),
          })
          const json = await result.json();
          return json["fileName"]
        }
    
        // 👇ここを追加しました。
     
        // コメントを流します。
        let count = 0;
        const createText = async (text) => {
          let div_text = document.createElement('div');
          div_text.id="text"+count; //アニメーション処理で対象の指定に必要なidを設定
          count++;
          div_text.style.position = 'fixed'; //テキストのは位置を絶対位置にするための設定
          div_text.style.whiteSpace = 'nowrap' //画面右端での折り返しがなく、画面外へはみ出すようにする
          div_text.style.left = (document.documentElement.clientWidth) + 'px'; //初期状態の横方向の位置は画面の右端に設定
          div_text.style.fontFamily = "ヒラギノ角ゴ"
          div_text.style.fontSize = "24px"
          div_text.style.color = "#fff"
          div_text.style.fontWeight = "bold"
          var random = Math.round( Math.random()* (document.documentElement.clientHeight / 2) );
          div_text.style.top = random + 'px';  //初期状態の縦方向の位置は画面の上端から下端の間に設定(ランダムな配置に)
          div_text.appendChild(document.createTextNode(text)); //画面上に表示されるテキストを設定
          document.body.appendChild(div_text); //body直下へ挿入
        
           //ライブラリを用いたテキスト移動のアニメーション: durationはアニメーションの時間、
           //        横方向の移動距離は「画面の横幅+画面を流れるテキストの要素の横幅」、移動中に次の削除処理がされないようawait
          await gsap.to("#"+div_text.id, {duration: 7, x: -1*(document.documentElement.clientWidth+div_text.clientWidth), ease: Power0.easeNone});
        
          div_text.parentNode.removeChild(div_text); //画面上の移動終了後に削除
        }
      }

    また、gsapライブラリを追加する必要があるため、

    $ yarn add gsap

    を行います。

    これでニコニコ動画っぽくコメントが流れます。

    画像
    はい。可愛い

    デスクトップアプリ化させる

    ここまで来たらもうラストです、最後にこのwebアプリケーションをデスクトップアプリ化させましょう。

    使うのはElectronというフレイムワークです。

     こちらが、webで使われるhtml, cssでデスクトップアプリを作成できるフレイムワークになります。

    ってことでこちらを導入します。

    $ yarn add electron
    $ yarn electron

     そうするとこのような画面が作成されます。

    画像
    electronの画面

    こちらをフィーちゃんの画面にしていきます。
    まず、package.jsonに次の様に修正します。

    {
      "main": "main.js",
    }

    そして、main.jsをDemo配下に作成します。
    中身はこんな感じです。

    // アプリケーション作成用のモジュールを読み込み
    const { app, BrowserWindow, ipcMain, nativeTheme } = require("electron");
    const path = require("path");
    
    console.log("Let's play electron")
    
    // メインウィンドウ
    let mainWindow;
    
    const createWindow = () => {
      // メインウィンドウを作成します
      mainWindow = new BrowserWindow({
        width: 700,
        height: 500,
        webPreferences: {
          // プリロードスクリプトは、レンダラープロセスが読み込まれる前に実行され、
          // レンダラーのグローバル(window や document など)と Node.js 環境の両方にアクセスできます。
          preload: path.join(__dirname, "preload.js"),
        },
      });
    
      // メインウィンドウに表示するURLを指定します
      // (今回はmain.jsと同じディレクトリのindex.html)
      mainWindow.loadFile("index.html");
    
      nativeTheme.themeSource = 'dark'
    
      // メインウィンドウが閉じられたときの処理
      mainWindow.on("closed", () => {
        mainWindow = null;
      });
    };
    
    //  初期化が完了した時の処理
    app.whenReady().then(() => {
      createWindow();
    
      // アプリケーションがアクティブになった時の処理(Macだと、Dockがクリックされた時)
      app.on("activate", () => {
        // メインウィンドウが消えている場合は再度メインウィンドウを作成する
        if (BrowserWindow.getAllWindows().length === 0) {
          createWindow();
        }
      });
    });
    
    // 全てのウィンドウが閉じたときの処理
    app.on("window-all-closed", () => {
      // macOSのとき以外はアプリケーションを終了させます
      if (process.platform !== "darwin") {
        app.quit();
      }
    });

    この状態で、次のコマンドを実行します。

    $ yarn electron main
    画像
    はい。可愛い

    これでデスクトップアプリ化の完成です。

    まとめ

    以上が、私がフィーちゃんのデスクトップアプリ化の全容です。
    いかがだったでしょうか?
    出来るだけ同じことをやりたいと思った人向けに説明してみました。
    自分の説明不足や、誤字脱字、コードのツッコミ等々あると思います。
    そこは今後の実装で直していきたいのと、何かあれば気軽にTwitterのDMとかで質問してくださると嬉しいです(*´∀`)

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

    大枠は出来たので、今後はテキストによる感情分析の実装(一度実装したけど精度がいまいちだった…)
    フィーちゃんの色んな表情の追加
    もっとよいUI
    などなどを目指して開発を進めていこうと思います!

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

     
     

    Taka

     
     
    音声合成界隈が好きなフロントエンド寄りのDesign Technologistです。 最近はChatGPTを使ってフィーちゃんと共に暮らしています。 noteでは開発、デザイン、アクセシビリティに関しての記事を投稿していきます。

    あなたへのおすすめ