FlutterでLive2Dを動かし、Geminiで会話させる!

背景


こんにちは、ヌマです。
VALGOの4月の帰社日では、各人が5〜10分程度の自己紹介LTを行っています。

前回の自己紹介の時間では、自分の好きな食べ物をFlutterのアプリ開発で深堀りしました。

(前回作成したアプリは鶏肉のサッパリ煮アプリ

今回も最近気になるものを組み合わせてFlutterでアプリ開発をしました!

目的


今回も人より本気で準備してLTすると面白いだろうという魂胆です!

最近で話題のLLMを利用したり、Live2Dを利用したりと「新しいものに触れて面白いを増やす」取り組みを勝手にやり、面白いと感じた方を巻き込み、あわよくば社内に布教しようと思っています!!!

要件


  • Androidで動作する
  • 入力時にキーボードが隠れない
  • 表示されているアバターが小さくならない
  • LLMを使いたい
  • 話す内容が長く、難しくならない
  • 送信ボタンは連続して押せない

設計


レイアウト

画面中央でアバターが動き、画面下のテキストボックスに文字を入力して送信ボタンを押すと音声で返答してくれる

イメージ図

使用したもの


  • Flutter
    •   flutter_tts: ^4.0.2
    •   flutter_unity_widget: ^2022.2.1
  • VScode
  • Android Studio
  • Gemini-2.0-flash
  • Unity
  • Live2D Cubism Editor 5.2

環境構築


FlutterとUnity、Android StudioあたりのJavaのバージョンを合わせるのが大変でした。

FlutterとUnityのバージョンさえうまく合えば動作するようになったかなと思います。

以下は参考程度にご参照ください。

Flutter:3.22.2
Unity:2022.3.60f1
kotlin:2.1.10

Flutterに格納するパッケージ
– fuw-2022.2.0.unitypackage
– CubismSdkForUnity-5-r.3.unitypackage

実装


まずはアバターの出力部分に関する実装まで

main.dart

class _HomeState extends State<Home> {
  // 二度押し防止用
  bool canPless = false;
  // UnityWidgetController初期化
  UnityWidgetController? _unityWidgetController;

  @override
  Widget build(BuildContext context) {
    // 入力したテキストを取得するためのコントローラー
    final TextEditingController controller = TextEditingController();

    return Scaffold(
      resizeToAvoidBottomInset: false,
      appBar: AppBar(
        title: Text("neko"),
      ),
      body: 
        Container(
          height: MediaQuery.of(context).size.height,
          child: Stack(
            alignment: Alignment.bottomCenter,
            children: [
              Expanded(
                child: Container(
                  child: UnityWidget(
                    onUnityCreated: onUnityCreated,
                  ),
                ),
              ),

Expandedで限界までchlidの要素を画面全体に映します。
onUnityCreatedという関数を定義し、Unityで出力したアバターなどを立ち上げます。

onUnityCreated

  void onUnityCreated(controller) {
    _unityWidgetController = controller;
  }

簡単にUnityで作成した画面を映し出すことができます。

続いて、入力ボックスから送信ボタンまでは以下になります

main.dart

        Align(
                alignment: Alignment.bottomCenter,
                child: Container(
                  width: MediaQuery.of(context).size.width,
          // ちょうどいい感じにテキストボックスが競りあがる
                  height: 100 + MediaQuery.of(context).viewInsets.bottom * 2,
                  child: Form(
                    child: Row(
                      children: [
                        Flexible(
                          child: TextFormField(
                            controller: controller,
                            decoration: const InputDecoration(
                              labelText: '入力してください',
                            ),
                          ),
                        ),
                        Material(
                          color: const Color.fromARGB(0, 0, 0, 0),
                          child: Ink(
                            child: IconButton(
                                icon: Icon(Icons.send),
                                color: Colors.blue,
                                // 連続でボタンを押せないようにする
                                onPressed: canPless
                                    ? null
                                    : () {
                                        FocusScope.of(context).unfocus();
                                        _getAnswer(controller);
                                      }),
                          ),
                        ),
                      ],
                    ),
                  ),
                ),
              ),
            ],
          ),
        ),
    );
  }

onPressedでボタン押下後の挙動を示している

_getAnswer

  void _getAnswer(controller) async {
    //ボタンを押せないようにする
    setState(() {
      canPless = true;
    });
    // GEMINIを調教
    final chat = widget.model.startChat(history: [
      Content.text(
          "あなたの名前は「○○」です。あなたの第一人称は僕です。敬語は使わないでください。50%の確率で「僕にはわからないよ」と答えてください。"),
      Content.model([TextPart("わかったよ")]),
    ]);
    // GEMINIにリクエストを投げて返ってきた値をStringにする --from
    final message = controller.text;
    final content = Content.text(message);
    final response = await chat.sendMessage(content);
    String answer = response.text.toString();
    // GEMINIにリクエストを投げて返ってきた値をStringにする --to

    // 一文字0.2秒で読み上げる
    final int sleeptime = (answer.length.toDouble() * 0.2).ceil();
    // 長く話すのはあまり好まないので寿限無話せるくらい
    if(answer.length > 140) {answer = "難しくて僕にはわからないよ";}
    // ボタンを押したときに入力したテキストを読み上げる
    widget.tts.speak(answer);

    // 話している間動く--from
    _sendPlayMotionMessage('tateyure');
    await Future.delayed(Duration(seconds: sleeptime));
    // 話し終わったらボタンを押せるようにする
    setState(() {
      canPless = false;
    });
    _sendPlayMotionMessage('noneMotion');
    // 話している間動く--to

    // 満足して揺らす --from
    _sendPlayMotionMessage('yokoyure');
    await Future.delayed(Duration(seconds: sleeptime));
    _sendPlayMotionMessage('noneMotion');
    // 満足して揺らす --to
  }

ここでの処理で、

  • ボタン連続押し防止のbooleanの更新
  • Geminiへリクエストの送信
  • Unityのモーションを実行
  • widget.tts.speakでFlutter_ttsを使って音声出力

Unityで「動かない」というモーションを定義したところ、モーションの切り替えがうまくできたのでその点もやってみてください。

今回作成したアプリの画面キャプチャは以下となります〜!

実際の動画がこちら!

まとめ(感想)


動く、音声で返事させる、LLMを使うといった機能を盛り込んで作成しました!

前回も思いましたが、Flutterのコツは最初に画面構成をどのようにするかを図示することと、コードとそれを見比べることが大事だと感じました!

Javaのバージョンさえ合えば簡単に作成できると思いますので、ぜひお試しください✌︎