merge changes-3
parent
c07a8f54c7
commit
889ae24f07
Binary file not shown.
|
Before Width: | Height: | Size: 147 KiB After Width: | Height: | Size: 23 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
@ -0,0 +1,196 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:nfc_in_flutter/nfc_in_flutter.dart';
|
||||
|
||||
void showNfcReader(BuildContext context, {Function onNcfScan}) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
enableDrag: false,
|
||||
isDismissible: false,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(12), topRight: Radius.circular(12)),
|
||||
),
|
||||
backgroundColor: Colors.white,
|
||||
builder: (context) {
|
||||
return NfcLayout(
|
||||
onNcfScan: onNcfScan,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
class NfcLayout extends StatefulWidget {
|
||||
Function onNcfScan;
|
||||
|
||||
NfcLayout({this.onNcfScan});
|
||||
|
||||
@override
|
||||
_NfcLayoutState createState() => _NfcLayoutState();
|
||||
}
|
||||
|
||||
class _NfcLayoutState extends State<NfcLayout> {
|
||||
StreamSubscription<NDEFMessage> _stream;
|
||||
bool _reading = false;
|
||||
Widget mainWidget;
|
||||
String nfcId;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
setState(() {
|
||||
// _reading = true;
|
||||
// Start reading using NFC.readNDEF()
|
||||
_stream = NFC
|
||||
.readNDEF(
|
||||
once: false,
|
||||
throwOnUserCancel: false,
|
||||
readerMode: NFCDispatchReaderMode())
|
||||
.listen((NDEFMessage message) {
|
||||
setState(() {
|
||||
_reading = true;
|
||||
mainWidget = doneNfc();
|
||||
});
|
||||
Future.delayed(const Duration(milliseconds: 500), () {
|
||||
_stream?.cancel();
|
||||
widget.onNcfScan(nfcId);
|
||||
Navigator.pop(context);
|
||||
});
|
||||
print("read NDEF id: ${message.id}");
|
||||
// widget.onNcfScan(message.id);
|
||||
nfcId = message.id;
|
||||
}, onError: (e) {
|
||||
// Check error handling guide below
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
(mainWidget == null && !_reading)
|
||||
? mainWidget = scanNfc()
|
||||
: mainWidget = doneNfc();
|
||||
return AnimatedSwitcher(
|
||||
duration: Duration(milliseconds: 500), child: mainWidget);
|
||||
}
|
||||
|
||||
Widget scanNfc() {
|
||||
return Container(
|
||||
key: ValueKey(1),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
SizedBox(
|
||||
height: 30,
|
||||
),
|
||||
Text(
|
||||
"Ready To Scan",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 24,
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 30,
|
||||
),
|
||||
Image.asset(
|
||||
"assets/images/nfc/ic_nfc.png",
|
||||
height: MediaQuery.of(context).size.width / 3,
|
||||
),
|
||||
SizedBox(
|
||||
height: 30,
|
||||
),
|
||||
Text(
|
||||
"Approach an NFC Tag",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 30,
|
||||
),
|
||||
ButtonTheme(
|
||||
minWidth: MediaQuery.of(context).size.width / 1.2,
|
||||
height: 45.0,
|
||||
buttonColor: Colors.grey[300],
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: RaisedButton(
|
||||
onPressed: () {
|
||||
_stream?.cancel();
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Text("CANCEL"),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 30,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget doneNfc() {
|
||||
return Container(
|
||||
key: ValueKey(2),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
SizedBox(
|
||||
height: 30,
|
||||
),
|
||||
Text(
|
||||
"Successfully Scanned",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 24,
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 30,
|
||||
),
|
||||
Image.asset(
|
||||
"assets/images/nfc/ic_done.png",
|
||||
height: MediaQuery.of(context).size.width / 3,
|
||||
),
|
||||
SizedBox(
|
||||
height: 30,
|
||||
),
|
||||
Text(
|
||||
"Approach an NFC Tag",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 30,
|
||||
),
|
||||
ButtonTheme(
|
||||
minWidth: MediaQuery.of(context).size.width / 1.2,
|
||||
height: 45.0,
|
||||
buttonColor: Colors.grey[300],
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: RaisedButton(
|
||||
// onPressed: () {
|
||||
// _stream?.cancel();
|
||||
// widget.onNcfScan(nfcId);
|
||||
// Navigator.pop(context);
|
||||
// },
|
||||
onPressed: null,
|
||||
|
||||
child: Text("DONE"),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 30,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,33 @@
|
||||
name: speech_to_text_example
|
||||
description: Demonstrates how to use the speech_to_text plugin.
|
||||
version: 1.1.0
|
||||
publish_to: 'none'
|
||||
|
||||
environment:
|
||||
sdk: ">=2.1.0 <3.0.0"
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
|
||||
cupertino_icons: ^0.1.2
|
||||
permission_handler: ^5.0.1+1
|
||||
|
||||
provider:
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
||||
speech_to_text:
|
||||
path: ../
|
||||
|
||||
# The following section is specific to Flutter.
|
||||
flutter:
|
||||
|
||||
uses-material-design: true
|
||||
|
||||
assets:
|
||||
- assets/sounds/speech_to_text_listening.m4r
|
||||
- assets/sounds/speech_to_text_cancel.m4r
|
||||
- assets/sounds/speech_to_text_stop.m4r
|
||||
@ -0,0 +1,44 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'speech_recognition_error.g.dart';
|
||||
|
||||
/// A single error returned from the underlying speech services.
|
||||
///
|
||||
/// Errors are either transient or permanent. Permanent errors
|
||||
/// block speech recognition from continuing and must be
|
||||
/// addressed before recogntion will work. Transient errors
|
||||
/// cause individual recognition sessions to fail but subsequent
|
||||
/// attempts may well succeed.
|
||||
@JsonSerializable()
|
||||
class SpeechRecognitionError {
|
||||
/// Use this to differentiate the various error conditions.
|
||||
///
|
||||
/// Not meant for display to the user.
|
||||
final String errorMsg;
|
||||
|
||||
/// True means that recognition cannot continue until
|
||||
/// the error is resolved.
|
||||
final bool permanent;
|
||||
|
||||
SpeechRecognitionError(this.errorMsg, this.permanent);
|
||||
|
||||
factory SpeechRecognitionError.fromJson(Map<String, dynamic> json) =>
|
||||
_$SpeechRecognitionErrorFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$SpeechRecognitionErrorToJson(this);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return "SpeechRecognitionError msg: $errorMsg, permanent: $permanent";
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
other is SpeechRecognitionError &&
|
||||
errorMsg == other.errorMsg &&
|
||||
permanent == other.permanent;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => errorMsg.hashCode;
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'speech_recognition_error.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
SpeechRecognitionError _$SpeechRecognitionErrorFromJson(
|
||||
Map<String, dynamic> json) {
|
||||
return SpeechRecognitionError(
|
||||
json['errorMsg'] as String,
|
||||
json['permanent'] as bool,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$SpeechRecognitionErrorToJson(
|
||||
SpeechRecognitionError instance) =>
|
||||
<String, dynamic>{
|
||||
'errorMsg': instance.errorMsg,
|
||||
'permanent': instance.permanent,
|
||||
};
|
||||
@ -0,0 +1,30 @@
|
||||
import 'package:speech_to_text/speech_recognition_error.dart';
|
||||
import 'package:speech_to_text/speech_recognition_result.dart';
|
||||
|
||||
enum SpeechRecognitionEventType {
|
||||
finalRecognitionEvent,
|
||||
partialRecognitionEvent,
|
||||
errorEvent,
|
||||
statusChangeEvent,
|
||||
soundLevelChangeEvent,
|
||||
}
|
||||
|
||||
/// A single event in a stream of speech recognition events.
|
||||
///
|
||||
/// Use [eventType] to determine what type of event it is and depending on that
|
||||
/// use the other properties to get information about it.
|
||||
class SpeechRecognitionEvent {
|
||||
final SpeechRecognitionEventType eventType;
|
||||
final SpeechRecognitionError _error;
|
||||
final SpeechRecognitionResult _result;
|
||||
final bool _listening;
|
||||
final double _level;
|
||||
|
||||
SpeechRecognitionEvent(
|
||||
this.eventType, this._result, this._error, this._listening, this._level);
|
||||
|
||||
bool get isListening => _listening;
|
||||
double get level => _level;
|
||||
SpeechRecognitionResult get recognitionResult => _result;
|
||||
SpeechRecognitionError get error => _error;
|
||||
}
|
||||
@ -0,0 +1,140 @@
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'speech_recognition_result.g.dart';
|
||||
|
||||
/// A sequence of recognized words from the speech recognition
|
||||
/// service.
|
||||
///
|
||||
/// Depending on the platform behaviour the words may come in all
|
||||
/// at once at the end or as partial results as each word is
|
||||
/// recognized. Use the [finalResult] flag to determine if the
|
||||
/// result is considered final by the platform.
|
||||
@JsonSerializable(explicitToJson: true)
|
||||
class SpeechRecognitionResult {
|
||||
List<SpeechRecognitionWords> _alternates;
|
||||
|
||||
/// Returns a list of possible transcriptions of the speech.
|
||||
///
|
||||
/// The first value is always the same as the [recognizedWords]
|
||||
/// value. Use the confidence for each alternate transcription
|
||||
/// to determine how likely it is. Note that not all platforms
|
||||
/// do a good job with confidence, there are convenience methods
|
||||
/// on [SpeechRecogntionWords] to work with possibly missing
|
||||
/// confidence values.
|
||||
List<SpeechRecognitionWords> get alternates =>
|
||||
UnmodifiableListView(_alternates);
|
||||
|
||||
/// The sequence of words that is the best transcription of
|
||||
/// what was said.
|
||||
///
|
||||
/// This is the same as the first value of [alternates].
|
||||
String get recognizedWords =>
|
||||
_alternates.isNotEmpty ? _alternates.first.recognizedWords : "";
|
||||
|
||||
/// False means the words are an interim result, true means
|
||||
/// they are the final recognition.
|
||||
final bool finalResult;
|
||||
|
||||
/// The confidence that the [recognizedWords] are correct.
|
||||
///
|
||||
/// Confidence is expressed as a value between 0 and 1. -1
|
||||
/// means that the confidence value was not available.
|
||||
double get confidence =>
|
||||
_alternates.isNotEmpty ? _alternates.first.confidence : 0;
|
||||
|
||||
/// true if there is confidence in this recognition, false otherwise.
|
||||
///
|
||||
/// There are two separate ways for there to be confidence, the first
|
||||
/// is if the confidence is missing, which is indicated by a value of
|
||||
/// -1. The second is if the confidence is greater than or equal
|
||||
/// [threshold]. If [threshold] is not provided it defaults to 0.8.
|
||||
bool isConfident(
|
||||
{double threshold = SpeechRecognitionWords.confidenceThreshold}) =>
|
||||
_alternates.isNotEmpty
|
||||
? _alternates.first.isConfident(threshold: threshold)
|
||||
: false;
|
||||
|
||||
/// true if [confidence] is not the [missingConfidence] value, false
|
||||
/// otherwise.
|
||||
bool get hasConfidenceRating =>
|
||||
_alternates.isNotEmpty ? _alternates.first.hasConfidenceRating : false;
|
||||
|
||||
SpeechRecognitionResult(this._alternates, this.finalResult);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return "SpeechRecognitionResult words: $_alternates, final: $finalResult";
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
other is SpeechRecognitionResult &&
|
||||
recognizedWords == other.recognizedWords &&
|
||||
finalResult == other.finalResult;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => recognizedWords.hashCode;
|
||||
|
||||
factory SpeechRecognitionResult.fromJson(Map<String, dynamic> json) =>
|
||||
_$SpeechRecognitionResultFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$SpeechRecognitionResultToJson(this);
|
||||
}
|
||||
|
||||
/// A set of words recognized in a [SpeechRecognitionResult].
|
||||
///
|
||||
/// Each result will have one or more [SpeechRecognitionWords]
|
||||
/// with a varying degree of confidence about each set of words.
|
||||
@JsonSerializable()
|
||||
class SpeechRecognitionWords {
|
||||
/// The sequence of words recognized
|
||||
final String recognizedWords;
|
||||
|
||||
/// The confidence that the [recognizedWords] are correct.
|
||||
///
|
||||
/// Confidence is expressed as a value between 0 and 1. 0
|
||||
/// means that the confidence value was not available. Use
|
||||
/// [isConfident] which will ignore 0 values automatically.
|
||||
final double confidence;
|
||||
|
||||
static const double confidenceThreshold = 0.8;
|
||||
static const double missingConfidence = -1;
|
||||
|
||||
const SpeechRecognitionWords(this.recognizedWords, this.confidence);
|
||||
|
||||
/// true if there is confidence in this recognition, false otherwise.
|
||||
///
|
||||
/// There are two separate ways for there to be confidence, the first
|
||||
/// is if the confidence is missing, which is indicated by a value of
|
||||
/// -1. The second is if the confidence is greater than or equal
|
||||
/// [threshold]. If [threshold] is not provided it defaults to 0.8.
|
||||
bool isConfident({double threshold = confidenceThreshold}) =>
|
||||
confidence == missingConfidence || confidence >= threshold;
|
||||
|
||||
/// true if [confidence] is not the [missingConfidence] value, false
|
||||
/// otherwise.
|
||||
bool get hasConfidenceRating => confidence != missingConfidence;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return "SpeechRecognitionWords words: $recognizedWords, confidence: $confidence";
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
other is SpeechRecognitionWords &&
|
||||
recognizedWords == other.recognizedWords &&
|
||||
confidence == other.confidence;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => recognizedWords.hashCode;
|
||||
|
||||
factory SpeechRecognitionWords.fromJson(Map<String, dynamic> json) =>
|
||||
_$SpeechRecognitionWordsFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$SpeechRecognitionWordsToJson(this);
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'speech_recognition_result.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
SpeechRecognitionResult _$SpeechRecognitionResultFromJson(
|
||||
Map<String, dynamic> json) {
|
||||
return SpeechRecognitionResult(
|
||||
(json['alternates'] as List)
|
||||
?.map((e) => e == null
|
||||
? null
|
||||
: SpeechRecognitionWords.fromJson(e as Map<String, dynamic>))
|
||||
?.toList(),
|
||||
json['finalResult'] as bool,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$SpeechRecognitionResultToJson(
|
||||
SpeechRecognitionResult instance) =>
|
||||
<String, dynamic>{
|
||||
'alternates': instance.alternates?.map((e) => e?.toJson())?.toList(),
|
||||
'finalResult': instance.finalResult,
|
||||
};
|
||||
|
||||
SpeechRecognitionWords _$SpeechRecognitionWordsFromJson(
|
||||
Map<String, dynamic> json) {
|
||||
return SpeechRecognitionWords(
|
||||
json['recognizedWords'] as String,
|
||||
(json['confidence'] as num)?.toDouble(),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$SpeechRecognitionWordsToJson(
|
||||
SpeechRecognitionWords instance) =>
|
||||
<String, dynamic>{
|
||||
'recognizedWords': instance.recognizedWords,
|
||||
'confidence': instance.confidence,
|
||||
};
|
||||
@ -0,0 +1,511 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:clock/clock.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:speech_to_text/speech_recognition_error.dart';
|
||||
import 'package:speech_to_text/speech_recognition_result.dart';
|
||||
|
||||
enum ListenMode {
|
||||
deviceDefault,
|
||||
dictation,
|
||||
search,
|
||||
confirmation,
|
||||
}
|
||||
|
||||
/// Notified as words are recognized with the current set of recognized words.
|
||||
///
|
||||
/// See the [onResult] argument on the [listen] method for use.
|
||||
typedef SpeechResultListener = void Function(SpeechRecognitionResult result);
|
||||
|
||||
/// Notified if errors occur during recognition or intialization.
|
||||
///
|
||||
/// Possible errors per the Android docs are described here:
|
||||
/// https://developer.android.com/reference/android/speech/SpeechRecognizer
|
||||
/// "error_audio_error"
|
||||
/// "error_client"
|
||||
/// "error_permission"
|
||||
/// "error_network"
|
||||
/// "error_network_timeout"
|
||||
/// "error_no_match"
|
||||
/// "error_busy"
|
||||
/// "error_server"
|
||||
/// "error_speech_timeout"
|
||||
/// See the [onError] argument on the [initialize] method for use.
|
||||
typedef SpeechErrorListener = void Function(
|
||||
SpeechRecognitionError errorNotification);
|
||||
|
||||
/// Notified when recognition status changes.
|
||||
///
|
||||
/// See the [onStatus] argument on the [initialize] method for use.
|
||||
typedef SpeechStatusListener = void Function(String status);
|
||||
|
||||
/// Notified when the sound level changes during a listen method.
|
||||
///
|
||||
/// [level] is a measure of the decibels of the current sound on
|
||||
/// the recognition input. See the [onSoundLevelChange] argument on
|
||||
/// the [listen] method for use.
|
||||
typedef SpeechSoundLevelChange = Function(double level);
|
||||
|
||||
/// An interface to device specific speech recognition services.
|
||||
///
|
||||
/// The general flow of a speech recognition session is as follows:
|
||||
/// ```Dart
|
||||
/// SpeechToText speech = SpeechToText();
|
||||
/// bool isReady = await speech.initialize();
|
||||
/// if ( isReady ) {
|
||||
/// await speech.listen( resultListener: resultListener );
|
||||
/// }
|
||||
/// ...
|
||||
/// // At some point later
|
||||
/// speech.stop();
|
||||
/// ```
|
||||
class SpeechToText {
|
||||
static const String listenMethod = 'listen';
|
||||
static const String textRecognitionMethod = 'textRecognition';
|
||||
static const String notifyErrorMethod = 'notifyError';
|
||||
static const String notifyStatusMethod = 'notifyStatus';
|
||||
static const String soundLevelChangeMethod = "soundLevelChange";
|
||||
static const String notListeningStatus = "notListening";
|
||||
static const String listeningStatus = "listening";
|
||||
|
||||
static const MethodChannel speechChannel =
|
||||
const MethodChannel('plugin.csdcorp.com/speech_to_text');
|
||||
static final SpeechToText _instance =
|
||||
SpeechToText.withMethodChannel(speechChannel);
|
||||
bool _initWorked = false;
|
||||
bool _recognized = false;
|
||||
bool _listening = false;
|
||||
bool _cancelOnError = false;
|
||||
bool _partialResults = false;
|
||||
int _listenStartedAt = 0;
|
||||
int _lastSpeechEventAt = 0;
|
||||
Duration _pauseFor;
|
||||
Duration _listenFor;
|
||||
|
||||
/// True if not listening or the user called cancel / stop, false
|
||||
/// if cancel/stop were invoked by timeout or error condition.
|
||||
bool _userEnded = false;
|
||||
String _lastRecognized = "";
|
||||
String _lastStatus = "";
|
||||
double _lastSoundLevel = 0;
|
||||
Timer _listenTimer;
|
||||
LocaleName _systemLocale;
|
||||
SpeechRecognitionError _lastError;
|
||||
SpeechResultListener _resultListener;
|
||||
SpeechErrorListener errorListener;
|
||||
SpeechStatusListener statusListener;
|
||||
SpeechSoundLevelChange _soundLevelChange;
|
||||
|
||||
final MethodChannel channel;
|
||||
factory SpeechToText() => _instance;
|
||||
|
||||
@visibleForTesting
|
||||
SpeechToText.withMethodChannel(this.channel);
|
||||
|
||||
/// True if words have been recognized during the current [listen] call.
|
||||
///
|
||||
/// Goes false as soon as [cancel] is called.
|
||||
bool get hasRecognized => _recognized;
|
||||
|
||||
/// The last set of recognized words received.
|
||||
///
|
||||
/// This is maintained across [cancel] calls but cleared on the next
|
||||
/// [listen].
|
||||
String get lastRecognizedWords => _lastRecognized;
|
||||
|
||||
/// The last status update received, see [initialize] to register
|
||||
/// an optional listener to be notified when this changes.
|
||||
String get lastStatus => _lastStatus;
|
||||
|
||||
/// The last sound level received during a listen event.
|
||||
///
|
||||
/// The sound level is a measure of how loud the current
|
||||
/// input is during listening. Use the [onSoundLevelChange]
|
||||
/// argument in the [listen] method to get notified of
|
||||
/// changes.
|
||||
double get lastSoundLevel => _lastSoundLevel;
|
||||
|
||||
/// True if [initialize] succeeded
|
||||
bool get isAvailable => _initWorked;
|
||||
|
||||
/// True if [listen] succeeded and [stop] or [cancel] has not been called.
|
||||
///
|
||||
/// Also goes false when listening times out if listenFor was set.
|
||||
bool get isListening => _listening;
|
||||
bool get isNotListening => !isListening;
|
||||
|
||||
/// The last error received or null if none, see [initialize] to
|
||||
/// register an optional listener to be notified of errors.
|
||||
SpeechRecognitionError get lastError => _lastError;
|
||||
|
||||
/// True if an error has been received, see [lastError] for details
|
||||
bool get hasError => null != lastError;
|
||||
|
||||
/// Returns true if the user has already granted permission to access the
|
||||
/// microphone, does not prompt the user.
|
||||
///
|
||||
/// This method can be called before [initialize] to check if permission
|
||||
/// has already been granted. If this returns false then the [initialize]
|
||||
/// call will prompt the user for permission if it is allowed to do so.
|
||||
/// Note that applications cannot ask for permission again if the user has
|
||||
/// denied them permission in the past.
|
||||
Future<bool> get hasPermission async {
|
||||
bool hasPermission = await channel.invokeMethod('has_permission');
|
||||
return hasPermission;
|
||||
}
|
||||
|
||||
/// Initialize speech recognition services, returns true if
|
||||
/// successful, false if failed.
|
||||
///
|
||||
/// This method must be called before any other speech functions.
|
||||
/// If this method returns false no further [SpeechToText] methods
|
||||
/// should be used. Should only be called once if successful but does protect
|
||||
/// itself if called repeatedly. False usually means that the user has denied
|
||||
/// permission to use speech. The usual option in that case is to give them
|
||||
/// instructions on how to open system settings and grant permission.
|
||||
///
|
||||
/// [onError] is an optional listener for errors like
|
||||
/// timeout, or failure of the device speech recognition.
|
||||
/// [onStatus] is an optional listener for status changes from
|
||||
/// listening to not listening.
|
||||
/// [debugLogging] controls whether there is detailed logging from the underlying
|
||||
/// plugins. It is off by default, usually only useful for troubleshooting issues
|
||||
/// with a paritcular OS version or device, fairly verbose
|
||||
Future<bool> initialize(
|
||||
{SpeechErrorListener onError,
|
||||
SpeechStatusListener onStatus,
|
||||
debugLogging = false}) async {
|
||||
if (_initWorked) {
|
||||
return Future.value(_initWorked);
|
||||
}
|
||||
errorListener = onError;
|
||||
statusListener = onStatus;
|
||||
channel.setMethodCallHandler(_handleCallbacks);
|
||||
_initWorked = await channel
|
||||
.invokeMethod('initialize', {"debugLogging": debugLogging});
|
||||
return _initWorked;
|
||||
}
|
||||
|
||||
/// Stops the current listen for speech if active, does nothing if not.
|
||||
///
|
||||
/// Stopping a listen session will cause a final result to be sent. Each
|
||||
/// listen session should be ended with either [stop] or [cancel], for
|
||||
/// example in the dispose method of a Widget. [cancel] is automatically
|
||||
/// invoked by a permanent error if [cancelOnError] is set to true in the
|
||||
/// [listen] call.
|
||||
///
|
||||
/// *Note:* Cannot be used until a successful [initialize] call. Should
|
||||
/// only be used after a successful [listen] call.
|
||||
Future<void> stop() async {
|
||||
_userEnded = true;
|
||||
return _stop();
|
||||
}
|
||||
|
||||
Future<void> _stop() async {
|
||||
if (!_initWorked) {
|
||||
return;
|
||||
}
|
||||
_shutdownListener();
|
||||
await channel.invokeMethod('stop');
|
||||
}
|
||||
|
||||
/// Cancels the current listen for speech if active, does nothing if not.
|
||||
///
|
||||
/// Canceling means that there will be no final result returned from the
|
||||
/// recognizer. Each listen session should be ended with either [stop] or
|
||||
/// [cancel], for example in the dispose method of a Widget. [cancel] is
|
||||
/// automatically invoked by a permanent error if [cancelOnError] is set
|
||||
/// to true in the [listen] call.
|
||||
///
|
||||
/// *Note* Cannot be used until a successful [initialize] call. Should only
|
||||
/// be used after a successful [listen] call.
|
||||
Future<void> cancel() async {
|
||||
_userEnded = true;
|
||||
return _cancel();
|
||||
}
|
||||
|
||||
Future<void> _cancel() async {
|
||||
if (!_initWorked) {
|
||||
return;
|
||||
}
|
||||
_shutdownListener();
|
||||
await channel.invokeMethod('cancel');
|
||||
}
|
||||
|
||||
/// Starts a listening session for speech and converts it to text,
|
||||
/// invoking the provided [onResult] method as words are recognized.
|
||||
///
|
||||
/// Cannot be used until a successful [initialize] call. There is a
|
||||
/// time limit on listening imposed by both Android and iOS. The time
|
||||
/// depends on the device, network, etc. Android is usually quite short,
|
||||
/// especially if there is no active speech event detected, on the order
|
||||
/// of ten seconds or so.
|
||||
///
|
||||
/// When listening is done always invoke either [cancel] or [stop] to
|
||||
/// end the session, even if it times out. [cancelOnError] provides an
|
||||
/// automatic way to ensure this happens.
|
||||
///
|
||||
/// [onResult] is an optional listener that is notified when words
|
||||
/// are recognized.
|
||||
///
|
||||
/// [listenFor] sets the maximum duration that it will listen for, after
|
||||
/// that it automatically stops the listen for you.
|
||||
///
|
||||
/// [pauseFor] sets the maximum duration of a pause in speech with no words
|
||||
/// detected, after that it automatically stops the listen for you.
|
||||
///
|
||||
/// [localeId] is an optional locale that can be used to listen in a language
|
||||
/// other than the current system default. See [locales] to find the list of
|
||||
/// supported languages for listening.
|
||||
///
|
||||
/// [onSoundLevelChange] is an optional listener that is notified when the
|
||||
/// sound level of the input changes. Use this to update the UI in response to
|
||||
/// more or less input. The values currently differ between Ancroid and iOS,
|
||||
/// haven't yet been able to determine from the Android documentation what the
|
||||
/// value means. On iOS the value returned is in decibels.
|
||||
///
|
||||
/// [cancelOnError] if true then listening is automatically canceled on a
|
||||
/// permanent error. This defaults to false. When false cancel should be
|
||||
/// called from the error handler.
|
||||
///
|
||||
/// [partialResults] if true the listen reports results as they are recognized,
|
||||
/// when false only final results are reported. Defaults to true.
|
||||
///
|
||||
/// [onDevice] if true the listen attempts to recognize locally with speech never
|
||||
/// leaving the device. If it cannot do this the listen attempt will fail. This is
|
||||
/// usually only needed for sensitive content where privacy or security is a concern.
|
||||
Future listen(
|
||||
{SpeechResultListener onResult,
|
||||
Duration listenFor,
|
||||
Duration pauseFor,
|
||||
String localeId,
|
||||
SpeechSoundLevelChange onSoundLevelChange,
|
||||
cancelOnError = false,
|
||||
partialResults = true,
|
||||
onDevice = false,
|
||||
ListenMode listenMode = ListenMode.confirmation}) async {
|
||||
if (!_initWorked) {
|
||||
throw SpeechToTextNotInitializedException();
|
||||
}
|
||||
_userEnded = false;
|
||||
_cancelOnError = cancelOnError;
|
||||
_recognized = false;
|
||||
_resultListener = onResult;
|
||||
_soundLevelChange = onSoundLevelChange;
|
||||
_partialResults = partialResults;
|
||||
Map<String, dynamic> listenParams = {
|
||||
"partialResults": partialResults || null != pauseFor,
|
||||
"onDevice": onDevice,
|
||||
"listenMode": listenMode.index,
|
||||
};
|
||||
if (null != localeId) {
|
||||
listenParams["localeId"] = localeId;
|
||||
}
|
||||
try {
|
||||
bool started = await channel.invokeMethod(listenMethod, listenParams);
|
||||
if (started) {
|
||||
_listenStartedAt = clock.now().millisecondsSinceEpoch;
|
||||
_setupListenAndPause(pauseFor, listenFor);
|
||||
}
|
||||
} on PlatformException catch (e) {
|
||||
throw ListenFailedException(e.details);
|
||||
}
|
||||
}
|
||||
|
||||
void _setupListenAndPause(Duration pauseFor, Duration listenFor) {
|
||||
_pauseFor = null;
|
||||
_listenFor = null;
|
||||
if (null == pauseFor && null == listenFor) {
|
||||
return;
|
||||
}
|
||||
var minDuration;
|
||||
if (null == pauseFor) {
|
||||
_listenFor = Duration(milliseconds: listenFor.inMilliseconds);
|
||||
minDuration = listenFor;
|
||||
} else if (null == listenFor) {
|
||||
_pauseFor = Duration(milliseconds: pauseFor.inMilliseconds);
|
||||
minDuration = pauseFor;
|
||||
} else {
|
||||
_listenFor = Duration(milliseconds: listenFor.inMilliseconds);
|
||||
_pauseFor = Duration(milliseconds: pauseFor.inMilliseconds);
|
||||
var minMillis = min(listenFor.inMilliseconds - _elapsedListenMillis,
|
||||
pauseFor.inMilliseconds);
|
||||
minDuration = Duration(milliseconds: minMillis);
|
||||
}
|
||||
_listenTimer = Timer(minDuration, _stopOnPauseOrListen);
|
||||
}
|
||||
|
||||
int get _elapsedListenMillis =>
|
||||
clock.now().millisecondsSinceEpoch - _listenStartedAt;
|
||||
int get _elapsedSinceSpeechEvent =>
|
||||
clock.now().millisecondsSinceEpoch - _lastSpeechEventAt;
|
||||
|
||||
void _stopOnPauseOrListen() {
|
||||
if (null != _listenFor &&
|
||||
_elapsedListenMillis >= _listenFor.inMilliseconds) {
|
||||
_stop();
|
||||
} else if (null != _pauseFor &&
|
||||
_elapsedSinceSpeechEvent >= _pauseFor.inMilliseconds) {
|
||||
_stop();
|
||||
} else {
|
||||
_setupListenAndPause(_pauseFor, _listenFor);
|
||||
}
|
||||
}
|
||||
|
||||
/// returns the list of speech locales available on the device.
|
||||
///
|
||||
/// This method is useful to find the identifier to use
|
||||
/// for the [listen] method, it is the [localeId] member of the
|
||||
/// [LocaleName].
|
||||
///
|
||||
/// Each [LocaleName] in the returned list has the
|
||||
/// identifier for the locale as well as a name for
|
||||
/// display. The name is localized for the system locale on
|
||||
/// the device.
|
||||
Future<List<LocaleName>> locales() async {
|
||||
if (!_initWorked) {
|
||||
throw SpeechToTextNotInitializedException();
|
||||
}
|
||||
final List<dynamic> locales = await channel.invokeMethod('locales');
|
||||
List<LocaleName> filteredLocales = locales
|
||||
.map((locale) {
|
||||
var components = locale.split(":");
|
||||
if (components.length != 2) {
|
||||
return null;
|
||||
}
|
||||
return LocaleName(components[0], components[1]);
|
||||
})
|
||||
.where((item) => item != null)
|
||||
.toList();
|
||||
if (filteredLocales.isNotEmpty) {
|
||||
_systemLocale = filteredLocales.first;
|
||||
} else {
|
||||
_systemLocale = null;
|
||||
}
|
||||
filteredLocales.sort((ln1, ln2) => ln1.name.compareTo(ln2.name));
|
||||
return filteredLocales;
|
||||
}
|
||||
|
||||
/// returns the locale that will be used if no localeId is passed
|
||||
/// to the [listen] method.
|
||||
Future<LocaleName> systemLocale() async {
|
||||
if (null == _systemLocale) {
|
||||
await locales();
|
||||
}
|
||||
return Future.value(_systemLocale);
|
||||
}
|
||||
|
||||
Future _handleCallbacks(MethodCall call) async {
|
||||
// print("SpeechToText call: ${call.method} ${call.arguments}");
|
||||
switch (call.method) {
|
||||
case textRecognitionMethod:
|
||||
if (call.arguments is String) {
|
||||
_onTextRecognition(call.arguments);
|
||||
}
|
||||
break;
|
||||
case notifyErrorMethod:
|
||||
if (call.arguments is String) {
|
||||
await _onNotifyError(call.arguments);
|
||||
}
|
||||
break;
|
||||
case notifyStatusMethod:
|
||||
if (call.arguments is String) {
|
||||
_onNotifyStatus(call.arguments);
|
||||
}
|
||||
break;
|
||||
case soundLevelChangeMethod:
|
||||
if (call.arguments is double) {
|
||||
_onSoundLevelChange(call.arguments);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
void _onTextRecognition(String resultJson) {
|
||||
_lastSpeechEventAt = clock.now().millisecondsSinceEpoch;
|
||||
Map<String, dynamic> resultMap = jsonDecode(resultJson);
|
||||
SpeechRecognitionResult speechResult =
|
||||
SpeechRecognitionResult.fromJson(resultMap);
|
||||
if (!_partialResults && !speechResult.finalResult) {
|
||||
return;
|
||||
}
|
||||
_recognized = true;
|
||||
// print("Recognized text $resultJson");
|
||||
|
||||
_lastRecognized = speechResult.recognizedWords;
|
||||
if (null != _resultListener) {
|
||||
_resultListener(speechResult);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onNotifyError(String errorJson) async {
|
||||
if (isNotListening && _userEnded) {
|
||||
return;
|
||||
}
|
||||
Map<String, dynamic> errorMap = jsonDecode(errorJson);
|
||||
SpeechRecognitionError speechError =
|
||||
SpeechRecognitionError.fromJson(errorMap);
|
||||
_lastError = speechError;
|
||||
if (null != errorListener) {
|
||||
errorListener(speechError);
|
||||
}
|
||||
if (_cancelOnError && speechError.permanent) {
|
||||
await _cancel();
|
||||
}
|
||||
}
|
||||
|
||||
void _onNotifyStatus(String status) {
|
||||
_lastStatus = status;
|
||||
_listening = status == listeningStatus;
|
||||
// print(status);
|
||||
if (null != statusListener) {
|
||||
statusListener(status);
|
||||
}
|
||||
}
|
||||
|
||||
void _onSoundLevelChange(double level) {
|
||||
if (isNotListening) {
|
||||
return;
|
||||
}
|
||||
_lastSoundLevel = level;
|
||||
if (null != _soundLevelChange) {
|
||||
_soundLevelChange(level);
|
||||
}
|
||||
}
|
||||
|
||||
_shutdownListener() {
|
||||
_listening = false;
|
||||
_recognized = false;
|
||||
_listenTimer?.cancel();
|
||||
_listenTimer = null;
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
Future processMethodCall(MethodCall call) async {
|
||||
return await _handleCallbacks(call);
|
||||
}
|
||||
}
|
||||
|
||||
/// A single locale with a [name], localized to the current system locale,
|
||||
/// and a [localeId] which can be used in the [listen] method to choose a
|
||||
/// locale for speech recognition.
|
||||
class LocaleName {
|
||||
final String localeId;
|
||||
final String name;
|
||||
LocaleName(this.localeId, this.name);
|
||||
}
|
||||
|
||||
/// Thrown when a method is called that requires successful
|
||||
/// initialization first.
|
||||
class SpeechToTextNotInitializedException implements Exception {}
|
||||
|
||||
/// Thrown when listen fails to properly start a speech listening session
|
||||
/// on the device
|
||||
class ListenFailedException implements Exception {
|
||||
final String details;
|
||||
ListenFailedException(this.details);
|
||||
}
|
||||
@ -0,0 +1,201 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:speech_to_text/speech_recognition_error.dart';
|
||||
import 'package:speech_to_text/speech_recognition_event.dart';
|
||||
import 'package:speech_to_text/speech_recognition_result.dart';
|
||||
import 'package:speech_to_text/speech_to_text.dart';
|
||||
|
||||
/// Simplifies interaction with [SpeechToText] by handling all the callbacks and notifying
|
||||
/// listeners as events happen.
|
||||
///
|
||||
/// Here's an example of using the [SpeechToTextProvider]
|
||||
/// ```
|
||||
/// var speechProvider = SpeechToTextProvider( SpeechToText());
|
||||
/// var available = await speechProvider.initialize();
|
||||
/// StreamSubscription<SpeechRecognitionEvent> _subscription;
|
||||
/// _subscription = speechProvider.recognitionController.stream.listen((recognitionEvent) {
|
||||
/// if (recognitionEvent.eventType == SpeechRecognitionEventType.finalRecognitionEvent ) {
|
||||
/// print("I heard: ${recognitionEvent.recognitionResult.recognizedWords}");
|
||||
/// }
|
||||
/// });
|
||||
/// speechProvider.addListener(() {
|
||||
/// var words = speechProvider.lastWords;
|
||||
/// });
|
||||
class SpeechToTextProvider extends ChangeNotifier {
|
||||
final StreamController<SpeechRecognitionEvent> _recognitionController =
|
||||
StreamController.broadcast();
|
||||
final SpeechToText _speechToText;
|
||||
SpeechRecognitionResult _lastResult;
|
||||
double _lastLevel = 0;
|
||||
List<LocaleName> _locales = [];
|
||||
LocaleName _systemLocale;
|
||||
|
||||
/// Only construct one instance in an application.
|
||||
///
|
||||
/// Do not call `initialize` on the [SpeechToText] that is passed as a parameter, instead
|
||||
/// call the [initialize] method on this class.
|
||||
SpeechToTextProvider(this._speechToText);
|
||||
|
||||
Stream<SpeechRecognitionEvent> get stream => _recognitionController.stream;
|
||||
|
||||
/// Returns the last result received, may be null.
|
||||
SpeechRecognitionResult get lastResult => _lastResult;
|
||||
|
||||
/// Returns the last error received, may be null.
|
||||
SpeechRecognitionError get lastError => _speechToText.lastError;
|
||||
|
||||
/// Returns the last sound level received.
|
||||
///
|
||||
/// Note this is only available when the `soundLevel` is set to true on
|
||||
/// a call to [listen], will be 0 at all other times.
|
||||
double get lastLevel => _lastLevel;
|
||||
|
||||
/// Initializes the provider and the contained [SpeechToText] instance.
|
||||
///
|
||||
/// Returns true if [SpeechToText] was initialized successful and can now
|
||||
/// be used, false otherwse.
|
||||
Future<bool> initialize() async {
|
||||
if (isAvailable) {
|
||||
return isAvailable;
|
||||
}
|
||||
bool availableBefore = _speechToText.isAvailable;
|
||||
bool available =
|
||||
await _speechToText.initialize(onStatus: _onStatus, onError: _onError);
|
||||
if (available) {
|
||||
_locales = [];
|
||||
_locales.addAll(await _speechToText.locales());
|
||||
_systemLocale = await _speechToText.systemLocale();
|
||||
}
|
||||
if (availableBefore != available) {
|
||||
notifyListeners();
|
||||
}
|
||||
return available;
|
||||
}
|
||||
|
||||
/// Returns true if the provider has been initialized and can be used to recognize speech.
|
||||
bool get isAvailable => _speechToText.isAvailable;
|
||||
|
||||
/// Returns true if the provider cannot be used to recognize speech, either because it has not
|
||||
/// yet been initialized or because initialization failed.
|
||||
bool get isNotAvailable => !_speechToText.isAvailable;
|
||||
|
||||
/// Returns true if [SpeechToText] is listening for new speech.
|
||||
bool get isListening => _speechToText.isListening;
|
||||
|
||||
/// Returns true if [SpeechToText] is not listening for new speech.
|
||||
bool get isNotListening => _speechToText.isNotListening;
|
||||
|
||||
/// Returns true if [SpeechToText] has a previous error.
|
||||
bool get hasError => _speechToText.hasError;
|
||||
|
||||
/// Returns true if [lastResult] has a last result.
|
||||
bool get hasResults => null != _lastResult;
|
||||
|
||||
/// Returns the list of locales that are available on the device for speech recognition.
|
||||
List<LocaleName> get locales => _locales;
|
||||
|
||||
/// Returns the locale that is currently set as active on the device.
|
||||
LocaleName get systemLocale => _systemLocale;
|
||||
|
||||
/// Start listening for new events, set [partialResults] to true to receive interim
|
||||
/// recognition results.
|
||||
///
|
||||
/// [soundLevel] set to true to be notified on changes to the input sound level
|
||||
/// on the microphone.
|
||||
///
|
||||
/// [listenFor] sets the maximum duration that it will listen for, after
|
||||
/// that it automatically stops the listen for you.
|
||||
///
|
||||
/// [pauseFor] sets the maximum duration of a pause in speech with no words
|
||||
/// detected, after that it automatically stops the listen for you.
|
||||
///
|
||||
/// Call this only after a successful [initialize] call
|
||||
void listen(
|
||||
{bool partialResults = false,
|
||||
bool soundLevel = false,
|
||||
Duration listenFor,
|
||||
Duration pauseFor}) {
|
||||
_lastLevel = 0;
|
||||
_lastResult = null;
|
||||
if (soundLevel) {
|
||||
_speechToText.listen(
|
||||
partialResults: partialResults,
|
||||
listenFor: listenFor,
|
||||
pauseFor: pauseFor,
|
||||
cancelOnError: true,
|
||||
onResult: _onListenResult,
|
||||
// onSoundLevelChange: _onSoundLevelChange);
|
||||
);
|
||||
} else {
|
||||
_speechToText.listen(
|
||||
partialResults: partialResults,
|
||||
listenFor: listenFor,
|
||||
pauseFor: pauseFor,
|
||||
cancelOnError: true,
|
||||
onResult: _onListenResult);
|
||||
}
|
||||
}
|
||||
|
||||
/// Stops a current active listening session.
|
||||
///
|
||||
/// Call this after calling [listen] to stop the recognizer from listening further
|
||||
/// and return the current result as final.
|
||||
void stop() {
|
||||
_speechToText.stop();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Cancel a current active listening session.
|
||||
///
|
||||
/// Call this after calling [listen] to stop the recognizer from listening further
|
||||
/// and ignore any results recognized so far.
|
||||
void cancel() {
|
||||
_speechToText.cancel();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _onError(SpeechRecognitionError errorNotification) {
|
||||
_recognitionController.add(SpeechRecognitionEvent(
|
||||
SpeechRecognitionEventType.errorEvent,
|
||||
null,
|
||||
errorNotification,
|
||||
isListening,
|
||||
null));
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _onStatus(String status) {
|
||||
_recognitionController.add(SpeechRecognitionEvent(
|
||||
SpeechRecognitionEventType.statusChangeEvent,
|
||||
null,
|
||||
null,
|
||||
isListening,
|
||||
null));
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _onListenResult(SpeechRecognitionResult result) {
|
||||
_lastResult = result;
|
||||
_recognitionController.add(SpeechRecognitionEvent(
|
||||
result.finalResult
|
||||
? SpeechRecognitionEventType.finalRecognitionEvent
|
||||
: SpeechRecognitionEventType.partialRecognitionEvent,
|
||||
result,
|
||||
null,
|
||||
isListening,
|
||||
null));
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// void _onSoundLevelChange(double level) {
|
||||
// _lastLevel = level;
|
||||
// _recognitionController.add(SpeechRecognitionEvent(
|
||||
// SpeechRecognitionEventType.soundLevelChangeEvent,
|
||||
// null,
|
||||
// null,
|
||||
// null,
|
||||
// level));
|
||||
// notifyListeners();
|
||||
// }
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
name: speech_to_text
|
||||
description: A Flutter plugin that exposes device specific speech to text recognition capability.
|
||||
|
||||
|
||||
|
||||
environment:
|
||||
sdk: ">=2.1.0 <3.0.0"
|
||||
flutter: ">=1.10.0"
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
json_annotation: ^3.0.0
|
||||
clock: ^1.0.1
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
build_runner: ^1.0.0
|
||||
json_serializable: ^3.0.0
|
||||
fake_async: ^1.0.1
|
||||
|
||||
flutter:
|
||||
plugin:
|
||||
platforms:
|
||||
android:
|
||||
package: com.csdcorp.speech_to_text
|
||||
pluginClass: SpeechToTextPlugin
|
||||
ios:
|
||||
pluginClass: SpeechToTextPlugin
|
||||
|
||||
@ -0,0 +1,65 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:speech_to_text/speech_recognition_error.dart';
|
||||
|
||||
void main() {
|
||||
const String msg1 = "msg1";
|
||||
|
||||
setUp(() {});
|
||||
|
||||
group('properties', () {
|
||||
test('equals true for same object', () {
|
||||
SpeechRecognitionError error = SpeechRecognitionError(msg1, false);
|
||||
expect(error, error);
|
||||
});
|
||||
test('equals true for different object same values', () {
|
||||
SpeechRecognitionError error1 = SpeechRecognitionError(msg1, false);
|
||||
SpeechRecognitionError error2 = SpeechRecognitionError(msg1, false);
|
||||
expect(error1, error2);
|
||||
});
|
||||
test('equals false for different object', () {
|
||||
SpeechRecognitionError error1 = SpeechRecognitionError(msg1, false);
|
||||
SpeechRecognitionError error2 = SpeechRecognitionError("msg2", false);
|
||||
expect(error1, isNot(error2));
|
||||
});
|
||||
test('hash same for same object', () {
|
||||
SpeechRecognitionError error = SpeechRecognitionError(msg1, false);
|
||||
expect(error.hashCode, error.hashCode);
|
||||
});
|
||||
test('hash same for different object same values', () {
|
||||
SpeechRecognitionError error1 = SpeechRecognitionError(msg1, false);
|
||||
SpeechRecognitionError error2 = SpeechRecognitionError(msg1, false);
|
||||
expect(error1.hashCode, error2.hashCode);
|
||||
});
|
||||
test('hash different for different object', () {
|
||||
SpeechRecognitionError error1 = SpeechRecognitionError(msg1, false);
|
||||
SpeechRecognitionError error2 = SpeechRecognitionError("msg2", false);
|
||||
expect(error1.hashCode, isNot(error2.hashCode));
|
||||
});
|
||||
test('toString as expected', () {
|
||||
SpeechRecognitionError error1 = SpeechRecognitionError(msg1, false);
|
||||
expect(error1.toString(),
|
||||
"SpeechRecognitionError msg: $msg1, permanent: false");
|
||||
});
|
||||
});
|
||||
group('json', () {
|
||||
test('loads properly', () {
|
||||
var json = jsonDecode('{"errorMsg":"$msg1","permanent":true}');
|
||||
SpeechRecognitionError error = SpeechRecognitionError.fromJson(json);
|
||||
expect(error.errorMsg, msg1);
|
||||
expect(error.permanent, isTrue);
|
||||
json = jsonDecode('{"errorMsg":"$msg1","permanent":false}');
|
||||
error = SpeechRecognitionError.fromJson(json);
|
||||
expect(error.permanent, isFalse);
|
||||
});
|
||||
test('roundtrips properly', () {
|
||||
var json = jsonDecode('{"errorMsg":"$msg1","permanent":true}');
|
||||
SpeechRecognitionError error = SpeechRecognitionError.fromJson(json);
|
||||
var roundtripJson = error.toJson();
|
||||
SpeechRecognitionError roundtripError =
|
||||
SpeechRecognitionError.fromJson(roundtripJson);
|
||||
expect(error, roundtripError);
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -0,0 +1,42 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:speech_to_text/speech_recognition_event.dart';
|
||||
|
||||
import 'test_speech_channel_handler.dart';
|
||||
|
||||
void main() {
|
||||
group('properties', () {
|
||||
test('status listening matches', () {
|
||||
var event = SpeechRecognitionEvent(
|
||||
SpeechRecognitionEventType.statusChangeEvent, null, null, true, null);
|
||||
expect(event.eventType, SpeechRecognitionEventType.statusChangeEvent);
|
||||
expect(event.isListening, isTrue);
|
||||
});
|
||||
test('result matches', () {
|
||||
var event = SpeechRecognitionEvent(
|
||||
SpeechRecognitionEventType.finalRecognitionEvent,
|
||||
TestSpeechChannelHandler.firstRecognizedResult,
|
||||
null,
|
||||
null,
|
||||
null);
|
||||
expect(event.eventType, SpeechRecognitionEventType.finalRecognitionEvent);
|
||||
expect(event.recognitionResult,
|
||||
TestSpeechChannelHandler.firstRecognizedResult);
|
||||
});
|
||||
test('error matches', () {
|
||||
var event = SpeechRecognitionEvent(SpeechRecognitionEventType.errorEvent,
|
||||
null, TestSpeechChannelHandler.firstError, null, null);
|
||||
expect(event.eventType, SpeechRecognitionEventType.errorEvent);
|
||||
expect(event.error, TestSpeechChannelHandler.firstError);
|
||||
});
|
||||
test('sound level matches', () {
|
||||
var event = SpeechRecognitionEvent(
|
||||
SpeechRecognitionEventType.soundLevelChangeEvent,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
TestSpeechChannelHandler.level1);
|
||||
expect(event.eventType, SpeechRecognitionEventType.soundLevelChangeEvent);
|
||||
expect(event.level, TestSpeechChannelHandler.level1);
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -0,0 +1,134 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:speech_to_text/speech_recognition_result.dart';
|
||||
|
||||
void main() {
|
||||
final String firstRecognizedWords = 'hello';
|
||||
final String secondRecognizedWords = 'hello there';
|
||||
final double firstConfidence = 0.85;
|
||||
final double secondConfidence = 0.62;
|
||||
final String firstRecognizedJson =
|
||||
'{"alternates":[{"recognizedWords":"$firstRecognizedWords","confidence":$firstConfidence}],"finalResult":false}';
|
||||
final String secondRecognizedJson =
|
||||
'{"alternates":[{"recognizedWords":"$secondRecognizedWords","confidence":$secondConfidence}],"finalResult":false}';
|
||||
final SpeechRecognitionWords firstWords =
|
||||
SpeechRecognitionWords(firstRecognizedWords, firstConfidence);
|
||||
final SpeechRecognitionWords secondWords =
|
||||
SpeechRecognitionWords(secondRecognizedWords, secondConfidence);
|
||||
|
||||
setUp(() {});
|
||||
|
||||
group('recognizedWords', () {
|
||||
test('empty if no alternates', () {
|
||||
SpeechRecognitionResult result = SpeechRecognitionResult([], true);
|
||||
expect(result.recognizedWords, isEmpty);
|
||||
});
|
||||
test('matches first alternate', () {
|
||||
SpeechRecognitionResult result =
|
||||
SpeechRecognitionResult([firstWords, secondWords], true);
|
||||
expect(result.recognizedWords, firstRecognizedWords);
|
||||
});
|
||||
});
|
||||
group('alternates', () {
|
||||
test('empty if no alternates', () {
|
||||
SpeechRecognitionResult result = SpeechRecognitionResult([], true);
|
||||
expect(result.alternates, isEmpty);
|
||||
});
|
||||
test('expected contents', () {
|
||||
SpeechRecognitionResult result =
|
||||
SpeechRecognitionResult([firstWords, secondWords], true);
|
||||
expect(result.alternates, contains(firstWords));
|
||||
expect(result.alternates, contains(secondWords));
|
||||
});
|
||||
test('in order', () {
|
||||
SpeechRecognitionResult result =
|
||||
SpeechRecognitionResult([firstWords, secondWords], true);
|
||||
expect(result.alternates.first, firstWords);
|
||||
});
|
||||
});
|
||||
group('confidence', () {
|
||||
test('0 if no alternates', () {
|
||||
SpeechRecognitionResult result = SpeechRecognitionResult([], true);
|
||||
expect(result.confidence, 0);
|
||||
});
|
||||
test('isConfident false if no alternates', () {
|
||||
SpeechRecognitionResult result = SpeechRecognitionResult([], true);
|
||||
expect(result.isConfident(), isFalse);
|
||||
});
|
||||
test('isConfident matches first alternate', () {
|
||||
SpeechRecognitionResult result =
|
||||
SpeechRecognitionResult([firstWords, secondWords], true);
|
||||
expect(result.isConfident(), firstWords.isConfident());
|
||||
});
|
||||
test('hasConfidenceRating false if no alternates', () {
|
||||
SpeechRecognitionResult result = SpeechRecognitionResult([], true);
|
||||
expect(result.hasConfidenceRating, isFalse);
|
||||
});
|
||||
test('hasConfidenceRating matches first alternate', () {
|
||||
SpeechRecognitionResult result =
|
||||
SpeechRecognitionResult([firstWords, secondWords], true);
|
||||
expect(result.hasConfidenceRating, firstWords.hasConfidenceRating);
|
||||
});
|
||||
});
|
||||
group('json', () {
|
||||
test('loads correctly', () {
|
||||
var json = jsonDecode(firstRecognizedJson);
|
||||
SpeechRecognitionResult result = SpeechRecognitionResult.fromJson(json);
|
||||
expect(result.recognizedWords, firstRecognizedWords);
|
||||
expect(result.confidence, firstConfidence);
|
||||
});
|
||||
test('roundtrips correctly', () {
|
||||
var json = jsonDecode(firstRecognizedJson);
|
||||
SpeechRecognitionResult result = SpeechRecognitionResult.fromJson(json);
|
||||
var roundTripJson = result.toJson();
|
||||
SpeechRecognitionResult roundtripResult =
|
||||
SpeechRecognitionResult.fromJson(roundTripJson);
|
||||
expect(result, roundtripResult);
|
||||
});
|
||||
});
|
||||
group('overrides', () {
|
||||
test('toString works with no alternates', () {
|
||||
SpeechRecognitionResult result = SpeechRecognitionResult([], true);
|
||||
expect(
|
||||
result.toString(), "SpeechRecognitionResult words: [], final: true");
|
||||
});
|
||||
test('toString works with alternates', () {
|
||||
SpeechRecognitionResult result =
|
||||
SpeechRecognitionResult([firstWords], true);
|
||||
expect(result.toString(),
|
||||
"SpeechRecognitionResult words: [SpeechRecognitionWords words: hello, confidence: 0.85], final: true");
|
||||
});
|
||||
test('hash same for same object', () {
|
||||
SpeechRecognitionResult result =
|
||||
SpeechRecognitionResult([firstWords], true);
|
||||
expect(result.hashCode, result.hashCode);
|
||||
});
|
||||
test('hash differs for different objects', () {
|
||||
SpeechRecognitionResult result1 =
|
||||
SpeechRecognitionResult([firstWords], true);
|
||||
SpeechRecognitionResult result2 =
|
||||
SpeechRecognitionResult([secondWords], true);
|
||||
expect(result1.hashCode, isNot(result2.hashCode));
|
||||
});
|
||||
test('equals same for same object', () {
|
||||
SpeechRecognitionResult result =
|
||||
SpeechRecognitionResult([firstWords], true);
|
||||
expect(result, result);
|
||||
});
|
||||
test('equals same for different object same values', () {
|
||||
SpeechRecognitionResult result1 =
|
||||
SpeechRecognitionResult([firstWords], true);
|
||||
SpeechRecognitionResult result1a =
|
||||
SpeechRecognitionResult([firstWords], true);
|
||||
expect(result1, result1a);
|
||||
});
|
||||
test('equals differs for different objects', () {
|
||||
SpeechRecognitionResult result1 =
|
||||
SpeechRecognitionResult([firstWords], true);
|
||||
SpeechRecognitionResult result2 =
|
||||
SpeechRecognitionResult([secondWords], true);
|
||||
expect(result1, isNot(result2));
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -0,0 +1,86 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:speech_to_text/speech_recognition_result.dart';
|
||||
|
||||
void main() {
|
||||
final String firstRecognizedWords = 'hello';
|
||||
final String secondRecognizedWords = 'hello there';
|
||||
final double firstConfidence = 0.85;
|
||||
final double secondConfidence = 0.62;
|
||||
final String firstRecognizedJson =
|
||||
'{"recognizedWords":"$firstRecognizedWords","confidence":$firstConfidence}';
|
||||
final SpeechRecognitionWords firstWords =
|
||||
SpeechRecognitionWords(firstRecognizedWords, firstConfidence);
|
||||
final SpeechRecognitionWords secondWords =
|
||||
SpeechRecognitionWords(secondRecognizedWords, secondConfidence);
|
||||
|
||||
setUp(() {});
|
||||
|
||||
group('properties', () {
|
||||
test('words', () {
|
||||
expect(firstWords.recognizedWords, firstRecognizedWords);
|
||||
expect(secondWords.recognizedWords, secondRecognizedWords);
|
||||
});
|
||||
test('confidence', () {
|
||||
expect(firstWords.confidence, firstConfidence);
|
||||
expect(secondWords.confidence, secondConfidence);
|
||||
expect(firstWords.hasConfidenceRating, isTrue);
|
||||
});
|
||||
test('equals true for same object', () {
|
||||
expect(firstWords, firstWords);
|
||||
});
|
||||
test('equals true for different object with same values', () {
|
||||
SpeechRecognitionWords firstWordsA =
|
||||
SpeechRecognitionWords(firstRecognizedWords, firstConfidence);
|
||||
expect(firstWords, firstWordsA);
|
||||
});
|
||||
test('equals false for different results', () {
|
||||
expect(firstWords, isNot(secondWords));
|
||||
});
|
||||
test('hash same for same object', () {
|
||||
expect(firstWords.hashCode, firstWords.hashCode);
|
||||
});
|
||||
test('hash same for different object with same values', () {
|
||||
SpeechRecognitionWords firstWordsA =
|
||||
SpeechRecognitionWords(firstRecognizedWords, firstConfidence);
|
||||
expect(firstWords.hashCode, firstWordsA.hashCode);
|
||||
});
|
||||
test('hash different for different results', () {
|
||||
expect(firstWords.hashCode, isNot(secondWords.hashCode));
|
||||
});
|
||||
});
|
||||
group('isConfident', () {
|
||||
test('true when >= 0.8', () {
|
||||
expect(firstWords.isConfident(), isTrue);
|
||||
});
|
||||
test('false when < 0.8', () {
|
||||
expect(secondWords.isConfident(), isFalse);
|
||||
});
|
||||
test('respects threshold', () {
|
||||
expect(secondWords.isConfident(threshold: 0.5), isTrue);
|
||||
});
|
||||
test('true when missing', () {
|
||||
SpeechRecognitionWords words = SpeechRecognitionWords(
|
||||
firstRecognizedWords, SpeechRecognitionWords.missingConfidence);
|
||||
expect(words.isConfident(), isTrue);
|
||||
expect(words.hasConfidenceRating, isFalse);
|
||||
});
|
||||
});
|
||||
group('json', () {
|
||||
test('loads correctly', () {
|
||||
var json = jsonDecode(firstRecognizedJson);
|
||||
SpeechRecognitionWords words = SpeechRecognitionWords.fromJson(json);
|
||||
expect(words.recognizedWords, firstRecognizedWords);
|
||||
expect(words.confidence, firstConfidence);
|
||||
});
|
||||
test('roundtrips correctly', () {
|
||||
var json = jsonDecode(firstRecognizedJson);
|
||||
SpeechRecognitionWords words = SpeechRecognitionWords.fromJson(json);
|
||||
var roundTripJson = words.toJson();
|
||||
SpeechRecognitionWords roundtripWords =
|
||||
SpeechRecognitionWords.fromJson(roundTripJson);
|
||||
expect(words, roundtripWords);
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -0,0 +1,196 @@
|
||||
import 'package:fake_async/fake_async.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:speech_to_text/speech_to_text.dart';
|
||||
import 'package:speech_to_text/speech_to_text_provider.dart';
|
||||
|
||||
import 'test_speech_channel_handler.dart';
|
||||
import 'test_speech_listener.dart';
|
||||
|
||||
void main() {
|
||||
SpeechToTextProvider provider;
|
||||
SpeechToText speechToText;
|
||||
TestSpeechChannelHandler speechHandler;
|
||||
TestSpeechListener speechListener;
|
||||
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
setUp(() {
|
||||
speechToText = SpeechToText.withMethodChannel(SpeechToText.speechChannel);
|
||||
speechHandler = TestSpeechChannelHandler(speechToText);
|
||||
speechToText.channel
|
||||
.setMockMethodCallHandler(speechHandler.methodCallHandler);
|
||||
provider = SpeechToTextProvider(speechToText);
|
||||
speechListener = TestSpeechListener(provider);
|
||||
provider.addListener(speechListener.onNotify);
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
speechToText.channel.setMockMethodCallHandler(null);
|
||||
});
|
||||
|
||||
group('delegates', () {
|
||||
test('isListening matches delegate defaults', () {
|
||||
expect(provider.isListening, speechToText.isListening);
|
||||
expect(provider.isNotListening, speechToText.isNotListening);
|
||||
});
|
||||
test('isAvailable matches delegate defaults', () {
|
||||
expect(provider.isAvailable, speechToText.isAvailable);
|
||||
expect(provider.isNotAvailable, !speechToText.isAvailable);
|
||||
});
|
||||
test('isAvailable matches delegate after init', () async {
|
||||
expect(await provider.initialize(), isTrue);
|
||||
expect(provider.isAvailable, speechToText.isAvailable);
|
||||
expect(provider.isNotAvailable, !speechToText.isAvailable);
|
||||
});
|
||||
test('hasError matches delegate after error', () async {
|
||||
expect(await provider.initialize(), isTrue);
|
||||
expect(provider.hasError, speechToText.hasError);
|
||||
});
|
||||
});
|
||||
group('listening', () {
|
||||
test('notifies on initialize', () async {
|
||||
fakeAsync((fa) {
|
||||
provider.initialize();
|
||||
fa.flushMicrotasks();
|
||||
expect(speechListener.notified, isTrue);
|
||||
expect(speechListener.isAvailable, isTrue);
|
||||
});
|
||||
});
|
||||
test('notifies on listening', () async {
|
||||
fakeAsync((fa) {
|
||||
setupForListen(provider, fa, speechListener);
|
||||
expect(speechListener.notified, isTrue);
|
||||
expect(speechListener.isListening, isTrue);
|
||||
expect(provider.hasResults, isFalse);
|
||||
});
|
||||
});
|
||||
test('notifies on final words', () async {
|
||||
fakeAsync((fa) {
|
||||
setupForListen(provider, fa, speechListener);
|
||||
speechListener.reset();
|
||||
speechHandler.notifyFinalWords();
|
||||
fa.flushMicrotasks();
|
||||
expect(speechListener.notified, isTrue);
|
||||
expect(provider.hasResults, isTrue);
|
||||
var result = speechListener.recognitionResult;
|
||||
expect(result.recognizedWords,
|
||||
TestSpeechChannelHandler.secondRecognizedWords);
|
||||
expect(result.finalResult, isTrue);
|
||||
});
|
||||
});
|
||||
test('hasResult false after listening before new results', () async {
|
||||
fakeAsync((fa) {
|
||||
setupForListen(provider, fa, speechListener);
|
||||
speechHandler.notifyFinalWords();
|
||||
provider.stop();
|
||||
setupForListen(provider, fa, speechListener);
|
||||
fa.flushMicrotasks();
|
||||
expect(provider.hasResults, isFalse);
|
||||
});
|
||||
});
|
||||
test('notifies on partial words', () async {
|
||||
fakeAsync((fa) {
|
||||
setupForListen(provider, fa, speechListener, partialResults: true);
|
||||
speechListener.reset();
|
||||
speechHandler.notifyPartialWords();
|
||||
fa.flushMicrotasks();
|
||||
expect(speechListener.notified, isTrue);
|
||||
expect(provider.hasResults, isTrue);
|
||||
var result = speechListener.recognitionResult;
|
||||
expect(result.recognizedWords,
|
||||
TestSpeechChannelHandler.firstRecognizedWords);
|
||||
expect(result.finalResult, isFalse);
|
||||
});
|
||||
});
|
||||
});
|
||||
group('soundLevel', () {
|
||||
test('notifies when requested', () async {
|
||||
fakeAsync((fa) {
|
||||
setupForListen(provider, fa, speechListener,
|
||||
partialResults: true, soundLevel: true);
|
||||
speechListener.reset();
|
||||
speechHandler.notifySoundLevel();
|
||||
fa.flushMicrotasks();
|
||||
expect(speechListener.notified, isTrue);
|
||||
expect(speechListener.soundLevel, TestSpeechChannelHandler.level2);
|
||||
});
|
||||
});
|
||||
test('no notification by default', () async {
|
||||
fakeAsync((fa) {
|
||||
setupForListen(provider, fa, speechListener,
|
||||
partialResults: true, soundLevel: false);
|
||||
speechListener.reset();
|
||||
speechHandler.notifySoundLevel();
|
||||
fa.flushMicrotasks();
|
||||
expect(speechListener.notified, isFalse);
|
||||
expect(speechListener.soundLevel, 0);
|
||||
});
|
||||
});
|
||||
});
|
||||
group('stop/cancel', () {
|
||||
test('notifies on stop', () async {
|
||||
fakeAsync((fa) {
|
||||
provider.initialize();
|
||||
setupForListen(provider, fa, speechListener);
|
||||
speechListener.reset();
|
||||
provider.stop();
|
||||
fa.flushMicrotasks();
|
||||
expect(speechListener.notified, isTrue);
|
||||
expect(speechListener.isListening, isFalse);
|
||||
});
|
||||
});
|
||||
test('notifies on cancel', () async {
|
||||
fakeAsync((fa) {
|
||||
provider.initialize();
|
||||
setupForListen(provider, fa, speechListener);
|
||||
speechListener.reset();
|
||||
provider.cancel();
|
||||
fa.flushMicrotasks();
|
||||
expect(speechListener.notified, isTrue);
|
||||
expect(speechListener.isListening, isFalse);
|
||||
});
|
||||
});
|
||||
});
|
||||
group('error handling', () {
|
||||
test('hasError matches delegate default', () async {
|
||||
expect(await provider.initialize(), isTrue);
|
||||
expect(provider.hasError, speechToText.hasError);
|
||||
});
|
||||
test('notifies on error', () async {
|
||||
fakeAsync((fa) {
|
||||
provider.initialize();
|
||||
setupForListen(provider, fa, speechListener);
|
||||
speechListener.reset();
|
||||
speechHandler.notifyPermanentError();
|
||||
expect(speechListener.notified, isTrue);
|
||||
expect(speechListener.hasError, isTrue);
|
||||
});
|
||||
});
|
||||
});
|
||||
group('locale', () {
|
||||
test('locales empty before init', () async {
|
||||
expect(provider.systemLocale, isNull);
|
||||
expect(provider.locales, isEmpty);
|
||||
});
|
||||
test('set from SpeechToText after init', () async {
|
||||
fakeAsync((fa) {
|
||||
speechHandler.setupLocales();
|
||||
provider.initialize();
|
||||
fa.flushMicrotasks();
|
||||
expect(
|
||||
provider.systemLocale.localeId, TestSpeechChannelHandler.localeId1);
|
||||
expect(provider.locales, hasLength(speechHandler.locales.length));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void setupForListen(SpeechToTextProvider provider, FakeAsync fa,
|
||||
TestSpeechListener speechListener,
|
||||
{bool partialResults = false, bool soundLevel = false}) {
|
||||
provider.initialize();
|
||||
fa.flushMicrotasks();
|
||||
speechListener.reset();
|
||||
provider.listen(partialResults: partialResults, soundLevel: soundLevel);
|
||||
fa.flushMicrotasks();
|
||||
}
|
||||
@ -0,0 +1,425 @@
|
||||
import 'package:fake_async/fake_async.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:speech_to_text/speech_recognition_error.dart';
|
||||
import 'package:speech_to_text/speech_recognition_result.dart';
|
||||
import 'package:speech_to_text/speech_to_text.dart';
|
||||
|
||||
import 'test_speech_channel_handler.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
TestSpeechListener listener;
|
||||
TestSpeechChannelHandler speechHandler;
|
||||
SpeechToText speech;
|
||||
|
||||
setUp(() {
|
||||
listener = TestSpeechListener();
|
||||
speech = SpeechToText.withMethodChannel(SpeechToText.speechChannel);
|
||||
speechHandler = TestSpeechChannelHandler(speech);
|
||||
speech.channel.setMockMethodCallHandler(speechHandler.methodCallHandler);
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
speech.channel.setMockMethodCallHandler(null);
|
||||
});
|
||||
|
||||
group('hasPermission', () {
|
||||
test('true if platform reports true', () async {
|
||||
expect(await speech.hasPermission, true);
|
||||
});
|
||||
test('false if platform reports false', () async {
|
||||
speechHandler.hasPermissionResult = false;
|
||||
expect(await speech.hasPermission, false);
|
||||
});
|
||||
});
|
||||
group('init', () {
|
||||
test('succeeds on platform success', () async {
|
||||
expect(await speech.initialize(), true);
|
||||
expect(speechHandler.initInvoked, true);
|
||||
expect(speech.isAvailable, true);
|
||||
});
|
||||
test('only invokes once', () async {
|
||||
expect(await speech.initialize(), true);
|
||||
speechHandler.initInvoked = false;
|
||||
expect(await speech.initialize(), true);
|
||||
expect(speechHandler.initInvoked, false);
|
||||
});
|
||||
test('fails on platform failure', () async {
|
||||
speechHandler.initResult = false;
|
||||
expect(await speech.initialize(), false);
|
||||
expect(speech.isAvailable, false);
|
||||
});
|
||||
});
|
||||
|
||||
group('listen', () {
|
||||
test('fails with exception if not initialized', () async {
|
||||
try {
|
||||
await speech.listen();
|
||||
fail("Expected an exception.");
|
||||
} on SpeechToTextNotInitializedException {
|
||||
// This is a good result
|
||||
}
|
||||
});
|
||||
test('fails with exception if init fails', () async {
|
||||
try {
|
||||
speechHandler.initResult = false;
|
||||
await speech.initialize();
|
||||
await speech.listen();
|
||||
fail("Expected an exception.");
|
||||
} on SpeechToTextNotInitializedException {
|
||||
// This is a good result
|
||||
}
|
||||
});
|
||||
test('invokes listen after successful init', () async {
|
||||
await speech.initialize();
|
||||
await speech.listen();
|
||||
expect(speechHandler.listenLocale, isNull);
|
||||
expect(speechHandler.listenInvoked, true);
|
||||
});
|
||||
test('converts platformException to listenFailed', () async {
|
||||
await speech.initialize();
|
||||
speechHandler.listenException = true;
|
||||
try {
|
||||
await speech.listen();
|
||||
fail("Should have thrown");
|
||||
} on ListenFailedException catch (e) {
|
||||
expect(e.details, TestSpeechChannelHandler.listenExceptionDetails);
|
||||
} catch (wrongE) {
|
||||
fail("Should have been ListenFailedException");
|
||||
}
|
||||
});
|
||||
test('stops listen after listenFor duration', () async {
|
||||
fakeAsync((fa) {
|
||||
speech.initialize();
|
||||
fa.flushMicrotasks();
|
||||
speech.listen(listenFor: Duration(seconds: 2));
|
||||
fa.flushMicrotasks();
|
||||
expect(speech.isListening, isTrue);
|
||||
fa.elapse(Duration(seconds: 2));
|
||||
expect(speech.isListening, isFalse);
|
||||
});
|
||||
});
|
||||
test('stops listen after listenFor duration even with speech event',
|
||||
() async {
|
||||
fakeAsync((fa) {
|
||||
speech.initialize();
|
||||
fa.flushMicrotasks();
|
||||
speech.listen(listenFor: Duration(seconds: 1));
|
||||
speech.processMethodCall(MethodCall(SpeechToText.textRecognitionMethod,
|
||||
TestSpeechChannelHandler.firstRecognizedJson));
|
||||
fa.flushMicrotasks();
|
||||
expect(speech.isListening, isTrue);
|
||||
fa.elapse(Duration(seconds: 1));
|
||||
expect(speech.isListening, isFalse);
|
||||
});
|
||||
});
|
||||
test('stops listen after pauseFor duration with no speech', () async {
|
||||
fakeAsync((fa) {
|
||||
speech.initialize();
|
||||
fa.flushMicrotasks();
|
||||
speech.listen(pauseFor: Duration(seconds: 2));
|
||||
fa.flushMicrotasks();
|
||||
expect(speech.isListening, isTrue);
|
||||
fa.elapse(Duration(seconds: 2));
|
||||
expect(speech.isListening, isFalse);
|
||||
});
|
||||
});
|
||||
test('stops listen after pauseFor with longer listenFor duration',
|
||||
() async {
|
||||
fakeAsync((fa) {
|
||||
speech.initialize();
|
||||
fa.flushMicrotasks();
|
||||
speech.listen(
|
||||
pauseFor: Duration(seconds: 1), listenFor: Duration(seconds: 5));
|
||||
fa.flushMicrotasks();
|
||||
expect(speech.isListening, isTrue);
|
||||
fa.elapse(Duration(seconds: 1));
|
||||
expect(speech.isListening, isFalse);
|
||||
});
|
||||
});
|
||||
test('stops listen after listenFor with longer pauseFor duration',
|
||||
() async {
|
||||
fakeAsync((fa) {
|
||||
speech.initialize();
|
||||
fa.flushMicrotasks();
|
||||
speech.listen(
|
||||
listenFor: Duration(seconds: 1), pauseFor: Duration(seconds: 5));
|
||||
fa.flushMicrotasks();
|
||||
expect(speech.isListening, isTrue);
|
||||
fa.elapse(Duration(seconds: 1));
|
||||
expect(speech.isListening, isFalse);
|
||||
});
|
||||
});
|
||||
test('keeps listening after pauseFor with speech event', () async {
|
||||
fakeAsync((fa) {
|
||||
speech.initialize();
|
||||
fa.flushMicrotasks();
|
||||
speech.listen(pauseFor: Duration(seconds: 2));
|
||||
fa.flushMicrotasks();
|
||||
fa.elapse(Duration(seconds: 1));
|
||||
speech.processMethodCall(MethodCall(SpeechToText.textRecognitionMethod,
|
||||
TestSpeechChannelHandler.firstRecognizedJson));
|
||||
fa.flushMicrotasks();
|
||||
fa.elapse(Duration(seconds: 1));
|
||||
expect(speech.isListening, isTrue);
|
||||
});
|
||||
});
|
||||
test('uses localeId if provided', () async {
|
||||
await speech.initialize();
|
||||
await speech.listen(localeId: TestSpeechChannelHandler.localeId1);
|
||||
expect(speechHandler.listenInvoked, true);
|
||||
expect(speechHandler.listenLocale, TestSpeechChannelHandler.localeId1);
|
||||
});
|
||||
test('calls speech listener', () async {
|
||||
await speech.initialize();
|
||||
await speech.listen(onResult: listener.onSpeechResult);
|
||||
await speech.processMethodCall(MethodCall(
|
||||
SpeechToText.textRecognitionMethod,
|
||||
TestSpeechChannelHandler.firstRecognizedJson));
|
||||
expect(listener.speechResults, 1);
|
||||
expect(
|
||||
listener.results, [TestSpeechChannelHandler.firstRecognizedResult]);
|
||||
expect(speech.lastRecognizedWords,
|
||||
TestSpeechChannelHandler.firstRecognizedWords);
|
||||
});
|
||||
test('calls speech listener with multiple', () async {
|
||||
await speech.initialize();
|
||||
await speech.listen(onResult: listener.onSpeechResult);
|
||||
await speech.processMethodCall(MethodCall(
|
||||
SpeechToText.textRecognitionMethod,
|
||||
TestSpeechChannelHandler.firstRecognizedJson));
|
||||
await speech.processMethodCall(MethodCall(
|
||||
SpeechToText.textRecognitionMethod,
|
||||
TestSpeechChannelHandler.secondRecognizedJson));
|
||||
expect(listener.speechResults, 2);
|
||||
expect(listener.results, [
|
||||
TestSpeechChannelHandler.firstRecognizedResult,
|
||||
TestSpeechChannelHandler.secondRecognizedResult
|
||||
]);
|
||||
expect(speech.lastRecognizedWords,
|
||||
TestSpeechChannelHandler.secondRecognizedWords);
|
||||
});
|
||||
});
|
||||
|
||||
group('status callback', () {
|
||||
test('invoked on listen', () async {
|
||||
await speech.initialize(
|
||||
onError: listener.onSpeechError, onStatus: listener.onSpeechStatus);
|
||||
await speech.processMethodCall(MethodCall(
|
||||
SpeechToText.notifyStatusMethod, SpeechToText.listeningStatus));
|
||||
expect(listener.speechStatus, 1);
|
||||
expect(listener.statuses.contains(SpeechToText.listeningStatus), true);
|
||||
});
|
||||
});
|
||||
|
||||
group('soundLevel callback', () {
|
||||
test('invoked on listen', () async {
|
||||
await speech.initialize();
|
||||
await speech.listen(onSoundLevelChange: listener.onSoundLevel);
|
||||
await speech.processMethodCall(MethodCall(
|
||||
SpeechToText.soundLevelChangeMethod,
|
||||
TestSpeechChannelHandler.level1));
|
||||
expect(listener.soundLevel, 1);
|
||||
expect(listener.soundLevels, contains(TestSpeechChannelHandler.level1));
|
||||
});
|
||||
test('sets lastLevel', () async {
|
||||
await speech.initialize();
|
||||
await speech.listen(onSoundLevelChange: listener.onSoundLevel);
|
||||
await speech.processMethodCall(MethodCall(
|
||||
SpeechToText.soundLevelChangeMethod,
|
||||
TestSpeechChannelHandler.level1));
|
||||
expect(speech.lastSoundLevel, TestSpeechChannelHandler.level1);
|
||||
});
|
||||
});
|
||||
|
||||
group('cancel', () {
|
||||
test('does nothing if not initialized', () async {
|
||||
speech.cancel();
|
||||
expect(speechHandler.cancelInvoked, false);
|
||||
});
|
||||
test('cancels an active listen', () async {
|
||||
await speech.initialize();
|
||||
await speech.listen();
|
||||
await speech.cancel();
|
||||
expect(speechHandler.cancelInvoked, true);
|
||||
expect(speech.isListening, isFalse);
|
||||
});
|
||||
});
|
||||
group('stop', () {
|
||||
test('does nothing if not initialized', () async {
|
||||
speech.stop();
|
||||
expect(speechHandler.cancelInvoked, false);
|
||||
});
|
||||
test('stops an active listen', () async {
|
||||
await speech.initialize();
|
||||
speech.listen();
|
||||
speech.stop();
|
||||
expect(speechHandler.stopInvoked, true);
|
||||
});
|
||||
});
|
||||
group('error', () {
|
||||
test('notifies handler with transient', () async {
|
||||
await speech.initialize(onError: listener.onSpeechError);
|
||||
await speech.listen();
|
||||
await speech.processMethodCall(MethodCall(SpeechToText.notifyErrorMethod,
|
||||
TestSpeechChannelHandler.transientErrorJson));
|
||||
expect(listener.speechErrors, 1);
|
||||
expect(listener.errors.first.permanent, isFalse);
|
||||
});
|
||||
test('notifies handler with permanent', () async {
|
||||
await speech.initialize(onError: listener.onSpeechError);
|
||||
await speech.listen();
|
||||
await speech.processMethodCall(MethodCall(SpeechToText.notifyErrorMethod,
|
||||
TestSpeechChannelHandler.permanentErrorJson));
|
||||
expect(listener.speechErrors, 1);
|
||||
expect(listener.errors.first.permanent, isTrue);
|
||||
});
|
||||
test('continues listening on transient', () async {
|
||||
await speech.initialize(onError: listener.onSpeechError);
|
||||
await speech.listen();
|
||||
await speech.processMethodCall(MethodCall(SpeechToText.notifyErrorMethod,
|
||||
TestSpeechChannelHandler.transientErrorJson));
|
||||
expect(speech.isListening, isTrue);
|
||||
});
|
||||
test('continues listening on permanent if cancel not explicitly requested',
|
||||
() async {
|
||||
await speech.initialize(onError: listener.onSpeechError);
|
||||
await speech.listen();
|
||||
await speech.processMethodCall(MethodCall(SpeechToText.notifyErrorMethod,
|
||||
TestSpeechChannelHandler.permanentErrorJson));
|
||||
expect(speech.isListening, isTrue);
|
||||
});
|
||||
test('stops listening on permanent if cancel explicitly requested',
|
||||
() async {
|
||||
await speech.initialize(onError: listener.onSpeechError);
|
||||
await speech.listen(cancelOnError: true);
|
||||
await speech.processMethodCall(MethodCall(SpeechToText.notifyErrorMethod,
|
||||
TestSpeechChannelHandler.permanentErrorJson));
|
||||
expect(speech.isListening, isFalse);
|
||||
});
|
||||
test('Error not sent after cancel', () async {
|
||||
await speech.initialize(onError: listener.onSpeechError);
|
||||
await speech.listen();
|
||||
await speech.cancel();
|
||||
await speech.processMethodCall(MethodCall(SpeechToText.notifyErrorMethod,
|
||||
TestSpeechChannelHandler.permanentErrorJson));
|
||||
expect(speech.isListening, isFalse);
|
||||
expect(listener.speechErrors, 0);
|
||||
});
|
||||
test('Error still sent after implicit cancel', () async {
|
||||
await speech.initialize(onError: listener.onSpeechError);
|
||||
await speech.listen(cancelOnError: true);
|
||||
await speech.processMethodCall(MethodCall(SpeechToText.notifyErrorMethod,
|
||||
TestSpeechChannelHandler.permanentErrorJson));
|
||||
await speech.processMethodCall(MethodCall(SpeechToText.notifyErrorMethod,
|
||||
TestSpeechChannelHandler.permanentErrorJson));
|
||||
expect(speech.isListening, isFalse);
|
||||
expect(listener.speechErrors, 2);
|
||||
});
|
||||
});
|
||||
group('locales', () {
|
||||
test('fails with exception if not initialized', () async {
|
||||
try {
|
||||
await speech.locales();
|
||||
fail("Expected an exception.");
|
||||
} on SpeechToTextNotInitializedException {
|
||||
// This is a good result
|
||||
}
|
||||
});
|
||||
test('system locale null if not initialized', () async {
|
||||
LocaleName current;
|
||||
try {
|
||||
current = await speech.systemLocale();
|
||||
fail("Expected an exception.");
|
||||
} on SpeechToTextNotInitializedException {
|
||||
expect(current, isNull);
|
||||
}
|
||||
});
|
||||
test('handles an empty list', () async {
|
||||
await speech.initialize(onError: listener.onSpeechError);
|
||||
List<LocaleName> localeNames = await speech.locales();
|
||||
expect(speechHandler.localesInvoked, isTrue);
|
||||
expect(localeNames, isEmpty);
|
||||
});
|
||||
test('returns expected locales', () async {
|
||||
await speech.initialize(onError: listener.onSpeechError);
|
||||
speechHandler.locales.add(TestSpeechChannelHandler.locale1);
|
||||
speechHandler.locales.add(TestSpeechChannelHandler.locale2);
|
||||
List<LocaleName> localeNames = await speech.locales();
|
||||
expect(localeNames, hasLength(speechHandler.locales.length));
|
||||
expect(localeNames[0].localeId, TestSpeechChannelHandler.localeId1);
|
||||
expect(localeNames[0].name, TestSpeechChannelHandler.name1);
|
||||
expect(localeNames[1].localeId, TestSpeechChannelHandler.localeId2);
|
||||
expect(localeNames[1].name, TestSpeechChannelHandler.name2);
|
||||
});
|
||||
test('skips incorrect locales', () async {
|
||||
await speech.initialize(onError: listener.onSpeechError);
|
||||
speechHandler.locales.add("InvalidJunk");
|
||||
speechHandler.locales.add(TestSpeechChannelHandler.locale1);
|
||||
List<LocaleName> localeNames = await speech.locales();
|
||||
expect(localeNames, hasLength(1));
|
||||
expect(localeNames[0].localeId, TestSpeechChannelHandler.localeId1);
|
||||
expect(localeNames[0].name, TestSpeechChannelHandler.name1);
|
||||
});
|
||||
test('system locale matches first returned locale', () async {
|
||||
await speech.initialize(onError: listener.onSpeechError);
|
||||
speechHandler.locales.add(TestSpeechChannelHandler.locale1);
|
||||
speechHandler.locales.add(TestSpeechChannelHandler.locale2);
|
||||
LocaleName current = await speech.systemLocale();
|
||||
expect(current.localeId, TestSpeechChannelHandler.localeId1);
|
||||
});
|
||||
});
|
||||
group('status', () {
|
||||
test('recognized false at start', () async {
|
||||
expect(speech.hasRecognized, isFalse);
|
||||
});
|
||||
test('listening false at start', () async {
|
||||
expect(speech.isListening, isFalse);
|
||||
});
|
||||
});
|
||||
test('available false at start', () async {
|
||||
expect(speech.isAvailable, isFalse);
|
||||
});
|
||||
test('hasError false at start', () async {
|
||||
expect(speech.hasError, isFalse);
|
||||
});
|
||||
test('lastError null at start', () async {
|
||||
expect(speech.lastError, isNull);
|
||||
});
|
||||
test('status empty at start', () async {
|
||||
expect(speech.lastStatus, isEmpty);
|
||||
});
|
||||
}
|
||||
|
||||
class TestSpeechListener {
|
||||
int speechResults = 0;
|
||||
List<SpeechRecognitionResult> results = [];
|
||||
int speechErrors = 0;
|
||||
List<SpeechRecognitionError> errors = [];
|
||||
int speechStatus = 0;
|
||||
List<String> statuses = [];
|
||||
int soundLevel = 0;
|
||||
List<double> soundLevels = [];
|
||||
|
||||
void onSpeechResult(SpeechRecognitionResult result) {
|
||||
++speechResults;
|
||||
results.add(result);
|
||||
}
|
||||
|
||||
void onSpeechError(SpeechRecognitionError errorResult) {
|
||||
++speechErrors;
|
||||
errors.add(errorResult);
|
||||
}
|
||||
|
||||
void onSpeechStatus(String status) {
|
||||
++speechStatus;
|
||||
statuses.add(status);
|
||||
}
|
||||
|
||||
void onSoundLevel(double level) {
|
||||
++soundLevel;
|
||||
soundLevels.add(level);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue