batch-translate
複数の書籍をまとめて処理できます。分割ページ用の切り取り画像を生成し、全ページのOCR処理を実行して、文脈を考慮した翻訳を行います。書籍の処理、OCR、翻訳、またはバッチ処理が必要な場合に使用してください。
description の原文を見る
Batch process books through the complete pipeline - generate cropped images for split pages, OCR all pages, then translate with context. Use when asked to process, OCR, translate, or batch process one or more books.
SKILL.md 本文
書籍一括翻訳ワークフロー
書籍を完全なパイプラインで処理: 切り抜き → OCR → 翻訳
ロードマップ参照
翻訳優先度リストについては .claude/ROADMAP.md を参照してください。
優先度1 = 未翻訳 - 処理の最優先対象:
- Kircher百科事典(Oedipus、Musurgia、Ars Magna Lucis)
- Fludd: Utriusque Cosmi Historia
- Theatrum Chemicum、Musaeum Hermeticum
- Cardano: De Subtilitate
- Della Porta: Magia Naturalis
- Lomazzo、Poliziano、Landino
# ロードマップを優先度付きで取得
curl -s "https://sourcelibrary.org/api/books/roadmap" | jq '.books[] | select(.priority == 1) | {title, notes}'
ロードマップソース: src/app/api/books/roadmap/route.ts
概要
このワークフローは歴史的書籍スキャンの完全な処理パイプラインを扱います:
- 切り抜き画像の生成 - 2ページ見開きを分割して個別ページを抽出
- OCR - Gemini visionを使ってページ画像からテキストを抽出
- 翻訳 - OCR済みテキストを前ページコンテキスト付きで翻訳し連続性を確保
APIエンドポイント
| エンドポイント | 目的 |
|---|---|
GET /api/books | すべての書籍をリストアップ |
GET /api/books/BOOK_ID | すべてのページを含む書籍を取得 |
POST /api/jobs | 処理ジョブを作成 |
POST /api/jobs/JOB_ID/process | ジョブの次のチャンクを処理 |
POST /api/process/batch-ocr | 最大5ページを直接OCR |
POST /api/process/batch-translate | 最大10ページを直接翻訳 |
一括処理オプション
オプション1: Vercel Cron(大量処理に推奨)
2つのサーバーレス関数がOCR一括パイプライン全体を自動化します:
| エンドポイント | 目的 | スケジュール |
|---|---|---|
POST /api/cron/submit-ocr | OCRが必要なすべてのページの一括ジョブを作成 | 毎日午前0時 |
POST /api/cron/batch-processor | 結果をダウンロード、DBに保存 | 6時間ごと |
# 手動トリガー - 保留中のOCRをすべて送信
curl -X POST https://sourcelibrary.org/api/cron/submit-ocr
# 手動トリガー - 完了したバッチを処理
curl -X POST https://sourcelibrary.org/api/cron/batch-processor
タイムライン:
- T+0時間: バッチジョブを送信
- T+2-24時間: Gemini処理
- T+24時間: バッチプロセッサが結果を保存(6時間ごとに実行)
重要: 結果は48時間後に期限切れになります - batch-processorは最低でも48時間ごとに1回実行する必要があります。
詳細なドキュメントについては docs/BATCH-OCR-CRON-SETUP.md を参照してください。
オプション2: ジョブシステム(対象を絞った処理用)
すべての一括ジョブはGemini Batch APIを使用して50%のコスト削減を実現します。
| ジョブタイプ | API | モデル | コスト |
|---|---|---|---|
| 単一ページ | リアルタイム | gemini-3-flash-preview | 通常価格 |
| batch_ocr | Batch API | gemini-3-flash-preview | 50%割引 |
| batch_translate | Batch API | gemini-3-flash-preview | 50%割引 |
重要: OCRと翻訳のすべてのタスクに必ず gemini-3-flash-preview を使用してください。gemini-2.5-flash は使用しないでください。
詳細なドキュメントについては docs/BATCH-PROCESSING.md を参照してください。
バッチジョブの仕組み
- ジョブを作成 →
use_batch_api: trueが自動設定される /processを繰り返し呼び出し → 各呼び出しで20ページを準備- すべて準備完了 → Gemini Batch APIに送信
/processを再度呼び出し → 結果をポーリング(2-24時間で完了)- 完了時 → 結果を保存、ジョブ完了
OCR出力形式
OCRはセマンティックタグ付きMarkdown出力を使用します:
Markdownフォーマット
# ## ###は見出し(大きいテキスト = 大きい見出し)**太字**、*斜体*は強調->センター配置テキスト<-はセンター行(見出しには非対応)> ブロッククォートは引用/祈文用---は区切り線- テーブルは実際の表形式データのみ
メタデータタグ(読者には非表示)
| タグ | 目的 |
|---|---|
<lang>X</lang> | 検出言語 |
<page-num>N</page-num> | ページ/葉番号 |
<header>X</header> | ランニングヘッダー |
<sig>X</sig> | 印刷者の印(A2、B1など) |
<meta>X</meta> | 隠れたメタデータ |
<warning>X</warning> | 品質問題 |
<vocab>X</vocab> | インデックス用の重要用語 |
インライン注釈(読者に表示)
| タグ | 目的 |
|---|---|
<margin>X</margin> | 欄外注釈(段落の前) |
<gloss>X</gloss> | 行間注釈 |
<insert>X</insert> | 枠囲みテキスト、追加 |
<unclear>X</unclear> | 判読困難な箇所 |
<note>X</note> | 解釈的注釈 |
<term>X</term> | 技術用語 |
<image-desc>X</image-desc> | 図解の説明 |
重要なOCRルール
- 元のスペル、大文字小文字、句読点を保存
- ページ番号/ヘッダー/署名はメタデータタグのみ
- 端のテキストは無視(見開き内の対向ページから)
- 画像/図解は
<image-desc>で説明し、テーブルは使用しない <vocab>重要用語、名前、コンセプト</vocab>で終了
ステップ1: 書籍ステータスを分析
まず、書籍に必要な作業を確認します:
# 書籍を取得してページステータスを分析
curl -s "https://sourcelibrary.org/api/books/BOOK_ID" > /tmp/book.json
# ステータス別にページを数える(重要: 存在確認ではなく長さ > 0をチェック - 空文字列はtruthyです!)
jq '{
title: .title,
total_pages: (.pages | length),
split_pages: [.pages[] | select(.crop)] | length,
needs_crop: [.pages[] | select(.crop) | select(.cropped_photo | not)] | length,
has_ocr: [.pages[] | select((.ocr.data // "") | length > 0)] | length,
needs_ocr: [.pages[] | select((.ocr.data // "") | length == 0)] | length,
has_translation: [.pages[] | select((.translation.data // "") | length > 0)] | length,
needs_translation: [.pages[] | select((.ocr.data // "") | length > 0) | select((.translation.data // "") | length == 0)] | length
}' /tmp/book.json
不正なOCRを検出
切り抜き画像が生成される前にOCRされたページは不正なOCRを持っています(見開きの両ページを含みます)。これらを検出します:
# 切り抜きデータ + OCRがあるがOCR時に切り抜き写真がないページを検出
# これらのOCRテキストに「two-page」または「spread」を含むことが多い
jq '[.pages[] | select(.crop) | select(.ocr.data) |
select(.ocr.data | test("two-page|spread"; "i"))] | length' /tmp/book.json
ステップ2: 切り抜き画像を生成
2ページ見開きを分割する書籍の場合、個別ページ画像を生成します:
# 切り抜きが必要なページIDを取得
CROP_IDS=$(jq '[.pages[] | select(.crop) | select(.cropped_photo | not) | .id]' /tmp/book.json)
# 切り抜きジョブを作成
curl -s -X POST "https://sourcelibrary.org/api/jobs" \
-H "Content-Type: application/json" \
-d "{
\"type\": \"generate_cropped_images\",
\"book_id\": \"BOOK_ID\",
\"book_title\": \"BOOK_TITLE\",
\"page_ids\": $CROP_IDS
}"
ジョブを処理します:
# 処理をトリガー(1リクエストあたり40ページ、自動継続)
curl -s -X POST "https://sourcelibrary.org/api/jobs/JOB_ID/process"
ステップ3: ページをOCR
オプションA: ジョブシステムを使用(大量バッチ用)
# OCRが必要なページIDを取得(null単体ではなく空文字列をチェック)
OCR_IDS=$(jq '[.pages[] | select((.ocr.data // "") | length == 0) | .id]' /tmp/book.json)
# OCRジョブを作成
curl -s -X POST "https://sourcelibrary.org/api/jobs" \
-H "Content-Type: application/json" \
-d "{
\"type\": \"batch_ocr\",
\"book_id\": \"BOOK_ID\",
\"book_title\": \"BOOK_TITLE\",
\"model\": \"gemini-3-flash-preview\",
\"language\": \"Latin\",
\"page_ids\": $OCR_IDS
}"
オプションB: Batch APIを直接使用(小規模バッチまたは上書き用)
# OCRを上書き付きで実行(不正なOCRを修正)
curl -s -X POST "https://sourcelibrary.org/api/process/batch-ocr" \
-H "Content-Type: application/json" \
-d '{
"pages": [
{"pageId": "PAGE_ID_1", "imageUrl": "", "pageNumber": 0},
{"pageId": "PAGE_ID_2", "imageUrl": "", "pageNumber": 0}
],
"language": "Latin",
"model": "gemini-3-flash-preview",
"overwrite": true
}'
batch-ocr APIは自動的に利用可能な場合cropped_photoを使用します。
ステップ4: ページを翻訳
オプションA: ジョブシステムを使用
# 翻訳が必要なページIDを取得(OCRコンテンツがある必要があり、空文字列をチェック)
TRANS_IDS=$(jq '[.pages[] | select((.ocr.data // "") | length > 0) | select((.translation.data // "") | length == 0) | .id]' /tmp/book.json)
# 翻訳ジョブを作成
curl -s -X POST "https://sourcelibrary.org/api/jobs" \
-H "Content-Type: application/json" \
-d "{
\"type\": \"batch_translate\",
\"book_id\": \"BOOK_ID\",
\"book_title\": \"BOOK_TITLE\",
\"model\": \"gemini-3-flash-preview\",
\"language\": \"Latin\",
\"page_ids\": $TRANS_IDS
}"
オプションB: コンテキスト付きBatch APIを使用
より良い連続性を得るため、前ページコンテキスト付きで翻訳します:
# ページ番号でソートしたOCRテキスト付きページを取得(空文字列をチェック)
PAGES=$(jq '[.pages | sort_by(.page_number) | .[] |
select((.ocr.data // "") | length > 0) | select((.translation.data // "") | length == 0) |
{pageId: .id, ocrText: .ocr.data, pageNumber: .page_number}]' /tmp/book.json)
# コンテキスト付きで翻訳(5-10ページのバッチで処理)
curl -s -X POST "https://sourcelibrary.org/api/process/batch-translate" \
-H "Content-Type: application/json" \
-d "{
\"pages\": $BATCH,
\"model\": \"gemini-3-flash-preview\",
\"sourceLanguage\": \"Latin\",
\"targetLanguage\": \"English\",
\"previousContext\": \"PREVIOUS_PAGE_TRANSLATION_TEXT\"
}"
書籍全体処理スクリプト
書籍を完全なパイプラインで処理します:
#!/bin/bash
BOOK_ID="YOUR_BOOK_ID"
MODEL="gemini-3-flash-preview"
BASE_URL="https://sourcelibrary.org"
# 1. 書籍データを取得
echo "書籍を取得中..."
BOOK=$(curl -s "$BASE_URL/api/books/$BOOK_ID")
TITLE=$(echo "$BOOK" | jq -r '.title[0:40]')
echo "処理中: $TITLE"
# 2. 不足している切り抜きを生成
NEEDS_CROP=$(echo "$BOOK" | jq '[.pages[] | select(.crop) | select(.cropped_photo | not)] | length')
if [ "$NEEDS_CROP" != "0" ]; then
echo "$NEEDS_CROP個の切り抜き画像を生成中..."
CROP_IDS=$(echo "$BOOK" | jq '[.pages[] | select(.crop) | select(.cropped_photo | not) | .id]')
JOB=$(curl -s -X POST "$BASE_URL/api/jobs" -H "Content-Type: application/json" \
-d "{\"type\":\"generate_cropped_images\",\"book_id\":\"$BOOK_ID\",\"page_ids\":$CROP_IDS}")
JOB_ID=$(echo "$JOB" | jq -r '.job.id')
while true; do
RESULT=$(curl -s -X POST "$BASE_URL/api/jobs/$JOB_ID/process")
[ "$(echo "$RESULT" | jq -r '.done')" = "true" ] && break
sleep 2
done
echo "切り抜き完了!"
BOOK=$(curl -s "$BASE_URL/api/books/$BOOK_ID")
fi
# 3. OCR不足ページ(空文字列をチェック)
NEEDS_OCR=$(echo "$BOOK" | jq '[.pages[] | select((.ocr.data // "") | length == 0)] | length')
if [ "$NEEDS_OCR" != "0" ]; then
echo "$NEEDS_OCR ページをOCR中..."
OCR_IDS=$(echo "$BOOK" | jq '[.pages[] | select((.ocr.data // "") | length == 0) | .id]')
TOTAL=$(echo "$OCR_IDS" | jq 'length')
for ((i=0; i<TOTAL; i+=5)); do
BATCH=$(echo "$OCR_IDS" | jq ".[$i:$((i+5))] | [.[] | {pageId: ., imageUrl: \"\", pageNumber: 0}]")
curl -s -X POST "$BASE_URL/api/process/batch-ocr" -H "Content-Type: application/json" \
-d "{\"pages\":$BATCH,\"model\":\"$MODEL\"}" > /dev/null
echo -n "."
done
echo " OCR完了!"
BOOK=$(curl -s "$BASE_URL/api/books/$BOOK_ID")
fi
# 4. コンテキスト付きで翻訳(空文字列をチェック)
NEEDS_TRANS=$(echo "$BOOK" | jq '[.pages[] | select((.ocr.data // "") | length > 0) | select((.translation.data // "") | length == 0)] | length')
if [ "$NEEDS_TRANS" != "0" ]; then
echo "$NEEDS_TRANS ページを翻訳中..."
PAGES=$(echo "$BOOK" | jq '[.pages | sort_by(.page_number) | .[] |
select((.ocr.data // "") | length > 0) | select((.translation.data // "") | length == 0) |
{pageId: .id, ocrText: .ocr.data, pageNumber: .page_number}]')
TOTAL=$(echo "$PAGES" | jq 'length')
PREV_CONTEXT=""
for ((i=0; i<TOTAL; i+=5)); do
BATCH=$(echo "$PAGES" | jq ".[$i:$((i+5))]")
if [ -n "$PREV_CONTEXT" ]; then
RESP=$(curl -s -X POST "$BASE_URL/api/process/batch-translate" -H "Content-Type: application/json" \
-d "{\"pages\":$BATCH,\"model\":\"$MODEL\",\"previousContext\":$(echo "$PREV_CONTEXT" | jq -Rs .)}")
else
RESP=$(curl -s -X POST "$BASE_URL/api/process/batch-translate" -H "Content-Type: application/json" \
-d "{\"pages\":$BATCH,\"model\":\"$MODEL\"}")
fi
# コンテキスト用の最後の翻訳を取得
LAST_ID=$(echo "$BATCH" | jq -r '.[-1].pageId')
PREV_CONTEXT=$(echo "$RESP" | jq -r ".translations[\"$LAST_ID\"] // \"\"" | head -c 1500)
echo -n "."
done
echo " 翻訳完了!"
fi
echo "書籍処理完了!"
不正なOCRを修正
切り抜き画像が存在する前にOCRされたページはテキストが両ページを含んでいます。以下で修正します:
# 1. 最初に切り抜き画像を生成(上記ステップ2)
# 2. 不正なOCRのページを検出
BAD_OCR_IDS=$(jq '[.pages[] | select(.crop) | select(.ocr.data) |
select(.ocr.data | test("two-page|spread"; "i")) | .id]' /tmp/book.json)
# 3. 上書きで再OCR
TOTAL=$(echo "$BAD_OCR_IDS" | jq 'length')
for ((i=0; i<TOTAL; i+=5)); do
BATCH=$(echo "$BAD_OCR_IDS" | jq ".[$i:$((i+5))] | [.[] | {pageId: ., imageUrl: \"\", pageNumber: 0}]")
curl -s -X POST "https://sourcelibrary.org/api/process/batch-ocr" \
-H "Content-Type: application/json" \
-d "{\"pages\":$BATCH,\"model\":\"gemini-3-flash-preview\",\"overwrite\":true}"
done
すべての書籍を処理
最適化一括スクリプト(Tier 1)
適切なレート制限付きですべての書籍を処理するスクリプト:
#!/bin/bash
# Tier 1(300 RPM)に最適化 - 他のTierではSLEEP_TIMEを調整
BASE_URL="https://sourcelibrary.org"
# 重要: gemini-3-flash-previewを使用し、gemini-2.5-flashは使用しない
MODEL="gemini-3-flash-preview"
BATCH_SIZE=5
SLEEP_TIME=0.4 # Tier 1: 0.4秒、Tier 2: 0.12秒、Tier 3: 0.06秒
process_book() {
BOOK_ID="$1"
BOOK_DATA=$(curl -s "$BASE_URL/api/books/$BOOK_ID")
TITLE=$(echo "$BOOK_DATA" | jq -r '.title[0:30]')
# 必要な作業を確認(重要: 空文字列検出)
NEEDS_CROP=$(echo "$BOOK_DATA" | jq '[.pages[] | select(.crop) | select(.cropped_photo | not)] | length')
NEEDS_OCR=$(echo "$BOOK_DATA" | jq '[.pages[] | select((.ocr.data // "") | length == 0)] | length')
NEEDS_TRANSLATE=$(echo "$BOOK_DATA" | jq '[.pages[] | select((.ocr.data // "") | length > 0) | select((.translation.data // "") | length == 0)] | length')
if [ "$NEEDS_CROP" = "0" ] && [ "$NEEDS_OCR" = "0" ] && [ "$NEEDS_TRANSLATE" = "0" ]; then
echo "スキップ: $TITLE"
return
fi
echo "開始: $TITLE [切り抜き:$NEEDS_CROP OCR:$NEEDS_OCR 翻訳:$NEEDS_TRANSLATE]"
# ステップ1: 切り抜き
if [ "$NEEDS_CROP" != "0" ]; then
CROP_IDS=$(echo "$BOOK_DATA" | jq '[.pages[] | select(.crop) | select(.cropped_photo | not) | .id]')
JOB_RESP=$(curl -s -X POST "$BASE_URL/api/jobs" \
-H 'Content-Type: application/json' \
-d "{\"type\": \"generate_cropped_images\", \"book_id\": \"$BOOK_ID\", \"page_ids\": $CROP_IDS}")
JOB_ID=$(echo "$JOB_RESP" | jq -r '.job.id')
if [ "$JOB_ID" != "null" ]; then
while true; do
RESULT=$(curl -s -X POST "$BASE_URL/api/jobs/$JOB_ID/process")
[ "$(echo "$RESULT" | jq -r '.done')" = "true" ] && break
sleep 1
done
fi
BOOK_DATA=$(curl -s "$BASE_URL/api/books/$BOOK_ID")
fi
# ステップ2: OCR
NEEDS_OCR=$(echo "$BOOK_DATA" | jq '[.pages[] | select((.ocr.data // "") | length == 0)] | length')
if [ "$NEEDS_OCR" != "0" ]; then
OCR_IDS=$(echo "$BOOK_DATA" | jq '[.pages[] | select((.ocr.data // "") | length == 0) | .id]')
TOTAL_OCR=$(echo "$OCR_IDS" | jq 'length')
for ((i=0; i<TOTAL_OCR; i+=BATCH_SIZE)); do
BATCH=$(echo "$OCR_IDS" | jq ".[$i:$((i+BATCH_SIZE))]")
PAGES=$(echo "$BATCH" | jq '[.[] | {pageId: ., imageUrl: "", pageNumber: 0}]')
RESP=$(curl -s -X POST "$BASE_URL/api/process/batch-ocr" \
-H 'Content-Type: application/json' \
-d "{\"pages\": $PAGES, \"model\": \"$MODEL\"}")
if echo "$RESP" | grep -q "429\|rate"; then
echo "レート制限: $TITLE - 10秒待機"
sleep 10
i=$((i-BATCH_SIZE)) # このバッチを再試行
fi
sleep $SLEEP_TIME
done
echo "OCR完了: $TITLE"
BOOK_DATA=$(curl -s "$BASE_URL/api/books/$BOOK_ID")
fi
# ステップ3: コンテキスト付きで翻訳
NEEDS_TRANSLATE=$(echo "$BOOK_DATA" | jq '[.pages[] | select((.ocr.data // "") | length > 0) | select((.translation.data // "") | length == 0)] | length')
if [ "$NEEDS_TRANSLATE" != "0" ]; then
TRANSLATE_PAGES=$(echo "$BOOK_DATA" | jq '[.pages | sort_by(.page_number) | .[] | select((.ocr.data // "") | length > 0) | select((.translation.data // "") | length == 0) | {pageId: .id, ocrText: .ocr.data, pageNumber: .page_number}]')
TOTAL_TRANS=$(echo "$TRANSLATE_PAGES" | jq 'length')
PREV_CONTEXT=""
for ((i=0; i<TOTAL_TRANS; i+=BATCH_SIZE)); do
BATCH=$(echo "$TRANSLATE_PAGES" | jq ".[$i:$((i+BATCH_SIZE))]")
if [ -n "$PREV_CONTEXT" ]; then
RESP=$(curl -s -X POST "$BASE_URL/api/process/batch-translate" \
-H 'Content-Type: application/json' \
-d "{\"pages\": $BATCH, \"model\": \"$MODEL\", \"previousContext\": \"$PREV_CONTEXT\"}")
else
RESP=$(curl -s -X POST "$BASE_URL/api/process/batch-translate" \
-H 'Content-Type: application/json' \
-d "{\"pages\": $BATCH, \"model\": \"$MODEL\"}")
fi
if echo "$RESP" | grep -q "429\|rate"; then
echo "レート制限: $TITLE - 10秒待機"
sleep 10
i=$((i-BATCH_SIZE)) # このバッチを再試行
else
LAST_ID=$(echo "$BATCH" | jq -r '.[-1].pageId')
PREV_CONTEXT=$(echo "$RESP" | jq -r ".translations[\"$LAST_ID\"] // \"\"" | head -c 1500)
fi
sleep $SLEEP_TIME
done
echo "翻訳完了: $TITLE"
fi
echo "完了: $TITLE"
}
export -f process_book
export BASE_URL MODEL BATCH_SIZE SLEEP_TIME
echo "=== 一括処理 ==="
echo "バッチ: $BATCH_SIZE | 待機: ${SLEEP_TIME}秒"
curl -s "$BASE_URL/api/books" | jq -r '.[] | .id' > /tmp/book_ids.txt
TOTAL=$(wc -l < /tmp/book_ids.txt | tr -d ' ')
echo "$TOTAL冊の書籍を処理中..."
cat /tmp/book_ids.txt | xargs -P 1 -I {} bash -c 'process_book "$@"' _ {}
echo "
ライセンス: MIT(寛容ライセンスのため全文を引用しています) · 原本リポジトリ
詳細情報
- 作者
- majiayu000
- ライセンス
- MIT
- 最終更新
- 2026/5/4
Source: https://github.com/majiayu000/claude-skill-registry / ライセンス: MIT