Transcript → Rule Cards · 技術解說

逐字稿校正與規則萃取流程

轉錄完成的逐字稿,是怎麼經過 term_fix.TERM_TABLE 與 GOOGLE_API_KEY,最後變成一張張可回溯的規則卡?這兩者其實是前後兩站、互不相干。

term_fix 確定性、不花錢 Gemini 唯一花錢的一步 quote 三段式 不採信 LLM 自報 原始逐字稿 一個字不改
0

一張圖看懂全貌

影片線為例;課程線同一套,教材線沒有逐字稿所以不走校正

關鍵只有一句:校正只在記憶體裡做,餵給 LLM 與拿來定位引文的是同一份校正後文字,硬碟上的原始逐字稿永遠不動。金鑰只在第三站用到,其餘五站都是本機確定性運算。

原始逐字稿 transcripts/<vid>.md 證據,一個字不改 ① term_fix.fix TERM_TABLE 115 條 只在記憶體,不寫檔 ② build_prompt 前 4 萬字+指標詞彙 +既有 rule_id 目錄 ③ Gemini 呼叫 GOOGLE_API_KEY 先扣今日額度(100) 唯一花錢的一站 ④ 解析 JSON → 卡片 ⑤ 逐卡對照「校正後逐字稿」 ≥0.8 已驗證|0.5–0.8 且連續 ≥8 字相同留但未驗證|其餘丟 timestamp 由 quote 位置推導、覆寫 LLM 自報;video_id/date 我們填 同一份校正稿 ⑥ 四道護欄 分類白名單/欄位非空 無個股名/出處必備 規則卡 rule_cards/<vid>.json term_fixed: true 本機、確定性、免費 雲端、花錢、限額 本機

綠色虛線是整條線的靈魂:餵入與定位必須是同一份校正稿,否則含「權證」的引文會對不上原稿裡的「權陣」而被當成幻覺丟掉。

A

術語校正:TERM_TABLE

whisper 把「買盤竭盡」聽成「捷徑」127 次,語意完全相反

2026-08-28 抽查 14 份課程逐字稿,「買賣盤竭盡」這個核心轉折概念被整批轉成「捷徑」127 次、正確拼寫 0 次。重轉沒用:whisper 的 initial_prompt 只影響開頭 30 秒,改 prompt 重轉一支 46 分鐘影片,輸出與舊版逐位元組相同。所以校正放在消費端,原始檔不動。

2026-10-01 補充:確定性解碼(temperature=0)一旦陷入幻覺迴圈,連換切點重轉也逃不掉。處置神器第 3 支的開頭 20 分鐘,整片、20 分鐘分段、5 分鐘小塊三次都只剩 85 字;改以溫度回退(壓縮比過高就以較高溫度重解)後,同一段轉出 985 字。現在課程線的分段轉錄遇到單段字數低於 25 字/分,會自動以溫度回退重轉一次;這只補救整段掉字,辨音錯字仍靠校正表。

怎麼來的不是憑印象列的

  • 從既有規則卡的 indicators 撈出 53 個受控術語
  • 反掃 14 份逐字稿,找出被辨音取代的形式
  • 每條記下實際命中次數,有測試釘住「沒有命中證據不准加」
  • 之後每次萃取實跑漏進卡片的錯字(軍架線、歸離、權陣)再逐條補

怎麼用綁上下文,不做裸詞替換

  • 鍵是「買盤捷徑」不是「捷徑」——哪天講者真的說「這是條捷徑」不會被改壞
  • 長字串先替換,避免短規則吃掉長規則的一部分
  • TERM_GUARDS 前字排除:「全正」前面是「完/安」不改(完全正確、安全正常),「頭信」前面是「多/空/龍」不改(多頭信心)——後者是就地校正卡片時實際誤改過一次才補上的
  • fix_with_report() 回報改了哪幾條、幾次;筆記與卡片都標「已校正」
  • 三個消費者:影片線萃取、課程線萃取、課程整合筆記(只為顯示)

關鍵片段 1 · 對照表與替換

整個模組只有一張 dict 和一個迴圈。值得看的是每條後面的命中數:那是證據,不是裝飾。

scripts/xiaoge/term_fix.pyPYTHON
# {錯誤形式: (正確形式, 實際命中次數)}——命中數是對 14 份課程逐字稿的實測值
TERM_TABLE: dict[str, tuple[str, int]] = {
    # 核心概念(語意完全相反,最嚴重)——綁定「買盤/賣盤/脈盤」前綴
    "買盤捷徑": ("買盤竭盡", 64),
    "賣盤捷徑": ("賣盤竭盡", 50),
    "脈盤捷徑": ("賣盤竭盡", 3),      # 「賣盤」也被聽成「脈盤」
    # 技術指標
    "軍線": ("均線", 10),
    "歸離": ("乖離", 3),          # 2026-08-30 重萃時漏進 quote 才補
    # 權證工具:「權陣大戶/大出權陣/用權陣」8 次全指權證
    "權陣": ("權證", 8),
    …共 115 條(2026-09-25 補「全政/全正/全鎮/全證→權證」「頭信→投信」「科轉債→可轉債」等 14 條,再補逐字稿原文欄露出的「軍量→均量」「存碼→籌碼」「贈雨→贈與」4 條)
    …2026-10-01 再補處置策略用語 57 條(累犯/處置/位階/葛蘭碧/個股名等),見下一張卡
    …2026-10-08/10-13 再補 PPA 審閱用語 18 條(停牌→聽牌、格子沖→隔日沖、自行商→自營商、開步林→開布林、魏華科→蔚華科、文貌→穩懋等),見再下一張卡
}

def fix_with_report(text):
    changes = {}
    # 長的先替換:避免短規則吃掉長規則的一部分
    for wrong in sorted(TERM_TABLE, key=len, reverse=True):
        right, _ = TERM_TABLE[wrong]
        n = text.count(wrong)
        if n:
            text = text.replace(wrong, right)
            changes[f"{wrong}→{right}"] = n
    return text, changes
註解為什麼重要:校正表是「消費端修正」而非「改資料」。原始逐字稿是證據,比照萃取線「不採信 LLM 自報,但也不竄改原始資料」的作風。這也是為什麼影片線 2026-08-30 才接上校正時,先前就地改過的卡片重萃又把錯字帶回來——不接源頭,改下游沒有用。

2026-10-01 批次 · 處置策略用語 +57 條

處置神器課程(雙刀處置策略實戰班)的核心名詞整批被聽錯,而且舊逐字稿也大量中標:處置神器以外的 71 份裡,「儲值」(=處置)28 處、「格蘭幣」(=葛蘭碧)13 處。做法同上——對 74 份逐字稿(影片 57+課程 17)反掃,每條逐筆看過上下文才收。57 條分六組:初犯/累犯、處置、位階/均值/浪子回頭/找買點空點、葛蘭碧/通道、被關/停損/尾盤等、個股名(雙刀案例的主角,用畫面上的名稱對過)。

代表例子整組名詞一起被聽錯

  • 累販→累犯:「累販跌的機率非常大」(初犯也有「初販/初飯/粗範」)
  • 儲值→處置:「台股最大bug就是儲值股」「儲值神器」
  • 君子以上→均值以上:「君子以上做空 君子以下做舵」=均值以上做空、以下做多
  • 格蘭幣→葛蘭碧:「格蘭幣八大法則」

刻意不改長得像錯字的正常用詞

  • 「儲權」是除權(儲權息、儲權前一天買),不跟著「儲值」改成處置
  • 「穩貿」是券商分點名(穩貿分點),只收「穩帽→穩懋」
  • 「八國聯軍」「正人君子」「通告費」是正常用詞:聯軍 用 TERM_GUARDS 排除前字「國」,「君子」只綁「以上/以下」,「通告」只綁「上/下/布林」

2026-10-08/10-13 批次 · PPA 訂閱線審閱補詞 +18 條(97 → 115)

PPA 訂閱線(付費專欄影片逐字稿,走同一份校正)試跑與 PM 審閱規則卡時,逐條抓出的辨音錯字;命中數都是對影片線+課程+PPA 逐字稿轉繁體後實測、逐筆看過上下文才收。專名同音(股名、券商分點)另由 name_fix 比對籌碼湖名冊處理,不進這張表。

代表例子審閱時被 PM 指出的錯字

  • 停牌→聽牌(8 處)、格子沖→隔日沖(11 處):76 份逐字稿裡全是此義,沒有真的暫停交易
  • 自行商→自營商(43 處/14 檔)、出價單→觸價單(7 處)、真頂→臻鼎(6 處)
  • 開步林/開步零/開埠零→開布林、價籌倍率→價籌背離、魏華科→蔚華科、文貌→穩懋(20 處,「3105文貌」)

刻意不改會撞到正常用詞或真用詞

  • 「處置癌」是小哥的真用詞,不是處置股聽錯,不改
  • 「高利→高力」(8996)不收:會撞到「高利率/高利貸」,只就地校正卡片
  • 備關→被關 用 TERM_GUARDS 排除「準備關門」「設備關機」
B

LLM 呼叫與證據鏈

金鑰怎麼解析、額度怎麼守、LLM 說的話怎麼驗

一集逐字稿進 extract_for_episode() 之後的六步。LLM 只負責「找出規則」,事實一律由我們填或推導。

  1. 校正term_fix.fix

    逐字稿在記憶體裡套 TERM_TABLE,這份校正稿同時給第 2 步與第 5 步用。

  2. 組 promptbuild_prompt

    取前 40,000 字,附指標詞彙表、五類 category 白名單、provenance 與「個股名不得進 condition/action」的硬規則,以及既有 rule_id 目錄(上限 1000,重萃時排除本集舊卡)讓跨集命名對齊;並要求逐條列出、名詞解釋做成知識卡。

  3. 呼叫 Gemini_gemini_complete

    每集呼叫兩輪:第二輪把第一輪的卡列進 prompt、只補漏掉的,兩輪合併時同 id 去重(extract_episode_two_pass)。每輪都解析 GOOGLE_API_KEY(環境變數 → 專案根 .env → 缺就 fail fast),經 call_with_rate_limit_retry 先扣今日額度,再用 google-genai 打 gemini-3.8-flash;被伺服器每分鐘上限(429)擋下會退回額度、等一下再試。

  4. 解析parse_llm_response

    抽 JSON 陣列。「明確的 []」是合法空結果(整集都是行情點評);截斷或畸形的回應一律報錯、不寫檔,留在佇列下次重跑(2026-09-27 影片線跟進課程線)。

  5. 逐卡驗證引文match_quote

    在校正稿裡模糊比對 quote。相似度=2×相同字數÷兩邊總長;比對前兩邊統一成繁體、略過 ## MM:SS 標題與截圖行、去標點空白;引文含「...」就拆段各自比對、依字數加權;在逐字稿上逐字滑動取最高分。判定:≥0.8 已驗證;0.5–0.8 且命中處有連續 ≥8 字與逐字稿相同(錨點)保留標未驗證;其餘整卡丟(幻覺)。影片卡驗證率 97%(2026-10-26 實測 769 張中 746 張;2026-09-25 改良前 61%)。命中處另截出逐字稿原文存成 quote_source,手冊在與引文用字不同時並列。timestamp 由命中位置往前找最近的 ## MM:SS 推導、覆寫 LLM 自報;截圖取最近且確實存在的幀。

  6. 四道護欄filter_cards

    category 白名單、condition/action 非空、個股名不得進規則本文、provenance 必備。「有產出但全被丟棄」會 raise 退回佇列重跑,不會把空結果固化成已完成。

關鍵片段 2 · 同一份校正稿,餵入與定位都用它

這段是整條線最容易做錯的地方。註解裡寫的「實測 3 張卡」與「定位失敗被當幻覺丟掉」都是真的踩過。

scripts/xiaoge/extract_rules.py · extract_for_episode()PYTHON
# 術語校正:餵入與 quote 定位**都用 fix 後的文字**(同一空間)。餵原稿會讓
# 「權陣」這類辨音錯誤原樣穿透進 condition/quote(實測 3 張卡);餵校正後
# 卻對原稿定位,含「權證」的 quote 會定位失敗被當幻覺丟掉。
transcript = term_fix.fix(transcript)
prompt = build_prompt(transcript, meta, rule_catalog=rule_catalog)
raw = complete_fn(prompt)                      # 測試注入假函式;正式跑 _gemini_complete
cards = parse_llm_response(raw)

for card in cards:
    prov = card.get("provenance") or {}
    match = match_quote(prov.get("quote", ""), transcript)
    status = classify_quote(match.similarity, match.anchor)   # 0.8/0.5+連續 8 字
    position = match.position
    if status == "rejected":          # 幻覺風險,整卡丟
        quote_rejected += 1
        continue
    prov["video_id"] = video_id                  # 事實由我們填,不採信 LLM
    prov["episode_title"] = episode_title
    prov["date"] = date
    prov["quote_verified"] = status == "verified"   # ≥0.8
    prov["term_fixed"] = True            # quote 是 fix 後空間的文字
    set_quote_source(prov, transcript, match)    # 逐字稿截出的原文
    # timestamp 一律用「quote 定位 → 往前找最近標題」推導出的值覆蓋 LLM 給的值
    derived = derive_timestamp_from_quote(transcript, position)
    if derived: prov["timestamp"] = derived
    else:       prov.pop("timestamp", None)   # 寧可缺席交給護欄丟,不留猜測值
註解為什麼重要:曾因採信 LLM 自報的時間戳,讓卡片指向不存在的截圖。現在 provenance 裡除了 quote 本身,沒有任何一個欄位是 LLM 說了算:video_id 與日期來自 manifest,timestamp 由引文實際位置推導,截圖只配確實存在的檔案。

關鍵片段 3 · 金鑰解析、額度預約與限流重試

金鑰在整條線只出現在這裡。三件設計:金鑰解析與 groq 那把口徑一致;額度在呼叫之前扣,寧可少打不可超打;被伺服器每分鐘上限擋下時退回額度、等一下再試。

scripts/xiaoge/extract_rules.py + llm_config.pyPYTHON
def _resolve_google_api_key() -> str:
    key = os.environ.get("GOOGLE_API_KEY")         # 1. 環境變數優先
    if not key:
        env = PROJECT_ROOT / ".env"                 # 2. 其次專案根 .env 的那一行
        if env.exists():
            for line in env.read_text(encoding="utf-8").splitlines():
                if line.startswith("GOOGLE_API_KEY="):
                    key = line.split("=", 1)[1].strip().strip('"').strip("'")
                    break
    if not key:
        raise RuntimeError("缺少 GOOGLE_API_KEY:…")   # 3. 缺就 fail fast,不靜默
    return key

def _gemini_complete(prompt: str) -> str:
    api_key = _resolve_google_api_key()
    return llm_config.call_with_rate_limit_retry(      # 每次嘗試前預約今日額度(上限 100)
        lambda: llm_config.gemini_generate(prompt, api_key, temperature=llm_config.TEXT_TEMPERATURE))

# call_with_rate_limit_retry:reserve_call(已滿就 raise DailyQuotaExceeded)→ 呼叫 →
#   被 429 擋下:退回額度、等 20/40/60 秒(伺服器有建議就照它)再試;到底仍被擋 raise RateLimited
# gemini_generate:google-genai 打一次、回傳文字;單次逾時 600 秒,SDK 只重試 503(429 留給上面那層)
註解為什麼重要:DailyQuotaExceeded 刻意不繼承 RuntimeError,且 main() 的 except 必須排在 except Exception 之前並 break。否則額度用盡會被逐集容錯接走,對剩下每一集各記一次失敗、燒光重試計數。這個缺陷模式本專案一天內在四個階段各踩過一次。每分鐘限流重試到底丟的 RateLimited 是它的子類,同一個 except 一併接住。
站在哪花錢?產物
① 術語校正term_fix.fix否,確定性記憶體裡的校正稿;檔案不動
② 組 promptbuild_prompt否前 4 萬字+詞彙表+rule_id 目錄
③ Gemini_gemini_complete是,每日 100 次LLM 原始文字
④ 解析parse_llm_response否卡片清單(可能為空)
⑤ 引文驗證match_quote否quote_verified、quote_source(逐字稿原文)、推導的 timestamp、frame
⑥ 護欄filter_cards否rule_cards/<vid>.json,manifest 標 extracted

注意事項 · 踩過的坑

🔁加校正詞要有命中證據:有測試釘住 TERM_TABLE 每條的命中數必須 ≥1。憑印象加詞會把講者的正確口語改成你以為的術語。新錯字通常是重萃後在卡片 quote 裡發現的,補進表後要 --force 重萃受影響的那支,或比照 cea44b2 就地校正卡片文字。加短鍵前先數它的前一個字:「全正」「頭信」都是一般中文會出現的組合,要用 TERM_GUARDS 排除,並逐條看替換前後文。
🧭餵入與定位必須同空間:改任何一邊(例如只餵校正稿、卻對原稿定位)都會讓含校正詞的引文被當幻覺丟掉,而且不會報錯,只會在「quote 相似度過低丟棄」的計數裡多幾張。
🔑金鑰只認兩個地方:環境變數或專案根 .env。寫在別處會直到真的呼叫 LLM 才爆認證錯誤。groq 那把 GROQ_API_KEY 同一套解析。
⛔額度用盡不是那一集的錯:except DailyQuotaExceeded 要排在 except Exception 之前並 break,不可呼叫 _save_failed。這是 CLAUDE.md 上寫的硬規則。
📄教材線不走校正:教材沒有逐字稿,萃取走視覺模型看頁面圖,引文一律標未經機器驗證。課程線走同一套校正但多三個陷阱(fulltext 課程不得用 page_map、重疊尾段先截、examples 也要驗)。
🈶繁簡轉換是另一層:zh_convert 只在簡報線與手機隨身讀轉引文顯示,不進萃取,與 term_fix 無關。