- 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>
92 lines
3.5 KiB
Python
92 lines
3.5 KiB
Python
"""舊版 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 安裝路徑;其他平台靠 PATH(soffice/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(
|
||
"找不到 LibreOffice(soffice);.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()
|