帳票の画像を、スクリーンリーダーが意味の通る順で読める形に組み直すデモです。
同じ画像に対する3通りの読み取り結果を1画面に並べます。
| 欄 | 中身 |
|---|---|
| A | /ocr/text の出力をそのまま |
| B | /ocr/markdown の出力を、Markdown の表記どおりに HTML へ変換したもの |
| C | /ocr/fields と /ocr/markdown の cells の座標から、読み上げ順を組み直したもの |
A と B で崩れている箇所は直していません。直すと比較になりません。
/ocr/markdown は文書の見た目を素直に写します。そのため横に並んだ2つの表は「1つの8列の表」として返ります。
| 検査項目 | 今回 | 基準値 | 判定 | 検査項目 | 今回 | 基準値 | 判定 |
| 収縮期血圧 | 128 | 100~139 | | 総コレステロール | 210 | 140~219 | |
↑ ここから右は別の検査
目で見れば罫線で分かりますが、読み上げでは左右の別々の検査が1行に混ざります。座標を使うと機械的に分離できます。
- 要素の並び順は再帰的な XY カットで決めます。横向きの空白帯で切れるなら切り、切れなければ縦向きの空白帯で切ります。請求書の下部(左に備考、右に合計欄)もこれで分かれます。
- 表の中の段組は、列の中心座標の周期性で判定します。
⚠️ 隙間の大きさでは判定できません——健診結果の見本では、いちばん広い隙間(110px)は段の境界ではなく、境界は90pxで2番目でした。列中心の差は 569.8 / 570.4 / 570.5 / 570.4 px(ずれ 0.65px)で、右半分が左半分の平行移動であることがはっきり出ます。
- 同じ値が別の列に出ていないかを座標から判定します(「単価 60,000」と「金額 60,000」など)
evidence.sourceとocr_confidenceを見て、座標の確からしさを枠の描き分けに反映しますreview.reasonsを補助として足します- 検算——請求書は明細合計と小計・消費税・合計・請求金額の突き合わせ、健診表は今回値・基準値・判定記号の整合
元画像の上に、cells の座標どおりの位置で、読み上げ順に並んだフォーカス可能な領域を置きます。
- Tab を押すと、読み上げ順に移動します
- 枠は CSS の
:focusで描きます(JavaScript の同期処理を使っていないのでずれません) - 実線=値、破線=補完に使った列見出し。列見出しは値と離れた場所にあるので、「今回 128」という一文が画像上の2か所の合成であることが見えます
pip install fastapi uvicorn jinja2 python-multipart pillow
python -m uvicorn app.main:app --port 8000そのうえで、次のURLを開いてください。見本の解析から読み上げの再生まで自動で走ります。
http://localhost:8000/?doc=health_check&autoplay=1&delay=6&pace=3.5
doc は health_check(健康診断の結果表)か invoice(請求書)。
pace は1項目あたりの秒数、delay は再生を始めるまでの秒数です。
画面の「この端末の音声で読む」ボタンを押すと、同じ文をブラウザの音声合成で読み上げます。読み上げ順が組み直されていることは、これで確認できます。
🔴 スクリーンリーダーをお使いの方は、このボタンを押さないでください。読み上げが二重に鳴ります。
- Python 3.12 以上
pip install fastapi uvicorn jinja2 python-multipart pillowpillowは見本画像の生成にだけ使います- 🔵 読み上げ順の再構成(
app/reorder.py)と検証(app/verify.py)は標準ライブラリだけで動きます
cp .env.example .env
# .env を開いて SPACE_OCR_API_KEY を入れる🔴 .env は公開物に含めません(.gitignore 済み)。コードにキーを書いている箇所はありません。
.env に SPACE_OCR_CACHE=1 を置くと、画像のハッシュをキーに応答を .cache/ へ保存して使い回します。既定は 0(毎回APIを呼びます)。
python src/freeze_saved_response.py # 保存済みの正常な応答を .cache に入れる
python src/pick_good_response.py health_check 12 # 当たりを引いて .cache に固定する
python src/pick_good_response.py health_check 12 measure # 固定せず、安定性を数えるだけ
python tools/summarize_gacha.py # 抽選ログを表にまとめる
python src/pick_good_response.py invoice 10 # または、通った回を引いて固定するpython -m uvicorn app.main:app --reload --port 8000http://localhost:8000 を開きます。
- 書類の種類を選ぶ(請求書 / 健康診断の結果表)
- 画像を選ぶ(ドラッグ&ドロップも可)か、「見本を使って解析する」を押す
- 5〜20秒で結果が出ます(3エンドポイントを並列で呼びます)
- C-1 の画像の中を Tab で送ると、読み上げ順に枠が移ります
samples/ に3枚あります。実在の個人情報・機密情報は使っていません。すべて架空です。
| 見本 | 何を確かめるためのものか |
|---|---|
sample_invoice.png |
罫線のある明細表。同じ数字を意図的に重ねてある(「ご請求金額 132,000」と「合計 132,000」、明細1行目の「単価 60,000」と「金額 60,000」)。下部は左に備考・右に合計欄で、要素レベルの2段組になっている |
sample_health.png |
2段組の検査結果。判定記号 H / L が値と別のセルにある。「38」を左右の段にまたいで2回、「40」を基準値の中で2回重ねてある |
sample_meter.png |
検針票。ラベルが上・値が下の箱が横に並ぶ。単位(kWh)が値と離れ、マイナスの金額を含む。引き算の検算(今回指示数 − 前回指示数 = ご使用量)が立つ |
生成し直す場合:
python samples/make_sample_invoice.py
python samples/make_sample_health.py
python samples/make_sample_meter.py画面の枠が正しい位置に出ているかは、同じ座標を画像に焼いて確認できます。
python tools/draw_boxes.py health all # 全単位の主枠
python tools/draw_boxes.py health 9 # 9番目の 主枠+値+列見出しapp/
main.py FastAPI(画面と /analyze)
spaceocr.py APIクライアント(3エンドポイントを並列で呼ぶ)
reorder.py 🔵読み上げ順の再構成(標準ライブラリのみ)
verify.py 🔵※要確認の判定と検算(標準ライブラリのみ)
render.py A / B / C の HTML 生成
fieldspec/ 書類ごとの項目宣言(新しい書類を足すときはここに1枚足す)
templates/ 画面
static/ CSS と JavaScript
samples/ 見本画像とその生成コード
tools/ 枠の位置を確かめる検証用スクリプト
書類を足すときに書き換えるのは fieldspec/ の JSON 1枚と、検算のルールだけです。読み上げ順の再構成は書類ごとに分岐させていません(分岐させると、その帳票専用の処理になってしまうため)。
モグテックラボ 守井 清吾(工学博士・情報工学)