轉換工具強化:失敗診斷訊息 + 舊版格式升版 + 掃描 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,61 @@
"""影像 → Markdown 轉錄(本地 vision 模型AGENTS.md §1.1 資料落地)。
用於 vision fallback文字抽取失敗的頁面 render 成影像後,逐頁交本地 vision
模型忠實轉錄為 Markdown。模型設定讀 config/models.yaml 的 tasks.vision§1.2
未設定即報錯、不使用預設值;:cloud 模型一律拒絕(影像不得離開本機)。
"""
import base64
import pathlib
import sys
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1])) # tools/ 供 import kb
import kb
VISION_PROMPT = (
"你是文件轉錄引擎。忠實轉錄此頁影像中的所有文字與表格內容為 Markdown"
"表格用 Markdown table保留標題階層。只輸出轉錄內容本身不要加任何說明或臆測。"
"影像中沒有可辨識內容時,只回覆「(本頁無可辨識文字)」。")
def _require_vision(cfg):
"""取得並驗證 vision 模型設定。缺設定 / :cloud 皆報錯信任邊界§1.1/§1.2)。"""
spec = (cfg.get("tasks") or {}).get("vision")
if not isinstance(spec, dict) or not spec.get("model") or spec["model"] == "CHANGE_ME":
raise SystemExit(
"models.yaml 未設定 tasks.vision.model--vision 需要合規本地 vision 模型"
"(請自行確認非中國關聯、非 :cloudAGENTS.md §1.1/§1.3)。")
if str(spec["model"]).endswith(":cloud"):
raise SystemExit("tasks.vision.model 為 :cloud 模型禁止文件影像不得離開本機§1.1)。")
return spec
def _b64(path):
return base64.b64encode(pathlib.Path(path).read_bytes()).decode("ascii")
def transcribe(cfg, image_paths):
"""逐頁轉錄影像,回傳 (每頁 markdown 列表, 實際使用的 model)。
單頁重試 max_retries 次仍失敗即拋 RuntimeErrorvision 無文字 fallback 可用——
fallback 模型非 vision不切換
"""
spec = _require_vision(cfg)
model = spec["model"]
retries = int(spec.get("max_retries", 2))
temperature = float(spec.get("temperature", 0.1))
pages = []
for idx, img in enumerate(image_paths, 1):
b64 = _b64(img)
last = None
for _ in range(max(1, retries)):
try:
text = kb.ollama_chat(cfg, model, VISION_PROMPT, temperature, images=[b64])
if text and text.strip():
pages.append(text.strip())
break
last = "空回應"
except Exception as e:
last = f"{type(e).__name__}: {e}"
else:
raise RuntimeError(f"vision 轉錄失敗(第 {idx}model={model}{last}")
return pages, model