本專案旨在開發一套智能化台語朗讀自動評分系統,透過結合先進的語音辨識技術與台語語言學專業知識,為台語學習者提供即時、客觀、精準的發音評估工具,降低台語學習門檻,促進本土語言傳承。
顯示層:台語漢字(使用者友善)
評分層:台羅拼音(精確比對)
| 評分維度 | 權重 | 說明 |
|---|---|---|
| 整體準確度 | 70% | 完整語句的相似度 |
| 字元準確度 | 20% | 逐字元比對正確率 |
| 聲調準確度 | 10% | 聲調符號匹配度 |
整合深度學習語音辨識技術與台語音韻學專業知識,確保評分的技術性與專業性。
將前沿 AI 技術落地於本土語言教育,縮小城鄉教育資源差距。
透過數位化方式記錄與推廣台語,為語言保存貢獻科技力量。
收集(匿名化)學習數據,分析台語學習痛點,優化教學策略。
台語(閩南語)作為台灣重要的本土語言,亟需有效的學習與評量工具。隨著108課綱將本土語言列為必修,台語教學需求大增,但師資與教材仍顯不足。本專案結合 AI 語音辨識技術,開發自動化評分系統,解決台語學習的痛點。
✅ 文稿生成(台羅+漢字雙語顯示)
✅ 錄音功能(支援瀏覽器與桌面)
✅ 語音辨識(Whisper 台語模型)
✅ 基礎評分機制(編輯距離演算法)
✅ Web 介面(Gradio/Streamlit)
❌ 詳細發音口型指導
❌ 多人競賽/排行榜模式
❌ 原生行動裝置 APP
❌ 即時語音對話練習
❌ 商業化付費功能
| 指標 | 目標值 |
|---|---|
| 語音辨識準確率 | ≥ 85% |
| 評分與專家相關係數 | ≥ 0.80 |
| 系統回應時間 | ≤ 5 秒 |
| 使用者滿意度 (SUS) | ≥ 70 分 |
| 文稿庫規模 | ≥ 100 篇 |
┌──────────────────────────────────────────────────────┐
│ 前端介面層 (Presentation Layer) │
├──────────────────────────────────────────────────────┤
│ 文稿顯示區 │ 錄音控制面板 │ 評分結果展示 │
│ ┌────────┐ │ ┌──────────┐ │ ┌──────────────┐ │
│ │漢字文本│ │ │🎤 錄音鈕 │ │ │分數: 87 分 │ │
│ │台羅註解│ │ │⏸ 停止 │ │ │等級: 良好 │ │
│ └────────┘ │ │🔊 播放 │ │ │建議事項... │ │
│ │ └──────────┘ │ └──────────────┘ │
└──────────────┬───────────────────────────────────────┘
│ HTTP/WebSocket
┌──────────────▼───────────────────────────────────────┐
│ 應用服務層 (Application Layer) │
├──────────────────────────────────────────────────────┤
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │文稿管理服務 │ │錄音處理服務 │ │評分計算服務 │ │
│ │- 文稿選取 │ │- 音訊驗證 │ │- 語音辨識 │ │
│ │- 難度篩選 │ │- 格式轉換 │ │- 文字比對 │ │
│ │- 隨機生成 │ │- 降噪處理 │ │- 分數計算 │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└──────────────┬───────────────────────────────────────┘
│
┌──────────────▼───────────────────────────────────────┐
│ 核心引擎層 (Core Engine Layer) │
├──────────────────────────────────────────────────────┤
│ ┌──────────────────┐ ┌──────────────────────────┐ │
│ │ Whisper 台語模型 │ │ 文字處理引擎 │ │
│ │ (NUTN-KWS v0.5) │ │ - 正規化 │ │
│ │ - 語音轉文字 │ │ - Levenshtein 距離 │ │
│ │ - 音訊特徵提取 │ │ - 台羅/漢字轉換(備用) │ │
│ └──────────────────┘ └──────────────────────────┘ │
└──────────────┬───────────────────────────────────────┘
│
┌──────────────▼───────────────────────────────────────┐
│ 資料持久層 (Data Persistence Layer) │
├──────────────────────────────────────────────────────┤
│ ┌────────────┐ ┌────────────┐ ┌────────────────┐│
│ │文稿資料庫 │ │音檔儲存 │ │學習記錄 DB ││
│ │(JSON/SQLite│ │(/tmp/*.wav)│ │(使用者歷程) ││
│ └────────────┘ └────────────┘ └────────────────┘│
└──────────────────────────────────────────────────────┘
[開始] 使用者進入系統
↓
(1) 選擇難度 (easy/medium/hard)
↓
(2) 系統隨機選取文稿
↓
(3) 顯示漢字文本給使用者
│
│ (內部保存台羅標準答案)
↓
(4) 使用者閱讀文本
↓
(5) 點擊「開始錄音」按鈕
↓
(6) 麥克風擷取音訊 (16kHz, Mono)
↓
(7) 使用者點擊「停止」或達到時間上限
↓
(8) 音訊儲存為 WAV 檔案
↓
(9) [驗證] 檢查音訊品質
│ ├─ 音量太小 → 提示重錄
│ └─ 品質良好 → 繼續
↓
(10) 送入 Whisper 台語模型
↓
(11) 模型輸出辨識文字
│ ├─ 情境A: 台羅拼音 → 直接進入評分
│ └─ 情境B: 漢字 → 轉換為台羅 → 進入評分
↓
(12) 文字正規化處理
(移除多餘空白、統一大小寫)
↓
(13) 計算編輯距離 (Levenshtein Distance)
↓
(14) 計算各項評分指標
├─ 整體準確度 (70%)
├─ 字元準確度 (20%)
└─ 聲調準確度 (10%)
↓
(15) 加權計算最終分數
↓
(16) 生成評分報告
├─ 分數與等級
├─ 標準答案 vs 辨識結果
├─ 錯誤位置標示
└─ 改進建議
↓
(17) 顯示結果給使用者
↓
(18) [選項] 重新練習 / 換下一題 / 查看歷史
↓
[結束]
main.py
├─ imports ─→ text_manager.py
│ └─ 依賴 ─→ data/texts.json
│
├─ imports ─→ recorder.py
│ ├─ 依賴 ─→ sounddevice
│ └─ 依賴 ─→ scipy
│
├─ imports ─→ whisper_model.py
│ ├─ 依賴 ─→ transformers
│ ├─ 依賴 ─→ torch
│ └─ 載入 ─→ NUTN-KWS/Whisper-Taiwanese-model-v0.5
│
└─ imports ─→ scorer.py
├─ 依賴 ─→ Levenshtein
└─ 呼叫 ─→ whisper_model.py
| 技術 | 版本 | 用途 | 選擇理由 |
|---|---|---|---|
| Python | 3.9+ | 主要開發語言 | ML/AI 生態系完整,函式庫豐富 |
| Gradio | 4.x | Web 介面框架 | 快速建立 ML 應用介面,內建錄音元件 |
| Streamlit | 1.x | 替代方案 | 更靈活的客製化,適合複雜介面 |
| 技術 | 版本 | 用途 | 選擇理由 |
|---|---|---|---|
| Whisper (台語版) | v0.5 | 語音辨識 | NUTN-KWS 專為台語微調 |
| Transformers | 4.30+ | 模型載入 | Hugging Face 標準介面 |
| PyTorch | 2.0+ | 深度學習框架 | Whisper 依賴的後端 |
| 技術 | 版本 | 用途 | 選擇理由 |
|---|---|---|---|
| sounddevice | 0.4+ | 錄音功能 | 跨平台音訊 I/O |
| scipy | 1.10+ | 音訊檔案處理 | WAV 格式讀寫 |
| librosa | 0.10+ | (可選) 音訊分析 | 特徵提取、降噪 |
| 技術 | 版本 | 用途 | 選擇理由 |
|---|---|---|---|
| python-Levenshtein | 0.21+ | 編輯距離計算 | 高效能 C 實作 |
| 台灣言語工具 | latest | (備用) 漢字轉台羅 | 本土開發的台語工具 |
| 技術 | 版本 | 用途 | 選擇理由 |
|---|---|---|---|
| JSON | - | 文稿資料庫 | 輕量、易編輯、版本控制友善 |
| SQLite | 3.x | 學習記錄 | 本地資料庫,無需額外服務 |
# 套件管理
pip / conda
# 版本控制
Git + GitHub
# 開發環境
VS Code / PyCharm
# 虛擬環境
venv / conda env
# 程式碼品質
pylint / black / mypy
# 測試框架
pytest
# 文件生成
Sphinx / MkDocs
模型名稱: NUTN-KWS/Whisper-Taiwanese-model-v0.5
基底架構: OpenAI Whisper (Transformer Encoder-Decoder)
微調來源: 台語語音資料集
參數量: ~244M (medium 版本)
輸入規格:
- 格式: WAV, MP3, FLAC
- 採樣率: 16000 Hz (建議)
- 聲道: 單聲道 (Mono)
- 長度: 最長 30 秒 / 段
輸出格式:
- 待確認: 台羅拼音 或 漢字
- 編碼: UTF-8
- 結構: 純文字字串
推理效能:
- CPU: ~5-10 秒 / 30秒音檔
- GPU (CUDA): ~2-3 秒 / 30秒音檔
- 記憶體: ~2GB (模型載入)
準確率 (WER):
- 乾淨語音: ~10-15%
- 一般環境: ~20-30%
- 嘈雜環境: ~40-50%
作業系統:
- Windows 10/11
- macOS 11+ (Big Sur)
- Ubuntu 20.04+ / Debian 11+
硬體需求:
- CPU: Intel i5 / AMD Ryzen 5 以上
- RAM: 16GB 建議 (最低 8GB)
- 硬碟: 20GB 可用空間
- GPU: NVIDIA GTX 1060 以上 (可選,用於加速)
- 麥克風: 內建或外接皆可
網路:
- 初次下載模型需要網路 (~1GB)
- 執行時可離線運作
伺服器規格 (若部署至雲端):
- vCPU: 4 核心
- RAM: 16GB
- 儲存: 50GB SSD
- 頻寬: 10Mbps+
推薦平台:
- Heroku
- Google Cloud Run
- AWS EC2
- Render
from dataclasses import dataclass
from datetime import datetime
from typing import Optional
@dataclass
class TaiwaneseText:
"""台語文稿資料結構"""
id: str # UUID 唯一識別碼
hanzi: str # 漢字文本(顯示用)
tailo: str # 台羅拼音(標準答案)
difficulty: str # 難度: "easy" / "medium" / "hard"
category: str # 分類: "daily" / "greeting" / "literature" 等
word_count: int # 字數統計
audio_reference: Optional[str] = None # 參考音檔路徑(可選)
tags: list[str] = None # 標籤 ["購物", "日常"]
created_at: datetime = None # 建立時間
author: str = "system" # 來源作者
def __post_init__(self):
if self.created_at is None:
self.created_at = datetime.now()
if self.tags is None:
self.tags = []
# 範例資料
example_text = TaiwaneseText(
id="550e8400-e29b-41d4-a716-446655440000",
hanzi="你好,食飽未?",
tailo="Lí hó, tsia̍h-pá buē?",
difficulty="easy",
category="greeting",
word_count=7,
tags=["問候", "日常"]
)
{
"texts": [
{
"id": "text_001",
"hanzi": "你好,食飽未?",
"tailo": "Lí hó, tsia̍h-pá buē?",
"difficulty": "easy",
"category": "greeting",
"word_count": 7,
"tags": ["問候", "日常"],
"created_at": "2025-01-01T00:00:00"
},
{
"id": "text_002",
"hanzi": "我的名叫阿明,今年二十歲,佇台北讀冊。",
"tailo": "Guá ê miâ kiò A-bîng, kin-nî jī-tsa̍p huè, tī Tâi-pak tha̍k-chheh.",
"difficulty": "medium",
"category": "self_introduction",
"word_count": 18,
"tags": ["自我介紹", "學習"],
"created_at": "2025-01-01T00:00:00"
},
{
"id": "text_003",
"hanzi": "一枝草,一點露,天無絕人之路。",
"tailo": "Tsi̍t ki tsháu, tsi̍t tiám lōo, thinn bô tsa̍t jîn tsi lo̍o.",
"difficulty": "hard",
"category": "proverb",
"word_count": 14,
"tags": ["俗諺", "哲理"],
"created_at": "2025-01-01T00:00:00"
}
],
"metadata": {
"total_count": 100,
"last_updated": "2025-01-15T12:00:00",
"version": "1.0"
}
}
# text_manager.py
import json
import random
from pathlib import Path
from typing import List, Optional
import logging
class TextManager:
"""台語文稿管理系統"""
def __init__(self, data_dir: str = "data/texts"):
self.data_dir = Path(data_dir)
self.data_dir.mkdir(parents=True, exist_ok=True)
self.texts_file = self.data_dir / "texts.json"
self.texts = self._load_texts()
logging.info(f"✅ 載入 {len(self.texts)} 篇文稿")
def _load_texts(self) -> List[dict]:
"""載入文稿資料"""
if not self.texts_file.exists():
logging.warning("⚠️ 文稿檔案不存在,建立空資料庫")
self._create_default_texts()
try:
with open(self.texts_file, 'r', encoding='utf-8') as f:
data = json.load(f)
return data.get('texts', [])
except Exception as e:
logging.error(f"❌ 載入失敗: {e}")
return []
def _create_default_texts(self):
"""建立預設文稿"""
default_data = {
"texts": [
{
"id": "text_001",
"hanzi": "你好,食飽未?",
"tailo": "Lí hó, tsia̍h-pá buē?",
"difficulty": "easy",
"category": "greeting",
"word_count": 7
},
{
"id": "text_002",
"hanzi": "我欲去學校讀冊。",
"tailo": "Guá beh khì ha̍k-hāu tha̍k-chheh.",
"difficulty": "easy",
"category": "daily",
"word_count": 9
},
{
"id": "text_003",
"hanzi": "今仔日天氣真好。",
"tailo": "Kin-á-ji̍t thinn-khì tsin hó.",
"difficulty": "easy",
"category": "daily",
"word_count": 8
}
],
"metadata": {
"total_count": 3,
"last_updated": "2025-01-01T00:00:00",
"version": "1.0"
}
}
with open(self.texts_file, 'w', encoding='utf-8') as f:
json.dump(default_data, f, ensure_ascii=False, indent=2)
def get_random_text(self,
difficulty: Optional[str] = None,
category: Optional[str] = None) -> Optional[dict]:
"""隨機取得文稿"""
filtered = self.texts
# 依難度篩選
if difficulty:
filtered = [t for t in filtered if t['difficulty'] == difficulty]
# 依分類篩選
if category:
filtered = [t for t in filtered if t['category'] == category]
if not filtered:
logging.warning("⚠️ 沒有符合條件的文稿")
return None
return random.choice(filtered)
def get_text_by_id(self, text_id: str) -> Optional[dict]:
"""依 ID 取得文稿"""
for text in self.texts:
if text['id'] == text_id:
return text
return None
def add_text(self,
hanzi: str,
tailo: str,
difficulty: str,
category: str,
tags: List[str] = None) -> str:
"""新增文稿"""
import uuid
new_text = {
"id": f"text_{str(uuid.uuid4())[:8]}",
"hanzi": hanzi,
"tailo": tailo,
"difficulty": difficulty,
"category": category,
"word_count": len(hanzi),
"tags": tags or [],
"created_at": datetime.now().isoformat()
}
self.texts.append(new_text)
self._save_texts()
logging.info(f"✅ 新增文稿: {new_text['id']}")
return new_text['id']
def _save_texts(self):
"""儲存文稿資料"""
data = {
"texts": self.texts,
"metadata": {
"total_count": len(self.texts),
"last_updated": datetime.now().isoformat(),
"version": "1.0"
}
}
with open(self.texts_file, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
def get_statistics(self) -> dict:
"""取得統計資訊"""
stats = {
"total": len(self.texts),
"by_difficulty": {},
"by_category": {}
}
for text in self.texts:
# 難度統計
diff = text['difficulty']
stats['by_difficulty'][diff] = stats['by_difficulty'].get(diff, 0) + 1
# 分類統計
cat = text['category']
stats['by_category'][cat] = stats['by_category'].get(cat, 0) + 1
return stats
| 難度 | 字數範圍 | 特徵 | 目標數量 |
|---|---|---|---|
| Easy | 5-15 字 | 日常詞彙、簡單句型、無罕見音 | 40 篇 |
| Medium | 15-30 字 | 含變調、複合句、常用成語 | 40 篇 |
| Hard | 30+ 字 | 文學用語、複雜句型、方言詞 | 20 篇 |
分類架構:
├── greeting (問候語) - 15 篇
├── self_introduction (自我介紹) - 15 篇
├── daily (日常對話) - 30 篇
├── shopping (購物) - 10 篇
├── family (家庭) - 10 篇
├── proverb (俗諺語) - 15 篇
└── literature (文學) - 5 篇
AUDIO_SETTINGS = {
'sample_rate': 16000, # Hz (Whisper 建議值)
'channels': 1, # 單聲道
'dtype': 'int16', # 16-bit PCM
'format': 'wav', # 檔案格式
'max_duration': 60, # 秒
'min_duration': 1, # 秒
'buffer_size': 1024 # 樣本
}
# 音訊品質檢查閾值
QUALITY_THRESHOLDS = {
'min_rms': 100, # 最小 RMS 音量
'max_rms': 30000, # 最大 RMS 音量
'min_snr_db': 10, # 最小信噪比 (dB)
'max_clipping_ratio': 0.01 # 最大削波比例
}
# recorder.py
import sounddevice as sd
import scipy.io.wavfile as wav
import numpy as np
from pathlib import Path
import logging
from typing import Tuple, Optional
class AudioRecorder:
"""音訊錄製與處理"""
def __init__(self,
sample_rate: int = 16000,
channels: int = 1,
output_dir: str = "recordings"):
self.sample_rate = sample_rate
self.channels = channels
self.output_dir = Path(output_dir)
self.output_dir.mkdir(parents=True, exist_ok=True)
self.recording = None
self.is_recording = False
# 檢查音訊裝置
self._check_audio_device()
def _check_audio_device(self):
"""檢查可用的音訊裝置"""
try:
devices = sd.query_devices()
logging.info(f"✅ 偵測到 {len(devices)} 個音訊裝置")
# 找到預設輸入裝置
default_input = sd.query_devices(kind='input')
logging.info(f"📍 預設麥克風: {default_input['name']}")
except Exception as e:
logging.error(f"❌ 音訊裝置檢查失敗: {e}")
def record(self, duration: int = 30) -> np.ndarray:
"""
開始錄音
Args:
duration: 錄音時長(秒)
Returns:
音訊資料 (numpy array)
"""
try:
logging.info(f"🎙️ 開始錄音 ({duration} 秒)...")
# 錄製音訊
self.recording = sd.rec(
int(duration * self.sample_rate),
samplerate=self.sample_rate,
channels=self.channels,
dtype='int16'
)
# 等待錄音完成
sd.wait()
logging.info("✅ 錄音完成")
return self.recording
except Exception as e:
logging.error(f"❌ 錄音失敗: {e}")
return None
def record_realtime(self, callback=None):
"""
即時錄音(可中斷)
Args:
callback: 回調函數,接收音訊片段
"""
# 進階功能:實作可中斷的即時錄音
# 用於未來版本的即時回饋功能
pass
def save(self,
audio_data: np.ndarray,
filename: str = "recording.wav") -> str:
"""
儲存音檔
Args:
audio_data: 音訊資料
filename: 檔案名稱
Returns:
完整檔案路徑
"""
if audio_data is None:
logging.error("❌ 無音訊資料可儲存")
return None
filepath = self.output_dir / filename
try:
wav.write(str(filepath), self.sample_rate, audio_data)
logging.info(f"💾 音檔已儲存: {filepath}")
return str(filepath)
except Exception as e:
logging.error(f"❌ 儲存失敗: {e}")
return None
def validate_quality(self, audio_data: np.ndarray) -> Tuple[bool, str]:
"""
驗證音訊品質
Returns:
(是否通過, 訊息)
"""
if audio_data is None or len(audio_data) == 0:
return False, "音訊資料為空"
# 1. 檢查音量
rms = np.sqrt(np.mean(audio_data.astype(float)**2))
if rms < 100:
return False, "音量太小,請靠近麥克風"
if rms > 30000:
return False, "音量過大,請降低音量或遠離麥克風"
# 2. 檢查削波(Clipping)
max_amplitude = np.max(np.abs(audio_data))
if max_amplitude >= 32767: # int16 最大值
clipping_ratio = np.sum(np.abs(audio_data) >= 32760) / len(audio_data)
if clipping_ratio > 0.01: # 超過 1% 削波
return False, "訊號削波嚴重,請降低音量"
# 3. 檢查靜音
silent_ratio = np.sum(np.abs(audio_data) < 50) / len(audio_data)
if silent_ratio > 0.9: # 超過 90% 靜音
return False, "未偵測到語音,請確認麥克風"
# 4. 檢查時長
duration = len(audio_data) / self.sample_rate
if duration < 1:
return False, "錄音時間太短"
return True, f"音訊品質良好 (RMS: {rms:.0f}, 時長: {duration:.1f}秒)"
def analyze_audio(self, audio_data: np.ndarray) -> dict:
"""
分析音訊特徵(用於除錯)
Returns:
音訊特徵字典
"""
if audio_data is None:
return {}
duration = len(audio_data) / self.sample_rate
rms = np.sqrt(np.mean(audio_data.astype(float)**2))
peak = np.max(np.abs(audio_data))
return {
'duration': duration,
'sample_rate': self.sample_rate,
'rms': float(rms),
'peak': int(peak),
'samples': len(audio_data),
'dtype': str(audio_data.dtype)
}
def apply_noise_reduction(self, audio_data: np.ndarray) -> np.ndarray:
"""
簡易降噪處理(可選)
Note: 基礎版本,進階可使用 noisereduce 套件
"""
# 簡單的高通濾波器,移除低頻噪音
# 進階功能待實作
return audio_data
# 使用範例
if __name__ == "__main__":
recorder = AudioRecorder()
# 錄音 10 秒
audio = recorder.record(duration=10)
# 驗證品質
is_valid, message = recorder.validate_quality(audio)
print(f"品質驗證: {message}")
if is_valid:
# 儲存檔案
filepath = recorder.save(audio, "test_recording.wav")
# 分析資訊
info = recorder.analyze_audio(audio)
print(f"音訊資訊: {info}")
# Gradio 提供內建的 gr.Audio 元件,簡化錄音實作
import gradio as gr
def create_audio_interface():
"""建立錄音介面"""
with gr.Blocks() as demo:
gr.Markdown("## 🎤 台語錄音")
# Gradio 會自動處理錄音
audio_input = gr.Audio(
source="microphone", # 使用麥克風
type="filepath", # 回傳檔案路徑
label="開始錄音",
format="wav" # WAV 格式
)
status = gr.Textbox(label="狀態")
def process_audio(audio_path):
if audio_path is None:
return "請先錄音"
# 驗證音訊品質
recorder = AudioRecorder()
rate, data = wav.read(audio_path)
is_valid, msg = recorder.validate_quality(data)
return msg
audio_input.change(
fn=process_audio,
inputs=[audio_input],
outputs=[status]
)
return demo
# whisper_model.py
from transformers import pipeline
import torch
import logging
from functools import lru_cache
from typing import Optional
class WhisperTaiwanese:
"""Whisper 台語模型封裝"""
MODEL_NAME = "NUTN-KWS/Whisper-Taiwanese-model-v0.5"
def __init__(self, use_gpu: bool = True):
self.device = self._get_device(use_gpu)
self.pipe = None
self._is_loaded = False
logging.info(f"🖥️ 使用裝置: {self.device}")
def _get_device(self, use_gpu: bool) -> int:
"""偵測可用裝置"""
if use_gpu and torch.cuda.is_available():
logging.info("✅ 偵測到 GPU (CUDA)")
return 0
else:
logging.info("📌 使用 CPU")
return -1
@lru_cache(maxsize=1)
def load_model(self):
"""
載入模型(使用快取避免重複載入)
"""
if self._is_loaded:
return self.pipe
try:
logging.info(f"⏳ 載入模型: {self.MODEL_NAME}")
self.pipe = pipeline(
"automatic-speech-recognition",
model=self.MODEL_NAME,
device=self.device
)
self._is_loaded = True
logging.info("✅ 模型載入完成")
return self.pipe
except Exception as e:
logging.error(f"❌ 模型載入失敗: {e}")
raise
def transcribe(self,
audio_path: str,
return_timestamps: bool = False) -> dict:
"""
語音轉文字
Args:
audio_path: 音檔路徑
return_timestamps: 是否回傳時間戳
Returns:
辨識結果字典
"""
if not self._is_loaded:
self.load_model()
try:
logging.info(f"🎯 開始辨識: {audio_path}")
result = self.pipe(
audio_path,
return_timestamps=return_timestamps
)
logging.info(f"✅ 辨識完成: {result['text'][:50]}...")
return result
except Exception as e:
logging.error(f"❌ 辨識失敗: {e}")
return {"text": "", "error": str(e)}
def batch_transcribe(self, audio_paths: list) -> list:
"""批次辨識(用於測試)"""
results = []
for path in audio_paths:
result = self.transcribe(path)
results.append(result)
return results
def get_model_info(self) -> dict:
"""取得模型資訊"""
return {
"model_name": self.MODEL_NAME,
"device": "GPU" if self.device == 0 else "CPU",
"is_loaded": self._is_loaded,
"cuda_available": torch.cuda.is_available()
}
# 全域單例模式
_whisper_instance = None
def get_whisper_model(use_gpu: bool = True) -> WhisperTaiwanese:
"""取得 Whisper 模型實例(單例)"""
global _whisper_instance
if _whisper_instance is None:
_whisper_instance = WhisperTaiwanese(use_gpu=use_gpu)
_whisper_instance.load_model()
return _whisper_instance
# text_processor.py
import re
import logging
class TextProcessor:
"""文字處理工具"""
@staticmethod
def is_tailo(text: str) -> bool:
"""
判斷文字是否為台羅拼音
檢查項目:
- 包含台羅特殊符號 (ⁿ, -, ́, ̀ 等)
- 拉丁字母為主
- 符合台羅音節結構
"""
# 台羅特徵
tailo_markers = [
'ⁿ', # 鼻音符號
'\u0301', # 聲調符號 ́
'\u0300', # 聲調符號 ̀
'\u0302', # 聲調符號 ̂
'\u030d', # 聲調符號 ̍
'ō', 'ū', 'ī', 'ā', 'ē', # 長音
]
# 檢查是否包含台羅符號
has_tailo_marks = any(marker in text for marker in tailo_markers)
# 檢查拉丁字母比例
latin_ratio = sum(c.isalpha() for c in text) / len(text) if text else 0
is_likely_tailo = has_tailo_marks or latin_ratio > 0.5
logging.debug(f"判斷文字格式: '{text[:20]}...' → {'台羅' if is_likely_tailo else '漢字'}")
return is_likely_tailo
@staticmethod
def normalize_tailo(text: str) -> str:
"""
正規化台羅文字
- 統一小寫
- 移除多餘空白
- 保留聲調符號
"""
# 轉小寫(但保留聲調符號)
text = text.lower()
# 移除多餘空白
text = re.sub(r'\s+', ' ', text).strip()
# 移除標點符號(可選)
# text = re.sub(r'[,!?.;:,!?。;:]', '', text)
return text
@staticmethod
def normalize_hanzi(text: str) -> str:
"""正規化漢字文字"""
# 移除空白
text = re.sub(r'\s+', '', text)
# 移除標點
text = re.sub(r'[,!?.;:,!?。;:]', '', text)
return text
@staticmethod
def hanzi_to_tailo(hanzi_text: str) -> str:
"""
漢字轉台羅(備用方案)
Note: 需要整合外部工具
- 台灣言語工具
- 自建字典
"""
try:
# TODO: 整合轉換工具
# from tai5_uan5_gian5_gi2_kang1_ku7 import ...
logging.warning("⚠️ 漢字轉台羅功能尚未實作")
return hanzi_text
except Exception as e:
logging.error(f"❌ 轉換失敗: {e}")
return hanzi_text
# 使用範例
processor = TextProcessor()
# 測試格式判斷
text1 = "Lí hó, tsia̍h-pá buē?"
print(processor.is_tailo(text1)) # True
text2 = "你好,食飽未?"
print(processor.is_tailo(text2)) # False
# 正規化
normalized = processor.normalize_tailo(" Lí Hó ")
print(normalized) # "lí hó"
# recognition.py
from whisper_model import get_whisper_model
from text_processor import TextProcessor
class RecognitionPipeline:
"""完整辨識流程"""
def __init__(self):
self.whisper = get_whisper_model()
self.processor = TextProcessor()
def recognize(self, audio_path: str) -> dict:
"""
執行語音辨識並處理結果
Returns:
{
'text': 辨識文字,
'format': 'tailo' or 'hanzi',
'normalized': 正規化後的文字,
'raw': 原始辨識結果
}
"""
# Step 1: 語音辨識
result = self.whisper.transcribe(audio_path)
raw_text = result.get('text', '')
if not raw_text:
return {
'text': '',
'format': 'unknown',
'normalized': '',
'raw': result
}
# Step 2: 判斷格式
is_tailo = self.processor.is_tailo(raw_text)
text_format = 'tailo' if is_tailo else 'hanzi'
# Step 3: 正規化
if is_tailo:
normalized = self.processor.normalize_tailo(raw_text)
else:
# 如果是漢字,嘗試轉換為台羅
logging.info("⚠️ 偵測到漢字輸出,需要轉換...")
normalized = self.processor.hanzi_to_tailo(raw_text)
return {
'text': raw_text,
'format': text_format,
'normalized': normalized,
'raw': result
}
# 使用範例
pipeline = RecognitionPipeline()
result = pipeline.recognize("recording.wav")
print(f"原始: {result['text']}")
print(f"格式: {result['format']}")
print(f"正規化: {result['normalized']}")
# scorer.py
from Levenshtein import distance as levenshtein_distance
from typing import Dict, List, Tuple
import re
import logging
class TaiwaneseScorer:
"""台語發音評分引擎"""
# 評分權重
WEIGHTS = {
'overall_accuracy': 0.70, # 整體準確度
'character_accuracy': 0.20, # 字元準確度
'tone_accuracy': 0.10 # 聲調準確度
}
def __init__(self):
self.processor = TextProcessor()
def score(self,
reference: str,
recognized: str) -> Dict:
"""
計算評分
Args:
reference: 標準答案(台羅)
recognized: 辨識結果(台羅)
Returns:
評分結果字典
"""
# 正規化
ref_normalized = self.processor.normalize_tailo(reference)
rec_normalized = self.processor.normalize_tailo(recognized)
# 計算各項指標
overall = self._overall_accuracy(ref_normalized, rec_normalized)
character = self._character_accuracy(ref_normalized, rec_normalized)
tone = self._tone_accuracy(ref_normalized, rec_normalized)
# 加權計算最終分數
final_score = (
overall * self.WEIGHTS['overall_accuracy'] +
character * self.WEIGHTS['character_accuracy'] +
tone * self.WEIGHTS['tone_accuracy']
)
# 生成評分報告
return {
'final_score': round(final_score, 2),
'level': self._get_level(final_score),
'metrics': {
'overall_accuracy': round(overall, 2),
'character_accuracy': round(character, 2),
'tone_accuracy': round(tone, 2)
},
'reference': ref_normalized,
'recognized': rec_normalized,
'edit_distance': levenshtein_distance(ref_normalized, rec_normalized),
'errors': self._find_errors(ref_normalized, rec_normalized)
}
def _overall_accuracy(self, reference: str, recognized: str) -> float:
"""
整體準確度(基於編輯距離)
公式: (1 - 編輯距離 / 最大長度) × 100
"""
if not reference or not recognized:
return 0.0
edit_dist = levenshtein_distance(reference, recognized)
max_len = max(len(reference), len(recognized))
accuracy = 100 * (1 - edit_dist / max_len)
return max(0, accuracy)
def _character_accuracy(self, reference: str, recognized: str) -> float:
"""
字元準確度(逐字元比對)
"""
ref_chars = reference.split()
rec_chars = recognized.split()
# 計算正確字元數
correct = 0
for i in range(min(len(ref_chars), len(rec_chars))):
if ref_chars[i] == rec_chars[i]:
correct += 1
total = len(ref_chars)
if total == 0:
return 0.0
return 100 * correct / total
def _tone_accuracy(self, reference: str, recognized: str) -> float:
"""
聲調準確度(比對聲調符號)
"""
# 提取聲調符號
ref_tones = self._extract_tones(reference)
rec_tones = self._extract_tones(recognized)
if not ref_tones:
return 100.0 # 無聲調標記,給予滿分
# 計算正確聲調數
correct_tones = sum(
1 for r, p in zip(ref_tones, rec_tones) if r == p
)
return 100 * correct_tones / len(ref_tones)
def _extract_tones(self, text: str) -> List[str]:
"""提取聲調符號"""
tone_marks = []
# Unicode 組合字元的聲調符號
tone_diacritics = [
'\u0301', # ́ 第2調
'\u0300', # ̀ 第3調
'\u0302', # ̂ 第5調
'\u0304', # ̄ 第7調
'\u030d', # ̍ 第8調
]
for char in text:
for tone in tone_diacritics:
if tone in char:
tone_marks.append(tone)
break
return tone_marks
def _find_errors(self, reference: str, recognized: str) -> List[Dict]:
"""
找出錯誤位置與類型
Returns:
錯誤列表
"""
errors = []
ref_tokens = reference.split()
rec_tokens = recognized.split()
# 逐詞比對
max_len = max(len(ref_tokens), len(rec_tokens))
for i in range(max_len):
ref = ref_tokens[i] if i < len(ref_tokens) else ""
rec = rec_tokens[i] if i < len(rec_tokens) else ""
if ref != rec:
error_type = self._classify_error(ref, rec)
errors.append({
'position': i,
'expected': ref,
'actual': rec,
'type': error_type
})
return errors
def _classify_error(self, expected: str, actual: str) -> str:
"""
分類錯誤類型
- substitution: 替換錯誤
- insertion: 插入錯誤
- deletion: 刪除錯誤
- tone: 聲調錯誤
"""
if not expected:
return "insertion"
if not actual:
return "deletion"
# 移除聲調比較基本音節
expected_base = re.sub(r'[\u0300-\u036f]', '', expected)
actual_base = re.sub(r'[\u0300-\u036f]', '', actual)
if expected_base == actual_base:
return "tone" # 僅聲調不同
else:
return "substitution" # 音節錯誤
def _get_level(self, score: float) -> str:
"""評分等級"""
if score >= 90:
return "優秀"
elif score >= 80:
return "良好"
elif score >= 70:
return "尚可"
elif score >= 60:
return "待改進"
else:
return "需重學"
def generate_feedback(self, result: Dict) -> str:
"""
生成個人化回饋建議
"""
score = result['final_score']
errors = result['errors']
feedback = []
# 整體評價
if score >= 90:
feedback.append("👏 發音非常標準,繼續保持!")
elif score >= 80:
feedback.append("😊 發音清晰,略有小瑕疵。")
elif score >= 70:
feedback.append("🤔 基本正確,但需要多加練習。")
else:
feedback.append("💪 還有進步空間,建議重複練習。")
# 錯誤分析
if errors:
tone_errors = [e for e in errors if e['type'] == 'tone']
other_errors = [e for e in errors if e['type'] != 'tone']
if tone_errors:
feedback.append(f"\n⚠️ 聲調錯誤 {len(tone_errors)} 處,請注意聲調變化。")
if other_errors:
feedback.append(f"\n⚠️ 發音錯誤 {len(other_errors)} 處,建議逐字練習。")
# 列出前 3 個錯誤
feedback.append("\n**錯誤詳情:**")
for i, error in enumerate(errors[:3], 1):
feedback.append(
f"{i}. 位置 {error['position']}: "
f"應為「{error['expected']}」,實為「{error['actual']}」"
)
return "\n".join(feedback)
# 使用範例
scorer = TaiwaneseScorer()
reference = "Lí hó, tsia̍h-pá buē?"
recognized = "Lí hó, tsiah-pa bue?"
result = scorer.score(reference, recognized)
print(f"分數: {result['final_score']}")
print(f"等級: {result['level']}")
print(f"\n回饋:")
print(scorer.generate_feedback(result))
# visualization.py
def create_score_chart(metrics: dict) -> str:
"""生成評分圖表(使用 ASCII 藝術或 Plotly)"""
overall = metrics['overall_accuracy']
character = metrics['character_accuracy']
tone = metrics['tone_accuracy']
# 簡易長條圖
chart = f"""
整體準確度: {'█' * int(overall/5):<20} {overall:.1f}%
字元準確度: {'█' * int(character/5):<20} {character:.1f}%
聲調準確度: {'█' * int(tone/5):<20} {tone:.1f}%
"""
return chart
Week 1
- [ ] Day 1-2: 環境設定
```bash
# 建立專案目錄
mkdir taiwanese-pronunciation-scorer
cd taiwanese-pronunciation-scorer
# 建立虛擬環境
python -m venv venv
source venv/bin/activate # Linux/Mac
# venv\Scripts\activate # Windows
# 安裝核心套件
pip install torch transformers
pip install sounddevice scipy
pip install python-Levenshtein
pip install gradio
# 建立專案結構
mkdir -p {data/texts,recordings,logs,tests}
touch {main.py,text_manager.py,recorder.py,whisper_model.py,scorer.py}
```
print("⏳ 開始下載模型...")
pipe = pipeline(
"automatic-speech-recognition",
model="NUTN-KWS/Whisper-Taiwanese-model-v0.5"
)
print("✅ 模型下載完成")
# 測試辨識(需準備測試音檔)
# result = pipe("test_audio.wav")
# print(f"辨識結果: {result['text']}")
```
# 準備 3 種測試音檔:
# 1. 清晰台語發音
# 2. 含噪音的音檔
# 3. 不同說話速度
test_files = [
"test_clear.wav",
"test_noisy.wav",
"test_fast.wav"
]
for audio_file in test_files:
result = pipe(audio_file)
text = result['text']
# 判斷輸出格式
is_tailo = check_if_tailo(text)
print(f"\n檔案: {audio_file}")
print(f"辨識: {text}")
print(f"格式: {'台羅' if is_tailo else '漢字'}")
# ⚠️ 關鍵決策點: 根據結果決定後續開發策略
```
Week 2
- [ ] Day 8-10: 準備測試資料
- 建立 30 筆文稿(easy: 20, medium: 8, hard: 2)
- 錄製 10 個測試音檔(不同性別/年齡/口音)
- 建立評分基準(邀請台語專家)
Day 15-16: 文稿管理系統
# 實作內容:
# 1. TextManager 類別
# 2. texts.json 資料結構
# 3. CRUD 操作
# 4. 統計功能
# 驗收標準:
assert len(text_manager.texts) >= 30
assert text_manager.get_random_text('easy') is not None
Day 17-18: 錄音模組
# 實作內容:
# 1. AudioRecorder 類別
# 2. 音訊品質驗證
# 3. WAV 檔案儲存
# 4. 音訊分析工具
# 測試項目:
# - 錄音 10 次,成功率 100%
# - 品質驗證能正確識別低音量/削波
Day 19-20: 文字處理工具
# 實作內容:
# 1. TextProcessor 類別
# 2. 格式判斷 (is_tailo)
# 3. 文字正規化
# 4. (可選) 漢字轉台羅
# 測試案例:
test_cases = [
("Lí hó", True), # 應判斷為台羅
("你好", False), # 應判斷為漢字
(" Lí ", "lí") # 正規化測試
]
Day 21: 整合測試與除錯
Day 22-24: 評分引擎
# 實作內容:
# 1. TaiwaneseScorer 類別
# 2. 多維度評分計算
# 3. 錯誤分析
# 4. 回饋生成
# 驗收標準:
# - 與專家評分相關係數 > 0.75
# - 能正確識別聲調/音節錯誤
Day 25-27: Whisper 整合
# 實作內容:
# 1. WhisperTaiwanese 類別
# 2. RecognitionPipeline
# 3. 錯誤處理
# 4. 效能優化(模型快取)
# 測試項目:
# - 連續辨識 10 次,無記憶體洩漏
# - 平均推理時間 < 5 秒
Day 28: 端對端測試
# 完整流程測試
def test_e2e():
# 1. 選擇文稿
text = text_manager.get_random_text()
# 2. (人工)錄音
audio = recorder.record(10)
path = recorder.save(audio)
# 3. 辨識
result = pipeline.recognize(path)
# 4. 評分
score = scorer.score(text['tailo'], result['normalized'])
# 5. 驗證
assert 0 <= score['final_score'] <= 100
assert score['level'] in ["優秀", "良好", "尚可", "待改進", "需重學"]
print("✅ 端對端測試通過")
Day 29-30: 基礎介面
# app.py
import gradio as gr
with gr.Blocks(theme=gr.themes.Soft()) as demo:
gr.Markdown("# 🎙️ 台語朗讀自動評分系統")
with gr.Row():
difficulty = gr.Dropdown(
choices=["easy", "medium", "hard"],
label="選擇難度",
value="easy"
)
generate_btn = gr.Button("📝 生成文稿", variant="primary")
text_display = gr.Textbox(
label="請朗讀以下文本",
lines=4,
interactive=False,
scale=2
)
with gr.Row():
audio_input = gr.Audio(
source="microphone",
type="filepath",
label="🎤 開始錄音"
)
submit_btn = gr.Button("🎯 提交評分", variant="primary", size="lg")
result_display = gr.Markdown(label="評分結果")
# 事件綁定
generate_btn.click(
fn=generate_text,
inputs=[difficulty],
outputs=[text_display]
)
submit_btn.click(
fn=process_audio,
inputs=[audio_input, text_display],
outputs=[result_display]
)
demo.launch()
Day 31-32: 功能完善
- 加入載入動畫
- 錯誤提示優化
- 音訊波形顯示
- 重新錄製按鈕
Day 33-35: 結果展示優化
# 美化評分結果顯示
def format_result(score_data):
result_md = f"""
## 📊 評分結果
### 🎯 總分: **{score_data['final_score']}** 分
**等級**: {get_emoji(score_data['level'])} {score_data['level']}
---
### 📈 詳細指標
| 項目 | 分數 | 進度 |
|------|------|------|
| 整體準確度 | {score_data['metrics']['overall_accuracy']:.1f}% | {progress_bar(score_data['metrics']['overall_accuracy'])} |
| 字元準確度 | {score_data['metrics']['character_accuracy']:.1f}% | {progress_bar(score_data['metrics']['character_accuracy'])} |
| 聲調準確度 | {score_data['metrics']['tone_accuracy']:.1f}% | {progress_bar(score_data['metrics']['tone_accuracy'])} |
---
### 📝 對照
**標準答案**: `{score_data['reference']}`
**辨識結果**: `{score_data['recognized']}`
**編輯距離**: {score_data['edit_distance']}
---
### 💡 改進建議
{generate_feedback(score_data)}
"""
return result_md
Day 36-38: 使用者測試
- 邀請 10 位使用者測試
- 收集回饋問卷(SUS 量表)
- 記錄使用問題
Day 39-41: 優化改進
- 修復發現的 Bug
- 優化使用流程
- 改善錯誤訊息
- 效能調校
Day 42: 功能凍結與文件
Day 43-44: 功能測試
| 測試項目 | 測試案例數 | 通過標準 |
|---|---|---|
| 文稿載入 | 100 | 全部正確 |
| 錄音功能 | 50 | 成功率 > 95% |
| 辨識準確率 | 100 | 平均 > 85% |
| 評分準確性 | 50 | 相關係數 > 0.8 |
| 介面響應 | 20 | 無卡頓/錯誤 |
Day 45-46: 效能測試
# performance_test.py
import time
import psutil
def test_performance():
"""效能測試"""
# 1. 模型載入時間
start = time.time()
model = get_whisper_model()
load_time = time.time() - start
assert load_time < 30, "模型載入過慢"
# 2. 辨識速度
start = time.time()
result = model.transcribe("test.wav")
infer_time = time.time() - start
assert infer_time < 5, "辨識過慢"
# 3. 記憶體使用
memory = psutil.Process().memory_info().rss / 1024 / 1024
assert memory < 3000, f"記憶體使用過高: {memory}MB"
print(f"""
✅ 效能測試通過
- 載入時間: {load_time:.2f}s
- 辨識時間: {infer_time:.2f}s
- 記憶體: {memory:.0f}MB
""")
Day 47-49: 壓力測試
- 連續使用 1 小時無崩潰
- 同時 5 個使用者無延遲
- 處理異常輸入(過長/過短音檔)
Day 50-52: 文件撰寫
📄 待完成文件:
- README.md (專案說明)
- INSTALL.md (安裝指南)
- USER_GUIDE.md (使用手冊)
- API.md (API 文件)
- CONTRIBUTING.md (貢獻指南)
- CHANGELOG.md (版本記錄)
Day 53-54: 部署準備
# requirements.txt
torch==2.0.1
transformers==4.30.0
sounddevice==0.4.6
scipy==1.10.1
python-Levenshtein==0.21.1
gradio==4.0.0
# Docker 配置 (可選)
FROM python:3.10-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["python", "main.py"]
Day 55-56: 正式發布
- GitHub Release
- 部署至雲端平台(Hugging Face Spaces / Render)
- 公告與推廣
| 分類 | 難度分布 | 總數 | 來源 |
|---|---|---|---|
| 問候語 | E:10, M:4, H:1 | 15 | 日常用語 |
| 自我介紹 | E:8, M:6, H:1 | 15 | 教材改編 |
| 日常對話 | E:15, M:12, H:3 | 30 | 生活情境 |
| 購物 | E:5, M:4, H:1 | 10 | 市場對話 |
| 家庭 | E:5, M:4, H:1 | 10 | 家庭對話 |
| 俗諺語 | E:2, M:8, H:5 | 15 | 台語諺語 |
| 文學 | E:0, M:2, H:3 | 5 | 詩詞散文 |
| 總計 | 45, 40, 15 | 100 | - |
1. 你好。(Lí hó.)
2. 食飽未?(Tsia̍h-pá buē?)
3. 我愛你。(Guá ài lí.)
4. 多謝你。(To-siā lí.)
5. 你叫啥名?(Lí kiò siáⁿ-miâ?)
6. 我欲去學校。(Guá beh khì ha̍k-hāu.)
7. 今仔日天氣真好。(Kin-á-ji̍t thinn-khì tsin hó.)
8. 這个幾錢?(Tsit ê guī-tsé tsînn?)
9. 我毋知。(Guá m̄-tsai.)
10. 請坐。(Tshiánn tsē.)
1. 我的名叫阿明,今年二十歲,佇台北讀冊。
(Guá ê miâ kiò A-bîng, kin-nî jī-tsa̍p huè, tī Tâi-pak tha̍k-chheh.)
2. 阮兜有五个人,阿爸、阿母、阮佮兩个小弟。
(Guán tau ū gōo ê lâng, a-pah, a-bú, guán kah nn̄g ê sió-tī.)
3. 這條路一直行過去,看著紅綠燈就倒手仔爿。
(Tsit tiâu lōo it-ti̍t kiânn kuè-khì, khuànn-tio̍h âng-li̍k-ting tō tò-tshiú-á-pîng.)
1. 一枝草,一點露,天無絕人之路;日頭赤焱焱,隨人顧性命。
(Tsi̍t ki tsháu, tsi̍t tiám lōo, thinn bô tsa̍t jîn tsi lo̍o; ji̍t-thâu tshiah-iānn-iānn, suî lâng kòo sìng-miā.)
2. 阮阿公講,伊細漢的時陣,生活真艱苦,逐工愛去田裡做穡,日時做甲烏暗才轉來兜。
(Guán a-kong kóng, i sè-hàn ê sî-tsūn, sing-ua̍h tsin kan-khóo, ta̍k-kang ài khì tshân-lí tsò-sit, ji̍t-sî tsò kah oo-àm tsiah tńg-lâi tau.)
Step 1: 初稿撰寫
↓
Step 2: 台語專家校驗
├─ 檢查漢字用字
├─ 確認台羅拼音正確性
└─ 驗證聲調標記
↓
Step 3: 難度評估
├─ 字數統計
├─ 罕見詞彙檢查
└─ 語法複雜度分析
↓
Step 4: 標籤分類
↓
Step 5: 錄入資料庫
↓
Step 6: 測試驗證
環境要求:
- 地點: 安靜室內空間
- 背景噪音: < 40 dB
- 迴音: 最小化(可用軟材料吸音)
設備規格:
- 麥克風: 中等品質(非專業錄音室級)
- 距離: 15-30 公分
- 採樣率: 16000 Hz
- 格式: WAV (PCM)
錄製標準:
- 語速: 正常(每分鐘 150-200 字)
- 音量: 適中(RMS 5000-15000)
- 清晰度: 咬字清楚,不含糊
| ID | 性別 | 年齡 | 口音 | 台語程度 | 錄製數量 |
|---|---|---|---|---|---|
| S01 | 男 | 25-35 | 台北腔 | 流利 | 20 篇 |
| S02 | 女 | 25-35 | 台北腔 | 流利 | 20 篇 |
| S03 | 男 | 40-50 | 南部腔 | 母語 | 15 篇 |
| S04 | 女 | 40-50 | 南部腔 | 母語 | 15 篇 |
| S05 | 男/女 | 18-25 | 混合 | 中等 | 10 篇 |
{
"audio_id": "audio_001_S01_easy",
"text_id": "text_001",
"speaker_id": "S01",
"difficulty": "easy",
"recording_date": "2025-01-10",
"duration": 3.2,
"sample_rate": 16000,
"quality_check": {
"rms": 8500,
"snr_db": 25,
"is_clipping": false
},
"human_score": 95, // 人工評分(用於驗證)
"notes": ""
}
## 台語發音評分準則
### 評分維度 (總分 100)
1. **音節正確性 (50 分)**
- 聲母正確: 20 分
- 韻母正確: 20 分
- 音節完整: 10 分
2. **聲調準確度 (30 分)**
- 調值正確: 30 分
- (台語7調+輕聲)
3. **流暢度 (20 分)**
- 斷句自然: 10 分
- 速度適中: 5 分
- 無停頓: 5 分
### 評分細則
**優秀 (90-100)**
- 發音標準,無明顯錯誤
- 聲調準確,符合變調規則
- 流暢自然
**良好 (80-89)**
- 發音清晰,1-2處小錯
- 聲調大致正確
- 稍有停頓
**尚可 (70-79)**
- 基本可理解
- 3-4處錯誤
- 聲調偶有錯誤
**待改進 (60-69)**
- 多處明顯錯誤
- 聲調不穩定
- 影響理解
**需重學 (<60)**
- 無法理解
- 嚴重錯誤
- 需重新學習
| 風險項目 | 機率 | 影響 | 風險等級 | 應對策略 |
|---|---|---|---|---|
| Whisper 輸出格式不符預期 | 高 | 高 | 🔴 高 | 備用漢字轉台羅方案 |
| 辨識準確率低於預期 | 中 | 高 | 🟠 中高 | 音訊前處理 + 模型微調 |
| 錄音品質不穩定 | 中 | 中 | 🟡 中 | 品質檢查機制 |
| 評分標準爭議 | 中 | 中 | 🟡 中 | 專家委員會制定 |
| 模型推理速度慢 | 低 | 中 | 🟢 低 | GPU加速/模型量化 |
| 系統穩定性問題 | 低 | 高 | 🟡 中 | 完善錯誤處理 |
情境: 模型輸出漢字而非台羅
方案A: 整合台灣言語工具
from tai5_uan5_gian5_gi2_kang1_ku7 import TaiBunTaigiHuanLoGiSuKhiPhing
def hanzi_to_tailo_v1(hanzi):
"""使用台灣言語工具轉換"""
converter = TaiBunTaigiHuanLoGiSuKhiPhing()
try:
tailo = converter.chu_im(hanzi)
return tailo
except Exception as e:
logging.error(f"轉換失敗: {e}")
return None
方案B: 自建漢字-台羅對照表
# 建立常用詞對照字典
HANZI_TAILO_DICT = {
"你好": "Lí hó",
"食飽未": "Tsia̍h-pá buē",
"多謝": "To-siā",
# ... 擴充至 1000+ 詞條
}
def hanzi_to_tailo_v2(hanzi):
"""字典查詢轉換"""
return HANZI_TAILO_DICT.get(hanzi, hanzi)
方案C: 降級為漢字比對
def fallback_scoring(ref_hanzi, rec_hanzi):
"""漢字比對(精度較低)"""
# 僅比對漢字,無法評估發音細節
return levenshtein_distance(ref_hanzi, rec_hanzi)
策略1: 音訊前處理
import noisereduce as nr
import librosa
def preprocess_audio(audio_path):
"""音訊前處理流程"""
# 1. 載入音訊
y, sr = librosa.load(audio_path, sr=16000)
# 2. 降噪
y_denoised = nr.reduce_noise(y=y, sr=sr)
# 3. 音量正規化
y_normalized = librosa.util.normalize(y_denoised)
# 4. 靜音移除
y_trimmed, _ = librosa.effects.trim(y_normalized)
return y_trimmed, sr
策略2: 收集錯誤案例
# 建立錯誤案例資料庫
class ErrorCaseDB:
"""錯誤案例追蹤"""
def log_error(self, audio_path, expected, recognized, score):
"""記錄辨識錯誤"""
error_case = {
'timestamp': datetime.now(),
'audio': audio_path,
'expected': expected,
'recognized': recognized,
'score': score,
'edit_distance': levenshtein_distance(expected, recognized)
}
# 儲存至資料庫
self._save_to_db(error_case)
# 如果累積100個案例,觸發分析
if self._count() >= 100:
self.analyze_patterns()
def analyze_patterns(self):
"""分析錯誤模式"""
# 找出常見錯誤類型
# 用於改進模型或評分機制
pass
策略3: 提示使用者改善錄音
RECORDING_TIPS = """
📌 錄音小技巧:
1. 找一個安靜的環境
2. 麥克風距離嘴巴 15-30 公分
3. 音量適中,不要太大聲或太小聲
4. 咬字清楚,語速正常
5. 避免背景音樂或電視聲
"""
應對: 建立評分委員會
組成:
- 台語教師 x 2
- 台語研究者 x 1
- 技術開發者 x 1
- 使用者代表 x 1
職責:
1. 制定詳細評分準則
2. 標註測試集標準答案
3. 定期檢視評分爭議案例
4. 調整評分權重
方案1: 模型量化
# 使用 INT8 量化減少模型大小
from optimum.onnxruntime import ORTModelForSpeechSeq2Seq
quantized_model = ORTModelForSpeechSeq2Seq.from_pretrained(
MODEL_NAME,
export=True,
provider="CPUExecutionProvider"
)
方案2: GPU 加速
# 確保安裝 CUDA 版本的 PyTorch
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118
方案3: 非同步處理
import asyncio
async def async_recognize(audio_path):
"""非同步辨識"""
loop = asyncio.get_event_loop()
result = await loop.run_in_executor(
None,
whisper.transcribe,
audio_path
)
return result
2025年1月 - 2月
Week 1 [====環境建置====]
Mon ███ Python環境配置
Tue ███ 套件安裝
Wed ███ 專案結構建立
Thu ███ Whisper模型下載
Fri ███ 模型測試
Week 2 [====驗證實驗====]
Mon ███ 輸出格式測試
Tue ███ 準備測試資料
Wed ███ 錄音功能驗證
Thu ███ 辨識準確率測試
Fri ███ 技術可行性報告
✅ 里程碑1: 技術驗證完成
Week 3 [====核心開發====]
Mon ███ TextManager開發
Tue ███ 文稿資料庫建立
Wed ███ AudioRecorder開發
Thu ███ 錄音測試
Fri ███ TextProcessor開發
Week 4 [====評分引擎====]
Mon ███ TaiwaneseScorer開發
Tue ███ 評分演算法實作
Wed ███ Whisper整合
Thu ███ 端對端測試
Fri ███ Bug修復
✅ 里程碑2: 核心功能完成
Week 5 [====介面開發====]
Mon ███ Gradio基礎介面
Tue ███ 事件綁定
Wed ███ 結果展示設計
Thu ███ UI/UX優化
Fri ███ 錯誤處理
Week 6 [====測試優化====]
Mon ███ 使用者測試
Tue ███ 收集回饋
Wed ███ Bug修復
Thu ███ 效能優化
Fri ███ 功能凍結
✅ 里程碑3: MVP完成
Week 7 [====系統測試====]
Mon ███ 功能測試
Tue ███ 效能測試
Wed ███ 壓力測試
Thu ███ 回歸測試
Fri ███ 測試報告
Week 8 [====部署發布====]
Mon ███ 文件撰寫
Tue ███ README完成
Wed ███ 部署準備
Thu ███ 雲端部署
Fri ███ 正式發布
✅ 里程碑4: 正式上線
| 日期 | 里程碑 | 交付物 | 驗收標準 |
|---|---|---|---|
| Week 2 | 技術驗證完成 | 可行性報告 | ✅ 模型可運作 ✅ 辨識率>70% ✅ 確認輸出格式 |
| Week 4 | 核心功能完成 | CLI 可執行版本 | ✅ 錄音正常 ✅ 辨識正常 ✅ 評分正常 |
| Week 6 | MVP 完成 | Web 介面版本 | ✅ 介面可用 ✅ 端對端流程完整 ✅ SUS>70 |
| Week 8 | 正式發布 | 生產版本 | ✅ 通過所有測試 ✅ 文件完整 ✅ 已部署上線 |
風險緩衝時間: 每個階段預留 10% 時間
調整原則:
1. 若 Week 2 輸出格式測試發現問題
→ 延長 1 週開發轉換工具
2. 若 Week 4 核心功能延遲
→ 壓縮 Week 5 介面開發時間
→ 或延後發布 1 週
3. 若 Week 6 使用者測試發現重大問題
→ 立即修復,延後非關鍵功能
緊急應變:
- 技術阻礙 → 啟動備案方案
- 資源不足 → 縮減功能範圍至核心MVP
- 時程壓力 → 砍掉非必要功能(如歷史記錄)
| 類別 | 指標 | 目標值 | 測量方式 | 重要性 |
|---|---|---|---|---|
| 效能 | 辨識準確率 (WER) | ≥ 85% | 100筆測試音檔 | ⭐⭐⭐ |
| 效能 | 系統回應時間 | ≤ 5秒 | 平均處理時間 | ⭐⭐⭐ |
| 效能 | 錄音成功率 | ≥ 95% | 100次錄音操作 | ⭐⭐ |
| 準確 | 評分相關係數 | ≥ 0.80 | 與專家評分比較 | ⭐⭐⭐ |
| 準確 | 評分一致性 | ≥ 0.85 | 重複測試變異數 | ⭐⭐ |
| 品質 | 代碼覆蓋率 | ≥ 60% | pytest-cov | ⭐⭐ |
| 體驗 | 使用者滿意度 (SUS) | ≥ 70分 | SUS問卷 | ⭐⭐⭐ |
| 體驗 | 任務完成率 | ≥ 90% | 可用性測試 | ⭐⭐ |
程式碼
taiwanese-pronunciation-scorer/
├── README.md ✅ 專案說明
├── requirements.txt ✅ 依賴套件
├── main.py ✅ 主程式
├── text_manager.py ✅ 文稿管理
├── recorder.py ✅ 錄音模組
├── whisper_model.py ✅ 語音辨識
├── scorer.py ✅ 評分引擎
├── text_processor.py ✅ 文字處理
├── config.py ✅ 配置檔
├── utils.py ✅ 工具函式
├── tests/ ✅ 單元測試
│ ├── test_text_manager.py
│ ├── test_recorder.py
│ ├── test_scorer.py
│ └── ...
├── data/
│ └── texts.json ✅ 文稿資料庫 (100篇)
├── docs/
│ ├── INSTALL.md ✅ 安裝指南
│ ├── USER_GUIDE.md ✅ 使用手冊
│ ├── API.md ✅ API文件
│ └── ARCHITECTURE.md ✅ 架構說明
└── examples/ ✅ 使用範例
資料集
- ✅ 台語文稿庫: 100+ 篇(分級分類)
- ✅ 測試音檔集: 50+ 個(多說話者)
- ✅ 評分標準文件
- ✅ 錯誤案例資料庫
文件
- ✅ 技術白皮書
- ✅ 評分準則手冊
- ✅ 開發者指南
- ✅ 測試報告
- ✅ 使用者手冊
辨識準確率測試
def evaluate_recognition_accuracy():
"""評估辨識準確率"""
test_set = load_test_audios() # 100個測試音檔
results = []
for audio, ground_truth in test_set:
recognized = whisper.transcribe(audio)
wer = calculate_wer(ground_truth, recognized)
results.append(wer)
avg_wer = np.mean(results)
accuracy = 100 * (1 - avg_wer)
print(f"辨識準確率: {accuracy:.2f}%")
return accuracy
評分準確性測試
def evaluate_scoring_accuracy():
"""評估評分準確性"""
test_cases = load_expert_scored_cases() # 50個專家已評分案例
system_scores = []
expert_scores = []
for case in test_cases:
sys_score = scorer.score(case['reference'], case['recognized'])
system_scores.append(sys_score['final_score'])
expert_scores.append(case['expert_score'])
# 計算皮爾森相關係數
correlation = np.corrcoef(system_scores, expert_scores)[0, 1]
print(f"評分相關係數: {correlation:.3f}")
return correlation
SUS 問卷(System Usability Scale)
1. 我認為我會經常使用這個系統。
□ 強烈不同意 □ 不同意 □ 中立 □ 同意 □ 強烈同意
2. 我覺得這個系統太複雜了。
□ 強烈不同意 □ 不同意 □ 中立 □ 同意 □ 強烈同意
3. 我覺得這個系統很容易使用。
□ 強烈不同意 □ 不同意 □ 中立 □ 同意 □ 強烈同意
4. 我需要技術人員的幫助才能使用這個系統。
□ 強烈不同意 □ 不同意 □ 中立 □ 同意 □ 強烈同意
5. 我覺得這個系統的各項功能整合得很好。
□ 強烈不同意 □ 不同意 □ 中立 □ 同意 □ 強烈同意
... (共10題)
計分公式:
SUS分數 = [(奇數題總分 - 5) + (25 - 偶數題總分)] × 2.5
評估標準:
- 90-100: 優秀 (A+)
- 80-89: 良好 (A)
- 70-79: 尚可 (B)
- 60-69: 待改進 (C)
- <60: 不及格 (F)
任務完成率測試
測試任務:
1. 選擇一個簡單難度的文稿
2. 錄製朗讀音檔
3. 提交並查看評分結果
4. 理解評分報告內容
5. 重新練習一次
成功標準:
- 能獨立完成 5 個任務
- 完成時間 < 5 分鐘
- 無需求助
目標: ≥ 90% 使用者完成率
生成學習報告
```
[ ] 多人模式
```
功能:
競賽活動
```
[ ] 行動APP
```
平台:
額外功能:
- 離線使用(模型本地化)
- 推播通知提醒練習
- 社群分享
```
def analyze_prosody(audio):
"""韻律分析"""
# 音高 (Pitch)
pitch = librosa.yin(audio, sr=16000)
# 能量 (Energy)
energy = librosa.feature.rms(y=audio)[0]
# 語速 (Speaking Rate)
tempo = estimate_tempo(audio)
return {
'pitch_variation': np.std(pitch),
'avg_energy': np.mean(energy),
'speaking_rate': tempo
}
```
實作:
- 每種方言獨立的評分標準
- 方言特徵標註
- 自動識別方言類型
```
智能化功能:
1. 發音診斷
- 自動識別常見錯誤
- 針對性練習推薦
2. 個人化課程
- 根據程度自動調整難度
- 智能生成練習內容
3. 語音合成 (TTS)
- 提供標準發音示範
- 逐字播放功能
- 調整語速功能
新增語言:
- 客家語(四縣腔、海陸腔)
- 原住民語(阿美語、排灣語等)
- 其他閩南語方言(廈門話、潮州話)
技術挑戰:
- 各語言專用 Whisper 模型
- 羅馬拼音系統差異
- 評分標準制定
功能設計:
1. 使用者社群
- 分享學習心得
- 互相批改練習
- 問答討論區
2. 教師資源
- 教案分享
- 測驗卷生成
- 班級管理工具
3. 內容貢獻
- 使用者上傳文稿
- 社群審核機制
- 貢獻者排行榜
合作模式:
1. 學校授權版
- 客製化功能
- 學生資料管理
- 成績匯出報表
2. 教材整合
- 搭配課本使用
- 同步課程進度
- 考試模擬
3. 師資培訓
- 教師使用指導
- 教學工作坊
- 技術支援
免費版:
- 每日 10 次練習
- 基礎文稿庫 (100篇)
- 簡易評分報告
付費版 (NT$ 99/月):
- 無限練習次數
- 完整文稿庫 (1000+篇)
- 詳細分析報告
- 學習歷程追蹤
- 無廣告
- 離線使用
方案:
- 學校授權: NT$ 10,000/年 (50人以下)
- 企業方案: 客製報價
附加服務:
- 技術支援
- 客製化功能
- 資料分析報告
- 使用培訓
最低配置:
- CPU: Intel i5 第8代 / AMD Ryzen 5
- RAM: 8GB
- 硬碟: 10GB 可用空間
- 麥克風: 任何可用的輸入裝置
建議配置:
- CPU: Intel i7 第10代+ / AMD Ryzen 7
- RAM: 16GB+
- 硬碟: 20GB SSD
- GPU: NVIDIA GTX 1660 以上 (可選)
- 麥克風: 中等品質外接麥克風
理想配置 (開發/測試):
- CPU: Intel i9 / AMD Ryzen 9
- RAM: 32GB
- 硬碟: 50GB NVMe SSD
- GPU: NVIDIA RTX 3060+
- 麥克風: 專業級 USB 麥克風
作業系統:
- Windows: 10 / 11 (64-bit)
- macOS: 11+ (Big Sur, Monterey, Ventura)
- Linux: Ubuntu 20.04+ / Debian 11+
Python:
- 版本: 3.9, 3.10, 3.11 (推薦 3.10)
- 不支援: < 3.9, 3.12+ (套件相容性)
CUDA (使用GPU時):
- 版本: 11.8 或 12.1
- cuDNN: 8.9+
其他工具:
- Git: 最新版本
- FFmpeg: 最新版本 (音訊處理)
# requirements.txt
# 核心套件
torch==2.0.1
transformers==4.30.2
sounddevice==0.4.6
scipy==1.10.1
numpy==1.24.3
# 文字處理
python-Levenshtein==0.21.1
# 介面
gradio==4.0.2
# 音訊處理 (可選)
librosa==0.10.0
noisereduce==3.0.0
# 資料處理
pandas==2.0.3
# 工具
python-dotenv==1.0.0
pyyaml==6.0
# 測試
pytest==7.4.0
pytest-cov==4.1.0
# 文件
mkdocs==1.5.0
mkdocs-material==9.1.0
# 開發工具
black==23.7.0
pylint==2.17.5
mypy==1.4.1
1. "Robust Speech Recognition via Large-Scale Weak Supervision"
- Radford et al., 2022
- Whisper 原始論文
2. "Taiwanese Hokkien Speech Recognition"
- 台語語音辨識相關研究
3. "Automatic Pronunciation Assessment for Language Learning"
- 自動發音評分系統綜述
4. "Tonal Languages and Speech Recognition"
- 聲調語言的語音辨識挑戰
A: 台羅(台灣閩南語羅馬字拼音)是教育部公告的官方標準,具有:
- 標準化: 統一的拼寫規則
- 完整性: 能精確標示所有音素
- 教育性: 學校教材採用
- 可讀性: 拉丁字母易於電腦處理
A: MVP 階段以標準腔為主,但設計上預留擴展性:
- 目前: 以教育部標準音為基準
- 未來: 可新增方言模式(泉州腔、漳州腔等)
- 實作: 透過不同的評分權重檔調整
A: 多管齊下提升準確率:
1. 音訊前處理: 降噪、正規化
2. 提示使用者: 錄音環境建議
3. 模型微調: 收集資料fine-tune
4. 後處理: 常見錯誤校正
A: 評分機制設計考量:
- 客觀演算法: 基於編輯距離
- 專家驗證: 與人工評分比對
- 透明化: 提供詳細評分依據
- 可調整: 權重可根據使用回饋調整
A: 分階段實作:
- MVP階段: 需要網路(模型首次下載)
- 執行時: 可離線使用
- 未來: 完整離線模式(行動APP)
| 角色 | 人數 | 職責 | 所需技能 |
|---|---|---|---|
| 專案經理 | 1 | 進度管控、風險管理、協調溝通 | 專案管理、敏捷開發 |
| 後端工程師 | 1-2 | 核心系統開發、模型整合 | Python, ML, APIs |
| 前端工程師 | 1 | 介面設計與實作 | Web開發, Gradio/Streamlit |
| 台語專家 | 1 | 文稿校驗、評分標準制定 | 台語專業、語言學 |
| 測試工程師 | 1 | 品質保證、測試自動化 | 測試方法論、自動化 |
| UX設計師 | 0-1 | 使用者體驗設計(可選) | UI/UX、使用者研究 |
版本控制: Git + GitHub
專案管理: Trello / Notion / Jira
文件協作: Google Docs / Notion
溝通工具: Slack / Discord
設計工具: Figma (UI設計)
測試管理: TestRail (可選)
# 主分支
main # 穩定版本
develop # 開發分支
# 功能分支
feature/text-manager
feature/audio-recorder
feature/scoring-engine
feature/web-interface
# 工作流程
git checkout develop
git checkout -b feature/新功能
# ... 開發 ...
git add .
git commit -m "feat: 新增xxx功能"
git push origin feature/新功能
# 發起 Pull Request
格式: <type>(<scope>): <subject>
Type:
- feat: 新功能
- fix: Bug修復
- docs: 文件更新
- style: 格式調整
- refactor: 重構
- test: 測試
- chore: 雜項
範例:
feat(scorer): 新增聲調評分功能
fix(recorder): 修復音量過低問題
docs(readme): 更新安裝說明
本台語朗讀自動評分系統不僅是一個技術專案,更承載著本土語言傳承與科技賦能教育的使命。透過 AI 技術的應用,我們期望:
✅ 技術可行性驗證 - 務必在 Week 2 確認 Whisper 模型輸出格式
✅ 高品質文稿庫 - 100+ 篇經專家校驗的標準文稿
✅ 合理評分標準 - 與專家共同制定,持續優化
✅ 使用者中心設計 - 介面簡潔、流程順暢、回饋即時
✅ 持續迭代改進 - 收集使用資料,不斷優化系統
這個專案若能成功,將可能產生以下影響:
🌱 教育普及 - 讓偏鄉或缺乏台語環境的學生也能學習
👨👩👧👦 世代傳承 - 協助年輕世代重新連結母語
🏫 教師增能 - 減輕教師負擔,提供教學輔助工具
🔬 學術貢獻 - 累積台語語音資料,促進研究發展
🌍 語言保存 - 為台語數位化保存盡一份心力
感謝您對本土語言教育的關注與投入!
這是一個有意義且具挑戰性的專案,需要耐心與毅力。遇到困難時,請記住:
"一枝草,一點露,天無絕人之路"
每一步努力都在為台語的未來鋪路。
讓我們一起用科技的力量,為台語學習開創新的可能!
專案資訊
參與貢獻
歡迎任何形式的貢獻:
- 🐛 回報問題
- 💡 功能建議
- 📝 文稿貢獻
- 🔧 程式碼改進
- 📖 文件優化
預祝專案成功! 🎉
本計劃案提供完整的開發藍圖,從技術選型、功能設計到實作步驟均有詳細規劃。實際執行時可根據資源狀況與遇到的問題靈活調整,保持敏捷開發的彈性。