昨天我們成功建立了多輪澄清對話機制與脈絡記憶強化,讓 AI 能透過連續追問來精準理解使用者意圖。
今天的目標是升級中文時間表達式解析能力,讓它能夠精準處理各種複雜的相對週別表達,並且將轉錄引擎從 OpenAI Whisper 升級到 Faster-Whisper ,可望大幅提升音訊轉錄的處理速度與效率。
在開始重構前,我先來整理常見的時間表達式。
在實際的會議逐字稿中,會有許多相對週別的時間描述
這些表達看似簡單,但在程式邏輯上需要考慮很多邊界情況,例如
除了相對週別,還有其他常見的時間表達:
現在要建立一個強大的時間解析系統,關鍵就在於解析順序。
在舊版的 calculateDeadlineFromExpression
函式中,我使用的是單一層級的條件判斷,這會導致一個嚴重問題:當時間表達式包含多個關鍵字時,可能會被錯誤配對。
例如:「下週三天後」這個表達式,如果先匹配「三天後」的規則,就會忽略「下週」這個重要資訊。
我在新版程式碼中設計了一個六層優先級解析系統
// 【第一優先級】相對週別表達
// 「這週X」、「本週X」、「上週X」、「下週X」
// 【第二優先級】工作日表達
// 「X個工作日」、「X個工作天」
// 【第三優先級】數字天數表達
// 「X天後」、「X天內」
// 【第四優先級】相對日期表達
// 「今天」、「明天」、「後天」、「昨天」、「前天」
// 【第五優先級】週數表達
// 「X週後」、「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);
}
getDay()
數字(週日=0,週一=1...週六=6)「下週」的定義是下個週一開始的那一週,這個邏輯在 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);
}
這個函式採用兩階段計算
這樣的設計確保了「下週」的定義是一致的,無論今天是週幾。
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);
}
工作日計算需要排除週末,這在 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
函式支援「三個工作日」這種表達現在將所有邏輯整合到 calculateDeadlineFromExpression
函式中。
在舊版中,時間解析只有簡單的條件判斷
// 舊版:單一層級判斷
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];
}
// ... 其他優先級
改進項目 | 舊版做法 | 新版做法 | 優勢 |
---|---|---|---|
匹配方式 | 使用 includes() 簡單字串包含 |
使用 match() 正則表達式精確匹配 |
避免誤判,可提取關鍵資訊 |
解析順序 | 無明確順序,依照條件順序判斷 | 六層優先級系統,從特殊到一般 | 確保複雜表達式被正確解析 |
週別處理 | 僅支援「下週」,無法處理具體星期 | 完整支援「這週X」「下週X」「上週X」 | 涵蓋所有常見週別表達 |
工作日 | 不支援 | 完整支援,自動排除週末 | 符合實際業務需求 |
從 OpenAI Whisper 升級到 Faster Whispe 有什麼好處?為什麼需要升級?
在使用中 OpenAI Whisper 有以下幾個問題
medium
模型時,RAM 佔用了 4~5 GB這些問題在處理長時間會議或多個會議時,會嚴重影響系統的可用性。
Faster Whisper 是基於 CTranslate2 重新實作的 Whisper 模型,它帶來了以下幾項核心優勢
現在來修改 whisper_service.py
,將轉錄引擎升級為 Faster Whisper。
首先先安裝 Faster Whisper 套件
pip install faster-whisper
打開 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)
}
項目 | 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 可引導模型使用繁體中文 |
我在初始化時根據裝置自動選擇最佳精度:
if device == "cuda":
compute_type = "float16" # GPU 使用 float16
else:
compute_type = "int8" # CPU 使用 int8
這樣可以在不同環境下都獲得最佳效能。
initial_prompt="以下是繁體中文的句子"
這個提示詞可以引導模型優先使用繁體中文輸出,減少簡繁混合的問題。
Faster-Whisper 的 segments
是生成器,需要轉換為列表才能重複使用:
segments_list = list(segments)
現在來測試升級後的系統。
我準備了一些測試案例來驗證時間解析系統,有
測試案例都正確通過,系統能夠準確解析複雜的中文時間表達式。
我使用一段 1 分半的音訊檔進行測試
因為都是用 CPU 而不是 GPU ,因此轉錄的效率差異不大,但是記憶體的使用量有大幅的減少許多且轉綠的品質也有維持在一定的水準上。
在新版的程式碼中,我也加入了詳細的 JSDoc 註解。
/**
* 將自然語言時間表達式轉換為 ISO 格式日期
*
* 支援繁體中文常見的時間表達方式,包括相對日期、週別表達、工作日等。
* 使用優先級系統解析表達式,按照特定順序匹配規則。
*
* 支援的表達式類型:
* - 相對週別:「這週五」、「下週三」、「上週一」
* - 工作日:「三個工作日」、「五個工作天」
* - 天數:「三天後」、「兩天內」
* - 相對日期:「今天」、「明天」、「後天」、「昨天」、「前天」
* - 週數:「兩週後」、「一周後」
* - 月份:「月底」、「月初」、「月末」
*
* @function calculateDeadlineFromExpression
* @param {string} timeExpression - 時間表達式,如「下週五」、「三天後」
* @param {Date} [referenceDate=new Date()] - 基準日期,預設為當天
* @returns {string} ISO 格式日期字串 (YYYY-MM-DD)
*/
function calculateDeadlineFromExpression(timeExpression, referenceDate = new Date()) {
// ... 程式內容
}
✅ 完成項目
今天我繼續讓我的 AI 助理變得更聰明也更有效率,透過建立一套強大的「時間解析引擎」,它現在能精準掌握模糊的時間指令,同時也從 OpenAI Whisper 升級至 Faster-Whisper 。
雖然 Faster-Whisper 能提升轉錄速度,但在我的筆電上因硬體限制而效果不明顯,不過,它帶來的好處是記憶體用量大幅降低,有效減輕了筆電的負擔,讓資源有限的裝置也能順暢執行,這讓我的整體效率提升了不少。
🎯 明天計劃
新增會議摘要與任務清單的下載功能,讓處理完後的結果能輕鬆匯出。