做多語音的 AI 對話應用(韓語、日語、中文都要顧到)時,常碰到「AI 聽錯字」「語音辨識吃掉句尾」或「日韓語特有發音切不乾淨」的問題。大部分時候問題不在錄音程式本身,而是聲學參數和模型 、語系調校沒對上。
這篇來看韓語和日語在聲學辨識上到底差在哪,並動手做一個能依語言動態調校的多語言音訊服務
把 Audio Chunk 丟給 Whisper 或其他 STT 引擎之前,不同語言對音訊參數的敏感度差很多。
日語的促音(っ)和長音很吃時間長度,切片切太短,比如 50ms,促音或長音會被硬生生截斷,Whisper 就直接辨識成別的字了。把 Chunk 切片拉到 150ms ~ 250ms 比較穩,採樣率維持 16kHz 16-bit PCM 。
另一個坑是無聲化,です(desu)、ます(masu)這種結尾的 u 音,日本人講話常常發得很輕,甚至幾乎沒聲音,麥克風的靜音偵測如果調太敏感,會把這種輕聲句尾當成靜音,直接切掉。
韓語文字和實際發音差很多,例如 한국어 唸出來其實是 한구거。如果照字面單獨切割,Whisper 很容易產生幻覺,辨識出完全不相關的內容。這種情況可以靠 Whisper API 的 prompt 參數塞入韓文常見語境或標準敬語(像 안녕하세 以糾錯。
還有一個容易忽略的地方,韓語的 ㄱ/ㄲ/ㅋ(k/kk/kh)在高頻區段的聲波特徵其實不一樣。音訊如果壓縮過頭、採樣率降太低,這些高頻細節會先消失,辨識率也會跟著掉下來。
(audio_config.dart)
建立 lib/models/audio_config.dart,針對各語言配置最佳的音訊與 Prompt 參數
enum SupportedLanguage {
zh('zh', '中文 (Mandarin)', '以下是中文對話。'),
ja('ja', '日本語 (Japanese)', '以下は日本語の会話です。です、ます、よろしくお願いいたします。'),
ko('ko', '한국어 (Korean)', '다음은 한국어 대화입니다. 안녕하세요, 감사합니다, ~습니다.');
final String code;
final String label;
final String initialPrompt; // 用於提示 Whisper 模型的語境引導
const SupportedLanguage(this.code, this.label, this.initialPrompt);
}
class LanguageAudioConfig {
final SupportedLanguage language;
final int sampleRate; // 採樣率 (Hz)
final int chunkIntervalMs; // 切片間隔 (毫秒)
LanguageAudioConfig({
required this.language,
this.sampleRate = 16000,
required this.chunkIntervalMs,
});
// 取得特定語言的最佳優化設定檔
factory LanguageAudioConfig.forLanguage(SupportedLanguage lang) {
switch (lang) {
case SupportedLanguage.ja:
return LanguageAudioConfig(
language: lang,
sampleRate: 16000,
chunkIntervalMs: 200, // 日語:適度放寬切片時間,保護促音與長音
);
case SupportedLanguage.ko:
return LanguageAudioConfig(
language: lang,
sampleRate: 16000,
chunkIntervalMs: 150, // 韓語:標準切片,主要依賴 initialPrompt 糾錯
);
case SupportedLanguage.zh:
default:
return LanguageAudioConfig(
language: lang,
sampleRate: 16000,
chunkIntervalMs: 100, // 中文:低延遲 100ms
);
}
}
}
whisper_service.dart)更新 Whisper REST API 呼叫邏輯,動態注入語言代碼 (language) 與語境提示 (prompt)
import 'dart:io';
import 'package:http/http.dart' as http;
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'dart:convert';
import '../models/audio_config.dart';
class MultiLangWhisperService {
final String _baseUrl = 'https://api.openai.com/v1/audio/transcriptions';
Future<String?> transcribe({
required File audioFile,
required SupportedLanguage config,
}) async {
final apiKey = dotenv.env['OPENAI_API_KEY'];
if (apiKey == null || apiKey.isEmpty) return null;
try {
var request = http.MultipartRequest('POST', Uri.parse(_baseUrl));
request.headers['Authorization'] = 'Bearer $apiKey';
request.fields['model'] = 'whisper-1';
// 動態傳入語言代碼(如 'ja', 'ko', 'zh')
request.fields['language'] = config.code;
// 傳入引導 Prompt,大幅提升日韓語辨識準確率與標點符號發布
request.fields['prompt'] = config.initialPrompt;
request.files.add(await http.MultipartFile.fromPath('file', audioFile.path));
var streamedResponse = await request.send();
var response = await http.Response.fromStream(streamedResponse);
if (response.statusCode == 200) {
var jsonResponse = jsonDecode(utf8.decode(response.bodyBytes));
return jsonResponse['text'];
} else {
print('辨識失敗: ${response.body}');
return null;
}
} catch (e) {
print('錯誤: $error');
return null;
}
}
}
multi_lang_page.dart)打造一個可以讓使用者切換語言(日文 / 韓文 / 中文),並帶入對應聲学與模型設定的介面
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
import 'models/audio_config.dart';
import 'services/audio_recorder_service.dart';
import 'services/whisper_service.dart';
class MultiLangAudioPage extends StatefulWidget {
const MultiLangAudioPage({super.key});
@override
State<MultiLangAudioPage> createState() => _MultiLangAudioPageState();
}
class _MultiLangAudioPageState extends State<MultiLangAudioPage> {
final AudioRecorderService _recorder = AudioRecorderService();
final MultiLangWhisperService _whisper = MultiLangWhisperService();
SupportedLanguage _selectedLang = SupportedLanguage.ja; // 預設測試日語
bool _isRecording = false;
bool _isLoading = false;
String _resultText = '';
String? _audioPath;
void _toggleRecording() async {
if (_isRecording) {
await _recorder.stopRecording();
setState(() {
_isRecording = false;
_isLoading = true;
});
if (_audioPath != null) {
// 發送並帶入選定的語言調校設定
final text = await _whisper.transcribe(
audioFile: File(_audioPath!),
config: _selectedLang,
);
setState(() {
_resultText = text ?? '辨識失敗';
_isLoading = false;
});
}
} else {
final tempDir = await getTemporaryDirectory();
_audioPath = '${tempDir.path}/test_${_selectedLang.code}.m4a';
await _recorder.startFileRecording(_audioPath!);
setState(() {
_isRecording = true;
_resultText = '';
});
}
}
@override
Widget build(BuildContext context) {
final currentConfig = LanguageAudioConfig.forLanguage(_selectedLang);
return Scaffold(
appBar: AppBar(title: const Text('Day 12 - 多國語音聲學調校')),
body: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
children: [
// 語言切換選單
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('選擇目標語言:', style: TextStyle(fontSize: 16)),
DropdownButton<SupportedLanguage>(
value: _selectedLang,
items: SupportedLanguage.values.map((lang) {
return DropdownMenuItem(
value: lang,
child: Text(lang.label),
);
}).toList(),
onChanged: (val) {
if (val != null) setState(() => _selectedLang = val);
},
),
],
),
const SizedBox(height: 10),
// 顯示該語言的聲學調校參數
Card(
color: Colors.blueGrey[50],
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
children: [
Text('聲學配置: SampleRate ${currentConfig.sampleRate}Hz | Chunk: ${currentConfig.chunkIntervalMs}ms'),
const SizedBox(height: 5),
Text('Prompt 補全: "${currentConfig.language.initialPrompt}"', style: const TextStyle(fontSize: 12, color: Colors.grey)),
],
),
),
),
const SizedBox(height: 30),
// 錄音按鈕
ElevatedButton.icon(
onPressed: _isLoading ? null : _toggleRecording,
icon: Icon(_isRecording ? Icons.stop : Icons.mic),
label: Text(_isRecording ? '停止並分析' : '開始測試發音 (${_selectedLang.code.toUpperCase()})'),
style: ElevatedButton.styleFrom(
backgroundColor: _isRecording ? Colors.red : Colors.indigo,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
),
),
const SizedBox(height: 30),
if (_isLoading) const CircularProgressIndicator(),
if (_resultText.isNotEmpty) ...[
const Text('辨識結果:', style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 10),
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(color: Colors.grey[200], borderRadius: BorderRadius.circular(8)),
child: Text(_resultText, style: const TextStyle(fontSize: 18)),
),
]
],
),
),
);
}
}
こんにちは、初めまして。どうぞよろしくお願いします。
です/ます 句尾與連音。안녕하세요. 반갑습니다.