iPhoneのカメラを向けると、AIに周囲のものについて尋ねることができる。
ビジュアルインテリジェンスという機能です。
「Googleレンズ」と「かこって検索」を合わせたようなAIレンズです。
スクリーンショットでも使えます。
中身はChatGPTです。
機能拡張を有効にすると、画像がChatGPTに渡されます。
このVisual Intelligenceの中身のAI、ChatGPTだけではなく自前のカスタムモデルも使えるようになりました(iOS 26)。
つまりユーザーがパッと撮った写真をさまざまな便利なモデルに渡せる。
カスタムモデルを使うと、
- タスク特化型モデルが使える
- インターネットなしでも動く
- LLMだけでなく、物体検出やセグメンテーションなどのコンピューターヴィジョンモデルも使える
といったメリットがあります。
ということで、画像LLM好きの僕は、
MiniCPM-V 4.6という軽量なVLMを組み込んでみました。
自分で変換したモデルが、アプリを開かなくても動く。うれしい。
やり方
どうすればOSが自分のモデルを呼んでくれるか。
AIが答えを返すコードは、自分のアプリの中に書きます。
決まった形の型を1つ書けば終わります。
アプリがインストールされると、OSがアプリのメタデータを読んで「このアプリはビジュアル検索に答えられる」と知ります。
つまりカスタムAIをVisual Intelligenceで使うには、そのAIを呼べるアプリがインストールされている必要があります。
順番にするとこうです。
- アプリビルド時にアプリ内の決まった型をXcodeが抽出してアプリ内のメタデータにする
- インストール時にOSが読む
- カメラを向けた時にOSがあなたのアプリをバックグラウンドで起動
-
values(for:)を呼ぶ ← 自分が書いたコードはここ - 返ってきた結果を、OSが検索画面に描画
- 結果をタップすると、OSが
perform()を呼ぶ。アプリが開く
肝心なのはAIの処理が動く場所で、OSの中ではなく、自分のアプリのプロセスの中で動きます。
だからモデルのロードも推論も、普段どおり自分のコードで書くだけです。
その代わり、メモリの制約も自分のアプリが背負います。
書く型は2つです。
1つ目。
写真を受け取って、答えを返す。
struct VisualSearchValueQuery: IntentValueQuery {
@Dependency var engine: MiniCPMVIEngine
func values(for input: SemanticContentDescriptor) async throws -> [VisualAnswerEntity] {
guard let pixelBuffer = input.pixelBuffer,
let cgImage = MiniCPMVIEngine.cgImage(from: pixelBuffer) else { return [] }
let answer = try await engine.caption(cgImage) // ここでMiniCPM-Vが答える
return [VisualAnswerEntity(answer: answer, thumbnail: ...)]
}
}
目印は引数の型で、values(for:) が SemanticContentDescriptor を受け取っていると、OSはこれをビジュアル検索用のクエリだと判断します。OSから渡されるのは、ラベルとピクセルバッファの2つだけです。
2つ目。結果をタップしてアプリが開いたときの動き。
struct OpenVisualAnswerIntent: OpenIntent {
static let title: LocalizedStringResource = "Open Answer"
@Parameter(title: "Answer") var target: VisualAnswerEntity
}
あと一つだけ。
モデルのエンジンをアプリの起動時に登録しておきます。
@main
struct MiniCPMVisualIntelApp: App {
init() {
AppDependencyManager.shared.add(dependency: MiniCPMVIEngine.shared)
}
var body: some Scene { WindowGroup { ContentView() } }
}
これで完了です。
他は設定も、使用許可もいりません。
バックグラウンド起動のときも init() は先に走る。
だからクエリが呼ばれる頃には間に合っています。
OSとやり取りする型にはモデルを指定する引数はどこにもなく、values(for:) の中で何を動かしても、システムからは見えません。
僕が落ちた落とし穴を踏まないために
- 2つ目の
OpenIntentを書き忘れると、エラーも警告も出ないまま、アプリが検索結果に出てこなくなる。 - モデルダウンロードはできない。 モデルは先にアプリを開いてダウンロードしておく必要がある。
- コンパイルを先にしておく。 初回推論は遅いので、真っ黒なダミー画像で1回走らせておく。
-
CGImageはアクターをまたげない。 クエリはメインアクターの外で走る。モデル本体はMetalバッファを持つのでメインアクターの中にいる。CGImageはSendableではないので、素直に書くとSwift 6のコンパイラに止められる。境界を渡る前に[Float16]に変換してしまえば通る。 -
落ちてもいいように書く。 モデルの呼び出しは
try?にしてある。メモリ不足で殺されても、例外が飛んでも、「タップして聞く」という結果が返るだけ。検索結果が空になることはない。 - 1アプリに1クエリしか置けない。 つまりアプリ1つにつきタブ1つ、モデル1本。検出とVLMを両方見せたいならアプリを分ける。
2GBのモデルは、メモリをどれだけ使うのか
バックグラウンド起動のメモリ上限を、Appleは公開していません。
なので、前にQwen3-VL-2Bで同様のことをしたときに測りました。
ディスク上は2.3GB。実際に計上されたピークは約440MBでした。
エンジンが重みをmmapしているからです。ファイルから読んでいるだけのページは、プロセスのメモリとして数えられない。
OSがアプリを殺すかどうかを判断するのはこの値なので、GB単位ではなく数百MB単位の話になります。
MiniCPM-V 4.6は1.3Bで、これよりさらに小さい。
今回は数値を測っていませんが、問題なく動きました。
速度は、iPhone 17 Proでprefill 50.6 tok/s、decode 51.5 tok/sです。常駐は約1.5GB。
デコーダの出力層をint8にした版に差し替えると、decodeは70.0 tok/sまで上がります(+36%)。
やってみて
Visual Intelligenceに接続するのは、ファインチューニングしたVLMでも、検出器でも、自社カタログの埋め込み検索でもいい。
そしてユーザーは、アプリを開かない。カメラを向けるだけ。
コード: apps/MiniCPMVisualIntel
モデル: mlboydaisuke/MiniCPM-V-4.6-CoreAI(Apache-2.0)
同じ配線でQwen3-VL-2Bを動かす版もあります: Examples/AskVLM
ビルドするには、coreai-models に apps/coreai-pipelined-static-inputs.patch と apps/coreai-pipelined-extra-states.patch を当てます。画像バッファと、このモデルが使う追加の内部状態のためのものです。
参考:
