はじめに
紙や画像で回収した手書きアンケートの集計作業。
手書き文字を1行ずつ目視でExcelに入力し、設問ごとに集計してグラフ化・レポート化する作業は時間もかかり、手入力による転記ミスのリスクもあります。
そこで今回は、スキャンしたアンケート(画像や多ページPDF)をアップロードするだけで、AIが手書き文字を自動抽出し、画面上で手修正(Human-in-the-Loop)を行った上で、分析レポートまで自動生成してくれるWebアプリを試作しました。
Webアプリの画面はPythonの Streamlit で作成し、文字認識とデータ分析には Google の Gemini API (gemini-3.8-flash) を利用しています。
実際に30ページ規模のPDFアンケートのサンプルを読み込ませ、手書き回答の修正からGoogleアナリティクス(GA)風の分析サマリー出力まで問題なく動作することを確認できました。
開発環境
| 項目 | 環境 |
|---|---|
| OS | macOS Sequoia |
| 言語 | Python 3.11 |
| Webフレームワーク | Streamlit |
| 主なライブラリ | Pandas, Pillow (PIL), google-genai
|
| AIモデル | Google Gemini API (gemini-3.8-flash) |
| 開発環境 | Virtual Environment (venv) |
今回は個人のMacローカル環境で動作するプロトタイプとして開発を行いました。
1. アプリの基本構成
処理の基本フローは以下の通りです。
アンケートスキャンデータ(PNG / JPG / PDF)
↓
Streamlit画面からアップロード
↓
Gemini API (gemini-3.8-flash) でマルチモーダル解析
↓
JSON形式で設問・回答・AI確信度を出力
↓
Streamlit (st.data_editor) で画面上にテーブル表示
↓
人間による手書き誤読のチェック・直接修正(Human-in-the-Loop)
↓
確定データからKPI・GA風分析レポートを自動生成
ポイントは、AIの抽出結果をそのままデータベース化するのではなく、人間が画面上で簡単に誤読をダブルクリック修正できる「Human-in-the-Loop」のインターフェースを挟んでいる点です。
2. スキャンデータ(画像・PDF)からJSON抽出を行う
最新SDKの google-genai を使用し、アップロードされた画像やPDFを直接Geminiに入力して、構造化データ(JSON)として抽出します。
PDFファイルの場合は types.Part.from_bytes を使うことで、事前に画像化するような前処理を行わずに、直接PDFバイナリデータを送信できます。
import json
from google import genai
from google.genai import types
client = genai.Client(api_key="YOUR_API_KEY")
# PDFファイルデータの準備
file_bytes = uploaded_file.read()
contents_data = types.Part.from_bytes(data=file_bytes, mime_type="application/pdf")
prompt = """
提供されたアンケートデータから、以下の項目を抽出して必ずJSON形式のみで出力してください。
余計な解説文やバッククォート(```json など)は含めないでください。
出力フォーマット:
[
{
"設問ID": "Q1",
"質問項目": "設問の文章",
"回答状況": "抽出した回答内容(手書き文字含む)",
"AI確信度": "高 または 要確認(低)"
}
]
手書き文字で読み取りに自信がない箇所は、AI確信度を「要確認(低)」にしてください。
"""
response = client.models.generate_content(
model="gemini-3.8-flash",
contents=[contents_data, prompt],
config=types.GenerateContentConfig(
response_mime_type="application/json"
)
)
extracted_data = json.loads(response.text)
response_mime_type="application/json" を設定することで、AI側の出力をマークダウンなどの余計な装飾のない純粋なJSON文字列に限定できます。
3. Human-in-the-Loop(人間の手修正)画面の実装
抽出したデータは Streamlit の st.data_editor を使って対話的なテーブルとして出力します。
AI確信度が「要確認(低)」になっているセルや、誤読のあるテキストをユーザーが直接ダブルクリックして修正できるようにしています。
import pandas as pd
import streamlit as st
df = pd.DataFrame(st.session_state["extracted_data"])
# インタラクティブな編集可能テーブル
edited_df = st.data_editor(
df,
num_rows="dynamic",
use_container_width=True,
key="survey_editor"
)
この編集結果(edited_df)が確定データとして次の分析フェーズへ引き継がれます。
4. 分析レポートの自動生成と503混雑エラーへの対策
確定したデータフレームをJSON化し、再び Gemini API に渡して分析結果(主要傾向・課題点・推奨アクション)をテキストで出力します。
無料アカウントでは、開発の過程で、Googleサーバー側のアクセス集中による 503 UNAVAILABLE(一時的なサービス混雑エラー) が発生しやすいです。自動リトライおよび代替モデルへのフォールバック処理を実装してカバーすることもできますが、今回は何回も実行ボタンを押してレポートを出してもらいました。
import time
PRIMARY_MODEL = "gemini-3.8-flash"
FALLBACK_MODELS = ["gemini-3.5-flash", "gemini-2.5-flash"]
def generate_content_with_retry(client, contents, config=None):
"""503エラー発生時に自動で再試行・モデル切り替えを行う堅牢化関数"""
models_to_try = [PRIMARY_MODEL] + FALLBACK_MODELS
for model_name in models_to_try:
for attempt in range(3): # 各モデル最大3回リトライ
try:
if config:
return client.models.generate_content(model=model_name, contents=contents, config=config)
else:
return client.models.generate_content(model=model_name, contents=contents)
except Exception as e:
if "503" in str(e) or "UNAVAILABLE" in str(e):
time.sleep(2) # 503混雑時は2秒待機して再試行
continue
else:
raise e
raise Exception("全モデルが一時的に混雑しています。数十秒おいて再度お試しください。")
5. 開発時の技術的課題と対処のプロセス
今回の開発で特に直面した技術的な課題と、その切り分け・解決手順について記録しておきます。
課題1: API呼び出し時の 404 NOT_FOUND エラー多発
開発初期、ドキュメントや従来コードで一般的だった gemini-1.5-flash や gemini-2.5-flash などのモデル識別子を指定した際、404 NOT_FOUND や「This model is no longer available to new users」といった非推奨エラーが発生し続けました。
切り分けと解決手順:
Google AI Studio側の仕様変更およびSDK(google-genai)での識別子の扱いを確かめるため、使用したAPIキーで現時点で実際に呼び出し可能なモデル一覧を出力する診断スクリプトを作成して実行しました。
import google.generativeai as genai
api_key = input("Gemini API Key を入力してください: ")
genai.configure(api_key=api_key)
print("\n--- 利用可能なモデル一覧 ---")
for m in genai.list_models():
if 'generateContent' in m.supported_generation_methods:
print(m.name)
このスクリプトを実行した結果、ターミナル出力から自身の環境・APIキーにおいて現在サポートされている最新の有効モデル識別子が models/gemini-3.8-flash であることが分かり、アプリ側の MODEL_ID を変更したことで正常通信が確立しました。
課題2: 分析レポート生成時の 503 UNAVAILABLE エラー
データ抽出後の「レポート作成」ボタンを押した際、google.genai.errors.ServerError: 503 UNAVAILABLE が発生し、処理が中断する現象が起きました。
原因と対策:
コードの文法エラーではなく、Gemini側のサーバー負荷が高まった際に出る一時的なリクエスト制限が原因でした。手動で数回再実行することで動作しました。前述の generate_content_with_retry(自動で2秒待機して再試行・代替モデルへフォールバックするロジック)をプログラム内に組み込むことでも、画面がエラー停止しないよう対策できると思います。
6. 無料枠と有料枠の挙動差、および会社導入時の「投資」の考え方
開発テスト段階では Google AI Studio で発行した 無料枠(Free Tier) のAPIキーを使用しました。しかし、実運用や会社組織としての導入を見据える場合は、アカウントの仕様差と費用対効果(ROI)を認識しておく必要があります。
無料枠(Free Tier)と従量課金枠(Pay-as-you-go)の違い
- リクエスト数制限(RPM / TPM)とレスポンス優先度
- 無料枠は1分あたりの呼び出し回数(RPM)やトークン数(TPM)に上限があり、今回遭遇した
503 UNAVAILABLE(混雑エラー)の影響を直接受けやすくなります。 - 有料の従量課金枠(Pay-as-you-go)や Google Cloud (Vertex AI) 経由に切り替えることで、優先的なコンピュートリソースが割り振られ、応答速度の安定化とエラー発生率の低下が期待できます。
- データの保護ポリシー
- 無料枠では入力データがモデル改善のために利用される可能性がある設定がデフォルト適用されるケースがあります。企業で個人情報や顧客アンケートを扱う場合は、データが学習に使われない有料アカウント(または事業用権限)での契約が必須となります。この辺りは、最近のサイバー攻撃による個人情報流出問題もあるので、そもそも個人情報の入るアンケート内容は読み込ませない方がリスク回避としては良いと思います。
業務効率化への投資としてのコスト感
手作業で手書きアンケートを打ち込む場合、1枚あたり数分〜十数分の人件費(人・時間コスト)が発生します。一方、Gemini API(Flashモデル)の従量課金コストは1リクエスト(数十ページ)あたり数円〜数十円程度です。
「手入力の作業時間を9割削減する」「誤入力のチェック時間を最小化する」という業務改善効果を鑑みれば、API課金費用は非常に費用対効果の高い業務投資枠として捉えることができます。
7. 社内・他メンバーへ展開する場合のアイデア
現在は個人の開発環境(ターミナル操作+APIキー手動入力)で動作させていますが、社内の他のスタッフにも気軽に使ってもらうアプリにしていく場合、以下の改善策が考えられます。
① APIキー入力の自動化
サイドバーでスタッフにAPIキーを入力させるのではなく、サーバー側で環境変数(ENV)として保持することで、キー入力の手間を省きます。
② イントラネット(社内LAN)内でのWeb化
クラウドに公開するとセキュリティリスクが発生するため、社内サーバーや共通PC上にアプリを配置し、社内ネットワーク内からブラウザ経由(http://192.168.x.x:8501 等)でアクセスできる環境を構築することが適しています。
③ ワンクリック起動ショートカットの作成
サーバー構築を行わない場合は、スタッフのMacデスクトップ上に起動スクリプト(.command)を配置し、ダブルクリックだけでバックグラウンド処理とブラウザ起動が自動で行われる仕組みを用意することも有効です。
おわりに
今回の試作を通じて、Streamlit と Gemini API を組み合わせることで、画像/PDFのOCR抽出から手動データ修正、そしてAIによるサマリー分析までを Python コードで一気通貫に構築できることが確認できました。
手書きアンケート処理のように「完全にAI任せにはできないが、手入力は面倒」という業務において、人間とAIが協調する「Human-in-the-Loop」型のWebツールは非常に有効だと感じます。