轉換工具強化:失敗診斷訊息 + 舊版格式升版 + 掃描 PDF vision fallback

- convert.py:轉換失敗改印例外型別、批次尾端彙總、--traceback 旗標;
  空白轉換結果示警(避免純圖片 docx 等假成功靜默入庫)
- from_legacy.py(B):.doc/.xls/.ppt 經本地 LibreOffice headless 升版為
  現代格式後再走既有轉換器;raw/originals 仍保存真正的舊格式原始檔
- to_image.py + from_vision.py(A):needs_ocr 的掃描/圖片型 PDF 逐頁
  render → 本地 vision 模型轉錄(--vision);:cloud/未設定防護,
  模型設定走 config/models.yaml 的 tasks.vision(預設不啟用)
- kb.ollama_chat 加 images 參數(向後相容);models.yaml 加 vision 範例
- selfcheck 補 legacy 路由、to_image 真測、from_vision 注入測、
  --vision 端到端,全數 ALL PASS

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
LittleYellow
2026-07-21 07:00:32 +08:00
parent 1d1454e1b9
commit cfd1876359
7 changed files with 428 additions and 37 deletions

View File

@@ -0,0 +1,91 @@
"""舊版 OLE 二進位格式(.doc/.xls/.ppt→ 現代格式(.docx/.xlsx/.pptx升版。
以 LibreOffice headless 在本機升版deterministic、內容不外流AGENTS.md §1.1
升版後由 convert.py 回頭走既有 from_docx / from_xlsx / from_pptx 轉換器。
升版產物是暫時中介檔,不進 raw/raw/originals 仍保存真正的舊格式原始檔§1.4)。
可獨立執行python tools/convert/from_legacy.py <file.doc> [--out DIR]
"""
import argparse
import os
import pathlib
import shutil
import subprocess
import sys
import tempfile
LEGACY_TARGET = {".doc": "docx", ".xls": "xlsx", ".ppt": "pptx"}
# 常見 Windows 安裝路徑;其他平台靠 PATHsoffice/libreoffice或 SOFFICE_BIN 覆寫。
_WIN_CANDIDATES = (
r"C:\Program Files\LibreOffice\program\soffice.exe",
r"C:\Program Files (x86)\LibreOffice\program\soffice.exe",
)
def find_soffice():
"""定位 LibreOffice 執行檔。SOFFICE_BIN 環境變數優先(唯一允許的覆寫來源)。"""
env = os.environ.get("SOFFICE_BIN")
if env and pathlib.Path(env).is_file():
return env
for c in _WIN_CANDIDATES:
if pathlib.Path(c).is_file():
return c
return shutil.which("soffice") or shutil.which("libreoffice")
def upgrade(src, out_dir, timeout=180):
"""升版 src 至 out_dir回傳升版後檔案路徑。
找不到 LibreOffice、格式不支援、或升版未產出檔案時拋 RuntimeError
convert.py 逐檔捕捉並回報,不中斷批次)。
"""
src = pathlib.Path(src)
ext = src.suffix.lower()
if ext not in LEGACY_TARGET:
raise RuntimeError(f"非舊版格式:{src.name}")
soffice = find_soffice()
if not soffice:
raise RuntimeError(
"找不到 LibreOfficesoffice.doc/.xls/.ppt 升版需要它。"
"請安裝 LibreOffice或以 SOFFICE_BIN 環境變數指定執行檔路徑。")
out_dir = pathlib.Path(out_dir)
out_dir.mkdir(parents=True, exist_ok=True)
target = LEGACY_TARGET[ext]
# 每次呼叫用獨立 user profile避免 headless 並行/殘留 profile 造成的鎖定失敗
# LibreOffice headless 的已知痛點§13.3:真實環境不是規格書上的理想值)。
profile = out_dir / ".lo-profile"
try:
subprocess.run(
[soffice, f"-env:UserInstallation=file:///{profile.as_posix().lstrip('/')}",
"--headless", "--convert-to", target, "--outdir", str(out_dir), str(src)],
check=True, capture_output=True, text=True, timeout=timeout)
except subprocess.CalledProcessError as e:
raise RuntimeError(f"LibreOffice 升版失敗exit {e.returncode}{e.stderr.strip()}")
except subprocess.TimeoutExpired:
raise RuntimeError(f"LibreOffice 升版逾時(>{timeout}s{src.name}")
finally:
shutil.rmtree(profile, ignore_errors=True)
out = out_dir / f"{src.stem}.{target}"
if not out.is_file():
raise RuntimeError(f"LibreOffice 未產出預期檔案:{out.name}")
return out
def main(argv=None):
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("src")
ap.add_argument("--out", help="升版產物輸出目錄(預設暫存目錄)")
a = ap.parse_args(argv)
out_dir = pathlib.Path(a.out) if a.out else pathlib.Path(tempfile.mkdtemp(prefix="ppqa-legacy-"))
try:
print(upgrade(a.src, out_dir))
except RuntimeError as e:
print(f"error: {e}", file=sys.stderr)
raise SystemExit(1)
if __name__ == "__main__":
main()