space ocr
ガイド記事料金ドキュメント

なぜ space-ocr は他の LLM OCR と違うのか:検証できる構造化抽出

LLM に直接 OCR をさせる場合との違い。space-ocr はフィールドをページ上の座標と verified 判定付きで返し、確認すべき項目は review の一覧に載せる。

9 分で読了· 2026-08-31

領収書や請求書を GPT-4o、Gemini、Claude に渡して、合計金額・取引先・明細を読み取ってもらうことはできる。たいていはそれらしい JSON が返ってくる。問題が出てくるのは、これを大量処理で信頼しようとしたときだ。モデルが返すのは文字列であり、文字列には所在がない。合計が 48,200 と返ってきたとして、それはページのどのピクセルを読んだ結果なのか。その数字は実際に書類へ印字されていたのか、それともモデルがもっともらしい値を埋めただけなのか。LLM を素のまま呼び出すだけでは、自分でページを読み直さないかぎりこれには答えられない。

この差こそ、汎用 LLM を OCR ツールとして使うことと、space-ocr を使うことの違いのすべてだ。space-ocr は LLM を否定するものではない。内部では現在、OCR エンジン(Google Cloud Vision)と、構造化を担う Gemini を組み合わせている — これは実装の詳細であって、API の契約ではない。space-ocr が付け加えているのは、モデルの周りに組んだレイヤーだ。返す値は、OCR が実際にページ上で認識したものと照合され、判定が付き、シートにアップロードすればそのままクエリできる 1 行として保存される。検証できる値ごとの出所と、データベースを立てずにクエリできる構造化された出力。この 2 つこそ、素の LLM 呼び出しでは自分で用意しなければならない部分だ。

素の LLM OCR と space-ocr

素の LLM OCR(GPT-4o / Gemini / Claude)space-ocr
値ごとの位置JSON 抽出の呼び出しで返るのはテキストで、別の OCR パスと突き合わせた出所座標は契約に含まれない位置が確定した値ごとにボックス(0–1000 グリッド上の xmin, ymin, xmax, ymax)と 4 点の傾き対応クアッド。確定しなかった値は review.flagged に nobox として載る
値ごとの検証なし。文字列を信じるしかない判定は cells[path].verified、理由は review.reasons。照合そのものは evidence に入る — text_match、source、そして match_ratio(値の文字のうち、ページ上で検出されたシンボルの中に見つかった割合)
宣言したルール検証は自分で書くrequired・pattern・enum・min/max・near / not_near はリクエストと一緒に宣言するとサーバー側で判定され、違反は review.flagged に載る
文脈での値の確認自分で書類を読み直すアプリでセルをクリックすると、元画像上でその領域が正確にハイライトされる
出力の形プロンプト任せで、実行のたびに変わる JSON固定スキーマ。fields を一度定義するか autoFields に提案させれば、data.values はその形で返る
保存とクエリ自分で作るPOST /ocr/fields はレスポンスで返すだけで画像を保存しない。シートにアップロードすれば 1 ページが 1 行になり、GET /view(where, sort, select, limit, offset)でクエリできる。OCR の再実行なし、課金なし
文字種モデルとプロンプト次第日本語・韓国語・中国語・英語ほかを自動判定。言語パラメータは不要
セットアッププロンプト・リトライ・パース・検証のパイプラインを自作Bearer キー付きの HTTPS 呼び出し 1 回
✓ Verified

「検証済み」が実際に何を意味するのかをはっきりさせておきたい。誇張しやすいところだからだ。言語モデルは座標を出力しない。返すのは各値のテキストと単語トークンのヒントだけで、そこからエンジンが、そのテキストを Google Cloud Vision がページ上で検出したシンボルと 1 文字ずつ照合する。実在するシンボルの上にボックスを合わせ、その照合の結果は cells[path].evidence に載る。文字照合そのものが text_match、値の文字のうち見つかった割合が match_ratio、ボックスをどう求めたかが source だ。verified はその上に立つ判定で、理由が 1 つでも付けば false、照合が走って何も立たなければ true、照合する相手が無ければ null になる。一致が弱ければ low_ratio のような理由が立ち、そのパスが review.flagged に並ぶ — 人の確認に回す一覧はこれだ。values はモデルが読んだ文字列であってバイト単位の複製ではないので、マスタと完全一致で突き合わせたいときは evidence.printed_text(その座標で OCR が読んだ文字)を使う。これはモデルが決して間違えないという約束ではない。トークンのヒントはずれることがあるし、二つのエンジンが同じ誤読で一致してしまうこともある。意味するのは、各値を鵜呑みにするのではなく、ページと照合して結果を知らせているということだ。

POST /ocr/fields — レスポンス内の 1 フィールド
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
{
  "status": "success",
  "data": {
    "values": {
      "vendor": "ACME Trading Co."
    },
    "cells": {
      "vendor": {
        "box": { "xmin": 120, "ymin": 84, "xmax": 512, "ymax": 118 },
        "quad": [
          { "x": 120, "y": 84 }, { "x": 512, "y": 84 },
          { "x": 512, "y": 118 }, { "x": 120, "y": 118 }
        ],
        "verified": true,
        "review": null,
        "evidence": {
          "text_match": true,
          "source": "vision_symbol_match",
          "match_ratio": 1.0,
          "ocr_confidence": 0.97
        }
      }
    },
    "review": {
      "unit": "field",
      "declared": 1,
      "returned": 1,
      "boxed": 1,
      "verified": 1,
      "flagged": []
    },
    "image": { "width": 1654, "height": 2339 }
  }
}

ボックスは画像サイズに依存しない 0–1000 グリッド上で返るので、描画するときはピクセルにスケールする:pixel_x = xmin / 1000 * image_width。幅と高さは同じレスポンスの data.image から取る — これは実際に読み取ったページ(EXIF の回転を反映済み、大きな写真は縮小済み)であって、送ったファイルそのものではない。4 点のクアッドは傾いたり回転したりしたスキャンに追従し、左上・右上・右下・左下の順で並ぶ。何かが合わなければ、同じパスが data.review.flagged に { "path": "total", "reasons": ["text_mismatch"] } の形で載る。確認に回すのは、しきい値を決めて切るスコアではなく、そのまま辿れる一覧だ。汎用モデルへの JSON 抽出の呼び出しにこれらは付いてこないので、監査証跡が欲しければ自分で組み立てることになる。

値からクエリできるテーブルへ

素の LLM 呼び出しは JSON で終わる。それを自分で永続化しなければならず、「今四半期で 40,000 を超える請求書はどれか」と尋ねたくなった時点で、まずデータベースとクエリ層を作ることになる。POST /ocr/fields も JSON で終わる — レスポンスで返すだけで、画像は保存しない。違うのは、同じ抽出を保存レイヤーに通せることだ。列を決めてシートを作り(POST /create)、そこへページをアップロードすれば(POST /upload)、1 ページが同じ values・cells・review を持つ 1 行になる。あとはクエリが API 呼び出しで済む:GET /view に where=total>=40000、sort=-invoice_date、select=vendor,total、それにページングの limit と offset を渡す。処理はサーバー側で走り、OCR を再実行せず、課金もされない。シートは CSV にエクスポートできる(BOM 付き UTF-8 なので、日本語・韓国語・中国語のテキストや通貨も Excel で正しく開き、明細の配列はそれぞれ独立した行に展開される)。

素の LLM のほうが向いている場面

汎用 LLM が正しい選択になるのは、一度きりの読み取りや、ざっくりした要約、書類の意味を推論したいときだ。「この契約は何についてのものか」はモデル向きの問いであって、座標付き OCR の問いではない。space-ocr を持ち出すのは、書類を大量に処理していて、各値が検証でき、一貫して構造化され、クエリできる必要があるときだ。買掛金処理の自動化、経費の突き合わせ、名刺の CRM への取り込み、たまった領収書のデジタル化などがこれにあたる。正直に位置づけるなら、space-ocr は検証と保存のレイヤーを備えた LLM ベースの OCR であって、モデルそのものの競合ではない。

料金はスキャンごとの従量課金で、任意で月額プランも選べる。毎月一定数の無料スキャンが付き、失敗したスキャンには課金されない。現在の金額は料金ページに載せている。

space-ocr は OCR 用途で GPT-4o や Gemini を置き換えるものですか?
いいえ。space-ocr は内部で LLM を使っています。現在のスタックは OCR に Google Cloud Vision、構造化に Gemini を組み合わせたもので、これは実装の詳細であって API の契約ではありません。違いはモデルの周りのレイヤーです。各値をページのシンボルと照合して判定を付け、シートにアップロードすれば結果はクエリできる行として保存されます。モデルの置き換えではなく、LLM ベースの OCR です。
値が幻覚でないと、どうやって分かりますか?
見るところは 3 つです。cells[path].verified が判定で、理由が 1 つでも付けば false、照合が走って何も立たなければ true、照合する相手が無ければ null になります。data.review.flagged は確認すべきパスの一覧で、それぞれに reasons が付きます。cells[path].evidence には照合そのもの(text_match・match_ratio・source)が入りますが、これは照合が走った値にだけ付くキーです。キーが無いことは「問題なし」ではなく「判断できなかった」を意味します。実際のページと照合する仕組みであって、モデルが決して間違えないという保証ではありません。二つのエンジンが同じ誤読で一致してしまうこともあります。
space-ocr はどんな座標形式を返しますか?
正規化された 0–1000 グリッド上の 4 つの整数 xmin, ymin, xmax, ymax によるボックスと、傾いたり回転したりしたページ向けの 4 点の向き付きクアッド(quad)です。クアッドの頂点は左上・右上・右下・左下の順に並びます。ピクセルへは同じレスポンスの data.image を基準に換算します。例:pixel_x = xmin / 1000 * image_width。
自分のデータベースなしで、抽出したデータをクエリできますか?
はい、ページがシートに入っていれば可能です。POST /ocr/fields は JSON を返すだけで何も保存しません。シートを作り(POST /create)ページをアップロードすると(POST /upload)、1 ページが 1 行になります。あとは GET /view が where、sort、select、limit、offset でサーバー側からフィルタします(例:where=total>=40000)。OCR を再実行せず、課金もされません。シートは CSV にエクスポートできます。
書類の言語を space-ocr に指定する必要はありますか?
いいえ。言語は日本語・韓国語・中国語・英語ほかにわたって自動判定されます。設定する言語パラメータはありません。
PDF は読めますか?
Web アプリは PDF の各ページを画像にラスタライズしてから OCR します。API 自体はラスター画像(JPEG, PNG, GIF, BMP, TIFF, WebP)を受け取り、1 回の呼び出しにつき 1 枚なので、送る前に PDF のページを画像に変換してください。
どうやって呼び出しますか?
Bearer の spocr_ キーを付けて POST /ocr/fields に HTTPS リクエストを 1 回送り、画像と fields スキーマを渡します(autoFields: true で提案させることもできます)。レスポンスは 2 つに分かれていて、data.values には宣言したスキーマだけが入り、data.cells[path] に値ごとの box・quad・verified・review・evidence が、data.review に確認すべき項目のまとめが入ります。