FR-030.3 — 證據分類支援 .xlsx / .pptx(本地抽文字)

規劃日期:2026-05-29 撰寫人:raymond + Claude 狀態:設計草稿,待 user 確認抽取預設 → spec review → implementation plan arc 關聯:↳ FR-030 子(延續「補上分類器目前跳過的常見檔型」,sibling of FR-030.2)

§1

一句話描述

讓 FR-030 證據分類器支援 Excel(.xlsx)與 PowerPoint(.pptx)證據 —— 兩者 Claude API 無原生 block,沿用 FR-030.2 已建好的「本地抽文字 → text block」路線(同 docx 機制),把 cell / 投影片文字抽出來送分類,分類與歸檔流程完全不變。

§2

為什麼要做

合規證據裡 Excel 清單(資產清冊、帳號表、設定矩陣)與簡報(教育訓練、政策說明)很常見。FR-030.2 完工後,分類器已能吃文字檔 + 圖片 + PDF,但 .xlsx / .pptx 仍落在 build_content_blocks 的「unsupported → skip」分支,被列未處理要人工歸檔。本期補上這兩種。

§3

為什麼是「抽文字」不是 Vision

檔型 Claude API 原生 block 處理路線
圖片 / PDF 有(image / document) base64 直送(FR-030.2 已做)
docx / xlsx / pptx 本地抽純文字 → text block

xlsx / pptx 跟 docx 同類,沒有原生 block,必須自己在本地解析成文字。所以本功能不碰 vision 路徑,只擴充 FR-030.2 的 text 分支。

§4

核心原則(同 FR-030.2)

原分類 / 歸檔流程不變。_state.json 結構、Drive 歸檔、review UI 全沿用。xlsx / pptx 只是多兩種能抽成文字的檔型,出分類器後與其它檔走同一條歸檔路。


§5

架構:擴充 FR-030.2 的 text 分支

FR-030.2 後,build_content_blocks 的文字分支為:

if ext in {".docx", ".csv", ".log", ".txt", ".md"}:
    text = _extract_text_legacy(path)
    return [{"type": "text", "text": f"Filename: {filename}\n..."}], None

本期改動:

  1. ext 集合加 .xlsx.pptx
  2. _extract_text_legacy 加兩個分支抽取(mirror 既有 .docx 用 python-docx 的 pattern)

抽取邏輯(待 user 確認的預設)

.xlsx(openpyxl)

elif ext == ".xlsx":
    from openpyxl import load_workbook
    wb = load_workbook(str(path), read_only=True, data_only=True)  # data_only=取值不取公式
    parts = []
    for ws in wb.worksheets:
        parts.append(f"# Sheet: {ws.title}")
        for row in ws.iter_rows(values_only=True):
            cells = [str(c) for c in row if c is not None]
            if cells:
                parts.append(" | ".join(cells))
    text_out = "\n".join(parts)
  • 所有工作表、逐列 cell 用 | 串接(比照 docx 表格)
  • data_only=True:取計算後的值而非公式字串
  • read_only=True:大檔記憶體友善

.pptx(python-pptx)

elif ext == ".pptx":
    from pptx import Presentation
    prs = Presentation(str(path))
    parts = []
    for i, slide in enumerate(prs.slides, 1):
        parts.append(f"# Slide {i}")
        for shape in slide.shapes:
            if shape.has_text_frame and shape.text_frame.text.strip():
                parts.append(shape.text_frame.text)
            if shape.has_table:
                for r in shape.table.rows:
                    parts.append(" | ".join(c.text for c in r.cells))
    text_out = "\n".join(parts)
  • 所有投影片的文字框 + 表格
  • 預設不含講者備忘稿(speaker notes)— 通常非證據內容(← 待 user 確認,要含 notes 可加 slide.notes_slide

兩者最後沿用既有 MAX_TEXT_CHARS=8000 截斷(在 _extract_text_legacy 內)。

待 user 確認的設計決策

  1. xlsx 取值不取公式data_only=True)— OK?
  2. pptx 不含講者備忘稿 — OK?要含的話加一行。
  3. 超大 xlsx / 超多投影片:是否要設上限(如 xlsx >N 列、pptx >M 頁就截斷或 skip)?或單純靠 MAX_TEXT_CHARS 截斷即可(傾向後者,簡單)。

§6

範圍邊界

✅ 本次做:

  • .xlsx / .pptx 走本地抽文字 → text block → 分類
  • 新增依賴 openpyxl / python-pptx(container requirements)
  • 擴充 _extract_text_legacy + build_content_blocks ext 集合 + 單元測試

❌ 本次不做:

  • .xls / .ppt(舊二進位格式,需 LibreOffice 轉換,另議)
  • xlsx 內嵌圖表 / 圖片的視覺辨識(只抽文字)
  • 成本控管、_state.json schema 變更、review UI 改版、vision 路徑任何改動
§7

測試

  • 單元測試 build_content_blocks.xlsx / .pptx:用 openpyxl / python-pptx 動態建小檔,斷言抽出的 text block 含 cell / slide 文字
  • 回歸:既有 docx/csv/image/pdf 分支不受影響
§8

整合點 / 既有資產

  • FR-030.2 已建管道:scripts/evidence/classify/docker/container_entrypoint.py_extract_text_legacy / build_content_blocks
  • FR-030.2 spec:docs/features/FR-030.2-2605-evidence-classify-image-vision/design.md
  • container 依賴:scripts/evidence/classify/docker/requirements.txt
  • 測試:scripts/evidence/classify/docker/test_container_entrypoint.py(沿用,加 case)

§9

§11 實作後對齊(Reconciliation,2026-05-30 收尾補)

本 FR 原本只規劃 xlsx/pptx 抽文字,但 2026-05-30 的 task arc 一路長成「證據分類器全面強化」。實際交付遠超原 design,以下逐條對齊:

# 原設計 實際做了什麼 原因
1 xlsx/pptx 抽文字 ✅ 照做
2 「不做」內嵌圖視覺辨識 ❌ 推翻 → 做了(office 內嵌圖 → LibreOffice→PDF→vision,cost-aware:有圖才轉) user 提出「貼圖的文件分不到」是真痛點;BE 既有 LibreOffice converter 可借鏡
3 (無)trigger 選項 新增:選模型(sonnet/opus,UI flag 隱藏預設 sonnet)+ analyze-only 歸檔模式 user 要先驗分析效果不污染 Drive + 想試 Opus
4 (無)一次歸檔 新增 POST /classification-run/<id>/archive + 審閱頁「立即歸檔」按鈕/提醒 analyze-only 跑完需事後正式歸檔
5 (無)可觀測性 新增 container log 上傳 Drive(憑證遮蔽)+ token/成本統計(UI flag 隱藏預設關) 查圖片 Fail 免 SSH + 想知道成本
6 (無)prompt 調校 用真實證據集三方對照 → 加證據提示 + AO-letter 完整性 + 過配收斂 真實 run 發現的分類落差

關鍵決策軌跡(詳見 analysis)

  • 分類落差三分法:辨識缺口(可 prompt 修)/ 字母粒度(B 規則)/ User 標籤爭議(改模型無解,需修 ground truth)。詳見 docs/analysis/2026-05-30-cmmc-evidence-classification-accuracy-gap.md
  • 不降 confidence 門檻:用 prompt 讓對的 AO 信心升高,而非降門檻(會惡化過配)
  • prompt 收斂到此為止:過配 22→8 後,剩餘多為單一頑固檔,再追會過擬合此資料集
  • 內嵌圖只在「有圖」才轉 PDF:避免 text-only 文件白付 vision 成本

對話紀錄

docs/conversation-history/2026-05-30/evidence-classify-arc/(逐輪 dump,2 parts)