Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Androidifyから学ぶFirebase AI Logic SDKの使い方

Androidifyから学ぶFirebase AI Logic SDKの使い方

Avatar for Ryosuke Horie

Ryosuke Horie

September 04, 2025
Tweet

More Decks by Ryosuke Horie

Other Decks in Programming

Transcript

  1. 2025年6月27日 DroidKaigi.collect { #20@Tokyo } 株式会社ZOZO
 IT統括本部 技術戦略部 CTOブロック
 テックリード


    堀江 亮介 Copyright © ZOZO, Inc. 1 Androidifyから学ぶ Firebase AI Logic SDKの使い方
  2. © ZOZO, Inc. 株式会社ZOZO IT統括本部 技術戦略部 CTOブロック テックリード 堀江 亮介

    2018年 M&Aに伴いZOZOテクノロジーズに入社 ZOZOグループの組織再編に伴い転籍し現職 組織横断での課題解決と採用および技術広報業務を担当 Google I/Oは2018年以来の現地参加 3
  3. © ZOZO, Inc. https://zozo.jp/ 6 • ファッションEC • 1,600以上のショップ、9,000以上のブランドの取り扱い •

    常時107万点以上の商品アイテム数と毎日平均2,700点以上の新着 商品を掲載(2025年3月末時点) • ブランド古着のファッションゾーン「ZOZOUSED」や コスメ専門モール「ZOZOCOSME」、シューズ専門ゾーン 「ZOZOSHOES」、ラグジュアリー&デザイナーズゾーン 「ZOZOVILLA」を展開 • 即日配送サービス • ギフトラッピングサービス • ツケ払い など
  4. © ZOZO, Inc. https://wear.jp/ 7 • あなたの「似合う」が探せるファッションコーディネートアプリ • 1,800万ダウンロード突破、コーディネート投稿総数は1,400万 件以上(2025年3月末時点)

    • コーディネートや最新トレンド、メイクなど豊富なファッション 情報をチェック • AIを活用したファッションジャンル診断や、フルメイクをARで試 せる「WEARお試しメイク」を提供 • コーディネート着用アイテムを公式サイトで購入可能 • WEAR公認の人気ユーザーをWEARISTAと認定。モデル・タレン ト・デザイナー・インフルエンサーといった各界著名人も参加
  5. © ZOZO, Inc. 8 1. Firebase AI Logic SDKの概要 2.

    Firebase AI Logic SDKのセットアップ 3. Gemini APIとImagenの実装方法 4. デモ 5. まとめ 目次
  6. © ZOZO, Inc. 10 Firebase AI Logic SDKとは • Googleの生成AIをAndroidアプリに簡単に統合

    ◦ 🚀 GeminiとImagenへの統一的なアクセス ◦ 🔒 Firebase AppCheckによるセキュアなAPI保護 ◦ ⚙ Remote Configでの動的なモデル設定 ◦ 📊 費用・使用状況のモニタリングの統合 ◦ 🛡 組み込みのセーフティ設定とコンテンツフィルタリング https://firebase.google.com/docs/ai-logic
  7. © ZOZO, Inc. 11 なぜFirebase AI Logic SDKを使うのか 直接API利用 Firebase

    AI Logic SDK APIキーをクライアントコードに埋め込む必要あり APIキーはサーバー側で安全に管理 使用量追跡・コスト管理を別途実装 Firebase Consoleに統合 単一のAPIプロバイダーに固定 Gemini Developer API/Vertex AIを選択可能 ファイル管理を別途実装 Cloud Storage for Firebaseと統合
  8. © ZOZO, Inc. • GeminiとImagenを活用 • Material 3 ExpressiveによるUI実装 •

    Navigation 3によるナビゲーション実装 • CameraXによるカスタムカメラ実装 • Media3 Composeで動画再生 • ML Kitとの統合でリアルタイムポーズ検出 • オープンソース 13 Androidifyの特徴
  9. © ZOZO, Inc. 16 Firebase AI Logicの設定とアプリへの接続 • Gemini Developer

    API ◦ 無料の Spark 料金プランで利用可能 • Vertex AI Gemini API ◦ 従量課金制の Blaze 料金プランが必要 ◦ Androidifyはこちらを利用
  10. © ZOZO, Inc. 17 Firebase AI Logic SDKを追加 dependencies {

    implementation(platform("com.google.firebase:firebase-bom:33.15.0")) implementation("com.google.firebase:firebase-ai") }
  11. © ZOZO, Inc. • 不正なクライアントからAPIを保護 • Firebase AI Logic SDKがプロキシゲートウェイを提供

    • AppCheckと統合され生成AIモデルへのAPIリクエストを保護 18 AppCheckの有効化
  12. © ZOZO, Inc. • AppCheckの初期化 19 AppCheckの有効化 Firebase.initialize(context = this)

    Firebase.appCheck.installAppCheckProviderFactory( PlayIntegrityAppCheckProviderFactory.getInstance(), ) https://firebase.google.com/docs/app-check/android/play-integrity-provider
  13. © ZOZO, Inc. 23 Firebase AI SDKの初期化 // FirebaseAiDataSourceImpl.kt @Singleton

    class FirebaseAiDataSourceImpl @Inject constructor( private val remoteConfigDataSource: RemoteConfigDataSource, ) : FirebaseAiDataSource { // Geminiモデルの初期化 private fun createGenerativeTextModel( jsonSchema: Schema, temperature: Float? = null ): GenerativeModel { return Firebase.ai(backend = GenerativeBackend.vertexAI()).generativeModel(...) } // Imagenモデルの初期化 private fun createGenerativeImageModel(): ImagenModel { return Firebase.ai(backend = GenerativeBackend.vertexAI()).imagenModel(...) } }
  14. © ZOZO, Inc. 24 Firebase AI SDKの初期化 private fun createGenerativeTextModel(

    jsonSchema: Schema, temperature: Float? = null ): GenerativeModel { return Firebase.ai(backend = GenerativeBackend.vertexAI()).generativeModel( modelName = remoteConfigDataSource.textModelName(),// Remote Configから取得 generationConfig = generationConfig { responseMimeType = "application/json" responseSchema = jsonSchema // 構造化されたレスポンス this.temperature = temperature }, safetySettings = listOf( SafetySetting(HarmCategory.HARASSMENT, HarmBlockThreshold.LOW_AND_ABOVE), SafetySetting(HarmCategory.HATE_SPEECH, HarmBlockThreshold.LOW_AND_ABOVE), SafetySetting(HarmCategory.SEXUALLY_EXPLICIT, HarmBlockThreshold.LOW_AND_ABOVE), SafetySetting(HarmCategory.DANGEROUS_CONTENT, HarmBlockThreshold.LOW_AND_ABOVE), SafetySetting(HarmCategory.CIVIC_INTEGRITY, HarmBlockThreshold.LOW_AND_ABOVE), ), ) }
  15. © ZOZO, Inc. 25 Firebase AI SDKの初期化 private fun createGenerativeImageModel():

    ImagenModel { return Firebase.ai(backend = GenerativeBackend.vertexAI()).imagenModel( remoteConfigDataSource.imageModelName(), safetySettings = ImagenSafetySettings( safetyFilterLevel = ImagenSafetyFilterLevel.BLOCK_LOW_AND_ABOVE, personFilterLevel = ImagenPersonFilterLevel.ALLOW_ADULT, ), ) }
  16. © ZOZO, Inc. 30 // CreationViewModel#startClicked fun startClicked() { imageGenerationJob?.cancel()

    imageGenerationJob = viewModelScope.launch { if (internetConnectivityManager.isInternetAvailable()) { try { uiState.update {...} val bitmap = when (uiState.value.selectedPromptOption) { PromptType.PHOTO -> { val selectedImage = _uiState.value.imageUri if (selectedImage == null) {...} else { imageGenerationRepository.generateFromImage( fileProvider.copyToInternalStorage(selectedImage), _uiState.value.botColor.getVerboseDescription(), ) } } PromptType.TEXT -> imageGenerationRepository.generateFromDescription(...) } _uiState.update {...} } catch (e: Exception) {...} } else {...} } }
  17. © ZOZO, Inc. 31 // CreationViewModel#startClicked fun startClicked() { imageGenerationJob?.cancel()

    imageGenerationJob = viewModelScope.launch { if (internetConnectivityManager.isInternetAvailable()) { try { _uiState.update {...} val bitmap = when (uiState.value.selectedPromptOption) { PromptType.PHOTO -> { val selectedImage = _uiState.value.imageUri if (selectedImage == null) {...} else { imageGenerationRepository.generateFromImage( fileProvider.copyToInternalStorage(selectedImage), _uiState.value.botColor.getVerboseDescription(), ) } } PromptType.TEXT -> imageGenerationRepository.generateFromDescription(...) } _uiState.update {...} } catch (e: Exception) {...} } else {...} } }
  18. © ZOZO, Inc. 32 // ImageGenerationRepository#generateFromImage @Singleton internal class ImageGenerationRepositoryImpl

    @Inject constructor(...) : ImageGenerationRepository { override suspend fun generateFromImage( file: File, skinTone: String, ): Bitmap { checkInternetConnection()      // 1. 画像の検証(全身が写っているか) val validatedImage = validateImageIsFullPerson(file) if (!validatedImage.success) {...}  // 2. 画像から説明文を生成 val imageDescription = firebaseAiDataSource.generateDescriptivePromptFromImage( BitmapFactory.decodeFile(file.absolutePath), ) // 3. 説明文を使って画像生成 if (!imageDescription.success || imageDescription.userDescription == null) {...} return firebaseAiDataSource.generateImageFromPromptAndSkinTone( imageDescription.userDescription.toString(), skinTone, ) } }
  19. © ZOZO, Inc. 33 // ImageGenerationRepository#generateFromImage @Singleton internal class ImageGenerationRepositoryImpl

    @Inject constructor(...) : ImageGenerationRepository { override suspend fun generateFromImage( file: File, skinTone: String, ): Bitmap { checkInternetConnection()      // 1. 画像の検証(全身が写っているか) val validatedImage = validateImageIsFullPerson(file) if (!validatedImage.success) {...}  // 2. 画像から説明文を生成 val imageDescription = firebaseAiDataSource.generateDescriptivePromptFromImage( BitmapFactory.decodeFile(file.absolutePath), ) // 3. 説明文を使って画像生成 if (!imageDescription.success || imageDescription.userDescription == null) {...} return firebaseAiDataSource.generateImageFromPromptAndSkinTone( imageDescription.userDescription.toString(), skinTone, ) } }
  20. © ZOZO, Inc. 34 override suspend fun validateImageHasEnoughInformation(image: Bitmap): ValidatedImage

    { val jsonSchema = Schema.obj( properties = mapOf( "success" to Schema.boolean(), "error" to Schema.enumeration( values = ImageValidationError.entries.map { it.description }, description = "Error message", nullable = true, ), ), optionalProperties = listOf("error"), ) val generativeModel = createGenerativeTextModel(jsonSchema) return executeImageValidation( generativeModel, remoteConfigDataSource.promptImageValidation(), image, ) } enum class ImageValidationError(val description: String) { NOT_PERSON("not_a_person"), NOT_ENOUGH_DETAIL("not_enough_detail"), POLICY_VIOLATION("policy_violation"), OTHER("other"), }
  21. © ZOZO, Inc. // 生成AIモデルによるコンテンツ生成の実行と結果のパース private suspend fun executeImageValidation( generativeModel:

    GenerativeModel, prompt: String, image: Bitmap, ): ValidatedImage { val response = generativeModel.generateContent( content { text(prompt) image(image) }, ) val jsonResponse = Json.parseToJsonElement(response.text!!) val isSuccess = jsonResponse.jsonObject["success"]?.jsonPrimitive?.booleanOrNull == true val error = jsonResponse.jsonObject["error"]?.jsonPrimitive?.content val errorEnum = ImageValidationError.entries.find { it.description == error } return ValidatedImage(isSuccess, errorEnum) } // Gemini APIからのレスポンス { "content": { "role": "model", "parts": [ { "text": "{\n \"success\": true,\n \"error\": null\n}" } ] }, } 35
  22. © ZOZO, Inc. 36 プロンプトはどこ? • Firebase Remote Configで管理 •

    remote_config_defaults.xmlにデフォルトのプロンプトが記載 • 指定しなければのデフォルト値を利用
  23. © ZOZO, Inc. 37 // ImageGenerationRepository#generateFromImage @Singleton internal class ImageGenerationRepositoryImpl

    @Inject constructor(...) : ImageGenerationRepository { override suspend fun generateFromImage( file: File, skinTone: String, ): Bitmap { checkInternetConnection()      // 1. 画像の検証(全身が写っているか) val validatedImage = validateImageIsFullPerson(file) if (!validatedImage.success) {...}  // 2. 画像から説明文を生成 val imageDescription = firebaseAiDataSource.generateDescriptivePromptFromImage( BitmapFactory.decodeFile(file.absolutePath), ) // 3. 説明文を使って画像生成 if (!imageDescription.success || imageDescription.userDescription == null) {...} return firebaseAiDataSource.generateImageFromPromptAndSkinTone( imageDescription.userDescription.toString(), skinTone, ) } }
  24. © ZOZO, Inc. 38 override suspend fun generateDescriptivePromptFromImage(image: Bitmap): ValidatedDescription

    { val jsonSchema = Schema.obj( properties = mapOf( "success" to Schema.boolean(), "user_description" to Schema.string(), ), optionalProperties = listOf("user_description"), ) val generativeModel = createGenerativeTextModel(jsonSchema) return executeImageDescriptionGeneration( generativeModel, remoteConfigDataSource.promptImageDescription(), image, ) } private suspend fun executeImageDescriptionGeneration( generativeModel: GenerativeModel,prompt: String,image: Bitmap, ): ValidatedDescription { val response = generativeModel.generateContent( content { text(prompt) image(image) }, ) val jsonResponse = Json.parseToJsonElement(response.text!!) val isSuccess = jsonResponse.jsonObject["success"]?.jsonPrimitive?.booleanOrNull == true val userDescription = jsonResponse.jsonObject["user_description"]?.jsonPrimitive?.content return ValidatedDescription(isSuccess, userDescription) }
  25. © ZOZO, Inc. 39 override suspend fun generateDescriptivePromptFromImage(image: Bitmap): ValidatedDescription

    { val jsonSchema = Schema.obj( properties = mapOf( "success" to Schema.boolean(), "user_description" to Schema.string(), ), optionalProperties = listOf("user_description"), ) val generativeModel = createGenerativeTextModel(jsonSchema) return executeImageDescriptionGeneration( generativeModel, remoteConfigDataSource.promptImageDescription(), image, ) } private suspend fun executeImageDescriptionGeneration( generativeModel: GenerativeModel,prompt: String,image: Bitmap, ): ValidatedDescription { val response = generativeModel.generateContent( content { text(prompt) image(image) }, ) val jsonResponse = Json.parseToJsonElement(response.text!!) val isSuccess = jsonResponse.jsonObject["success"]?.jsonPrimitive?.booleanOrNull == true val userDescription = jsonResponse.jsonObject["user_description"]?.jsonPrimitive?.content return ValidatedDescription(isSuccess, userDescription) }
  26. © ZOZO, Inc. 40 override suspend fun generateDescriptivePromptFromImage(image: Bitmap): ValidatedDescription

    { val jsonSchema = Schema.obj( properties = mapOf( "success" to Schema.boolean(), "user_description" to Schema.string(), ), optionalProperties = listOf("user_description"), ) val generativeModel = createGenerativeTextModel(jsonSchema) return executeImageDescriptionGeneration( generativeModel, remoteConfigDataSource.promptImageDescription(), image, ) } private suspend fun executeImageDescriptionGeneration( generativeModel: GenerativeModel,prompt: String,image: Bitmap, ): ValidatedDescription { val response = generativeModel.generateContent( content { text(prompt) image(image) }, ) val jsonResponse = Json.parseToJsonElement(response.text!!) val isSuccess = jsonResponse.jsonObject["success"]?.jsonPrimitive?.booleanOrNull == true val userDescription = jsonResponse.jsonObject["user_description"]?.jsonPrimitive?.content return ValidatedDescription(isSuccess, userDescription) }
  27. © ZOZO, Inc. 41 override suspend fun generateDescriptivePromptFromImage(image: Bitmap): ValidatedDescription

    { val jsonSchema = Schema.obj( properties = mapOf( "success" to Schema.boolean(), "user_description" to Schema.string(), ), optionalProperties = listOf("user_description"), ) val generativeModel = createGenerativeTextModel(jsonSchema) return executeImageDescriptionGeneration( generativeModel, remoteConfigDataSource.promptImageDescription(), image, ) } private suspend fun executeImageDescriptionGeneration( generativeModel: GenerativeModel,prompt: String,image: Bitmap, ): ValidatedDescription { val response = generativeModel.generateContent( content { text(prompt) image(image) }, ) val jsonResponse = Json.parseToJsonElement(response.text!!) val isSuccess = jsonResponse.jsonObject["success"]?.jsonPrimitive?.booleanOrNull == true val userDescription = jsonResponse.jsonObject["user_description"]?.jsonPrimitive?.content return ValidatedDescription(isSuccess, userDescription) }
  28. © ZOZO, Inc. 42 generateDescriptivePromptFromImageの結果 「A person with short, straight,

    dark brown hair. This person is wearing a short-sleeved, dark blue (#3B4C6A) button-up shirt with a mandarin collar on its body. They are also wearing olive green (#6B6B47) trousers. On their feet are white sneakers with two velcro straps and dark blue stripes. The person has a black watch on their left wrist and a black lanyard with a white rectangular badge around their neck. A black bag is held under one arm.」 「短くストレートな暗い茶色の髪の人物。服装は、マンダリンカラー(立ち襟)の半袖 で、ダークブルー(#3B4C6A)のボタンアップシャツを着ています。パンツはオリー ブグリーン(#6B6B47)です。足元は、濃紺のストライプが入った白いスニーカーを 履いており、靴紐の代わりに2本のベルクロストラップ(マジックテープ)が付いてい ます。左の手首には黒い腕時計をし、首からは白い長方形の名札が付いた黒い ネックストラップを下げています。片方の腕に黒いバッグを抱えています。」
  29. © ZOZO, Inc. 43 // ImageGenerationRepository#generateFromImage @Singleton internal class ImageGenerationRepositoryImpl

    @Inject constructor(...) : ImageGenerationRepository { override suspend fun generateFromImage( file: File, skinTone: String, ): Bitmap { checkInternetConnection()      // 1. 画像の検証(全身が写っているか) val validatedImage = validateImageIsFullPerson(file) if (!validatedImage.success) {...}  // 2. 画像から説明文を生成 val imageDescription = firebaseAiDataSource.generateDescriptivePromptFromImage( BitmapFactory.decodeFile(file.absolutePath), ) // 3. 説明文を使って画像生成 if (!imageDescription.success || imageDescription.userDescription == null) {...} return firebaseAiDataSource.generateImageFromPromptAndSkinTone( imageDescription.userDescription.toString(), skinTone, ) } }
  30. © ZOZO, Inc. 44 Firebase AI SDKの初期化 override suspend fun

    generateImageFromPromptAndSkinTone(prompt: String, skinTone: String): Bitmap { val generativeModel = createGenerativeImageModel() // Retrieve the base prompt template from Remote Config val basePromptTemplate = remoteConfigDataSource.promptImageGenerationWithSkinTone() // Perform the substitution val imageGenerationPrompt = basePromptTemplate .replace("{prompt}", prompt) .replace("{skinTone}", skinTone) return executeImageGeneration( generativeModel, imageGenerationPrompt, ) } private suspend fun executeImageGeneration( generativeModel: ImagenModel, prompt: String, ): Bitmap { val response = generativeModel.generateImages(prompt) return response.images.first().asBitmap() }
  31. © ZOZO, Inc. 45 Firebase AI SDKの初期化 override suspend fun

    generateImageFromPromptAndSkinTone(prompt: String, skinTone: String): Bitmap { val generativeModel = createGenerativeImageModel() // Retrieve the base prompt template from Remote Config val basePromptTemplate = remoteConfigDataSource.promptImageGenerationWithSkinTone() // Perform the substitution val imageGenerationPrompt = basePromptTemplate .replace("{prompt}", prompt) .replace("{skinTone}", skinTone) return executeImageGeneration( generativeModel, imageGenerationPrompt, ) } private suspend fun executeImageGeneration( generativeModel: ImagenModel, prompt: String, ): Bitmap { val response = generativeModel.generateImages(prompt) return response.images.first().asBitmap() }
  32. © ZOZO, Inc. 46 ImagenModelに与えるプロンプトと生成結果 「This 3D rendered, cartoonish Android

    mascot rendered in a photorealistic style, with the Android Green #50C168 skin color and A person with short, straight, dark brown hair. This person is wearing a short-sleeved, dark blue (#3B4C6A) button-up shirt with a mandarin collar on its body. They are also wearing olive green (#6B6B47) trousers. On their feet are white sneakers with two velcro straps and dark blue stripes. The person has a black watch on their left wrist and a black lanyard with a white rectangular badge around their neck. A black bag is held under one arm., the pose is relaxed and straightforward, facing directly forward with his shoulders at ease, as if posing for a photo. The cartoonish exaggeration is subtle, lending a playful touch to the otherwise realistic rendering of the figure. The figure is centered against a muted, neutral warm cream coloured background (#F8F2E4) gives the figurine a unique and collectible appeal. The bot should take on the body shape of the newest Google Android Robot (Shape: It has the distinctive rounded body. The main body is a slightly barrel-shaped form with a smooth, continuous surface connecting to the head without a distinct neck, a semi-circular head with Two short, straight antennae protrude vertically from the top of the dome, positioned towards the sides, and simple, cylindrical arms and legs.) but the characteristics of the description should be used. It should NOT use the model shape or color from pre 2024.」
  33. © ZOZO, Inc. 47 ImagenModelに与えるプロンプトと生成結果 「これは、Android Green #50C168の肌の色と短くストレートな暗い茶色の髪の人 物。服装は、マンダリンカラー(立ち襟)の半袖で、ダークブルー( #3B4C6A)のボタン

    アップシャツを着ています。パンツはオリーブグリーン( #6B6B47)です。足元は、濃紺 のストライプが入った白いスニーカーを履いており、靴紐の代わりに 2本のベルクロス トラップ(マジックテープ)が付いています。左の手首には黒い腕時計をし、首からは白 い長方形の名札が付いた黒いネックストラップを下げています。片方の腕に黒いバッ グを抱えています。 の特徴を持つ、3Dレンダリングされたフォトリアルなスタイルの 漫画風Androidマスコットです。ポーズはリラックスしてまっすぐで、まるで写真撮 影のためにポーズをとっているかのように、肩の力を抜いて真正面を向いています。 漫画風の誇張は控えめで、フィギュアの写実的なレンダリングに遊び心のある雰囲気 を与えています。フィギュアは、落ち着いたニュートラルで暖かみのあるクリーム色 の背景(#F8F2E4)の中央に配置され、ユニークでコレクションアイテムのような魅 力を放っています。このボットは、最新のGoogle Androidロボットのボディ形状 (特徴的な丸みを帯びたボディ。本体は滑らかで連続した表面を持つわずかに樽のよ うな形で、明確な首なしで頭部に接続されています。半円形の頭部のドーム上部から は、短くまっすぐな2本のアンテナが側面に向かって垂直に突き出ており、腕と脚はシ ンプルな円筒形です)を採用する必要がありますが、特徴は上記の説明文のものを使 用してください。2024年より前のモデルの形状や色は使用しないでください。」
  34. © ZOZO, Inc. 51 まとめ • Androidifyがドロイド君の画像を生成するフローを解説しました • Firebase AI

    Logic SDKは簡単に利用可能 ◦ Gemini、Imagenモデルを単一SDKで利用 ◦ コンテンツフィルタリングが自動適用 ◦ JSON形式での確実なレスポンス取得 ◦ AppCheckとの連携 • Firebase AI Logic SDKにはLive APIなど他にも面白い機能があるのでぜひ触ってみてください