dramaling-vocab-learning/frontend/lib/services/speechAssessment.ts

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
}