stability.aiのREST APIを使って簡単に画像生成アプリを作ってみた

# はじめに

前回「[Stable Diffusion APIを使って簡単に画像生成アプリを作ってみた](https://qiita.com/razuma/items/6af94f8d0edf67a0bffe)」と言う記事を書きましたが、今回は本家のstability.aiのREST APIを使って画像生成アプリをつくりました。

# 利用したサービス
Stable Diffusion本家のStability AI
https://platform.stability.ai/
画像１枚作成ごとにコストがかかる。最初に少しだけ無料でトークンをもらえます、その後は自分で追加していく方式。設定するパラメータによってかかるコストが違います。

# 今回使用したAPI
v1/generationのtext-to-image
https://platform.stability.ai/docs/api-reference#tag/v1generation

設定できる各パラメータについては以下

height: 画像の高さ、設定できる値は使用できるモデルによって決まっている
width: 画像の幅、設定できる値は使用できるモデルによって決まっている
text_prompts: オブジェクトの配列でtextにプロンプトを設定、weightが正だとPositive、負だとNegativeのプロンプトになる
cfg_scale: プロンプトに対してどれだけ忠実に生成するか
clip_guidance_preset: クリップガイダンスプリセット
sampler: サンプラー
samples: 生成する画像数
seed: シード値、同プロンプトで同じ値にすると同じ画像になる。0か未指定だとランダム。
steps: 画像の品質
style_preset: 画像のスタイルを設定（アニメ、フォトなど）
extras: 開発中、実験的な機能のために使われる

# 実際のコード

一応、参考までに呼び出しのコードを記載、HTTPクライアントはAndroidではokhttp、iOSではAlamofireを使用しています。

```kotlin
// Android okhttpの初期化などは省略
val textPrompts = arrayListOf<HashMap<String, Any>>()
val positivePrompt : HashMap<String, Any> = HashMap()
positivePrompt.put("text", positiveText)
positivePrompt.put("weight", 1)
textPrompts.add(positivePrompt)

val negativePrompt: HashMap<String, Any> = HashMap()
negativePrompt.put("text", negativeText)
negativePrompt.put("weight", -1)
textPrompts.add(negativePrompt)

val gson = Gson()
val paramMap : HashMap<String, Any> = HashMap()
paramMap.put("steps", steps)
paramMap.put("width", width)
paramMap.put("height", height)
paramMap.put("seed", 0)
paramMap.put("cfg_scale", guidanceScale)
paramMap.put("samples", 1)
paramMap.put("text_prompts", textPrompts)
paramMap.put("style_preset", stylePreset)

val paramJsonData = gson.toJson(paramMap)
val getImageApiURL = "https://api.stability.ai/v1/generation/stable-diffusion-xl-1024-v1-0/text-to-image"
val apiKey = "APIキー"
val JSON_MEDIA = "application/json; charset=utf-8".toMediaType()

val request = Request.Builder()
            .url(getImageApiURL)
            .addHeader("Content-Type", "application/json")
            .addHeader("Authorization", "Bearer $apiKey")
            .post(paramJsonData.toRequestBody(JSON_MEDIA))
            .build()

okHttpClient.newCall(request).enqueue(object : Callback {
    override fun onFailure(call: Call, e: IOException) {
        // エラー処理
    }

    override fun onResponse(call: Call, response: Response) {
        val result = if (response.isSuccessful) {
            response.body?.string()
        } else {
            // エラー処理
            return
        }

        val getResultData:GetImageResponse?
        try {
            getResultData = Gson().fromJson(result, GetImageResponse::class.java)
        }catch (e:Exception){
            // エラー処理
            return
        }
    
        val base64Image = getResultData.artifacts[0].base64
        val decodedString: ByteArray = Base64.decode(base64Image, Base64.DEFAULT)
        // 生成された画像
        val generateImage = BitmapFactory.decodeByteArray(decodedString, 0, decodedString.size)
    }
})

///
data class GetImageResponse(
    val artifacts: Array<GetImageArtifact>
)

data class GetImageArtifact(
    val base64: String,
    val finishReason: String
)

```

```swift
// iOS
var textPrompts:[Parameters] = []
let positivePrompt: Parameters = [
    "text": positiveText,
    "weight":1,
]
textPrompts.append(positivePrompt)

let negativePrompt: Parameters = [
    "text": negativeText,
    "weight":-1,
]
textPrompts.append(negativePrompt)
        
let requestUrl = "https://api.stability.ai/v1/generation/stable-diffusion-xl-1024-v1-0/text-to-image"
let apiKey = "APIキー"
        
let param: Parameters = [
    "steps": steps,
    "width":width,
    "height":height,
    "seed":0,
    "cfg_scale":guidanceScale,
    "samples":1,
    "style_preset":stylePreset,
    "text_prompts":textPrompts
]

let headers: HTTPHeaders = [
    "Content-Type":"application/json",
    "Authorization": "Bearer " + apiKey
]

AF.request(requestUrl,method: .post,parameters: param,encoding: JSONEncoding.default,headers: headers)
            .responseData { response in
                switch response.result {
                case .success(let data):                                        
                    let decoder: JSONDecoder = JSONDecoder()
                    do {
                        let getImageRes: GetImageResponse = try decoder.decode(GetImageResponse.self, from: (response.data)!)
                        let artifact = getImageRes.artifacts[0]
                        // 生成された画像
                        let generateImage = self.convertBase64ToImage(artifact.base64)
                    } catch {
                        // エラー
                    }
                case .failure(let error):
                    // エラー
                }
            }

/// 
struct GetImageResponse:Codable {
    let artifacts:[GetImageArtifact]
}

struct GetImageArtifact:Codable {
    let base64:String
    let finishReason:String
}

private func convertBase64ToImage(_ base64String: String) -> UIImage? {
    guard let imageData = Data(base64Encoded: base64String) else { return nil }
    return UIImage(data: imageData)
}
```

# 感想や前回のとの違い

前回使ったサービスではユーザ作成モデルなど色々なモデルが使えましたが
こちら側では「Stable Diffusion XL 1.0」と「Stable Diffusion 1.6」の２種類となっており、Styleを設定して画像のテイストを変えることができる感じでした。

使った分だけお金がかかる方式なのでどれぐらいの需要があるか把握するためには良さそう。また先にクレジットを購入する方式なので高額請求などがくることはないのは安心。（クレジットがなくなると生成できなくなってしまうが）

出てくるイラストのクオリティ自体は前回に比べると結構高めなんじゃないかと思いましたが、自由度はそこまでないので色んなモデルを使いたかったり、追加学習させたかったりする場合はやはり自分で実装しなければいけないかなと感じました。

アプリは前回作成したアプリをアップデートして上記のAPIを使うようにアップデートして公開います、興味のある方はお試しください
https://play.google.com/store/apps/details?id=jp.co.togatta.aiart
https://apps.apple.com/jp/app/ai-art-generator-simple/id6464369305

以上です。