import 'dart:io'; import 'package:audioplayers/audioplayers.dart'; import 'package:flutter_tts/flutter_tts.dart'; import 'package:path_provider/path_provider.dart'; import 'package:record/record.dart'; import 'package:speech_to_text/speech_to_text.dart'; /// Device speech facilities are optional. Callers must keep a text fallback /// because availability depends on device language packs and permissions. class VoiceService { VoiceService._(); static final instance = VoiceService._(); final FlutterTts _tts = FlutterTts(); final SpeechToText _stt = SpeechToText(); final AudioRecorder _recorder = AudioRecorder(); final AudioPlayer _player = AudioPlayer(); bool _speechReady = false; bool _ttsInitialized = false; void Function(String status)? _statusListener; void Function(String error)? _errorListener; Future _initTts() async { if (_ttsInitialized) return; try { if (Platform.isIOS) { await _tts.setIosAudioCategory( IosTextToSpeechAudioCategory.playback, [ IosTextToSpeechAudioCategoryOptions.allowBluetooth, IosTextToSpeechAudioCategoryOptions.allowBluetoothA2DP, IosTextToSpeechAudioCategoryOptions.mixWithOthers, ], ); } await _tts.setVolume(1.0); await _tts.setPitch(1.0); _ttsInitialized = true; } catch (_) {} } Future speak(String text, {bool slow = false}) async { try { await _initTts(); await _tts.stop(); try { final isAvailable = await _tts.isLanguageAvailable('en-US'); if (isAvailable == true) { await _tts.setLanguage('en-US'); } else { await _tts.setLanguage('en'); } } catch (_) { try { await _tts.setLanguage('en-US'); } catch (_) {} } await _tts.setSpeechRate(slow ? 0.35 : 0.48); await _tts.speak(text); } catch (_) {} } Future stopSpeaking() async { try { await _tts.stop(); } catch (_) {} } Future hasRecordPermission() => _recorder.hasPermission(); Future startRecording() async { if (!await _recorder.hasPermission()) return false; final directory = await getApplicationDocumentsDirectory(); final recordings = Directory('${directory.path}/recordings'); if (!await recordings.exists()) await recordings.create(recursive: true); final timestamp = DateTime.now().microsecondsSinceEpoch; await _recorder.start( const RecordConfig(encoder: AudioEncoder.aacLc), path: '${recordings.path}/practice_$timestamp.m4a', ); return true; } Future stopRecording() => _recorder.stop(); Future playRecording(String path) async { await _player.stop(); await _player.play(DeviceFileSource(path)); } Future stopRecordingPlayback() => _player.stop(); Future deleteRecording(String? path) async { if (path == null || path.isEmpty) return; final file = File(path); if (await file.exists()) await file.delete(); } Future deleteAllRecordings() async { final directory = await getApplicationDocumentsDirectory(); final recordings = Directory('${directory.path}/recordings'); if (!await recordings.exists()) return 0; final files = await recordings .list() .where((item) => item is File) .toList(); for (final file in files.cast()) { await file.delete(); } return files.length; } Future> listRecordingPaths() async { final directory = await getApplicationDocumentsDirectory(); final recordings = Directory('${directory.path}/recordings'); if (!await recordings.exists()) return const []; final files = await recordings .list() .where((item) => item is File && item.path.endsWith('.m4a')) .cast() .toList(); files.sort((left, right) => right.path.compareTo(left.path)); return files.map((file) => file.path).toList(); } Future disposeRecording() async { await _recorder.dispose(); await _player.dispose(); } Future initializeSpeech({ void Function(String status)? onStatus, void Function(String error)? onError, }) async { _statusListener = onStatus; _errorListener = onError; try { _speechReady = await _stt.initialize( onError: (val) { _errorListener?.call(val.errorMsg); }, onStatus: (val) { _statusListener?.call(val); }, debugLogging: false, ); return _speechReady; } catch (_) { _speechReady = false; return false; } } Future startListening( void Function(String text, bool finalResult) onResult, { void Function(String status)? onStatus, void Function(String error)? onError, }) async { _statusListener = onStatus; _errorListener = onError; try { if (!_speechReady || !_stt.isAvailable) { final ready = await initializeSpeech(onStatus: onStatus, onError: onError); if (!ready) return false; } String? targetLocaleId = 'en_US'; try { final locales = await _stt.locales(); if (locales.isNotEmpty) { final enLocale = locales.firstWhere( (l) => l.localeId.toLowerCase().startsWith('en'), orElse: () => locales.first, ); targetLocaleId = enLocale.localeId; } } catch (_) {} await _stt.listen( onResult: (result) => onResult(result.recognizedWords, result.finalResult), listenOptions: SpeechListenOptions( localeId: targetLocaleId, listenFor: const Duration(seconds: 30), pauseFor: const Duration(seconds: 4), partialResults: true, cancelOnError: false, ), ); return _stt.isListening; } catch (e) { if (onError != null) onError(e.toString()); return false; } } Future stopListening() => _stt.stop(); bool get isListening => _stt.isListening; }