110 lines
3.3 KiB
Dart
110 lines
3.3 KiB
Dart
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<bool> 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<void> 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<void> 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<void>.delayed(const Duration(milliseconds: 300));
|
|
_onResult = null;
|
|
_onStatusChanged?.call(false);
|
|
}
|
|
|
|
Future<void> 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<String?> _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;
|
|
}
|
|
}
|