LAITTER
紙の郵便物をスキャンすると、何についての書類か、何をいつまでにすべきかがわかります。Apple Intelligence、LFM2 2.6B、ルールのいずれかで、すべて iPhone 上で読み取ります。最初の 2 通は無料(キーチェーン)、その後は買い切りでアンロックします。
テキストからのデータ抽出、分類とタグ付け、要約、返信の下書き、音声メモのタスク化、チャット。OnDevice Kit はこうした LLM 機能を iOS アプリの中で動かします。サーバーも API 料金も不要で、データは端末の外に出ません。
Apple Intelligence が使える端末ではそれが、使えない端末では llama.cpp で動くローカルモデルが答え、最後の砦として独自ルールが控えます。回答の形を一度定義すれば、どの層が答えても同じ Codable 値が返ります。
Swift 6、iOS 17 以降、macOS 14 以降に対応。買い切り、ソースコード全公開。私たち自身の 2 つのアプリから切り出したコードです。
import OnDeviceKit
struct Letter: Codable, Sendable {
var sender: String
var action: String
var deadline: String
var steps: [String]
}
let schema = Schema("Letter") {
Property("sender", .string, "Who sent it, short")
Property("action", .choice(["none", "pay", "reply", "sign"]))
Property("deadline", .date, "Only if the letter names one")
Property("steps", .list(.string, max: 4), "Tiny steps")
}
// Downloads once, in the background, checked against its SHA-256
let store = ModelStore(.lfm2_2_6B)
let assistant = Assistant<Letter>(
schema: schema,
instructions: "Read the letter. Use only facts from it.",
model: store.readyFile,
rules: { text in Letter.guess(from: text) }
)
let (letter, tier) = try await assistant.run(scannedText)
// tier: .apple, .local or .rulesimport OnDeviceKit
struct Receipt: Codable, Sendable {
var merchant: String
var total: Double
var category: String
var date: String
}
let schema = Schema("Receipt") {
Property("merchant", .string)
Property("total", .number, "The amount paid")
Property("category", .choice(["groceries", "restaurant",
"transport", "shopping", "other"]))
Property("date", .date)
}
let assistant = Assistant<Receipt>(
schema: schema,
instructions: "Read the receipt text from the camera.",
model: store.readyFile,
rules: { Receipt.totalOnly(from: $0) }
)
let (receipt, tier) = try await assistant.run(ocrText)
expenses.add(receipt)import OnDeviceKit
struct Plan: Codable, Sendable {
struct Task: Codable, Sendable {
var title: String
var due: String
var priority: Int
}
var tasks: [Task]
}
let task = Schema("Task") {
Property("title", .string, "Short, starts with a verb")
Property("due", .date)
Property("priority", .integer(1...3))
}
let schema = Schema("Plan") {
Property("tasks", .list(.object(task), max: 8))
}
// The transcript comes from Apple's Speech framework
let (plan, tier) = try await Assistant<Plan>(
schema: schema,
instructions: "Turn the voice note into tasks.",
model: store.readyFile
).run(transcript)import OnDeviceKit
struct Triage: Codable, Sendable {
var mood: String
var topic: String
var urgent: Bool
var reply: String
}
let schema = Schema("Triage") {
Property("mood", .choice(["happy", "neutral",
"annoyed", "angry"]))
Property("topic", .choice(["billing", "bug",
"feature", "account"]))
Property("urgent", .boolean)
Property("reply", .string, "A short, warm first reply")
}
let assistant = Assistant<Triage>(
schema: schema,
instructions: "Sort the customer message and draft a reply.",
model: store.readyFile,
rules: { Triage.keywords(in: $0) }
)
let (triage, tier) = try await assistant.run(message)City Energy より 2 回目のご案内です。請求書 4711 のお支払いを確認できておりません。2026 年 10 月 30 日までに 84.20 EUR を、参照番号 4711 にてお振り込みください。期限を過ぎた場合は延滞料が発生します。
GREEN MARKET BERLIN
Bananas 1.29
Oat milk 2.49
Pasta 1.79
…
TOTAL EUR 23.47
27.10.2026 18:42
「明日、水道屋さんに電話しないと。これは急ぎ。日曜はサムの誕生日だからプレゼントを用意する。あと、どこかでライプツィヒ行きの電車を予約しておく。」
「今月、二重に請求されていませんか?今日中に対応してください。」
{
"sender": "City Energy",
"action": "pay",
"deadline": "2026-10-30",
"steps": [
"Open your banking app",
"Transfer 84.20 EUR",
"Use reference 4711"
]
}{
"sender": "City Energy",
"action": "pay",
"deadline": "2026-10-30",
"steps": [
"Transfer 84.20 EUR",
"Use reference 4711",
"Pay before 30 October"
]
}{
"sender": "City Energy",
"action": "pay",
"deadline": "",
"steps": []
}{
"merchant": "Green Market",
"total": 23.47,
"category": "groceries",
"date": "2026-10-27"
}{
"merchant": "Green Market Berlin",
"total": 23.47,
"category": "groceries",
"date": "2026-10-27"
}{
"merchant": "",
"total": 23.47,
"category": "other",
"date": "2026-10-27"
}{
"tasks": [
{ "title": "Call the plumber",
"due": "2026-10-29", "priority": 3 },
{ "title": "Buy a birthday gift for Sam",
"due": "2026-11-02", "priority": 2 },
{ "title": "Book the train to Leipzig",
"due": "", "priority": 1 }
]
}{
"tasks": [
{ "title": "Call the plumber",
"due": "2026-10-29", "priority": 3 },
{ "title": "Buy Sam's birthday gift",
"due": "2026-11-02", "priority": 2 },
{ "title": "Book train to Leipzig",
"due": "", "priority": 1 }
]
}このケースにはルールを指定していません。Assistant はエラーを投げるので、アプリ側では文字起こしをそのままメモとして表示します。
{
"mood": "annoyed",
"topic": "billing",
"urgent": true,
"reply": "Sorry about that! Refund is on its way."
}{
"mood": "annoyed",
"topic": "billing",
"urgent": true,
"reply": "Sorry! We are refunding it now."
}{
"mood": "neutral",
"topic": "billing",
"urgent": false,
"reply": ""
}出力はイメージです。実際の文面はモデルと指示によって変わりますが、データの形は変わりません。
「テキストを入れて答えを得る」と説明できる機能なら、端末内で動きます。回答を Schema として記述すれば、どの層もその形どおりに返します。構造が不要ならフリーテキストをストリーミングすることもできます。
手紙、請求書、契約書、カメラで撮ったレシートから、差出人、金額、日付、次にやることを取り出します。
.date, .number, .choiceメッセージの振り分け、メモへのラベル付け、フィードバックや日記のトピックと感情の判定。
.choice, .list(.string)Speech フレームワークの文字起こしから、タスク、買い物リスト、手順ガイドを作ります。
.list(.object(Task))長いメール、チャットのスレッド、1 ページ分のメモを、落ち着いた 2 文にまとめます。
.stringお客さまへの最初の返信、丁寧なお断り、短い返信候補 3 つ。
.list(.string, max: 3)住所、予約確認メール、履歴書を貼り付けるだけで、フォームの各項目を埋めます。
.object(Address)短いテキストを、やさしい言葉づかい、より親しみやすいトーン、短い文、別の言語に書き換えます。
.string回答をトークン単位でストリーミングするプライベートなアシスタント。任意の GGUF モデルと、そのモデル固有のチャットテンプレートで動きます。
LocalLLM.stream小さなオンデバイスモデルが苦手なこと:非常に長い文書(数ページなら問題ありませんが、本 1 冊は無理です)、与えられていない世界の知識、長い推論の連鎖。テキストを渡し、そのテキストについて尋ねる使い方なら、驚くほど良い結果が出ます。
4 つの Swift モジュール、サンプルアプリ、テスト、スクリーンショット生成ツール。オンデバイス AI アプリに必ず必要で、誰も 2 度は書きたくない部分をまとめました。
SchemaOnDeviceLLM
欲しい JSON を一度記述するだけです。Kit はそれを llama.cpp 用の GBNF 文法にコンパイルするので、デコードが制約され、小さなモデルでも壊れた JSON を出力できません。同時に Apple の Foundation Models 用の DynamicGenerationSchema にもコンパイルします。
フィールド型:.string、.choice、.integer(0...3)、.number、.boolean、.date、.list(_, max:)、ネストした .object。
root ::= "{" ws "\"sender\":" ws str "," ws "\"action\":" ws root-action-choice "," ws "\"deadline\":" ws date "," ws "\"steps\":" ws root-steps-list ws "}"
root-action-choice ::= "\"none\"" | "\"pay\"" | "\"reply\"" | "\"sign\""
root-steps-list ::= "[" ws "]" | "[" ws str ( ws "," ws str ){0,3} ws "]"
date ::= "\"\"" | "\"" [0-9]{4} "-" [0-9]{2} "-" [0-9]{2} "\""
ws ::= [ \t\n]{0,8}LocalLLMOnDeviceLLM
n_batch = n_ctx を設定するため、長いプロンプトで llama.cpp の GGML_ASSERT(n_tokens_all <= cparams.n_batch) による強制終了が起きません。promptTooLong を投げます。let llm = try LocalLLM(model: url) // loads the GGUF once
// Free text
let summary = try await llm.generate([
.system("Summarize in two calm sentences."), .user(email)
])
// Chat, streamed token by token
for try await piece in llm.stream(history + [.user(question)]) {
answer += piece
}
// Structured, forced into your schema
let recipe = try await llm.generate(Recipe.self, schema: recipeSchema,
instructions: "...", input: pastedText)AssistantOnDeviceLLM
[.apple, .local, .rules] の順に試し(順序は変更可能)、最初にデコードできた回答を、それを返した層と一緒に返します。Example を 1〜2 個渡してください。良い例が 1 つあれば、1 ページ分のルールよりも多くのことを 2B モデルに教えられます。
ModelCache.shared は呼び出しをまたいで 1 つのモデルをメモリに保持し、unload() でメモリ警告時に解放します。
let assistant = Assistant<Note>(
schema: Note.schema,
instructions: "Turn the text into a calm, clear note.",
examples: [Note.example],
order: [.apple, .local, .rules],
model: store.readyFile,
rules: Note.guess
)
if await assistant.currentTier() == nil { showDownloadPrompt = true }
let (note, tier) = try await assistant.run(input)ModelStoreOnDeviceModels
URLSession を使用。アプリを閉じてもダウンロードを続け、次回起動時に再接続します。@State private var store = ModelStore(
.lfm2_2_6B.mirrored(first: URL(string: "https://your.cdn/lfm2.gguf")!)
)
switch store.state {
case .notDownloaded: Button("Download (1.6 GB)") { store.download() }
case .downloading(let p): ProgressView(value: p)
case .verifying: Text("Checking …")
case .ready: Text("Ready")
case .failed(let reason): Text(reason)
}FreeQuota、DailyPass、UnlockOnDeviceStore
キーチェーンに保存するので、再インストールしてもリセットされない無料枠。1 日 1 回のパス。Transaction.updates を監視する StoreKit 2 の非消耗型アイテム。承認と購入のリクエスト、ファミリー共有、返金に対応し、最後の状態をキャッシュするので、起動時にペイウォールが一瞬表示されることもありません。
let quota = FreeQuota("summaries", free: 3) // Keychain
@State var unlock = Unlock("com.you.app.unlimited") // StoreKit 2
if quota.isAvailable || unlock.isUnlocked {
summarize(); quota.use()
} else {
showPaywall = true
}
Button("Unlock for \(unlock.price ?? "…")") { Task { try await unlock.buy() } }
Button("Restore purchase") { Task { try? await unlock.restore() } }Tools/screenshots
capture.sh はアプリをビルドし、ステータスバーをきれいな 9:41 に設定して、シミュレータで全言語の全画面を撮影します。compose.py がキャプションとデバイスフレームを付け、App Store Connect が求める 2 サイズ(6.9 インチ 1320×2868、6.3 インチ 1206×2622)で書き出します。
キャプションは macOS 自身のテキストエンジンで描画するので、アラビア語、ヘブライ語、日本語、韓国語、中国語も正しく表示されます。
PROJECT=MyApp.xcodeproj SCHEME=MyApp BUNDLE=com.you.myapp \
./capture.sh en de ja ar
python3 compose.pyほかに、LFM2 2.6B をダウンロードして任意のテキストをメモに変換し、どの層が答えたか、どれだけ時間がかかったかを表示する小さな SwiftUI アプリ OnDeviceDemo と、ONDEVICE_MODEL が GGUF ファイルを指していれば実際のモデルで統合テストを実行する Swift Testing のテストスイートも含まれます。
OnDevice Kit は週末に作ったデモではありません。theyplay.games の 2 つの iOS アプリの土台となるコードで、両方で同じ部品が必要になった段階で切り出しました。
紙の郵便物をスキャンすると、何についての書類か、何をいつまでにすべきかがわかります。Apple Intelligence、LFM2 2.6B、ルールのいずれかで、すべて iPhone 上で読み取ります。最初の 2 通は無料(キーチェーン)、その後は買い切りでアンロックします。
家の中のモノの使い方を音声で記録すると、家族やゲスト向けのわかりやすい手順になります。同じく 3 層構成で、モデルは自社サーバーから配信し、Hugging Face をフォールバックに使用。App Store のスクリーンショットは 10 言語です。
2 つのアプリから学んだことがあります。2〜3B のモデルは、指示を長くするより、プロンプトに具体例を 1 つ入れるほうがはるかに良くなります。Assistant が examples を受け取るのはそのためです。
チャットテンプレートを持つ GGUF モデルならどれでも動きます。以下は動作を確認したモデルです。LFM2 2.6B は、サイズと SHA-256 を含む ModelSpec として最初から用意されています。
| モデル | サイズ(Q4_K_M) | 得意なこと | ライセンス |
|---|---|---|---|
| LFM2 2.6B Liquid AI | 1.6 GB | 抽出、8 言語、高速 | LFM Open License(年間売上 $10M 未満は無料) |
| Qwen3 4B Instruct | 2.5 GB | 推論、多言語 | Apache 2.0 |
| Gemma 3 1B | 0.8 GB | 古い端末での短いタスク | Gemma Terms of Use |
| Llama 3.2 3B | 2.0 GB | 英語のテキスト | Llama 3.2 Community License |
リリース前に各モデルのライセンスを確認してください。モデルを配布するのはあなたのアプリです。
2 つのアプリを作る中で学んだことを、購入しなくても使えるコード付きでまとめました。
Swift Package Manager で llama.cpp を iOS アプリに追加し、GGUF モデルを Metal で読み込んで、サーバーなしで iPhone 上でテキストを生成します。
llama.cpp を GBNF 文法で制約し、小さなオンデバイスモデルが指定した形の JSON しか書けないようにします。文法の解説、Swift でのジェネレータ、注意点をまとめました。
Apple Intelligence 対応端末では Foundation Models、非対応端末ではローカル GGUF モデルやルールにフォールバック。DynamicGenerationSchema で構造化出力を統一します。
llama.cpp が長いプロンプトで GGML_ASSERT(n_tokens_all <= cparams.n_batch) により落ちる理由と、Swift での 3 つの修正方法を解説します。
大容量の GGUF モデルを URLSession のバックグラウンドダウンロードで取得。アプリのサスペンドに耐え、再起動後に再接続し、ミラーへフォールバックして SHA-256 で検証します。
数回分の無料利用を提供し、そのカウンターをキーチェーンに保存して再インストールでリセットされないようにします。アンロックは StoreKit 2 の非消耗型課金として販売し、復元にも対応します。
支払いは一度だけ。クライアント向けに開発するアプリを含め、いくつのアプリでも使えます。
$79 買い切り
対象:開発者 1 名
$199 買い切り
対象:同一企業内の開発者 10 名まで
決済と請求書の発行は Polar が行います。価格は USD で、該当する場合は付加価値税(VAT)が加算されます。12 か月の経過後も、それまでに受け取ったすべてのバージョンを引き続き使用できます。ライセンスにより、Kit を自分のアプリで使用・改変できます。ソースコードの再販売や公開は認められません。llama.cpp は MIT ライセンスで、各モデルにはそれぞれ独自のライセンスがあります。
OnDevice Kit の Swift ソースコード一式(OnDeviceLLM、OnDeviceModels、OnDeviceStore の各モジュールと、それらをまとめる OnDeviceKit)、サンプルアプリ OnDeviceDemo、テストスイート、スクリーンショットツールです。Xcode にローカルパッケージとして追加する Swift パッケージの形で提供します。llama.cpp は Swift Package Manager 経由で取り込まれます。リリース b11887 に iOS シミュレータ対応を加えただけの無改変ビルドで、バージョンとチェックサムで固定しています。
無料・有料を問わず、クライアント向けに開発するアプリも含め、自分のアプリでいくつでも Kit を使用・改変できます。Solo は開発者 1 名、Team は同一企業内の開発者 10 名までが対象です。ソースコードを単体で、またはテンプレート、スターターキット、講座の一部として再販売、サブライセンス、公開することはできません。コンパイル済みのアプリは問題ありません。アップデートは 12 か月間含まれ、それまでに受け取ったすべてのバージョンは引き続き使用できます。
動きます。llama.cpp の公式リリースには iOS シミュレータ用のスライスがないため、Kit では同じリリースをシミュレータ対応込みでビルドした無改変版をリンクしています。シミュレータではローカルモデルが CPU で動くため、2〜3B のモデルだと数秒ではなく 1〜2 分かかります。UI の開発には十分ですが、実際の速度は実機で確認してください。シミュレータで Apple Intelligence を使うには、Mac 側で Apple Intelligence をオンにする必要があります。
LFM2 2.6B(1.6 GB)のような 4 ビットの 2〜3B モデルには、RAM 6 GB 以上の iPhone が必要です。iPhone 13 Pro と Pro Max、iPhone 14 以降が該当します。それより古い端末では、Gemma 3 1B(0.8 GB)のような 1B モデルで短いタスクをこなせますし、ルール層はどの端末でも動きます。Apple Intelligence には、iOS 26 を搭載した iPhone 15 Pro 以降で、機能がオンになっていることが必要です。利用できない理由は AppleModel.unavailableReason で確認できます。
自分たちのアプリでは次のようにしています。審査メモに「アプリはリクエストに応じて言語モデルをダウンロードし、端末内で実行する」と記載する。ダウンロードを任意にするか、オンボーディングで説明する。ダウンロード前にサイズを表示する。設定画面からモデルのライセンスにリンクする。詳しくはドキュメントを参照してください。
ありません。Kit はどこにもデータを送信せず、アナリティクスも含みません。ネットワーク通信は、設定した URL からのモデルのダウンロードだけです。購入は通常どおり Apple の StoreKit を経由します。
あなたにあります。モデルを配布するのはあなたのアプリだからです。モデルは Kit に含まれません。LFM2 は LFM Open License(年間売上 $10M 未満なら商用利用無料)、Qwen3 は Apache 2.0、Gemma と Llama にはそれぞれ独自の利用規約があります。リリース前に、選んだモデルのライセンスを必ず読んでください。
Xcode 26 と Swift 6 です。アプリのターゲットは iOS 17 以降、macOS 14 以降にできます。iOS 26 と macOS 26 では、Apple Intelligence が利用可能な場合に自動的に使われます。OnDeviceKit ですべてをまとめて取り込むことも、必要なモジュールだけを使うこともできます。
購入後すぐにソースコード一式を受け取れるため、購入の取り消しはできません。ただし、お使いの環境で Kit が説明どおりに動作しない場合は、14 日以内に info@theyplay.games までメールでご連絡ください。問題の解決をお手伝いし、解決できない場合は返金します。
スキーマ、3 層構成、モデルのダウンロード、無料枠、アンロック、スクリーンショット。すべてを、あなたが所有する 1 つのパッケージで。