🏍️

バイクにSiriボタンを付けた話 #2 AIアプリ編

2024/09/04に公開
1

概要

バイク(や自転車)にiPhoneを付けて、ナビとして使っている人は多いでしょう。
今回、バイクのハンドルにiPhoneのリモコンボタンをつけて、Siriを起動できるようにしました。
マイク付きのイヤホンを使っていると、それでSiriと対話できます。すごく便利です。

とは言え、Siriの対応に少し気に入らない部分もあったので、Googleの生成AIであるGeminiのAPIを使って、音声AI対話アプリを作ってもみました。

前編(ハードウェア編)と後編(AIアプリ編・本記事)に分けてお伝えします。

ここまでの話は前編を参照してください。

Siriの問題点

Siriは、何か問い合わせたときに、会話を続ける「対話モード」(正式な呼び名は知りませんが)になる場合と、回答を返すだけの場合とがあります。

対話モードでやりとりが続くと絞り込んだり次の候補をお願いしたりできますが、回答を返すだけの場合、絞り込みや別の候補を出してもらうのが結構面倒です。
バイク走行中にSiriボタンを長押ししてSiriを起動する場合には特にです。

具体的には、川崎駅付近を走行中にワークマンを探したら、ワークマン女子の店舗が候補に挙がってきて、それを除外できないということがありました。
対話モードにならなかったり、対話モードになっても滑舌が悪くてご認識されると対話が終了してしまったりで、なかなか思ったような回答が得られませんでした。

代替策

生成AIを使った音声対話型AIアプリを作って試してみることにしました。

具体的には、APIを無償で試用できるGoogleのGeminiを使うことにしました。

Geminiの使い方

iOS+Swiftの場合、非常に簡単です。

  1. XcodeのFile→Add Package Dependencies...を選ぶ
  2. 「Search or Enter Package URL」フィールドに https://github.com/google-gemini/generative-ai-swift を入力して「Add Package」ボタンを押す
  3. APIキーを取得する。https://zenn.dev/peishim/articles/2e2e8408888f59 などの記事を参考に
  4. チュートリアルに沿ってコードを書く
    1. Configを生成する
    2. モデルを生成する
    3. 会話履歴(プロンプト)を渡して、チャットを開始する
    4. チャットのこちらからのメッセージを送信して、回答を受け取る
    5. 4.に戻る

チュートリアルに「注意: Google AI Swift SDK はプロトタイピングでのみ使用することをおすすめします。」と書いてあるので、現時点でプロダクトには利用しないほうが良さそうですけども。

音声入力

今回、音声入力(音声認識)には、iOSのSpeech Recognitionを使用しました。

などを参考にしました。

音声出力

音声出力は、iOSのSpeech Synthesizerを使用しました。

などを参考にしました。

音声入力と出力を同時に使う

音声の入力と出力を同じセッションで行うには、以下のようにします。

let audioSession = AVAudioSession.sharedInstance()
try audioSession.setCategory(.playAndRecord, mode: .voiceChat)
try audioSession.setActive(true, options: .notifyOthersOnDeactivation)

こんな感じで、カテゴリを.playAndRecordにして、モードを.voiceChat(もしくは.videoChat)にすると音声入力と、音声出力の両方ができます。
ちなみに、AVSpeechSynthesizer.usesApplicationAudioSessionfalseにすると、両方向ができなくなってしまうように見えます(セッションが分かれるせいではないかと想像します)。

ところで、音声合成をAVSpeechSynthesizerで行う場合、AVAudioOutputNodesetVoiceProcessingEnabled(true)をすると、再生音量が大きくできるみたいなのです。
(ドキュメントとかに書いてあるわけではなく、やってみたら大きくなった)
これをやらないととても小さな声で再生するので聞き取れません。

ただしこれだけやると、エラーが大量にXcodeのログウィンドウに出るようになります。

let outputNode = self.audioEngine.outputNode
try outputNode.setVoiceProcessingEnabled(true)

let mainMixer = self.audioEngine.mainMixerNode
let outputFormat = outputNode.outputFormat(forBus: 0)
self.audioEngine.connect(mainMixer, to: outputNode, format: outputFormat)
Thread.sleep(forTimeInterval: 1.0)

ミキサーをアウトプットに接続すると、ログが出ないようになります。
あと最後の1秒のスリープですが、少し間を空けてから次の処理に行かないと、音が出なかったり音声認識されなかったりするので入れています。1秒で良いかどうかは分かりません。

ともかく、音声認識&音声合成の処理に関してはまだまだうまく行かないことがあるので、もう少しちゃんと動くようになったらサンプルコードを紹介したいところです。

実装結果(動画)

実際の動作の様子を動画にしました。
(YouTube版の再生回数が0だったのでアニメーションGIFにしました)

ちょっと長いYouTube版。
※音が出るので再生時には注意してください。

https://youtu.be/Xt3V43vPBd8

やりとり自体は非常に楽しいので、それなりに何か別の用途も考えられるんじゃないかとは思いますが、今回の目的には正直向いていない感じです。
こうした、最新の情報を聞く系の対話では、聞くたびに回答が変化します。異なるサーバに聞いているせいなのか何なのか。

ちなみにこのGemini、やけにフランクな口調で話しかけてきていますが、これはプロンプトのせいです。
この試作版のプロンプト(会話履歴)は以下の通りです。

let history: [ModelContent] = [
    ModelContent(role: "user", parts: "あなたは10代の女子です。高校生のノリのような言葉遣いをしてください。"),
    ModelContent(role: "model", parts: "了解!"),
    ModelContent(role: "user", parts: "また以降のやりとりでは、簡潔かつフレンドリーに、そして音声読み上げに適した形式で回答してください。"),
    ModelContent(role: "model", parts: "そうするよ!"),
    ModelContent(role: "user", parts: "回答から絵文字を除外してください。"),
    ModelContent(role: "model", parts: "わかったよー!"),
]
let chat = model.startChat(history: history)

Gemini WebアプリとGemini APIの違い

WebアプリのGeminiはGoogleマップ機能拡張があって、川崎駅周辺のワークマンを正しく検索しますし、対話になっているのでワークマン女子も除外できます。
APIのほうは機能拡張のような仕組みを持っていないようです(とGemini自身が言っていました)。

じゃあWebアプリの音声UIを作ったらどうかとも一瞬思ったりもしましたが、読み上げに適した形式で回答してくれているわけでもなく、色々と苦労しそうです。

次のステップ

バックエンドにサーバを立てて、検索エンジン+生成AIのようにやれば良いんでしょうが、そこまではしたくないんですよね。
既存のものとしてPerplexityほか色々ありますが、無料である程度試用できそうなものがあるのかどうか。

もうちょっと色々調べてみます…

Discussion