Files
pp-qa-km/tools/convert/from_legacy.py
LittleYellow cfd1876359 轉換工具強化:失敗診斷訊息 + 舊版格式升版 + 掃描 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>
2026-07-21 07:00:32 +08:00

92 lines
3.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""舊版 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()