#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
convert_ocr.py
OCR専用の独立変換ツール。
- 出力は常に Markdown (.md)
- OCRエンジンは tesseract / genai
- 入力は画像, PDF, および (LibreOffice/soffice があれば) Office 文書
- --list で利用可能な形式と依存関係を表示
主な使い方:
python convert_ocr.py input.png
python convert_ocr.py input.pdf --engine tesseract --lang jpn+eng
python convert_ocr.py input.png --engine genai --ini ai_ocr2md.ini
python convert_ocr.py slides.pptx --engine tesseract --keep-images
python convert_ocr.py --list
:doc:`convert_ocr_usage`
"""
from __future__ import annotations
import argparse
import os
import re
import shutil
import subprocess
import sys
import tempfile
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, List, Optional, Sequence, Tuple
# ---------- optional imports ----------
try:
from PIL import Image, ImageGrab
except Exception: # pragma: no cover
Image = None
ImageGrab = None
try:
import fitz # PyMuPDF
except Exception: # pragma: no cover
fitz = None
try:
import pytesseract
except Exception: # pragma: no cover
pytesseract = None
# ---------- constants ----------
IMAGE_EXTS = {".png", ".jpg", ".jpeg", ".bmp", ".tif", ".tiff", ".webp"}
PDF_EXTS = {".pdf"}
OFFICE_EXTS = {
".pptx", ".ppt", ".docx", ".doc", ".xlsx", ".xls", ".odp", ".odt", ".ods"
}
DEFAULT_GENAI_PROMPT = (
"画像内のテキストを正確に抽出し、Markdown形式で出力してください。"
" 見出し・箇条書き・表をできるだけ保ち、数式は LaTeX で表現してください。"
)
# ---------- data structures ----------
[ドキュメント]
@dataclass
class ImageItem:
"""
OCR処理対象の画像情報を保持するデータクラスです。
:param path: 画像ファイルへのパス。
:type path: Path
:param label: 画像を識別するためのラベル (例: ファイル名、ページ番号)。
:type label: str
"""
path: Path
label: str
# ---------- utility ----------
[ドキュメント]
def eprint(*args: object, **kwargs: object) -> None:
"""
標準エラー出力にメッセージを出力します。
`print` 関数と同じ引数を受け取りますが、出力先は `sys.stderr` です。
デバッグ情報やエラーメッセージの表示に便利です。
:param args: `print` 関数に渡される位置引数。
:type args: object
:param kwargs: `print` 関数に渡されるキーワード引数。特に `file=sys.stderr` が設定されます。
:type kwargs: object
:returns: なし
:rtype: None
"""
print(*args, file=sys.stderr, **kwargs)
[ドキュメント]
def which(cmd: str) -> Optional[str]:
"""
指定されたコマンドがシステムPATH上に存在するかを確認し、そのパスを返します。
`shutil.which` のラッパー関数です。
:param cmd: 検索するコマンド名。
:type cmd: str
:returns: コマンドの絶対パス、または見つからなかった場合は `None`。
:rtype: Optional[str]
"""
return shutil.which(cmd)
[ドキュメント]
def command_exists_any(names: Sequence[str]) -> Optional[str]:
"""
指定されたコマンド名のいずれかがシステムPATH上に存在するかを確認します。
与えられたシーケンス内の各コマンド名を順番に検索し、
最初に見つかったコマンドの絶対パスを返します。
:param names: 検索するコマンド名のシーケンス。
:type names: Sequence[str]
:returns: 最初に見つかったコマンドの絶対パス、またはすべて見つからなかった場合は `None`。
:rtype: Optional[str]
"""
for name in names:
found = which(name)
if found:
return found
return None
[ドキュメント]
def slugify_filename(name: str) -> str:
"""
ファイル名をURLフレンドリーな形式(スラッグ)に変換します。
ファイル名として適さない文字(英数字、ピリオド、アンダースコア、ハイフン以外の文字)を
アンダースコアに置換します。
:param name: 変換するファイル名または文字列。
:type name: str
:returns: 変換されたスラッグ形式のファイル名。
:rtype: str
"""
return re.sub(r"[^A-Za-z0-9._-]+", "_", name)
[ドキュメント]
def default_output_path(input_value: str, output: Optional[str]) -> Path:
"""
出力ファイルパスが指定されていない場合に、既定の出力パスを生成します。
`output` 引数が指定されている場合はそれを優先します。
`input_value` が "clip" の場合、既定で `clipboard_ocr.md` を返します。
それ以外の場合、`input_value` の拡張子を `.md` に変更したパスを返します。
:param input_value: 入力ファイルのパス、または "clip"。
:type input_value: str
:param output: ユーザーが指定した出力パス。
:type output: Optional[str]
:returns: 生成された出力ファイルのパス。
:rtype: Path
"""
if output:
return Path(output)
if input_value.lower() == "clip":
return Path("clipboard_ocr.md")
return Path(input_value).with_suffix(".md")
# ---------- ini reader ----------
[ドキュメント]
def read_simple_ini(path: Path) -> Dict[str, str]:
"""
key=value 形式の簡易 INI ファイルを読み込み、辞書として返します。
# または ; で始まるコメント行、および空行をスキップします。
三連引用符(\'\'\' または \"\"\")で囲まれた複数行の値に対応しています。
値に含まれる `$VAR` 形式の環境変数参照は、その環境変数の値に展開されます。
:param path: 読み込むINIファイルへのパス。
:type path: Path
:returns: INIファイルから読み込んだキーと値の辞書。
:rtype: Dict[str, str]
"""
result: Dict[str, str] = {}
text = path.read_text(encoding="utf-8")
lines = text.splitlines()
i = 0
while i < len(lines):
line = lines[i].strip()
i += 1
if not line or line.startswith("#") or line.startswith(";"):
continue
if "=" not in line:
continue
key, value = line.split("=", 1)
key = key.strip()
value = value.strip()
if value.startswith("'''") or value.startswith('"""'):
quote = value[:3]
if value.endswith(quote) and len(value) >= 6:
result[key] = value[3:-3]
continue
collected = [value[3:]]
while i < len(lines):
cur = lines[i]
i += 1
if cur.endswith(quote):
collected.append(cur[:-3])
break
collected.append(cur)
result[key] = "\n".join(collected)
else:
result[key] = value
# simple environment expansion: $VAR
env_pattern = re.compile(r"\$([A-Za-z_][A-Za-z0-9_]*)")
for key, value in list(result.items()):
result[key] = env_pattern.sub(lambda m: os.getenv(m.group(1), m.group(0)), value)
return result
# ---------- availability / list ----------
[ドキュメント]
def availability_report() -> str:
"""
本ツールのOCRエンジン、レンダラー、対応入力タイプなどの利用可能性レポートを生成します。
各OCRエンジン (Tesseract, GenAI) およびレンダリングに必要なライブラリ
(Pillow, PyMuPDF) や外部コマンド (soffice) の利用可能性をチェックし、
その結果と対応する入力タイプ、簡単な使用例を含むレポートテキストを返します。
:returns: 利用可能性レポートの文字列。
:rtype: str
"""
soffice = command_exists_any(["soffice", "libreoffice"])
lines = []
lines.append("convert_ocr.py : 利用可能な入力 / エンジン / 依存関係")
lines.append("")
lines.append("[OCR engine]")
lines.append(f"- tesseract : {'OK' if pytesseract is not None else 'NG'}"
f"{' (pytesseract import可)' if pytesseract is not None else ' (pytesseract が必要)'}")
lines.append(f"- genai : {'OK' if probe_genai_backend()[0] else 'NG'}"
f" ({probe_genai_backend()[1]})")
lines.append("")
lines.append("[renderer]")
lines.append(f"- Pillow : {'OK' if Image is not None else 'NG'}")
lines.append(f"- PyMuPDF : {'OK' if fitz is not None else 'NG'}")
lines.append(f"- soffice : {'OK' if soffice else 'NG'}"
f"{' (' + soffice + ')' if soffice else ' (LibreOffice があれば Office 文書入力可)'}")
lines.append("")
lines.append("[input type]")
lines.append(f"- image : {', '.join(sorted(IMAGE_EXTS))}")
lines.append(f"- pdf : {', '.join(sorted(PDF_EXTS))}")
lines.append(f"- office : {', '.join(sorted(OFFICE_EXTS))} ※ soffice が必要")
lines.append("- clipboard : clip ※ Pillow.ImageGrab が使える環境のみ")
lines.append("")
lines.append("[output]")
lines.append("- 常に .md")
lines.append("")
lines.append("[examples]")
lines.append("- python convert_ocr.py input.png")
lines.append("- python convert_ocr.py input.pdf --engine tesseract --lang jpn+eng")
lines.append("- python convert_ocr.py input.png --engine genai --ini ai_ocr2md.ini")
lines.append("- python convert_ocr.py slides.pptx --engine tesseract --keep-images")
return "\n".join(lines)
# ---------- genai backend ----------
[ドキュメント]
def probe_genai_backend() -> Tuple[bool, str]:
"""
利用可能なGenAIバックエンドをプローブ(調査)します。
`tkai_lib` または `google.generativeai` のいずれかが現在の環境で
インポート可能かを確認します。
:returns: 利用可能かどうかの真偽値と、利用可能なバックエンドを示す文字列のタプル。
いずれも利用できない場合は `(False, "tkai_lib または google-generativeai が必要")` を返します。
:rtype: Tuple[bool, str]
"""
try:
import tkai_lib # type: ignore
return True, "tkai_lib"
except Exception:
pass
try:
import google.generativeai # type: ignore
return True, "google.generativeai"
except Exception:
pass
return False, "tkai_lib または google-generativeai が必要"
[ドキュメント]
def load_genai_prompt(ini_path: Optional[str]) -> str:
"""
指定されたINIファイルからGenAIプロンプトを読み込みます。
INIファイルが見つからない、またはプロンプトキースが存在しない場合、
既定のプロンプト (`DEFAULT_GENAI_PROMPT`) を使用します。
プロンプトは `PROMPT`, `PROMPT_TEMPLATE_JA`, `PROMPT_TEMPLATE_EN` の順で検索されます。
:param ini_path: プロンプトが定義されているINIファイルへのパス。`None` の場合、既定のプロンプトを使用。
:type ini_path: Optional[str]
:returns: 読み込まれた、または既定のプロンプト文字列。
:rtype: str
"""
if not ini_path:
return DEFAULT_GENAI_PROMPT
path = Path(ini_path)
if not path.exists():
eprint(f"[warn] ini file not found: {path}. 既定プロンプトを使用します。")
return DEFAULT_GENAI_PROMPT
data = read_simple_ini(path)
return (
data.get("PROMPT")
or data.get("PROMPT_TEMPLATE_JA")
or data.get("PROMPT_TEMPLATE_EN")
or DEFAULT_GENAI_PROMPT
)
[ドキュメント]
def init_genai_environment(ai_env: Optional[str]) -> None:
"""
指定された環境変数ファイルからGenAI関連の環境変数を読み込み、設定します。
ファイルが存在する場合、その中の `key=value` 形式の各行を環境変数として設定します。
ただし、すでに同じキーの環境変数が存在する場合は上書きしません。
:param ai_env: 環境変数が定義されているファイルへのパス。`None` の場合、何もしません。
:type ai_env: Optional[str]
:returns: なし
:rtype: None
"""
if not ai_env:
return
env_path = Path(ai_env)
if not env_path.exists():
return
data = read_simple_ini(env_path)
for key, value in data.items():
if key and value and key not in os.environ:
os.environ[key] = value
[ドキュメント]
def call_genai_ocr(prompt: str, image_path: Path, api: str = "gemini", model: Optional[str] = None,
ai_env: Optional[str] = None) -> str:
"""
GenAIサービスを呼び出して画像OCRを実行します。
`tkai_lib` または `google.generativeai` のいずれかのバックエンドを使用して、
指定されたプロンプトと画像でOCRを実行します。
`ai_env` が指定されている場合、そこから環境変数をロードします。
現在、GenAI APIは `gemini` のみサポートしています。
:param prompt: GenAIに渡すプロンプト文字列。
:type prompt: str
:param image_path: OCR対象の画像ファイルへのパス。
:type image_path: Path
:param api: 使用するGenAI APIの名前 (既定: "gemini")。
:type api: str
:param model: 使用するGenAIモデルの名前。`None` の場合、環境変数 `gemini_model` または "gemini-1.5-pro" を使用。
:type model: Optional[str]
:param ai_env: AI用環境変数が定義されているINIファイルへのパス。
:type ai_env: Optional[str]
:returns: GenAIによって抽出されたテキスト。
:rtype: str
:raises RuntimeError: Pillowが利用できない、APIキーが設定されていない、GenAIバックエンドが利用できない、
またはGenAIからの応答が空の場合。
"""
if Image is None:
raise RuntimeError("Pillow is required for genai OCR.")
init_genai_environment(ai_env)
img = Image.open(image_path)
# Backend 1: user's existing wrapper
try:
import tkai_lib # type: ignore
if hasattr(tkai_lib, "read_ai_config") and ai_env:
tkai_lib.read_ai_config(ai_env)
if api != "gemini":
raise RuntimeError("この実装の genai は現在 gemini を想定しています。")
model_name = model or os.getenv("gemini_model", "gemini-1.5-pro")
api_key = os.getenv("GOOGLE_API_KEY")
if not api_key:
raise RuntimeError("GOOGLE_API_KEY is not set.")
tkai_lib.genai.configure(api_key=api_key)
m = tkai_lib.genai.GenerativeModel(model_name)
response = m.generate_content([prompt, img])
text = getattr(response, "text", None)
if not text:
raise RuntimeError("genai response.text is empty.")
return text.strip()
except ImportError:
pass
# Backend 2: direct google.generativeai
try:
import google.generativeai as genai # type: ignore
if api != "gemini":
raise RuntimeError("この実装の genai は現在 gemini を想定しています。")
model_name = model or os.getenv("gemini_model", "gemini-1.5-pro")
api_key = os.getenv("GOOGLE_API_KEY")
if not api_key:
raise RuntimeError("GOOGLE_API_KEY is not set.")
genai.configure(api_key=api_key)
m = genai.GenerativeModel(model_name)
response = m.generate_content([prompt, img])
text = getattr(response, "text", None)
if not text:
raise RuntimeError("genai response.text is empty.")
return text.strip()
except ImportError as exc:
raise RuntimeError("genai backend not available. Install tkai_lib or google-generativeai.") from exc
# ---------- input normalization ----------
[ドキュメント]
def ensure_clipboard_image(work_dir: Path) -> Path:
"""
クリップボードに画像が存在する場合、それを一時ファイルとして保存します。
Pillowの `ImageGrab` を使用してクリップボードから画像データを取得し、
指定された作業ディレクトリ内に `clipboard_input.png` として保存します。
:param work_dir: 画像を保存する作業ディレクトリへのパス。
:type work_dir: Path
:returns: 保存された画像ファイルへのパス。
:rtype: Path
:raises RuntimeError: Pillow.ImageGrabが利用できない場合、またはクリップボードに画像が含まれていない場合。
"""
if ImageGrab is None:
raise RuntimeError("Pillow.ImageGrab is not available in this environment.")
img = ImageGrab.grabclipboard()
if not isinstance(img, Image.Image):
raise RuntimeError("Clipboard does not contain an image.")
out = work_dir / "clipboard_input.png"
img.save(out, "PNG")
return out
[ドキュメント]
def render_pdf_to_images(pdf_path: Path, out_dir: Path, dpi: int) -> List[ImageItem]:
"""
PDFファイル内の各ページを画像としてレンダリングします。
PyMuPDF (fitz) を使用してPDFの各ページを指定されたDPIでPNG画像に変換し、
指定された出力ディレクトリに保存します。
:param pdf_path: レンダリングするPDFファイルへのパス。
:type pdf_path: Path
:param out_dir: レンダリングされた画像を保存するディレクトリへのパス。
:type out_dir: Path
:param dpi: 画像の解像度 (dots per inch)。
:type dpi: int
:returns: レンダリングされた各画像の `ImageItem` オブジェクトのリスト。
:rtype: List[ImageItem]
:raises RuntimeError: PyMuPDF (fitz) が利用できない場合。
"""
if fitz is None:
raise RuntimeError("PyMuPDF (fitz) is required for PDF input.")
out_dir.mkdir(parents=True, exist_ok=True)
items: List[ImageItem] = []
doc = fitz.open(pdf_path)
try:
matrix = fitz.Matrix(dpi / 72.0, dpi / 72.0)
for page_idx in range(len(doc)):
page = doc.load_page(page_idx)
pix = page.get_pixmap(matrix=matrix, alpha=False)
out_path = out_dir / f"page_{page_idx + 1:04d}.png"
pix.save(str(out_path))
items.append(ImageItem(path=out_path, label=f"Page {page_idx + 1}"))
finally:
doc.close()
return items
[ドキュメント]
def convert_office_to_pdf(input_path: Path, out_dir: Path) -> Path:
"""
Office文書をPDF形式に変換します。
LibreOfficeの `soffice` コマンドラインツールを使用して、
指定されたOffice文書 (PPTX, DOCX, XLSX など) をPDFに変換します。
変換されたPDFファイルは `out_dir` に保存されます。
:param input_path: 変換するOffice文書へのパス。
:type input_path: Path
:param out_dir: 変換されたPDFを保存するディレクトリへのパス。
:type out_dir: Path
:returns: 変換されたPDFファイルへのパス。
:rtype: Path
:raises RuntimeError: LibreOffice/sofficeが見つからない場合、またはsofficeでの変換に失敗した場合。
"""
soffice = command_exists_any(["soffice", "libreoffice"])
if not soffice:
raise RuntimeError(
"Office文書入力には LibreOffice/soffice が必要です。"
" 先に PDF に変換するか、LibreOffice をインストールしてください。"
)
out_dir.mkdir(parents=True, exist_ok=True)
cmd = [
soffice,
"--headless",
"--convert-to", "pdf",
"--outdir", str(out_dir),
str(input_path),
]
proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
if proc.returncode != 0:
raise RuntimeError(
"soffice conversion failed.\n"
f"stdout:\n{proc.stdout}\n"
f"stderr:\n{proc.stderr}"
)
pdf_path = out_dir / (input_path.stem + ".pdf")
if not pdf_path.exists():
candidates = sorted(out_dir.glob("*.pdf"))
if not candidates:
raise RuntimeError("soffice did not produce a PDF file.")
pdf_path = candidates[0]
return pdf_path
[ドキュメント]
def collect_images(input_value: str, work_dir: Path, dpi: int) -> List[ImageItem]:
"""
指定された入力(画像、PDF、Office文書、クリップボード)からOCR処理用の画像を収集します。
入力タイプに応じて以下の処理を行います:
- "clip": クリップボードから画像を直接取得。
- 画像ファイル: そのまま `ImageItem` としてリストに追加。
- PDFファイル: 各ページを画像にレンダリング。
- Office文書: LibreOffice (soffice) を使用してPDFに変換後、各ページを画像にレンダリング。
すべての中間ファイルは `work_dir` に作成されます。
:param input_value: 入力源。画像/PDF/Office文書のパス、または "clip"。
:type input_value: str
:param work_dir: 中間ファイルを保存する作業ディレクトリへのパス。
:type work_dir: Path
:param dpi: PDF/Office文書を画像化する際のDPI。
:type dpi: int
:returns: 収集された各画像の `ImageItem` オブジェクトのリスト。
:rtype: List[ImageItem]
:raises FileNotFoundError: 入力ファイルが見つからない場合。
:raises RuntimeError: サポートされていない入力タイプ、または必要な依存関係が不足している場合。
"""
if input_value.lower() == "clip":
clip_path = ensure_clipboard_image(work_dir)
return [ImageItem(path=clip_path, label="Clipboard")]
input_path = Path(input_value)
if not input_path.exists():
raise FileNotFoundError(f"input file not found: {input_path}")
suffix = input_path.suffix.lower()
if suffix in IMAGE_EXTS:
return [ImageItem(path=input_path, label=input_path.name)]
if suffix in PDF_EXTS:
return render_pdf_to_images(input_path, work_dir / f"{slugify_filename(input_path.stem)}_images", dpi)
if suffix in OFFICE_EXTS:
pdf_dir = work_dir / f"{slugify_filename(input_path.stem)}_pdf"
pdf_path = convert_office_to_pdf(input_path, pdf_dir)
return render_pdf_to_images(pdf_path, work_dir / f"{slugify_filename(input_path.stem)}_images", dpi)
raise RuntimeError(f"unsupported input type: {suffix}")
# ---------- OCR ----------
[ドキュメント]
def tesseract_ocr_image(image_path: Path, lang: str = "jpn+eng") -> str:
"""
Tesseract OCRエンジンを使用して画像からテキストを抽出します。
`pytesseract` ライブラリを介して、指定された画像ファイルから
指定された言語でテキストを抽出します。
:param image_path: OCRを実行する画像ファイルへのパス。
:type image_path: Path
:param lang: OCRに使用する言語コード (例: "jpn", "eng", "jpn+eng")。
:type lang: str
:returns: 画像から抽出されたテキスト。
:rtype: str
:raises RuntimeError: pytesseractまたはPillowが利用できない場合。
"""
if pytesseract is None:
raise RuntimeError("pytesseract is required for tesseract OCR.")
if Image is None:
raise RuntimeError("Pillow is required for tesseract OCR.")
img = Image.open(image_path)
return pytesseract.image_to_string(img, lang=lang).strip()
[ドキュメント]
def build_markdown(items: List[ImageItem], texts: List[str], engine: str, source_name: str) -> str:
"""
複数のOCR結果と画像情報からMarkdown形式のレポートを作成します。
入力された画像アイテムのリストとそれに対応するOCR結果のテキストリストを結合し、
ソース名とOCRエンジン情報をヘッダーに含むMarkdown形式の文字列を生成します。
各画像アイテムは、`## ページ名` の形式でセクション化されます。
:param items: 処理された各画像に関する `ImageItem` オブジェクトのリスト。
:type items: List[ImageItem]
:param texts: 各画像から抽出されたテキストのリスト。`items` と同じ順序で対応します。
:type texts: List[str]
:param engine: OCRに使用されたエンジン名 (例: "tesseract", "genai")。
:type engine: str
:param source_name: 元の入力源の名前 (例: "input.pdf", "clipboard")。
:type source_name: str
:returns: 生成されたMarkdown文字列。
:rtype: str
"""
lines: List[str] = []
lines.append(f"# OCR Result: {source_name}")
lines.append("")
lines.append(f"- Engine: `{engine}`")
lines.append(f"- Segments: {len(items)}")
lines.append("")
for idx, (item, text) in enumerate(zip(items, texts), start=1):
lines.append("---")
lines.append("")
lines.append(f"## {item.label}")
lines.append("")
if text.strip():
lines.append(text.rstrip())
else:
lines.append("*(empty result)*")
lines.append("")
return "\n".join(lines).rstrip() + "\n"
# ---------- main ----------
[ドキュメント]
def parse_args(argv: Optional[Sequence[str]] = None) -> argparse.Namespace:
"""
コマンドライン引数を解析します。
入力ファイル、出力ファイル、OCRエンジン、言語、DPI、一時ファイル保持設定などの
各種コマンドラインオプションを解析し、`argparse.Namespace` オブジェクトとして返します。
:param argv: コマンドライン引数のリスト。`None` の場合、`sys.argv` を使用します。
:type argv: Optional[Sequence[str]]
:returns: 解析された引数を格納する `argparse.Namespace` オブジェクト。
:rtype: argparse.Namespace
"""
parser = argparse.ArgumentParser(description="OCR専用変換ツール。出力は常に Markdown (.md)")
parser.add_argument("input", nargs="?", default="", help="入力ファイル。画像/PDF/Office文書、または 'clip'")
parser.add_argument("--output", "-o", default=None, help="出力Markdownファイル名")
parser.add_argument("--engine", "-e", choices=["tesseract", "genai"], default="tesseract",
help="OCRエンジン")
parser.add_argument("--ini", "-i", default=None, help="genAI用プロンプト設定ファイル")
parser.add_argument("--api", "-a", default="gemini", help="genAI API名 (既定: gemini)")
parser.add_argument("--model", "-m", default=None, help="genAIモデル名")
parser.add_argument("--lang", default="jpn+eng", help="tesseract OCR言語")
parser.add_argument("--dpi", type=int, default=300, help="PDF/Officeを画像化する際のDPI")
parser.add_argument("--keep-images", action="store_true",
help="中間画像・中間PDFを削除せず残す")
parser.add_argument("--workdir", default="", help="作業ディレクトリ。未指定時は一時ディレクトリ")
parser.add_argument("--ai-env", default="ai.env", help="AI用環境変数ファイル (既定: ai.env)")
parser.add_argument("--list", action="store_true", help="対応形式・依存関係・実行例を表示して終了")
return parser.parse_args(argv)
[ドキュメント]
def main(argv: Optional[Sequence[str]] = None) -> int:
"""
スクリプトのメインエントリポイントです。
コマンドライン引数を解析し、OCR処理のフロー全体を管理します。
具体的には、以下の処理を実行します:
1. 引数解析。
2. `--list` オプションが指定された場合、利用可能性レポートを表示して終了。
3. 入力値のチェックと出力パスの決定。
4. 作業ディレクトリの準備(一時ディレクトリまたは指定されたディレクトリ)。
5. 入力タイプに応じて画像を収集 (クリップボード、画像ファイル、PDF、Office文書)。
6. 選択されたOCRエンジン (TesseractまたはGenAI) で画像からテキストを抽出。
7. 抽出されたテキストと画像情報からMarkdownファイルを構築。
8. Markdownファイルを作業ディレクトリに出力パスに書き込み。
9. エラー処理と、必要に応じて一時ディレクトリのクリーンアップ。
:param argv: コマンドライン引数のリスト。`None` の場合、`sys.argv` を使用します。
:type argv: Optional[Sequence[str]]
:returns: 成功時は `0`、エラー時は `1` (一般的なエラー) または `2` (引数エラー) の終了コード。
:rtype: int
"""
args = parse_args(argv)
if args.list:
print(availability_report())
return 0
if not args.input:
eprint("error: input is required unless --list is specified")
return 2
output_path = default_output_path(args.input, args.output)
workdir_obj: Optional[tempfile.TemporaryDirectory[str]] = None
if args.workdir:
work_dir = Path(args.workdir)
work_dir.mkdir(parents=True, exist_ok=True)
else:
workdir_obj = tempfile.TemporaryDirectory(prefix="convert_ocr_")
work_dir = Path(workdir_obj.name)
try:
items = collect_images(args.input, work_dir, args.dpi)
texts: List[str] = []
if args.engine == "tesseract":
for item in items:
print(f"[tesseract] OCR: {item.label} -> {item.path}")
texts.append(tesseract_ocr_image(item.path, lang=args.lang))
else:
prompt = load_genai_prompt(args.ini)
for item in items:
print(f"[genai] OCR: {item.label} -> {item.path}")
texts.append(call_genai_ocr(prompt, item.path, api=args.api, model=args.model, ai_env=args.ai_env))
md_text = build_markdown(items, texts, args.engine, Path(args.input).name if args.input != "clip" else "clipboard")
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(md_text, encoding="utf-8")
print(f"OK: wrote {output_path}")
if args.keep_images:
print(f"work files kept in: {work_dir}")
workdir_obj = None # prevent cleanup of temp dir object below
return 0
except Exception as exc:
eprint(f"ERROR: {exc}")
return 1
finally:
if workdir_obj is not None:
workdir_obj.cleanup()
if __name__ == "__main__":
raise SystemExit(main())