AIの代表格と言えば「ChatGPT」かと思いますが、Gooleの公式資料を見ているとSwift向けのライブラリーがあったりと意外と使いやすそうな「Gemini」なのでさっそく勉強を兼ねて使ってみようと思います。
Geminiとは?
「Gemini」とはグーグルの生成AIサービスです。
「Google DeepMind」が開発したマルチモーダル大規模言語モデル「Gemini」
最近ではグーグルのAI「Gemini」をiPhoneに搭載?と、まだ噂レベルではありますがニュースで報じられた事もある有名な生成AIの1つです。
・前提条件
・Xcode 15.0 以降
・Swift アプリは、iOS 15 以降または macOS 12 以降
・参考
今回はGoole のチュートリアルを参考に書いていこうと思います。
https://ai.google.dev/gemini-api/docs/get-started/tutorial?lang=swift&hl=ja#generate-text-from-text-and-image-input
・Gemini API
Gemini API を使用するには、API キーが必要です。Google AI Studio でキーを作成します。↓
https://aistudio.google.com/app/apikey?hl=ja
API キーはバージョン管理システムにチェックインしないことを強くおすすめします。もう 1 つの方法は、GenerativeAI-Info.plist ファイルに保存し、.plist ファイルから API キーを読み取ることです。この .plist ファイルをアプリのルートフォルダに配置し、バージョン管理から除外してください。
プロジェクトにInfo.plist(Propaty List) を追加して、右クリックし「Open As → Source Code」で開き、以下のように記述します。
API_KEY 部分には「Google AI Studio」から取得したAPIキーを指定してください。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>API_KEY</key>
<string>_API_KEY_</string>
</dict>
</plist>
.plistからAPIを取得するコードを書きます。
enum APIKey {
// .plist から API キーを取得します.
static var ApiKey: String {
guard let filePath = Bundle.main.path(forResource: "作成した.plist名", ofType: "plist")
else {
fatalError("Couldn't find file ' .plist'.")
}
let plist = NSDictionary(contentsOfFile: filePath)
guard let value = plist?.object(forKey: "API_KEY") as? String else {
fatalError("Couldn't find key 'API_KEY' in '.plist'.")
}
if value.starts(with: "_") {
fatalError("Follow the instructions at https://ai.google.dev/tutorials/setup to get an API key.")
}
return value
}
これでAPIキーの設定はOKです!
・APIの利用料金
利用料金については以下を参照して下さい↓
https://ai.google.dev/pricing?hl=ja
・SDK パッケージをプロジェクトに追加する
ここからは実装にむけて公式で推奨しているライブラリーを準備していきます。
Swift 向けの Gemini API を利用するための GoogleGenerativeAI パッケージをアプリに追加します。
追加するには [Add Packages] ダイアログで、検索バーにパッケージの URL を貼り付けAdd Package」ボタンを押します。
これで準備は完了なので実装に移っていきます。
・実装
まずAPI 呼び出しを行う前に、生成モデルを初期化する必要があります。
今回はテキストと画像入力からテキストを生成する方法を学習しようと思いますので、
Gemini 1.0 Pro Visionを使用します。
//生成モデルを初期化する
let visionModel = GenerativeModel(name: "gemini-pro-vision", apiKey: APIKey.ApiKey)
モデルバリエーションはチュートリアルから参照できます↓
次にGeminiのテキスト生成コードを書いていきます。今回は写真から名前を推測してもらおうと思います。enumでエラーハンドリングも追加していきます。
class GeminiAI {
enum GeminError: Error {
// 画像が不正
case invalidPrompt
// リクエストの失敗
case requestFailed
// リクエストが不正
case badRequest
// サーバーエラー
case internalSeverError
}
// モデルの生成
// vision(画像・テキスト入力)
let visionModel = GenerativeModel(name: "gemini-pro-vision", apiKey: APIKey.ApiKey)
// 写真から名前を推測する関数
func predictName(image: UIImage) async throws -> String? {
do {
let prompt = "これは何ですか?"
// 生成結果から名前を抽出
let response = try await visionModel.generateContent(prompt, image)
if let text = response.text {
return text
}
} catch GeminError.invalidPrompt {
print("画像が不正です.")
// 画像が不正の場合の処理(例: 別の画像を取得する)
return nil
} catch GeminError.requestFailed {
print("リクエストが失敗しました.")
// リクエストが失敗した場合の処理(例: リクエストを再実行する)
return nil
} catch GeminError.badRequest {
print("リクエストが不正です.")
// リクエストが不正の場合の処理(例: リクエストパラメーターを確認する)
return nil
} catch GeminError.internalSeverError {
print("サーバーエラーが発生しました.")
// サーバーエラーが発生した場合の処理(例: 時間をおいて再実行する)
return nil
}
print("予期せぬエラーが発生しました.")
return nil
}
}
今回はあくまで勉強の一環なので、プロンプトはシンプルですが実際に使用するならば写真が何なのか?検索したいことは何なのか?などを考慮してもっと具体的に構成する必要がありそうです。
公式のプロンプトギャラリーを参考にするのもいいかと思います。↓
今回はカメラを利用するのでAVFounddationとPhotosを使っていきます。
そのため.plistに次のような記述を追加します。
<key>NSCameraUsageDescription</key>
<string>カメラを使用します</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>写真を使用します</string>
<key>NSCameraPortraitEffectEnabled</key>
<false/>
実際に使用する際は、ユーザーへの警告表示を具体的にしないと審査でリジェクトされるので注意が必要です。
カメラ処理のコードを書き、撮影後の処理の部分にGeminiAIの処理を記載して写真撮影が完了した場合に画像判定の処理を行っていこうと思います。
また、今回は撮った画像のフォトライブラリーへの保存は行わないのであくまでアクセス許可だけを記述します。
// カメラを起動する
func startCamera() {
// UIImagePickerControllerのインスタンスを作成
let picker = UIImagePickerController()
picker.sourceType = .camera
picker.delegate = self
// カメラの起動モードを写真撮影モードに設定
picker.cameraCaptureMode = .photo
// カメラのプレビュー画面を調整
picker.cameraViewTransform = CGAffineTransform(scaleX: 1, y: 1)
// カメラ画面を表示
present(picker, animated: true)
}
// 写真撮影が完了した時に呼び出される
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
// 撮影した画像を取得
guard let image = info[.originalImage] as? UIImage else {
return
}
// プレビュー画像に設定ための処理を書く
Task { @MainActor in
_ = Task {
// 撮影後の処理を書く
let photoName = try await GeminiAI().predictName(image: image)
// 推測結果を表示
if let name = photoName {
print("名前: \(foodName)")
// 名前を表示する処理を追加する.
} else {
print("名前の推測に失敗しました.")
// 名前を取得できなかった場合の処理を追加する.
}
}
}
// カメラを閉じる
picker.dismiss(animated: true)
}
// カメラアクセス リクエストステータス
func allowedRequestStatus() -> Bool {
var avState = false
var phState = false
// カメラへのアクセス承認状態取得
switch AVCaptureDevice.authorizationStatus(for: .video) {
case .authorized:
avState = true
break
case .notDetermined:
// カメラへのアクセス承認アラート表示
AVCaptureDevice.requestAccess(for: .video, completionHandler: { granted in
// メインスレッドで非同期処理
Task {@MainActor in
avState = granted
}
})
default:
avState = false
break
}
// フォトライブラリーへのアクセス承認状態取得
switch PHPhotoLibrary.authorizationStatus(for: .addOnly) {
case .authorized:
phState = true
break
case .notDetermined:
// フォトライブラリーへのアクセス承認申請アラート表示
PHPhotoLibrary.requestAuthorization(for: .addOnly, handler: { status in
// メインスレッドで非同期処理
Task {@MainActor in
if status == .authorized {
phState = true
}
}
})
default:
phState = false
break
}
if avState && phState {
return true
} else {
return false
}
}
// 承認状態識別(許可ならSetupAVCapture呼び出し)
func allowedStatus() -> Bool {
var avState = false
var phState = false
if AVCaptureDevice.authorizationStatus(for: .video) == .authorized {
avState = true
}
if PHPhotoLibrary.authorizationStatus(for: .addOnly) == .authorized {
phState = true
}
if avState && phState {
return true
} else {
return false
}
}
これであとはボタンや画像のプレビュー、検索結果の表示を実像してテストしてみます。
import UIKit
import Photos
import AVFoundation
import GoogleGenerativeAI
class ViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
// プレビュー画像を表示するUIImageView
private let previewImageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFit
return imageView
}()
override func viewDidLoad() {
super.viewDidLoad()
_ = self.allowedRequestStatus()
addButton()
// viewにプレビューを描写
self.view.addSubview(previewImageView)
// プレビュー画像の自動レイアウト制約変換(true 有効/ false 無効)
previewImageView.translatesAutoresizingMaskIntoConstraints = false
// プレビュー画像の上部と親ビューの上部の余白を100ポイントに設定(有効)
previewImageView.topAnchor.constraint(equalTo: view.topAnchor, constant: 100).isActive = true
// プレビュー画像の左端と親ビューの左端の余白を50ポイントに設定(有効)
previewImageView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 50).isActive = true
// プレビュー画像の右端と親ビューの右端の余白を-50ポイントに設定(有効)
previewImageView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -50).isActive = true
// プレビュー画像の高さは幅と等しく設定(有効)
previewImageView.heightAnchor.constraint(equalTo: previewImageView.widthAnchor).isActive = true
}
func showLabel(for name: String) {
let label = UILabel()
label.text = "\(name)"
label.numberOfLines = 0
label.font = .systemFont(ofSize: 18)
label.textColor = .white
let labelWidth: CGFloat = 300
self.view.addSubview(label)
label.translatesAutoresizingMaskIntoConstraints = false
label.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
label.topAnchor.constraint(equalTo: previewImageView.bottomAnchor, constant: 50).isActive = true
label.widthAnchor.constraint(equalToConstant: labelWidth).isActive = true
}
func addButton() {
let button = UIButton()
button.setTitle("Photo", for: .normal)
button.contentHorizontalAlignment = .center
button.setTitleColor(.white, for: .normal)
button.backgroundColor = .red
button.layer.cornerRadius = 50
button.layer.borderWidth = 2
button.layer.borderColor = UIColor.white.cgColor
button.addTarget(self, action: #selector(addTapped), for: .touchUpInside)
let buttonWidth: CGFloat = 100
let buttonHeight: CGFloat = 100
self.view.addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
button.widthAnchor.constraint(equalToConstant: buttonWidth).isActive = true
button.heightAnchor.constraint(equalToConstant: buttonHeight).isActive = true
button.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
button.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -50).isActive = true
}
@objc func addTapped() {
// カメラアクセスの許可の確認
if allowedStatus() {
// カメラ起動の処理を追加する.
startCamera()
} else {
print("Error: No camera activation approval")
// カメラアクセスに関する説明を表示させる処理を追加する.
}
}
// カメラを起動する
func startCamera() {
// UIImagePickerControllerのインスタンスを作成
let picker = UIImagePickerController()
picker.sourceType = .camera
picker.delegate = self
// カメラの起動モードを写真撮影モードに設定
picker.cameraCaptureMode = .photo
// カメラのプレビュー画面を調整
picker.cameraViewTransform = CGAffineTransform(scaleX: 1, y: 1)
// カメラ画面を表示
present(picker, animated: true)
}
// 写真撮影が完了した時に呼び出される
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
// 撮影した画像を取得
guard let image = info[.originalImage] as? UIImage else {
return
}
// プレビュー画像に設定
previewImageView.image = image
Task { @MainActor in
_ = Task {
let photoName = try await GeminiAI().predictName(image: image)
// 推測結果を表示
if let name = photoName {
print("料理名: \(name)")
// 名前を表示する処理を追加する.
showLabel(for: name)
} else {
print("料理名の推測に失敗しました.")
// 名前を取得できなかった場合の処理を追加する.
}
}
}
// カメラを閉じる
picker.dismiss(animated: true)
}
// カメラアクセス リクエストステータス
func allowedRequestStatus() -> Bool {
var avState = false
var phState = false
// カメラへのアクセス承認状態取得
switch AVCaptureDevice.authorizationStatus(for: .video) {
case .authorized:
avState = true
break
case .notDetermined:
// カメラへのアクセス承認アラート表示
AVCaptureDevice.requestAccess(for: .video, completionHandler: { granted in
// メインスレッドで非同期処理
Task {@MainActor in
avState = granted
}
})
default:
avState = false
break
}
// フォトライブラリーへのアクセス承認状態取得
switch PHPhotoLibrary.authorizationStatus(for: .addOnly) {
case .authorized:
phState = true
break
case .notDetermined:
// フォトライブラリーへのアクセス承認申請アラート表示
PHPhotoLibrary.requestAuthorization(for: .addOnly, handler: { status in
// メインスレッドで非同期処理
Task {@MainActor in
if status == .authorized {
phState = true
}
}
})
default:
phState = false
break
}
if avState && phState {
return true
} else {
return false
}
}
// 承認状態識別(許可ならSetupAVCapture呼び出し)
func allowedStatus() -> Bool {
var avState = false
var phState = false
if AVCaptureDevice.authorizationStatus(for: .video) == .authorized {
avState = true
}
if PHPhotoLibrary.authorizationStatus(for: .addOnly) == .authorized {
phState = true
}
if avState && phState {
return true
} else {
return false
}
}
}
・サンプル動画
おわりに
ライブラリーが用意してあるので比較的簡単に少ないコードで扱うことができました!
さらに知識を深めて自身のアプリに活かしてみようかなと思います。