HOWLRSProduct engineering相談する
Build notes
AI・PWAデモ公開

Generic Camera — カメラで撮るだけで「何か」を判定する汎用 PWA

魚種・果実成熟度・汎用識別など、判定ドメインを切り替えられる Rust + Next.js のカメラ PWA。Gemini をバックエンドに、現場利用を想定した 3 種類の判定フローを staging デモとして公開しています。

Project snapshot

この実装で扱ったこと

Stack
  • Next.js
  • Rust
  • axum
  • Gemini API
  • Cloud Run
目次 15 sections
  1. Generic Camera とは
  2. 何を解決するのか
  3. アクセスコード方式で営業デモに直行する
  4. 確信度は星 5 段階で出す
  5. モバイル前提でレイアウトする
  6. バックエンドは Rust + axum + Gemini
  7. デプロイは GitHub Actions → Cloud Run
  8. staging 環境を今すぐ試す
  9. QR で今すぐ試す
  10. 設計上のこだわり
  11. 12 専門家ペルソナ × Gemini Pro レビュー
  12. Plan 1 → Plan 2 / Plan 3 の差分管理
  13. iOS Safari ダークモードの罠を踏み抜く
  14. 既知の制約
  15. まとめ

Generic Camera とは

漁港、農園、検品ラインといった「目で見て判定する」業務には共通の課題があります。 カメラで対象を撮影し、その場で AI に判定させ、結果を即座に返す — このフローを最短経路で実装した汎用 PWA が Generic Camera です。

ドメイン (判定対象の領域) を YAML で定義するだけで、UI もスキーマもそのまま流用できます。リリース時点では「汎用判定」「魚種判定」「みかん成熟度判定」の 3 ドメインを同梱しており、画面上のタブを切り替えるだけで判定対象を切り替えられます。

ソフトウェアエンジニアの寺島和宏 (howlrs) が、現場の判定業務を「スマホ 1 台 + ブラウザ」で立ち上げられるようにするために設計しました。

Generic Camera のメイン画面。ヘッダーに「Generic Camera (デモモード)」と 3 つのドメイン切替タブ、中央に大きな「カメラを起動」ボタン。

何を解決するのか

現場の判定業務をデジタル化しようとすると、たいてい次のような壁にぶつかります。

  • ドメインごとに専用アプリを作ると保守コストが膨らむ
  • ネイティブアプリにすると配布・端末管理・OS 追従が重い
  • 営業デモ用と本番運用用でコードベースが分かれてしまう

Generic Camera は「カメラ + 判定」という共通の骨格を 1 つの PWA に固定し、判定ロジックの差分を YAML 1 ファイルで吸収する 設計にしました。新しい判定対象にも、UI の共通部分を保ったままドメイン定義を追加して対応できます。

アクセスコード方式で営業デモに直行する

最初の画面はアクセスコード入力です。?code=XXXX のクエリで起動するか、画面上のフォームに入力すれば、その値が localStorage に保存されて以降は省略されます。

このアクセスコードは Next.js Route Handler 側で API キーにマップし、ブラウザへのレスポンスには API キーを含めない構造です。共有先には有効期間を限定したコードを別経路で案内し、デモ環境への入口として使います。

商談が終わってアクセスコードを失効させたければ、サーバ側のシークレットを書き換えるだけ。ブラウザに残ったコード自体は鍵としての効力を失うので、端末回収不要で「貸出デモ」を回せます。

確信度は星 5 段階で出す

判定結果は LLM の生確率や confidence 数値ではなく、星 5 段階 で表示します。「0.83」と「0.84」を比べる時間は現場には無いので、人間が瞬時に直感できる粒度に丸めるのが正解、という設計判断です。

確信度の生数値は折り畳み JSON ビューに格納してあります。営業先から「これ、内部的にはどんなパラメータで判定してるの?」と問われた瞬間に展開して見せるためです。説明責任は果たしつつ、メイン UI は誤解されにくいレベルに整えています。

モバイル前提でレイアウトする

PWA として実機に入れて使う想定なので、すべての UI は最初からモバイル幅で組まれています。max-w-md mx-auto でレイアウトを固定し、デスクトップで開いても崩れません。

iPhone 14 サイズで撮影したメイン画面。ヘッダーが画面幅いっぱいに広がり、ドメインタブとカメラ起動ボタンが指で押しやすい大きさで縦に並んでいる。

カメラ起動時には getUserMedia({ facingMode: "environment" }) で背面カメラを優先し、撮影画像は 長辺 1024px / JPEG q=0.85 にリサイズしてから API に送ります。回線が細い現場でもタイムアウトしにくいよう、ローディングメッセージは 10 秒・30 秒・60 秒で段階的に表現を切り替え、利用者を不安にさせません。

バックエンドは Rust + axum + Gemini

API 側 (apps/api/) は Rust + axum で書かれており、エンドポイントは /v1/recognize / /v1/domains / /health の 3 本だけ。受け取った画像を Gemini API (gemini-3.1-flash-lite-preview) に流し、ドメイン定義 YAML に紐付いた JSON Schema で結果を再検証してから返します。

Gemini が出力スキーマを破ったときは、 label="parse_error" で必ず生存 します。「現場でアプリが落ちる」を最も避けたい挙動として、部分パースを許容する設計にしました。営業デモ中に LLM がコケても画面は動き続けます。

レート制限は API キー単位で governor クレートを使って RATE_LIMIT_PER_MIN=30 に。撮影画像は base64 でサーバを 1 度通過するだけで、永続化は一切しません。

デプロイは GitHub Actions → Cloud Run

インフラは Cloud Run (asia-northeast1) の staging 環境で運用しています。API と PWA の 2 サービスを GitHub Actions からデプロイします。

  • api-deploy.yml / pwa-deploy.yml で main push → staging へ自動反映

prod 環境は終了しており、利用者にはアクセスコード付きの staging URL を案内しています。

Secret Manager で API キーやアクセスコードを管理し、Cloud Run にはランタイムで注入。Service Account github-actions-deployer には必要最低限の 7 ロールしか持たせていません。

ビルドイメージにもこだわりがあり、 API 側の distroless イメージは sha256 digest pin で再現性を確保しつつ、Bookworm ベースの builder と glibc を揃えています (Trixie ベースの rust:slim を使うと distroless cc-debian12 で GLIBC_2.38 not found で落ちる罠を踏み抜きました)。

staging 環境を今すぐ試す

Generic Camera は prod を終了し、現在は staging 環境で提供しています。公開URLは以下です。利用条件と画面上の案内を確認してから試してください。

https://generic-camera-pwa-staging-giyklvtu5q-an.a.run.app/

リンクを開いた後、画面上で「汎用判定」、魚種判定、みかん成熟度判定を切り替えられます。

QR で今すぐ試す

スマホでは上記のURLを直接開いてください。背面カメラと中央ガイド枠が立ち上がったら、手元にあるものを撮影して AI 判定を試せます。

設計上のこだわり

12 専門家ペルソナ × Gemini Pro レビュー

設計仕様 (docs/superpowers/specs/2026-04-27-generic-camera-design.md) は 12 専門家ペルソナの議論と Gemini Pro による deep レビューを経て確定しました。MVP に何を残し、何を Plan 2 / Plan 3 に送るかを 1 件ずつ判断し、carry-forward 注記まで仕様書に残してあります。

Plan 1 → Plan 2 / Plan 3 の差分管理

最初の Plan 1 は「Hello from API / PWA を Cloud Run に乗せる」ところまで。ここで許容した妥協 (例: digest pin なし、graceful shutdown なし、USER root、package-lock.json 未コミット) は、すべて Plan 2 / Plan 3 の plan markdown に carry-forward 項目として転記してあります。「あとで直す」が泡のように消えない仕組みです。

iOS Safari ダークモードの罠を踏み抜く

color-scheme: light dark を入れた状態で値の <span> に明示的な text-color を付けないと、iOS Safari がダークモード時に UA 既定で白文字にしてしまい、bg-white のカード上で値が完全に消えるという地雷を発見しました。修正は color-scheme: light 固定 + 値側にも明示的な色クラス。同じ罠を踏まないように設計知見として残しています。

既知の制約

  • アクセスコードは平文 localStorage 保存です。営業デモ用途で割り切っており、ローテーション前提で運用しています
  • Gemini Free tier ではレートが不足するため、有償 API キー前提です
  • 撮影画像は base64 で 1 度だけサーバを通過し、永続化はしません

まとめ

「カメラで撮るだけで何かを判定する」という、現場でよく出てくる業務パターンを、 ドメイン定義を差し替えるだけで再利用できる形 に切り出した PWA です。現在は staging 環境で、スマホ 1 台から利用を始められます。

Keep exploring

開発支援

Claude Checker — Claude Code セッションをかんばんで監視するローカルダッシュボード

6 min read
金融ツール

レンコンシェフ / Lotus root Chef — 価格追従型の自動指値デスクトップアプリ(Rust製)

3 min read
デスクトップツール

N Delete(エヌデリ) — ファイル多重上書き削除の試作ツール

3 min read