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