# ローカルAI OCR 基盤（Mac mini）

Mac mini 上のローカル生成AI（Ollama）を使い、**画像から文字を読み取る**仕組み。
入口は zsystem.jp のブラウザ画面で、画像＋指示（プロンプト）を送ると結果テキストが返る。

- 初期構築: 2026-06-18〜19（JST）
- このドキュメントの場所: `Dropbox/www/macmini/ollama-ocr.md`
- ※**コード本体は Dropbox 外**（`/Users/menou/ollama-ocr/`）。このファイルは「何をどう設定したか」の記録。

---

## 1. 全体構成（データの流れ）

```
ブラウザ（画像アップロード＋AIへの指示）
  │  HTTP POST (multipart)
  ▼
zsystem.jp /ocrtest/index.php   ← PHP 5.3（古い構文NG）※現状ローカルのみ・未デプロイ
  │  cURL POST {image(base64), prompt?} + X-API-Key
  ▼
[SSHリバーストンネル]  Mac mini → zsystem.jp、zs:127.0.0.1:11500 → Mac:127.0.0.1:11500
  ▼
Mac mini ocr.php（php -S 127.0.0.1:11500）  ← PHP 8.5
  │  APIキー検証 → Ollama へ {model, prompt, images:[base64]}
  ▼
Ollama（127.0.0.1:11434） → qwen2.5vl:7b で読み取り
  ▲────────────── 結果テキストを逆順で返す ──────────────┘
```

ポイント:
- **ブラウザは zsystem.jp としか通信しない**。Mac mini は外部に一切公開しない。
- zs サーバー内の `localhost:11500` がトンネル経由で Mac mini の OCR ラッパーに届く。
- Ollama 本体（11434）は認証が無いので**絶対に外部公開しない**。間に必ずラッパー（APIキー認証）を挟む。

---

## 2. 環境

| 項目 | 内容 |
|---|---|
| 機種 | Apple M4 Mac mini / メモリ 24GB |
| OS | macOS 27.0（プレリリース） |
| Ollama | Homebrew formula 版（`brew install ollama`）、`brew services` で常駐 |
| Ollama API | `http://127.0.0.1:11434`（独自API） / `…/v1`（OpenAI互換） |
| ラッパー言語 | PHP 8.5（Mac側）／ 入口は PHP 5.3（zsystem.jp側） |

### 導入済みモデル
| モデル | 用途 | 備考 |
|---|---|---|
| **qwen2.5vl:7b** | **OCR本番（既定）** | 日本語OCRに強い。**これを使う** |
| qwen2.5:7b | テキスト生成・tool calling・エージェント | 画像なし用途 |
| llama3.2:3b | 軽量・動作確認用 | |
| ~~llama3.2-vision:11b~~ | （使えない） | **このOllamaは mllama 非対応で読込不可**。OCRには qwen2.5vl を採用した経緯 |

24GB の安全圏は 7B〜14B 級。32B は無理せず見送り。

---

## 3. コンポーネントと場所

### Mac mini 側： `/Users/menou/ollama-ocr/`（Dropbox外）
| ファイル | 役割 |
|---|---|
| `ocr.php` | ラッパーAPI。`/health`（疎通）と `/ocr`（OCR実行）。`set_time_limit(0)`、`CURLOPT_TIMEOUT=300` |
| `apikey.txt` | 共有APIキー（`chmod 600`）。zs側 index.php の `$OCR_APIKEY` と一致必須 |
| `serve.sh` | ラッパー手動起動（`php -S 127.0.0.1:11500 ocr.php`） |
| `start-tunnel.sh` | autossh リバーストンネル起動（接続先は zsystem.jp/CLAUDE.md 準拠） |
| `wrapper.log` / `tunnel.log` | launchd の標準出力ログ |
| `test.png` | 動作確認用テスト画像（日本語＋英数字） |

### 自動起動： `~/Library/LaunchAgents/`
| plist | 内容 |
|---|---|
| `com.menou.ollama-ocr.wrapper.plist` | ラッパーを RunAtLoad + KeepAlive で常駐 |
| `com.menou.ollama-ocr.tunnel.plist` | autossh トンネルを RunAtLoad + KeepAlive で常駐 |

※ いずれも**ユーザーLaunchAgent**（Ollama 本体と同方式）。ログインユーザーのセッションがある間だけ動く。完全ヘッダレス運用には自動ログイン設定が別途必要。

### zsystem.jp 側： `…/Dropbox/www/zsystem.jp/ocrtest/index.php`
- ブラウザ用アップロード画面。**PHP 5.3 互換で記述**（`??`・`[]`・アロー関数・型宣言は使わない）。
- 「AIへの指示」テキスト欄＋プリセット（カンバン＋現品ラベル / 伝票→JSON / 名刺）。
- `set_time_limit(360)`、`curl_close` は使わない（PHP8で deprecated・5.3でも単発不要）。
- **現状ローカルのみ。zsystem.jp サーバーへは未デプロイ。**

---

## 4. ラッパー API 仕様

```
GET  /health
  → {"ok":true,"service":"ollama-ocr"}

POST /ocr
  ヘッダ: X-API-Key: <apikey.txt の値>
  ボディ(JSON): {"image":"<base64>", "prompt":"(任意)", "model":"(任意)", "num_ctx":"(任意)"}
  応答(JSON): {"ok":true,"text":"...","model":"qwen2.5vl:7b","elapsed_ms":1234}
```
- `prompt` 省略時は「画像の文字をそのまま全部書き出す」既定プロンプト。
- **`num_ctx` 既定 8192**（2026-07-31 変更、範囲 2048〜32768）。
  Ollama の既定 4096 では、画像トークン＋長めのプロンプトで
  `exceed_context_size_error`（例: request 4924 tokens > 4096）になり **HTTP 400** が返る。
  項目を多く指定するプロンプト（例: ellio/shearspec の28項目読み取り）では必ず超えるため既定を引き上げた。
  さらに長いプロンプトを使う場合は呼び出し側で `num_ctx` を指定する。
- 指示例：「カンバンの5桁の数字と現品ラベルの内容を読み取って返して」「伝票番号・日付・金額をJSONで」など。

---

## 5. 接続情報（要点）

| 項目 | 値 |
|---|---|
| OCRラッパー ポート | 127.0.0.1:**11500** |
| Ollama ポート | 127.0.0.1:**11434** |
| トンネル | Mac → zsystem.jp（ポート2222, ssh-rsa必須, zadmin） / `-R 127.0.0.1:11500:127.0.0.1:11500` |
| 認証 | 共有APIキー（`apikey.txt`） |

※ zsystem.jp の SSH 詳細・パスワードは `Dropbox/www/zsystem.jp/CLAUDE.md` に集約。

---

## 6. 運用コマンド

```bash
# 状態確認
launchctl list | grep menou.ollama-ocr
curl -s http://127.0.0.1:11500/health

# 再起動 / 停止 / 再登録
launchctl kickstart -k gui/$(id -u)/com.menou.ollama-ocr.wrapper
launchctl kickstart -k gui/$(id -u)/com.menou.ollama-ocr.tunnel
launchctl bootout   gui/$(id -u)/com.menou.ollama-ocr.wrapper   # 停止
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.menou.ollama-ocr.wrapper.plist  # 再登録

# ログ
tail -f /Users/menou/ollama-ocr/wrapper.log
tail -f /Users/menou/ollama-ocr/tunnel.log

# Ollama 本体
ollama list
brew services restart ollama
```

---

## 7. トラブルシュート / ハマりどころ

| 症状 | 原因 | 対処 |
|---|---|---|
| `Could not connect to 127.0.0.1 port 11500` | ラッパーが落ちている／トンネル未確立 | `launchctl list \| grep ollama-ocr`、`curl …/health`。落ちていれば kickstart |
| ラッパーが突然停止 | **PHP 30秒制限**で `curl_exec` 中に Fatal → `php -S` ごと停止 | `ocr.php` の `set_time_limit(0)` で対策済み |
| OCRが極端に遅い／30秒超 | **mediaanalysisd** 等がCPU 100%占有して推論が遅い | `sudo killall mediaanalysisd`（launchdが自動再起動。可逆）。macOS27プレリリースで張り付く既知挙動 |
| `exceed_context_size_error`（HTTP 400 / 502として返る） | Ollama既定の `num_ctx=4096` を画像トークン＋プロンプトが超過 | `ocr.php` で `options.num_ctx` を指定（既定8192）。足りなければ呼び出し側から `num_ctx` を上げる |
| `unknown model architecture: 'mllama'` | このOllamaは llama3.2-vision 非対応 | OCRは **qwen2.5vl** を使う（採用済み） |
| `curl_close() is deprecated`（PHP8.5） | PHP8で無効化 | 呼ばない（削除済み）。PHP5.3でも単発スクリプトでは不要 |
| zsで動かない構文エラー | リモートは **PHP 5.3** | `??`/`[]`/アロー関数/型宣言を使わない |

---

## 8. セキュリティ

- Ollama（11434）は外部非公開。トンネルは zs 側 `localhost` のみバインド。
- ラッパーは APIキー必須（`hash_equals` で比較）。キーは `apikey.txt`(600)・zs index.php の双方に同値。
- キーは秘密情報。公開リポジトリ等に置かない。

---

## 9. 未実施 / TODO

- [ ] **zsystem.jp サーバーへ `ocrtest/index.php` をデプロイ**（差分提示→承認→デプロイのルール厳守）。デプロイ後、実機 PHP5.3 で `set_time_limit` が有効か確認。
- [ ] 必要なら完全ヘッダレス運用（自動ログイン設定）。
- [ ] コード本体（`~/ollama-ocr/`）は Dropbox 外＝バックアップ対象外。必要ならコピーを macmini 配下に保管検討。
- [ ] 精度が不足する帳票向けに、用途別プロンプト／出力JSON固定／上位モデル比較（Apple Vision・Tesseract 等）。
