メむンコンテンツぞスキップ
芋出し画像

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

    こんばんは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);  //衚瀺サむズを倍に倉曎
              projection.translate(0, -2.5);  
            } else {
              // projection.scale(height / width, 1.0);
              projection.scale(2.0, width / height * 2.0);  //衚瀺サむズを倍に倉曎
              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では開発、デザむン、アクセシビリティに関しおの蚘事を投皿しおいきたす。

    あなたぞのおすすめ