FlutterでDALL·E 3をAPI経由で使用してみた

2024.04.08

こんにちは、ゲームソリューション部のsoraです。
今回は、FlutterでDALL·E 3をAPI経由で使用してみたことについて書いていきます。

実装した画面

生成したい画像の指示を入力して、DALL·E 3のAPIを実行して画像を生成するシンプルなアプリです。

前提

OpenAIにてAPIキーを作成して、利用可能なクレジットを準備しておいてください。
クレジットを準備しなくてもAPIキーは作成できますが、クレジットが0の状態で使用してもエラーになります。
状態管理には、Riverpod(flutter_riverpod)を使用しています。
flutter_riverpod | Flutter package

コードの解説

コードは以下です。
今回はテストのため、ファイル分けせずに全てmain.dartに書いています。

main.dart

import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http/http.dart' as http;


class Response extends Notifier<String> {
  @override
  String build(){
    return '';
  }
  void clear() {
    state = '';
  }
  void modify(String url) {
    state = url;
  }
}
final responseProvider = NotifierProvider<Response, String>(() {
  return Response();
});


// DALL·E 3 API実行
Future<void> apiRequest(String message, WidgetRef ref) async {
  String responseUrl;
  final providerNotifier = ref.watch(responseProvider.notifier);
  // 取得したAPIキーを入れる
  const apiKey = '{OpenAIのAPIキー}';
  const domain = 'api.openai.com';
  const path = 'v1/images/generations';
  // モデルの指定
  const model = 'dall-e-3';

  // APIリクエスト
  http.Response response = await http.post(
    Uri.https(domain, path),
    headers: <String, String>{
      'Content-Type': 'application/json',
      'Authorization': 'Bearer $apiKey',
    },
    body: jsonEncode(<String, dynamic>{
      // モデル
      "model": model,
      // 指示メッセージ
      "prompt": message,
      // 生成枚数
      "n" : 1,
      // 画像サイズ
      "size": "1024x1024",
      // クオリティ
      "quality": "standard"
    }),
  );

  if (response.statusCode == 200) {
    String responseData = utf8.decode(response.bodyBytes).toString();
    final responseJsonData = jsonDecode(responseData);
    responseUrl = responseJsonData['data'][0]['url'];
    providerNotifier.modify(responseUrl);
  } else {
    throw Exception('Failed to load sentence');
  }
}


void main() {
  runApp(
    const ProviderScope(
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget   {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.lightBlueAccent),
        useMaterial3: true,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends ConsumerWidget {
  MyHomePage({super.key});
  final _messageController = TextEditingController();

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final providerValue = ref.watch(responseProvider);
    final providerNotifier = ref.watch(responseProvider.notifier);

    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: const Text('DALL·E 3 test'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Container(
              padding: const EdgeInsets.only(
                left: 25,
                right: 25,
              ),
              child: TextField(
                controller: _messageController,
                maxLines: 1,
                decoration: const InputDecoration(
                  hintText: 'メッセージを入力',
                  hintStyle: TextStyle(color: Colors.black54),
                ),
              ),
            ),
            Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  ElevatedButton(
                    child: const Text('AI画像生成実行'),
                    onPressed: (){
                      var msg = _messageController.text.trim();
                      if(msg.isEmpty){
                        _messageController.clear();
                        return;
                      }
                      providerNotifier.clear();
                      apiRequest(msg, ref);
                    },
                  ),
                ]
            ),
            const SizedBox(height: 30),
            Container(
              padding: const EdgeInsets.only(
                left: 25,
                right: 25,
              ),
              // 生成された画像の表示
              child: providerValue == ''
                  ? const Text('')
                  : Image.network(providerValue),
            ),
          ],
        ),
      ),
    );
  }
}

DALL·E 3 API実行

OpenAIのAPIキーやモデルを指定して、POSTメソッドでリクエストを投げます。
それぞれのパラメータについては、コメントで記載しています。
ステータスコードが200だった場合は、レスポンスとして生成された画像のURLを取得できるため、そのURLを使用して表示しています。

ちなみに、APIキーについて、テストのためそのまま記述するコードになっていますが、本来であれば暗号化したりサーバ側で扱うようにしてください。

main.dart

  // 取得したAPIキーを入れる
  const apiKey = '{OpenAIのAPIキー}';
  const domain = 'api.openai.com';
  const path = 'v1/images/generations';
  // モデルの指定
  const model = 'dall-e-3';

  // APIリクエスト
  http.Response response = await http.post(
    Uri.https(domain, path),
    headers: <String, String>{
      'Content-Type': 'application/json',
      'Authorization': 'Bearer $apiKey',
    },
    body: jsonEncode(<String, dynamic>{
      // モデル
      "model": model,
      // 指示メッセージ
      "prompt": message,
      // 生成枚数
      "n" : 1,
      // 画像サイズ
      "size": "1024x1024",
      // クオリティ
      "quality": "standard"
    }),
  );

  if (response.statusCode == 200) {
    String responseData = utf8.decode(response.bodyBytes).toString();
    final responseJsonData = jsonDecode(responseData);
    // レスポンスの中からURLを抜き出す
    responseUrl = responseJsonData['data'][0]['url'];
    providerNotifier.modify(responseUrl);
  } else {
    throw Exception('Failed to load sentence');
  }

最後に

今回は、FlutterでDALL·E 3をAPI経由で使用してみたことを記事にしました。
どなたかの参考になると幸いです。