192 lines
5.4 KiB
TypeScript
192 lines
5.4 KiB
TypeScript
import { API_CONFIG } from '@/lib/config/api'
|
|
|
|
export interface PronunciationScores {
|
|
overall: number
|
|
accuracy: number
|
|
fluency: number
|
|
completeness: number
|
|
prosody: number
|
|
}
|
|
|
|
export interface WordLevelResult {
|
|
word: string
|
|
accuracyScore: number
|
|
errorType: string
|
|
suggestion?: string
|
|
}
|
|
|
|
export interface PronunciationResult {
|
|
assessmentId: string
|
|
flashcardId: string
|
|
referenceText: string
|
|
transcribedText: string
|
|
scores: PronunciationScores
|
|
wordLevelResults: WordLevelResult[]
|
|
feedback: string[]
|
|
confidenceLevel: number
|
|
processingTime: number
|
|
createdAt: string
|
|
}
|
|
|
|
export interface SpeechAssessmentResponse {
|
|
success: boolean
|
|
data?: PronunciationResult
|
|
error?: string
|
|
message?: string
|
|
}
|
|
|
|
export interface SpeechServiceStatus {
|
|
isAvailable: boolean
|
|
serviceName: string
|
|
checkTime: string
|
|
message: string
|
|
}
|
|
|
|
class SpeechAssessmentService {
|
|
private readonly baseUrl: string
|
|
|
|
constructor() {
|
|
this.baseUrl = API_CONFIG.BASE_URL
|
|
}
|
|
|
|
async evaluatePronunciation(
|
|
audioBlob: Blob,
|
|
referenceText: string,
|
|
flashcardId: string,
|
|
language: string = 'en-US'
|
|
): Promise<SpeechAssessmentResponse> {
|
|
try {
|
|
// 準備 FormData
|
|
const formData = new FormData()
|
|
formData.append('audio', audioBlob, 'recording.webm')
|
|
formData.append('referenceText', referenceText)
|
|
formData.append('flashcardId', flashcardId)
|
|
formData.append('language', language)
|
|
|
|
console.log('🎤 發送發音評估請求:', {
|
|
flashcardId,
|
|
referenceText: referenceText.substring(0, 50) + '...',
|
|
audioSize: `${(audioBlob.size / 1024).toFixed(1)} KB`,
|
|
language
|
|
})
|
|
|
|
const response = await fetch(`${this.baseUrl}/api/speech/pronunciation-assessment`, {
|
|
method: 'POST',
|
|
body: formData
|
|
})
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json().catch(() => ({}))
|
|
throw new Error(errorData.message || `HTTP ${response.status}: ${response.statusText}`)
|
|
}
|
|
|
|
const result: SpeechAssessmentResponse = await response.json()
|
|
|
|
if (!result.success) {
|
|
throw new Error(result.message || result.error || '發音評估失敗')
|
|
}
|
|
|
|
console.log('✅ 發音評估完成:', {
|
|
overallScore: result.data?.scores.overall,
|
|
confidenceLevel: result.data?.confidenceLevel,
|
|
processingTime: `${result.data?.processingTime}ms`
|
|
})
|
|
|
|
return result
|
|
|
|
} catch (error) {
|
|
console.error('❌ 發音評估錯誤:', error)
|
|
|
|
const errorMessage = error instanceof Error ? error.message : '發音評估失敗'
|
|
|
|
return {
|
|
success: false,
|
|
error: errorMessage,
|
|
message: this.getErrorMessage(errorMessage)
|
|
}
|
|
}
|
|
}
|
|
|
|
async checkServiceStatus(): Promise<SpeechServiceStatus> {
|
|
try {
|
|
const response = await fetch(`${this.baseUrl}/api/speech/service-status`)
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
|
}
|
|
|
|
const result = await response.json()
|
|
|
|
return result.success ? result.data : {
|
|
isAvailable: false,
|
|
serviceName: 'Azure Speech Services',
|
|
checkTime: new Date().toISOString(),
|
|
message: '服務狀態檢查失敗'
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('語音服務狀態檢查失敗:', error)
|
|
|
|
return {
|
|
isAvailable: false,
|
|
serviceName: 'Azure Speech Services',
|
|
checkTime: new Date().toISOString(),
|
|
message: error instanceof Error ? error.message : '無法連接到語音服務'
|
|
}
|
|
}
|
|
}
|
|
|
|
private getErrorMessage(error: string): string {
|
|
// 將技術錯誤訊息轉換為用戶友善的中文訊息
|
|
if (error.includes('AUDIO_TOO_SHORT')) {
|
|
return '錄音時間太短,請至少錄製 1 秒鐘'
|
|
}
|
|
if (error.includes('AUDIO_TOO_LARGE')) {
|
|
return '音頻檔案過大,請縮短錄音時間'
|
|
}
|
|
if (error.includes('INVALID_AUDIO_FORMAT')) {
|
|
return '音頻格式不支援,請重新錄製'
|
|
}
|
|
if (error.includes('NO_SPEECH_DETECTED')) {
|
|
return '未檢測到語音,請確保麥克風正常並大聲說話'
|
|
}
|
|
if (error.includes('SPEECH_SERVICE_ERROR')) {
|
|
return '語音識別服務暫時不可用,請稍後再試'
|
|
}
|
|
if (error.includes('NetworkError') || error.includes('fetch')) {
|
|
return '網路連接錯誤,請檢查網路連接'
|
|
}
|
|
|
|
return '發音評估過程中發生錯誤,請稍後再試'
|
|
}
|
|
|
|
// 輔助方法:檢查瀏覽器是否支援錄音
|
|
isRecordingSupported(): boolean {
|
|
return !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia)
|
|
}
|
|
|
|
// 輔助方法:請求麥克風權限
|
|
async requestMicrophonePermission(): Promise<boolean> {
|
|
try {
|
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
|
|
// 立即停止,只是檢查權限
|
|
stream.getTracks().forEach(track => track.stop())
|
|
return true
|
|
} catch (error) {
|
|
console.warn('麥克風權限請求失敗:', error)
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
// 導出單例實例
|
|
export const speechAssessmentService = new SpeechAssessmentService()
|
|
|
|
// 導出類型供其他組件使用
|
|
export type {
|
|
PronunciationResult,
|
|
PronunciationScores,
|
|
WordLevelResult,
|
|
SpeechAssessmentResponse,
|
|
SpeechServiceStatus
|
|
} |