1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Flutter + NCMB + OpenAIで画像生成アプリを作る(その3: チャット画面の作成)

Posted at

NCMBでは公式SDKとしてSwift/Objective-C/Kotlin/Java/Unity/JavaScript SDKを用意しています。また、それ以外にもコミュニティSDKとして、非公式ながらFlutter/React Native/Google Apps Script/C#/Ruby/Python/PHPなど幅広い言語向けにSDKが開発されています。

今回はコミュニティSDKの一つ、Flutter/Dart SDKを使ってOpenAIの画像生成APIを用いたAI画像生成アプリを作ってみます。前回は画像生成に関わるスクリプトの処理について解説しましたので、今回はチャット画面を作成します。

コード

今回のコードは NCMBMania/flutter-image-gen にアップロードしてあります。実装時の参考にしてください。

チャット画面について

このファイルは lib/chat.dart として作成しています。チャット画面はflutter_chat_ui | Flutter Packageを用いるので、とても簡単です。

import 'dart:core';

import 'package:flutter/material.dart';
import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
import 'package:flutter_chat_ui/flutter_chat_ui.dart';
import 'package:ncmb/ncmb.dart';
import 'dart:typed_data';
import 'dart:html' as webFile;

class ChatPage extends StatefulWidget {
  const ChatPage({super.key});

  @override
  State<ChatPage> createState() => _ChatPageState();
}

class _ChatPageState extends State<ChatPage> {
  @override
  Widget build(BuildContext context) => Scaffold(
        body: Chat(
          messages: _messages,
          onSendPressed: _handleSendPressed,
          showUserAvatars: false,
          showUserNames: false,
          user: const types.User(
            id: 'user',
          ),
        ),
      );
  // 省略
}

localhost_55761_.png

Dartの実装

以下のコードはすべて lib/chat.dart の _ChatPageState 中に記述します。

必要な変数の定義

まずチャットメッセージ用の変数を定義します。

List<types.Message> _messages = [];

初期化された際の処理

初期化時の処理で、過去のメッセージを取得します。

  @override
  void initState() {
    super.initState();
    _loadMessages();
  }

  void _loadMessages() async {
    // メッセージを取得するクエリを作成
    var query = NCMBQuery('ImageGen');
    // 1日前以降のメッセージを取得
    var date = DateTime.now().subtract(const Duration(days: 1));
    query.greaterThanOrEqualTo('createDate', date);
    query.order('createDate', descending: true); // 古いデータが上に来るようにする
    var ary = (await query.fetchAll()).map((o) => o as NCMBObject).toList();
    // 取得したNCMBObjectのリストをチャットメッセージに変換
    final messages = await Future.wait(ary.map((e) => toMessage(e)));
    // メッセージを更新
    setState(() {
      _messages = messages.expand((l) => l).toList();
    });
  }

toMessage 関数はNCMBObjectをチャットメッセージに変換します。チャットメッセージはプロンプトとしてのテキストメッセージと、レスポンスとしての画像メッセージの2つに分かれます。

  // NCMBObjectをチャットメッセージに変換する関数
  Future<List<types.Message>> toMessage(NCMBObject obj) async {
    var messages = <types.Message>[];
    var message = toTextMessage(obj);
    messages.add(message);
    var imageMessage = await toImageMessage(obj);
    messages.add(imageMessage);
    return messages.reversed.toList();
  }

  // NCMBObjectをTextMessageに変換する関数
  types.TextMessage toTextMessage(NCMBObject obj) {
    return types.TextMessage(
      author: types.User(id: obj.getString('role', defaultValue: 'user')),
      createdAt: obj.getDateTime('createDate').microsecondsSinceEpoch,
      id: "${obj.objectId}-text",
      text: obj.getString('content'),
    );
  }

  // NCMBObjectをImageMessageに変換する関数
  Future<types.ImageMessage> toImageMessage(NCMBObject obj) async {
    // 画像ファイルをダウンロード
    final imageName = obj.getString('image', defaultValue: '');
    final file = await NCMBFile.download(imageName);
    // 画像ファイルを表示するためのURIを作成
    final blob = ByteData.sublistView(file.data);
    final path = webFile.Url.createObjectUrlFromBlob(webFile.Blob([blob]));
    return types.ImageMessage(
      author: types.User(id: obj.getString('role', defaultValue: 'assistant')),
      createdAt: obj.getDateTime('createDate').microsecondsSinceEpoch,
      id: "${obj.objectId}-image",
      name: imageName,
      height: 256,
      width: 256,
      size: file.data.length,
      uri: path,
    );
  }

チャットを送信・保存する処理

チャットメッセージを書き込んで送信ボタンを押すと _handleSendPressed 関数が呼び出されます。

void _handleSendPressed(types.PartialText message) async {
  // 以下はこの中に記述
};

入力テキストを取得

まず入力されたテキストを取得し、そのデータをNCMBのデータストアインスタンスにします。そして、保存後にテキストメッセージを描画します。

// メッセージを保存
var obj = NCMBObject('ImageGen');
obj.set('content', message.text);
obj.set('role', 'user');
await obj.save();
final textMessage = toTextMessage(obj);
_addMessage(textMessage);

OpenAI APIの呼び出し

NCMBのスクリプトを実行します。これでOpenAI APIを呼び出して、結果を受け取ります。

// OpenAIのAPIを呼び出す
var script = NCMBScript('image.js').body('prompt', message.text);
var res = (await script.post()) as Map<String, dynamic>;

結果をデータストアに保存

受け取った結果は objimage に紐付けて保存します。

// OpenAIの返答を保存
obj.set('image', res['fileName'] as String);
await obj.save();

後は画像をチャット画面に表示して完了です。

final imageMessage = await toImageMessage(obj);
_addMessage(imageMessage);

これで完成です。

実行例

以下は実行例です。

neko.mp4-min.gif

全体のコード

lib/chat.dart のコードは以下の通りです。

import 'dart:core';

import 'package:flutter/material.dart';
import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
import 'package:flutter_chat_ui/flutter_chat_ui.dart';
import 'package:ncmb/ncmb.dart';
import 'dart:typed_data';
import 'dart:html' as webFile;

class ChatPage extends StatefulWidget {
  const ChatPage({super.key});

  @override
  State<ChatPage> createState() => _ChatPageState();
}

class _ChatPageState extends State<ChatPage> {
  List<types.Message> _messages = [];

  @override
  void initState() {
    super.initState();
    _loadMessages();
  }

  @override
  Widget build(BuildContext context) => Scaffold(
        body: Chat(
          messages: _messages,
          onSendPressed: _handleSendPressed,
          showUserAvatars: false,
          showUserNames: false,
          user: const types.User(
            id: 'user',
          ),
        ),
      );

  void _addMessage(types.Message message) {
    setState(() {
      _messages.insert(0, message);
    });
  }

  void _handleSendPressed(types.PartialText message) async {
    // メッセージを保存
    var obj = NCMBObject('ImageGen');
    obj.set('content', message.text);
    obj.set('role', 'user');
    await obj.save();
    final textMessage = toTextMessage(obj);
    _addMessage(textMessage);
    // OpenAIのAPIを呼び出す
    var script = NCMBScript('image.js').body('prompt', message.text);
    var res = (await script.post()) as Map<String, dynamic>;
    // OpenAIの返答を保存
    obj.set('image', res['fileName'] as String);
    await obj.save();
    final imageMessage = await toImageMessage(obj);
    _addMessage(imageMessage);
  }

  // NCMBObjectをチャットメッセージに変換する関数
  Future<List<types.Message>> toMessage(NCMBObject obj) async {
    var messages = <types.Message>[];
    var message = toTextMessage(obj);
    messages.add(message);
    var imageMessage = await toImageMessage(obj);
    messages.add(imageMessage);
    return messages.reversed.toList();
  }

  // NCMBObjectをTextMessageに変換する関数
  types.TextMessage toTextMessage(NCMBObject obj) {
    return types.TextMessage(
      author: types.User(id: obj.getString('role', defaultValue: 'user')),
      createdAt: obj.getDateTime('createDate').microsecondsSinceEpoch,
      id: "${obj.objectId}-text",
      text: obj.getString('content'),
    );
  }

  // NCMBObjectをImageMessageに変換する関数
  Future<types.ImageMessage> toImageMessage(NCMBObject obj) async {
    // 画像ファイルをダウンロード
    final imageName = obj.getString('image', defaultValue: '');
    final file = await NCMBFile.download(imageName);
    // 画像ファイルを表示するためのURIを作成
    final blob = ByteData.sublistView(file.data);
    final path = webFile.Url.createObjectUrlFromBlob(webFile.Blob([blob]));
    return types.ImageMessage(
      author: types.User(id: obj.getString('role', defaultValue: 'assistant')),
      createdAt: obj.getDateTime('createDate').microsecondsSinceEpoch,
      id: "${obj.objectId}-image",
      name: imageName,
      height: 256,
      width: 256,
      size: file.data.length,
      uri: path,
    );
  }

  void _loadMessages() async {
    // メッセージを取得するクエリを作成
    var query = NCMBQuery('ImageGen');
    // 1日前以降のメッセージを取得
    var date = DateTime.now().subtract(const Duration(days: 1));
    // query.greaterThanOrEqualTo('createDate', date);
    query.order('createDate', descending: true); // 古いデータが上に来るようにする
    var ary = (await query.fetchAll()).map((o) => o as NCMBObject).toList();
    // 取得したNCMBObjectのリストをTextMessageに変換
    final messages = await Future.wait(ary.map((e) => toMessage(e)));
    // メッセージを更新
    setState(() {
      _messages = messages.expand((l) => l).toList();
    });
  }
}

まとめ

今回のAI画像生成アプリではNCMBの以下の機能を利用しました。

  • データストア
    • データ保存
    • データ検索
  • ファイルストア
    • ファイルアップロード
    • ファイルダウンロード
  • スクリプト
    • OpenAI APIの実行

NCMBには他にも認証、プッシュ通知などの機能があります。ぜひそれらの機能も利用してアプリを開発してください。

mBaaSでサーバー開発不要! | ニフクラ mobile backend

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?