TextPluck ができるまで — ブラウザ内tesseractで画像から文字を抜く設計
tesseract.js + WASM SIMD でブラウザ完結のOCRを実装したWebサービスの設計記録。日本語OCRの落とし穴、UI上の工夫、スキャンビーム演出までを解説します。
TextPluck ができるまで — ブラウザ内tesseractで画像から文字を抜く設計
TextPluck は、画像内の文字を アップロードせずに 読み取るWebサービスです。 tesseract.js を WebAssembly でブラウザ内に呼び込み、 日本語・英語・中国語・韓国語の OCR をすべてクライアント側で完結させました。 voice-scribe (音声→テキスト) と対をなす、画像→テキストの軸として置いています。
なぜこの形にしたか
OCR は需要が広い割に、 「無料のオンラインOCR」 サイトの多くは画像を自社サーバーに送る前提です。 レシート、契約書、本人確認書類、スクリーンショット、子供のノートの写真。 これらを得体の知れないサイトにアップロードするのは正直やりたくない。
tesseract.js は LSTM ベースのOCRエンジン tesseract を WebAssembly でブラウザに持ち込むライブラリで、 SIMD 対応の WASM が普通に動く今、 実用速度で OCR を完結させられます。 voice-scribe (音声) → clip-cast (動画) → bg-snap (画像背景透過) と続く ラボの「ブラウザ完結メディア処理」 路線の4本目として、これを採用しました。
visual direction
フォレンジック・ドキュメントスキャナー。
- 深いミッドナイトブルー (
#060a14) を背景に、コバルト (#5b8def) を主アクセント - 上下に流れるシアン (
#7ed8ff) のスキャンビームを画像領域に常時表示 (OCR 中はもっと速く) - voice-scribe (mint cyan / 縦オシロ) / clip-cast (amber / 横タイムライン) / bg-snap (violet / フォトスタジオ) と意図的に温度・形・モチーフをずらしてある
- スキャンライン演出は ヒーロー背景にも薄く入れて、サービス全体に「読み取っている感」を出す
実装の見どころ
1. 言語切替で再OCRをトリガ
OCR は言語選択で結果が劇的に変わります。 日本語と英語が混ざった画像で eng だけを使うと日本語部分が全部 garbage に なります。
実装では言語チップ (jpn / jpn+eng / eng / chi_sim / kor) を上に置いて、 クリックしたら即時に同じ画像で再OCR する形にしました。 ユーザーが結果を見ながらリトライできるのは tesseract の精度限界を補う上で大きい。
const chip = (l: OcrLang) => (
<button onClick={() => rerunWithLang(l)} ...>{label(l)}</button>
)
2. worker をキャッシュ
createWorker は初回 ~3-5秒かかります (WASM ロード + 言語モデルDL)。 言語ごとにキャッシュして再利用しています。
const workerCache = new Map<OcrLang, Worker>()
// 2回目以降は cached.recognize(img) だけで済む
これで「言語チップを切り替えるたびに再OCR」が現実的な速度になります。
3. スキャンビーム演出
OCR 実行中は画像領域にシアンの水平ビームを keyframes で上下に走らせる:
.scanBeam {
background: linear-gradient(180deg, transparent 45%,
rgba(126,216,255,0.9) 50%, transparent 55%);
background-size: 100% 100%;
animation: tp-scan 1.8s linear infinite;
mix-blend-mode: screen;
}
@keyframes tp-scan {
0% { background-position: 0 -100%; }
100% { background-position: 0 200%; }
}
mix-blend-mode: screen で下の画像に明色合成されて、 古典的なスキャナーの読み取り光が現実っぽく走ります。 「何かが起きている」 という感覚をはっきり出すための演出です。
4. 信頼度を一緒に出す
OCR は確実に間違えるので、結果と一緒に confidence を表示しました。 80% 未満なら別言語で試すか、画像を撮り直すかの判断材料になります。 単に結果だけ出して放置するより、 ユーザーが次の手を打ちやすくなります。
5. dynamic import + ssr:false
tesseract.js も VoiceScribe / ClipCast / BGSnap と同様、 client component 専用です。 Next.js 16 では Server Component 側から ssr:false を付けられないので、 loader を別ファイルに切り出して dynamic(() => import(...), { ssr: false }) で囲んでいます。
苦労したところ
tesseract.jsの logger callback は型が緩く、 status string の中身がバージョン依存。 ステップ判定はincludes("recogni")などの文字列マッチで凌いだ。- 日本語モデルの初回ロード時間。 ~5-15MB だがネット越しなので3-8秒程度かかる。 progress を見せて 「読み込み → 認識」 の2段階で UI に反映する。
- 手書き文字は厳しい。 印刷物中心のユースケースであることを FAQ で明記し、 将来 TrOCR への切替を予告した。
今後の拡張
- 複数画像のバッチ処理: 1枚ずつではなく、複数ファイルをキューで連続OCR
- PDF ページ単位の読み込み: PDF をクライアント側でレンダリングして各ページ OCR
- TrOCR / PaddleOCR への切替: 精度重視モードを追加 (重量級モデルだが日本語も強い)
- 座標 (bbox) 付き結果のエクスポート: hOCR / ALTO 形式での書き出し
- clip-cast との連携: 動画フレームから抽出 → テキスト書き起こし
- bg-snap との連携: 背景透過してから OCR、というワークフロー
このサービスから言える事
ラボの「ブラウザ完結メディア処理」が4本そろいました:
- voice-scribe (音声 → テキスト)
- clip-cast (動画 → トリミング)
- bg-snap (画像 → 透過)
- text-pluck (画像 → テキスト)
メディア処理の代表的なケースは、サーバーに上げる必要が無くなった、 ということを4方向から実証しています。 「無料SaaSにアップロードする」 という UX の標準仕様そのものを更新するべきタイミングが来ています。
Next Action
読んだあとに、そのまま使って確かめる。
この開発ログは TextPluck をどう育てているかの記録です。読んだらそのままサービス本体へ戻って、価値を確かめられるようにしています。