- 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>
62 lines
2.7 KiB
Python
62 lines
2.7 KiB
Python
"""影像 → 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 模型"
|
||
"(請自行確認非中國關聯、非 :cloud,AGENTS.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 次仍失敗即拋 RuntimeError(vision 無文字 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
|