JINSテックブログ
🍒

テイスティングノート管理アプリ作った②

に公開

はじめに

JINSに入社してもうすぐ1年!
最近は他部署とお仕事で関わる機会が増え、社内での認知度もだいぶ上がってきたかな?と思います。
ITデジタル部のいしざき(@ishizak1111)です。

今回も趣味でアプリを開発した話をします。
前回はFletというPythonからモバイルアプリを作成できるフレームワークを使って実装しましたが、色々あってFlutterにしました。

動機

私はウイスキー🥃やコーヒー☕が好きでよく飲むのですが、数日後には「どんな味だったっけ?」
と忘れてしまいがち。体調や飲み方、飲んだ回数によっても拾える味が変わってくるので一期一会を感じます。
手軽に写真とテイスティングノートを一緒に管理したい!というのが今回の開発の主な動機です。(前回の記事から引用)

ちなみにウイスキーはクライヌリッシュ🐱が好きです。
熟成させた樽の種類によっては青リンゴにも、焼きリンゴにも感じられるウイスキーだと思います。

スモーキーなウイスキーも好きですが、麦っぽさ残る甘いウイスキーがマイブーム。
夜でも食べられるスイーツ感🍰が、『仕事終わりのご褒美』を演出してくれている気がします。

使ったフレームワークなど

前回実現できなかった
『アプリ上でカメラを開いて画像登録』を実現するために、重い腰を上げてFlutterにしました。

  • Flutter
  • SQLite (テイスティングノート保存用DB)
  • Pixel6 (デバッグ用Android端末)

できたこと

前回同様、最初の印象と最後の印象がメモできるようにしました。
テイスティングノートはSQLiteに登録されます。
また、今回はカメラを開いて写真も保存できます!👏👏

表示する画面も工夫しました。
画像上に情報を載せることで、スクショした画像をそのままインスタにアップロードできます。
(モバイル担当のJINS社員にオーバーレイできることを教えてもらいました。感謝。)

コード(写真保存部分一部抜粋)

Future<void> _pickImageFromCamera() async {

    try {
      final XFile? image = await _picker.pickImage(source: ImageSource.camera);
      if (image != null) {
        setState(() {
          _selectedImagePath = image.path;
        });
      }
    } catch (e) {
      ScaffoldMessenger.of(
        context,
      ).showSnackBar(SnackBar(content: Text(Failed to pick image from camera!: $e')));
    }
  }


void _showImagePickerDialog() {

    showDialog(
      context: context,
      builder: (BuildContext context) {
        return AlertDialog(
          title: const Text('写真を登録’),
          content: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              ListTile(
                leading: const Icon(Icons.camera_alt),
                title: const Text('カメラ'),
                onTap: () {
                  Navigator.of(context).pop();
                  _pickImageFromCamera();
                },
              ),
              ListTile(
                leading: const Icon(Icons.photo_library),
                title: const Text('ギャラリー'),
                onTap: () {
                  Navigator.of(context).pop();
                  _pickImageFromGallery();
                },
              ),
            ],
          ),
        );
      },
    );
  }

さて、このアプリを実際に使ってみたところ、みんなでお酒を飲んでいる場ではそもそも文字を打つのが面倒なことに気がつきました。

ということで、まだ開発段階ですが写真からタイトルを自動で入力してくれる機能を
AIの力も借りて作成しました。

class GeminiService {
  static String get _apiKey {
    final apiKey = dotenv.env['GEMINI_API_KEY'];
    return apiKey;
  }

  static GenerativeModel? _model;

  static GenerativeModel get model {
    _model = GenerativeModel(model: 'gemini-1.5-flash', apiKey: _apiKey);
    return _model;
  }

  static Future<String?> analyzeImageForDrinkName(String imagePath) async {
      final imageFile = File(imagePath);
      final imageBytes = await imageFile.readAsBytes();
      const prompt = '''
この画像から、コーヒー豆またはウイスキーの商品名を推測してください。

以下の条件に従って回答してください:
1. 画像にパッケージやラベル、ボトルが写っている場合、コーヒー豆の産地と銘柄、またはウイスキーの商品名や銘柄名を推測してください
2. わからない場合は「不明」と返してください
3. 回答は商品名のみを簡潔に返してください(説明文は不要)

例:
- グァテマラ
- モカハラー
- マッカラン 12年
- 不明
''';

      final content = [
        Content.multi([TextPart(prompt), DataPart('image/jpeg', imageBytes)]),
      ];
      final response = await model.generateContent(content);
      final text = response.text?.trim();
      return text
}

デバッグ中はパッケージを読み込んで、ちょっと微妙とはいえタイトルを自動生成してくれました。
が、実機で動かすとどうも上手く読めないっぽい。。。

プロンプトの問題か、API_KEYの渡し方に問題があるのか。(調査中)

サントリー角も分からないようじゃ無理か。ラベル名はね、入れとかないと。

できなかったこと

アップデート時のデータ引き継ぎ

アプリのSQliteに保存しているので、アプデしてアプリを再インストールするとノートが全て消える。

クラウドストレージに変える or エクスポートインポート機能を作成することで解決しそう。

一定期間が過ぎると画像が消えてしまう…

原因調査中。

アプリ > カメラから画像を登録した際、デバイス(Photos)には画像が保存されない。

そのため画像は一時メモリ的なところに保存されており、何かしらのタイミングで消えてしまうのでは無いかと予想。

カメラから画像登録する際も、一旦デバイスに保存してからそのパスを参照するように変更してみたい。

最後に

自前のテイスティングデータが揃ってきたら、統計とって分析したりクラスタリングしたりレコメンデーション機能作ったりしたいです。

どうやらPythonで作ったモデルをFlutter内で使えるっぽい。絶対楽しい...

ということで、このシリーズもうちょっと続きそうです。
ご覧いただきありがとうございました!

参考

JINSテックブログ
JINSテックブログ

Discussion