AI分析アプリを作るとき、一番悩むのは 「ユーザーの画像をどこまでクラウドに送るか」 です。画像をそのままマルチモーダルLLMに投げれば実装はシンプルですが、ユーザーの体感プライバシーは大きく下がります。特にチャットのスクリーンショットは、相手の名前・アイコン・会話内容が全部入った究極のセンシティブ情報です。
恋愛メッセージ分析アプリ Relora では、この問題を 「OCRはiOS上でローカル完結、LLMだけクラウド」 というハイブリッド設計で解決しました。本記事はその設計判断と、iOS側・サーバー側のコード構造を解説します。
この記事で分かること
- 「画像を送らない」ことでユーザーに提供できるプライバシー保証
- iOS側で完結するOCRパイプラインの構造(
VNRecognizeTextRequest) - テキストのみをAPI Gateway → Lambda → Bedrockに送る通信設計
- マルチモーダルLLMを使わない判断の技術的・経済的理由
- ローカル処理とクラウド処理の責務分離
- プライバシーポリシーと実装を一致させる重要性
背景: マルチモーダル vs ハイブリッド
Claude Sonnet 4.6やGPT-4oのマルチモーダルLLMに画像を直接投げれば、OCRも分析も1回で終わります。コード量は最小。でも次のコストがあります。
- プライバシー: スクショそのものがサーバーを経由する。「相手の顔アイコン」「LINE名」「プロフ画像」が全てAWS上に一時的に残る。
-
料金: 画像入力は同じ分析内容でもテキストより多くのトークンを消費する。Anthropicは画像を長辺1568pxに自動リサイズしたうえで
(width × height) / 750トークンとして課金するため、iPhoneスクショ1枚は実測で約1,500トークンになる。 - 速度: 画像認識とテキスト推論を同時にする分、レスポンスが遅い。
Reloraは次の理由でハイブリッドを選びました。
- ユーザーに「画像は一切サーバーに送られない」と明言できる
- Apple Vision OCRはローカルで高速(100ms程度)かつ無料
- LLM入力はテキスト化された後なので、1分析あたりのトークン数を圧縮できる(Bedrock経由でQwen3 Next 80Bなら1分析0.2円未満、Sonnet 4.6でも約2円程度)
- オフライン時も少なくともOCRまでは動作する
パイプライン全体
┌─────────────────────────────────────────────────────────┐
│ iOS (Relora.app) │
│ │
│ ① UIImagePickerController / PhotosPicker │
│ ↓ UIImage │
│ ② ImageAnalysisService │
│ - VNRecognizeTextRequest │
│ - boundingBox で [me]/[them] 判定 │
│ - 複数行バブルを結合 │
│ ↓ 構造化テキスト([them] 今日どうする? [me] ...) │
│ ③ CloudAnalysisService │
│ - Cognito JWT取得(Keychain保存) │
│ - HTTPS POST (JSON body, 画像なし) │
└─────────────────────────────────────────────────────────┘
↓ HTTPS (TLS 1.3)
┌─────────────────────────────────────────────────────────┐
│ AWS Cloud │
│ │
│ ④ API Gateway (Cognito Authorizer) │
│ ⑤ Lambda (analyze) │
│ - レート制限(DynamoDB アトミックカウンタ) │
│ - プロンプト構築 │
│ - Bedrock Guardrails │
│ ⑥ Bedrock Converse API │
│ - Qwen3 Next 80B (free) / Sonnet 4.6 (premium) │
└─────────────────────────────────────────────────────────┘
↑ JSON
↓ iOS側でパース → 画面表示
画像は ③ の送信バウンダリを決して越えない のが核です。
iOS側: ローカルOCRパイプライン
ImageAnalysisService の責務
actor ImageAnalysisService {
/// UIImageから構造化チャットテキストを抽出する
func structureChat(from image: UIImage) async throws -> String {
let observations = try await recognizeText(in: image)
let grouped = groupByBubble(observations)
let labeled = assignSenderLabels(grouped, imageWidth: image.size.width)
return labeled.map { "[\($0.sender)] \($0.text)" }
.joined(separator: "\n")
}
private func recognizeText(in image: UIImage) async throws
-> [VNRecognizedTextObservation]
{
guard let cgImage = image.cgImage else { throw OCRError.invalidImage }
let request = VNRecognizeTextRequest()
request.recognitionLevel = .accurate
request.usesLanguageCorrection = true
request.recognitionLanguages = ["ja-JP", "en-US", "ko-KR", "zh-Hans"]
let handler = VNImageRequestHandler(cgImage: cgImage, options: [:])
try handler.perform([request])
return request.results ?? []
}
}
VNRecognizeTextRequest は完全にデバイス内処理です。ネットワークを一切使わないため、機内モードでも動作します。
送信者判定
boundingBoxの minX / maxX で左右判定 → 同じサイドかつY座標が近いものを1バブルとして結合、という流れです。コードと設計判断の詳細は別記事 Apple Vision OCRのboundingBoxで送信者を自動判定する に書きました。
重要: 画像オブジェクトはメモリからも素早く解放
func analyze(image: UIImage) async throws -> Analysis {
let extractedText = try await imageService.structureChat(from: image)
// この時点で image 変数のスコープを抜けさせる
// UIImage は ARC で解放される
return try await cloudService.submit(text: extractedText, ...)
}
Swiftの仕様上、関数スコープを抜けたローカル変数は即座にARCで解放されます。image を後段に持ち越さない構造にすることで、メモリ上の画像データすら分析中には残らない ようにしています。
サーバー側: テキストのみ受け取るAPI
API契約
POST /v1/analyze
Authorization: Bearer <Cognito JWT>
X-App-Language: ja
Content-Type: application/json
{
"extractedText": "[them] 今日どうする?\n[me] いいね!",
"situation": "マッチングアプリで知り合った",
"relationship": "出会って1ヶ月",
"purpose": "次のデートに繋げたい",
"userNote": "最近返信が遅い",
"tier": "free",
"userProfile": {"gender": "female", "ageRange": "20s"}
}
リクエストボディにバイナリフィールドが存在しない ことが、設計上のアピールポイントです。API GatewayのログやLambdaのCloudWatchログに、画像データが残る可能性が構造的にゼロになります。
Lambda側(抜粋)
def lambda_handler(event, context):
claims = event.get("requestContext", {}).get("authorizer", {}) \
.get("jwt", {}).get("claims", {})
user_id = claims.get("sub")
if not user_id:
return _error(401, "Unauthorized")
body = json.loads(event.get("body", "{}"))
extracted_text = body.get("extractedText", "")
if len(extracted_text) > 10000:
return _error(400, "extractedText too long")
# ...プロンプト組み立て、Bedrock呼び出し
画像を受け取るパラメータは存在しない ため、誤って追加される未来もありません。API仕様がそのままプライバシー保証になります。
マルチモーダルLLMを使わない経済的理由
仮にスクショ1枚をそのままClaude Sonnet 4.6に投げた場合のコスト試算(Bedrock on-demand: input $3 / output $15 per 1M tokens、1USD=150円換算):
| 項目 | マルチモーダル | ハイブリッド |
|---|---|---|
| 画像入力 | 約 1,500 tokens(リサイズ後) | 0 |
| OCR結果テキスト | 0(同時処理) | 約 300 tokens |
| コンテキスト(プロンプト等) | 約 1,500 tokens | 約 1,500 tokens |
| 出力 | 約 500 tokens | 約 500 tokens |
| 合計トークン | 約 3,500 | 約 2,300 |
| 1回あたりコスト(Sonnet 4.6 概算) | 約 2.5円 | 約 1.9円 |
| 1回あたりコスト(Qwen3 Next 80B 概算) | ― | 約 0.13円 |
絶対額の差は1回あたり数十銭〜数円ですが、月10万分析を超える規模になると有意な差 になります。さらに重要なのはトークン経済よりプライバシー設計の一貫性で、ここがコストのみで判断するべきでない理由です。なお無料枠(Qwen3 Next 80B)はSonnetよりおよそ1桁以上安く、無料プラン提供を成立させる前提になっています。
プライバシーポリシーとの一致
設計とプライバシーポリシーを一致させるには、「送信しない情報」を明文化する のが重要です。Reloraのプライバシーポリシーは以下の記述を含みます。
本アプリは、分析対象の画像データをお客様のデバイス外に送信しません。画像からのテキスト抽出はApple Vision frameworkを用いてお使いのiPhone上で完結し、当社のサーバーおよび第三者のサービスに画像が送られることはありません。
実装と文言を1対1で対応させることで、「プライバシー訴求は誇張では?」という疑念を構造的に封じます。
ローカル処理とクラウド処理の責務分離
設計判断を整理すると:
| 処理 | 配置 | 理由 |
|---|---|---|
| OCR | ローカル (Vision) | プライバシー + 無料 + 高速 |
| 送信者判定 | ローカル | 座標情報を使うため、サーバーでは不可能 |
| コンテキスト入力UI | ローカル | ユーザーのモチベーション文脈 |
| JWT認証 | サーバー (Cognito) | セキュリティ |
| レート制限 | サーバー (DynamoDB) | クライアント信頼できない |
| プロンプト構築 | サーバー | プロンプトを露出させない |
| LLM推論 | サーバー (Bedrock) | 無料枠とモデル選択の柔軟性 |
| JSONパース | ローカル | 表示と密結合 |
| 履歴永続化 | ローカル (SwiftData) | プライバシー |
「ユーザーデータに近い処理ほどローカル、計算リソースが要る処理ほどクラウド」 という原則で、責務を分離しています。
オフライン動作の扱い
ネットワーク切断時の挙動:
- OCRは動作する(Vision はローカル)
- 分析結果の取得はできない(UIでエラーを表示)
- 過去の分析履歴はSwiftDataから読める
これは プライバシー第一設計の副次的メリット で、「圏外でも会話の内容を確認したい」というニーズに応えられます。
まとめ
- マルチモーダルLLMに画像を直接投げるのは楽だが、プライバシー・コスト・速度で不利
- 「OCRローカル + LLMクラウド」のハイブリッドは、iOS個人開発でも再現可能な設計
- API仕様から画像受け取りを排除することで、プライバシーポリシーと実装を構造的に一致させられる
- 1分析あたりのトークン数を3割程度カットでき、月数万〜数十万分析の規模では有意なコスト差になる
- Vision frameworkは日本語・英語・韓国語・中国語に対応、遅延100ms前後で実用的
クラウドLLMを使う個人iOSアプリは、「画像は送らない」を構造で保証するだけで、ユーザーとの信頼関係がぐっと強くなります。
関連記事:
- [Q1: Apple Vision OCRのboundingBoxで送信者を自動判定する]
- [Q2: LLMのJSON出力を壊さない ― 多段デコード・自動修復パーサーの設計]
- [Z1: スクショ→AI分析アプリの全体設計 ― iOS MVVM + AWSサーバーレス]
Relora(App Store): https://apps.apple.com/app/relora/id6762029713