AndroidでのプライバシーファーストなOCR: Sublyがクラウドを使わず請求書を読み取る仕組み
ML Kit Text Recognitionを使ったAndroidのオンデバイスOCRに関する、2026年の実践的なガイド。セットアップ、コード、そしてユーザーのデータをサーバーに残さないためのフィールド抽出のコツを解説します。
ユーザーが請求書にスマートフォンを向けた瞬間、あなたのアプリには選択肢があります。その画像をクラウドのOCRサービスに送信し、400ミリ秒で洗練されたテキストを受け取ることもできます。ただしその代償として、誰かの私的な金融ドキュメントをあなたのサーバーと第三者経由で流すことになります。あるいは、処理を端末上で行い、より誠実なプライバシーポリシーを書き、次のデータ漏洩が自分の責任になるかもしれないという心配を二度としなくて済むようにすることもできます。
私は後者の道を選びました。SublyはAndroid向けのサブスクリプション管理アプリで、NetflixやSpotifyの請求書をスキャンすると、サービス名、金額、頻度、次回の請求日を自動で入力してくれます。何ひとつスマートフォンの外に出ることはありません。この記事では、それが2026年にどのように機能するのか、セットアップ、コード、そして苦労して学んだフィールド抽出のコツを順を追って説明します。
クラウドがこれほど簡単なのに、なぜオンデバイスなのか
クラウドのOCR APIは一行で済みます。Google Cloud Vision、AWS Textract、Azure Read、どれも鮮明な結果を返してくれますし、どれもユーザーの画像を欲しがります。サブスクリプション管理アプリの場合、それらの画像には次のようなものが含まれ得ます。
- アカウント保有者の名前、下4桁、あるいは完全なメールアドレス
- 印刷された請求書の下部にある住所ブロック
- 支払い方法の詳細
- 全額、明細項目、日付
このリストのどれ一つとして、私が第三者のログに残しておきたいと思うものはありません。そして製品の観点からも、「あなたの請求書をどこにも送信しません」というのは、「あなたの請求書をGoogle Cloudに送信し、30日以内に削除します」よりもはるかに強力な宣伝文句です。だからアーキテクチャは価値に従います。画像を端末上に留め、抽出したフィールドだけを永続化し、スキャンのためにインターネット接続を一切開かないのです。
最もプライベートなバイトとは、決してスマートフォンから出ていかないバイトです。
ML Kit Text Recognition v2: 主力エンジン
GoogleのML Kit Text Recognition v2は、2026年においてAndroidのオンデバイスOCRとして退屈だが正しい答えです。ラテン文字モデルをアプリに同梱して出荷でき(約4MB)、Pixelクラスのスマートフォン上で数十ミリ秒で動作し、レシート、請求書、メールのスクリーンショットに十分対応できる程度に印刷テキストを扱えます。
私が気に入っている点は次のとおりです。
- ネットワーク権限が不要。
INTERNETを外したアプリを出荷しても、OCRは動作します。それだけで統合にかける時間の価値があります。 - オフラインで動作する。 機内モードでのスキャンはバグではなく機能です。
- アプリごとのモデル。 モデルを同梱するということは、初回起動時に予期しないダウンロードが発生せず、ユーザーのWi-Fiが不安定でも
Try Againダイアログが出ないということです。 - 無料。 スケールしてもAPIコストがかかりません。個人開発のアプリにとって、それはローンチできるか考えすぎて止まるかの分かれ目です。
トレードオフはエッジケースでの精度です。手書き文字は当たり外れがあり、密に印刷された感熱式レシートはレイアウトを混乱させることがあり、非ラテン文字には別のモデルが必要です。レシート・請求書・メールというユースケースにおいては、十分すぎるほど優秀です。
2026年のセットアップ
Gradleの依存関係:
// app/build.gradle.kts
dependencies {
implementation("com.google.mlkit:text-recognition:16.0.1")
implementation("androidx.camera:camera-camera2:1.4.0")
implementation("androidx.camera:camera-lifecycle:1.4.0")
implementation("androidx.camera:camera-view:1.4.0")
}
そしてマニフェストに一行加えることで、モデルが同梱され事前にダウンロードされることをPlayが認識します。
<application>
<meta-data
android:name="com.google.mlkit.vision.DEPENDENCIES"
android:value="ocr" />
</application>
セットアップはこれだけです。APIキーも、サービスアカウントのJSONも、署名付きリクエストもありません。
CameraXフレームからテキストを認識する
これがコアループです。CameraXがImageProxyフレームを渡してくれるので、それをInputImageでラップすると、認識器が段落、行、矩形を持つTextオブジェクトを返してくれます。
import com.google.mlkit.vision.common.InputImage
import com.google.mlkit.vision.text.TextRecognition
import com.google.mlkit.vision.text.latin.TextRecognizerOptions
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageProxy
private val recognizer = TextRecognition.getClient(
TextRecognizerOptions.DEFAULT_OPTIONS,
)
@androidx.camera.core.ExperimentalGetImage
fun analyzeFrame(image: ImageProxy, onScanned: (String) -> Unit) {
val media = image.image ?: return image.close()
val input = InputImage.fromMediaImage(media, image.imageInfo.rotationDegrees)
recognizer.process(input)
.addOnSuccessListener { result ->
// Whole page as a single string, line breaks preserved.
onScanned(result.text)
}
.addOnCompleteListener { image.close() }
}
知っておく価値のある点がいくつかあります。
- エラー時でも、
image.close()は必ずちょうど一度だけ呼び出してください。ML Kitのリスナーパターンは、忘れると痛い目に遭わせてきます。 result.textはイージーモードです。より良い抽出のためにはresult.textBlocksを使い、レシート上でどの行が隣接しているかを追跡できるようにしたいところです。- ML Kitは矩形も返します。オーバーレイの描画(「これを検出しました」)や、ユーザーが実際に請求書をフレームに収めたかどうかの妥当性チェックに役立ちます。
フィールド抽出: 生テキストから構造化データへ
OCRが与えてくれるのはテキストです。面白い仕事は、そのテキストをサブスクリプションに変えることです。Sublyにとって、それは4つのフィールドを意味します。
- サービス名 — 「Netflix」「Spotify」「ChatGPT Plus」
- 金額 — 「₺199.00」または「$9.99」
- 請求頻度 — 月額、年額
- 次回請求日 — 未来の日付
実用的なオンデバイスのパイプラインはこのようになります。
data class ExtractedSubscription(
val service: String?,
val amount: Money?,
val cycle: BillingCycle?,
val nextDate: LocalDate?,
)
fun extract(text: String): ExtractedSubscription {
val lines = text.lines().map { it.trim() }.filter { it.isNotEmpty() }
return ExtractedSubscription(
service = findService(lines),
amount = findAmount(lines),
cycle = findCycle(lines),
nextDate = findNextDate(lines),
)
}
それぞれのfind*関数は、小さくテスト可能なものです。実際の実装から得たいくつかの教訓を挙げます。
- サービス名が最も難しいものです。一般的なサービスの辞書(Netflix、Spotify、ChatGPT、YouTube Premium、Apple Music…)で80%のケースをカバーできます。残りについては、レシートの最初の目立つ行や、請求書メールの
From:/Sent by:ヘッダー周辺のヒューリスティックがうまく機能します。 - 金額が最も簡単です。
(?:[₺$€£])\s?\d{1,3}(?:[.,]\d{3})*(?:[.,]\d{2})?のようなロケール対応の正規表現でほとんどの通貨をカバーできます。パースする前に、必ず桁区切り記号を取り除いてください。 - サイクルは金額まわりの表現から得られます。「per month」「/mo」「monthly」「aylık」「yearly」「/yr」など。トルコ語と英語の小さな語彙で必要なものはカバーできます。
- 次回日付には2つの戦略があります。明示的なフレーズ(「next billing on」「renews on」)があればそれを拾い上げ、なければレシート上の最新の日付に1サイクルを加えて推測します。
どれも華々しいものではありません。そしてそのすべてが端末上で行われるので、失敗しても請求書が漏洩することはありません。
ユーザーに処理内容を見せる
スキャナーは、何をしたかを見せなければブラックボックスです。Sublyのパターンはこうです。フレームがマッチしたらすぐに、抽出されたフィールドとEditボタンを載せたカードをカメラプレビューの上に描画します。OCRへの信頼は脆いものです。Editボタンは誠実で、しかもタダです。
@Composable
fun ExtractedCard(s: ExtractedSubscription, onEdit: () -> Unit, onAccept: () -> Unit) {
Card(...) {
Text("AI detected", style = labelMono)
Text("${s.service ?: "—"} ${s.amount?.format() ?: ""}", style = title)
Text("${s.cycle?.label} · next ${s.nextDate?.format()}", style = body)
Row {
TextButton(onClick = onEdit) { Text("Edit") }
FilledButton(onClick = onAccept) { Text("Add") }
}
}
}
4つのフィールドのうち2つが欠けている場合は、自動で追加しないでください。取得できた分をあらかじめ埋めた手動入力フォームにユーザーを誘導します。
オンデバイスOCRで得られないもの
いくつかの誠実なトレードオフを挙げます。
- 言語。 各文字体系には独自のモデルが必要です。ラテン文字はデフォルトで同梱されていますが、中国語、日本語、韓国語、デーヴァナーガリーは別です。
- 表形式のレイアウト。 複数列の請求書から列と行を正確に再構築する必要がある場合、予想以上に多くのレイアウトコードを書くことになります。ほとんどのサブスクリプションのレシートでは、これは問題になりません。
- 手書き文字。 約束しないでください。クラウドのOCRですらここは信頼できません。
- モデルサイズ。 複数の言語モデルを追加すると、APKが目に見えて大きくなり得ます。インストールサイズを気にするなら、PlayのApp Bundleとfeature deliveryを使いましょう。
宣伝文句はおのずと書ける
私たちのプライバシーポリシーで言えるようにしておきたい一文があります。「あなたが撮影した請求書の画像は、私たちのサーバーにアップロードされず、処理後も保持されません。」オンデバイスのML Kitを使えば、その一文は文字どおりコードの動作そのものです。それはマーケティングではなく、アーキテクチャの表明であり、プライバシーポリシーがその逆ではなく、そこから導かれるのです。
それが2026年におけるオンデバイスOCRの本当の論拠です。単に少し速い、少し安いというだけではありません。「私たちはあなたのデータを決して見ません」というのが、法務が「たぶん大丈夫」と言ったからではなく、それが真実だから言えることなのです。
もしあなたがサブスクリプション/レシート/個人向け金融の領域で何かを作っていて、どちらに進むべきか迷っているなら、デフォルトでオンデバイスを選んでください。ライブラリは無料で、プライバシーの物語は鉄壁で、最悪の事態は、競合が自社のスタックを作り直さない限り文字どおり真似できない機能を出荷してしまうことです。
SublyはGoogle Playで公開中です。約束によってではなく、アーキテクチャによってプライバシーファーストです。詳しくはアプリページで。
// 関連記事
ジャーナルの他の記事
Granynを作る: 銀行ログインなしの家計簿を、3つのテーブルで
Granynが銀行口座を一つも連携せずに、複数通貨での支出を追跡し、定期的な請求を検知する仕組み — その裏にあるRoomスキーマと、そこで生じるトレードオフ。
Stocky を作る:キッチンで何が切れるかを予測する — しかも端末の上で
バーコードのスキャンとちょっとした使用履歴を、切らす前に現れる買い物リストへと Stocky がどう変えるのか。すべての計算も、すべてのデータも、あなたの電話の中にとどめたまま。
2026年のローカルファーストなAndroid: SQLite、Room、そしてユーザーデータを端末に残すこと
RoomとSQLiteでローカルファーストなAndroidアプリを作るための2026年版ガイド — スキーマ設計、マイグレーション、WAL、エクスポート、そして同期をいつ足すべきか(いつ足すべきでないか)。