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 { 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 { 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 { 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 }