import 'package:flutter/foundation.dart'; import 'package:speech_to_text/speech_recognition_error.dart'; import 'package:speech_to_text/speech_to_text.dart' as stt; typedef DictationResultCallback = void Function(String words, bool isFinalResult); typedef DictationStatusCallback = void Function(bool isListening); typedef DictationErrorCallback = void Function(String errorCode); class SpeechDictationService { SpeechDictationService._(); static final SpeechDictationService instance = SpeechDictationService._(); final stt.SpeechToText _speech = stt.SpeechToText(); DictationResultCallback? _onResult; DictationStatusCallback? _onStatusChanged; DictationErrorCallback? _onError; bool get isListening => _speech.isListening; Future initialize({ required DictationStatusCallback onStatusChanged, required DictationErrorCallback onError, }) async { _onStatusChanged = onStatusChanged; _onError = onError; if (_speech.isAvailable) return true; return _speech.initialize( onStatus: _handleStatus, onError: _handleError, debugLogging: kDebugMode, options: [stt.SpeechToText.androidNoBluetooth], ); } Future listen({required DictationResultCallback onResult}) async { _onResult = onResult; final localeId = await _preferredSpanishLocaleId(); await _speech.listen( onResult: (result) => _onResult?.call(result.recognizedWords, result.finalResult), listenOptions: stt.SpeechListenOptions( cancelOnError: true, partialResults: true, listenMode: stt.ListenMode.dictation, autoPunctuation: true, pauseFor: const Duration(seconds: 8), listenFor: const Duration(minutes: 2), localeId: localeId, ), ); } Future stop() async { await _speech.stop(); // Android puede entregar el resultado final poco después de informar que // dejó de escuchar. Esta breve espera permite conservar esas últimas // palabras antes de cerrar el receptor. await Future.delayed(const Duration(milliseconds: 300)); _onResult = null; _onStatusChanged?.call(false); } Future endSession() async { _onResult = null; _onStatusChanged = null; _onError = null; if (_speech.isAvailable) { await _speech.cancel(); } } void _handleStatus(String status) { if (status == stt.SpeechToText.listeningStatus) { _onStatusChanged?.call(true); } else if (status == stt.SpeechToText.doneStatus || status == stt.SpeechToText.notListeningStatus) { _onStatusChanged?.call(false); } } void _handleError(SpeechRecognitionError error) { _onResult = null; _onError?.call(error.errorMsg); _onStatusChanged?.call(false); } Future _preferredSpanishLocaleId() async { try { final locales = await _speech.locales(); for (final locale in locales) { final normalized = locale.localeId.toLowerCase().replaceAll('_', '-'); if (normalized == 'es-cl') return locale.localeId; } for (final locale in locales) { if (locale.localeId.toLowerCase().startsWith('es')) { return locale.localeId; } } } catch (_) { // El reconocedor usará el idioma predeterminado del dispositivo. } return null; } }