iT邦幫忙

2025 iThome 鐵人賽

DAY 24
0
生成式 AI

打造基於 MCP 協議與 n8n 工作流的會議處理 Agent系列 第 24

Day 24 效能與精度雙升級 — 導入優先級時間解析與 Faster-Whisper

  • 分享至 

  • xImage
  •  

昨天我們成功建立了多輪澄清對話機制與脈絡記憶強化,讓 AI 能透過連續追問來精準理解使用者意圖。

今天的目標是升級中文時間表達式解析能力,讓它能夠精準處理各種複雜的相對週別表達,並且將轉錄引擎從 OpenAI Whisper 升級到 Faster-Whisper ,可望大幅提升音訊轉錄的處理速度與效率。

今天的目標與挑戰

  • 強化完整的中文時間表達式解析能力,支援相對週別、工作日、月份等複雜表達
  • 設計優先級解析架構,確保時間表達式能按正確邏輯順序匹配
  • 從 OpenAI Whisper 升級到 Faster-Whisper,提升轉錄效能
  • 確保升級後系統的穩定性與準確度

Step 1:分析中文時間表達式的複雜性

在開始重構前,我先來整理常見的時間表達式。

1-1 相對週別表達的挑戰

在實際的會議逐字稿中,會有許多相對週別的時間描述

  • 「這週五」:指的是本週的週五,需要根據今天是週幾來計算
  • 「下週三」:指的是下一個週一開始的那週的週三
  • 「上週一」:指的是上一個週一開始的那週的週一

這些表達看似簡單,但在程式邏輯上需要考慮很多邊界情況,例如

  • 如果今天是週日,「這週五」是指過去的週五還是未來的週五?
  • 「下週」的定義是從哪一天開始計算?
  • 如何處理「週」和「周」的同義詞?

1-2 工作日與期限表達

除了相對週別,還有其他常見的時間表達:

  • 工作日表達:「三個工作日後」、「五個工作天內」(需排除週末)
  • 月份表達:「月底前」、「月初」、「月末」
  • 數字天數:「三天後」、「兩天內」、「一週後」
  • 模糊時間:「最近」、「不久」、「儘快」

Step 2:設計優先級解析架構

現在要建立一個強大的時間解析系統,關鍵就在於解析順序

2-1 為什麼需要優先級?

在舊版的 calculateDeadlineFromExpression 函式中,我使用的是單一層級的條件判斷,這會導致一個嚴重問題:當時間表達式包含多個關鍵字時,可能會被錯誤配對。

例如:「下週三天後」這個表達式,如果先匹配「三天後」的規則,就會忽略「下週」這個重要資訊。

2-2 設計六層優先級系統

我在新版程式碼中設計了一個六層優先級解析系統

// 【第一優先級】相對週別表達
// 「這週X」、「本週X」、「上週X」、「下週X」

// 【第二優先級】工作日表達
// 「X個工作日」、「X個工作天」

// 【第三優先級】數字天數表達
// 「X天後」、「X天內」

// 【第四優先級】相對日期表達
// 「今天」、「明天」、「後天」、「昨天」、「前天」

// 【第五優先級】週數表達
// 「X週後」、「X周後」

// 【第六優先級】月份相對表達
// 「月底」、「月初」、「月末」

優先級設計理念

  • 最具體的放前面:相對週別(如「這週五」)比單純的週數(如「一週後」)更具體,應該優先配對
  • 避免誤判:「三天後」的規則如果放在前面,會導致「下週三天後」被錯誤解析
  • 從特殊到一般:工作日需要特殊計算邏輯,因此優先於一般的天數表達

Step 3:建立相對週別計算函式群

現在要設計核心的週別計算邏輯。

3-1 「這週X」的計算邏輯

我新增了 calculateThisWeek 函式

function calculateThisWeek(dayChar, refDate) {
  const dayMap = {'一':1,'二':2,'三':3,'四':4,'五':5,'六':6,'日':0,'天':0};
  const targetDay = dayMap[dayChar];
  const currentDay = refDate.getDay();
  
  let daysToAdd = targetDay - currentDay;
  
  // 週日特殊處理:「這週五」應往回推到本週已過去的週五
  if (currentDay === 0 && daysToAdd > 0) {
    daysToAdd -= 7;
  }
  // 如果目標日期已過,保持負值往回推
  else if (daysToAdd < 0 && currentDay !== 0) {
    // 保持負值
  }
  
  return addDays(refDate, daysToAdd);
}

程式說明

  • dayMap 對應表:將中文星期對應到 JavaScript 的 getDay() 數字(週日=0,週一=1...週六=6)
  • 週日特殊處理:如果今天是週日,「這週五」指的是剛過去的週五,所以要減 7 天
  • 負值邏輯:如果目標日期已過,保持負值讓它往回推

3-2 「下週X」的計算邏輯

「下週」的定義是下個週一開始的那一週,這個邏輯在 calculateNextWeek 函式中實作

function calculateNextWeek(dayChar, refDate) {
  const dayMap = {'一':1,'二':2,'三':3,'四':4,'五':5,'六':6,'日':0,'天':0};
  const targetDay = dayMap[dayChar];
  const currentWeekday = refDate.getDay();
  
  // 計算到下週一的天數
  let daysToNextMonday;
  if (currentWeekday === 0) {
    // 週日 -> 下週一是明天
    daysToNextMonday = 1;
  } else if (currentWeekday === 6) {
    // 週六 -> 下週一是後天
    daysToNextMonday = 2;
  } else {
    // 週一到週五 -> 計算剩餘天數+週末2天+1
    daysToNextMonday = (7 - currentWeekday) + 1;
  }
  
  // 找到下週一的日期
  const nextMonday = addDays(refDate, daysToNextMonday);
  
  // 從下週一計算到目標星期幾的偏移量
  let daysFromMonday;
  if (targetDay === 0) {
    // 目標是週日,偏移量是6天
    daysFromMonday = 6;
  } else {
    // 目標是週一到週六,偏移量是 targetDay - 1
    daysFromMonday = targetDay - 1;
  }
  
  return addDays(nextMonday, daysFromMonday);
}

程式說明

這個函式採用兩階段計算

  • 第一階段:先計算到「下週一」的天數,分別處理週日、週六、平日三種情況
  • 第二階段:從下週一往後推到目標星期幾

這樣的設計確保了「下週」的定義是一致的,無論今天是週幾。

3-3 「上週X」的計算邏輯

calculateLastWeek 函式則是先回推一週,再計算到目標星期幾:

function calculateLastWeek(dayChar, refDate) {
  const dayMap = {'一':1,'二':2,'三':3,'四':4,'五':5,'六':6,'日':0,'天':0};
  const targetDay = dayMap[dayChar];
  
  // 先回到上週的同一天
  const result = addWeeks(refDate, -1);
  const currentDay = result.getDay();
  let daysToAdd = targetDay - currentDay;
  
  return addDays(result, daysToAdd);
}

Step 4:實作工作日計算邏輯

工作日計算需要排除週末,這在 calculateWorkdays 函式中實作

function calculateWorkdays(numStr, refDate) {
  let workdays = parseChineseNumber(numStr);
  
  let result = new Date(refDate);
  let count = 0;
  
  while (count < workdays) {
    result.setDate(result.getDate() + 1);
    // 排除週末(週六=6,週日=0)
    if (result.getDay() !== 0 && result.getDay() !== 6) {
      count++;
    }
  }
  
  return result;
}

程式說明

  • 逐日檢查:每次往後推一天,檢查是否為工作日
  • 排除週末:只有週一到週五(getDay() 返回 1~5)才計入工作日
  • 中文數字支援:透過 parseChineseNumber 函式支援「三個工作日」這種表達

Step 5:整合完整的優先級解析系統

現在將所有邏輯整合到 calculateDeadlineFromExpression 函式中。

5-1 新舊版本對比

在舊版中,時間解析只有簡單的條件判斷

// 舊版:單一層級判斷
if (expr.includes('今天') || expr.includes('今日')) {
  return todayStr;
}
if (expr.includes('明天') || expr.includes('明日')) {
  const result = new Date(today);
  result.setDate(result.getDate() + 1);
  return result.toISOString().split('T')[^0];
}
// ... 其他條件

新版採用優先級匹配系統

// 新版:六層優先級系統
// 【第一優先級】相對週別表達
const thisWeekMatch = expr.match(/(這|本)(週|禮拜|星期)([一二三四五六日天])/);
if (thisWeekMatch) {
  const result = calculateThisWeek(thisWeekMatch[^3], referenceDate);
  console.log(`[時間解析] 「這週X」規則: ${timeExpression} -> ${result.toISOString().split('T')[^0]}`);
  return result.toISOString().split('T')[^0];
}

const lastWeekMatch = expr.match(/(上|前)(週|禮拜|星期)([一二三四五六日天])/);
if (lastWeekMatch) {
  const result = calculateLastWeek(lastWeekMatch[^3], referenceDate);
  console.log(`[時間解析] 「上週X」規則: ${timeExpression} -> ${result.toISOString().split('T')[^0]}`);
  return result.toISOString().split('T')[^0];
}

const nextWeekMatch = expr.match(/(下|次)(週|禮拜|星期)([一二三四五六日天])/);
if (nextWeekMatch) {
  const result = calculateNextWeek(nextWeekMatch[^3], referenceDate);
  console.log(`[時間解析] 「下週X」規則: ${timeExpression} -> ${result.toISOString().split('T')[^0]}`);
  return result.toISOString().split('T')[^0];
}

// 【第二優先級】工作日表達
const workdayMatch = expr.match(/([一二三四五六七八九十\d]+)個?工作[日天]/);
if (workdayMatch) {
  const result = calculateWorkdays(workdayMatch[^1], referenceDate);
  console.log(`[時間解析] 「工作日」規則: ${timeExpression} -> ${result.toISOString().split('T')[^0]}`);
  return result.toISOString().split('T')[^0];
}
// ... 其他優先級

5-2 核心改進點

改進項目 舊版做法 新版做法 優勢
匹配方式 使用 includes() 簡單字串包含 使用 match() 正則表達式精確匹配 避免誤判,可提取關鍵資訊
解析順序 無明確順序,依照條件順序判斷 六層優先級系統,從特殊到一般 確保複雜表達式被正確解析
週別處理 僅支援「下週」,無法處理具體星期 完整支援「這週X」「下週X」「上週X」 涵蓋所有常見週別表達
工作日 不支援 完整支援,自動排除週末 符合實際業務需求

Step 6:升級為 Faster Whisper

從 OpenAI Whisper 升級到 Faster Whispe 有什麼好處?為什麼需要升級?

6-1 效能瓶頸分析

在使用中 OpenAI Whisper 有以下幾個問題

  • 轉錄速度慢:處理一段 1 分鐘到 1 分半的會議錄音,OpenAI Whisper 需要約 3~4 分鐘
  • 記憶體佔用高:載入 medium 模型時,RAM 佔用了 4~5 GB
  • 等待時間長:使用者上傳音訊後,需要長時間等待才能看到轉錄結果

這些問題在處理長時間會議或多個會議時,會嚴重影響系統的可用性。

6-2 Faster Whisper 的技術優勢

Faster Whisper 是基於 CTranslate2 重新實作的 Whisper 模型,它帶來了以下幾項核心優勢

  1. 速度提升:透過 CTranslate2 推理引擎的最佳化技術,在相同精度下可顯著提升推理速度
    • 根據社群測試回報,在 GPU 環境下速度提升可達原版的數倍,大幅縮短轉錄等待時間
    • CPU 環境下的效能改善同樣明顯,適合沒有獨立顯卡的使用者
    • CTranslate2 採用權重量化、層融合、批次重排等技術最佳化 Transformer 網路結構,實現高效能運算
  2. 記憶體最佳化:透過多項最佳化技術可大幅降低記憶體佔用,讓資源受限的環境也能順暢運作
    • 根據文獻顯示,使用 Float16 精度時,GPU 與 CPU 記憶體佔用都有顯著下降
    • 透過量化技術可進一步減少記憶體需求,例如 int8 量化能在保持準確度的前提下大幅降低資源消耗
    • CTranslate2 實作了動態記憶體管理機制,針對大規模部署場景進行最佳化
  3. 準確度保持:使用與 OpenAI Whisper 相同的模型權重,確保轉錄品質不打折
    • 在不犧牲準確度的前提下提升速度與效率
    • 繁體中文識別準確度與原版相當,經測試維持在 95% 以上
    • 透過調整權重補償參數表現力減少,確保預測準確度不會下降
  4. 量化支援:提供多種量化格式選擇,可根據硬體環境彈性調整
    • Float16:平衡速度與準確度,適合 GPU 環境使用
    • int8:進一步降低記憶體佔用與運算負擔,適合 CPU 或資源受限環境
    • int8_float16:混合精度量化,提供最佳效能表現

Step 7:實作 Faster Whisper 升級

現在來修改 whisper_service.py,將轉錄引擎升級為 Faster Whisper。

7-1 安裝 Faster Whisper

首先先安裝 Faster Whisper 套件

pip install faster-whisper

7-2 修改 WhisperService 類別

打開 whisper_service.py,將原本的 OpenAI Whisper 替換為 Faster-Whisper

from faster_whisper import WhisperModel
import time
import os

class WhisperService:
    # 載入 Faster-Whisper 模型,只載入一次
    def __init__(self, model_name="medium", device="cpu", compute_type="int8"):
        print(f"[WhisperService] 載入 Faster-Whisper 模型 {model_name} 中...")
        
        # 根據裝置選擇最佳的 compute_type
        if device == "cuda":
            compute_type = "float16"  # GPU 使用 float16 效能最佳
        else:
            compute_type = "int8"  # CPU 使用 int8 降低記憶體佔用
        
        self.model = WhisperModel(
            model_name,
            device=device,
            compute_type=compute_type
        )
        
        print(f"[WhisperService] Faster-Whisper 模型 {model_name} 載入完成")
        print(f"[WhisperService] 裝置: {device}, 精度: {compute_type}")
    
    # 轉錄單一音訊,回傳結果字典
    def transcribe(self, audio_path: str, language: str = "zh") -> dict:
        if not os.path.isfile(audio_path):
            return {"error": f"找不到檔案:{audio_path}"}
        
        print(f"[WhisperService] 開始轉錄:{audio_path}")
        start = time.time()
        
        try:
            # Faster-Whisper 的 transcribe 回傳 (segments, info)
            segments, info = self.model.transcribe(
                audio_path,
                language=language,
                beam_size=5,  # 使用 beam search 提升準確度
                initial_prompt="以下是繁體中文的句子"  # 提示模型使用繁體中文
            )
            
            # 將 segments 轉換為列表(Faster-Whisper 回傳的是生成器)
            segments_list = list(segments)
            
            # 組合完整文字
            full_text = " ".join([segment.text for segment in segments_list])
            
            # 計算音訊總長度(從 info 獲取)
            duration = info.duration
            
            elapsed = time.time() - start
            
            # 成功時回傳
            return {
                "audio_path": audio_path,
                "text": full_text,
                "segments": [
                    {
                        "start": seg.start,
                        "end": seg.end,
                        "text": seg.text
                    }
                    for seg in segments_list
                ],
                "duration": duration,
                "transcribe_time": elapsed,
                "language": info.language,
                "language_probability": info.language_probability,
                "error": None
            }
        
        except Exception as e:
            # 失敗時回傳,但 error 不為 None
            return {
                "audio_path": audio_path,
                "text": "",
                "segments": [],
                "duration": 0,
                "transcribe_time": 0,
                "error": str(e)
            }

7-3 新舊版本對比

項目 OpenAI Whisper Faster-Whisper
匯入模組 import whisper from faster_whisper import WhisperModel
模型載入 whisper.load_model(model_name) WhisperModel(model_name, device, compute_type)
轉錄方法 model.transcribe(audio, language) model.transcribe(audio, language, beam_size, initial_prompt)
回傳格式 直接回傳字典 回傳 (segments, info) 元組,需轉換
segments 型態 列表 生成器,需用 list() 轉換
額外參數 initial_prompt 可引導模型使用繁體中文

7-4 改進說明

動態選擇 compute_type

我在初始化時根據裝置自動選擇最佳精度:

if device == "cuda":
    compute_type = "float16"  # GPU 使用 float16
else:
    compute_type = "int8"  # CPU 使用 int8

這樣可以在不同環境下都獲得最佳效能。

initial_prompt 引導繁體中文

initial_prompt="以下是繁體中文的句子"

這個提示詞可以引導模型優先使用繁體中文輸出,減少簡繁混合的問題。

生成器轉列表

Faster-Whisper 的 segments 是生成器,需要轉換為列表才能重複使用:

segments_list = list(segments)

Step 8:測試與驗證

現在來測試升級後的系統。

8-1 測試時間解析功能

我準備了一些測試案例來驗證時間解析系統,有

  • 這週五前完成
  • 下週三交付
  • 上週一討論過
  • 三個工作日後
  • 兩週後
  • 月底前

測試結果

測試案例都正確通過,系統能夠準確解析複雜的中文時間表達式。

8-2 測試 Faster-Whisper 轉錄效能

我使用一段 1 分半的音訊檔進行測試

測試環境

  • CPU:Intel i7-8565U
  • RAM:20 GB
  • GPU:NVIDIA MX130 2GB

轉錄結果

因為都是用 CPU 而不是 GPU ,因此轉錄的效率差異不大,但是記憶體的使用量有大幅的減少許多且轉綠的品質也有維持在一定的水準上。


Step 9:程式碼文件化與最佳實踐

在新版的程式碼中,我也加入了詳細的 JSDoc 註解。

9-1 函式文件化範例

/**
 * 將自然語言時間表達式轉換為 ISO 格式日期
 * 
 * 支援繁體中文常見的時間表達方式,包括相對日期、週別表達、工作日等。
 * 使用優先級系統解析表達式,按照特定順序匹配規則。
 * 
 * 支援的表達式類型:
 * - 相對週別:「這週五」、「下週三」、「上週一」
 * - 工作日:「三個工作日」、「五個工作天」
 * - 天數:「三天後」、「兩天內」
 * - 相對日期:「今天」、「明天」、「後天」、「昨天」、「前天」
 * - 週數:「兩週後」、「一周後」
 * - 月份:「月底」、「月初」、「月末」
 * 
 * @function calculateDeadlineFromExpression
 * @param {string} timeExpression - 時間表達式,如「下週五」、「三天後」
 * @param {Date} [referenceDate=new Date()] - 基準日期,預設為當天
 * @returns {string} ISO 格式日期字串 (YYYY-MM-DD)
 */
function calculateDeadlineFromExpression(timeExpression, referenceDate = new Date()) {
  // ... 程式內容
}

9-2 文件化的價值

  • 可維護性:未來修改時能快速理解函式用途
  • 團隊協作:若有其他開發者可以輕鬆理解程式碼邏輯
  • 除錯效率:清楚的文件能加速問題定位

今天的成果總結

完成項目

  • 建立了比較完整的中文時間表達式解析系統,支援相對週別、工作日、月份等複雜表達
  • 設計了六層優先級解析架構,確保時間表達式能按正確邏輯順序匹配
  • 成功從 OpenAI Whisper 升級到 Faster-Whisper,最佳化了記憶體佔用
  • 加入完整的 JSDoc 文件註解,提升程式碼可維護性
  • 保持轉錄準確度,繁體中文辨識率維持在 95% 以上

心得

今天我繼續讓我的 AI 助理變得更聰明也更有效率,透過建立一套強大的「時間解析引擎」,它現在能精準掌握模糊的時間指令,同時也從 OpenAI Whisper 升級至 Faster-Whisper 。

雖然 Faster-Whisper 能提升轉錄速度,但在我的筆電上因硬體限制而效果不明顯,不過,它帶來的好處是記憶體用量大幅降低,有效減輕了筆電的負擔,讓資源有限的裝置也能順暢執行,這讓我的整體效率提升了不少。

🎯 明天計劃

新增會議摘要與任務清單的下載功能,讓處理完後的結果能輕鬆匯出。


上一篇
Day 23 智慧對話升級 — 多輪澄清與脈絡記憶強化
下一篇
Day 25 實用性升級 — 新增會議記錄與任務清單下載功能
系列文
打造基於 MCP 協議與 n8n 工作流的會議處理 Agent28
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言