Loading
BLOG 開発者ブログ

2024年5月30日

Apple Vision ProとGeminiを利用して会話アプリケーションを作ってみた

はじめに

こんにちは。
クラウドソリューショングループのyamasaki.sです。

Apple Vision ProとGoogleの生成AIであるGeminiを利用して簡単な会話アプリケーションを作成してみました!

基礎的なプロジェクト作成や実装については
Vision Proのシミュレーター起動方法と基礎実装をわかりやすく紹介!
で紹介されているため、今回の記事では省かせていただきます。

目次

Geminiについて

Geminiとは何なのか。というのをGeminiに聞いたところ、以下のような回答が返ってきました。

ということのようです。
ちなみに、ChatGPTは2024年5月時点ではGoogleの生成AIであるGeminiの存在を知らないようでした。。。

Gemini API

Geminiを利用するためのAPI Keyを取得します。
Google AI Studioにアクセスし、「APIキーを作成」をクリックします。

「新しいプロジェクトでAPIキーを作成」をクリックすると、APIキーが生成されます。

続いて公式が出しているチュートリアルを元にGemini APIをプロジェクトに組み込みます。

まず、取得したAPIキーは推奨されている通りplistから参照するようにします。
「GenerativeAI-Info.plist」を作成し、以下のようにします。
(「API_KEY」の「Value」には、先ほど生成したAPIキーを入力します)

次に公式のチュートリアルには、サンプルとしてplistからAPIキーを取得する処理が記載されているのでコピーして実装します。

enum APIKey {
    static var `default`: String {
        guard let filePath = Bundle.main.path(forResource: "GenerativeAI-Info", ofType: "plist")
        else {
            fatalError("Couldn't find file 'GenerativeAI-Info.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 'GenerativeAI-Info.plist'.")
        }
        if value.starts(with: "_") {
            fatalError(
                "Follow the instructions at https://ai.google.dev/tutorials/setup to get an API key."
            )
        }
        return value
    }
}

実装

Gemini APIを利用できる環境は整ったので、処理の実装を行います。
今回は「テキストのみの入力からテキストを生成する」処理の実装のみになります。

import Foundation
import GoogleGenerativeAI

class GeminiUtil: ObservableObject {
    // モデル生成
    let model = GenerativeModel(
        name: "models/gemini-pro",
        apiKey: APIKey.default)
    
    func chat(text: String) async throws -> String {
        do {
            let response = try await model.generateContent(text)
            return response.text ?? ""
        } catch {
            print("GEMINI ERROR: \(error)")
            return ""
        }
    }
    
}

入力に関しては、マイクから音声認識を利用して認識した音声をテキストとして入力します。
まずは、音声認識の使用確認と音声認識の使用許可要求の処理を実装します。

    // 音声認識使用確認
    private func authorization() -> Bool{
        if(AVCaptureDevice.authorizationStatus(for: AVMediaType.audio) == .authorized &&
            SFSpeechRecognizer.authorizationStatus() == .authorized){
            return true
        } else {
            return false
        }
    }
    // 音声認識使用許可要求
    private func requestAccess(completion:@escaping () -> Void){
        /// 音声認識の使用許可
        AVCaptureDevice.requestAccess(for: AVMediaType.audio) { granted in
            if granted {
                SFSpeechRecognizer.requestAuthorization { status in
                    completion()
                }
            } else {
                completion()
            }
        }
    }

次に、音声認識の開始と終了処理を実装します。

    // 音声認識の開始
    func startSpeechRecognition(completionHandler:@escaping (String?)->()){
        do {
            // タスクの停止
            if (self.recognitionTask != nil) {
                self.recognitionTask.cancel()
                self.recognitionTask.finish()
                self.recognitionTask = nil
            }
            
            let inputNode = self.audioEngine.inputNode
            let recordingFormat = inputNode.outputFormat(forBus: 0)
            inputNode.installTap(onBus: 0, bufferSize: 2048, format: recordingFormat) {(buffer, time) in
               self.recognitionReq.append(buffer)
            }
            
            // 開始
            self.audioEngine.prepare()
            try self.audioEngine.start()
            self.recognitionTask = self.recognizer.recognitionTask(with: recognitionReq, resultHandler:{(result, error) in
                if let error = error {
                    print("RECODING ERROR: \(error)")
                    return
                }
                if result?.isFinal == true {
                    completionHandler(result!.bestTranscription.formattedString)
                }
            })
        } catch {
            print(error.localizedDescription)
        }
    }
    
    // 音声認識の停止
    func stopSpeechRecognition() {
        self.audioEngine.stop()
        self.audioEngine.inputNode.removeTap(onBus: 0)
        self.recognitionReq.endAudio()
    }

次に認識した音声の文字列をGemini APIに投げてレスポンスを取得します。
今回は、オブジェクトを一度タップすると認識を開始、認識した内容も元にGeminiの返答を受け取り、もう一度タップすると認識を終了。という形にします。

        .gesture(TapGesture().targetedToAnyEntity().onEnded { _ in
            enlarge.toggle()
            if (enlarge) {
                viewModel.startSpeechRecognition { result in
                    Task {
                        guard let result = result else { return }
                        let response = try await openAI.chat(text: result)
                        print("User RESULT: \(result)")
                        print("System RESULT: \(response)")
                    }
                }
                return
            } else {
                viewModel.stopSpeechRecognition()
            }
        })

最後に、Gemini APIで受け取ったレスポンスをiOSの音声合成(AVSpeechSynthesisVoice)を利用して喋らせてみましょう。

    // テキスト読み上げ
    func textToSpeech(_ text: String) {
        recognitionResult = ""
        let utterance = AVSpeechUtterance(string: text)
        utterance.voice = AVSpeechSynthesisVoice(language: "en-US")
        utterance.rate = 0.5
        synthesizer.speak(utterance)
    }
        .gesture(TapGesture().targetedToAnyEntity().onEnded { _ in
  
            ...
            
            Task {
                
                ...
                
                viewModel.textToSpeech(result)
            }
        })

ビルド

ビルドして実際に動かして見るとこんな感じ。
(※音声出るので音量注意です。)

英語力はさておき、うまく会話することができました!
聞き取りにくいかもしれないので、内容としては以下のように会話しています。

User : What are you
Gemini : I am a large language model, trained by Google. My name is Gemini.

さいごに

最近流行りの生成AIと組み合わせたアプリケーションを簡単にですが、作成してみました。
2024年5月時点では、Apple Vision Proはまだ日本で発売されておらず日本語対応していないため、音声認識についてもまだ英語のみになっています。
翻訳APIなどを通すことで無理やり日本語での会話はできるようになるかもしれないですね。
生成AIとXRの組み合わせは、将来的に大きな可能性を秘めていそうで楽しみですね!

次はカスタムジェスチャーなど実機でしか確認できない機能を取り入れて、実機で動かしてみたいと思います。
生成AIも今流行りのGPT-4oにすることでかなり自然な会話をすることもできそうなので色々と試してみたいですね!

yamasakiのブログ