Phase 2: 轉換管線 — 五個本地轉換器 + convert.py 統一入口 + selfcheck

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
LittleYellow
2026-07-14 20:51:17 +08:00
parent 59c16ea393
commit 8d58436f05
9 changed files with 673 additions and 0 deletions

70
tools/convert/from_pdf.py Normal file
View File

@@ -0,0 +1,70 @@
"""PDF → Markdown。無文字層掃描件回報 needs_ocr 並跳過,不擅自呼叫 OCR。
可獨立執行python tools/convert/from_pdf.py <file.pdf> [-o out.md] [--dry-run]
"""
import argparse
import pathlib
import fitz # pymupdf
# ponytail: 掃描件偵測用「平均每頁字元數 < 25」的 naive heuristic
# 混合型 PDF部分頁掃描會整份判為有文字層升級路徑逐頁判定 + 逐頁標記。
_MIN_CHARS_PER_PAGE = 25
def convert(src, assets_dir=None, md_dir=None):
"""回傳 (markdown, status)。status ∈ {'ok', 'needs_ocr'}。
ponytail: 表格輸出為純文字行不重建表格結構升級路徑page.find_tables()。
"""
src = pathlib.Path(src)
md_dir = pathlib.Path(md_dir) if md_dir else src.parent
doc = fitz.open(str(src))
pages = [page.get_text("text").strip() for page in doc]
total = sum(len(p) for p in pages)
if total < _MIN_CHARS_PER_PAGE * max(len(pages), 1):
doc.close()
return "", "needs_ocr"
counter = 0
blocks = []
for i, page in enumerate(doc):
blocks.append(f"<!-- page {i + 1} -->\n\n{pages[i]}")
if assets_dir is None:
continue
for img in page.get_images(full=True):
xref = img[0]
pix = fitz.Pixmap(doc, xref)
if pix.n > 4: # CMYK 等 → 轉 RGB
pix = fitz.Pixmap(fitz.csRGB, pix)
counter += 1
name = f"img{counter}.png"
assets_dir.mkdir(parents=True, exist_ok=True)
pix.save(str(assets_dir / name))
rel = (assets_dir / name).relative_to(md_dir).as_posix()
blocks.append(f"![{name}]({rel})")
doc.close()
return "\n\n".join(blocks), "ok"
def main(argv=None):
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("src")
ap.add_argument("-o", "--output")
ap.add_argument("--dry-run", action="store_true")
a = ap.parse_args(argv)
if a.dry_run:
md, status = convert(a.src)
print(f"[dry-run] {a.src}: status={status}, {len(md)} chars")
return
out = pathlib.Path(a.output) if a.output else pathlib.Path(a.src).with_suffix(".md")
md, status = convert(a.src, assets_dir=out.parent / f"{out.stem}-assets", md_dir=out.parent)
if status == "needs_ocr":
print(f"needs_ocr: {a.src} 無文字層,已跳過(不擅自 OCR")
raise SystemExit(2)
out.write_text(md, encoding="utf-8")
print(out)
if __name__ == "__main__":
main()